From bccd20d9786e2ec5f1445e1b20fb806ddfff78b1 Mon Sep 17 00:00:00 2001 From: Wei Kang Date: Tue, 16 May 2023 12:44:52 +0800 Subject: [PATCH 001/100] Traning with byte level BPE (TAL_CSASR) (#1033) * Add byte level bpe tal_csasr recipe * Minor fixes to decoding and exporting * Fix prepare.sh * Update results --- egs/tal_csasr/ASR/RESULTS.md | 46 + egs/tal_csasr/ASR/local/prepare_char.py | 3 +- egs/tal_csasr/ASR/local/train_bbpe_model.py | 1 + egs/tal_csasr/ASR/prepare.sh | 82 +- .../__init__.py | 0 .../asr_datamodule.py | 1 + .../beam_search.py | 1 + .../decode.py | 815 +++++++++++ .../decoder.py | 1 + .../encoder_interface.py | 1 + .../export.py | 320 +++++ .../jit_pretrained.py | 273 ++++ .../joiner.py | 1 + .../model.py | 1 + .../optim.py | 1 + .../pretrained.py | 355 +++++ .../scaling.py | 1 + .../scaling_converter.py | 1 + .../test_model.py | 1 + .../train.py | 1260 +++++++++++++++++ .../zipformer.py | 1 + icefall/byte_utils.py | 2 + 22 files changed, 3135 insertions(+), 33 deletions(-) create mode 120000 egs/tal_csasr/ASR/local/train_bbpe_model.py create mode 100644 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/__init__.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/asr_datamodule.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/beam_search.py create mode 100755 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decode.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decoder.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/encoder_interface.py create mode 100755 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/export.py create mode 100755 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/jit_pretrained.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/joiner.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/model.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/optim.py create mode 100755 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/pretrained.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling_converter.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/test_model.py create mode 100755 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py create mode 120000 egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/zipformer.py diff --git a/egs/tal_csasr/ASR/RESULTS.md b/egs/tal_csasr/ASR/RESULTS.md index ddff0ab61..e696279bd 100644 --- a/egs/tal_csasr/ASR/RESULTS.md +++ b/egs/tal_csasr/ASR/RESULTS.md @@ -1,5 +1,51 @@ ## Results +#### Pruned transducer stateless 7 (zipformer) + +See + +[./pruned_transducer_stateless7_bbpe](./pruned_transducer_stateless7_bbpe) + +**Note**: The modeling units are byte level BPEs + +The best results I have gotten are: + +Vocab size | greedy (dev & test) | modified beam search (dev & test) | | +-- | -- | -- | -- +500 | 6.88 & 6.98 | 6.87 & 6.94 | --epoch 35 --avg 26 + +The training command: + +``` +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +./pruned_transducer_stateless7_bbpe/train.py \ + --world-size 4 \ + --start-epoch 1 \ + --num-epochs 35 \ + --use-fp16 1 \ + --max-duration 800 \ + --bbpe-model data/lang_bbpe_500/bbpe.model \ + --exp-dir pruned_transducer_stateless7_bbpe/exp \ + --master-port 12535 +``` + +The decoding command: + +``` + ./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 35 \ + --avg 26 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-sym-per-frame 1 \ + --bpe-model data/lang_bbpe_500/bbpe.model \ + --max-duration 2000 \ + --decoding-method greedy_search # modified_beam_search +``` + +The pretrained model is available at: https://huggingface.co/pkufool/icefall_asr_tal_csasr_pruned_transducer_stateless7_bbpe + + ### TAL_CSASR Mix Chars and BPEs training results (Pruned Transducer Stateless5) #### 2022-06-22 diff --git a/egs/tal_csasr/ASR/local/prepare_char.py b/egs/tal_csasr/ASR/local/prepare_char.py index 1262baf63..499937462 100755 --- a/egs/tal_csasr/ASR/local/prepare_char.py +++ b/egs/tal_csasr/ASR/local/prepare_char.py @@ -211,8 +211,9 @@ def main(): lang_dir = Path("data/lang_char") text_file = lang_dir / "text_with_bpe" bpe_model = lang_dir / "bpe.model" + words_file = lang_dir / "words.txt" - word_sym_table = k2.SymbolTable.from_file(lang_dir / "words.txt") + word_sym_table = k2.SymbolTable.from_file(words_file) words = word_sym_table.symbols diff --git a/egs/tal_csasr/ASR/local/train_bbpe_model.py b/egs/tal_csasr/ASR/local/train_bbpe_model.py new file mode 120000 index 000000000..7fb4a9f9d --- /dev/null +++ b/egs/tal_csasr/ASR/local/train_bbpe_model.py @@ -0,0 +1 @@ +../../../aishell/ASR/local/train_bbpe_model.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/prepare.sh b/egs/tal_csasr/ASR/prepare.sh index c5d498d74..352e8ba66 100755 --- a/egs/tal_csasr/ASR/prepare.sh +++ b/egs/tal_csasr/ASR/prepare.sh @@ -31,6 +31,15 @@ dl_dir=$PWD/download . shared/parse_options.sh || exit 1 +# vocab size for sentence piece models. +# It will generate data/lang_bbpe_xxx, +# data/lang_bbpe_yyy if the array contains xxx, yyy +vocab_sizes=( + # 2000 + 1000 + 500 +) + # All files generated by this script are saved in "data". # You can safely remove "data" and rerun this script to regenerate it. mkdir -p data @@ -117,55 +126,44 @@ if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then # You can also use other BPE models if available. if [ ! -f $lang_char_dir/bpe.model ]; then wget -O $lang_char_dir/bpe.model \ - https://huggingface.co/luomingshuang/bpe_models_trained_with_Librispeech/resolve/main/lang_bpe_5000/bpe.model + https://huggingface.co/luomingshuang/bpe_models_trained_with_Librispeech/resolve/main/lang_bpe_500/bpe.model fi - # Prepare text. - # Note: in Linux, you can install jq with the following command: - # 1. wget -O jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 - # 2. chmod +x ./jq - # 3. cp jq /usr/bin - if [ ! -f $lang_char_dir/text_full ]; then + # we extract text from manifests rather than the label.txt in corpus, because + # the texts in manifests have been normalized in lhotse. + if [ ! -f $lang_char_dir/text ]; then gunzip -c data/manifests/tal_csasr/tal_csasr_supervisions_train_set.jsonl.gz \ - | jq ".text" | sed 's/"//g' \ + | grep -o 'text":\s[^,]*' | sed 's/text": "//g;s/"//g' \ | ./local/text2token.py -t "char" > $lang_char_dir/text_train gunzip -c data/manifests/tal_csasr/tal_csasr_supervisions_dev_set.jsonl.gz \ - | jq ".text" | sed 's/"//g' \ + | grep -o 'text":\s[^,]*' | sed 's/text": "//g;s/"//g' \ | ./local/text2token.py -t "char" > $lang_char_dir/text_dev gunzip -c data/manifests/tal_csasr/tal_csasr_supervisions_test_set.jsonl.gz \ - | jq ".text" | sed 's/"//g' \ + | grep -o 'text":\s[^,]*' | sed 's/text": "//g;s/"//g' \ | ./local/text2token.py -t "char" > $lang_char_dir/text_test for r in text_train text_dev text_test ; do - cat $lang_char_dir/$r >> $lang_char_dir/text_full + cat $lang_char_dir/$r >> $lang_char_dir/text done fi - # Prepare text normalize - if [ ! -f $lang_char_dir/text ]; then - python ./local/text_normalize.py \ - --input $lang_char_dir/text_full \ - --output $lang_char_dir/text - fi - - # Prepare words segments - if [ ! -f $lang_char_dir/text_words_segmentation ]; then - python ./local/text2segments.py \ - --input $lang_char_dir/text \ - --output $lang_char_dir/text_words_segmentation - - cat $lang_char_dir/text_words_segmentation | sed "s/ /\n/g" \ - | sort -u | sed "/^$/d" \ - | uniq > $lang_char_dir/words_no_ids.txt - fi - # Prepare words.txt + # We assume you have install jieba, if not, please install + # it using: pip install jieba if [ ! -f $lang_char_dir/words.txt ]; then - ./local/prepare_words.py \ - --input $lang_char_dir/words_no_ids.txt \ - --output $lang_char_dir/words.txt + python -m jieba $lang_char_dir/text | sed 's/\///g;s/\s\+/ /g' > $lang_char_dir/text.seg + + (echo ' 0'; echo '!SIL 1'; echo ' 2'; echo ' 3';) \ + > $lang_char_dir/words.txt + + cat $lang_char_dir/text.seg | sed 's/ /\n/g' | sort -u | sed '/^$/d' \ + | awk '{print $1" "NR+3}' >> $lang_char_dir/words.txt + + num_lines=$(< $lang_char_dir/words.txt wc -l) + (echo "#0 $num_lines"; echo " $(($num_lines + 1))"; echo " $(($num_lines + 2))";) \ + >> $lang_char_dir/words.txt fi # Tokenize text with BPE model @@ -178,3 +176,23 @@ if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then python local/prepare_char.py fi fi + +if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then + log "Stage 7: Prepare Byte BPE based lang" + + for vocab_size in ${vocab_sizes[@]}; do + lang_dir=data/lang_bbpe_${vocab_size} + mkdir -p $lang_dir + # We reuse words.txt from phone based lexicon + # so that the two can share G.pt later. + cp $lang_char_dir/words.txt $lang_dir + cp $lang_char_dir/text $lang_dir + + if [ ! -f $lang_dir/bbpe.model ]; then + ./local/train_bbpe_model.py \ + --lang-dir $lang_dir \ + --vocab-size $vocab_size \ + --transcript $lang_dir/text + fi + done +fi diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/__init__.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/asr_datamodule.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/asr_datamodule.py new file mode 120000 index 000000000..c473a600a --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/asr_datamodule.py @@ -0,0 +1 @@ +../pruned_transducer_stateless5/asr_datamodule.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/beam_search.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/beam_search.py new file mode 120000 index 000000000..4eef3d295 --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/beam_search.py @@ -0,0 +1 @@ +../pruned_transducer_stateless5/beam_search.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decode.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decode.py new file mode 100755 index 000000000..885778965 --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decode.py @@ -0,0 +1,815 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao, +# Xiaoyu Yang, +# Wei Kang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) beam search (not recommended) +./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-duration 600 \ + --decoding-method beam_search \ + --beam-size 4 + +(3) modified beam search +./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +(4) fast beam search (one best) +./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 + +(5) fast beam search (nbest) +./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(6) fast beam search (nbest oracle WER) +./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_oracle \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(7) fast beam search (with LG) +./pruned_transducer_stateless7_bbpe/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_LG \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 +""" + +import argparse +import logging +import math +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import TAL_CSASRAsrDataModule +from beam_search import ( + beam_search, + fast_beam_search_nbest, + fast_beam_search_nbest_oracle, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from train import add_model_arguments, get_params, get_transducer_model + +from icefall import ( + LmScorer, + NgramLm, + byte_encode, + smart_byte_decode, + tokenize_by_CJK_char, +) +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless7/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bbpe_500/bbpe.model", + help="Path to the byte BPE model", + ) + + parser.add_argument( + "--lang-dir", + type=Path, + default="data/lang_bbpe_500", + help="The lang dir containing word table and LG graph", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + - fast_beam_search + - fast_beam_search_LG + - fast_beam_search_nbest + - fast_beam_search_nbest_oracle + If you use fast_beam_search_LG, you have to specify + `--lang-dir`, which should contain `LG.pt`. + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=20.0, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search, + fast_beam_search_nbest, fast_beam_search_LG, + and fast_beam_search_nbest_oracle + """, + ) + + parser.add_argument( + "--ngram-lm-scale", + type=float, + default=0.25, + help=""" + Used only when --decoding_method is fast_beam_search_LG. + It specifies the scale for n-gram LM scores. + """, + ) + + parser.add_argument( + "--ilme-scale", + type=float, + default=0.2, + help=""" + Used only when --decoding_method is fast_beam_search_LG. + It specifies the scale for the internal language model estimation. + """, + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=8, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=64, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + parser.add_argument( + "--num-paths", + type=int, + default=200, + help="""Number of paths for nbest decoding. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=0.5, + help="""Scale applied to lattice scores when computing nbest paths. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + batch: dict, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + encoder_out, encoder_out_lens = model.encoder(x=feature, x_lens=feature_lens) + + hyps = [] + + if params.decoding_method == "fast_beam_search": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + elif params.decoding_method == "fast_beam_search_LG": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + subtract_ilme=True, + ilme_scale=params.ilme_scale, + ) + for hyp in hyp_tokens: + hyps.append([word_table[i] for i in hyp]) + elif params.decoding_method == "fast_beam_search_nbest": + hyp_tokens = fast_beam_search_nbest( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + nbest_scale=params.nbest_scale, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + elif params.decoding_method == "fast_beam_search_nbest_oracle": + ref_texts = [] + for tx in supervisions["text"]: + ref_texts.append(byte_encode(tokenize_by_CJK_char(tx))) + + hyp_tokens = fast_beam_search_nbest_oracle( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + ref_texts=sp.encode(ref_texts), + nbest_scale=params.nbest_scale, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + elif params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + else: + batch_size = encoder_out.size(0) + + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyps.append(smart_byte_decode(sp.decode(hyp)).split()) + + if params.decoding_method == "greedy_search": + return {"greedy_search": hyps} + elif "fast_beam_search" in params.decoding_method: + key = f"beam_{params.beam}_" + key += f"max_contexts_{params.max_contexts}_" + key += f"max_states_{params.max_states}" + if "nbest" in params.decoding_method: + key += f"_num_paths_{params.num_paths}_" + key += f"nbest_scale_{params.nbest_scale}" + if "LG" in params.decoding_method: + key += f"_ngram_lm_scale_{params.ngram_lm_scale}" + key += f"_ilme_scale_{params.ilme_scale}" + + return {key: hyps} + else: + return {f"beam_size_{params.beam_size}": hyps} + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + LM: + A neural network LM, used during shallow fusion + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + sp=sp, + decoding_graph=decoding_graph, + word_table=word_table, + batch=batch, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = tokenize_by_CJK_char(ref_text).split() + this_batch.append((cut_id, ref_words, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = params.res_dir / f"recogs-{test_set_name}-{params.suffix}.txt" + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = params.res_dir / f"errs-{test_set_name}-{params.suffix}.txt" + + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = params.res_dir / f"wer-summary-{test_set_name}-{params.suffix}.txt" + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + TAL_CSASRAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "fast_beam_search", + "fast_beam_search_LG", + "fast_beam_search_nbest", + "fast_beam_search_nbest_oracle", + "modified_beam_search", + ) + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if "fast_beam_search" in params.decoding_method: + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + if "nbest" in params.decoding_method: + params.suffix += f"-nbest-scale-{params.nbest_scale}" + params.suffix += f"-num-paths-{params.num_paths}" + if "LG" in params.decoding_method: + params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" + params.suffix += f"-ilme-scale-{params.ilme_scale}" + elif "beam_search" in params.decoding_method: + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # and are defined in local/train_bbpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + if "fast_beam_search" in params.decoding_method: + if params.decoding_method == "fast_beam_search_LG": + lexicon = Lexicon(params.lang_dir) + word_table = lexicon.word_table + lg_filename = params.lang_dir / "LG.pt" + logging.info(f"Loading {lg_filename}") + decoding_graph = k2.Fsa.from_dict( + torch.load(lg_filename, map_location=device) + ) + decoding_graph.scores *= params.ngram_lm_scale + else: + word_table = None + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + else: + decoding_graph = None + word_table = None + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + tal_csasr = TAL_CSASRAsrDataModule(args) + + test_cuts = tal_csasr.test_cuts() + dev_cuts = tal_csasr.valid_cuts() + + test_dl = tal_csasr.test_dataloaders(test_cuts) + dev_dl = tal_csasr.test_dataloaders(dev_cuts) + + test_sets = ["test", "dev"] + test_dls = [test_dl, dev_dl] + + for test_set, test_dl in zip(test_sets, test_dls): + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + sp=sp, + word_table=word_table, + decoding_graph=decoding_graph, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decoder.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decoder.py new file mode 120000 index 000000000..8283d8c5a --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/decoder.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/decoder.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/encoder_interface.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/encoder_interface.py new file mode 120000 index 000000000..083f693ef --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/encoder_interface.py @@ -0,0 +1 @@ +../pruned_transducer_stateless5/encoder_interface.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/export.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/export.py new file mode 100755 index 000000000..862509d3f --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/export.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +# +# Copyright 2021 Xiaomi Corporation (Author: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script converts several saved checkpoints +# to a single one using model averaging. +""" + +Usage: + +(1) Export to torchscript model using torch.jit.script() + +./pruned_transducer_stateless7_bbpe/export.py \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --bpe-model data/lang_bbpe_500/bbpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +It will generate a file `cpu_jit.pt` in the given `exp_dir`. You can later +load it by `torch.jit.load("cpu_jit.pt")`. + +Note `cpu` in the name `cpu_jit.pt` means the parameters when loaded into Python +are on CPU. You can use `to("cuda")` to move them to a CUDA device. + +Check +https://github.com/k2-fsa/sherpa +for how to use the exported models outside of icefall. + +(2) Export `model.state_dict()` + +./pruned_transducer_stateless7_bbpe/export.py \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --bpe-model data/lang_bbpe_500/bbpe.model \ + --epoch 20 \ + --avg 10 + +It will generate a file `pretrained.pt` in the given `exp_dir`. You can later +load it by `icefall.checkpoint.load_checkpoint()`. + +To use the generated file with `pruned_transducer_stateless7_bbpe/decode.py`, +you can do: + + cd /path/to/exp_dir + ln -s pretrained.pt epoch-9999.pt + + cd /path/to/egs/librispeech/ASR + ./pruned_transducer_stateless7_bbpe/decode.py \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bbpe_500/bbpe.model + +Check ./pretrained.py for its usage. + +Note: If you don't want to train a model from scratch, we have +provided one for you. You can get it at + +https://huggingface.co/pkufool/icefall_asr_tal_csasr_pruned_transducer_stateless7_bbpe + +with the following commands: + + sudo apt-get install git-lfs + git lfs install + git clone https://huggingface.co/pkufool/icefall_asr_tal_csasr_pruned_transducer_stateless7_bbpe + # You will find the pre-trained model in icefall_asr_tal_csasr_pruned_transducer_stateless7_bbpe/exp +""" + +import argparse +import logging +from pathlib import Path + +import sentencepiece as spm +import torch +import torch.nn as nn +from scaling_converter import convert_scaled_to_non_scaled +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless7_bbpe/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bbpe_500/bbpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--jit", + type=str2bool, + default=False, + help="""True to save a model after applying torch.jit.script. + It will generate a file named cpu_jit.pt + + Check ./jit_pretrained.py for how to use it. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + if params.jit is True: + convert_scaled_to_non_scaled(model, inplace=True) + # We won't use the forward() method of the model in C++, so just ignore + # it here. + # Otherwise, one of its arguments is a ragged tensor and is not + # torch scriptabe. + model.__class__.forward = torch.jit.ignore(model.__class__.forward) + logging.info("Using torch.jit.script") + model = torch.jit.script(model) + filename = params.exp_dir / "cpu_jit.pt" + model.save(str(filename)) + logging.info(f"Saved to {filename}") + else: + logging.info("Not using torchscript. Export model.state_dict()") + # Save it using a format so that it can be loaded + # by :func:`load_checkpoint` + filename = params.exp_dir / "pretrained.pt" + torch.save({"model": model.state_dict()}, str(filename)) + logging.info(f"Saved to {filename}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/jit_pretrained.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/jit_pretrained.py new file mode 100755 index 000000000..a23e2a04f --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/jit_pretrained.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads torchscript models, exported by `torch.jit.script()` +and uses them to decode waves. +You can use the following command to get the exported models: + +./pruned_transducer_stateless7_bbpe/export.py \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --bpe-model data/lang_bbpe_500/bbpe.model \ + --epoch 20 \ + --avg 10 \ + --jit 1 + +Usage of this script: + +./pruned_transducer_stateless7_bbpe/jit_pretrained.py \ + --nn-model-filename ./pruned_transducer_stateless7_bbpe/exp/cpu_jit.pt \ + --bpe-model ./data/lang_bbpe_500/bbpe.model \ + /path/to/foo.wav \ + /path/to/bar.wav +""" + +import argparse +import logging +import math +from typing import List + +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence +from icefall import smart_byte_decode + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model-filename", + type=str, + required=True, + help="Path to the torchscript model cpu_jit.pt", + ) + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to bpe.model.""", + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float = 16000 +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +def greedy_search( + model: torch.jit.ScriptModule, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, +) -> List[List[int]]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (N, T, C) + encoder_out_lens: + A 1-D tensor of shape (N,). + Returns: + Return the decoded results for each utterance. + """ + assert encoder_out.ndim == 3 + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + device = encoder_out.device + blank_id = 0 # hard-code to 0 + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + context_size = model.decoder.context_size + hyps = [[blank_id] * context_size for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + device=device, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.decoder( + decoder_input, + need_pad=torch.tensor([False]), + ).squeeze(1) + + offset = 0 + for batch_size in batch_size_list: + start = offset + end = offset + batch_size + current_encoder_out = packed_encoder_out.data[start:end] + current_encoder_out = current_encoder_out + # current_encoder_out's shape: (batch_size, encoder_out_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + + logits = model.joiner( + current_encoder_out, + decoder_out, + ) + # logits'shape (batch_size, vocab_size) + + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v != blank_id: + hyps[i].append(v) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + device=device, + dtype=torch.int64, + ) + decoder_out = model.decoder( + decoder_input, + need_pad=torch.tensor([False]), + ) + decoder_out = decoder_out.squeeze(1) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + model = torch.jit.load(args.nn_model_filename) + + model.eval() + + model.to(device) + + sp = spm.SentencePieceProcessor() + sp.load(args.bpe_model) + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = 16000 + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence( + features, + batch_first=True, + padding_value=math.log(1e-10), + ) + + feature_lengths = torch.tensor(feature_lengths, device=device) + + encoder_out, encoder_out_lens = model.encoder( + x=features, + x_lens=feature_lengths, + ) + + hyps = greedy_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = smart_byte_decode(sp.decode(hyp)) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/joiner.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/joiner.py new file mode 120000 index 000000000..0f0c3c90a --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/joiner.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/joiner.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/model.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/model.py new file mode 120000 index 000000000..0d8bc665b --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/model.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/optim.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/optim.py new file mode 120000 index 000000000..8a05abb5f --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/optim.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/optim.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/pretrained.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/pretrained.py new file mode 100755 index 000000000..f365986f6 --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/pretrained.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads a checkpoint and uses it to decode waves. +You can generate the checkpoint with the following command: + +./pruned_transducer_stateless7_bbpe/export.py \ + --exp-dir ./pruned_transducer_stateless7_bbpe/exp \ + --bpe-model data/lang_bbpe_500/bbpe.model \ + --epoch 20 \ + --avg 10 + +Usage of this script: + +(1) greedy search +./pruned_transducer_stateless7_bbpe/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7_bbpe/exp/pretrained.pt \ + --bpe-model ./data/lang_bbpe_500/bbpe.model \ + --method greedy_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) beam search +./pruned_transducer_stateless7_bbpe/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7_bbpe/exp/pretrained.pt \ + --bpe-model ./data/lang_bbpe_500/bbpe.model \ + --method beam_search \ + --beam-size 4 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) modified beam search +./pruned_transducer_stateless7_bbpe/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7_bbpe/exp/pretrained.pt \ + --bpe-model ./data/lang_bbpe_500/bbpe.model \ + --method modified_beam_search \ + --beam-size 4 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(4) fast beam search +./pruned_transducer_stateless7_bbpe/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7_bbpe/exp/pretrained.pt \ + --bpe-model ./data/lang_bbpe_500/bbpe.model \ + --method fast_beam_search \ + --beam-size 4 \ + /path/to/foo.wav \ + /path/to/bar.wav + +You can also use `./pruned_transducer_stateless7_bbpe/exp/epoch-xx.pt`. + +Note: ./pruned_transducer_stateless7_bbpe/exp/pretrained.pt is generated by +./pruned_transducer_stateless7_bbpe/export.py +""" + + +import argparse +import logging +import math +from typing import List + +import k2 +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from beam_search import ( + beam_search, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from torch.nn.utils.rnn import pad_sequence +from train import add_model_arguments, get_params, get_transducer_model + +from icefall import smart_byte_decode +from icefall.utils import str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--checkpoint", + type=str, + required=True, + help="Path to the checkpoint. " + "The checkpoint is assumed to be saved by " + "icefall.checkpoint.save_checkpoint().", + ) + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to bpe.model.""", + ) + + parser.add_argument( + "--method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + - fast_beam_search + """, + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=4, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=4, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=8, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. Used only when + --method is greedy_search. + """, + ) + + add_model_arguments(parser) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + + params.update(vars(args)) + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + logging.info("Creating model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + checkpoint = torch.load(args.checkpoint, map_location="cpu") + model.load_state_dict(checkpoint["model"], strict=False) + model.to(device) + model.eval() + model.device = device + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + feature_lengths = torch.tensor(feature_lengths, device=device) + + encoder_out, encoder_out_lens = model.encoder(x=features, x_lens=feature_lengths) + + num_waves = encoder_out.size(0) + hyps = [] + msg = f"Using {params.method}" + if params.method == "beam_search": + msg += f" with beam size {params.beam_size}" + logging.info(msg) + + if params.method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + elif params.method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + elif params.method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(smart_byte_decode(hyp).split()) + else: + for i in range(num_waves): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError(f"Unsupported method: {params.method}") + + hyps.append(smart_byte_decode(sp.decode(hyp)).split()) + + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling.py new file mode 120000 index 000000000..5f9be9fe0 --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/scaling.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling_converter.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling_converter.py new file mode 120000 index 000000000..f9960e5c6 --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/scaling_converter.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/scaling_converter.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/test_model.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/test_model.py new file mode 120000 index 000000000..7ceac5d10 --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/test_model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/test_model.py \ No newline at end of file diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py new file mode 100755 index 000000000..32db5c801 --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py @@ -0,0 +1,1260 @@ +#!/usr/bin/env python3 +# Copyright 2021-2022 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo,) +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +./pruned_transducer_stateless7_bbpe/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --exp-dir pruned_transducer_stateless7_bbpe/exp \ + --max-duration 400 + +# For mix precision training: + +./pruned_transducer_stateless7_bbpe/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir pruned_transducer_stateless7_bbpe/exp \ + --max-duration 800 +""" + + +import argparse +import copy +import logging +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import TAL_CSASRAsrDataModule +from decoder import Decoder +from joiner import Joiner +from lhotse.cut import Cut, CutSet +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from model import Transducer +from optim import Eden, ScaledAdam +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer + +from icefall import diagnostics, byte_encode, tokenize_by_CJK_char +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.hooks import register_inf_check_hooks +from icefall.utils import ( + AttributeDict, + MetricsTracker, + filter_uneven_sized_batch, + setup_logger, + str2bool, +) + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for module in model.modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,4,3,2,4", + help="Number of zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--feedforward-dims", + type=str, + default="1024,1024,2048,2048,1024", + help="Feedforward dimension of the zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--nhead", + type=str, + default="8,8,8,8,8", + help="Number of attention heads in the zipformer encoder layers.", + ) + + parser.add_argument( + "--encoder-dims", + type=str, + default="384,384,384,384,384", + help="Embedding dimension in the 2 blocks of zipformer encoder layers, comma separated", + ) + + parser.add_argument( + "--attention-dims", + type=str, + default="192,192,192,192,192", + help="""Attention dimension in the 2 blocks of zipformer encoder layers, comma separated; + not the same as embedding dimension.""", + ) + + parser.add_argument( + "--encoder-unmasked-dims", + type=str, + default="256,256,256,256,256", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance " + " worse.", + ) + + parser.add_argument( + "--zipformer-downsampling-factors", + type=str, + default="1,2,4,8,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--cnn-module-kernels", + type=str, + default="31,31,31,31,31", + help="Sizes of kernels in convolution modules", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless7_bbpe/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bbpe_500/bbpe.model", + help="Path to the Byte BPE model", + ) + + parser.add_argument( + "--base-lr", type=float, default=0.05, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=5000, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=3.5, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network) part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--inf-check", + type=str2bool, + default=False, + help="Add hooks to check for infinite module outputs and gradients.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=2000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=30, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=200, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warmup period that dictates the decay of the + scale on "simple" (un-pruned) loss. + """ + params = AttributeDict( + { + "frame_shift_ms": 10.0, + "allowed_excess_duration_ratio": 0.1, + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 3000, + # parameters for zipformer + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed. + "warm_step": 2000, + "env_info": get_env_info(), + } + ) + + return params + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + # TODO: We can add an option to switch between Zipformer and Transformer + def to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + encoder = Zipformer( + num_features=params.feature_dim, + output_downsampling_factor=2, + zipformer_downsampling_factors=to_int_tuple( + params.zipformer_downsampling_factors + ), + encoder_dims=to_int_tuple(params.encoder_dims), + attention_dim=to_int_tuple(params.attention_dims), + encoder_unmasked_dims=to_int_tuple(params.encoder_unmasked_dims), + nhead=to_int_tuple(params.nhead), + feedforward_dim=to_int_tuple(params.feedforward_dims), + cnn_module_kernels=to_int_tuple(params.cnn_module_kernels), + num_encoder_layers=to_int_tuple(params.num_encoder_layers), + ) + return encoder + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_transducer_model(params: AttributeDict) -> nn.Module: + encoder = get_encoder_model(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = Transducer( + encoder=encoder, + decoder=decoder, + joiner=joiner, + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + if "cur_batch_idx" in saved_params: + params["cur_batch_idx"] = saved_params["cur_batch_idx"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute transducer loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Zipformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + # For the uneven-sized batch, the total duration after padding would possibly + # cause OOM. Hence, for each batch, which is sorted descendingly by length, + # we simply drop the last few shortest samples, so that the retained total frames + # (after padding) would not exceed `allowed_max_frames`: + # `allowed_max_frames = int(max_frames * (1.0 + allowed_excess_duration_ratio))`, + # where `max_frames = max_duration * 1000 // frame_shift_ms`. + # We set allowed_excess_duration_ratio=0.1. + max_frames = params.max_duration * 1000 // params.frame_shift_ms + allowed_max_frames = int(max_frames * (1.0 + params.allowed_excess_duration_ratio)) + batch = filter_uneven_sized_batch(batch, allowed_max_frames) + + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.warm_step + + texts = batch["supervisions"]["text"] + y = sp.encode(texts, out_type=int) + y = k2.RaggedTensor(y).to(device) + + with torch.set_grad_enabled(is_training): + simple_loss, pruned_loss = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + ) + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + + loss = simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + set_batch_count(model, params.batch_idx_train) + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + if cur_grad_scale < 1.0 or (cur_grad_scale < 8.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", + cur_grad_scale, + params.batch_idx_train, + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bbpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model).to(torch.float64) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + opts = diagnostics.TensorDiagnosticOptions( + 2**22 + ) # allow 4 megabytes per sub-module + diagnostic = diagnostics.attach_diagnostics(model, opts) + + if params.inf_check: + register_inf_check_hooks(model) + + tal_csasr = TAL_CSASRAsrDataModule(args) + train_cuts = tal_csasr.train_cuts() + + def remove_short_and_long_utt(c: Cut): + # Keep only utterances with duration between 1 second and 12 seconds + # + # Caution: There is a reason to select 12.0 here. Please see + # ../local/display_manifest_statistics.py + # + # You should use ../local/display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + if c.duration < 1.0 or c.duration > 20.0: + return False + + # In pruned RNN-T, we require that T >= S + # where T is the number of feature frames after subsampling + # and S is the number of tokens in the utterance + + # In ./zipformer.py, the conv module uses the following expression + # for subsampling + T = ((c.num_frames - 7) // 2 + 1) // 2 + tokens = sp.encode(c.supervisions[0].text, out_type=str) + + if T < len(tokens): + logging.warning( + f"Exclude cut with ID {c.id} from training. " + f"Number of frames (before subsampling): {c.num_frames}. " + f"Number of frames (after subsampling): {T}. " + f"Text: {c.supervisions[0].text}. " + f"Tokens: {tokens}. " + f"Number of tokens: {len(tokens)}" + ) + return False + + return True + + def tokenize_text_in_cut(c: Cut): + # Text normalize for each sample + text = c.supervisions[0].text + text = byte_encode(tokenize_by_CJK_char(text)) + c.supervisions[0].text = text + return c + + logging.info(f"Filtering short and long utterances.") + train_cuts = train_cuts.filter(remove_short_and_long_utt) + + logging.info(f"Tokenizing and encoding texts in train cuts.") + train_cuts = train_cuts.map(tokenize_text_in_cut) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = tal_csasr.train_dataloaders( + train_cuts, sampler_state_dict=sampler_state_dict + ) + + valid_cuts = tal_csasr.valid_cuts() + + logging.info(f"Tokenizing and encoding texts in valid cuts.") + valid_cuts = valid_cuts.map(tokenize_text_in_cut) + + valid_dl = tal_csasr.valid_dataloaders(valid_cuts) + + if not params.print_diagnostics: + scan_pessimistic_batches_for_oom( + model=model, + train_dl=train_dl, + optimizer=optimizer, + sp=sp, + params=params, + ) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + supervisions = batch["supervisions"] + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = sp.encode(supervisions["text"], out_type=int) + num_tokens = sum(len(i) for i in y) + logging.info(f"num tokens: {num_tokens}") + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + sp: spm.SentencePieceProcessor, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + loss.backward() + optimizer.zero_grad() + except Exception as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + display_and_save_batch(batch, params=params, sp=sp) + raise + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + + +def main(): + parser = get_parser() + TAL_CSASRAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/zipformer.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/zipformer.py new file mode 120000 index 000000000..f2f66041e --- /dev/null +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/zipformer.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/zipformer.py \ No newline at end of file diff --git a/icefall/byte_utils.py b/icefall/byte_utils.py index 7ee84ad27..79c1c7545 100644 --- a/icefall/byte_utils.py +++ b/icefall/byte_utils.py @@ -12,6 +12,7 @@ import unicodedata WHITESPACE_NORMALIZER = re.compile(r"\s+") SPACE = chr(32) SPACE_ESCAPE = chr(9601) +BPE_UNK = chr(8263) PRINTABLE_BASE_CHARS = [ 256, @@ -277,6 +278,7 @@ for c in PRINTABLE_BASE_CHARS: BYTE_TO_BCHAR = {b: chr(PRINTABLE_BASE_CHARS[b]) for b in range(256)} BCHAR_TO_BYTE = {bc: b for b, bc in BYTE_TO_BCHAR.items()} +BCHAR_TO_BYTE[BPE_UNK] = 32 # map unk to space def byte_encode(x: str) -> str: From 562bda91e498ce5b4188c78f16131e58d0d9a2e5 Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Wed, 17 May 2023 16:02:27 +0800 Subject: [PATCH 002/100] Add adaption recipe for pruned_transducer_stateless7 (#1059) * Add mux for finetune * Add comments * Fix for black * Update finetune.py --- egs/librispeech/ASR/finetune.sh | 1 + .../decode_gigaspeech.py | 35 ++++---- .../pruned_transducer_stateless7/finetune.py | 88 +++++++++++++------ 3 files changed, 78 insertions(+), 46 deletions(-) diff --git a/egs/librispeech/ASR/finetune.sh b/egs/librispeech/ASR/finetune.sh index 63d0966ed..bc6357312 100755 --- a/egs/librispeech/ASR/finetune.sh +++ b/egs/librispeech/ASR/finetune.sh @@ -79,6 +79,7 @@ if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then --use-averaged-model True \ --beam-size 4 \ --exp-dir pruned_transducer_stateless7/exp_giga_finetune \ + --bpe-model icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11/data/lang_bpe_500/bpe.model \ --max-duration 400 \ --decoding-method $m done diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/decode_gigaspeech.py b/egs/librispeech/ASR/pruned_transducer_stateless7/decode_gigaspeech.py index 4f64850b6..b0e4be0d1 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/decode_gigaspeech.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/decode_gigaspeech.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 # -# Copyright 2021-2022 Xiaomi Corporation (Author: Fangjun Kuang, +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, # Zengwei Yao, -# Xiaoyu Yang) +# Xiaoyu Yang, +# Yifan Yang,) # # See ../../../../LICENSE for clarification regarding multiple authors # @@ -20,36 +21,36 @@ """ Usage: (1) greedy search -./pruned_transducer_stateless7/decode.py \ +./pruned_transducer_stateless7/decode_gigaspeech.py \ --epoch 28 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless7/exp \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ --max-duration 600 \ --decoding-method greedy_search (2) beam search (not recommended) -./pruned_transducer_stateless7/decode.py \ +./pruned_transducer_stateless7/decode_gigaspeech.py \ --epoch 28 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless7/exp \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ --max-duration 600 \ --decoding-method beam_search \ --beam-size 4 (3) modified beam search -./pruned_transducer_stateless7/decode.py \ +./pruned_transducer_stateless7/decode_gigaspeech.py \ --epoch 28 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless7/exp \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ --max-duration 600 \ --decoding-method modified_beam_search \ --beam-size 4 (4) fast beam search (one best) -./pruned_transducer_stateless7/decode.py \ +./pruned_transducer_stateless7/decode_gigaspeech.py \ --epoch 28 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless7/exp \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ --max-duration 600 \ --decoding-method fast_beam_search \ --beam 20.0 \ @@ -57,10 +58,10 @@ Usage: --max-states 64 (5) fast beam search (nbest) -./pruned_transducer_stateless7/decode.py \ +./pruned_transducer_stateless7/decode_gigaspeech.py \ --epoch 28 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless7/exp \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ --max-duration 600 \ --decoding-method fast_beam_search_nbest \ --beam 20.0 \ @@ -70,10 +71,10 @@ Usage: --nbest-scale 0.5 (6) fast beam search (nbest oracle WER) -./pruned_transducer_stateless7/decode.py \ +./pruned_transducer_stateless7/decode_gigaspeech.py \ --epoch 28 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless7/exp \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ --max-duration 600 \ --decoding-method fast_beam_search_nbest_oracle \ --beam 20.0 \ @@ -83,10 +84,10 @@ Usage: --nbest-scale 0.5 (7) fast beam search (with LG) -./pruned_transducer_stateless7/decode.py \ +./pruned_transducer_stateless7/decode_gigaspeech.py \ --epoch 28 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless7/exp \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ --max-duration 600 \ --decoding-method fast_beam_search_nbest_LG \ --beam 20.0 \ @@ -187,7 +188,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless7/exp", + default="pruned_transducer_stateless7/exp_giga_finetune", help="The experiment dir", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py index 726a24809..02e8bd9eb 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 -# Copyright 2021-2022 Xiaomi Corp. (authors: Fangjun Kuang, +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, # Wei Kang, -# Mingshuang Luo,) -# Zengwei Yao) +# Mingshuang Luo, +# Zengwei Yao, +# Xiaoyu Yang, +# Yifan Yang) # # See ../../../../LICENSE for clarification regarding multiple authors # @@ -20,27 +22,23 @@ """ Usage: -export CUDA_VISIBLE_DEVICES="0,1,2,3" +export CUDA_VISIBLE_DEVICES="0,1" -./pruned_transducer_stateless7/train.py \ - --world-size 4 \ - --num-epochs 30 \ - --start-epoch 1 \ - --exp-dir pruned_transducer_stateless7/exp \ - --full-libri 1 \ - --max-duration 300 - -# For mix precision training: - -./pruned_transducer_stateless7/train.py \ - --world-size 4 \ - --num-epochs 30 \ +./pruned_transducer_stateless7/finetune.py \ + --world-size 2 \ + --num-epochs 20 \ --start-epoch 1 \ + --exp-dir pruned_transducer_stateless7/exp_giga_finetune \ + --subset S \ --use-fp16 1 \ - --exp-dir pruned_transducer_stateless7/exp \ - --full-libri 1 \ - --max-duration 550 - + --base-lr 0.005 \ + --lr-epochs 100 \ + --lr-batches 100000 \ + --bpe-model icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11/data/lang_bpe_500/bpe.model \ + --do-finetune True \ + --use-mux True \ + --finetune-ckpt icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11/exp/pretrained.pt \ + --max-duration 500 """ @@ -59,9 +57,10 @@ import torch import torch.multiprocessing as mp import torch.nn as nn from decoder import Decoder +from asr_datamodule import LibriSpeechAsrDataModule from gigaspeech import GigaSpeechAsrDataModule from joiner import Joiner -from lhotse.cut import Cut +from lhotse.cut import Cut, CutSet from lhotse.dataset.sampling.base import CutSampler from lhotse.utils import fix_random_seed from model import Transducer @@ -103,7 +102,21 @@ def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: def add_finetune_arguments(parser: argparse.ArgumentParser): - parser.add_argument("--do-finetune", type=str2bool, default=False) + parser.add_argument( + "--do-finetune", + type=str2bool, + default=True, + help="Whether to fine-tune.", + ) + parser.add_argument( + "--use-mux", + type=str2bool, + default=False, + help=""" + Whether to adapt. If true, we will mix 5% of the new data + with 95% of the original data to fine-tune. + """, + ) parser.add_argument( "--init-modules", @@ -907,7 +920,11 @@ def train_one_epoch( # NOTE: We use reduction==sum and loss is computed over utterances # in the batch and there is no normalization to it so far. scaler.scale(loss).backward() - set_batch_count(model, params.batch_idx_train) + # Skip the warmup by adding a huge number to batch_count + if params.do_finetune: + set_batch_count(model, params.batch_idx_train + 100000) + else: + set_batch_count(model, params.batch_idx_train) scheduler.step_batch(params.batch_idx_train) scaler.step(optimizer) @@ -1104,7 +1121,12 @@ def run(rank, world_size, args): parameters_names=parameters_names, ) - scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + scheduler = Eden( + optimizer=optimizer, + lr_batches=params.lr_batches, + lr_epochs=params.lr_epochs, + warmup_batches=0, + ) if checkpoints and "optimizer" in checkpoints: logging.info("Loading optimizer state dict") @@ -1129,7 +1151,15 @@ def run(rank, world_size, args): gigaspeech = GigaSpeechAsrDataModule(args) - train_cuts = gigaspeech.train_cuts() + if params.use_mux: + librispeech = LibriSpeechAsrDataModule(args) + train_cuts = CutSet.mux( + librispeech.train_all_shuf_cuts(), + gigaspeech.train_cuts(), + weights=[0.95, 0.05], + ) + else: + train_cuts = gigaspeech.train_cuts() def remove_short_and_long_utt(c: Cut): # Keep only utterances with duration between 1 second and 20 seconds @@ -1141,9 +1171,9 @@ def run(rank, world_size, args): # an utterance duration distribution for your dataset to select # the threshold if c.duration < 1.0 or c.duration > 20.0: - logging.warning( - f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" - ) + # logging.warning( + # f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + # ) return False # In pruned RNN-T, we require that T >= S From ae1949ddcc0b3499497a1a425b252f2f4c23a216 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 18 May 2023 20:56:58 +0800 Subject: [PATCH 003/100] Support using the latest master from tencent/ncnn (#1070) * Support using the latest master from tencent/ncnn * small fixes --- .../ncnn_custom_layer.py | 266 ++++++++++++++++++ .../streaming-ncnn-decode.py | 4 + .../zipformer2.py | 10 +- 3 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 egs/librispeech/ASR/pruned_transducer_stateless7_streaming/ncnn_custom_layer.py diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/ncnn_custom_layer.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/ncnn_custom_layer.py new file mode 100644 index 000000000..442a0a8af --- /dev/null +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/ncnn_custom_layer.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +# +# Copyright 2022-2023 Xiaomi Corp. (authors: Fangjun Kuang) +import ncnn +import numpy as np + + +layer_list = [] + + +def RegisterCustomLayers(net): + RegisterPoolingModuleNoProj(net) + RegisterTensorAsStrided(net) + RegisterSimpleUpsample(net) + RegisterStack(net) + + +def RegisterPoolingModuleNoProj(net): + net.register_custom_layer( + "PoolingModuleNoProj", + PoolingModuleNoProjCreator, + PoolingModuleNoProjDeleter, + ) + + +def PoolingModuleNoProjCreator(): + return PoolingModuleNoProj() + + +def PoolingModuleNoProjDeleter(l): + for i, layer in enumerate(layer_list): + if layer == l: + del layer_list[i] + break + + +def TensorAsStridedCreator(): + return TensorAsStrided() + + +def TensorAsStridedDeleter(l): + for i, layer in enumerate(layer_list): + if layer == l: + del layer_list[i] + break + + +def RegisterTensorAsStrided(net): + net.register_custom_layer( + "TensorAsStrided", + TensorAsStridedCreator, + TensorAsStridedDeleter, + ) + + +def SimpleUpsampleCreator(): + return SimpleUpsample() + + +def SimpleUpsampleDeleter(l): + for i, layer in enumerate(layer_list): + if layer == l: + del layer_list[i] + break + + +def RegisterSimpleUpsample(net): + net.register_custom_layer( + "SimpleUpsample", + SimpleUpsampleCreator, + SimpleUpsampleDeleter, + ) + + +def StackCreator(): + return Stack() + + +def StackDeleter(l): + for i, layer in enumerate(layer_list): + if layer == l: + del layer_list[i] + break + + +def RegisterStack(net): + net.register_custom_layer( + "Stack", + StackCreator, + StackDeleter, + ) + + +class PoolingModuleNoProj(ncnn.Layer): + def __init__(self): + super().__init__() + self.one_blob_only = False + self.support_inplace = False + layer_list.append(self) + + def forward(self, bottom_blobs, top_blobs, opt): + x = bottom_blobs[0] + cached_len = bottom_blobs[1] + cached_avg = bottom_blobs[2] + + # x.dims = 2, x.w = C, x.h = T, e.g., C=384, T=16 + # cached_len.dims = 1, cached_len.w = 1 + # cached_avg.dims = 2, cached_avg.w = C, cached_len.h = 1, e.g., C=384 + + x = x.numpy() # x is of shape (T, C), e.g., (16, 384) + x = x.cumsum(axis=0) + + cached_len = cached_len.numpy() + cached_avg = cached_avg.numpy() + + x = x + cached_len * cached_avg[0] + scale = np.arange(1, x.shape[0] + 1, dtype=np.float32).reshape(-1, 1) + x = x / (scale + cached_len) + + out_cached_len = cached_len + x.shape[0] + out_cached_avg = x[-1:] + + top_blobs[0].clone_from(ncnn.Mat(x), opt.blob_allocator) + top_blobs[1].clone_from(ncnn.Mat(out_cached_len), opt.blob_allocator) + top_blobs[2].clone_from(ncnn.Mat(out_cached_avg), opt.blob_allocator) + + # print(top_blobs[0].numpy().shape) + # print(top_blobs[1].numpy().shape) + # print(top_blobs[2].numpy().shape) + return 0 + + +class TensorAsStrided(ncnn.Layer): + def __init__(self): + super().__init__() + self.one_blob_only = True + self.support_inplace = False + + layer_list.append(self) + + def load_param(self, pd): + sizes = pd.get(0, ncnn.Mat()) + strides = pd.get(1, ncnn.Mat()) + storage_offset = pd.get(2, 0) + + assert sizes.dims == 1, sizes.dims + assert strides.dims == 1, strides.dims + + assert sizes.w == strides.w, (sizes.w, strides.w) + + self.sizes = sizes.numpy("i").tolist() + self.strides = strides.numpy("i").tolist() + self.storage_offset = storage_offset + + return 0 + + def forward(self, bottom_blob, top_blob, opt): + if bottom_blob.dims != 3: + raise ValueError( + f"Only 3-D tensors are supported. Given {bottom_blob.dims}" + ) + in_c = bottom_blob.c + in_h = bottom_blob.h + in_w = bottom_blob.w + + out_c = self.sizes[0] + out_h = self.sizes[1] + out_w = self.sizes[2] + + assert in_c == out_c, (in_c, out_c) + assert self.strides[0] == in_h * in_w, ( + self.strides[0], + in_h, + in_w, + in_h * in_w, + ) + + bottom_blob = bottom_blob.numpy() + out = np.empty((out_c, out_h, out_w), dtype=np.float32) + + for c in range(out_c): + p = bottom_blob[c].reshape(-1)[self.storage_offset :] + for h in range(out_h): + q = p[h * self.strides[1] :] + if True: + for w in range(out_w): + out[c][h][w] = q[w * self.strides[2]] + else: + out[c][h] = q[: (out_w * self.strides[2]) : self.strides[2]] + + top_blob.clone_from(ncnn.Mat(out), opt.blob_allocator) + + return 0 + + +class SimpleUpsample(ncnn.Layer): + def __init__(self): + super().__init__() + self.one_blob_only = True + self.support_inplace = False + + layer_list.append(self) + + def load_param(self, pd): + upsample = pd.get(0, 0) + num_channels = pd.get(1, 0) + bias_data_size = pd.get(2, 0) + + assert upsample * num_channels == bias_data_size, ( + upsample, + num_channels, + bias_data_size, + upsample * num_channels, + ) + + self.upsample = upsample + self.num_channels = num_channels + self.bias_data_size = bias_data_size + + return 0 + + def load_model(self, md): + bias = md.load(self.num_channels, self.upsample, 0) + assert bias.w == self.num_channels, (bias.w, self.num_channels) + assert bias.h == self.upsample, (bias.h, self.upsample) + + self.bias = bias.numpy() # its shape is (upsample, num_channels) + + return 0 + + def forward(self, bottom_blob, top_blob, opt): + assert bottom_blob.dims == 2, bottom_blob.dims + assert bottom_blob.w == self.num_channels, (bottom_blob.w, self.num_channels) + + bottom_blob = bottom_blob.numpy() + + out = np.expand_dims(bottom_blob, axis=1) + self.bias + out = out.reshape(-1, self.num_channels) + + top_blob.clone_from(ncnn.Mat(out), opt.blob_allocator) + + return 0 + + +class Stack(ncnn.Layer): + def __init__(self): + super().__init__() + self.one_blob_only = False + self.support_inplace = False + + layer_list.append(self) + + def load_param(self, pd): + axis = pd.get(0, 0) + + self.axis = axis + + return 0 + + def forward(self, bottom_blobs, top_blobs, opt): + bottom_blobs = [b.numpy() for b in bottom_blobs] + out = np.stack(bottom_blobs, axis=self.axis) + + top_blobs[0].clone_from(ncnn.Mat(out), opt.blob_allocator) + + return 0 diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming-ncnn-decode.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming-ncnn-decode.py index 8acace979..883fdcbdd 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming-ncnn-decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming-ncnn-decode.py @@ -43,6 +43,8 @@ import torch import torchaudio from kaldifeat import FbankOptions, OnlineFbank, OnlineFeature +from ncnn_custom_layer import RegisterCustomLayers + def get_args(): parser = argparse.ArgumentParser() @@ -202,6 +204,8 @@ class Model: encoder_param = args.encoder_param_filename encoder_model = args.encoder_bin_filename + RegisterCustomLayers(encoder_net) + encoder_net.load_param(encoder_param) encoder_net.load_model(encoder_model) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer2.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer2.py index be9cd1608..5284ed627 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer2.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer2.py @@ -1393,11 +1393,11 @@ class ZipformerEncoder(nn.Module): output, len_avg, avg, key, val, val2, conv1, conv2 = mod.streaming_forward( output, pos_emb, - cached_len=cached_len[i], - cached_avg=cached_avg[i], - cached_key=cached_key[i], - cached_val=cached_val[i], - cached_val2=cached_val2[i], + cached_len=state_select(cached_len), + cached_avg=state_select(cached_avg), + cached_key=state_select(cached_key), + cached_val=state_select(cached_val), + cached_val2=state_select(cached_val2), cached_conv1=state_select(cached_conv1), cached_conv2=state_select(cached_conv2), ) From a5bbfc6f7eea68ac2602bf1cb271c1373cd43ed6 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 19 May 2023 16:22:08 +0800 Subject: [PATCH 004/100] Update doc for exporting to ncnn (#1072) --- docs/source/model-export/export-ncnn-zipformer.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/source/model-export/export-ncnn-zipformer.rst b/docs/source/model-export/export-ncnn-zipformer.rst index 5c81d25ca..8440d26b7 100644 --- a/docs/source/model-export/export-ncnn-zipformer.rst +++ b/docs/source/model-export/export-ncnn-zipformer.rst @@ -276,7 +276,7 @@ The result looks like below: 7767517 2029 2547 - SherpaMetaData sherpa_meta_data1 0 0 0=2 1=32 2=4 3=7 -23316=5,2,4,3,2,4 -23317=5,384,384,384,384,384 -23318=5,192,192,192,192,192 -23319=5,1,2,4,8,2 -23320=5,31,31,31,31,31 + SherpaMetaData sherpa_meta_data1 0 0 0=2 1=32 2=4 3=7 15=1 -23316=5,2,4,3,2,4 -23317=5,384,384,384,384,384 -23318=5,192,192,192,192,192 -23319=5,1,2,4,8,2 -23320=5,31,31,31,31,31 Input in0 0 1 in0 **Explanation** @@ -300,6 +300,9 @@ The result looks like below: - ``3=7``, 3 is the key and 7 is the value of for the amount of padding used in the Conv2DSubsampling layer. It should be 7 for zipformer if you don't change zipformer.py. + - ``15=1``, attribute 15, this is the model version. Starting from + `sherpa-ncnn`_ v2.0, we require that the model version has to + be >= 1. - ``-23316=5,2,4,3,2,4``, attribute 16, this is an array attribute. It is attribute 16 since -23300 - (-23316) = 16. The first element of the array is the length of the array, which is 5 in our case. @@ -338,6 +341,8 @@ The result looks like below: +----------+--------------------------------------------+ | 3 | 7 (if you don't change code) | +----------+--------------------------------------------+ + | 15 | 1 (The model version) | + +----------+--------------------------------------------+ |-23316 | ``--num-encoder-layer`` | +----------+--------------------------------------------+ |-23317 | ``--encoder-dims`` | From f18b539fbcb5e8f2a357700d1169dda05845fb55 Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Fri, 19 May 2023 16:47:59 +0800 Subject: [PATCH 005/100] Add the upgraded Zipformer model (#1058) * add the zipformer codes, copied from branch from_dan_scaled_adam_exp1119 * support model export with torch.jit.script * update RESULTS.md * support exporting streaming model with torch.jit.script * add results of streaming models, with some minor changes * update README.md * add CI test * update k2 version in requirements-ci.txt * update pyproject.toml --- .flake8 | 1 + ...rispeech-streaming-zipformer-2023-05-18.sh | 115 + .../run-librispeech-zipformer-2023-05-18.sh | 93 + ...ispeech-streaming-zipformer-2023-05-18.yml | 174 ++ .../run-librispeech-zipformer-2023-05-18.yml | 159 ++ .github/workflows/test.yml | 1 + README.md | 12 +- egs/librispeech/ASR/README.md | 1 + egs/librispeech/ASR/RESULTS.md | 244 ++ egs/librispeech/ASR/zipformer/__init__.py | 0 .../ASR/zipformer/asr_datamodule.py | 1 + egs/librispeech/ASR/zipformer/beam_search.py | 1 + egs/librispeech/ASR/zipformer/decode.py | 834 ++++++ .../ASR/zipformer/decode_stream.py | 148 ++ egs/librispeech/ASR/zipformer/decoder.py | 123 + .../ASR/zipformer/encoder_interface.py | 1 + egs/librispeech/ASR/zipformer/export.py | 523 ++++ .../ASR/zipformer/generate_averaged_model.py | 202 ++ .../ASR/zipformer/jit_pretrained.py | 272 ++ .../ASR/zipformer/jit_pretrained_streaming.py | 269 ++ egs/librispeech/ASR/zipformer/joiner.py | 66 + egs/librispeech/ASR/zipformer/model.py | 217 ++ egs/librispeech/ASR/zipformer/optim.py | 1173 +++++++++ egs/librispeech/ASR/zipformer/pretrained.py | 382 +++ egs/librispeech/ASR/zipformer/scaling.py | 1797 +++++++++++++ .../ASR/zipformer/scaling_converter.py | 82 + .../ASR/zipformer/streaming_beam_search.py | 282 +++ .../ASR/zipformer/streaming_decode.py | 876 +++++++ egs/librispeech/ASR/zipformer/subsampling.py | 407 +++ egs/librispeech/ASR/zipformer/train.py | 1362 ++++++++++ egs/librispeech/ASR/zipformer/zipformer.py | 2237 +++++++++++++++++ icefall/diagnostics.py | 316 ++- icefall/utils.py | 58 + pyproject.toml | 2 + requirements-ci.txt | 2 +- 35 files changed, 12381 insertions(+), 52 deletions(-) create mode 100755 .github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh create mode 100755 .github/scripts/run-librispeech-zipformer-2023-05-18.sh create mode 100644 .github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml create mode 100644 .github/workflows/run-librispeech-zipformer-2023-05-18.yml create mode 100644 egs/librispeech/ASR/zipformer/__init__.py create mode 120000 egs/librispeech/ASR/zipformer/asr_datamodule.py create mode 120000 egs/librispeech/ASR/zipformer/beam_search.py create mode 100755 egs/librispeech/ASR/zipformer/decode.py create mode 100644 egs/librispeech/ASR/zipformer/decode_stream.py create mode 100644 egs/librispeech/ASR/zipformer/decoder.py create mode 120000 egs/librispeech/ASR/zipformer/encoder_interface.py create mode 100755 egs/librispeech/ASR/zipformer/export.py create mode 100755 egs/librispeech/ASR/zipformer/generate_averaged_model.py create mode 100755 egs/librispeech/ASR/zipformer/jit_pretrained.py create mode 100755 egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py create mode 100644 egs/librispeech/ASR/zipformer/joiner.py create mode 100644 egs/librispeech/ASR/zipformer/model.py create mode 100644 egs/librispeech/ASR/zipformer/optim.py create mode 100755 egs/librispeech/ASR/zipformer/pretrained.py create mode 100644 egs/librispeech/ASR/zipformer/scaling.py create mode 100644 egs/librispeech/ASR/zipformer/scaling_converter.py create mode 100644 egs/librispeech/ASR/zipformer/streaming_beam_search.py create mode 100755 egs/librispeech/ASR/zipformer/streaming_decode.py create mode 100644 egs/librispeech/ASR/zipformer/subsampling.py create mode 100755 egs/librispeech/ASR/zipformer/train.py create mode 100644 egs/librispeech/ASR/zipformer/zipformer.py diff --git a/.flake8 b/.flake8 index 41d8799c8..1c0c2cdbb 100644 --- a/.flake8 +++ b/.flake8 @@ -13,6 +13,7 @@ per-file-ignores = egs/librispeech/ASR/conv_emformer_transducer_stateless*/*.py: E501, E203 egs/librispeech/ASR/conformer_ctc*/*py: E501, egs/librispeech/ASR/zipformer_mmi/*.py: E501, E203 + egs/librispeech/ASR/zipformer/*.py: E501, E203 egs/librispeech/ASR/RESULTS.md: E999, # invalid escape sequence (cause by tex formular), W605 diff --git a/.github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh b/.github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh new file mode 100755 index 000000000..45324cb27 --- /dev/null +++ b/.github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash + +set -e + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 + +log "Downloading pre-trained model from $repo_url" +git lfs install +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +log "Display test files" +tree $repo/ +ls -lh $repo/test_wavs/*.wav + +pushd $repo/exp +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "exp/jit_script_chunk_16_left_128.pt" +git lfs pull --include "exp/pretrained.pt" +ln -s pretrained.pt epoch-99.pt +ls -lh *.pt +popd + +log "Export to torchscript model" +./zipformer/export.py \ + --exp-dir $repo/exp \ + --use-averaged-model false \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --epoch 99 \ + --avg 1 \ + --jit 1 + +ls -lh $repo/exp/*.pt + +log "Decode with models exported by torch.jit.script()" + +./zipformer/jit_pretrained_streaming.py \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --nn-model-filename $repo/exp/jit_script_chunk_16_left_128.pt \ + $repo/test_wavs/1089-134686-0001.wav + +for method in greedy_search modified_beam_search fast_beam_search; do + log "$method" + + ./zipformer/pretrained.py \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --method $method \ + --beam-size 4 \ + --checkpoint $repo/exp/pretrained.pt \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav +done + +echo "GITHUB_EVENT_NAME: ${GITHUB_EVENT_NAME}" +echo "GITHUB_EVENT_LABEL_NAME: ${GITHUB_EVENT_LABEL_NAME}" +if [[ x"${GITHUB_EVENT_NAME}" == x"schedule" || x"${GITHUB_EVENT_LABEL_NAME}" == x"run-decode" ]]; then + mkdir -p zipformer/exp + ln -s $PWD/$repo/exp/pretrained.pt zipformer/exp/epoch-999.pt + ln -s $PWD/$repo/data/lang_bpe_500 data/ + + ls -lh data + ls -lh zipformer/exp + + log "Decoding test-clean and test-other" + + # use a small value for decoding with CPU + max_duration=100 + + for method in greedy_search fast_beam_search modified_beam_search; do + log "Simulated streaming decoding with $method" + + ./zipformer/decode.py \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --decoding-method $method \ + --epoch 999 \ + --avg 1 \ + --use-averaged-model 0 \ + --max-duration $max_duration \ + --exp-dir zipformer/exp + done + + for method in greedy_search fast_beam_search modified_beam_search; do + log "Chunk-wise streaming decoding with $method" + + ./zipformer/streaming_decode.py \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --decoding-method $method \ + --epoch 999 \ + --avg 1 \ + --use-averaged-model 0 \ + --max-duration $max_duration \ + --exp-dir zipformer/exp + done + + rm zipformer/exp/*.pt +fi diff --git a/.github/scripts/run-librispeech-zipformer-2023-05-18.sh b/.github/scripts/run-librispeech-zipformer-2023-05-18.sh new file mode 100755 index 000000000..6aac1793e --- /dev/null +++ b/.github/scripts/run-librispeech-zipformer-2023-05-18.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +set -e + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 + +log "Downloading pre-trained model from $repo_url" +git lfs install +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +log "Display test files" +tree $repo/ +ls -lh $repo/test_wavs/*.wav + +pushd $repo/exp +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "exp/jit_script.pt" +git lfs pull --include "exp/pretrained.pt" +ln -s pretrained.pt epoch-99.pt +ls -lh *.pt +popd + +log "Export to torchscript model" +./zipformer/export.py \ + --exp-dir $repo/exp \ + --use-averaged-model false \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --epoch 99 \ + --avg 1 \ + --jit 1 + +ls -lh $repo/exp/*.pt + +log "Decode with models exported by torch.jit.script()" + +./zipformer/jit_pretrained.py \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --nn-model-filename $repo/exp/jit_script.pt \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav + +for method in greedy_search modified_beam_search fast_beam_search; do + log "$method" + + ./zipformer/pretrained.py \ + --method $method \ + --beam-size 4 \ + --checkpoint $repo/exp/pretrained.pt \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav +done + +echo "GITHUB_EVENT_NAME: ${GITHUB_EVENT_NAME}" +echo "GITHUB_EVENT_LABEL_NAME: ${GITHUB_EVENT_LABEL_NAME}" +if [[ x"${GITHUB_EVENT_NAME}" == x"schedule" || x"${GITHUB_EVENT_LABEL_NAME}" == x"run-decode" ]]; then + mkdir -p zipformer/exp + ln -s $PWD/$repo/exp/pretrained.pt zipformer/exp/epoch-999.pt + ln -s $PWD/$repo/data/lang_bpe_500 data/ + + ls -lh data + ls -lh zipformer/exp + + log "Decoding test-clean and test-other" + + # use a small value for decoding with CPU + max_duration=100 + + for method in greedy_search fast_beam_search modified_beam_search; do + log "Decoding with $method" + + ./zipformer/decode.py \ + --decoding-method $method \ + --epoch 999 \ + --avg 1 \ + --use-averaged-model 0 \ + --max-duration $max_duration \ + --exp-dir zipformer/exp + done + + rm zipformer/exp/*.pt +fi diff --git a/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml b/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml new file mode 100644 index 000000000..fa0bb3971 --- /dev/null +++ b/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml @@ -0,0 +1,174 @@ +# Copyright 2022 Fangjun Kuang (csukuangfj@gmail.com) + +# See ../../LICENSE for clarification regarding multiple authors +# +# 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-librispeech-streaming-zipformer-2023-05-18 +# zipformer + +on: + push: + branches: + - master + pull_request: + types: [labeled] + + schedule: + # minute (0-59) + # hour (0-23) + # day of the month (1-31) + # month (1-12) + # day of the week (0-6) + # nightly build at 15:50 UTC time every day + - cron: "50 15 * * *" + +concurrency: + group: run_librispeech_2023_05_18_streaming_zipformer-${{ github.ref }} + cancel-in-progress: true + +jobs: + run_librispeech_2023_05_18_streaming_zipformer: + if: github.event.label.name == 'zipformer' ||github.event.label.name == 'ready' || github.event.label.name == 'run-decode' || github.event_name == 'push' || github.event_name == 'schedule' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.8] + + fail-fast: false + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: '**/requirements-ci.txt' + + - name: Install Python dependencies + run: | + grep -v '^#' ./requirements-ci.txt | xargs -n 1 -L 1 pip install + pip uninstall -y protobuf + pip install --no-binary protobuf protobuf==3.20.* + + - name: Cache kaldifeat + id: my-cache + uses: actions/cache@v2 + with: + path: | + ~/tmp/kaldifeat + key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + + - name: Install kaldifeat + if: steps.my-cache.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/install-kaldifeat.sh + + - name: Cache LibriSpeech test-clean and test-other datasets + id: libri-test-clean-and-test-other-data + uses: actions/cache@v2 + with: + path: | + ~/tmp/download + key: cache-libri-test-clean-and-test-other + + - name: Download LibriSpeech test-clean and test-other + if: steps.libri-test-clean-and-test-other-data.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/download-librispeech-test-clean-and-test-other-dataset.sh + + - name: Prepare manifests for LibriSpeech test-clean and test-other + shell: bash + run: | + .github/scripts/prepare-librispeech-test-clean-and-test-other-manifests.sh + + - name: Cache LibriSpeech test-clean and test-other fbank features + id: libri-test-clean-and-test-other-fbank + uses: actions/cache@v2 + with: + path: | + ~/tmp/fbank-libri + key: cache-libri-fbank-test-clean-and-test-other-v2 + + - name: Compute fbank for LibriSpeech test-clean and test-other + if: steps.libri-test-clean-and-test-other-fbank.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/compute-fbank-librispeech-test-clean-and-test-other.sh + + - name: Inference with pre-trained model + shell: bash + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_LABEL_NAME: ${{ github.event.label.name }} + run: | + mkdir -p egs/librispeech/ASR/data + ln -sfv ~/tmp/fbank-libri egs/librispeech/ASR/data/fbank + ls -lh egs/librispeech/ASR/data/* + + sudo apt-get -qq install git-lfs tree + export PYTHONPATH=$PWD:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/kaldifeat/python:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/build/lib:$PYTHONPATH + + .github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh + + - name: Display decoding results for librispeech zipformer + if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' + shell: bash + run: | + cd egs/librispeech/ASR/ + tree ./zipformer/exp + + cd zipformer + + echo "results for zipformer, simulated streaming decoding" + echo "===greedy search===" + find exp/greedy_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/greedy_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "===fast_beam_search===" + find exp/fast_beam_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/fast_beam_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "===modified beam search===" + find exp/modified_beam_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/modified_beam_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "results for zipformer, chunk-wise streaming decoding" + echo "===greedy search===" + find exp/streaming/greedy_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/streaming/greedy_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "===fast_beam_search===" + find exp/streaming/fast_beam_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/streaming/fast_beam_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "===modified beam search===" + find exp/streaming/modified_beam_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/streaming/modified_beam_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + + - name: Upload decoding results for librispeech zipformer + uses: actions/upload-artifact@v2 + if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' + with: + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-zipformer-2022-11-11 + path: egs/librispeech/ASR/zipformer/exp/ diff --git a/.github/workflows/run-librispeech-zipformer-2023-05-18.yml b/.github/workflows/run-librispeech-zipformer-2023-05-18.yml new file mode 100644 index 000000000..febb55026 --- /dev/null +++ b/.github/workflows/run-librispeech-zipformer-2023-05-18.yml @@ -0,0 +1,159 @@ +# Copyright 2022 Fangjun Kuang (csukuangfj@gmail.com) + +# See ../../LICENSE for clarification regarding multiple authors +# +# 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-librispeech-zipformer-2023-05-18 +# zipformer + +on: + push: + branches: + - master + pull_request: + types: [labeled] + + schedule: + # minute (0-59) + # hour (0-23) + # day of the month (1-31) + # month (1-12) + # day of the week (0-6) + # nightly build at 15:50 UTC time every day + - cron: "50 15 * * *" + +concurrency: + group: run_librispeech_2023_05_18_zipformer-${{ github.ref }} + cancel-in-progress: true + +jobs: + run_librispeech_2023_05_18_zipformer: + if: github.event.label.name == 'zipformer' ||github.event.label.name == 'ready' || github.event.label.name == 'run-decode' || github.event_name == 'push' || github.event_name == 'schedule' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.8] + + fail-fast: false + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: '**/requirements-ci.txt' + + - name: Install Python dependencies + run: | + grep -v '^#' ./requirements-ci.txt | xargs -n 1 -L 1 pip install + pip uninstall -y protobuf + pip install --no-binary protobuf protobuf==3.20.* + + - name: Cache kaldifeat + id: my-cache + uses: actions/cache@v2 + with: + path: | + ~/tmp/kaldifeat + key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + + - name: Install kaldifeat + if: steps.my-cache.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/install-kaldifeat.sh + + - name: Cache LibriSpeech test-clean and test-other datasets + id: libri-test-clean-and-test-other-data + uses: actions/cache@v2 + with: + path: | + ~/tmp/download + key: cache-libri-test-clean-and-test-other + + - name: Download LibriSpeech test-clean and test-other + if: steps.libri-test-clean-and-test-other-data.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/download-librispeech-test-clean-and-test-other-dataset.sh + + - name: Prepare manifests for LibriSpeech test-clean and test-other + shell: bash + run: | + .github/scripts/prepare-librispeech-test-clean-and-test-other-manifests.sh + + - name: Cache LibriSpeech test-clean and test-other fbank features + id: libri-test-clean-and-test-other-fbank + uses: actions/cache@v2 + with: + path: | + ~/tmp/fbank-libri + key: cache-libri-fbank-test-clean-and-test-other-v2 + + - name: Compute fbank for LibriSpeech test-clean and test-other + if: steps.libri-test-clean-and-test-other-fbank.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/compute-fbank-librispeech-test-clean-and-test-other.sh + + - name: Inference with pre-trained model + shell: bash + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_LABEL_NAME: ${{ github.event.label.name }} + run: | + mkdir -p egs/librispeech/ASR/data + ln -sfv ~/tmp/fbank-libri egs/librispeech/ASR/data/fbank + ls -lh egs/librispeech/ASR/data/* + + sudo apt-get -qq install git-lfs tree + export PYTHONPATH=$PWD:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/kaldifeat/python:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/build/lib:$PYTHONPATH + + .github/scripts/run-librispeech-zipformer-2023-05-18.sh + + - name: Display decoding results for librispeech zipformer + if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' + shell: bash + run: | + cd egs/librispeech/ASR/ + tree ./zipformer/exp + + cd zipformer + echo "results for zipformer" + echo "===greedy search===" + find exp/greedy_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/greedy_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "===fast_beam_search===" + find exp/fast_beam_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/fast_beam_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "===modified beam search===" + find exp/modified_beam_search -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/modified_beam_search -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + - name: Upload decoding results for librispeech zipformer + uses: actions/upload-artifact@v2 + if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' + with: + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-zipformer-2022-11-11 + path: egs/librispeech/ASR/zipformer/exp/ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 079772e97..e04fb5655 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -113,6 +113,7 @@ jobs: cd ../pruned_transducer_stateless4 pytest -v -s + echo $PYTHONPATH cd ../pruned_transducer_stateless7 pytest -v -s diff --git a/README.md b/README.md index 476aae6de..a876fb24e 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,13 @@ We provide a Colab notebook for this recipe: [![Open In Colab](https://colab.res Please see for the **latest** results. -We provide 4 models for this recipe: +We provide 5 models for this recipe: - [conformer CTC model][LibriSpeech_conformer_ctc] - [TDNN LSTM CTC model][LibriSpeech_tdnn_lstm_ctc] - [Transducer: Conformer encoder + LSTM decoder][LibriSpeech_transducer] - [Transducer: Conformer encoder + Embedding decoder][LibriSpeech_transducer_stateless] +- [Transducer: Zipformer encoder + Embedding decoder][LibriSpeech_zipformer] #### Conformer CTC Model @@ -115,9 +116,11 @@ We provide a Colab notebook to run a pre-trained transducer conformer + stateles #### k2 pruned RNN-T -| | test-clean | test-other | -|-----|------------|------------| -| WER | 2.15 | 5.20 | +| Encoder | Params | test-clean | test-other | +|-----------------|--------|------------|------------| +| zipformer | 65.5M | 2.21 | 4.91 | +| zipformer-small | 23.2M | 2.46 | 5.83 | +| zipformer-large | 148.4M | 2.11 | 4.77 | Note: No auxiliary losses are used in the training and no LMs are used in the decoding. @@ -361,6 +364,7 @@ Please see: [![Open In Colab](https://colab.research.google.com/assets/colab-bad [LibriSpeech_conformer_ctc]: egs/librispeech/ASR/conformer_ctc [LibriSpeech_transducer]: egs/librispeech/ASR/transducer [LibriSpeech_transducer_stateless]: egs/librispeech/ASR/transducer_stateless +[LibriSpeech_zipformer]: egs/librispeech/ASR/zipformer [Aishell_tdnn_lstm_ctc]: egs/aishell/ASR/tdnn_lstm_ctc [Aishell_conformer_ctc]: egs/aishell/ASR/conformer_ctc [Aishell_pruned_transducer_stateless7]: egs/aishell/ASR/pruned_transducer_stateless7_bbpe diff --git a/egs/librispeech/ASR/README.md b/egs/librispeech/ASR/README.md index 82cef9817..6f5ee7846 100644 --- a/egs/librispeech/ASR/README.md +++ b/egs/librispeech/ASR/README.md @@ -34,6 +34,7 @@ The following table lists the differences among them. | `lstm_transducer_stateless` | LSTM | Embedding + Conv1d | Using LSTM with mechanisms in reworked model | | `lstm_transducer_stateless2` | LSTM | Embedding + Conv1d | Using LSTM with mechanisms in reworked model + gigaspeech (multi-dataset setup) | | `lstm_transducer_stateless3` | LSTM | Embedding + Conv1d | Using LSTM with mechanisms in reworked model + gradient filter + delay penalty | +| `zipformer` | Upgraded Zipformer | Embedding + Conv1d | The latest recipe | The decoder in `transducer_stateless` is modified from the paper [Rnn-Transducer with Stateless Prediction Network](https://ieeexplore.ieee.org/document/9054419/). diff --git a/egs/librispeech/ASR/RESULTS.md b/egs/librispeech/ASR/RESULTS.md index 2ca0558ab..ed456a617 100644 --- a/egs/librispeech/ASR/RESULTS.md +++ b/egs/librispeech/ASR/RESULTS.md @@ -1,5 +1,249 @@ ## Results +### zipformer (zipformer + pruned stateless transducer) + +See for more details. + +[zipformer](./zipformer) + +#### Non-streaming + +##### normal-scaled model, number of model parameters: 65549011, i.e., 65.55 M + +The tensorboard log can be found at + + +You can find a pretrained model, training logs, decoding logs, and decoding results at: + + +You can use to deploy it. + +| decoding method | test-clean | test-other | comment | +|----------------------|------------|------------|--------------------| +| greedy_search | 2.27 | 5.1 | --epoch 30 --avg 9 | +| modified_beam_search | 2.25 | 5.06 | --epoch 30 --avg 9 | +| fast_beam_search | 2.25 | 5.04 | --epoch 30 --avg 9 | +| greedy_search | 2.23 | 4.96 | --epoch 40 --avg 16 | +| modified_beam_search | 2.21 | 4.91 | --epoch 40 --avg 16 | +| fast_beam_search | 2.24 | 4.93 | --epoch 40 --avg 16 | + +The training command is: +```bash +export CUDA_VISIBLE_DEVICES="0,1,2,3" +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 40 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --causal 0 \ + --full-libri 1 \ + --max-duration 1000 +``` + +The decoding command is: +```bash +export CUDA_VISIBLE_DEVICES="0" +for m in greedy_search modified_beam_search fast_beam_search; do + ./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --use-averaged-model 1 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method $m +done +``` + +##### small-scaled model, number of model parameters: 23285615, i.e., 23.3 M + +The tensorboard log can be found at + + +You can find a pretrained model, training logs, decoding logs, and decoding results at: + + +You can use to deploy it. + +| decoding method | test-clean | test-other | comment | +|----------------------|------------|------------|--------------------| +| greedy_search | 2.64 | 6.14 | --epoch 30 --avg 8 | +| modified_beam_search | 2.6 | 6.01 | --epoch 30 --avg 8 | +| fast_beam_search | 2.62 | 6.06 | --epoch 30 --avg 8 | +| greedy_search | 2.49 | 5.91 | --epoch 40 --avg 13 | +| modified_beam_search | 2.46 | 5.83 | --epoch 40 --avg 13 | +| fast_beam_search | 2.46 | 5.87 | --epoch 40 --avg 13 | + +The training command is: +```bash +export CUDA_VISIBLE_DEVICES="0,1" +./zipformer/train.py \ + --world-size 2 \ + --num-epochs 40 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp-small \ + --causal 0 \ + --num-encoder-layers 2,2,2,2,2,2 \ + --feedforward-dim 512,768,768,768,768,768 \ + --encoder-dim 192,256,256,256,256,256 \ + --encoder-unmasked-dim 192,192,192,192,192,192 \ + --base-lr 0.04 \ + --full-libri 1 \ + --max-duration 1500 +``` + +The decoding command is: +```bash +export CUDA_VISIBLE_DEVICES="0" +for m in greedy_search modified_beam_search fast_beam_search; do + ./zipformer/decode.py \ + --epoch 40 \ + --avg 13 \ + --exp-dir zipformer/exp-small \ + --max-duration 600 \ + --causal 0 \ + --decoding-method $m \ + --num-encoder-layers 2,2,2,2,2,2 \ + --feedforward-dim 512,768,768,768,768,768 \ + --encoder-dim 192,256,256,256,256,256 \ + --encoder-unmasked-dim 192,192,192,192,192,192 +done +``` + +##### large-scaled model, number of model parameters: 148439574, i.e., 148.4 M + +The tensorboard log can be found at + + +You can find a pretrained model, training logs, decoding logs, and decoding results at: + + +You can use to deploy it. + +| decoding method | test-clean | test-other | comment | +|----------------------|------------|------------|--------------------| +| greedy_search | 2.12 | 4.91 | --epoch 30 --avg 9 | +| modified_beam_search | 2.11 | 4.9 | --epoch 30 --avg 9 | +| fast_beam_search | 2.13 | 4.93 | --epoch 30 --avg 9 | +| greedy_search | 2.12 | 4.8 | --epoch 40 --avg 13 | +| modified_beam_search | 2.11 | 4.7 | --epoch 40 --avg 13 | +| fast_beam_search | 2.13 | 4.78 | --epoch 40 --avg 13 | + +The training command is: +```bash +export CUDA_VISIBLE_DEVICES="0,1,2,3" +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 40 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp-large \ + --causal 0 \ + --num-encoder-layers 2,2,4,5,4,2 \ + --feedforward-dim 512,768,1536,2048,1536,768 \ + --encoder-dim 192,256,512,768,512,256 \ + --encoder-unmasked-dim 192,192,256,320,256,192 \ + --full-libri 1 \ + --max-duration 1000 +``` + +The decoding command is: +```bash +export CUDA_VISIBLE_DEVICES="0" +for m in greedy_search modified_beam_search fast_beam_search; do + ./zipformer/decode.py \ + --epoch 40 \ + --avg 16 \ + --exp-dir zipformer/exp-large \ + --max-duration 600 \ + --causal 0 \ + --decoding-method $m \ + --num-encoder-layers 2,2,4,5,4,2 \ + --feedforward-dim 512,768,1536,2048,1536,768 \ + --encoder-dim 192,256,512,768,512,256 \ + --encoder-unmasked-dim 192,192,256,320,256,192 +done +``` + +#### streaming + +##### normal-scaled model, number of model parameters: 66110931, i.e., 66.11 M + +The tensorboard log can be found at + + +You can find a pretrained model, training logs, decoding logs, and decoding results at: + + +You can use to deploy it. + +| decoding method | chunk size | test-clean | test-other | decoding mode | comment | +|----------------------|------------|------------|------------|---------------------|--------------------| +| greedy_search | 320ms | 3.06 | 7.81 | simulated streaming | --epoch 30 --avg 8 --chunk-size 16 --left-context-frames 128 | +| greedy_search | 320ms | 3.06 | 7.79 | chunk-wise | --epoch 30 --avg 8 --chunk-size 16 --left-context-frames 128 | +| modified_beam_search | 320ms | 3.01 | 7.69 | simulated streaming | --epoch 30 --avg 8 --chunk-size 16 --left-context-frames 128 | +| modified_beam_search | 320ms | 3.05 | 7.69 | chunk-wise | --epoch 30 --avg 8 --chunk-size 16 --left-context-frames 128 | +| fast_beam_search | 320ms | 3.04 | 7.68 | simulated streaming | --epoch 30 --avg 8 --chunk-size 16 --left-context-frames 128 | +| fast_beam_search | 320ms | 3.07 | 7.69 | chunk-wise | --epoch 30 --avg 8 --chunk-size 16 --left-context-frames 128 | +| greedy_search | 640ms | 2.81 | 7.15 | simulated streaming | --epoch 30 --avg 8 --chunk-size 32 --left-context-frames 256 | +| greedy_search | 640ms | 2.84 | 7.16 | chunk-wise | --epoch 30 --avg 8 --chunk-size 32 --left-context-frames 256 | +| modified_beam_search | 640ms | 2.79 | 7.05 | simulated streaming | --epoch 30 --avg 8 --chunk-size 32 --left-context-frames 256 | +| modified_beam_search | 640ms | 2.81 | 7.11 | chunk-wise | --epoch 30 --avg 8 --chunk-size 32 --left-context-frames 256 | +| fast_beam_search | 640ms | 2.84 | 7.04 | simulated streaming | --epoch 30 --avg 8 --chunk-size 32 --left-context-frames 256 | +| fast_beam_search | 640ms | 2.83 | 7.1 | chunk-wise | --epoch 30 --avg 8 --chunk-size 32 --left-context-frames 256 | + +Note: For decoding mode, `simulated streaming` indicates feeding full utterance during decoding using `decode.py`, + while `chunk-size` indicates feeding certain number of frames at each time using `streaming_decode.py`. + +The training command is: +```bash +export CUDA_VISIBLE_DEVICES="0,1,2,3" +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 40 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp-causal \ + --causal 1 \ + --full-libri 1 \ + --max-duration 1000 +``` + +The simulated streaming decoding command is: +```bash +export CUDA_VISIBLE_DEVICES="0" +for m in greedy_search modified_beam_search fast_beam_search; do + ./zipformer/decode.py \ + --epoch 30 \ + --avg 8 \ + --use-averaged-model 1 \ + --exp-dir ./zipformer/exp-causal \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --max-duration 600 \ + --decoding-method $m +done +``` + +The chunk-wise streaming decoding command is: +```bash +export CUDA_VISIBLE_DEVICES="0" +for m in greedy_search modified_beam_search fast_beam_search; do + ./zipformer/streaming_decode.py \ + --epoch 30 \ + --avg 8 \ + --use-averaged-model 1 \ + --exp-dir ./zipformer/exp-causal \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --num-decode-streams 2000 \ + --decoding-method $m +done +``` + ### pruned_transducer_stateless7 (zipformer + multidataset(LibriSpeech + GigaSpeech + CommonVoice 13.0)) See for more details. diff --git a/egs/librispeech/ASR/zipformer/__init__.py b/egs/librispeech/ASR/zipformer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/egs/librispeech/ASR/zipformer/asr_datamodule.py b/egs/librispeech/ASR/zipformer/asr_datamodule.py new file mode 120000 index 000000000..07f39b451 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/asr_datamodule.py @@ -0,0 +1 @@ +../transducer/asr_datamodule.py \ No newline at end of file diff --git a/egs/librispeech/ASR/zipformer/beam_search.py b/egs/librispeech/ASR/zipformer/beam_search.py new file mode 120000 index 000000000..8554e44cc --- /dev/null +++ b/egs/librispeech/ASR/zipformer/beam_search.py @@ -0,0 +1 @@ +../pruned_transducer_stateless2/beam_search.py \ No newline at end of file diff --git a/egs/librispeech/ASR/zipformer/decode.py b/egs/librispeech/ASR/zipformer/decode.py new file mode 100755 index 000000000..f4b81cfe3 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/decode.py @@ -0,0 +1,834 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) beam search (not recommended) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method beam_search \ + --beam-size 4 + +(3) modified beam search +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +(4) fast beam search (one best) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 + +(5) fast beam search (nbest) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(6) fast beam search (nbest oracle WER) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_oracle \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(7) fast beam search (with LG) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_LG \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 +""" + + +import argparse +import logging +import math +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import LibriSpeechAsrDataModule +from beam_search import ( + beam_search, + fast_beam_search_nbest, + fast_beam_search_nbest_LG, + fast_beam_search_nbest_oracle, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + make_pad_mask, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--lang-dir", + type=Path, + default="data/lang_bpe_500", + help="The lang dir containing word table and LG graph", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + - fast_beam_search + - fast_beam_search_nbest + - fast_beam_search_nbest_oracle + - fast_beam_search_nbest_LG + If you use fast_beam_search_nbest_LG, you have to specify + `--lang-dir`, which should contain `LG.pt`. + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=20.0, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search, + fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle + """, + ) + + parser.add_argument( + "--ngram-lm-scale", + type=float, + default=0.01, + help=""" + Used only when --decoding_method is fast_beam_search_nbest_LG. + It specifies the scale for n-gram LM scores. + """, + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=8, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=64, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; " + "2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + parser.add_argument( + "--num-paths", + type=int, + default=200, + help="""Number of paths for nbest decoding. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=0.5, + help="""Scale applied to lattice scores when computing nbest paths. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + batch: dict, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + if params.causal: + # this seems to cause insertions at the end of the utterance if used with zipformer. + pad_len = 30 + feature_lens += pad_len + feature = torch.nn.functional.pad( + feature, + pad=(0, 0, 0, pad_len), + value=LOG_EPS, + ) + + x, x_lens = model.encoder_embed(feature, feature_lens) + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = model.encoder( + x, x_lens, src_key_padding_mask + ) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + hyps = [] + + if params.decoding_method == "fast_beam_search": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "fast_beam_search_nbest_LG": + hyp_tokens = fast_beam_search_nbest_LG( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + nbest_scale=params.nbest_scale, + ) + for hyp in hyp_tokens: + hyps.append([word_table[i] for i in hyp]) + elif params.decoding_method == "fast_beam_search_nbest": + hyp_tokens = fast_beam_search_nbest( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + nbest_scale=params.nbest_scale, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "fast_beam_search_nbest_oracle": + hyp_tokens = fast_beam_search_nbest_oracle( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + ref_texts=sp.encode(supervisions["text"]), + nbest_scale=params.nbest_scale, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif ( + params.decoding_method == "greedy_search" + and params.max_sym_per_frame == 1 + ): + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + else: + batch_size = encoder_out.size(0) + + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyps.append(sp.decode(hyp).split()) + + if params.decoding_method == "greedy_search": + return {"greedy_search": hyps} + elif "fast_beam_search" in params.decoding_method: + key = f"beam_{params.beam}_" + key += f"max_contexts_{params.max_contexts}_" + key += f"max_states_{params.max_states}" + if "nbest" in params.decoding_method: + key += f"_num_paths_{params.num_paths}_" + key += f"nbest_scale_{params.nbest_scale}" + if "LG" in params.decoding_method: + key += f"_ngram_lm_scale_{params.ngram_lm_scale}" + + return {key: hyps} + else: + return {f"beam_size_{params.beam_size}": hyps} + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + sp=sp, + decoding_graph=decoding_graph, + word_table=word_table, + batch=batch, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = ref_text.split() + this_batch.append((cut_id, ref_words, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info( + f"batch {batch_str}, cuts processed until now is {num_cuts}" + ) + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir + / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "fast_beam_search", + "fast_beam_search_nbest", + "fast_beam_search_nbest_LG", + "fast_beam_search_nbest_oracle", + "modified_beam_search", + ) + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if params.causal: + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + params.suffix += f"-chunk-{params.chunk_size}" + params.suffix += f"-left-context-{params.left_context_frames}" + + if "fast_beam_search" in params.decoding_method: + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + if "nbest" in params.decoding_method: + params.suffix += f"-nbest-scale-{params.nbest_scale}" + params.suffix += f"-num-paths-{params.num_paths}" + if "LG" in params.decoding_method: + params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" + elif "beam_search" in params.decoding_method: + params.suffix += ( + f"-{params.decoding_method}-beam-size-{params.beam_size}" + ) + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # and are defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints( + params.exp_dir, iteration=-params.iter + )[: params.avg] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints( + params.exp_dir, iteration=-params.iter + )[: params.avg + 1] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + if "fast_beam_search" in params.decoding_method: + if params.decoding_method == "fast_beam_search_nbest_LG": + lexicon = Lexicon(params.lang_dir) + word_table = lexicon.word_table + lg_filename = params.lang_dir / "LG.pt" + logging.info(f"Loading {lg_filename}") + decoding_graph = k2.Fsa.from_dict( + torch.load(lg_filename, map_location=device) + ) + decoding_graph.scores *= params.ngram_lm_scale + else: + word_table = None + decoding_graph = k2.trivial_graph( + params.vocab_size - 1, device=device + ) + else: + decoding_graph = None + word_table = None + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + librispeech = LibriSpeechAsrDataModule(args) + + test_clean_cuts = librispeech.test_clean_cuts() + test_other_cuts = librispeech.test_other_cuts() + + test_clean_dl = librispeech.test_dataloaders(test_clean_cuts) + test_other_dl = librispeech.test_dataloaders(test_other_cuts) + + test_sets = ["test-clean", "test-other"] + test_dl = [test_clean_dl, test_other_dl] + + for test_set, test_dl in zip(test_sets, test_dl): + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + sp=sp, + word_table=word_table, + decoding_graph=decoding_graph, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/decode_stream.py b/egs/librispeech/ASR/zipformer/decode_stream.py new file mode 100644 index 000000000..946db275c --- /dev/null +++ b/egs/librispeech/ASR/zipformer/decode_stream.py @@ -0,0 +1,148 @@ +# Copyright 2022 Xiaomi Corp. (authors: Wei Kang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 +from typing import List, Optional, Tuple + +import k2 +import torch +from beam_search import Hypothesis, HypothesisList + +from icefall.utils import AttributeDict + + +class DecodeStream(object): + def __init__( + self, + params: AttributeDict, + cut_id: str, + initial_states: List[torch.Tensor], + decoding_graph: Optional[k2.Fsa] = None, + device: torch.device = torch.device("cpu"), + ) -> None: + """ + Args: + initial_states: + Initial decode states of the model, e.g. the return value of + `get_init_state` in conformer.py + decoding_graph: + Decoding graph used for decoding, may be a TrivialGraph or a HLG. + Used only when decoding_method is fast_beam_search. + device: + The device to run this stream. + """ + if params.decoding_method == "fast_beam_search": + assert decoding_graph is not None + assert device == decoding_graph.device + + self.params = params + self.cut_id = cut_id + self.LOG_EPS = math.log(1e-10) + + self.states = initial_states + + # It contains a 2-D tensors representing the feature frames. + self.features: torch.Tensor = None + + self.num_frames: int = 0 + # how many frames have been processed. (before subsampling). + # we only modify this value in `func:get_feature_frames`. + self.num_processed_frames: int = 0 + + self._done: bool = False + + # The transcript of current utterance. + self.ground_truth: str = "" + + # The decoding result (partial or final) of current utterance. + self.hyp: List = [] + + # how many frames have been processed, at encoder output + self.done_frames: int = 0 + + # The encoder_embed subsample features (T - 7) // 2 + # The ConvNeXt module needs (7 - 1) // 2 = 3 frames of right padding after subsampling + self.pad_length = 7 + 2 * 3 + + if params.decoding_method == "greedy_search": + self.hyp = [params.blank_id] * params.context_size + elif params.decoding_method == "modified_beam_search": + self.hyps = HypothesisList() + self.hyps.add( + Hypothesis( + ys=[params.blank_id] * params.context_size, + log_prob=torch.zeros(1, dtype=torch.float32, device=device), + ) + ) + elif params.decoding_method == "fast_beam_search": + # The rnnt_decoding_stream for fast_beam_search. + self.rnnt_decoding_stream: k2.RnntDecodingStream = k2.RnntDecodingStream( + decoding_graph + ) + else: + raise ValueError(f"Unsupported decoding method: {params.decoding_method}") + + @property + def done(self) -> bool: + """Return True if all the features are processed.""" + return self._done + + @property + def id(self) -> str: + return self.cut_id + + def set_features( + self, + features: torch.Tensor, + tail_pad_len: int = 0, + ) -> None: + """Set features tensor of current utterance.""" + assert features.dim() == 2, features.dim() + self.features = torch.nn.functional.pad( + features, + (0, 0, 0, self.pad_length + tail_pad_len), + mode="constant", + value=self.LOG_EPS, + ) + self.num_frames = self.features.size(0) + + def get_feature_frames(self, chunk_size: int) -> Tuple[torch.Tensor, int]: + """Consume chunk_size frames of features""" + chunk_length = chunk_size + self.pad_length + + ret_length = min(self.num_frames - self.num_processed_frames, chunk_length) + + ret_features = self.features[ + self.num_processed_frames : self.num_processed_frames + ret_length # noqa + ] + + self.num_processed_frames += chunk_size + if self.num_processed_frames >= self.num_frames: + self._done = True + + return ret_features, ret_length + + def decoding_result(self) -> List[int]: + """Obtain current decoding result.""" + if self.params.decoding_method == "greedy_search": + return self.hyp[self.params.context_size :] # noqa + elif self.params.decoding_method == "modified_beam_search": + best_hyp = self.hyps.get_most_probable(length_norm=True) + return best_hyp.ys[self.params.context_size :] # noqa + else: + assert self.params.decoding_method == "fast_beam_search" + return self.hyp diff --git a/egs/librispeech/ASR/zipformer/decoder.py b/egs/librispeech/ASR/zipformer/decoder.py new file mode 100644 index 000000000..45432d570 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/decoder.py @@ -0,0 +1,123 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 + +from scaling import Balancer + + +class Decoder(nn.Module): + """This class modifies the stateless decoder from the following paper: + + RNN-transducer with stateless prediction network + https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9054419 + + It removes the recurrent connection from the decoder, i.e., the prediction + network. Different from the above paper, it adds an extra Conv1d + right after the embedding layer. + + TODO: Implement https://arxiv.org/pdf/2109.07513.pdf + """ + + def __init__( + self, + vocab_size: int, + decoder_dim: int, + blank_id: int, + context_size: int, + ): + """ + Args: + vocab_size: + Number of tokens of the modeling unit including blank. + decoder_dim: + Dimension of the input embedding, and of the decoder output. + blank_id: + The ID of the blank symbol. + context_size: + Number of previous words to use to predict the next word. + 1 means bigram; 2 means trigram. n means (n+1)-gram. + """ + super().__init__() + + self.embedding = nn.Embedding( + num_embeddings=vocab_size, + embedding_dim=decoder_dim, + padding_idx=blank_id, + ) + # the balancers are to avoid any drift in the magnitude of the + # embeddings, which would interact badly with parameter averaging. + self.balancer = Balancer(decoder_dim, channel_dim=-1, + min_positive=0.0, max_positive=1.0, + min_abs=0.5, max_abs=1.0, + prob=0.05) + + self.blank_id = blank_id + + assert context_size >= 1, context_size + self.context_size = context_size + self.vocab_size = vocab_size + + if context_size > 1: + self.conv = nn.Conv1d( + in_channels=decoder_dim, + out_channels=decoder_dim, + kernel_size=context_size, + padding=0, + groups=decoder_dim // 4, # group size == 4 + bias=False, + ) + self.balancer2 = Balancer(decoder_dim, channel_dim=-1, + min_positive=0.0, max_positive=1.0, + min_abs=0.5, max_abs=1.0, + prob=0.05) + + def forward(self, y: torch.Tensor, need_pad: bool = True) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, U). + need_pad: + True to left pad the input. Should be True during training. + False to not pad the input. Should be False during inference. + Returns: + Return a tensor of shape (N, U, decoder_dim). + """ + y = y.to(torch.int64) + # this stuff about clamp() is a temporary fix for a mismatch + # at utterance start, we use negative ids in beam_search.py + embedding_out = self.embedding(y.clamp(min=0)) * (y >= 0).unsqueeze(-1) + + embedding_out = self.balancer(embedding_out) + + if self.context_size > 1: + embedding_out = embedding_out.permute(0, 2, 1) + if need_pad is True: + embedding_out = F.pad( + embedding_out, pad=(self.context_size - 1, 0) + ) + else: + # During inference time, there is no need to do extra padding + # as we only need one output + assert embedding_out.size(-1) == self.context_size + embedding_out = self.conv(embedding_out) + embedding_out = embedding_out.permute(0, 2, 1) + embedding_out = F.relu(embedding_out) + embedding_out = self.balancer2(embedding_out) + + return embedding_out diff --git a/egs/librispeech/ASR/zipformer/encoder_interface.py b/egs/librispeech/ASR/zipformer/encoder_interface.py new file mode 120000 index 000000000..aa5d0217a --- /dev/null +++ b/egs/librispeech/ASR/zipformer/encoder_interface.py @@ -0,0 +1 @@ +../transducer_stateless/encoder_interface.py \ No newline at end of file diff --git a/egs/librispeech/ASR/zipformer/export.py b/egs/librispeech/ASR/zipformer/export.py new file mode 100755 index 000000000..b996470aa --- /dev/null +++ b/egs/librispeech/ASR/zipformer/export.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script converts several saved checkpoints +# to a single one using model averaging. +""" + +Usage: + +(1) Export to torchscript model using torch.jit.script() + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +It will generate a file `jit_script.pt` in the given `exp_dir`. You can later +load it by `torch.jit.load("jit_script.pt")`. + +Check ./jit_pretrained.py for its usage. + +Check https://github.com/k2-fsa/sherpa +for how to use the exported models outside of icefall. + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +It will generate a file `jit_script_chunk_16_left_128.pt` in the given `exp_dir`. +You can later load it by `torch.jit.load("jit_script_chunk_16_left_128.pt")`. + +Check ./jit_pretrained_streaming.py for its usage. + +Check https://github.com/k2-fsa/sherpa +for how to use the exported models outside of icefall. + +(2) Export `model.state_dict()` + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --causal 1 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 + +It will generate a file `pretrained.pt` in the given `exp_dir`. You can later +load it by `icefall.checkpoint.load_checkpoint()`. + +- For non-streaming model: + +To use the generated file with `zipformer/decode.py`, +you can do: + + cd /path/to/exp_dir + ln -s pretrained.pt epoch-9999.pt + + cd /path/to/egs/librispeech/ASR + ./zipformer/decode.py \ + --exp-dir ./zipformer/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bpe_500/bpe.model + +- For streaming model: + +To use the generated file with `zipformer/decode.py` and `zipformer/streaming_decode.py`, you can do: + + cd /path/to/exp_dir + ln -s pretrained.pt epoch-9999.pt + + cd /path/to/egs/librispeech/ASR + + # simulated streaming decoding + ./zipformer/decode.py \ + --exp-dir ./zipformer/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bpe_500/bpe.model + + # chunk-wise streaming decoding + ./zipformer/streaming_decode.py \ + --exp-dir ./zipformer/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bpe_500/bpe.model + +Check ./pretrained.py for its usage. + +Note: If you don't want to train a model from scratch, we have +provided one for you. You can get it at + +- non-streaming model: +https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 + +- streaming model: +https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 + +with the following commands: + + sudo apt-get install git-lfs + git lfs install + git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 + git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 + # You will find the pre-trained models in exp dir +""" + +import argparse +import logging +from pathlib import Path +from typing import List, Tuple + +import sentencepiece as spm +import torch +from torch import Tensor, nn +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import make_pad_mask, str2bool +from scaling_converter import convert_scaled_to_non_scaled + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--jit", + type=str2bool, + default=False, + help="""True to save a model after applying torch.jit.script. + It will generate a file named cpu_jit.pt. + Check ./jit_pretrained.py for how to use it. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +class EncoderModel(nn.Module): + """A wrapper for encoder and encoder_embed""" + def __init__(self, encoder: nn.Module, encoder_embed: nn.Module) -> None: + super().__init__() + self.encoder = encoder + self.encoder_embed = encoder_embed + + def forward( + self, features: Tensor, feature_lengths: Tensor + ) -> Tuple[Tensor, Tensor]: + """ + Args: + features: (N, T, C) + feature_lengths: (N,) + """ + x, x_lens = self.encoder_embed(features, feature_lengths) + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = self.encoder( + x, x_lens, src_key_padding_mask + ) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + return encoder_out, encoder_out_lens + + +class StreamingEncoderModel(nn.Module): + """A wrapper for encoder and encoder_embed""" + + def __init__(self, encoder: nn.Module, encoder_embed: nn.Module) -> None: + super().__init__() + assert len(encoder.chunk_size) == 1, encoder.chunk_size + assert len(encoder.left_context_frames) == 1, encoder.left_context_frames + self.chunk_size = encoder.chunk_size[0] + self.left_context_len = encoder.left_context_frames[0] + + # The encoder_embed subsample features (T - 7) // 2 + # The ConvNeXt module needs (7 - 1) // 2 = 3 frames of right padding after subsampling + self.pad_length = 7 + 2 * 3 + + self.encoder = encoder + self.encoder_embed = encoder_embed + + def forward( + self, features: Tensor, feature_lengths: Tensor, states: List[Tensor] + ) -> Tuple[Tensor, Tensor, List[Tensor]]: + """Streaming forward for encoder_embed and encoder. + + Args: + features: (N, T, C) + feature_lengths: (N,) + states: a list of Tensors + + Returns encoder outputs, output lengths, and updated states. + """ + chunk_size = self.chunk_size + left_context_len = self.left_context_len + + cached_embed_left_pad = states[-2] + x, x_lens, new_cached_embed_left_pad = self.encoder_embed.streaming_forward( + x=features, + x_lens=feature_lengths, + cached_left_pad=cached_embed_left_pad, + ) + assert x.size(1) == chunk_size, (x.size(1), chunk_size) + + src_key_padding_mask = make_pad_mask(x_lens) + + # processed_mask is used to mask out initial states + processed_mask = torch.arange(left_context_len, device=x.device).expand( + x.size(0), left_context_len + ) + processed_lens = states[-1] # (batch,) + # (batch, left_context_size) + processed_mask = (processed_lens.unsqueeze(1) <= processed_mask).flip(1) + # Update processed lengths + new_processed_lens = processed_lens + x_lens + + # (batch, left_context_size + chunk_size) + src_key_padding_mask = torch.cat([processed_mask, src_key_padding_mask], dim=1) + + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + encoder_states = states[:-2] + + ( + encoder_out, + encoder_out_lens, + new_encoder_states, + ) = self.encoder.streaming_forward( + x=x, + x_lens=x_lens, + states=encoder_states, + src_key_padding_mask=src_key_padding_mask, + ) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + new_states = new_encoder_states + [ + new_cached_embed_left_pad, + new_processed_lens, + ] + return encoder_out, encoder_out_lens, new_states + + @torch.jit.export + def get_init_states( + self, + batch_size: int = 1, + device: torch.device = torch.device("cpu"), + ) -> List[torch.Tensor]: + """ + Returns a list of cached tensors of all encoder layers. For layer-i, states[i*6:(i+1)*6] + is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + states[-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + states[-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + """ + states = self.encoder.get_init_states(batch_size, device) + + embed_states = self.encoder_embed.get_init_states(batch_size, device) + states.append(embed_states) + + processed_lens = torch.zeros(batch_size, dtype=torch.int32, device=device) + states.append(processed_lens) + + return states + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + # if torch.cuda.is_available(): + # device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.eval() + + if params.jit is True: + convert_scaled_to_non_scaled(model, inplace=True) + # We won't use the forward() method of the model in C++, so just ignore + # it here. + # Otherwise, one of its arguments is a ragged tensor and is not + # torch scriptabe. + model.__class__.forward = torch.jit.ignore(model.__class__.forward) + + # Wrap encoder and encoder_embed as a module + if params.causal: + model.encoder = StreamingEncoderModel(model.encoder, model.encoder_embed) + chunk_size = model.encoder.chunk_size + left_context_len = model.encoder.left_context_len + filename = f"jit_script_chunk_{chunk_size}_left_{left_context_len}.pt" + else: + model.encoder = EncoderModel(model.encoder, model.encoder_embed) + filename = "jit_script.pt" + + logging.info("Using torch.jit.script") + model = torch.jit.script(model) + model.save(str(params.exp_dir / filename)) + logging.info(f"Saved to {filename}") + else: + logging.info("Not using torchscript. Export model.state_dict()") + # Save it using a format so that it can be loaded + # by :func:`load_checkpoint` + filename = params.exp_dir / "pretrained.pt" + torch.save({"model": model.state_dict()}, str(filename)) + logging.info(f"Saved to {filename}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/generate_averaged_model.py b/egs/librispeech/ASR/zipformer/generate_averaged_model.py new file mode 100755 index 000000000..fe29355f2 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/generate_averaged_model.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2022 Xiaomi Corporation (Author: Yifan Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) use the checkpoint exp_dir/epoch-xxx.pt +./zipformer/generate_averaged_model.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp + +It will generate a file `epoch-28-avg-15.pt` in the given `exp_dir`. +You can later load it by `torch.load("epoch-28-avg-15.pt")`. + +(2) use the checkpoint exp_dir/checkpoint-iter.pt +./zipformer/generate_averaged_model.py \ + --iter 22000 \ + --avg 5 \ + --exp-dir ./zipformer/exp + +It will generate a file `iter-22000-avg-5.pt` in the given `exp_dir`. +You can later load it by `torch.load("iter-22000-avg-5.pt")`. +""" + + +import argparse +from pathlib import Path + +import sentencepiece as spm +import torch +from asr_datamodule import LibriSpeechAsrDataModule + +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints_with_averaged_model, + find_checkpoints, +) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +@torch.no_grad() +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + print("Script started") + + device = torch.device("cpu") + print(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + print("About to create model") + model = get_transducer_model(params) + + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + print( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + filename = params.exp_dir / f"iter-{params.iter}-avg-{params.avg}.pt" + torch.save({"model": model.state_dict()}, filename) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + print( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + filename = params.exp_dir / f"epoch-{params.epoch}-avg-{params.avg}.pt" + torch.save({"model": model.state_dict()}, filename) + + num_param = sum([p.numel() for p in model.parameters()]) + print(f"Number of model parameters: {num_param}") + + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained.py b/egs/librispeech/ASR/zipformer/jit_pretrained.py new file mode 100755 index 000000000..4092d165e --- /dev/null +++ b/egs/librispeech/ASR/zipformer/jit_pretrained.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads torchscript models, exported by `torch.jit.script()` +and uses them to decode waves. +You can use the following command to get the exported models: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +Usage of this script: + +./zipformer/jit_pretrained.py \ + --nn-model-filename ./zipformer/exp/cpu_jit.pt \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + /path/to/foo.wav \ + /path/to/bar.wav +""" + +import argparse +import logging +import math +from typing import List + +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model-filename", + type=str, + required=True, + help="Path to the torchscript model cpu_jit.pt", + ) + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to bpe.model.""", + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float = 16000 +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +def greedy_search( + model: torch.jit.ScriptModule, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, +) -> List[List[int]]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (N, T, C) + encoder_out_lens: + A 1-D tensor of shape (N,). + Returns: + Return the decoded results for each utterance. + """ + assert encoder_out.ndim == 3 + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + device = encoder_out.device + blank_id = 0 # hard-code to 0 + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + context_size = model.decoder.context_size + hyps = [[blank_id] * context_size for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + device=device, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.decoder( + decoder_input, + need_pad=torch.tensor([False]), + ).squeeze(1) + + offset = 0 + for batch_size in batch_size_list: + start = offset + end = offset + batch_size + current_encoder_out = packed_encoder_out.data[start:end] + current_encoder_out = current_encoder_out + # current_encoder_out's shape: (batch_size, encoder_out_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + + logits = model.joiner( + current_encoder_out, + decoder_out, + ) + # logits'shape (batch_size, vocab_size) + + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v != blank_id: + hyps[i].append(v) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + device=device, + dtype=torch.int64, + ) + decoder_out = model.decoder( + decoder_input, + need_pad=torch.tensor([False]), + ) + decoder_out = decoder_out.squeeze(1) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + model = torch.jit.load(args.nn_model_filename) + + model.eval() + + model.to(device) + + sp = spm.SentencePieceProcessor() + sp.load(args.bpe_model) + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = 16000 + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence( + features, + batch_first=True, + padding_value=math.log(1e-10), + ) + + feature_lengths = torch.tensor(feature_lengths, device=device) + + encoder_out, encoder_out_lens = model.encoder( + features=features, + feature_lengths=feature_lengths, + ) + + hyps = greedy_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = sp.decode(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py b/egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py new file mode 100755 index 000000000..58d736685 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +# flake8: noqa +# Copyright 2022-2023 Xiaomi Corp. (authors: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads torchscript models exported by `torch.jit.script()` +and uses them to decode waves. +You can use the following command to get the exported models: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +Usage of this script: + +./zipformer/jit_pretrained_streaming.py \ + --nn-model-filename ./zipformer/exp-causal/jit_script_chunk_16_left_128.pt \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + /path/to/foo.wav \ +""" + +import argparse +import logging +import math +from typing import List, Optional + +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from kaldifeat import FbankOptions, OnlineFbank, OnlineFeature +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model-filename", + type=str, + required=True, + help="Path to the torchscript model cpu_jit.pt", + ) + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to bpe.model.""", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + parser.add_argument( + "sound_file", + type=str, + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +def greedy_search( + decoder: torch.jit.ScriptModule, + joiner: torch.jit.ScriptModule, + encoder_out: torch.Tensor, + decoder_out: Optional[torch.Tensor] = None, + hyp: Optional[List[int]] = None, + device: torch.device = torch.device("cpu"), +): + assert encoder_out.ndim == 2 + context_size = 2 + blank_id = 0 + + if decoder_out is None: + assert hyp is None, hyp + hyp = [blank_id] * context_size + decoder_input = torch.tensor(hyp, dtype=torch.int32, device=device).unsqueeze(0) + # decoder_input.shape (1,, 1 context_size) + decoder_out = decoder(decoder_input, torch.tensor([False])).squeeze(1) + else: + assert decoder_out.ndim == 2 + assert hyp is not None, hyp + + T = encoder_out.size(0) + for i in range(T): + cur_encoder_out = encoder_out[i : i + 1] + joiner_out = joiner(cur_encoder_out, decoder_out).squeeze(0) + y = joiner_out.argmax(dim=0).item() + + if y != blank_id: + hyp.append(y) + decoder_input = hyp[-context_size:] + + decoder_input = torch.tensor( + decoder_input, dtype=torch.int32, device=device + ).unsqueeze(0) + decoder_out = decoder(decoder_input, torch.tensor([False])).squeeze(1) + + return hyp, decoder_out + + +def create_streaming_feature_extractor(sample_rate) -> OnlineFeature: + """Create a CPU streaming feature extractor. + + At present, we assume it returns a fbank feature extractor with + fixed options. In the future, we will support passing in the options + from outside. + + Returns: + Return a CPU streaming feature extractor. + """ + opts = FbankOptions() + opts.device = "cpu" + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = sample_rate + opts.mel_opts.num_bins = 80 + return OnlineFbank(opts) + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + model = torch.jit.load(args.nn_model_filename) + model.eval() + model.to(device) + + encoder = model.encoder + decoder = model.decoder + joiner = model.joiner + + sp = spm.SentencePieceProcessor() + sp.load(args.bpe_model) + + logging.info("Constructing Fbank computer") + online_fbank = create_streaming_feature_extractor(args.sample_rate) + + logging.info(f"Reading sound files: {args.sound_file}") + wave_samples = read_sound_files( + filenames=[args.sound_file], + expected_sample_rate=args.sample_rate, + )[0] + logging.info(wave_samples.shape) + + logging.info("Decoding started") + + chunk_length = encoder.chunk_size * 2 + T = chunk_length + encoder.pad_length + + logging.info(f"chunk_length: {chunk_length}") + logging.info(f"T: {T}") + + states = encoder.get_init_states(device=device) + + tail_padding = torch.zeros(int(0.3 * args.sample_rate), dtype=torch.float32) + + wave_samples = torch.cat([wave_samples, tail_padding]) + + chunk = int(0.25 * args.sample_rate) # 0.2 second + num_processed_frames = 0 + + hyp = None + decoder_out = None + + start = 0 + while start < wave_samples.numel(): + logging.info(f"{start}/{wave_samples.numel()}") + end = min(start + chunk, wave_samples.numel()) + samples = wave_samples[start:end] + start += chunk + online_fbank.accept_waveform( + sampling_rate=args.sample_rate, + waveform=samples, + ) + while online_fbank.num_frames_ready - num_processed_frames >= T: + frames = [] + for i in range(T): + frames.append(online_fbank.get_frame(num_processed_frames + i)) + frames = torch.cat(frames, dim=0).to(device).unsqueeze(0) + x_lens = torch.tensor([T], dtype=torch.int32, device=device) + encoder_out, out_lens, states = encoder( + features=frames, + feature_lengths=x_lens, + states=states, + ) + num_processed_frames += chunk_length + + hyp, decoder_out = greedy_search( + decoder, joiner, encoder_out.squeeze(0), decoder_out, hyp, device=device + ) + + context_size = 2 + logging.info(args.sound_file) + logging.info(sp.decode(hyp[context_size:])) + + logging.info("Decoding Done") + + +torch.set_num_threads(4) +torch.set_num_interop_threads(1) +torch._C._jit_set_profiling_executor(False) +torch._C._jit_set_profiling_mode(False) +torch._C._set_graph_executor_optimize(False) +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/joiner.py b/egs/librispeech/ASR/zipformer/joiner.py new file mode 100644 index 000000000..f03cc930e --- /dev/null +++ b/egs/librispeech/ASR/zipformer/joiner.py @@ -0,0 +1,66 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 +from scaling import ScaledLinear + + +class Joiner(nn.Module): + def __init__( + self, + encoder_dim: int, + decoder_dim: int, + joiner_dim: int, + vocab_size: int, + ): + super().__init__() + + self.encoder_proj = ScaledLinear(encoder_dim, joiner_dim, initial_scale=0.25) + self.decoder_proj = ScaledLinear(decoder_dim, joiner_dim, initial_scale=0.25) + self.output_linear = nn.Linear(joiner_dim, vocab_size) + + def forward( + self, + encoder_out: torch.Tensor, + decoder_out: torch.Tensor, + project_input: bool = True, + ) -> torch.Tensor: + """ + Args: + encoder_out: + Output from the encoder. Its shape is (N, T, s_range, C). + decoder_out: + Output from the decoder. Its shape is (N, T, s_range, C). + project_input: + If true, apply input projections encoder_proj and decoder_proj. + If this is false, it is the user's responsibility to do this + manually. + Returns: + Return a tensor of shape (N, T, s_range, C). + """ + assert encoder_out.ndim == decoder_out.ndim, (encoder_out.shape, decoder_out.shape) + + if project_input: + logit = self.encoder_proj(encoder_out) + self.decoder_proj( + decoder_out + ) + else: + logit = encoder_out + decoder_out + + logit = self.output_linear(torch.tanh(logit)) + + return logit diff --git a/egs/librispeech/ASR/zipformer/model.py b/egs/librispeech/ASR/zipformer/model.py new file mode 100644 index 000000000..7fcab04ae --- /dev/null +++ b/egs/librispeech/ASR/zipformer/model.py @@ -0,0 +1,217 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, Wei Kang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 k2 +import torch +import torch.nn as nn +from encoder_interface import EncoderInterface + +from icefall.utils import add_sos, make_pad_mask +from scaling import ScaledLinear + + +class Transducer(nn.Module): + """It implements https://arxiv.org/pdf/1211.3711.pdf + "Sequence Transduction with Recurrent Neural Networks" + """ + + def __init__( + self, + encoder_embed: nn.Module, + encoder: EncoderInterface, + decoder: nn.Module, + joiner: nn.Module, + encoder_dim: int, + decoder_dim: int, + joiner_dim: int, + vocab_size: int, + ): + """ + Args: + encoder_embed: + It is a Convolutional 2D subsampling module. It converts + an input of shape (N, T, idim) to an output of of shape + (N, T', odim), where T' = (T-3)//2-2 = (T-7)//2. + encoder: + It is the transcription network in the paper. Its accepts + two inputs: `x` of (N, T, encoder_dim) and `x_lens` of shape (N,). + It returns two tensors: `logits` of shape (N, T, encoder_dm) and + `logit_lens` of shape (N,). + decoder: + It is the prediction network in the paper. Its input shape + is (N, U) and its output shape is (N, U, decoder_dim). + It should contain one attribute: `blank_id`. + joiner: + It has two inputs with shapes: (N, T, encoder_dim) and (N, U, decoder_dim). + Its output shape is (N, T, U, vocab_size). Note that its output contains + unnormalized probs, i.e., not processed by log-softmax. + """ + super().__init__() + assert isinstance(encoder, EncoderInterface), type(encoder) + assert hasattr(decoder, "blank_id") + + self.encoder_embed = encoder_embed + self.encoder = encoder + self.decoder = decoder + self.joiner = joiner + + self.simple_am_proj = ScaledLinear( + encoder_dim, + vocab_size, + initial_scale=0.25, + ) + self.simple_lm_proj = ScaledLinear( + decoder_dim, + vocab_size, + initial_scale=0.25, + ) + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + y: k2.RaggedTensor, + prune_range: int = 5, + am_scale: float = 0.0, + lm_scale: float = 0.0, + ) -> torch.Tensor: + """ + Args: + x: + A 3-D tensor of shape (N, T, C). + x_lens: + A 1-D tensor of shape (N,). It contains the number of frames in `x` + before padding. + y: + A ragged tensor with 2 axes [utt][label]. It contains labels of each + utterance. + prune_range: + The prune range for rnnt loss, it means how many symbols(context) + we are considering for each frame to compute the loss. + am_scale: + The scale to smooth the loss with am (output of encoder network) + part + lm_scale: + The scale to smooth the loss with lm (output of predictor network) + part + Returns: + Return the transducer loss. + + Note: + Regarding am_scale & lm_scale, it will make the loss-function one of + the form: + lm_scale * lm_probs + am_scale * am_probs + + (1-lm_scale-am_scale) * combined_probs + """ + assert x.ndim == 3, x.shape + assert x_lens.ndim == 1, x_lens.shape + assert y.num_axes == 2, y.num_axes + + assert x.size(0) == x_lens.size(0) == y.dim0 + + # logging.info(f"Memory allocated at entry: {torch.cuda.memory_allocated() // 1000000}M") + x, x_lens = self.encoder_embed(x, x_lens) + # logging.info(f"Memory allocated after encoder_embed: {torch.cuda.memory_allocated() // 1000000}M") + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, x_lens = self.encoder(x, x_lens, src_key_padding_mask) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + assert torch.all(x_lens > 0) + + # Now for the decoder, i.e., the prediction network + row_splits = y.shape.row_splits(1) + y_lens = row_splits[1:] - row_splits[:-1] + + blank_id = self.decoder.blank_id + sos_y = add_sos(y, sos_id=blank_id) + + # sos_y_padded: [B, S + 1], start with SOS. + sos_y_padded = sos_y.pad(mode="constant", padding_value=blank_id) + + # decoder_out: [B, S + 1, decoder_dim] + decoder_out = self.decoder(sos_y_padded) + + # Note: y does not start with SOS + # y_padded : [B, S] + y_padded = y.pad(mode="constant", padding_value=0) + + y_padded = y_padded.to(torch.int64) + boundary = torch.zeros( + (encoder_out.size(0), 4), + dtype=torch.int64, + device=encoder_out.device, + ) + boundary[:, 2] = y_lens + boundary[:, 3] = x_lens + + lm = self.simple_lm_proj(decoder_out) + am = self.simple_am_proj(encoder_out) + + # if self.training and random.random() < 0.25: + # lm = penalize_abs_values_gt(lm, 100.0, 1.0e-04) + # if self.training and random.random() < 0.25: + # am = penalize_abs_values_gt(am, 30.0, 1.0e-04) + + with torch.cuda.amp.autocast(enabled=False): + simple_loss, (px_grad, py_grad) = k2.rnnt_loss_smoothed( + lm=lm.float(), + am=am.float(), + symbols=y_padded, + termination_symbol=blank_id, + lm_only_scale=lm_scale, + am_only_scale=am_scale, + boundary=boundary, + reduction="sum", + return_grad=True, + ) + + # ranges : [B, T, prune_range] + ranges = k2.get_rnnt_prune_ranges( + px_grad=px_grad, + py_grad=py_grad, + boundary=boundary, + s_range=prune_range, + ) + + # am_pruned : [B, T, prune_range, encoder_dim] + # lm_pruned : [B, T, prune_range, decoder_dim] + am_pruned, lm_pruned = k2.do_rnnt_pruning( + am=self.joiner.encoder_proj(encoder_out), + lm=self.joiner.decoder_proj(decoder_out), + ranges=ranges, + ) + + # logits : [B, T, prune_range, vocab_size] + + # project_input=False since we applied the decoder's input projections + # prior to do_rnnt_pruning (this is an optimization for speed). + logits = self.joiner(am_pruned, lm_pruned, project_input=False) + + with torch.cuda.amp.autocast(enabled=False): + pruned_loss = k2.rnnt_loss_pruned( + logits=logits.float(), + symbols=y_padded, + ranges=ranges, + termination_symbol=blank_id, + boundary=boundary, + reduction="sum", + ) + + return (simple_loss, pruned_loss) diff --git a/egs/librispeech/ASR/zipformer/optim.py b/egs/librispeech/ASR/zipformer/optim.py new file mode 100644 index 000000000..abfb2092c --- /dev/null +++ b/egs/librispeech/ASR/zipformer/optim.py @@ -0,0 +1,1173 @@ +# Copyright 2022 Xiaomi Corp. (authors: Daniel Povey) +# +# See ../LICENSE for clarification regarding multiple authors +# +# 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 logging +import random +from collections import defaultdict +from typing import Dict, List, Optional, Tuple, Union + +import torch +from lhotse.utils import fix_random_seed +from torch import Tensor +from torch.optim import Optimizer + + +class BatchedOptimizer(Optimizer): + """ + This class adds to class Optimizer the capability to optimize parameters in batches: + it will stack the parameters and their grads for you so the optimizer can work + on tensors with an extra leading dimension. This is intended for speed with GPUs, + as it reduces the number of kernels launched in the optimizer. + + Args: + params: + """ + + def __init__(self, params, defaults): + super(BatchedOptimizer, self).__init__(params, defaults) + + @contextlib.contextmanager + def batched_params(self, param_group, group_params_names): + """ + This function returns (technically, yields) a list of + of tuples (p, state), where + p is a `fake` parameter that is stacked (over axis 0) from real parameters + that share the same shape, and its gradient is also stacked; + `state` is the state corresponding to this batch of parameters + (it will be physically located in the "state" for one of the real + parameters, the last one that has any particular shape and dtype). + + This function is decorated as a context manager so that it can + write parameters back to their "real" locations. + + The idea is, instead of doing: + + for p in group["params"]: + state = self.state[p] + ... + + you can do: + + with self.batched_params(group["params"]) as batches: + for p, state, p_names in batches: + ... + + + Args: + group: a parameter group, which is a list of parameters; should be + one of self.param_groups. + group_params_names: name for each parameter in group, + which is List[str]. + """ + batches = defaultdict( + list + ) # `batches` maps from tuple (dtype_as_str,*shape) to list of nn.Parameter + batches_names = defaultdict( + list + ) # `batches` maps from tuple (dtype_as_str,*shape) to list of str + + assert len(param_group) == len(group_params_names) + for p, named_p in zip(param_group, group_params_names): + key = (str(p.dtype), *p.shape) + batches[key].append(p) + batches_names[key].append(named_p) + + batches_names_keys = list(batches_names.keys()) + sorted_idx = sorted( + range(len(batches_names)), key=lambda i: batches_names_keys[i] + ) + batches_names = [batches_names[batches_names_keys[idx]] for idx in sorted_idx] + batches = [batches[batches_names_keys[idx]] for idx in sorted_idx] + + stacked_params_dict = dict() + + # turn batches into a list, in deterministic order. + # tuples will contain tuples of (stacked_param, state, stacked_params_names), + # one for each batch in `batches`. + tuples = [] + + for batch, batch_names in zip(batches, batches_names): + p = batch[0] + # we arbitrarily store the state in the + # state corresponding to the 1st parameter in the + # group. class Optimizer will take care of saving/loading state. + state = self.state[p] + p_stacked = torch.stack(batch) + grad = torch.stack( + [torch.zeros_like(p) if p.grad is None else p.grad for p in batch] + ) + p_stacked.grad = grad + stacked_params_dict[key] = p_stacked + tuples.append((p_stacked, state, batch_names)) + + yield tuples # <-- calling code will do the actual optimization here! + + for ((stacked_params, _state, _names), batch) in zip(tuples, batches): + for i, p in enumerate(batch): # batch is list of Parameter + p.copy_(stacked_params[i]) + + +class ScaledAdam(BatchedOptimizer): + """ + Implements 'Scaled Adam', a variant of Adam where we scale each parameter's update + proportional to the norm of that parameter; and also learn the scale of the parameter, + in log space, subject to upper and lower limits (as if we had factored each parameter as + param = underlying_param * log_scale.exp()) + + + Args: + params: The parameters or param_groups to optimize (like other Optimizer subclasses) + Unlike common optimizers, which accept model.parameters() or groups of parameters(), + this optimizer could accept model.named_parameters() or groups of named_parameters(). + See comments of function _get_names_of_parameters for its 4 possible cases. + lr: The learning rate. We will typically use a learning rate schedule that starts + at 0.03 and decreases over time, i.e. much higher than other common + optimizers. + clipping_scale: (e.g. 2.0) + A scale for gradient-clipping: if specified, the normalized gradients + over the whole model will be clipped to have 2-norm equal to + `clipping_scale` times the median 2-norm over the most recent period + of `clipping_update_period` minibatches. By "normalized gradients", + we mean after multiplying by the rms parameter value for this tensor + [for non-scalars]; this is appropriate because our update is scaled + by this quantity. + betas: beta1,beta2 are momentum constants for regular momentum, and moving sum-sq grad. + Must satisfy 0 < beta <= beta2 < 1. + scalar_lr_scale: A scaling factor on the learning rate, that we use to update the + scale of each parameter tensor and scalar parameters of the mode.. + If each parameter were decomposed + as p * p_scale.exp(), where (p**2).mean().sqrt() == 1.0, scalar_lr_scale + would be a the scaling factor on the learning rate of p_scale. + eps: A general-purpose epsilon to prevent division by zero + param_min_rms: Minimum root-mean-square value of parameter tensor, for purposes of + learning the scale on the parameters (we'll constrain the rms of each non-scalar + parameter tensor to be >= this value) + param_max_rms: Maximum root-mean-square value of parameter tensor, for purposes of + learning the scale on the parameters (we'll constrain the rms of each non-scalar + parameter tensor to be <= this value) + scalar_max: Maximum absolute value for scalar parameters (applicable if your + model has any parameters with numel() == 1). + size_update_period: The periodicity, in steps, with which we update the size (scale) + of the parameter tensor. This is provided to save a little time + in the update. + clipping_update_period: if clipping_scale is specified, this is the period + """ + + def __init__( + self, + params, + lr=3e-02, + clipping_scale=None, + betas=(0.9, 0.98), + scalar_lr_scale=0.1, + eps=1.0e-08, + param_min_rms=1.0e-05, + param_max_rms=3.0, + scalar_max=10.0, + size_update_period=4, + clipping_update_period=100, + ): + + defaults = dict( + lr=lr, + clipping_scale=clipping_scale, + betas=betas, + scalar_lr_scale=scalar_lr_scale, + eps=eps, + param_min_rms=param_min_rms, + param_max_rms=param_max_rms, + scalar_max=scalar_max, + size_update_period=size_update_period, + clipping_update_period=clipping_update_period, + ) + + # If params only contains parameters or group of parameters, + # i.e when parameter names are not given, + # this flag will be set to False in funciton _get_names_of_parameters. + self.show_dominant_parameters = True + param_groups, parameters_names = self._get_names_of_parameters(params) + super(ScaledAdam, self).__init__(param_groups, defaults) + assert len(self.param_groups) == len(parameters_names) + self.parameters_names = parameters_names + + def _get_names_of_parameters( + self, params_or_named_params + ) -> Tuple[List[Dict], List[List[str]]]: + """ + Args: + params_or_named_params: according to the way ScaledAdam is initialized in train.py, + this argument could be one of following 4 cases, + case 1, a generator of parameter, e.g.: + optimizer = ScaledAdam(model.parameters(), lr=params.base_lr, clipping_scale=3.0) + + case 2, a list of parameter groups with different config, e.g.: + model_param_groups = [ + {'params': model.encoder.parameters(), 'lr': 0.05}, + {'params': model.decoder.parameters(), 'lr': 0.01}, + {'params': model.joiner.parameters(), 'lr': 0.03}, + ] + optimizer = ScaledAdam(model_param_groups, lr=params.base_lr, clipping_scale=3.0) + + case 3, a generator of named_parameter, e.g.: + optimizer = ScaledAdam(model.named_parameters(), lr=params.base_lr, clipping_scale=3.0) + + case 4, a list of named_parameter groups with different config, e.g.: + model_named_param_groups = [ + {'named_params': model.encoder.named_parameters(), 'lr': 0.05}, + {'named_params': model.decoder.named_parameters(), 'lr': 0.01}, + {'named_params': model.joiner.named_parameters(), 'lr': 0.03}, + ] + optimizer = ScaledAdam(model_named_param_groups, lr=params.base_lr, clipping_scale=3.0) + + For case 1 and case 2, input params is used to initialize the underlying torch.optimizer. + For case 3 and case 4, firstly, names and params are extracted from input named_params, + then, these extracted params are used to initialize the underlying torch.optimizer, + and these extracted names are mainly used by function + `_show_gradient_dominating_parameter` + + Returns: + Returns a tuple containing 2 elements: + - `param_groups` with type List[Dict], each Dict element is a parameter group. + An example of `param_groups` could be: + [ + {'params': `one iterable of Parameter`, 'lr': 0.05}, + {'params': `another iterable of Parameter`, 'lr': 0.08}, + {'params': `a third iterable of Parameter`, 'lr': 0.1}, + ] + - `param_gruops_names` with type List[List[str]], + each `List[str]` is for a group['params'] in param_groups, + and each `str` is the name of a parameter. + A dummy name "foo" is related to each parameter, + if input are params without names, i.e. case 1 or case 2. + """ + # variable naming convention in this function: + # p is short for param. + # np is short for named_param. + # p_or_np is short for param_or_named_param. + # cur is short for current. + # group is a dict, e.g. {'params': iterable of parameter, 'lr': 0.05, other fields}. + # groups is a List[group] + + iterable_or_groups = list(params_or_named_params) + if len(iterable_or_groups) == 0: + raise ValueError("optimizer got an empty parameter list") + + # The first value of returned tuple. A list of dicts containing at + # least 'params' as a key. + param_groups = [] + + # The second value of returned tuple, + # a List[List[str]], each sub-List is for a group. + param_groups_names = [] + + if not isinstance(iterable_or_groups[0], dict): + # case 1 or case 3, + # the input is an iterable of parameter or named parameter. + param_iterable_cur_group = [] + param_names_cur_group = [] + for p_or_np in iterable_or_groups: + if isinstance(p_or_np, tuple): + # case 3 + name, param = p_or_np + else: + # case 1 + assert isinstance(p_or_np, torch.Tensor) + param = p_or_np + # Assign a dummy name as a placeholder + name = "foo" + self.show_dominant_parameters = False + param_iterable_cur_group.append(param) + param_names_cur_group.append(name) + param_groups.append({"params": param_iterable_cur_group}) + param_groups_names.append(param_names_cur_group) + else: + # case 2 or case 4 + # the input is groups of parameter or named parameter. + for cur_group in iterable_or_groups: + assert "named_params" in cur_group + name_list = [ x[0] for x in cur_group["named_params"] ] + p_list = [ x[1] for x in cur_group["named_params"] ] + del cur_group["named_params"] + cur_group["params"] = p_list + param_groups.append(cur_group) + param_groups_names.append(name_list) + + return param_groups, param_groups_names + + def __setstate__(self, state): + super(ScaledAdam, self).__setstate__(state) + + @torch.no_grad() + def step(self, closure=None): + """Performs a single optimization step. + + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + batch = True + + for group, group_params_names in zip(self.param_groups, self.parameters_names): + + with self.batched_params(group["params"], group_params_names) as batches: + + # batches is list of pairs (stacked_param, state). stacked_param is like + # a regular parameter, and will have a .grad, but the 1st dim corresponds to + # a stacking dim, it is not a real dim. + + if ( + len(batches[0][1]) == 0 + ): # if len(first state) == 0: not yet initialized + clipping_scale = 1 + else: + clipping_scale = self._get_clipping_scale(group, batches) + + for p, state, _ in batches: + # Perform optimization step. + # grad is not going to be None, we handled that when creating the batches. + grad = p.grad + if grad.is_sparse: + raise RuntimeError( + "ScaledAdam optimizer does not support sparse gradients" + ) + # State initialization + if len(state) == 0: + self._init_state(group, p, state) + + self._step_one_batch(group, p, state, clipping_scale) + + return loss + + def _init_state(self, group: dict, p: Tensor, state: dict): + """ + Initializes state dict for parameter 'p'. Assumes that dim 0 of tensor p + is actually the batch dimension, corresponding to batched-together + parameters of a given shape. + + + Args: + group: Dict to look up configuration values. + p: The parameter that we are initializing the state for + state: Dict from string to whatever state we are initializing + """ + size_update_period = group["size_update_period"] + + state["step"] = 0 + + kwargs = {"device": p.device, "dtype": p.dtype} + + # 'delta' implements conventional momentum. There are + # several different kinds of update going on, so rather than + # compute "exp_avg" like in Adam, we store and decay a + # parameter-change "delta", which combines all forms of + # update. this is equivalent to how it's done in Adam, + # except for the first few steps. + state["delta"] = torch.zeros_like(p, memory_format=torch.preserve_format) + + batch_size = p.shape[0] + numel = p.numel() // batch_size + + if numel > 1: + # "param_rms" just periodically records the scalar root-mean-square value of + # the parameter tensor. + # it has a shape like (batch_size, 1, 1, 1, 1) + param_rms = (p**2).mean(dim=list(range(1, p.ndim)), keepdim=True).sqrt() + state["param_rms"] = param_rms + + state["scale_exp_avg_sq"] = torch.zeros_like(param_rms) + state["scale_grads"] = torch.zeros( + size_update_period, *param_rms.shape, **kwargs + ) + + # exp_avg_sq is the weighted sum of scaled gradients. as in Adam. + state["exp_avg_sq"] = torch.zeros_like(p, memory_format=torch.preserve_format) + + def _get_clipping_scale( + self, group: dict, tuples: List[Tuple[Tensor, dict, List[str]]] + ) -> float: + """ + Returns a scalar factor <= 1.0 that dictates gradient clipping, i.e. we will scale the gradients + by this amount before applying the rest of the update. + + Args: + group: the parameter group, an item in self.param_groups + tuples: a list of tuples of (param, state, param_names) + where param is a batched set of parameters, + with a .grad (1st dim is batch dim) + and state is the state-dict where optimization parameters are kept. + param_names is a List[str] while each str is name for a parameter + in batched set of parameters "param". + """ + assert len(tuples) >= 1 + clipping_scale = group["clipping_scale"] + (first_p, first_state, _) = tuples[0] + step = first_state["step"] + if clipping_scale is None or step == 0: + # no clipping. return early on step == 0 because the other + # parameters' state won't have been initialized yet. + return 1.0 + clipping_update_period = group["clipping_update_period"] + + tot_sumsq = torch.tensor(0.0, device=first_p.device) + for (p, state, param_names) in tuples: + grad = p.grad + if grad.is_sparse: + raise RuntimeError( + "ScaledAdam optimizer does not support sparse gradients" + ) + if p.numel() == p.shape[0]: # a batch of scalars + tot_sumsq += (grad**2).sum() # sum() to change shape [1] to [] + else: + tot_sumsq += ((grad * state["param_rms"]) ** 2).sum() + + tot_norm = tot_sumsq.sqrt() + if "model_norms" not in first_state: + first_state["model_norms"] = torch.zeros( + clipping_update_period, device=p.device + ) + first_state["model_norms"][step % clipping_update_period] = tot_norm + + if step % clipping_update_period == 0: + # Print some stats. + # We don't reach here if step == 0 because we would have returned + # above. + sorted_norms = first_state["model_norms"].sort()[0].to("cpu") + quartiles = [] + for n in range(0, 5): + index = min( + clipping_update_period - 1, (clipping_update_period // 4) * n + ) + quartiles.append(sorted_norms[index].item()) + + median = quartiles[2] + threshold = clipping_scale * median + first_state["model_norm_threshold"] = threshold + percent_clipped = ( + first_state["num_clipped"] * 100.0 / clipping_update_period + if "num_clipped" in first_state + else 0.0 + ) + first_state["num_clipped"] = 0 + quartiles = " ".join(["%.3e" % x for x in quartiles]) + logging.info( + f"Clipping_scale={clipping_scale}, grad-norm quartiles {quartiles}, " + f"threshold={threshold:.3e}, percent-clipped={percent_clipped:.1f}" + ) + + if step < clipping_update_period: + return 1.0 # We have not yet estimated a norm to clip to. + else: + try: + model_norm_threshold = first_state["model_norm_threshold"] + except KeyError: + logging.info( + "Warning: model_norm_threshold not in state: possibly " + "you changed config when restarting, adding clipping_scale option?" + ) + return 1.0 + ans = min(1.0, (model_norm_threshold / (tot_norm + 1.0e-20)).item()) + if ans < 1.0: + first_state["num_clipped"] += 1 + if ans < 0.1: + logging.warn( + f"Scaling gradients by {ans}, model_norm_threshold={model_norm_threshold}" + ) + if self.show_dominant_parameters: + assert p.shape[0] == len(param_names) + self._show_gradient_dominating_parameter(tuples, tot_sumsq) + return ans + + def _show_gradient_dominating_parameter( + self, tuples: List[Tuple[Tensor, dict, List[str]]], tot_sumsq: Tensor + ): + """ + Show information of parameter which dominates tot_sumsq. + + Args: + tuples: a list of tuples of (param, state, param_names) + where param is a batched set of parameters, + with a .grad (1st dim is batch dim) + and state is the state-dict where optimization parameters are kept. + param_names is a List[str] while each str is name for a parameter + in batched set of parameters "param". + tot_sumsq: sumsq of all parameters. Though it's could be calculated + from tuples, we still pass it to save some time. + """ + all_sumsq_orig = {} + for (p, state, batch_param_names) in tuples: + # p is a stacked batch parameters. + batch_grad = p.grad + if p.numel() == p.shape[0]: # a batch of scalars + batch_sumsq_orig = batch_grad**2 + # Dummy values used by following `zip` statement. + batch_rms_orig = torch.ones(p.shape[0]) + else: + batch_rms_orig = state["param_rms"] + batch_sumsq_orig = ((batch_grad * batch_rms_orig) ** 2).sum( + dim=list(range(1, batch_grad.ndim)) + ) + + for name, sumsq_orig, rms, grad in zip( + batch_param_names, batch_sumsq_orig, batch_rms_orig, batch_grad + ): + + proportion_orig = sumsq_orig / tot_sumsq + all_sumsq_orig[name] = (proportion_orig, sumsq_orig, rms, grad) + + assert torch.isclose( + sum([value[0] for value in all_sumsq_orig.values()]).cpu(), + torch.tensor(1.0), + ) + sorted_by_proportion = { + k: v + for k, v in sorted( + all_sumsq_orig.items(), key=lambda item: item[1][0], reverse=True + ) + } + dominant_param_name = next(iter(sorted_by_proportion)) + ( + dominant_proportion, + dominant_sumsq, + dominant_rms, + dominant_grad, + ) = sorted_by_proportion[dominant_param_name] + logging.info( + f"Parameter dominating tot_sumsq {dominant_param_name}" + f" with proportion {dominant_proportion:.2f}," + f" where dominant_sumsq=(grad_sumsq*orig_rms_sq)" + f"={dominant_sumsq:.3e}," + f" grad_sumsq={(dominant_grad**2).sum():.3e}," + f" orig_rms_sq={(dominant_rms**2).item():.3e}" + ) + + def _step_one_batch( + self, group: dict, p: Tensor, state: dict, clipping_scale: float + ): + """ + Do the step for one parameter, which is actually going to be a batch of + `real` parameters, with dim 0 as the batch dim. + Args: + group: dict to look up configuration values + p: parameter to update (actually multiple parameters stacked together + as a batch) + state: state-dict for p, to look up the optimizer state + """ + lr = group["lr"] + size_update_period = group["size_update_period"] + beta1 = group["betas"][0] + + grad = p.grad + if clipping_scale != 1.0: + grad = grad * clipping_scale + step = state["step"] + delta = state["delta"] + + delta.mul_(beta1) + batch_size = p.shape[0] + numel = p.numel() // batch_size + if numel > 1: + # Update the size/scale of p, and set param_rms + scale_grads = state["scale_grads"] + scale_grads[step % size_update_period] = (p * grad).sum( + dim=list(range(1, p.ndim)), keepdim=True + ) + if step % size_update_period == size_update_period - 1: + param_rms = state["param_rms"] # shape: (batch_size, 1, 1, ..) + param_rms.copy_( + (p**2).mean(dim=list(range(1, p.ndim)), keepdim=True).sqrt() + ) + if step > 0: + # self._size_update() learns the overall scale on the + # parameter, by shrinking or expanding it. + self._size_update(group, scale_grads, p, state) + + if numel == 1: + # For parameters with 1 element we just use regular Adam. + # Updates delta. + self._step_scalar(group, p, state) + else: + self._step(group, p, state) + + state["step"] = step + 1 + + def _size_update( + self, group: dict, scale_grads: Tensor, p: Tensor, state: dict + ) -> None: + """ + Called only where p.numel() > 1, this updates the scale of the parameter. + If we imagine: p = underlying_param * scale.exp(), and we are doing + gradient descent on underlying param and on scale, this function does the update + on `scale`. + + Args: + group: dict to look up configuration values + scale_grads: a tensor of shape (size_update_period, batch_size, 1, 1,...) containing + grads w.r.t. the scales. + p: The parameter to update + state: The state-dict of p + """ + + param_rms = state["param_rms"] + beta1, beta2 = group["betas"] + size_lr = group["lr"] * group["scalar_lr_scale"] + param_min_rms = group["param_min_rms"] + param_max_rms = group["param_max_rms"] + eps = group["eps"] + step = state["step"] + batch_size = p.shape[0] + + size_update_period = scale_grads.shape[0] + # correct beta2 for the size update period: we will have + # faster decay at this level. + beta2_corr = beta2**size_update_period + + scale_exp_avg_sq = state["scale_exp_avg_sq"] # shape: (batch_size, 1, 1, ..) + scale_exp_avg_sq.mul_(beta2_corr).add_( + (scale_grads**2).mean(dim=0), # mean over dim `size_update_period` + alpha=1 - beta2_corr, + ) # shape is (batch_size, 1, 1, ...) + + # The 1st time we reach here is when size_step == 1. + size_step = (step + 1) // size_update_period + bias_correction2 = 1 - beta2_corr**size_step + # we don't bother with bias_correction1; this will help prevent divergence + # at the start of training. + + denom = scale_exp_avg_sq.sqrt() + eps + + scale_step = ( + -size_lr * (bias_correction2**0.5) * scale_grads.sum(dim=0) / denom + ) + + is_too_small = param_rms < param_min_rms + + # when the param gets too small, just don't shrink it any further. + scale_step.masked_fill_(is_too_small, 0.0) + + # and ensure the parameter rms after update never exceeds param_max_rms. + # We have to look at the trained model for parameters at or around the + # param_max_rms, because sometimes they can indicate a problem with the + # topology or settings. + scale_step = torch.minimum(scale_step, + (param_max_rms - param_rms) / param_rms) + + delta = state["delta"] + # the factor of (1-beta1) relates to momentum. + delta.add_(p * scale_step, alpha=(1 - beta1)) + + def _step(self, group: dict, p: Tensor, state: dict): + """ + This function does the core update of self.step(), in the case where the members of + the batch have more than 1 element. + + Args: + group: A dict which will be used to look up configuration values + p: The parameter to be updated + grad: The grad of p + state: The state-dict corresponding to parameter p + + This function modifies p. + """ + grad = p.grad + lr = group["lr"] + beta1, beta2 = group["betas"] + eps = group["eps"] + param_min_rms = group["param_min_rms"] + step = state["step"] + + exp_avg_sq = state["exp_avg_sq"] + exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=(1 - beta2)) + + this_step = state["step"] - (state["zero_step"] if "zero_step" in state else 0) + bias_correction2 = 1 - beta2 ** (this_step + 1) + if bias_correction2 < 0.99: + # note: not in-place. + exp_avg_sq = exp_avg_sq * (1.0 / bias_correction2) + + denom = exp_avg_sq.sqrt() + denom += eps + grad = grad / denom + + alpha = -lr * (1 - beta1) * state["param_rms"].clamp(min=param_min_rms) + + delta = state["delta"] + delta.add_(grad * alpha) + p.add_(delta) + + def _step_scalar(self, group: dict, p: Tensor, state: dict): + """ + A simplified form of the core update for scalar tensors, where we cannot get a good + estimate of the parameter rms. + """ + beta1, beta2 = group["betas"] + scalar_max = group["scalar_max"] + eps = group["eps"] + lr = group["lr"] * group["scalar_lr_scale"] + grad = p.grad + + exp_avg_sq = state["exp_avg_sq"] # shape: (batch_size,) + exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) + + # bias_correction2 is like in Adam. Don't bother with bias_correction1; + # slower update at the start will help stability anyway. + bias_correction2 = 1 - beta2 ** (state["step"] + 1) + denom = (exp_avg_sq / bias_correction2).sqrt() + eps + + delta = state["delta"] + delta.add_(grad / denom, alpha=-lr * (1 - beta1)) + p.clamp_(min=-scalar_max, max=scalar_max) + p.add_(delta) + + +class LRScheduler(object): + """ + Base-class for learning rate schedulers where the learning-rate depends on both the + batch and the epoch. + """ + + def __init__(self, optimizer: Optimizer, verbose: bool = False): + # Attach optimizer + if not isinstance(optimizer, Optimizer): + raise TypeError("{} is not an Optimizer".format(type(optimizer).__name__)) + self.optimizer = optimizer + self.verbose = verbose + + for group in optimizer.param_groups: + group.setdefault("base_lr", group["lr"]) + + self.base_lrs = [group["base_lr"] for group in optimizer.param_groups] + + self.epoch = 0 + self.batch = 0 + + def state_dict(self): + """Returns the state of the scheduler as a :class:`dict`. + + It contains an entry for every variable in self.__dict__ which + is not the optimizer. + """ + return { + "base_lrs": self.base_lrs, + "epoch": self.epoch, + "batch": self.batch, + } + + def load_state_dict(self, state_dict): + """Loads the schedulers state. + + Args: + state_dict (dict): scheduler state. Should be an object returned + from a call to :meth:`state_dict`. + """ + self.__dict__.update(state_dict) + + def get_last_lr(self) -> List[float]: + """Return last computed learning rate by current scheduler. Will be a list of float.""" + return self._last_lr + + def get_lr(self): + # Compute list of learning rates from self.epoch and self.batch and + # self.base_lrs; this must be overloaded by the user. + # e.g. return [some_formula(self.batch, self.epoch, base_lr) for base_lr in self.base_lrs ] + raise NotImplementedError + + def step_batch(self, batch: Optional[int] = None) -> None: + # Step the batch index, or just set it. If `batch` is specified, it + # must be the batch index from the start of training, i.e. summed over + # all epochs. + # You can call this in any order; if you don't provide 'batch', it should + # of course be called once per batch. + if batch is not None: + self.batch = batch + else: + self.batch = self.batch + 1 + self._set_lrs() + + def step_epoch(self, epoch: Optional[int] = None): + # Step the epoch index, or just set it. If you provide the 'epoch' arg, + # you should call this at the start of the epoch; if you don't provide the 'epoch' + # arg, you should call it at the end of the epoch. + if epoch is not None: + self.epoch = epoch + else: + self.epoch = self.epoch + 1 + self._set_lrs() + + def _set_lrs(self): + values = self.get_lr() + assert len(values) == len(self.optimizer.param_groups) + + for i, data in enumerate(zip(self.optimizer.param_groups, values)): + param_group, lr = data + param_group["lr"] = lr + self.print_lr(self.verbose, i, lr) + self._last_lr = [group["lr"] for group in self.optimizer.param_groups] + + def print_lr(self, is_verbose, group, lr): + """Display the current learning rate.""" + if is_verbose: + logging.info( + f"Epoch={self.epoch}, batch={self.batch}: adjusting learning rate" + f" of group {group} to {lr:.4e}." + ) + + +class Eden(LRScheduler): + """ + Eden scheduler. + The basic formula (before warmup) is: + lr = base_lr * (((batch**2 + lr_batches**2) / lr_batches**2) ** -0.25 * + (((epoch**2 + lr_epochs**2) / lr_epochs**2) ** -0.25)) * warmup + where `warmup` increases from linearly 0.5 to 1 over `warmup_batches` batches + and then stays constant at 1. + + + E.g. suggest base_lr = 0.04 (passed to optimizer) if used with ScaledAdam + + Args: + optimizer: the optimizer to change the learning rates on + lr_batches: the number of batches after which we start significantly + decreasing the learning rate, suggest 5000. + lr_epochs: the number of epochs after which we start significantly + decreasing the learning rate, suggest 6 if you plan to do e.g. + 20 to 40 epochs, but may need smaller number if dataset is huge + and you will do few epochs. + """ + + def __init__( + self, + optimizer: Optimizer, + lr_batches: Union[int, float], + lr_epochs: Union[int, float], + warmup_batches: Union[int, float] = 500.0, + warmup_start: float = 0.5, + verbose: bool = False, + ): + super(Eden, self).__init__(optimizer, verbose) + self.lr_batches = lr_batches + self.lr_epochs = lr_epochs + self.warmup_batches = warmup_batches + + assert 0.0 <= warmup_start <= 1.0, warmup_start + self.warmup_start = warmup_start + + def get_lr(self): + factor = ( + (self.batch**2 + self.lr_batches**2) / self.lr_batches**2 + ) ** -0.25 * ( + ((self.epoch**2 + self.lr_epochs**2) / self.lr_epochs**2) ** -0.25 + ) + warmup_factor = ( + 1.0 + if self.batch >= self.warmup_batches + else self.warmup_start + (1.0 - self.warmup_start) * (self.batch / self.warmup_batches) + # else 0.5 + 0.5 * (self.batch / self.warmup_batches) + ) + + return [x * factor * warmup_factor for x in self.base_lrs] + + +def _test_eden(): + m = torch.nn.Linear(100, 100) + optim = ScaledAdam(m.parameters(), lr=0.03) + + scheduler = Eden(optim, lr_batches=100, lr_epochs=2, verbose=True) + + for epoch in range(10): + scheduler.step_epoch(epoch) # sets epoch to `epoch` + + for step in range(20): + x = torch.randn(200, 100).detach() + x.requires_grad = True + y = m(x) + dy = torch.randn(200, 100).detach() + f = (y * dy).sum() + f.backward() + + optim.step() + scheduler.step_batch() + optim.zero_grad() + + logging.info(f"last lr = {scheduler.get_last_lr()}") + logging.info(f"state dict = {scheduler.state_dict()}") + + +# This is included mostly as a baseline for ScaledAdam. +class Eve(Optimizer): + """ + Implements Eve algorithm. This is a modified version of AdamW with a special + way of setting the weight-decay / shrinkage-factor, which is designed to make the + rms of the parameters approach a particular target_rms (default: 0.1). This is + for use with networks with 'scaled' versions of modules (see scaling.py), which + will be close to invariant to the absolute scale on the parameter matrix. + + The original Adam algorithm was proposed in `Adam: A Method for Stochastic Optimization`_. + The AdamW variant was proposed in `Decoupled Weight Decay Regularization`_. + Eve is unpublished so far. + + Arguments: + params (iterable): iterable of parameters to optimize or dicts defining + parameter groups + lr (float, optional): learning rate (default: 1e-3) + betas (Tuple[float, float], optional): coefficients used for computing + running averages of gradient and its square (default: (0.9, 0.999)) + eps (float, optional): term added to the denominator to improve + numerical stability (default: 1e-8) + weight_decay (float, optional): weight decay coefficient (default: 3e-4; + this value means that the weight would decay significantly after + about 3k minibatches. Is not multiplied by learning rate, but + is conditional on RMS-value of parameter being > target_rms. + target_rms (float, optional): target root-mean-square value of + parameters, if they fall below this we will stop applying weight decay. + + + .. _Adam: A Method for Stochastic Optimization: + https://arxiv.org/abs/1412.6980 + .. _Decoupled Weight Decay Regularization: + https://arxiv.org/abs/1711.05101 + .. _On the Convergence of Adam and Beyond: + https://openreview.net/forum?id=ryQu7f-RZ + """ + + def __init__( + self, + params, + lr=1e-3, + betas=(0.9, 0.98), + eps=1e-8, + weight_decay=1e-3, + target_rms=0.1, + ): + if not 0.0 <= lr: + raise ValueError("Invalid learning rate: {}".format(lr)) + if not 0.0 <= eps: + raise ValueError("Invalid epsilon value: {}".format(eps)) + if not 0.0 <= betas[0] < 1.0: + raise ValueError("Invalid beta parameter at index 0: {}".format(betas[0])) + if not 0.0 <= betas[1] < 1.0: + raise ValueError("Invalid beta parameter at index 1: {}".format(betas[1])) + if not 0 <= weight_decay <= 0.1: + raise ValueError("Invalid weight_decay value: {}".format(weight_decay)) + if not 0 < target_rms <= 10.0: + raise ValueError("Invalid target_rms value: {}".format(target_rms)) + defaults = dict( + lr=lr, + betas=betas, + eps=eps, + weight_decay=weight_decay, + target_rms=target_rms, + ) + super(Eve, self).__init__(params, defaults) + + def __setstate__(self, state): + super(Eve, self).__setstate__(state) + + @torch.no_grad() + def step(self, closure=None): + """Performs a single optimization step. + + Arguments: + closure (callable, optional): A closure that reevaluates the model + and returns the loss. + """ + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + for group in self.param_groups: + for p in group["params"]: + if p.grad is None: + continue + + # Perform optimization step + grad = p.grad + if grad.is_sparse: + raise RuntimeError("AdamW does not support sparse gradients") + + state = self.state[p] + + # State initialization + if len(state) == 0: + state["step"] = 0 + # Exponential moving average of gradient values + state["exp_avg"] = torch.zeros_like( + p, memory_format=torch.preserve_format + ) + # Exponential moving average of squared gradient values + state["exp_avg_sq"] = torch.zeros_like( + p, memory_format=torch.preserve_format + ) + + exp_avg, exp_avg_sq = state["exp_avg"], state["exp_avg_sq"] + + beta1, beta2 = group["betas"] + + state["step"] += 1 + bias_correction1 = 1 - beta1 ** state["step"] + bias_correction2 = 1 - beta2 ** state["step"] + + # Decay the first and second moment running average coefficient + exp_avg.mul_(beta1).add_(grad, alpha=1 - beta1) + exp_avg_sq.mul_(beta2).addcmul_(grad, grad, value=1 - beta2) + denom = (exp_avg_sq.sqrt() * (bias_correction2**-0.5)).add_( + group["eps"] + ) + + step_size = group["lr"] / bias_correction1 + target_rms = group["target_rms"] + weight_decay = group["weight_decay"] + + if p.numel() > 1: + # avoid applying this weight-decay on "scaling factors" + # (which are scalar). + is_above_target_rms = p.norm() > (target_rms * (p.numel() ** 0.5)) + p.mul_(1 - (weight_decay * is_above_target_rms)) + + p.addcdiv_(exp_avg, denom, value=-step_size) + + if random.random() < 0.0005: + step = (exp_avg / denom) * step_size + logging.info( + f"Delta rms = {(step**2).mean().item()}, shape = {step.shape}" + ) + + return loss + + +def _test_scaled_adam(hidden_dim: int): + import timeit + + from scaling import ScaledLinear + + E = 100 + B = 4 + T = 2 + logging.info("in test_eve_cain") + # device = torch.device('cuda') + device = torch.device("cpu") + dtype = torch.float32 + + fix_random_seed(42) + # these input_magnitudes and output_magnitudes are to test that + # Abel is working as we expect and is able to adjust scales of + # different dims differently. + input_magnitudes = (1.0 * torch.randn(E, dtype=dtype, device=device)).exp() + output_magnitudes = (1.0 * torch.randn(E, dtype=dtype, device=device)).exp() + + for iter in [1, 0]: + fix_random_seed(42) + Linear = torch.nn.Linear if iter == 0 else ScaledLinear + + m = torch.nn.Sequential( + Linear(E, hidden_dim), + torch.nn.PReLU(), + Linear(hidden_dim, hidden_dim), + torch.nn.PReLU(), + Linear(hidden_dim, E), + ).to(device) + + train_pairs = [ + ( + 100.0 + * torch.randn(B, T, E, device=device, dtype=dtype) + * input_magnitudes, + torch.randn(B, T, E, device=device, dtype=dtype) * output_magnitudes, + ) + for _ in range(20) + ] + + if iter == 0: + optim = Eve(m.parameters(), lr=0.003) + elif iter == 1: + optim = ScaledAdam(m.parameters(), lr=0.03, clipping_scale=2.0) + scheduler = Eden(optim, lr_batches=200, lr_epochs=5, verbose=False) + + start = timeit.default_timer() + avg_loss = 0.0 + for epoch in range(180): + scheduler.step_epoch() + # if epoch == 100 and iter in [2,3]: + # optim.reset_speedup() # check it doesn't crash. + + # if epoch == 130: + # opts = diagnostics.TensorDiagnosticOptions( + # 2 ** 22 + # ) # allow 4 megabytes per sub-module + # diagnostic = diagnostics.attach_diagnostics(m, opts) + + for n, (x, y) in enumerate(train_pairs): + y_out = m(x) + loss = ((y_out - y) ** 2).mean() * 100.0 + if epoch == 0 and n == 0: + avg_loss = loss.item() + else: + avg_loss = 0.98 * avg_loss + 0.02 * loss.item() + if n == 0 and epoch % 5 == 0: + # norm1 = '%.2e' % (m[0].weight**2).mean().sqrt().item() + # norm1b = '%.2e' % (m[0].bias**2).mean().sqrt().item() + # norm2 = '%.2e' % (m[2].weight**2).mean().sqrt().item() + # norm2b = '%.2e' % (m[2].bias**2).mean().sqrt().item() + # scale1 = '%.2e' % (m[0].weight_scale.exp().item()) + # scale1b = '%.2e' % (m[0].bias_scale.exp().item()) + # scale2 = '%.2e' % (m[2].weight_scale.exp().item()) + # scale2b = '%.2e' % (m[2].bias_scale.exp().item()) + lr = scheduler.get_last_lr()[0] + logging.info( + f"Iter {iter}, epoch {epoch}, batch {n}, avg_loss {avg_loss:.4g}, lr={lr:.4e}" + ) # , norms={norm1,norm1b,norm2,norm2b}") # scales={scale1,scale1b,scale2,scale2b} + loss.log().backward() + optim.step() + optim.zero_grad() + scheduler.step_batch() + + # diagnostic.print_diagnostics() + + stop = timeit.default_timer() + logging.info(f"Iter={iter}, Time taken: {stop - start}") + + logging.info(f"last lr = {scheduler.get_last_lr()}") + # logging.info("state dict = ", scheduler.state_dict()) + # logging.info("optim state_dict = ", optim.state_dict()) + logging.info(f"input_magnitudes = {input_magnitudes}") + logging.info(f"output_magnitudes = {output_magnitudes}") + + +if __name__ == "__main__": + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + logging.getLogger().setLevel(logging.INFO) + import subprocess + + s = subprocess.check_output( + "git status -uno .; git log -1; git diff HEAD .", shell=True + ) + logging.info(s) + import sys + + if len(sys.argv) > 1: + hidden_dim = int(sys.argv[1]) + else: + hidden_dim = 200 + + _test_scaled_adam(hidden_dim) + _test_eden() diff --git a/egs/librispeech/ASR/zipformer/pretrained.py b/egs/librispeech/ASR/zipformer/pretrained.py new file mode 100755 index 000000000..a4b7c2c36 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/pretrained.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads a checkpoint and uses it to decode waves. +You can generate the checkpoint with the following command: + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --causal 1 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 + +Usage of this script: + +- For non-streaming model: + +(1) greedy search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --method greedy_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) modified beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --method modified_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) fast beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --method fast_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +- For streaming model: + +(1) greedy search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --method greedy_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) modified beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --method modified_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) fast beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --method fast_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + + +You can also use `./zipformer/exp/epoch-xx.pt`. + +Note: ./zipformer/exp/pretrained.pt is generated by ./zipformer/export.py +""" + + +import argparse +import logging +import math +from typing import List + +import k2 +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from beam_search import ( + fast_beam_search_one_best, + greedy_search_batch, + modified_beam_search, +) +from icefall.utils import make_pad_mask +from torch.nn.utils.rnn import pad_sequence +from train import add_model_arguments, get_params, get_transducer_model + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--checkpoint", + type=str, + required=True, + help="Path to the checkpoint. " + "The checkpoint is assumed to be saved by " + "icefall.checkpoint.save_checkpoint().", + ) + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to bpe.model.""", + ) + + parser.add_argument( + "--method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - modified_beam_search + - fast_beam_search + """, + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=4, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=4, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=8, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. Used only when + --method is greedy_search. + """, + ) + + add_model_arguments(parser) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + + params.update(vars(args)) + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + if params.causal: + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + + logging.info("Creating model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + checkpoint = torch.load(args.checkpoint, map_location="cpu") + model.load_state_dict(checkpoint["model"], strict=False) + model.to(device) + model.eval() + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + feature_lengths = torch.tensor(feature_lengths, device=device) + + # model forward + x, x_lens = model.encoder_embed(features, feature_lengths) + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = model.encoder( + x, x_lens, src_key_padding_mask + ) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + hyps = [] + msg = f"Using {params.method}" + logging.info(msg) + + if params.method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + else: + raise ValueError(f"Unsupported method: {params.method}") + + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py new file mode 100644 index 000000000..908b60938 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -0,0 +1,1797 @@ +# Copyright 2022-2023 Xiaomi Corp. (authors: Daniel Povey) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 typing import Optional, Tuple, Union +import logging +import k2 +from torch.cuda.amp import custom_fwd, custom_bwd +import random +import torch +import math +import torch.nn as nn +from torch import Tensor + + +class PiecewiseLinear(object): + """ + Piecewise linear function, from float to float, specified as nonempty list of (x,y) pairs with + the x values in order. x values <[initial x] or >[final x] are map to [initial y], [final y] + respectively. + """ + def __init__(self, *args): + assert len(args) >= 1, len(args) + if len(args) == 1 and isinstance(args[0], PiecewiseLinear): + self.pairs = list(args[0].pairs) + else: + self.pairs = [ (float(x), float(y)) for x,y in args ] + for (x,y) in self.pairs: + assert isinstance(x, (float, int)), type(x) + assert isinstance(y, (float, int)), type(y) + + for i in range(len(self.pairs) - 1): + assert self.pairs[i + 1][0] > self.pairs[i][0], (i, self.pairs[i], self.pairs[i + 1]) + + def __str__(self): + # e.g. 'PiecewiseLinear((0., 10.), (100., 0.))' + return f'PiecewiseLinear({str(self.pairs)[1:-1]})' + + def __call__(self, x): + if x <= self.pairs[0][0]: + return self.pairs[0][1] + elif x >= self.pairs[-1][0]: + return self.pairs[-1][1] + else: + cur_x, cur_y = self.pairs[0] + for i in range(1, len(self.pairs)): + next_x, next_y = self.pairs[i] + if x >= cur_x and x <= next_x: + return cur_y + (next_y - cur_y) * (x - cur_x) / (next_x - cur_x) + cur_x, cur_y = next_x, next_y + assert False + + def __mul__(self, alpha): + return PiecewiseLinear( + * [(x, y * alpha) for x, y in self.pairs]) + + def __add__(self, x): + if isinstance(x, (float, int)): + return PiecewiseLinear( + * [(p[0], p[1] + x) for p in self.pairs]) + s, x = self.get_common_basis(x) + return PiecewiseLinear( + * [(sp[0], sp[1] + xp[1]) for sp, xp in zip(s.pairs, x.pairs)]) + + def max(self, x): + if isinstance(x, (float, int)): + x = PiecewiseLinear( (0, x) ) + s, x = self.get_common_basis(x, include_crossings=True) + return PiecewiseLinear( + * [(sp[0], max(sp[1], xp[1])) for sp, xp in zip(s.pairs, x.pairs)]) + + def min(self, x): + if isinstance(x, float) or isinstance(x, int): + x = PiecewiseLinear( (0, x) ) + s, x = self.get_common_basis(x, include_crossings=True) + return PiecewiseLinear( + * [ (sp[0], min(sp[1], xp[1])) for sp, xp in zip(s.pairs, x.pairs)]) + + def __eq__(self, other): + return self.pairs == other.pairs + + def get_common_basis(self, + p: 'PiecewiseLinear', + include_crossings: bool = False): + """ + Returns (self_mod, p_mod) which are equivalent piecewise lienar + functions to self and p, but with the same x values. + + p: the other piecewise linear function + include_crossings: if true, include in the x values positions + where the functions indicate by this and p crosss. + """ + assert isinstance(p, PiecewiseLinear), type(p) + + # get sorted x-values without repetition. + x_vals = sorted(set([ x for x, _ in self.pairs ] + [ x for x, _ in p.pairs ])) + y_vals1 = [ self(x) for x in x_vals ] + y_vals2 = [ p(x) for x in x_vals ] + + if include_crossings: + extra_x_vals = [] + for i in range(len(x_vals) - 1): + if (y_vals1[i] > y_vals2[i]) != (y_vals1[i+1] > y_vals2[i+1]): + # if the two lines in this subsegment potentially cross each other.. + diff_cur = abs(y_vals1[i] - y_vals2[i]) + diff_next = abs(y_vals1[i+1] - y_vals2[i+1]) + # `pos`, between 0 and 1, gives the relative x position, + # with 0 being x_vals[i] and 1 being x_vals[i+1]. + pos = diff_cur / (diff_cur + diff_next) + extra_x_val = x_vals[i] + pos * (x_vals[i+1] - x_vals[i]) + extra_x_vals.append(extra_x_val) + if len(extra_x_vals) > 0: + x_vals = sorted(set(x_vals + extra_x_vals)) + y_vals1 = [ self(x) for x in x_vals ] + y_vals2 = [ p(x) for x in x_vals ] + return ( PiecewiseLinear(* zip(x_vals, y_vals1)), + PiecewiseLinear(* zip(x_vals, y_vals2)) ) + + +class ScheduledFloat(torch.nn.Module): + """ + This object is a torch.nn.Module only because we want it to show up in [top_level module].modules(); + it does not have a working forward() function. You are supposed to cast it to float, as + in, float(parent_module.whatever), and use it as something like a dropout prob. + + It is a floating point value whose value changes depending on the batch count of the + training loop. It is a piecewise linear function where you specifiy the (x,y) pairs + in sorted order on x; x corresponds to the batch index. For batch-index values before the + first x or after the last x, we just use the first or last y value. + + Example: + self.dropout = ScheduledFloat((0.0, 0.2), (4000.0, 0.0), default=0.0) + + `default` is used when self.batch_count is not set or not in training mode or in + torch.jit scripting mode. + """ + def __init__(self, + *args, + default: float = 0.0): + super().__init__() + # self.batch_count and self.name will be written to in the training loop. + self.batch_count = None + self.name = None + self.default = default + self.schedule = PiecewiseLinear(*args) + + def extra_repr(self) -> str: + return f'batch_count={self.batch_count}, schedule={str(self.schedule.pairs[1:-1])}' + + def __float__(self): + batch_count = self.batch_count + if batch_count is None or not self.training or torch.jit.is_scripting(): + return float(self.default) + else: + ans = self.schedule(self.batch_count) + if random.random() < 0.0002: + logging.info(f"ScheduledFloat: name={self.name}, batch_count={self.batch_count}, ans={ans}") + return ans + + def __add__(self, x): + if isinstance(x, float) or isinstance(x, int): + return ScheduledFloat(self.schedule + x, + default=self.default) + else: + return ScheduledFloat(self.schedule + x.schedule, + default=self.default+x.default) + + def max(self, x): + if isinstance(x, float) or isinstance(x, int): + return ScheduledFloat(self.schedule.max(x), + default=self.default) + else: + return ScheduledFloat(self.schedule.max(x.schedule), + default=max(self.default, x.default)) + + +FloatLike = Union[float, ScheduledFloat] + + +def random_cast_to_half(x: Tensor, + min_abs: float = 5.0e-06) -> Tensor: + """ + A randomized way of casting a floating point value to half precision. + """ + if x.dtype == torch.float16: + return x + x_abs = x.abs() + is_too_small = (x_abs < min_abs) + # for elements where is_too_small is true, random_val will contain +-min_abs with + # probability (x.abs() / min_abs), and 0.0 otherwise. [so this preserves expectations, + # for those elements]. + random_val = min_abs * x.sign() * (torch.rand_like(x) * min_abs < x_abs) + return torch.where(is_too_small, random_val, x).to(torch.float16) + + +class CutoffEstimator: + """ + Estimates cutoffs of an arbitrary numerical quantity such that a specified + proportion of items will be above the cutoff on average. + + p is the proportion of items that should be above the cutoff. + """ + def __init__(self, p: float): + self.p = p + # total count of items + self.count = 0 + # total count of items that were above the cutoff + self.count_above = 0 + # initial cutoff value + self.cutoff = 0 + + def __call__(self, x: float) -> bool: + """ + Returns true if x is above the cutoff. + """ + ans = (x > self.cutoff) + self.count += 1 + if ans: + self.count_above += 1 + cur_p = self.count_above / self.count + delta_p = cur_p - self.p + if (delta_p > 0) == ans: + q = abs(delta_p) + self.cutoff = x * q + self.cutoff * (1-q) + return ans + + +class SoftmaxFunction(torch.autograd.Function): + """ + Tries to handle half-precision derivatives in a randomized way that should + be more accurate for training than the default behavior. + """ + @staticmethod + def forward(ctx, x: Tensor, dim: int): + ans = x.softmax(dim=dim) + # if x dtype is float16, x.softmax() returns a float32 because + # (presumably) that op does not support float16, and autocast + # is enabled. + if torch.is_autocast_enabled(): + ans = ans.to(torch.float16) + ctx.save_for_backward(ans) + ctx.x_dtype = x.dtype + ctx.dim = dim + return ans + + @staticmethod + def backward(ctx, ans_grad: Tensor): + ans, = ctx.saved_tensors + with torch.cuda.amp.autocast(enabled=False): + ans_grad = ans_grad.to(torch.float32) + ans = ans.to(torch.float32) + x_grad = ans_grad * ans + x_grad = x_grad - ans * x_grad.sum(dim=ctx.dim, keepdim=True) + return x_grad, None + + +def softmax(x: Tensor, dim: int): + if not x.requires_grad or torch.jit.is_scripting(): + return x.softmax(dim=dim) + + return SoftmaxFunction.apply(x, dim) + + +class MaxEigLimiterFunction(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: Tensor, + coeffs: Tensor, + direction: Tensor, + channel_dim: int, + grad_scale: float) -> Tensor: + ctx.channel_dim = channel_dim + ctx.grad_scale = grad_scale + ctx.save_for_backward(x.detach(), + coeffs.detach(), + direction.detach()) + return x + + @staticmethod + def backward(ctx, x_grad, *args): + with torch.enable_grad(): + (x_orig, coeffs, new_direction) = ctx.saved_tensors + x_orig.requires_grad = True + num_channels = x_orig.shape[ctx.channel_dim] + x = x_orig.transpose(ctx.channel_dim, -1).reshape(-1, num_channels) + new_direction.requires_grad = False + x = x - x.mean(dim=0) + x_var = (x ** 2).mean() + x_residual = x - coeffs * new_direction + x_residual_var = (x_residual ** 2).mean() + # `variance_proportion` is the proportion of the variance accounted for + # by the top eigen-direction. This is to be minimized. + variance_proportion = (x_var - x_residual_var) / (x_var + 1.0e-20) + variance_proportion.backward() + x_orig_grad = x_orig.grad + x_extra_grad = x_orig.grad * ctx.grad_scale * x_grad.norm() / (x_orig_grad.norm() + 1.0e-20) + return x_grad + x_extra_grad.detach(), None, None, None, None + + +class BiasNormFunction(torch.autograd.Function): + # This computes: + # scales = (torch.mean((x - bias) ** 2, keepdim=True)) ** -0.5 * log_scale.exp() + # return (x - bias) * scales + # (after unsqueezing the bias), but it does it in a memory-efficient way so that + # it can just store the returned value (chances are, this will also be needed for + # some other reason, related to the next operation, so we can save memory). + @staticmethod + def forward(ctx, x: Tensor, bias: Tensor, log_scale: Tensor, channel_dim: int, + store_output_for_backprop: bool) -> Tensor: + assert bias.ndim == 1 + if channel_dim < 0: + channel_dim = channel_dim + x.ndim + ctx.store_output_for_backprop = store_output_for_backprop + ctx.channel_dim = channel_dim + for _ in range(channel_dim + 1, x.ndim): + bias = bias.unsqueeze(-1) + scales = (torch.mean((x - bias) ** 2, dim=channel_dim, keepdim=True) ** -0.5) * log_scale.exp() + ans = x * scales + ctx.save_for_backward(ans.detach() if store_output_for_backprop else x, + scales.detach(), bias.detach(), log_scale.detach()) + return ans + + @staticmethod + def backward(ctx, ans_grad: Tensor) -> Tensor: + ans_or_x, scales, bias, log_scale = ctx.saved_tensors + if ctx.store_output_for_backprop: + x = ans_or_x / scales + else: + x = ans_or_x + x = x.detach() + x.requires_grad = True + bias.requires_grad = True + log_scale.requires_grad = True + with torch.enable_grad(): + # recompute scales from x, bias and log_scale. + scales = (torch.mean((x - bias) ** 2, dim=ctx.channel_dim, keepdim=True) ** -0.5) * log_scale.exp() + ans = x * scales + ans.backward(gradient=ans_grad) + return x.grad, bias.grad.flatten(), log_scale.grad, None, None + + +class BiasNorm(torch.nn.Module): + """ + This is intended to be a simpler, and hopefully cheaper, replacement for + LayerNorm. The observation this is based on, is that Transformer-type + networks, especially with pre-norm, sometimes seem to set one of the + feature dimensions to a large constant value (e.g. 50), which "defeats" + the LayerNorm because the output magnitude is then not strongly dependent + on the other (useful) features. Presumably the weight and bias of the + LayerNorm are required to allow it to do this. + + Instead, we give the BiasNorm a trainable bias that it can use when + computing the scale for normalization. We also give it a (scalar) + trainable scale on the output. + + + Args: + num_channels: the number of channels, e.g. 512. + channel_dim: the axis/dimension corresponding to the channel, + interprted as an offset from the input's ndim if negative. + shis is NOT the num_channels; it should typically be one of + {-2, -1, 0, 1, 2, 3}. + log_scale: the initial log-scale that we multiply the output by; this + is learnable. + log_scale_min: FloatLike, minimum allowed value of log_scale + log_scale_max: FloatLike, maximum allowed value of log_scale + store_output_for_backprop: only possibly affects memory use; recommend + to set to True if you think the output of this module is more likely + than the input of this module to be required to be stored for the + backprop. + """ + def __init__( + self, + num_channels: int, + channel_dim: int = -1, # CAUTION: see documentation. + log_scale: float = 1.0, + log_scale_min: float = -1.5, + log_scale_max: float = 1.5, + store_output_for_backprop: bool = False + ) -> None: + super(BiasNorm, self).__init__() + self.num_channels = num_channels + self.channel_dim = channel_dim + self.log_scale = nn.Parameter(torch.tensor(log_scale)) + self.bias = nn.Parameter(torch.zeros(num_channels)) + + self.log_scale_min = log_scale_min + self.log_scale_max = log_scale_max + + self.store_output_for_backprop = store_output_for_backprop + + def forward(self, x: Tensor) -> Tensor: + assert x.shape[self.channel_dim] == self.num_channels + + if torch.jit.is_scripting() or torch.jit.is_tracing(): + channel_dim = self.channel_dim + if channel_dim < 0: + channel_dim += x.ndim + bias = self.bias + for _ in range(channel_dim + 1, x.ndim): + bias = bias.unsqueeze(-1) + scales = ((torch.mean((x - bias) ** 2, dim=channel_dim, keepdim=True) ** -0.5) * + self.log_scale.exp()) + return x * scales + + log_scale = limit_param_value(self.log_scale, + min=float(self.log_scale_min), + max=float(self.log_scale_max), + training=self.training) + + return BiasNormFunction.apply(x, self.bias, log_scale, + self.channel_dim, + self.store_output_for_backprop) + + +def ScaledLinear(*args, + initial_scale: float = 1.0, + **kwargs) -> nn.Linear: + """ + Behaves like a constructor of a modified version of nn.Linear + that gives an easy way to set the default initial parameter scale. + + Args: + Accepts the standard args and kwargs that nn.Linear accepts + e.g. in_features, out_features, bias=False. + + initial_scale: you can override this if you want to increase + or decrease the initial magnitude of the module's output + (affects the initialization of weight_scale and bias_scale). + Another option, if you want to do something like this, is + to re-initialize the parameters. + """ + ans = nn.Linear(*args, **kwargs) + with torch.no_grad(): + ans.weight[:] *= initial_scale + if ans.bias is not None: + torch.nn.init.uniform_(ans.bias, + -0.1 * initial_scale, + 0.1 * initial_scale) + return ans + + +def ScaledConv1d(*args, + initial_scale: float = 1.0, + **kwargs) -> nn.Conv1d: + """ + Behaves like a constructor of a modified version of nn.Conv1d + that gives an easy way to set the default initial parameter scale. + + Args: + Accepts the standard args and kwargs that nn.Linear accepts + e.g. in_features, out_features, bias=False. + + initial_scale: you can override this if you want to increase + or decrease the initial magnitude of the module's output + (affects the initialization of weight_scale and bias_scale). + Another option, if you want to do something like this, is + to re-initialize the parameters. + """ + ans = nn.Conv1d(*args, **kwargs) + with torch.no_grad(): + ans.weight[:] *= initial_scale + if ans.bias is not None: + torch.nn.init.uniform_(ans.bias, + -0.1 * initial_scale, + 0.1 * initial_scale) + return ans + + +def ScaledConv2d(*args, + initial_scale: float = 1.0, + **kwargs) -> nn.Conv2d: + """ + Behaves like a constructor of a modified version of nn.Conv2d + that gives an easy way to set the default initial parameter scale. + + Args: + Accepts the standard args and kwargs that nn.Linear accepts + e.g. in_features, out_features, bias=False, but: + NO PADDING-RELATED ARGS. + + initial_scale: you can override this if you want to increase + or decrease the initial magnitude of the module's output + (affects the initialization of weight_scale and bias_scale). + Another option, if you want to do something like this, is + to re-initialize the parameters. + """ + ans = nn.Conv2d(*args, **kwargs) + with torch.no_grad(): + ans.weight[:] *= initial_scale + if ans.bias is not None: + torch.nn.init.uniform_(ans.bias, + -0.1 * initial_scale, + 0.1 * initial_scale) + return ans + + +class ChunkCausalDepthwiseConv1d(torch.nn.Module): + """ + Behaves like a depthwise 1d convolution, except that it is causal in + a chunkwise way, as if we had a block-triangular attention mask. + The chunk size is provided at test time (it should probably be + kept in sync with the attention mask). + + This has a little more than twice the parameters of a conventional + depthwise conv1d module: we implement it by having one + depthwise convolution, of half the width, that is causal (via + right-padding); and one depthwise convolution that is applied only + within chunks, that we multiply by a scaling factor which depends + on the position within the chunk. + + Args: + Accepts the standard args and kwargs that nn.Linear accepts + e.g. in_features, out_features, bias=False. + + initial_scale: you can override this if you want to increase + or decrease the initial magnitude of the module's output + (affects the initialization of weight_scale and bias_scale). + Another option, if you want to do something like this, is + to re-initialize the parameters. + """ + def __init__(self, + channels: int, + kernel_size: int, + initial_scale: float = 1.0, + bias: bool = True): + super().__init__() + assert kernel_size % 2 == 1 + + half_kernel_size = (kernel_size + 1) // 2 + # will pad manually, on one side. + self.causal_conv = nn.Conv1d(in_channels=channels, + out_channels=channels, + groups=channels, + kernel_size=half_kernel_size, + padding=0, + bias=True) + + self.chunkwise_conv = nn.Conv1d(in_channels=channels, + out_channels=channels, + groups=channels, + kernel_size=kernel_size, + padding=kernel_size // 2, + bias=bias) + + # first row is correction factors added to the scale near the left edge of the chunk, + # second row is correction factors added to the scale near the right edge of the chunk, + # both of these are added to a default scale of 1.0. + self.chunkwise_conv_scale = nn.Parameter(torch.zeros(2, channels, kernel_size)) + self.kernel_size = kernel_size + + with torch.no_grad(): + self.causal_conv.weight[:] *= initial_scale + self.chunkwise_conv.weight[:] *= initial_scale + if bias: + torch.nn.init.uniform_(self.causal_conv.bias, + -0.1 * initial_scale, + 0.1 * initial_scale) + + def forward(self, + x: Tensor, + chunk_size: int = -1) -> Tensor: + """ + Forward function. Args: + x: a Tensor of shape (batch_size, channels, seq_len) + chunk_size: the chunk size, in frames; does not have to divide seq_len exactly. + """ + (batch_size, num_channels, seq_len) = x.shape + + # half_kernel_size = self.kernel_size + 1 // 2 + # left_pad is half_kernel_size - 1 where half_kernel_size is the size used + # in the causal conv. It's the amount by which we must pad on the left, + # to make the convolution causal. + left_pad = self.kernel_size // 2 + + if chunk_size < 0 or chunk_size > seq_len: + chunk_size = seq_len + right_pad = -seq_len % chunk_size + + x = torch.nn.functional.pad(x, (left_pad, right_pad)) + + x_causal = self.causal_conv(x[..., :left_pad + seq_len]) + assert x_causal.shape == (batch_size, num_channels, seq_len) + + x_chunk = x[..., left_pad:] + num_chunks = x_chunk.shape[2] // chunk_size + x_chunk = x_chunk.reshape(batch_size, num_channels, num_chunks, chunk_size) + x_chunk = x_chunk.permute(0, 2, 1, 3).reshape(batch_size * num_chunks, + num_channels, chunk_size) + x_chunk = self.chunkwise_conv(x_chunk) # does not change shape + + chunk_scale = self._get_chunk_scale(chunk_size) + + x_chunk = x_chunk * chunk_scale + x_chunk = x_chunk.reshape(batch_size, num_chunks, + num_channels, chunk_size).permute(0, 2, 1, 3) + x_chunk = x_chunk.reshape(batch_size, num_channels, num_chunks * chunk_size)[..., :seq_len] + + return x_chunk + x_causal + + def _get_chunk_scale(self, chunk_size: int): + """Returns tensor of shape (num_channels, chunk_size) that will be used to + scale the output of self.chunkwise_conv.""" + left_edge = self.chunkwise_conv_scale[0] + right_edge = self.chunkwise_conv_scale[1] + if chunk_size < self.kernel_size: + left_edge = left_edge[:, :chunk_size] + right_edge = right_edge[:, -chunk_size:] + else: + t = chunk_size - self.kernel_size + channels = left_edge.shape[0] + pad = torch.zeros(channels, t, + device=left_edge.device, + dtype=left_edge.dtype) + left_edge = torch.cat((left_edge, pad), dim=-1) + right_edge = torch.cat((pad, right_edge), dim=-1) + return 1.0 + (left_edge + right_edge) + + def streaming_forward( + self, + x: Tensor, + cache: Tensor, + ) -> Tuple[Tensor, Tensor]: + """Streaming Forward function. + + Args: + x: a Tensor of shape (batch_size, channels, seq_len) + cache: cached left context of shape (batch_size, channels, left_pad) + """ + (batch_size, num_channels, seq_len) = x.shape + + # left_pad is half_kernel_size - 1 where half_kernel_size is the size used + # in the causal conv. It's the amount by which we must pad on the left, + # to make the convolution causal. + left_pad = self.kernel_size // 2 + + # Pad cache + assert cache.shape[-1] == left_pad, (cache.shape[-1], left_pad) + x = torch.cat([cache, x], dim=2) + # Update cache + cache = x[..., -left_pad:] + + x_causal = self.causal_conv(x) + assert x_causal.shape == (batch_size, num_channels, seq_len) + + x_chunk = x[..., left_pad:] + x_chunk = self.chunkwise_conv(x_chunk) # does not change shape + + chunk_scale = self._get_chunk_scale(chunk_size=seq_len) + x_chunk = x_chunk * chunk_scale + + return x_chunk + x_causal, cache + + +class BalancerFunction(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: Tensor, + min_mean: float, + max_mean: float, + min_rms: float, + max_rms: float, + grad_scale: float, + channel_dim: int, + ) -> Tensor: + if channel_dim < 0: + channel_dim += x.ndim + ctx.channel_dim = channel_dim + ctx.save_for_backward(x) + ctx.config = (min_mean, max_mean, min_rms, max_rms, grad_scale, channel_dim) + return x + + @staticmethod + def backward( + ctx, x_grad: Tensor + ) -> Tuple[Tensor, None, None, None, None, None]: + x, = ctx.saved_tensors + (min_mean, max_mean, min_rms, max_rms, grad_scale, channel_dim) = ctx.config + + try: + with torch.enable_grad(): + with torch.cuda.amp.autocast(enabled=False): + x = x.to(torch.float32) + x = x.detach() + x.requires_grad = True + mean_dims = [ i for i in range(x.ndim) if i != channel_dim ] + uncentered_var = (x ** 2).mean(dim=mean_dims, keepdim=True) + mean = x.mean(dim=mean_dims, keepdim=True) + stddev = (uncentered_var - (mean * mean)).clamp(min=1.0e-20).sqrt() + rms = uncentered_var.clamp(min=1.0e-20).sqrt() + + m = mean / stddev + # part of loss that relates to mean / stddev + m_loss = (m - m.clamp(min=min_mean, max=max_mean)).abs() + + # put a much larger scale on the RMS-max-limit loss, so that if both it and the + # m_loss are violated we fix the RMS loss first. + rms_clamped = rms.clamp(min=min_rms, max=max_rms) + r_loss = (rms_clamped / rms).log().abs() + + loss = (m_loss + r_loss) + + loss.backward(gradient=torch.ones_like(loss)) + loss_grad = x.grad + loss_grad_rms = (loss_grad ** 2).mean(dim=mean_dims, keepdim=True).sqrt().clamp(min=1.0e-20) + + loss_grad = loss_grad * (grad_scale / loss_grad_rms) + + x_grad_float = x_grad.to(torch.float32) + # scale each element of loss_grad by the absolute value of the corresponding + # element of x_grad, which we view as a noisy estimate of its magnitude for that + # (frame and dimension). later we can consider factored versions. + x_grad_mod = x_grad_float + (x_grad_float.abs() * loss_grad) + x_grad = x_grad_mod.to(x_grad.dtype) + except Exception as e: + logging.info(f"Caught exception in Balancer backward: {e}, size={list(x_grad.shape)}, will continue.") + + return x_grad, None, None, None, None, None, None + + +class Balancer(torch.nn.Module): + """ + Modifies the backpropped derivatives of a function to try to encourage, for + each channel, that it is positive at least a proportion `threshold` of the + time. It does this by multiplying negative derivative values by up to + (1+max_factor), and positive derivative values by up to (1-max_factor), + interpolated from 1 at the threshold to those extremal values when none + of the inputs are positive. + + Args: + num_channels: the number of channels + channel_dim: the dimension/axis corresponding to the channel, e.g. + -1, 0, 1, 2; will be interpreted as an offset from x.ndim if negative. + min_positive: the minimum, per channel, of the proportion of the time + that (x > 0), below which we start to modify the derivatives. + max_positive: the maximum, per channel, of the proportion of the time + that (x > 0), above which we start to modify the derivatives. + scale_gain_factor: determines the 'gain' with which we increase the + change in gradient once the constraints on min_abs and max_abs + are violated. + min_abs: the minimum average-absolute-value difference from the mean + value per channel, which we allow, before we start to modify + the derivatives to prevent this. + max_abs: the maximum average-absolute-value difference from the mean + value per channel, which we allow, before we start to modify + the derivatives to prevent this. + prob: determines the minimum probability with which we modify the + gradients for the {min,max}_positive and {min,max}_abs constraints, + on each forward(). This is done randomly to prevent all layers + from doing it at the same time. + """ + def __init__( + self, + num_channels: int, + channel_dim: int, + min_positive: FloatLike = 0.05, + max_positive: FloatLike = 0.95, + min_abs: FloatLike = 0.2, + max_abs: FloatLike = 100.0, + grad_scale: FloatLike = 0.04, + prob: Optional[FloatLike] = None, + ): + super().__init__() + + if prob is None: + prob = ScheduledFloat((0.0, 0.5), (8000.0, 0.125), default=0.4) + self.prob = prob + # 5% of the time we will return and do nothing because memory usage is + # too high. + self.mem_cutoff = CutoffEstimator(0.05) + + # actually self.num_channels is no longer needed except for an assertion. + self.num_channels = num_channels + self.channel_dim = channel_dim + self.min_positive = min_positive + self.max_positive = max_positive + self.min_abs = min_abs + self.max_abs = max_abs + self.grad_scale = grad_scale + + def forward(self, x: Tensor) -> Tensor: + if (torch.jit.is_scripting() or not x.requires_grad or + (x.is_cuda and self.mem_cutoff(torch.cuda.memory_allocated()))): + return _no_op(x) + + prob = float(self.prob) + if random.random() < prob: + # The following inner-functions convert from the way we historically specified + # these limitations, as limits on the absolute value and the proportion of positive + # values, to limits on the RMS value and the (mean / stddev). + def _abs_to_rms(x): + # for normally distributed data, if the expected absolute value is x, the + # expected rms value will be sqrt(pi/2) * x. + return 1.25331413732 * x + + def _proportion_positive_to_mean(x): + def _atanh(x): + eps = 1.0e-10 + # eps is to prevent crashes if x is exactly 0 or 1. + # we'll just end up returning a fairly large value. + return (math.log (1+x+eps) - math.log (1-x+eps)) / 2. + + def _approx_inverse_erf(x): + # 1 / (sqrt(pi) * ln(2)), + # see https://math.stackexchange.com/questions/321569/approximating-the-error-function-erf-by-analytical-functions + # this approximation is extremely crude and gets progressively worse for + # x very close to -1 or +1, but we mostly care about the "middle" region + # e.g. _approx_inverse_erf(0.05) = 0.0407316414078772, + # and math.erf(0.0407316414078772) = 0.045935330944660666, + # which is pretty close to 0.05. + return 0.8139535143 * _atanh(x) + # first convert x from the range 0..1 to the range -1..1 which the error + # function returns + x = -1 + (2 * x) + return _approx_inverse_erf(x) + + min_mean = _proportion_positive_to_mean(float(self.min_positive)) + max_mean = _proportion_positive_to_mean(float(self.max_positive)) + min_rms = _abs_to_rms(float(self.min_abs)) + max_rms = _abs_to_rms(float(self.max_abs)) + grad_scale = float(self.grad_scale) + + assert x.shape[self.channel_dim] == self.num_channels + + return BalancerFunction.apply( + x, min_mean, max_mean, min_rms, max_rms, grad_scale, self.channel_dim + ) + else: + return _no_op(x) + + +def penalize_abs_values_gt(x: Tensor, limit: float, penalty: float, + name: str = None) -> Tensor: + """ + Returns x unmodified, but in backprop will put a penalty for the excess of + the absolute values of elements of x over the limit "limit". E.g. if + limit == 10.0, then if x has any values over 10 it will get a penalty. + + Caution: the value of this penalty will be affected by grad scaling used + in automatic mixed precision training. For this reasons we use this, + it shouldn't really matter, or may even be helpful; we just use this + to disallow really implausible values of scores to be given to softmax. + + The name is for randomly printed debug info. + """ + x_sign = x.sign() + over_limit = (x.abs() - limit) > 0 + # The following is a memory efficient way to penalize the absolute values of + # x that's over the limit. (The memory efficiency comes when you think + # about which items torch needs to cache for the autograd, and which ones it + # can throw away). The numerical value of aux_loss as computed here will + # actually be larger than it should be, by limit * over_limit.sum(), but it + # has the same derivative as the real aux_loss which is penalty * (x.abs() - + # limit).relu(). + aux_loss = penalty * ((x_sign * over_limit).to(torch.int8) * x) + # note: we don't do sum() here on aux)_loss, but it's as if we had done + # sum() due to how with_loss() works. + x = with_loss(x, aux_loss, name) + # you must use x for something, or this will be ineffective. + return x + + +def _diag(x: Tensor): # like .diag(), but works for tensors with 3 dims. + if x.ndim == 2: + return x.diag() + else: + (batch, dim, dim) = x.shape + x = x.reshape(batch, dim * dim) + x = x[:, ::dim+1] + assert x.shape == (batch, dim) + return x + + +def _whitening_metric(x: Tensor, + num_groups: int): + """ + Computes the "whitening metric", a value which will be 1.0 if all the eigenvalues of + of the centered feature covariance are the same within each group's covariance matrix + and also between groups. + Args: + x: a Tensor of shape (*, num_channels) + num_groups: the number of groups of channels, a number >=1 that divides num_channels + Returns: + Returns a scalar Tensor that will be 1.0 if the data is "perfectly white" and + greater than 1.0 otherwise. + """ + assert x.dtype != torch.float16 + x = x.reshape(-1, x.shape[-1]) + (num_frames, num_channels) = x.shape + assert num_channels % num_groups == 0 + channels_per_group = num_channels // num_groups + x = x.reshape(num_frames, num_groups, channels_per_group).transpose(0, 1) + # x now has shape (num_groups, num_frames, channels_per_group) + # subtract the mean so we use the centered, not uncentered, covariance. + # My experience has been that when we "mess with the gradients" like this, + # it's better not do anything that tries to move the mean around, because + # that can easily cause instability. + x = x - x.mean(dim=1, keepdim=True) + # x_covar: (num_groups, channels_per_group, channels_per_group) + x_covar = torch.matmul(x.transpose(1, 2), x) + x_covar_mean_diag = _diag(x_covar).mean() + # the following expression is what we'd get if we took the matrix product + # of each covariance and measured the mean of its trace, i.e. + # the same as _diag(torch.matmul(x_covar, x_covar)).mean(). + x_covarsq_mean_diag = (x_covar ** 2).sum() / (num_groups * channels_per_group) + # this metric will be >= 1.0; the larger it is, the less 'white' the data was. + metric = x_covarsq_mean_diag / (x_covar_mean_diag ** 2 + 1.0e-20) + return metric + + +class WhiteningPenaltyFunction(torch.autograd.Function): + @staticmethod + def forward(ctx, + x: Tensor, + module: nn.Module) -> Tensor: + ctx.save_for_backward(x) + ctx.module = module + return x + + @staticmethod + def backward(ctx, + x_grad: Tensor): + x_orig, = ctx.saved_tensors + w = ctx.module + + try: + with torch.enable_grad(): + with torch.cuda.amp.autocast(enabled=False): + x_detached = x_orig.to(torch.float32).detach() + x_detached.requires_grad = True + + metric = _whitening_metric(x_detached, w.num_groups) + + if random.random() < 0.005 or __name__ == "__main__": + logging.info(f"Whitening: name={w.name}, num_groups={w.num_groups}, num_channels={x_orig.shape[-1]}, " + f"metric={metric.item():.2f} vs. limit={float(w.whitening_limit)}") + + if metric < float(w.whitening_limit): + w.prob = w.min_prob + return x_grad, None + else: + w.prob = w.max_prob + metric.backward() + penalty_grad = x_detached.grad + scale = w.grad_scale * (x_grad.to(torch.float32).norm() / + (penalty_grad.norm() + 1.0e-20)) + penalty_grad = penalty_grad * scale + return x_grad + penalty_grad.to(x_grad.dtype), None + except Exception as e: + logging.info(f"Caught exception in Whiten backward: {e}, size={list(x_grad.shape)}, will continue.") + return x_grad, None + + +class Whiten(nn.Module): + def __init__( + self, + num_groups: int, + whitening_limit: FloatLike, + prob: Union[float, Tuple[float,float]], + grad_scale: FloatLike): + """ + Args: + num_groups: the number of groups to divide the channel dim into before + whitening. We will attempt to make the feature covariance + within each group, after mean subtraction, as "white" as possible, + while having the same trace across all groups. + whitening_limit: a value greater than 1.0, that dictates how much + freedom we have to violate the constraints. 1.0 would mean perfectly + white, with exactly the same trace across groups; larger values + give more freedom. E.g. 2.0. + prob: the probability with which we apply the gradient modification + (also affects the grad scale). May be supplied as a float, + or as a pair (min_prob, max_prob) + + grad_scale: determines the scale on the gradient term from this object, + relative to the rest of the gradient on the attention weights. + E.g. 0.02 (you may want to use smaller values than this if prob is large) + """ + super(Whiten, self).__init__() + assert num_groups >= 1 + assert float(whitening_limit) >= 1 + assert grad_scale >= 0 + self.num_groups = num_groups + self.whitening_limit = whitening_limit + self.grad_scale = grad_scale + + if isinstance(prob, float): + prob = (prob, prob) + (self.min_prob, self.max_prob) = prob + assert 0 < self.min_prob <= self.max_prob <= 1 + self.prob = self.max_prob + self.name = None # will be set in training loop + + def forward(self, + x: Tensor) -> Tensor: + """ + In the forward pass, this function just returns the input unmodified. + In the backward pass, it will modify the gradients to ensure that the + distribution in each group has close to (lambda times I) as the covariance + after mean subtraction, with the same lambda across groups. + For whitening_limit > 1, there will be more freedom to violate this + constraint. + + Args: + x: the input of shape (*, num_channels) + + Returns: + x, unmodified. You should make sure + you use the returned value, or the graph will be freed + and nothing will happen in backprop. + """ + grad_scale = float(self.grad_scale) + if not x.requires_grad or random.random() > self.prob or grad_scale == 0: + return _no_op(x) + else: + return WhiteningPenaltyFunction.apply(x, self) + + +class WithLoss(torch.autograd.Function): + @staticmethod + def forward(ctx, x: Tensor, y: Tensor, name: str): + ctx.y_shape = y.shape + if random.random() < 0.002 and name is not None: + loss_sum = y.sum().item() + logging.info(f"WithLoss: name={name}, loss-sum={loss_sum:.3e}") + return x + + @staticmethod + def backward(ctx, ans_grad: Tensor): + return ans_grad, torch.ones(ctx.y_shape, + dtype=ans_grad.dtype, + device=ans_grad.device), None + + +def with_loss(x, y, name): + # returns x but adds y.sum() to the loss function. + return WithLoss.apply(x, y, name) + + +class ScaleGradFunction(torch.autograd.Function): + @staticmethod + def forward(ctx, x: Tensor, alpha: float) -> Tensor: + ctx.alpha = alpha + return x + + @staticmethod + def backward(ctx, grad: Tensor): + return grad * ctx.alpha, None + + +def scale_grad(x: Tensor, alpha: float): + return ScaleGradFunction.apply(x, alpha) + + +class ScaleGrad(nn.Module): + def __init__(self, alpha: float): + super().__init__() + self.alpha = alpha + + def forward(self, x: Tensor) -> Tensor: + if torch.jit.is_scripting() or not self.training: + return x + return scale_grad(x, self.alpha) + + +class LimitParamValue(torch.autograd.Function): + @staticmethod + def forward(ctx, x: Tensor, min: float, max: float): + ctx.save_for_backward(x) + assert max >= min + ctx.min = min + ctx.max = max + return x + + @staticmethod + def backward(ctx, x_grad: Tensor): + x, = ctx.saved_tensors + # where x < ctx.min, ensure all grads are negative (this will tend to make + # x more positive). + x_grad = x_grad * torch.where(torch.logical_and(x_grad > 0, x < ctx.min), -1.0, 1.0) + # where x > ctx.max, ensure all grads are positive (this will tend to make + # x more negative). + x_grad *= torch.where(torch.logical_and(x_grad < 0, x > ctx.max), -1.0, 1.0) + return x_grad, None, None + + +def limit_param_value(x: Tensor, + min: float, max: float, + prob: float = 0.6, + training: bool = True): + # You apply this to (typically) an nn.Parameter during training to ensure that its + # (elements mostly) stays within a supplied range. This is done by modifying the + # gradients in backprop. + # It's not necessary to do this on every batch: do it only some of the time, + # to save a little time. + if training and random.random() < prob: + return LimitParamValue.apply(x, min, max) + else: + return x + + +def _no_op(x: Tensor) -> Tensor: + if (torch.jit.is_scripting()): + return x + else: + # a no-op function that will have a node in the autograd graph, + # to avoid certain bugs relating to backward hooks + return x.chunk(1, dim=-1)[0] + + +class Identity(torch.nn.Module): + def __init__(self): + super(Identity, self).__init__() + + def forward(self, x): + return _no_op(x) + + +class DoubleSwishFunction(torch.autograd.Function): + """ + double_swish(x) = x * torch.sigmoid(x-1) + + This is a definition, originally motivated by its close numerical + similarity to swish(swish(x)), where swish(x) = x * sigmoid(x). + + Memory-efficient derivative computation: + double_swish(x) = x * s, where s(x) = torch.sigmoid(x-1) + double_swish'(x) = d/dx double_swish(x) = x * s'(x) + x' * s(x) = x * s'(x) + s(x). + Now, s'(x) = s(x) * (1-s(x)). + double_swish'(x) = x * s'(x) + s(x). + = x * s(x) * (1-s(x)) + s(x). + = double_swish(x) * (1-s(x)) + s(x) + ... so we just need to remember s(x) but not x itself. + """ + + @staticmethod + def forward(ctx, x: Tensor) -> Tensor: + requires_grad = x.requires_grad + if x.dtype == torch.float16: + x = x.to(torch.float32) + + s = torch.sigmoid(x - 1.0) + y = x * s + + if requires_grad: + deriv = (y * (1 - s) + s) + + # notes on derivative of x * sigmoid(x - 1): + # https://www.wolframalpha.com/input?i=d%2Fdx+%28x+*+sigmoid%28x-1%29%29 + # min \simeq -0.043638. Take floor as -0.044 so it's a lower bund + # max \simeq 1.1990. Take ceil to be 1.2 so it's an upper bound. + # the combination of "+ torch.rand_like(deriv)" and casting to torch.uint8 (which + # floors), should be expectation-preserving. + floor = -0.044 + ceil = 1.2 + d_scaled = ((deriv - floor) * (255.0 / (ceil - floor)) + torch.rand_like(deriv)) + if __name__ == "__main__": + # for self-testing only. + assert d_scaled.min() >= 0.0 + assert d_scaled.max() < 256.0 + d_int = d_scaled.to(torch.uint8) + ctx.save_for_backward(d_int) + if x.dtype == torch.float16 or torch.is_autocast_enabled(): + y = y.to(torch.float16) + return y + + @staticmethod + def backward(ctx, y_grad: Tensor) -> Tensor: + d, = ctx.saved_tensors + # the same constants as used in forward pass. + floor = -0.043637 + ceil = 1.2 + + d = (d * ((ceil - floor) / 255.0) + floor) + return y_grad * d + + +class DoubleSwish(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward(self, x: Tensor) -> Tensor: + """Return double-swish activation function which is an approximation to Swish(Swish(x)), + that we approximate closely with x * sigmoid(x-1). + """ + if torch.jit.is_scripting(): + return x * torch.sigmoid(x - 1.0) + return DoubleSwishFunction.apply(x) + + +# Dropout2 is just like normal dropout, except it supports schedules on the dropout rates. +class Dropout2(nn.Module): + def __init__(self, p: FloatLike): + super().__init__() + self.p = p + + def forward(self, x: Tensor) -> Tensor: + return torch.nn.functional.dropout(x, + p=float(self.p), + training=self.training) + + +class MulForDropout3(torch.autograd.Function): + # returns (x * y * alpha) where alpha is a float and y doesn't require + # grad and is zero-or-one. + @staticmethod + @custom_fwd + def forward(ctx, x, y, alpha): + assert not y.requires_grad + ans = x * y * alpha + ctx.save_for_backward(ans) + ctx.alpha = alpha + return ans + + @staticmethod + @custom_bwd + def backward(ctx, ans_grad): + ans, = ctx.saved_tensors + x_grad = ctx.alpha * ans_grad * (ans != 0) + return x_grad, None, None + + +# Dropout3 is just like normal dropout, except it supports schedules on the dropout rates, +# and it lets you choose one dimension to share the dropout mask over +class Dropout3(nn.Module): + def __init__(self, p: FloatLike, shared_dim: int): + super().__init__() + self.p = p + self.shared_dim = shared_dim + + def forward(self, x: Tensor) -> Tensor: + p = float(self.p) + if not self.training or p == 0: + return _no_op(x) + scale = 1.0 / (1 - p) + rand_shape = list(x.shape) + rand_shape[self.shared_dim] = 1 + mask = torch.rand(*rand_shape, device=x.device) > p + ans = MulForDropout3.apply(x, mask, scale) + return ans + + +class SwooshLFunction(torch.autograd.Function): + """ + swoosh(x) = log(1 + exp(x-4)) - 0.08*x - 0.035 + """ + + @staticmethod + def forward(ctx, x: Tensor) -> Tensor: + requires_grad = x.requires_grad + if x.dtype == torch.float16: + x = x.to(torch.float32) + + zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) + + coeff = -0.08 + + with torch.cuda.amp.autocast(enabled=False): + with torch.enable_grad(): + x = x.detach() + x.requires_grad = True + y = torch.logaddexp(zero, x - 4.0) + coeff * x - 0.035 + + if not requires_grad: + return y + + y.backward(gradient = torch.ones_like(y)) + + grad = x.grad + floor = coeff + ceil = 1.0 + coeff + 0.005 + + d_scaled = ((grad - floor) * (255.0 / (ceil - floor)) + torch.rand_like(grad)) + if __name__ == "__main__": + # for self-testing only. + assert d_scaled.min() >= 0.0 + assert d_scaled.max() < 256.0 + + d_int = d_scaled.to(torch.uint8) + ctx.save_for_backward(d_int) + if x.dtype == torch.float16 or torch.is_autocast_enabled(): + y = y.to(torch.float16) + return y + + @staticmethod + def backward(ctx, y_grad: Tensor) -> Tensor: + d, = ctx.saved_tensors + # the same constants as used in forward pass. + + coeff = -0.08 + floor = coeff + ceil = 1.0 + coeff + 0.005 + d = (d * ((ceil - floor) / 255.0) + floor) + return (y_grad * d) + + +class SwooshL(torch.nn.Module): + def forward(self, x: Tensor) -> Tensor: + """Return Swoosh-L activation. + """ + if torch.jit.is_scripting(): + zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) + return torch.logaddexp(zero, x - 4.0) - 0.08 * x - 0.035 + if not x.requires_grad: + return k2.swoosh_l_forward(x) + else: + return k2.swoosh_l(x) + # return SwooshLFunction.apply(x) + + +class SwooshRFunction(torch.autograd.Function): + """ + swoosh(x) = log(1 + exp(x-1)) - 0.08*x - 0.313261687 + + derivatives are between -0.08 and 0.92. + """ + + @staticmethod + def forward(ctx, x: Tensor) -> Tensor: + requires_grad = x.requires_grad + + if x.dtype == torch.float16: + x = x.to(torch.float32) + + zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) + + with torch.cuda.amp.autocast(enabled=False): + with torch.enable_grad(): + x = x.detach() + x.requires_grad = True + y = torch.logaddexp(zero, x - 1.) - 0.08 * x - 0.313261687 + + if not requires_grad: + return y + y.backward(gradient = torch.ones_like(y)) + + grad = x.grad + floor = -0.08 + ceil = 0.925 + + d_scaled = ((grad - floor) * (255.0 / (ceil - floor)) + torch.rand_like(grad)) + if __name__ == "__main__": + # for self-testing only. + assert d_scaled.min() >= 0.0 + assert d_scaled.max() < 256.0 + + d_int = d_scaled.to(torch.uint8) + ctx.save_for_backward(d_int) + if x.dtype == torch.float16 or torch.is_autocast_enabled(): + y = y.to(torch.float16) + return y + + @staticmethod + def backward(ctx, y_grad: Tensor) -> Tensor: + d, = ctx.saved_tensors + # the same constants as used in forward pass. + floor = -0.08 + ceil = 0.925 + d = (d * ((ceil - floor) / 255.0) + floor) + return (y_grad * d) + + +class SwooshR(torch.nn.Module): + def forward(self, x: Tensor) -> Tensor: + """Return Swoosh-R activation. + """ + if torch.jit.is_scripting(): + zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) + return torch.logaddexp(zero, x - 1.) - 0.08 * x - 0.313261687 + if not x.requires_grad: + return k2.swoosh_r_forward(x) + else: + return k2.swoosh_r(x) + # return SwooshRFunction.apply(x) + + +# simple version of SwooshL that does not redefine the backprop, used in +# ActivationDropoutAndLinearFunction. +def SwooshLForward(x: Tensor): + x_offset = x - 4.0 + log_sum = (1.0 + x_offset.exp()).log().to(x.dtype) + log_sum = torch.where(log_sum == float('inf'), x_offset, log_sum) + return log_sum - 0.08 * x - 0.035 + + +# simple version of SwooshR that does not redefine the backprop, used in +# ActivationDropoutAndLinearFunction. +def SwooshRForward(x: Tensor): + x_offset = x - 1.0 + log_sum = (1.0 + x_offset.exp()).log().to(x.dtype) + log_sum = torch.where(log_sum == float('inf'), x_offset, log_sum) + return log_sum - 0.08 * x - 0.313261687 + + +class ActivationDropoutAndLinearFunction(torch.autograd.Function): + @staticmethod + @custom_fwd + def forward(ctx, + x: Tensor, + weight: Tensor, + bias: Optional[Tensor], + activation: str, + dropout_p: float, + dropout_shared_dim: Optional[int]): + if dropout_p != 0.0: + dropout_shape = list(x.shape) + if dropout_shared_dim is not None: + dropout_shape[dropout_shared_dim] = 1 + # else it won't be very memory efficient. + dropout_mask = ((1.0 / (1.0 - dropout_p)) * + (torch.rand(*dropout_shape, + device=x.device, dtype=x.dtype) > dropout_p)) + else: + dropout_mask = None + + ctx.save_for_backward(x, weight, bias, dropout_mask) + + ctx.activation = activation + + forward_activation_dict = { + 'SwooshL': k2.swoosh_l_forward, + 'SwooshR': k2.swoosh_r_forward + } + # it will raise a KeyError if this fails. This will be an error. We let it + # propagate to the user. + activation_func = forward_activation_dict[activation] + x = activation_func(x) + if dropout_mask is not None: + x = x * dropout_mask + x = torch.nn.functional.linear(x, weight, bias) + return x + + @staticmethod + @custom_bwd + def backward(ctx, ans_grad: Tensor): + saved = ctx.saved_tensors + (x, weight, bias, dropout_mask) = saved + + forward_and_deriv_activation_dict = { + 'SwooshL': k2.swoosh_l_forward_and_deriv, + 'SwooshR': k2.swoosh_r_forward_and_deriv + } + # the following lines a KeyError if the activation is unrecognized. + # This will be an error. We let it propagate to the user. + func = forward_and_deriv_activation_dict[ctx.activation] + + y, func_deriv = func(x) + if dropout_mask is not None: + y = y * dropout_mask + # now compute derivative of y w.r.t. weight and bias.. + # y: (..., in_channels), ans_grad: (..., out_channels), + (out_channels, in_channels) = weight.shape + + in_channels = y.shape[-1] + g = ans_grad.reshape(-1, out_channels) + weight_deriv = torch.matmul(g.t(), + y.reshape(-1, in_channels)) + y_deriv = torch.matmul(ans_grad, weight) + bias_deriv = None if bias is None else g.sum(dim=0) + x_deriv = y_deriv * func_deriv + if dropout_mask is not None: + # order versus func_deriv does not matter + x_deriv = x_deriv * dropout_mask + + return x_deriv, weight_deriv, bias_deriv, None, None, None + + +class ActivationDropoutAndLinear(torch.nn.Module): + """ + This merges an activation function followed by dropout and then a nn.Linear module; + it does so in a memory efficient way so that it only stores the input to the whole + module. If activation == SwooshL and dropout_shared_dim != None, this will be + equivalent to: + nn.Sequential(SwooshL(), + Dropout3(dropout_p, shared_dim=dropout_shared_dim), + ScaledLinear(in_channels, out_channels, bias=bias, + initial_scale=initial_scale)) + If dropout_shared_dim is None, the dropout would be equivalent to + Dropout2(dropout_p). Note: Dropout3 will be more memory efficient as the dropout + mask is smaller. + + Args: + in_channels: number of input channels, e.g. 256 + out_channels: number of output channels, e.g. 256 + bias: if true, have a bias + activation: the activation function, for now just support SwooshL. + dropout_p: the dropout probability or schedule (happens after nonlinearity). + dropout_shared_dim: the dimension, if any, across which the dropout mask is + shared (e.g. the time dimension). If None, this may be less memory + efficient if there are modules before this one that cache the input + for their backprop (e.g. Balancer or Whiten). + """ + def __init__(self, + in_channels: int, + out_channels: int, + bias: bool = True, + activation: str = 'SwooshL', + dropout_p: FloatLike = 0.0, + dropout_shared_dim: Optional[int] = -1, + initial_scale: float = 1.0): + super().__init__() + # create a temporary module of nn.Linear that we'll steal the + # weights and bias from + l = ScaledLinear(in_channels, out_channels, + bias=bias, + initial_scale=initial_scale) + + self.weight = l.weight + # register_parameter properly handles making it a parameter when l.bias + # is None. I think there is some reason for doing it this way rather + # than just setting it to None but I don't know what it is, maybe + # something to do with exporting the module.. + self.register_parameter('bias', l.bias) + + self.activation = activation + self.dropout_p = dropout_p + self.dropout_shared_dim = dropout_shared_dim + + def forward(self, + x: Tensor): + if torch.jit.is_scripting() or torch.jit.is_tracing(): + if self.activation == 'SwooshL': + x = SwooshLForward(x) + elif self.activation == "SwooshR": + x = SwooshRForward(x) + else: + assert False, self.activation + return torch.nn.functional.linear(x, + self.weight, + self.bias) + + return ActivationDropoutAndLinearFunction.apply( + x, self.weight, self.bias, self.activation, + float(self.dropout_p), self.dropout_shared_dim) + + +def convert_num_channels(x: Tensor, num_channels: int) -> Tensor: + if num_channels <= x.shape[-1]: + return x[..., :num_channels] + else: + shape = list(x.shape) + shape[-1] = num_channels - shape[-1] + zeros = torch.zeros(shape, dtype=x.dtype, device=x.device) + return torch.cat((x, zeros), dim=-1) + + +def _test_whiten(): + for proportion in [0.1, 0.5, 10.0]: + logging.info(f"_test_whiten(): proportion = {proportion}") + x = torch.randn(100, 128) + direction = torch.randn(128) + coeffs = torch.randn(100, 1) + x += proportion * direction * coeffs + + x.requires_grad = True + + m = Whiten(1, # num_groups + 5.0, # whitening_limit, + prob=1.0, + grad_scale=0.1) # grad_scale + + for _ in range(4): + y = m(x) + + y_grad = torch.randn_like(x) + y.backward(gradient=y_grad) + + if proportion < 0.2: + assert torch.allclose(x.grad, y_grad) + elif proportion > 1.0: + assert not torch.allclose(x.grad, y_grad) + + +def _test_balancer_sign(): + probs = torch.arange(0, 1, 0.01) + N = 1000 + x = 1.0 * ((2.0 * (torch.rand(probs.numel(), N) < probs.unsqueeze(-1))) - 1.0) + x = x.detach() + x.requires_grad = True + m = Balancer( + probs.numel(), + channel_dim=0, + min_positive=0.05, + max_positive=0.95, + min_abs=0.0, + prob=1.0, + ) + + y_grad = torch.sign(torch.randn(probs.numel(), N)) + + y = m(x) + y.backward(gradient=y_grad) + print("_test_balancer_sign: x = ", x) + print("_test_balancer_sign: y grad = ", y_grad) + print("_test_balancer_sign: x grad = ", x.grad) + + +def _test_balancer_magnitude(): + magnitudes = torch.arange(0, 1, 0.01) + N = 1000 + x = torch.sign(torch.randn(magnitudes.numel(), N)) * magnitudes.unsqueeze( + -1 + ) + x = x.detach() + x.requires_grad = True + m = Balancer( + magnitudes.numel(), + channel_dim=0, + min_positive=0.0, + max_positive=1.0, + min_abs=0.2, + max_abs=0.7, + prob=1.0, + ) + + y_grad = torch.sign(torch.randn(magnitudes.numel(), N)) + + y = m(x) + y.backward(gradient=y_grad) + print("_test_balancer_magnitude: x = ", x) + print("_test_balancer_magnitude: y grad = ", y_grad) + print("_test_balancer_magnitude: x grad = ", x.grad) + + +def _test_double_swish_deriv(): + x = torch.randn(10, 12, dtype=torch.double) * 3.0 + x.requires_grad = True + m = DoubleSwish() + + tol = ((1.2-(-0.043637))/255.0) + torch.autograd.gradcheck(m, x, atol=tol) + + # for self-test. + x = torch.randn(1000, 1000, dtype=torch.double) * 3.0 + x.requires_grad = True + y = m(x) + + +def _test_swooshl_deriv(): + x = torch.randn(10, 12, dtype=torch.double) * 3.0 + x.requires_grad = True + m = SwooshL() + + tol = (1.0 / 255.0) + torch.autograd.gradcheck(m, x, atol=tol, eps=0.01) + + # for self-test. + x = torch.randn(1000, 1000, dtype=torch.double) * 3.0 + x.requires_grad = True + y = m(x) + + +def _test_swooshr_deriv(): + x = torch.randn(10, 12, dtype=torch.double) * 3.0 + x.requires_grad = True + m = SwooshR() + + tol = (1.0 / 255.0) + torch.autograd.gradcheck(m, x, atol=tol, eps=0.01) + + # for self-test. + x = torch.randn(1000, 1000, dtype=torch.double) * 3.0 + x.requires_grad = True + y = m(x) + + +def _test_softmax(): + a = torch.randn(2, 10, dtype=torch.float64) + b = a.clone() + a.requires_grad = True + b.requires_grad = True + a.softmax(dim=1)[:,0].sum().backward() + print("a grad = ", a.grad) + softmax(b, dim=1)[:,0].sum().backward() + print("b grad = ", b.grad) + assert torch.allclose(a.grad, b.grad) + + +def _test_piecewise_linear(): + p = PiecewiseLinear( (0, 10.0) ) + for x in [-100, 0, 100]: + assert p(x) == 10.0 + p = PiecewiseLinear( (0, 10.0), (1, 0.0) ) + for x, y in [ (-100, 10.0), (0, 10.0), (0.5, 5.0), (1, 0.0), (2, 0.0) ]: + print("x, y = ", x, y) + assert p(x) == y, (x, p(x), y) + + q = PiecewiseLinear((0.5, 15.0), (0.6, 1.0)) + x_vals = [ -1.0, 0.0, 0.1, 0.2, 0.5, 0.6, 0.7, 0.9, 1.0, 2.0 ] + pq = p.max(q) + for x in x_vals: + y1 = max(p(x), q(x)) + y2 = pq(x) + assert abs(y1 - y2) < 0.001 + pq = p.min(q) + for x in x_vals: + y1 = min(p(x), q(x)) + y2 = pq(x) + assert abs(y1 - y2) < 0.001 + pq = p + q + for x in x_vals: + y1 = p(x) + q(x) + y2 = pq(x) + assert abs(y1 - y2) < 0.001 + + +def _test_activation_dropout_and_linear(): + in_channels = 20 + out_channels = 30 + + for bias in [True, False]: + # actually we don't test for dropout_p != 0.0 because forward functions will give + # different answers. This is because we are using the k2 implementation of + # swoosh_l an swoosh_r inside SwooshL() and SwooshR(), and they call randn() + # internally, messing up the random state. + for dropout_p in [0.0]: + for activation in ['SwooshL', 'SwooshR']: + m1 = nn.Sequential(SwooshL() if activation == 'SwooshL' else SwooshR(), + Dropout3(p=dropout_p, shared_dim=-1), + ScaledLinear(in_channels, out_channels, bias=bias, + initial_scale=0.5)) + m2 = ActivationDropoutAndLinear(in_channels, out_channels, + bias=bias, initial_scale=0.5, + activation=activation, + dropout_p=dropout_p) + with torch.no_grad(): + m2.weight[:] = m1[2].weight + if bias: + m2.bias[:] = m1[2].bias + # make sure forward gives same result. + x1 = torch.randn(10, in_channels) + x1.requires_grad = True + + # TEMP. + assert torch.allclose(SwooshRFunction.apply(x1), + SwooshRForward(x1), + atol=1.0e-03) + + x2 = x1.clone().detach() + x2.requires_grad = True + seed = 10 + torch.manual_seed(seed) + y1 = m1(x1) + y_grad = torch.randn_like(y1) + y1.backward(gradient=y_grad) + torch.manual_seed(seed) + y2 = m2(x2) + y2.backward(gradient=y_grad) + + print(f"bias = {bias}, dropout_p = {dropout_p}, activation = {activation}") + print("y1 = ", y1) + print("y2 = ", y2) + assert torch.allclose(y1, y2, atol=0.02) + assert torch.allclose(m1[2].weight.grad, m2.weight.grad, + atol=1.0e-05) + if bias: + assert torch.allclose(m1[2].bias.grad, m2.bias.grad, + atol=1.0e-05) + print("x1.grad = ", x1.grad) + print("x2.grad = ", x2.grad) + + def isclose(a, b): + # return true if cosine similarity is > 0.9. + return (a * b).sum() > 0.9 * ((a**2).sum() * (b**2).sum()).sqrt() + # the SwooshL() implementation has a noisy gradient due to 1-byte + # storage of it. + assert isclose(x1.grad, x2.grad) + + +if __name__ == "__main__": + logging.getLogger().setLevel(logging.INFO) + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + _test_piecewise_linear() + _test_softmax() + _test_whiten() + _test_balancer_sign() + _test_balancer_magnitude() + _test_double_swish_deriv() + _test_swooshr_deriv() + _test_swooshl_deriv() + _test_activation_dropout_and_linear() diff --git a/egs/librispeech/ASR/zipformer/scaling_converter.py b/egs/librispeech/ASR/zipformer/scaling_converter.py new file mode 100644 index 000000000..683a03461 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/scaling_converter.py @@ -0,0 +1,82 @@ +# Copyright 2022-2023 Xiaomi Corp. (authors: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file replaces various modules in a model. +Specifically, ActivationBalancer is replaced with an identity operator; +Whiten is also replaced with an identity operator; +BasicNorm is replaced by a module with `exp` removed. +""" + +import copy +from typing import List, Tuple + +import torch +import torch.nn as nn +from scaling import Balancer, Dropout3, ScaleGrad, Whiten + + +# Copied from https://pytorch.org/docs/1.9.0/_modules/torch/nn/modules/module.html#Module.get_submodule # noqa +# get_submodule was added to nn.Module at v1.9.0 +def get_submodule(model, target): + if target == "": + return model + atoms: List[str] = target.split(".") + mod: torch.nn.Module = model + for item in atoms: + if not hasattr(mod, item): + raise AttributeError( + mod._get_name() + " has no " "attribute `" + item + "`" + ) + mod = getattr(mod, item) + if not isinstance(mod, torch.nn.Module): + raise AttributeError("`" + item + "` is not " "an nn.Module") + return mod + + +def convert_scaled_to_non_scaled( + model: nn.Module, + inplace: bool = False, + is_pnnx: bool = False, +): + """ + Args: + model: + The model to be converted. + inplace: + If True, the input model is modified inplace. + If False, the input model is copied and we modify the copied version. + is_pnnx: + True if we are going to export the model for PNNX. + Return: + Return a model without scaled layers. + """ + if not inplace: + model = copy.deepcopy(model) + + d = {} + for name, m in model.named_modules(): + if isinstance(m, (Balancer, Dropout3, ScaleGrad, Whiten)): + d[name] = nn.Identity() + + for k, v in d.items(): + if "." in k: + parent, child = k.rsplit(".", maxsplit=1) + setattr(get_submodule(model, parent), child, v) + else: + setattr(model, k, v) + + return model diff --git a/egs/librispeech/ASR/zipformer/streaming_beam_search.py b/egs/librispeech/ASR/zipformer/streaming_beam_search.py new file mode 100644 index 000000000..e6e0fb1c8 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/streaming_beam_search.py @@ -0,0 +1,282 @@ +# Copyright 2022 Xiaomi Corp. (authors: Wei Kang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 warnings +from typing import List + +import k2 +import torch +import torch.nn as nn +from beam_search import Hypothesis, HypothesisList, get_hyps_shape +from decode_stream import DecodeStream + +from icefall.decode import one_best_decoding +from icefall.utils import get_texts + + +def greedy_search( + model: nn.Module, + encoder_out: torch.Tensor, + streams: List[DecodeStream], +) -> None: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + + Args: + model: + The transducer model. + encoder_out: + Output from the encoder. Its shape is (N, T, C), where N >= 1. + streams: + A list of Stream objects. + """ + assert len(streams) == encoder_out.size(0) + assert encoder_out.ndim == 3 + + blank_id = model.decoder.blank_id + context_size = model.decoder.context_size + device = model.device + T = encoder_out.size(1) + + decoder_input = torch.tensor( + [stream.hyp[-context_size:] for stream in streams], + device=device, + dtype=torch.int64, + ) + # decoder_out is of shape (N, 1, decoder_out_dim) + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + + for t in range(T): + # current_encoder_out's shape: (batch_size, 1, encoder_out_dim) + current_encoder_out = encoder_out[:, t : t + 1, :] # noqa + + logits = model.joiner( + current_encoder_out.unsqueeze(2), + decoder_out.unsqueeze(1), + project_input=False, + ) + # logits'shape (batch_size, vocab_size) + logits = logits.squeeze(1).squeeze(1) + + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v != blank_id: + streams[i].hyp.append(v) + emitted = True + if emitted: + # update decoder output + decoder_input = torch.tensor( + [stream.hyp[-context_size:] for stream in streams], + device=device, + dtype=torch.int64, + ) + decoder_out = model.decoder( + decoder_input, + need_pad=False, + ) + decoder_out = model.joiner.decoder_proj(decoder_out) + + +def modified_beam_search( + model: nn.Module, + encoder_out: torch.Tensor, + streams: List[DecodeStream], + num_active_paths: int = 4, +) -> None: + """Beam search in batch mode with --max-sym-per-frame=1 being hardcoded. + + Args: + model: + The RNN-T model. + encoder_out: + A 3-D tensor of shape (N, T, encoder_out_dim) containing the output of + the encoder model. + streams: + A list of stream objects. + num_active_paths: + Number of active paths during the beam search. + """ + assert encoder_out.ndim == 3, encoder_out.shape + assert len(streams) == encoder_out.size(0) + + blank_id = model.decoder.blank_id + context_size = model.decoder.context_size + device = next(model.parameters()).device + batch_size = len(streams) + T = encoder_out.size(1) + + B = [stream.hyps for stream in streams] + + for t in range(T): + current_encoder_out = encoder_out[:, t].unsqueeze(1).unsqueeze(1) + # current_encoder_out's shape: (batch_size, 1, 1, encoder_out_dim) + + hyps_shape = get_hyps_shape(B).to(device) + + A = [list(b) for b in B] + B = [HypothesisList() for _ in range(batch_size)] + + ys_log_probs = torch.stack( + [hyp.log_prob.reshape(1) for hyps in A for hyp in hyps], dim=0 + ) # (num_hyps, 1) + + decoder_input = torch.tensor( + [hyp.ys[-context_size:] for hyps in A for hyp in hyps], + device=device, + dtype=torch.int64, + ) # (num_hyps, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False).unsqueeze(1) + decoder_out = model.joiner.decoder_proj(decoder_out) + # decoder_out is of shape (num_hyps, 1, 1, decoder_output_dim) + + # Note: For torch 1.7.1 and below, it requires a torch.int64 tensor + # as index, so we use `to(torch.int64)` below. + current_encoder_out = torch.index_select( + current_encoder_out, + dim=0, + index=hyps_shape.row_ids(1).to(torch.int64), + ) # (num_hyps, encoder_out_dim) + + logits = model.joiner(current_encoder_out, decoder_out, project_input=False) + # logits is of shape (num_hyps, 1, 1, vocab_size) + + logits = logits.squeeze(1).squeeze(1) + + log_probs = logits.log_softmax(dim=-1) # (num_hyps, vocab_size) + + log_probs.add_(ys_log_probs) + + vocab_size = log_probs.size(-1) + + log_probs = log_probs.reshape(-1) + + row_splits = hyps_shape.row_splits(1) * vocab_size + log_probs_shape = k2.ragged.create_ragged_shape2( + row_splits=row_splits, cached_tot_size=log_probs.numel() + ) + ragged_log_probs = k2.RaggedTensor(shape=log_probs_shape, value=log_probs) + + for i in range(batch_size): + topk_log_probs, topk_indexes = ragged_log_probs[i].topk(num_active_paths) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + topk_hyp_indexes = (topk_indexes // vocab_size).tolist() + topk_token_indexes = (topk_indexes % vocab_size).tolist() + + for k in range(len(topk_hyp_indexes)): + hyp_idx = topk_hyp_indexes[k] + hyp = A[i][hyp_idx] + + new_ys = hyp.ys[:] + new_token = topk_token_indexes[k] + if new_token != blank_id: + new_ys.append(new_token) + + new_log_prob = topk_log_probs[k] + new_hyp = Hypothesis(ys=new_ys, log_prob=new_log_prob) + B[i].add(new_hyp) + + for i in range(batch_size): + streams[i].hyps = B[i] + + +def fast_beam_search_one_best( + model: nn.Module, + encoder_out: torch.Tensor, + processed_lens: torch.Tensor, + streams: List[DecodeStream], + beam: float, + max_states: int, + max_contexts: int, +) -> None: + """It limits the maximum number of symbols per frame to 1. + + A lattice is first generated by Fsa-based beam search, then we get the + recognition by applying shortest path on the lattice. + + Args: + model: + An instance of `Transducer`. + encoder_out: + A tensor of shape (N, T, C) from the encoder. + processed_lens: + A tensor of shape (N,) containing the number of processed frames + in `encoder_out` before padding. + streams: + A list of stream objects. + beam: + Beam value, similar to the beam used in Kaldi.. + max_states: + Max states per stream per frame. + max_contexts: + Max contexts pre stream per frame. + """ + assert encoder_out.ndim == 3 + B, T, C = encoder_out.shape + assert B == len(streams) + + context_size = model.decoder.context_size + vocab_size = model.decoder.vocab_size + + config = k2.RnntDecodingConfig( + vocab_size=vocab_size, + decoder_history_len=context_size, + beam=beam, + max_contexts=max_contexts, + max_states=max_states, + ) + individual_streams = [] + for i in range(B): + individual_streams.append(streams[i].rnnt_decoding_stream) + decoding_streams = k2.RnntDecodingStreams(individual_streams, config) + + for t in range(T): + # shape is a RaggedShape of shape (B, context) + # contexts is a Tensor of shape (shape.NumElements(), context_size) + shape, contexts = decoding_streams.get_contexts() + # `nn.Embedding()` in torch below v1.7.1 supports only torch.int64 + contexts = contexts.to(torch.int64) + # decoder_out is of shape (shape.NumElements(), 1, decoder_out_dim) + decoder_out = model.decoder(contexts, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + # current_encoder_out is of shape + # (shape.NumElements(), 1, joiner_dim) + # fmt: off + current_encoder_out = torch.index_select( + encoder_out[:, t:t + 1, :], 0, shape.row_ids(1).to(torch.int64) + ) + # fmt: on + logits = model.joiner( + current_encoder_out.unsqueeze(2), + decoder_out.unsqueeze(1), + project_input=False, + ) + logits = logits.squeeze(1).squeeze(1) + log_probs = logits.log_softmax(dim=-1) + decoding_streams.advance(log_probs) + + decoding_streams.terminate_and_flush_to_streams() + + lattice = decoding_streams.format_output(processed_lens.tolist()) + best_path = one_best_decoding(lattice) + hyp_tokens = get_texts(best_path) + + for i in range(B): + streams[i].hyp = hyp_tokens[i] diff --git a/egs/librispeech/ASR/zipformer/streaming_decode.py b/egs/librispeech/ASR/zipformer/streaming_decode.py new file mode 100755 index 000000000..c2d58cb1e --- /dev/null +++ b/egs/librispeech/ASR/zipformer/streaming_decode.py @@ -0,0 +1,876 @@ +#!/usr/bin/env python3 +# Copyright 2022-2023 Xiaomi Corporation (Authors: Wei Kang, +# Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + +""" +Usage: +./zipformer/streaming_decode.py \ + --epoch 28 \ + --avg 15 \ + --causal 1 \ + --chunk-size 32 \ + --left-context-frames 256 \ + --exp-dir ./zipformer/exp \ + --decoding-method greedy_search \ + --num-decode-streams 2000 +""" + +import argparse +import logging +import math +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import numpy as np +import sentencepiece as spm +import torch +from asr_datamodule import LibriSpeechAsrDataModule +from decode_stream import DecodeStream +from kaldifeat import Fbank, FbankOptions +from lhotse import CutSet +from streaming_beam_search import ( + fast_beam_search_one_best, + greedy_search, + modified_beam_search, +) +from torch import Tensor, nn +from torch.nn.utils.rnn import pad_sequence +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import ( + AttributeDict, + make_pad_mask, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 0. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Supported decoding methods are: + greedy_search + modified_beam_search + fast_beam_search + """, + ) + + parser.add_argument( + "--num_active_paths", + type=int, + default=4, + help="""An interger indicating how many candidates we will keep for each + frame. Used only when --decoding-method is modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=4, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search""", + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=4, + help="""Used only when --decoding-method is + fast_beam_search""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=32, + help="""Used only when --decoding-method is + fast_beam_search""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--num-decode-streams", + type=int, + default=2000, + help="The number of streams that can be decoded parallel.", + ) + + add_model_arguments(parser) + + return parser + + +def get_init_states( + model: nn.Module, + batch_size: int = 1, + device: torch.device = torch.device("cpu"), +) -> List[torch.Tensor]: + """ + Returns a list of cached tensors of all encoder layers. For layer-i, states[i*6:(i+1)*6] + is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + states[-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + states[-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + """ + states = model.encoder.get_init_states(batch_size, device) + + embed_states = model.encoder_embed.get_init_states(batch_size, device) + states.append(embed_states) + + processed_lens = torch.zeros(batch_size, dtype=torch.int32, device=device) + states.append(processed_lens) + + return states + + +def stack_states(state_list: List[List[torch.Tensor]]) -> List[torch.Tensor]: + """Stack list of zipformer states that correspond to separate utterances + into a single emformer state, so that it can be used as an input for + zipformer when those utterances are formed into a batch. + + Args: + state_list: + Each element in state_list corresponding to the internal state + of the zipformer model for a single utterance. For element-n, + state_list[n] is a list of cached tensors of all encoder layers. For layer-i, + state_list[n][i*6:(i+1)*6] is (cached_key, cached_nonlin_attn, cached_val1, + cached_val2, cached_conv1, cached_conv2). + state_list[n][-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + state_list[n][-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + + Note: + It is the inverse of :func:`unstack_states`. + """ + batch_size = len(state_list) + assert (len(state_list[0]) - 2) % 6 == 0, len(state_list[0]) + tot_num_layers = (len(state_list[0]) - 2) // 6 + + batch_states = [] + for layer in range(tot_num_layers): + layer_offset = layer * 6 + # cached_key: (left_context_len, batch_size, key_dim) + cached_key = torch.cat( + [state_list[i][layer_offset] for i in range(batch_size)], dim=1 + ) + # cached_nonlin_attn: (num_heads, batch_size, left_context_len, head_dim) + cached_nonlin_attn = torch.cat( + [state_list[i][layer_offset + 1] for i in range(batch_size)], dim=1 + ) + # cached_val1: (left_context_len, batch_size, value_dim) + cached_val1 = torch.cat( + [state_list[i][layer_offset + 2] for i in range(batch_size)], dim=1 + ) + # cached_val2: (left_context_len, batch_size, value_dim) + cached_val2 = torch.cat( + [state_list[i][layer_offset + 3] for i in range(batch_size)], dim=1 + ) + # cached_conv1: (#batch, channels, left_pad) + cached_conv1 = torch.cat( + [state_list[i][layer_offset + 4] for i in range(batch_size)], dim=0 + ) + # cached_conv2: (#batch, channels, left_pad) + cached_conv2 = torch.cat( + [state_list[i][layer_offset + 5] for i in range(batch_size)], dim=0 + ) + batch_states += [ + cached_key, + cached_nonlin_attn, + cached_val1, + cached_val2, + cached_conv1, + cached_conv2, + ] + + cached_embed_left_pad = torch.cat( + [state_list[i][-2] for i in range(batch_size)], dim=0 + ) + batch_states.append(cached_embed_left_pad) + + processed_lens = torch.cat( + [state_list[i][-1] for i in range(batch_size)], dim=0 + ) + batch_states.append(processed_lens) + + return batch_states + + +def unstack_states(batch_states: List[Tensor]) -> List[List[Tensor]]: + """Unstack the zipformer state corresponding to a batch of utterances + into a list of states, where the i-th entry is the state from the i-th + utterance in the batch. + + Note: + It is the inverse of :func:`stack_states`. + + Args: + batch_states: A list of cached tensors of all encoder layers. For layer-i, + states[i*6:(i+1)*6] is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, + cached_conv1, cached_conv2). + state_list[-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + states[-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + + Returns: + state_list: A list of list. Each element in state_list corresponding to the internal state + of the zipformer model for a single utterance. + """ + assert (len(batch_states) - 2) % 6 == 0, len(batch_states) + tot_num_layers = (len(batch_states) - 2) // 6 + + processed_lens = batch_states[-1] + batch_size = processed_lens.shape[0] + + state_list = [[] for _ in range(batch_size)] + + for layer in range(tot_num_layers): + layer_offset = layer * 6 + # cached_key: (left_context_len, batch_size, key_dim) + cached_key_list = batch_states[layer_offset].chunk( + chunks=batch_size, dim=1 + ) + # cached_nonlin_attn: (num_heads, batch_size, left_context_len, head_dim) + cached_nonlin_attn_list = batch_states[layer_offset + 1].chunk( + chunks=batch_size, dim=1 + ) + # cached_val1: (left_context_len, batch_size, value_dim) + cached_val1_list = batch_states[layer_offset + 2].chunk( + chunks=batch_size, dim=1 + ) + # cached_val2: (left_context_len, batch_size, value_dim) + cached_val2_list = batch_states[layer_offset + 3].chunk( + chunks=batch_size, dim=1 + ) + # cached_conv1: (#batch, channels, left_pad) + cached_conv1_list = batch_states[layer_offset + 4].chunk( + chunks=batch_size, dim=0 + ) + # cached_conv2: (#batch, channels, left_pad) + cached_conv2_list = batch_states[layer_offset + 5].chunk( + chunks=batch_size, dim=0 + ) + for i in range(batch_size): + state_list[i] += [ + cached_key_list[i], + cached_nonlin_attn_list[i], + cached_val1_list[i], + cached_val2_list[i], + cached_conv1_list[i], + cached_conv2_list[i], + ] + + cached_embed_left_pad_list = batch_states[-2].chunk( + chunks=batch_size, dim=0 + ) + for i in range(batch_size): + state_list[i].append(cached_embed_left_pad_list[i]) + + processed_lens_list = batch_states[-1].chunk(chunks=batch_size, dim=0) + for i in range(batch_size): + state_list[i].append(processed_lens_list[i]) + + return state_list + + +def streaming_forward( + features: Tensor, + feature_lens: Tensor, + model: nn.Module, + states: List[Tensor], + chunk_size: int, + left_context_len: int, +) -> Tuple[Tensor, Tensor, List[Tensor]]: + """ + Returns encoder outputs, output lengths, and updated states. + """ + cached_embed_left_pad = states[-2] + ( + x, + x_lens, + new_cached_embed_left_pad, + ) = model.encoder_embed.streaming_forward( + x=features, + x_lens=feature_lens, + cached_left_pad=cached_embed_left_pad, + ) + assert x.size(1) == chunk_size, (x.size(1), chunk_size) + + src_key_padding_mask = make_pad_mask(x_lens) + + # processed_mask is used to mask out initial states + processed_mask = torch.arange(left_context_len, device=x.device).expand( + x.size(0), left_context_len + ) + processed_lens = states[-1] # (batch,) + # (batch, left_context_size) + processed_mask = (processed_lens.unsqueeze(1) <= processed_mask).flip(1) + # Update processed lengths + new_processed_lens = processed_lens + x_lens + + # (batch, left_context_size + chunk_size) + src_key_padding_mask = torch.cat( + [processed_mask, src_key_padding_mask], dim=1 + ) + + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + encoder_states = states[:-2] + ( + encoder_out, + encoder_out_lens, + new_encoder_states, + ) = model.encoder.streaming_forward( + x=x, + x_lens=x_lens, + states=encoder_states, + src_key_padding_mask=src_key_padding_mask, + ) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + new_states = new_encoder_states + [ + new_cached_embed_left_pad, + new_processed_lens, + ] + return encoder_out, encoder_out_lens, new_states + + +def decode_one_chunk( + params: AttributeDict, + model: nn.Module, + decode_streams: List[DecodeStream], +) -> List[int]: + """Decode one chunk frames of features for each decode_streams and + return the indexes of finished streams in a List. + + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + decode_streams: + A List of DecodeStream, each belonging to a utterance. + Returns: + Return a List containing which DecodeStreams are finished. + """ + device = model.device + chunk_size = int(params.chunk_size) + left_context_len = int(params.left_context_frames) + + features = [] + feature_lens = [] + states = [] + processed_lens = [] # Used in fast-beam-search + + for stream in decode_streams: + feat, feat_len = stream.get_feature_frames(chunk_size * 2) + features.append(feat) + feature_lens.append(feat_len) + states.append(stream.states) + processed_lens.append(stream.done_frames) + + feature_lens = torch.tensor(feature_lens, device=device) + features = pad_sequence(features, batch_first=True, padding_value=LOG_EPS) + + # Make sure the length after encoder_embed is at least 1. + # The encoder_embed subsample features (T - 7) // 2 + # The ConvNeXt module needs (7 - 1) // 2 = 3 frames of right padding after subsampling + tail_length = chunk_size * 2 + 7 + 2 * 3 + if features.size(1) < tail_length: + pad_length = tail_length - features.size(1) + feature_lens += pad_length + features = torch.nn.functional.pad( + features, + (0, 0, 0, pad_length), + mode="constant", + value=LOG_EPS, + ) + + states = stack_states(states) + + encoder_out, encoder_out_lens, new_states = streaming_forward( + features=features, + feature_lens=feature_lens, + model=model, + states=states, + chunk_size=chunk_size, + left_context_len=left_context_len, + ) + + encoder_out = model.joiner.encoder_proj(encoder_out) + + if params.decoding_method == "greedy_search": + greedy_search( + model=model, encoder_out=encoder_out, streams=decode_streams + ) + elif params.decoding_method == "fast_beam_search": + processed_lens = torch.tensor(processed_lens, device=device) + processed_lens = processed_lens + encoder_out_lens + fast_beam_search_one_best( + model=model, + encoder_out=encoder_out, + processed_lens=processed_lens, + streams=decode_streams, + beam=params.beam, + max_states=params.max_states, + max_contexts=params.max_contexts, + ) + elif params.decoding_method == "modified_beam_search": + modified_beam_search( + model=model, + streams=decode_streams, + encoder_out=encoder_out, + num_active_paths=params.num_active_paths, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + + states = unstack_states(new_states) + + finished_streams = [] + for i in range(len(decode_streams)): + decode_streams[i].states = states[i] + decode_streams[i].done_frames += encoder_out_lens[i] + if decode_streams[i].done: + finished_streams.append(i) + + return finished_streams + + +def decode_dataset( + cuts: CutSet, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[List[str], List[str]]]]: + """Decode dataset. + + Args: + cuts: + Lhotse Cutset containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + device = model.device + + opts = FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = 16000 + opts.mel_opts.num_bins = 80 + + log_interval = 100 + + decode_results = [] + # Contain decode streams currently running. + decode_streams = [] + for num, cut in enumerate(cuts): + # each utterance has a DecodeStream. + initial_states = get_init_states( + model=model, batch_size=1, device=device + ) + decode_stream = DecodeStream( + params=params, + cut_id=cut.id, + initial_states=initial_states, + decoding_graph=decoding_graph, + device=device, + ) + + audio: np.ndarray = cut.load_audio() + # audio.shape: (1, num_samples) + assert len(audio.shape) == 2 + assert audio.shape[0] == 1, "Should be single channel" + assert audio.dtype == np.float32, audio.dtype + + # The trained model is using normalized samples + assert audio.max() <= 1, "Should be normalized to [-1, 1])" + + samples = torch.from_numpy(audio).squeeze(0) + + fbank = Fbank(opts) + feature = fbank(samples.to(device)) + decode_stream.set_features(feature, tail_pad_len=30) + decode_stream.ground_truth = cut.supervisions[0].text + + decode_streams.append(decode_stream) + + while len(decode_streams) >= params.num_decode_streams: + finished_streams = decode_one_chunk( + params=params, model=model, decode_streams=decode_streams + ) + for i in sorted(finished_streams, reverse=True): + decode_results.append( + ( + decode_streams[i].id, + decode_streams[i].ground_truth.split(), + sp.decode(decode_streams[i].decoding_result()).split(), + ) + ) + del decode_streams[i] + + if num % log_interval == 0: + logging.info(f"Cuts processed until now is {num}.") + + # decode final chunks of last sequences + while len(decode_streams): + finished_streams = decode_one_chunk( + params=params, model=model, decode_streams=decode_streams + ) + for i in sorted(finished_streams, reverse=True): + decode_results.append( + ( + decode_streams[i].id, + decode_streams[i].ground_truth.split(), + sp.decode(decode_streams[i].decoding_result()).split(), + ) + ) + del decode_streams[i] + + if params.decoding_method == "greedy_search": + key = "greedy_search" + elif params.decoding_method == "fast_beam_search": + key = ( + f"beam_{params.beam}_" + f"max_contexts_{params.max_contexts}_" + f"max_states_{params.max_states}" + ) + elif params.decoding_method == "modified_beam_search": + key = f"num_active_paths_{params.num_active_paths}" + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + return {key: decode_results} + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir + / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + params.res_dir = params.exp_dir / "streaming" / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + assert params.causal, params.causal + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + params.suffix += f"-chunk-{params.chunk_size}" + params.suffix += f"-left-context-{params.left_context_frames}" + + # for fast_beam_search + if params.decoding_method == "fast_beam_search": + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # and is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints( + params.exp_dir, iteration=-params.iter + )[: params.avg] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if start >= 0: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints( + params.exp_dir, iteration=-params.iter + )[: params.avg + 1] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + model.device = device + + decoding_graph = None + if params.decoding_method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + librispeech = LibriSpeechAsrDataModule(args) + + test_clean_cuts = librispeech.test_clean_cuts() + test_other_cuts = librispeech.test_other_cuts() + + test_sets = ["test-clean", "test-other"] + test_cuts = [test_clean_cuts, test_other_cuts] + + for test_set, test_cut in zip(test_sets, test_cuts): + results_dict = decode_dataset( + cuts=test_cut, + params=params, + model=model, + sp=sp, + decoding_graph=decoding_graph, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/subsampling.py b/egs/librispeech/ASR/zipformer/subsampling.py new file mode 100644 index 000000000..47403f13c --- /dev/null +++ b/egs/librispeech/ASR/zipformer/subsampling.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Daniel Povey, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 typing import Tuple +import warnings + +import torch +from torch import Tensor, nn +from scaling import ( + Balancer, + BiasNorm, + Dropout3, + FloatLike, + Optional, + ScaledConv2d, + ScaleGrad, + ScheduledFloat, + SwooshL, + SwooshR, + Whiten, +) + + +class ConvNeXt(nn.Module): + """ + Our interpretation of the ConvNeXt module as used in https://arxiv.org/pdf/2206.14747.pdf + """ + + def __init__( + self, + channels: int, + hidden_ratio: int = 3, + kernel_size: Tuple[int, int] = (7, 7), + layerdrop_rate: FloatLike = None, + ): + super().__init__() + self.padding = ((kernel_size[0] - 1) // 2, (kernel_size[1] - 1) // 2) + hidden_channels = channels * hidden_ratio + if layerdrop_rate is None: + layerdrop_rate = ScheduledFloat((0.0, 0.2), (20000.0, 0.015)) + self.layerdrop_rate = layerdrop_rate + + self.depthwise_conv = nn.Conv2d( + in_channels=channels, + out_channels=channels, + groups=channels, + kernel_size=kernel_size, + padding=self.padding, + ) + + self.pointwise_conv1 = nn.Conv2d( + in_channels=channels, out_channels=hidden_channels, kernel_size=1 + ) + + self.hidden_balancer = Balancer( + hidden_channels, + channel_dim=1, + min_positive=0.3, + max_positive=1.0, + min_abs=0.75, + max_abs=5.0, + ) + + self.activation = SwooshL() + self.pointwise_conv2 = ScaledConv2d( + in_channels=hidden_channels, + out_channels=channels, + kernel_size=1, + initial_scale=0.01, + ) + + self.out_balancer = Balancer( + channels, + channel_dim=1, + min_positive=0.4, + max_positive=0.6, + min_abs=1.0, + max_abs=6.0, + ) + self.out_whiten = Whiten( + num_groups=1, + whitening_limit=5.0, + prob=(0.025, 0.25), + grad_scale=0.01, + ) + + def forward(self, x: Tensor) -> Tensor: + if torch.jit.is_scripting() or not self.training: + return self.forward_internal(x) + layerdrop_rate = float(self.layerdrop_rate) + + if layerdrop_rate != 0.0: + batch_size = x.shape[0] + mask = ( + torch.rand( + (batch_size, 1, 1, 1), dtype=x.dtype, device=x.device + ) + > layerdrop_rate + ) + else: + mask = None + # turns out this caching idea does not work with --world-size > 1 + # return caching_eval(self.forward_internal, x, mask) + return self.forward_internal(x, mask) + + def forward_internal( + self, x: Tensor, layer_skip_mask: Optional[Tensor] = None + ) -> Tensor: + """ + x layout: (N, C, H, W), i.e. (batch_size, num_channels, num_frames, num_freqs) + + The returned value has the same shape as x. + """ + bypass = x + x = self.depthwise_conv(x) + x = self.pointwise_conv1(x) + x = self.hidden_balancer(x) + x = self.activation(x) + x = self.pointwise_conv2(x) + + if layer_skip_mask is not None: + x = x * layer_skip_mask + + x = bypass + x + x = self.out_balancer(x) + x = x.transpose(1, 3) # (N, W, H, C); need channel dim to be last + x = self.out_whiten(x) + x = x.transpose(1, 3) # (N, C, H, W) + + return x + + def streaming_forward( + self, + x: Tensor, + cached_left_pad: Tensor, + ) -> Tuple[Tensor, Tensor]: + """ + Args: + x layout: (N, C, H, W), i.e. (batch_size, num_channels, num_frames, num_freqs) + cached_left_pad: (batch_size, num_channels, left_pad, num_freqs) + + Returns: + - The returned value has the same shape as x. + - Updated cached_left_pad. + """ + padding = self.padding + + # The length without right padding for depth-wise conv + T = x.size(2) - padding[0] + + bypass = x[:, :, :T, :] + + # Pad left side + assert cached_left_pad.size(2) == padding[0], ( + cached_left_pad.size(2), + padding[0], + ) + x = torch.cat([cached_left_pad, x], dim=2) + # Update cached left padding + cached_left_pad = x[:, :, T : padding[0] + T, :] + + # depthwise_conv + x = torch.nn.functional.conv2d( + x, + weight=self.depthwise_conv.weight, + bias=self.depthwise_conv.bias, + padding=(0, padding[1]), + groups=self.depthwise_conv.groups, + ) + x = self.pointwise_conv1(x) + x = self.hidden_balancer(x) + x = self.activation(x) + x = self.pointwise_conv2(x) + + x = bypass + x + return x, cached_left_pad + + +class Conv2dSubsampling(nn.Module): + """Convolutional 2D subsampling (to 1/2 length). + + Convert an input of shape (N, T, idim) to an output + with shape (N, T', odim), where + T' = (T-3)//2 - 2 == (T-7)//2 + + It is based on + https://github.com/espnet/espnet/blob/master/espnet/nets/pytorch_backend/transformer/subsampling.py # noqa + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + layer1_channels: int = 8, + layer2_channels: int = 32, + layer3_channels: int = 128, + dropout: FloatLike = 0.1, + ) -> None: + """ + Args: + in_channels: + Number of channels in. The input shape is (N, T, in_channels). + Caution: It requires: T >=7, in_channels >=7 + out_channels + Output dim. The output shape is (N, (T-3)//2, out_channels) + layer1_channels: + Number of channels in layer1 + layer1_channels: + Number of channels in layer2 + bottleneck: + bottleneck dimension for 1d squeeze-excite + """ + assert in_channels >= 7 + super().__init__() + + # The ScaleGrad module is there to prevent the gradients + # w.r.t. the weight or bias of the first Conv2d module in self.conv from + # exceeding the range of fp16 when using automatic mixed precision (amp) + # training. (The second one is necessary to stop its bias from getting + # a too-large gradient). + + self.conv = nn.Sequential( + nn.Conv2d( + in_channels=1, + out_channels=layer1_channels, + kernel_size=3, + padding=(0, 1), # (time, freq) + ), + ScaleGrad(0.2), + Balancer(layer1_channels, channel_dim=1, max_abs=1.0), + SwooshR(), + nn.Conv2d( + in_channels=layer1_channels, + out_channels=layer2_channels, + kernel_size=3, + stride=2, + padding=0, + ), + Balancer(layer2_channels, channel_dim=1, max_abs=4.0), + SwooshR(), + nn.Conv2d( + in_channels=layer2_channels, + out_channels=layer3_channels, + kernel_size=3, + stride=(1, 2), # (time, freq) + ), + Balancer(layer3_channels, channel_dim=1, max_abs=4.0), + SwooshR(), + ) + + # just one convnext layer + self.convnext = ConvNeXt(layer3_channels, kernel_size=(7, 7)) + + self.out_width = (((in_channels - 1) // 2) - 1) // 2 + self.layer3_channels = layer3_channels + + self.out = nn.Linear(self.out_width * layer3_channels, out_channels) + # use a larger than normal grad_scale on this whitening module; there is + # only one such module, so there is not a concern about adding together + # many copies of this extra gradient term. + self.out_whiten = Whiten( + num_groups=1, + whitening_limit=ScheduledFloat( + (0.0, 4.0), (20000.0, 8.0), default=4.0 + ), + prob=(0.025, 0.25), + grad_scale=0.02, + ) + + # max_log_eps=0.0 is to prevent both eps and the output of self.out from + # getting large, there is an unnecessary degree of freedom. + self.out_norm = BiasNorm(out_channels) + self.dropout = Dropout3(dropout, shared_dim=1) + + def forward( + self, x: torch.Tensor, x_lens: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Subsample x. + + Args: + x: + Its shape is (N, T, idim). + x_lens: + A tensor of shape (batch_size,) containing the number of frames in + + Returns: + - a tensor of shape (N, ((T-1)//2 - 1)//2, odim) + - output lengths, of shape (batch_size,) + """ + # On entry, x is (N, T, idim) + x = x.unsqueeze(1) # (N, T, idim) -> (N, 1, T, idim) i.e., (N, C, H, W) + # scaling x by 0.1 allows us to use a larger grad-scale in fp16 "amp" (automatic mixed precision) + # training, since the weights in the first convolution are otherwise the limiting factor for getting infinite + # gradients. + x = self.conv(x) + x = self.convnext(x) + + # Now x is of shape (N, odim, ((T-3)//2 - 1)//2, ((idim-1)//2 - 1)//2) + b, c, t, f = x.size() + + x = x.transpose(1, 2).reshape(b, t, c * f) + # now x: (N, ((T-1)//2 - 1))//2, out_width * layer3_channels)) + + x = self.out(x) + # Now x is of shape (N, ((T-1)//2 - 1))//2, odim) + x = self.out_whiten(x) + x = self.out_norm(x) + x = self.dropout(x) + + if torch.jit.is_scripting(): + x_lens = (x_lens - 7) // 2 + else: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + x_lens = (x_lens - 7) // 2 + assert x.size(1) == x_lens.max().item() + + return x, x_lens + + def streaming_forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + cached_left_pad: Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Subsample x. + + Args: + x: + Its shape is (N, T, idim). + x_lens: + A tensor of shape (batch_size,) containing the number of frames in + + Returns: + - a tensor of shape (N, ((T-1)//2 - 1)//2, odim) + - output lengths, of shape (batch_size,) + - updated cache + """ + # On entry, x is (N, T, idim) + x = x.unsqueeze(1) # (N, T, idim) -> (N, 1, T, idim) i.e., (N, C, H, W) + + # T' = (T-7)//2 + x = self.conv(x) + + # T' = (T-7)//2-3 + x, cached_left_pad = self.convnext.streaming_forward( + x, cached_left_pad=cached_left_pad + ) + + # Now x is of shape (N, odim, T', ((idim-1)//2 - 1)//2) + b, c, t, f = x.size() + + x = x.transpose(1, 2).reshape(b, t, c * f) + # now x: (N, T', out_width * layer3_channels)) + + x = self.out(x) + # Now x is of shape (N, T', odim) + x = self.out_norm(x) + + if torch.jit.is_scripting() or torch.jit.is_tracing(): + assert self.convnext.padding[0] == 3 + # The ConvNeXt module needs 3 frames of right padding after subsampling + x_lens = (x_lens - 7) // 2 - 3 + else: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # The ConvNeXt module needs 3 frames of right padding after subsampling + assert self.convnext.padding[0] == 3 + x_lens = (x_lens - 7) // 2 - 3 + + assert x.size(1) == x_lens.max().item() + + return x, x_lens, cached_left_pad + + @torch.jit.export + def get_init_states( + self, + batch_size: int = 1, + device: torch.device = torch.device("cpu"), + ) -> Tensor: + """Get initial states for Conv2dSubsampling module. + It is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + """ + left_pad = self.convnext.padding[0] + freq = self.out_width + channels = self.layer3_channels + cached_embed_left_pad = torch.zeros( + batch_size, channels, left_pad, freq + ).to(device) + + return cached_embed_left_pad diff --git a/egs/librispeech/ASR/zipformer/train.py b/egs/librispeech/ASR/zipformer/train.py new file mode 100755 index 000000000..5af4c9b78 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/train.py @@ -0,0 +1,1362 @@ +#!/usr/bin/env python3 +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo, +# Zengwei Yao, +# Daniel Povey) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# For non-streaming model training: +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --full-libri 1 \ + --max-duration 1000 + +# For streaming model training: +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --causal 1 \ + --full-libri 1 \ + --max-duration 1000 + +""" + + +import argparse +import copy +import logging +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import LibriSpeechAsrDataModule +from zipformer import Zipformer2 +from scaling import ScheduledFloat +from decoder import Decoder +from joiner import Joiner +from subsampling import Conv2dSubsampling +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from model import Transducer +from optim import Eden, ScaledAdam +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.hooks import register_inf_check_hooks +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.utils import ( + AttributeDict, + MetricsTracker, + setup_logger, + str2bool, + get_parameter_groups_with_lrs +) + +LRSchedulerType = Union[ + torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler +] + + +def get_adjusted_batch_count( + params: AttributeDict) -> float: + # returns the number of batches we would have used so far if we had used the reference + # duration. This is for purposes of set_batch_count(). + return (params.batch_idx_train * (params.max_duration * params.world_size) / + params.ref_duration) + + +def set_batch_count( + model: Union[nn.Module, DDP], batch_count: float +) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for name, module in model.named_modules(): + if hasattr(module, 'batch_count'): + module.batch_count = batch_count + if hasattr(module, 'name'): + module.name = name + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,3,4,3,2", + help="Number of zipformer encoder layers per stack, comma separated.", + ) + + parser.add_argument( + "--downsampling-factor", + type=str, + default="1,2,4,8,4,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--feedforward-dim", + type=str, + default="512,768,1024,1536,1024,768", + help="Feedforward dimension of the zipformer encoder layers, per stack, comma separated.", + ) + + parser.add_argument( + "--num-heads", + type=str, + default="4,4,4,8,4,4", + help="Number of attention heads in the zipformer encoder layers: a single int or comma-separated list.", + ) + + parser.add_argument( + "--encoder-dim", + type=str, + default="192,256,384,512,384,256", + help="Embedding dimension in encoder stacks: a single int or comma-separated list." + ) + + parser.add_argument( + "--query-head-dim", + type=str, + default="32", + help="Query/key dimension per head in encoder stacks: a single int or comma-separated list." + ) + + parser.add_argument( + "--value-head-dim", + type=str, + default="12", + help="Value dimension per head in encoder stacks: a single int or comma-separated list." + ) + + parser.add_argument( + "--pos-head-dim", + type=str, + default="4", + help="Positional-encoding dimension per head in encoder stacks: a single int or comma-separated list." + ) + + parser.add_argument( + "--pos-dim", + type=int, + default="48", + help="Positional-encoding embedding dimension" + ) + + parser.add_argument( + "--encoder-unmasked-dim", + type=str, + default="192,192,256,256,256,192", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "A single int or comma-separated list. Must be <= each corresponding encoder_dim." + ) + + parser.add_argument( + "--cnn-module-kernel", + type=str, + default="31,31,15,15,15,31", + help="Sizes of convolutional kernels in convolution modules in each encoder stack: " + "a single int or comma-separated list.", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--causal", + type=str2bool, + default=False, + help="If True, use causal version of model.", + ) + + parser.add_argument( + "--chunk-size", + type=str, + default="16,32,64,-1", + help="Chunk sizes (at 50Hz frame rate) will be chosen randomly from this list during training. " + " Must be just -1 if --causal=False" + ) + + parser.add_argument( + "--left-context-frames", + type=str, + default="64,128,256,-1", + help="Maximum left-contexts for causal training, measured in frames which will " + "be converted to a number of chunks. If splitting into chunks, " + "chunk left-context frames will be chosen randomly from this list; else not relevant." + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--base-lr", + type=float, + default=0.045, + help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=7500, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=3.5, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--ref-duration", + type=float, + default=600, + help="Reference batch duration for purposes of adjusting batch counts for setting various " + "schedules inside the model" + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; " + "2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network)" + "part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--inf-check", + type=str2bool, + default=False, + help="Add hooks to check for infinite module outputs and gradients.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=4000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=30, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=200, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warmup period that dictates the decay of the + scale on "simple" (un-pruned) loss. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 3000, # For the 100h subset, use 800 + # parameters for zipformer + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed. + "warm_step": 2000, + "env_info": get_env_info(), + } + ) + + return params + + +def _to_int_tuple(s: str): + return tuple(map(int, s.split(','))) + + +def get_encoder_embed(params: AttributeDict) -> nn.Module: + # encoder_embed converts the input of shape (N, T, num_features) + # to the shape (N, (T - 7) // 2, encoder_dims). + # That is, it does two things simultaneously: + # (1) subsampling: T -> (T - 7) // 2 + # (2) embedding: num_features -> encoder_dims + # In the normal configuration, we will downsample once more at the end + # by a factor of 2, and most of the encoder stacks will run at a lower + # sampling rate. + encoder_embed = Conv2dSubsampling( + in_channels=params.feature_dim, + out_channels=_to_int_tuple(params.encoder_dim)[0], + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)) + ) + return encoder_embed + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + encoder = Zipformer2( + output_downsampling_factor=2, + downsampling_factor=_to_int_tuple(params.downsampling_factor), + num_encoder_layers=_to_int_tuple(params.num_encoder_layers), + encoder_dim=_to_int_tuple(params.encoder_dim), + encoder_unmasked_dim=_to_int_tuple(params.encoder_unmasked_dim), + query_head_dim=_to_int_tuple(params.query_head_dim), + pos_head_dim=_to_int_tuple(params.pos_head_dim), + value_head_dim=_to_int_tuple(params.value_head_dim), + pos_dim=params.pos_dim, + num_heads=_to_int_tuple(params.num_heads), + feedforward_dim=_to_int_tuple(params.feedforward_dim), + cnn_module_kernel=_to_int_tuple(params.cnn_module_kernel), + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + warmup_batches=4000.0, + causal=params.causal, + chunk_size=_to_int_tuple(params.chunk_size), + left_context_frames=_to_int_tuple(params.left_context_frames), + ) + return encoder + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=max(_to_int_tuple(params.encoder_dim)), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_transducer_model(params: AttributeDict) -> nn.Module: + encoder_embed = get_encoder_embed(params) + encoder = get_encoder_model(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = Transducer( + encoder_embed=encoder_embed, + encoder=encoder, + decoder=decoder, + joiner=joiner, + encoder_dim=int(max(params.encoder_dim.split(','))), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + if "cur_batch_idx" in saved_params: + params["cur_batch_idx"] = saved_params["cur_batch_idx"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute CTC loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Zipformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + device = ( + model.device + if isinstance(model, DDP) + else next(model.parameters()).device + ) + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.warm_step + + texts = batch["supervisions"]["text"] + y = sp.encode(texts, out_type=int) + y = k2.RaggedTensor(y).to(device) + + with torch.set_grad_enabled(is_training): + simple_loss, pruned_loss = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + ) + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + + loss = ( + simple_loss_scale * simple_loss + + pruned_loss_scale * pruned_loss + ) + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = ( + (feature_lens // params.subsampling_factor).sum().item() + ) + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + saved_bad_model = False + + def save_bad_model(suffix: str = ""): + save_checkpoint_impl(filename=params.exp_dir / f"bad-model{suffix}-{rank}.pt", + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=0) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx % 10 == 0: + set_batch_count(model, get_adjusted_batch_count(params)) + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + save_bad_model() + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + + if cur_grad_scale < 8.0 or (cur_grad_scale < 32.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + if not saved_bad_model: + save_bad_model(suffix="-first-warning") + saved_bad_model = True + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + save_bad_model() + raise RuntimeError(f"grad_scale is too small, exiting: {cur_grad_scale}") + + if batch_idx % params.log_interval == 0: + cur_lr = max(scheduler.get_last_lr()) + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary( + tb_writer, "train/tot_", params.batch_idx_train + ) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info(f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB") + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model).to(torch.float64) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], + find_unused_parameters=True) + + optimizer = ScaledAdam( + get_parameter_groups_with_lrs( + model, lr=params.base_lr, include_names=True), + lr=params.base_lr, # should have no effect + clipping_scale=2.0, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + opts = diagnostics.TensorDiagnosticOptions( + 2 ** 22 + ) # allow 4 megabytes per sub-module + diagnostic = diagnostics.attach_diagnostics(model, opts) + + if params.inf_check: + register_inf_check_hooks(model) + + librispeech = LibriSpeechAsrDataModule(args) + + train_cuts = librispeech.train_clean_100_cuts() + if params.full_libri: + train_cuts += librispeech.train_clean_360_cuts() + train_cuts += librispeech.train_other_500_cuts() + + def remove_short_and_long_utt(c: Cut): + # Keep only utterances with duration between 1 second and 20 seconds + # + # Caution: There is a reason to select 20.0 here. Please see + # ../local/display_manifest_statistics.py + # + # You should use ../local/display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + if c.duration < 1.0 or c.duration > 20.0: + logging.warning( + f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + ) + return False + + # In pruned RNN-T, we require that T >= S + # where T is the number of feature frames after subsampling + # and S is the number of tokens in the utterance + + # In ./zipformer.py, the conv module uses the following expression + # for subsampling + T = ((c.num_frames - 7) // 2 + 1) // 2 + tokens = sp.encode(c.supervisions[0].text, out_type=str) + + if T < len(tokens): + logging.warning( + f"Exclude cut with ID {c.id} from training. " + f"Number of frames (before subsampling): {c.num_frames}. " + f"Number of frames (after subsampling): {T}. " + f"Text: {c.supervisions[0].text}. " + f"Tokens: {tokens}. " + f"Number of tokens: {len(tokens)}" + ) + return False + + return True + + train_cuts = train_cuts.filter(remove_short_and_long_utt) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = librispeech.train_dataloaders( + train_cuts, sampler_state_dict=sampler_state_dict + ) + + valid_cuts = librispeech.dev_clean_cuts() + valid_cuts += librispeech.dev_other_cuts() + valid_dl = librispeech.valid_dataloaders(valid_cuts) + + if not params.print_diagnostics: + scan_pessimistic_batches_for_oom( + model=model, + train_dl=train_dl, + optimizer=optimizer, + sp=sp, + params=params, + ) + + scaler = GradScaler(enabled=params.use_fp16, + init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + supervisions = batch["supervisions"] + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = sp.encode(supervisions["text"], out_type=int) + num_tokens = sum(len(i) for i in y) + logging.info(f"num tokens: {num_tokens}") + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + sp: spm.SentencePieceProcessor, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + loss.backward() + optimizer.zero_grad() + except Exception as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + display_and_save_batch(batch, params=params, sp=sp) + raise + logging.info(f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB") + + +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/zipformer.py b/egs/librispeech/ASR/zipformer/zipformer.py new file mode 100644 index 000000000..8d90198fd --- /dev/null +++ b/egs/librispeech/ASR/zipformer/zipformer.py @@ -0,0 +1,2237 @@ +#!/usr/bin/env python3 +# Copyright 2022-2023 Xiaomi Corp. (authors: Daniel Povey, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 math +import warnings +from typing import List, Optional, Tuple, Union +import logging +import torch +import random +from encoder_interface import EncoderInterface +from scaling import ( + Balancer, + BiasNorm, + Dropout2, + ChunkCausalDepthwiseConv1d, + ActivationDropoutAndLinear, + ScaledLinear, # not as in other dirs.. just scales down initial parameter values. + Whiten, + Identity, # more friendly to backward hooks than nn.Identity(), for diagnostic reasons. + penalize_abs_values_gt, + softmax, + ScheduledFloat, + FloatLike, + limit_param_value, + convert_num_channels, +) +from torch import Tensor, nn + + +class Zipformer2(EncoderInterface): + """ + Args: + + Note: all "int or Tuple[int]" arguments below will be treated as lists of the same length + as downsampling_factor if they are single ints or one-element tuples. The length of + downsampling_factor defines the number of stacks. + + output_downsampling_factor (int): how much to downsample at the output. Note: + we also downsample by a factor of 2 in the Conv2dSubsampling encoder. + You should probably leave this at 2. + downsampling_factor (Tuple[int]): downsampling factor for each encoder stack. + Note: this is in addition to the downsampling factor of 2 that is applied in + the frontend (self.encoder_embed). + encoder_dim (Tuple[int]): embedding dimension of each of the encoder stacks, one per + encoder stack. + num_encoder_layers (int or Tuple[int])): number of encoder layers for each stack + encoder_unmasked_dim (int or Tuple[int]): unmasked dimension in each of + the encoder stacks for purposes of per-frame dropout (recommend 256 for + now). + query_head_dim (int or Tuple[int]): dimension of query and key per attention + head: per stack, if a tuple.. + pos_head_dim (int or Tuple[int]): dimension of positional-encoding projection per + attention head + value_head_dim (int or Tuple[int]): dimension of value in each attention head + num_heads: (int or Tuple[int]): number of heads in the self-attention mechanism. + Must be at least 4. + feedforward_dim (int or Tuple[int]): hidden dimension in feedforward modules + cnn_module_kernel (int or Tuple[int])): Kernel size of convolution module + + pos_dim (int): the dimension of each positional-encoding vector prior to projection, + e.g. 128. + + dropout (float): dropout rate + warmup_batches (float): number of batches to warm up over; this controls + dropout of encoder layers. + causal (bool): if True, support chunkwise causal convolution. This should + not hurt WER as no modeling power is lost, but the convolution modules will be + slightly slower and use more memory. Enables use of the chunk_size and + left_context_chunks options in forward(), which simulates streaming + decoding. + chunk_size: (list of int): only set this to other than [-1] if causal; + the chunk size will be randomly chosen from this list. -1 means no chunking. + left_context_frames: (list of int): determines the number of left- + context chunks for causal training; will be rounded to a number of + chunks. Must not be less than cnn_module_kernel (after factoring in + rounding and downsampling); an error will be thrown if this is violated. + """ + def __init__( + self, + output_downsampling_factor: int = 2, + downsampling_factor: Tuple[int] = (2, 4), + encoder_dim: Union[int, Tuple[int]] = 384, + num_encoder_layers: Union[int, Tuple[int]] = 4, + encoder_unmasked_dim: Union[int, Tuple[int]] = 256, + query_head_dim: Union[int, Tuple[int]] = 24, + pos_head_dim: Union[int, Tuple[int]] = 4, + value_head_dim: Union[int, Tuple[int]] = 12, + num_heads: Union[int, Tuple[int]] = 8, + feedforward_dim: Union[int, Tuple[int]] = 1536, + cnn_module_kernel: Union[int, Tuple[int]] = 31, + pos_dim: int = 192, + dropout: FloatLike = None, # see code below for default + warmup_batches: float = 4000.0, + causal: bool = False, + chunk_size: Tuple[int] = [-1], + left_context_frames: Tuple[int] = [-1], + ) -> None: + super(Zipformer2, self).__init__() + + if dropout is None: + dropout = ScheduledFloat((0.0, 0.3), + (20000.0, 0.1)) + + def _to_tuple(x): + """ Converts a single int or a 1-tuple of an int to a tuple with the same length + as downsampling_factor""" + if isinstance(x, int): + x = (x,) + if len(x) == 1: + x = x * len(downsampling_factor) + else: + assert len(x) == len(downsampling_factor) and isinstance(x[0], int) + return x + + self.output_downsampling_factor = output_downsampling_factor # int + self.downsampling_factor = downsampling_factor # tuple + self.encoder_dim = encoder_dim = _to_tuple(encoder_dim) # tuple + self.encoder_unmasked_dim = encoder_unmasked_dim = _to_tuple(encoder_unmasked_dim) # tuple + num_encoder_layers = _to_tuple(num_encoder_layers) + self.query_head_dim = query_head_dim = _to_tuple(query_head_dim) + self.value_head_dim = value_head_dim = _to_tuple(value_head_dim) + pos_head_dim = _to_tuple(pos_head_dim) + self.num_heads = num_heads = _to_tuple(num_heads) + feedforward_dim = _to_tuple(feedforward_dim) + self.cnn_module_kernel = cnn_module_kernel = _to_tuple(cnn_module_kernel) + + self.causal = causal + self.chunk_size = chunk_size + self.left_context_frames = left_context_frames + + for u,d in zip(encoder_unmasked_dim, encoder_dim): + assert u <= d + + # each one will be Zipformer2Encoder or DownsampledZipformer2Encoder + encoders = [] + + num_encoders = len(downsampling_factor) + for i in range(num_encoders): + + encoder_layer = Zipformer2EncoderLayer( + embed_dim=encoder_dim[i], + pos_dim=pos_dim, + num_heads=num_heads[i], + query_head_dim=query_head_dim[i], + pos_head_dim=pos_head_dim[i], + value_head_dim=value_head_dim[i], + feedforward_dim=feedforward_dim[i], + dropout=dropout, + cnn_module_kernel=cnn_module_kernel[i], + causal=causal, + ) + + # For the segment of the warmup period, we let the Conv2dSubsampling + # layer learn something. Then we start to warm up the other encoders. + encoder = Zipformer2Encoder( + encoder_layer, + num_encoder_layers[i], + pos_dim=pos_dim, + dropout=dropout, + warmup_begin=warmup_batches * (i + 1) / (num_encoders + 1), + warmup_end=warmup_batches * (i + 2) / (num_encoders + 1), + final_layerdrop_rate=0.035 * (downsampling_factor[i] ** 0.5), + ) + + if downsampling_factor[i] != 1: + encoder = DownsampledZipformer2Encoder( + encoder, + dim=encoder_dim[i], + downsample=downsampling_factor[i], + dropout=dropout, + ) + + encoders.append(encoder) + + self.encoders = nn.ModuleList(encoders) + + self.downsample_output = SimpleDownsample(max(encoder_dim), + downsample=output_downsampling_factor, + dropout=dropout) + + def get_feature_masks( + self, + x: Tensor) -> Union[List[float], List[Tensor]]: + """ + In eval mode, returns [1.0] * num_encoders; in training mode, returns a number of + randomized feature masks, one per encoder. + On e.g. 15% of frames, these masks will zero out all enocder dims larger than + some supplied number, e.g. >256, so in effect on those frames we are using + a smaller encoer dim. + + We generate the random masks at this level because we want the 2 masks to 'agree' + all the way up the encoder stack. This will mean that the 1st mask will have + mask values repeated self.zipformer_subsampling_factor times. + + Args: + x: the embeddings (needed for the shape and dtype and device), of shape + (1, batch_size, encoder_dims0) + """ + num_encoders = len(self.encoder_dim) + if not self.training: + return [ 1.0 ] * num_encoders + + (num_frames0, batch_size, _encoder_dims0) = x.shape + + assert self.encoder_dim[0] == _encoder_dims0 + + feature_mask_dropout_prob = 0.125 + + # mask1 shape: (1, batch_size, 1) + mask1 = (torch.rand(1, batch_size, 1, + device=x.device) > + feature_mask_dropout_prob).to(x.dtype) + + # mask2 has additional sequences masked, about twice the number. + mask2 = torch.logical_and(mask1, + (torch.rand(1, batch_size, 1, + device=x.device) > + feature_mask_dropout_prob).to(x.dtype)) + + # dim: (1, batch_size, 2) + mask = torch.cat((mask1, mask2), dim=-1) + + feature_masks = [] + for i in range(num_encoders): + channels = self.encoder_dim[i] + feature_mask = torch.ones(1, batch_size, channels, + dtype=x.dtype, device=x.device) + u1 = self.encoder_unmasked_dim[i] + u2 = u1 + (channels - u1) // 2 + + feature_mask[:, :, u1:u2] *= mask[..., 0:1] + feature_mask[:, :, u2:] *= mask[..., 1:2] + + feature_masks.append(feature_mask) + + return feature_masks + + def get_chunk_info(self) -> Tuple[int, int]: + """ + Returns chunk_size and left_context_chunks. + """ + if not self.causal: + return -1, -1 + + if torch.jit.is_scripting(): + assert len(self.chunk_size) == 1, self.chunk_size + chunk_size = self.chunk_size[0] + else: + chunk_size = random.choice(self.chunk_size) + + if chunk_size == -1: + left_context_chunks = -1 + else: + if torch.jit.is_scripting(): + assert len(self.left_context_frames) == 1, self.left_context_frames + left_context_frames = self.left_context_frames[0] + else: + left_context_frames = random.choice(self.left_context_frames) + # Note: in Python, -1 // n == -1 for n > 0 + left_context_chunks = left_context_frames // chunk_size + if left_context_chunks == 0: + left_context_chunks = 1 + + return chunk_size, left_context_chunks + + def forward( + self, x: Tensor, + x_lens: Tensor, + src_key_padding_mask: Optional[Tensor] = None, + ) -> Tuple[Tensor, Tensor]: + """ + Args: + x: + The input tensor. Its shape is (seq_len, batch_size, feature_dim). + x_lens: + A tensor of shape (batch_size,) containing the number of frames in + `x` before padding. + src_key_padding_mask: + The mask for padding, of shape (batch_size, seq_len); True means + masked position. May be None. + Returns: + Return a tuple containing 2 tensors: + - embeddings: its shape is (output_seq_len, batch_size, max(encoder_dim)) + - lengths, a tensor of shape (batch_size,) containing the number + of frames in `embeddings` before padding. + """ + outputs = [] + if torch.jit.is_scripting(): + feature_masks = [1.0] * len(self.encoder_dim) + else: + feature_masks = self.get_feature_masks(x) + + chunk_size, left_context_chunks = self.get_chunk_info() + + if torch.jit.is_scripting(): + # Not support exporting a model for simulating streaming decoding + attn_mask = None + else: + attn_mask = self._get_attn_mask(x, chunk_size, left_context_chunks) + + for i, module in enumerate(self.encoders): + ds = self.downsampling_factor[i] + x = convert_num_channels(x, self.encoder_dim[i]) + + x = module(x, + chunk_size=chunk_size, + feature_mask=feature_masks[i], + src_key_padding_mask=(None if src_key_padding_mask is None + else src_key_padding_mask[...,::ds]), + attn_mask=attn_mask) + outputs.append(x) + + # if the last output has the largest dimension, x will be unchanged, + # it will be the same as outputs[-1]. Otherwise it will be concatenated + # from different pieces of 'outputs', taking each dimension from the + # most recent output that has it present. + x = self._get_full_dim_output(outputs) + x = self.downsample_output(x) + # class Downsample has this rounding behavior.. + assert self.output_downsampling_factor == 2 + if torch.jit.is_scripting(): + lengths = (x_lens + 1) // 2 + else: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + lengths = (x_lens + 1) // 2 + + return x, lengths + + def _get_attn_mask( + self, x: Tensor, + chunk_size: int, + left_context_chunks: int + ) -> Optional[Tensor]: + """ + Return None if chunk_size == -1, else return attention mask of shape + (seq_len, seq_len), interpreted as (tgt_seq_len, src_seq_len). True + means a masked position. + Args: + x: embeddings after self.encoder_embed(), of shape (seq_len, batch_size, embed_dim). + chunk_size: chunk size, must divide + """ + if chunk_size <= 0: + return None + assert all(chunk_size % d == 0 for d in self.downsampling_factor) + if left_context_chunks >= 0: + num_encoders = len(self.encoder_dim) + assert all (chunk_size * left_context_chunks >= + (self.cnn_module_kernel[i] // 2) * self.downsampling_factor[i] + for i in range(num_encoders)) + else: + left_context_chunks = 1000000 + + seq_len = x.shape[0] + + # t is frame index, shape (seq_len,) + t = torch.arange(seq_len, dtype=torch.int32, device=x.device) + # c is chunk index for each frame, shape (seq_len,) + if torch.jit.is_scripting(): + c = t // chunk_size + else: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + c = t // chunk_size + src_c = c + tgt_c = c.unsqueeze(-1) + + attn_mask = torch.logical_or(src_c > tgt_c, + src_c < tgt_c - left_context_chunks) + if __name__ == "__main__": + logging.info(f"attn_mask = {attn_mask}") + return attn_mask + + def _get_full_dim_output(self, outputs: List[Tensor]): + num_encoders = len(self.encoder_dim) + assert len(outputs) == num_encoders + output_dim = max(self.encoder_dim) + output_pieces = [ outputs[-1] ] + cur_dim = self.encoder_dim[-1] + for i in range(num_encoders - 2, -1, -1): + d = self.encoder_dim[i] + if d > cur_dim: + this_output = outputs[i] + output_pieces.append(this_output[..., cur_dim:d]) + cur_dim = d + assert cur_dim == output_dim + return torch.cat(output_pieces, dim=-1) + + def streaming_forward( + self, + x: Tensor, + x_lens: Tensor, + states: List[Tensor], + src_key_padding_mask: Tensor, + ) -> Tuple[Tensor, Tensor, List[Tensor]]: + """ + Args: + x: + The input tensor. Its shape is (seq_len, batch_size, feature_dim). + x_lens: + A tensor of shape (batch_size,) containing the number of frames in + `x` before padding. + states: list of cached tensors of all encoder layers. For layer-i, + states[i*6:(i+1)*6] is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, + cached_conv1, cached_conv2). + src_key_padding_mask: + The mask for padding, of shape (batch_size, seq_len); True means + masked position. May be None. + Returns: + Return a tuple containing 2 tensors: + - embeddings: its shape is (output_seq_len, batch_size, max(encoder_dim)) + - lengths, a tensor of shape (batch_size,) containing the number + of frames in `embeddings` before padding. + - updated states + """ + outputs = [] + new_states = [] + layer_offset = 0 + + for i, module in enumerate(self.encoders): + num_layers = module.num_layers + ds = self.downsampling_factor[i] + x = convert_num_channels(x, self.encoder_dim[i]) + + x, new_layer_states = module.streaming_forward( + x, + states=states[layer_offset * 6 : (layer_offset + num_layers) * 6], + left_context_len=self.left_context_frames[0] // ds, + src_key_padding_mask=src_key_padding_mask[..., ::ds], + ) + layer_offset += num_layers + outputs.append(x) + new_states += new_layer_states + + # if the last output has the largest dimension, x will be unchanged, + # it will be the same as outputs[-1]. Otherwise it will be concatenated + # from different pieces of 'outputs', taking each dimension from the + # most recent output that has it present. + x = self._get_full_dim_output(outputs) + x = self.downsample_output(x) + # class Downsample has this rounding behavior.. + assert self.output_downsampling_factor == 2 + if torch.jit.is_scripting() or torch.jit.is_tracing(): + lengths = (x_lens + 1) // 2 + else: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + lengths = (x_lens + 1) // 2 + + return x, lengths, new_states + + @torch.jit.export + def get_init_states( + self, + batch_size: int = 1, + device: torch.device = torch.device("cpu"), + ) -> List[Tensor]: + """Get initial states. + + A list of cached tensors of all encoder layers. For layer-i, states[i*6:(i+1)*6] + is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + """ + states = [] + for i, module in enumerate(self.encoders): + num_layers = module.num_layers + embed_dim = self.encoder_dim[i] + ds = self.downsampling_factor[i] + num_heads = self.num_heads[i] + key_dim = self.query_head_dim[i] * num_heads + value_dim = self.value_head_dim[i] * num_heads + downsample_left = self.left_context_frames[0] // ds + nonlin_attn_head_dim = 3 * embed_dim // 4 + conv_left_pad = self.cnn_module_kernel[i] // 2 + for layer in range(num_layers): + cached_key = torch.zeros(downsample_left, batch_size, key_dim).to(device) + cached_nonlin_attn = torch.zeros(1, batch_size, downsample_left, nonlin_attn_head_dim).to(device) + cached_val1 = torch.zeros(downsample_left, batch_size, value_dim).to(device) + cached_val2 = torch.zeros(downsample_left, batch_size, value_dim).to(device) + cached_conv1 = torch.zeros(batch_size, embed_dim, conv_left_pad).to(device) + cached_conv2 = torch.zeros(batch_size, embed_dim, conv_left_pad).to(device) + states += [cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2] + + return states + + +def _whitening_schedule(x: float, ratio: float = 2.0) -> ScheduledFloat: + return ScheduledFloat((0.0, x), + (20000.0, ratio * x), + default=x) + + +def _balancer_schedule(min_prob: float): + return ScheduledFloat((0.0, 0.4), (8000.0, min_prob)) + + +class Zipformer2EncoderLayer(nn.Module): + """ + Args: + embed_dim: the number of expected features in the input (required). + nhead: the number of heads in the multiheadattention models (required). + feedforward_dim: the dimension of the feedforward network model (default=2048). + dropout: the dropout value (default=0.1). + cnn_module_kernel (int): Kernel size of convolution module. + + Examples:: + >>> encoder_layer = Zipformer2EncoderLayer(embed_dim=512, nhead=8) + >>> src = torch.rand(10, 32, 512) + >>> pos_emb = torch.rand(32, 19, 512) + >>> out = encoder_layer(src, pos_emb) + """ + def __init__( + self, + embed_dim: int, + pos_dim: int, + num_heads: int, + query_head_dim: int, + pos_head_dim: int, + value_head_dim: int, + feedforward_dim: int, + dropout: FloatLike = 0.1, + cnn_module_kernel: int = 31, + causal: bool = False, + attention_skip_rate: FloatLike = ScheduledFloat((0.0, 0.2), (4000.0, 0.05), (16000, 0.0), default=0), + conv_skip_rate: FloatLike = ScheduledFloat((0.0, 0.2), (4000.0, 0.05), (16000, 0.0), default=0), + const_attention_rate: FloatLike = ScheduledFloat((0.0, 0.25), (4000.0, 0.025), default=0), + ff2_skip_rate: FloatLike = ScheduledFloat((0.0, 0.1), (4000.0, 0.01), (50000.0, 0.0)), + ff3_skip_rate: FloatLike = ScheduledFloat((0.0, 0.1), (4000.0, 0.01), (50000.0, 0.0)), + bypass_skip_rate: FloatLike = ScheduledFloat((0.0, 0.5), (4000.0, 0.02), default=0), + ) -> None: + super(Zipformer2EncoderLayer, self).__init__() + self.embed_dim = embed_dim + + # self.bypass implements layer skipping as well as bypass; see its default values. + self.bypass = BypassModule(embed_dim, skip_rate=bypass_skip_rate, + straight_through_rate=0) + # bypass_mid is bypass used in the middle of the layer. + self.bypass_mid = BypassModule(embed_dim, straight_through_rate=0) + + # skip probability for dynamic modules (meaning: anything but feedforward). + self.attention_skip_rate = copy.deepcopy(attention_skip_rate) + # an additional skip probability that applies to ConvModule to stop it from + # contributing too much early on. + self.conv_skip_rate = copy.deepcopy(conv_skip_rate) + + # ff2_skip_rate is to prevent the ff2 module from having output that's too big + # compared to its residual. + self.ff2_skip_rate = copy.deepcopy(ff2_skip_rate) + self.ff3_skip_rate = copy.deepcopy(ff3_skip_rate) + + self.const_attention_rate = copy.deepcopy(const_attention_rate) + + self.self_attn_weights = RelPositionMultiheadAttentionWeights( + embed_dim, pos_dim=pos_dim, num_heads=num_heads, + query_head_dim=query_head_dim, pos_head_dim=pos_head_dim, + dropout=0.0, + ) + + self.self_attn1 = SelfAttention(embed_dim, num_heads, + value_head_dim) + + self.self_attn2 = SelfAttention(embed_dim, num_heads, + value_head_dim) + + self.feed_forward1 = FeedforwardModule(embed_dim, + (feedforward_dim * 3) // 4, + dropout) + + self.feed_forward2 = FeedforwardModule(embed_dim, + feedforward_dim, + dropout) + + self.feed_forward3 = FeedforwardModule(embed_dim, + (feedforward_dim * 5) // 4, + dropout) + + self.nonlin_attention = NonlinAttention(embed_dim, + hidden_channels=3 * embed_dim // 4) + + self.conv_module1 = ConvolutionModule(embed_dim, + cnn_module_kernel, + causal=causal) + + self.conv_module2 = ConvolutionModule(embed_dim, + cnn_module_kernel, + causal=causal) + + # TODO: remove it + self.bypass_scale = nn.Parameter(torch.full((embed_dim,), 0.5)) + + self.norm = BiasNorm(embed_dim) + + self.balancer1 = Balancer( + embed_dim, channel_dim=-1, + min_positive=0.45, max_positive=0.55, + min_abs=0.2, max_abs=4.0, + ) + + # balancer for output of NonlinAttentionModule + self.balancer_na = Balancer( + embed_dim, channel_dim=-1, + min_positive=0.3, max_positive=0.7, + min_abs=ScheduledFloat((0.0, 0.004), (4000.0, 0.02)), + prob=0.05, # out of concern for memory usage + ) + + # balancer for output of feedforward2, prevent it from staying too + # small. give this a very small probability, even at the start of + # training, it's to fix a rare problem and it's OK to fix it slowly. + self.balancer_ff2 = Balancer( + embed_dim, channel_dim=-1, + min_positive=0.3, max_positive=0.7, + min_abs=ScheduledFloat((0.0, 0.0), (4000.0, 0.1), default=0.0), + max_abs=2.0, + prob=0.05, + ) + + self.balancer_ff3 = Balancer( + embed_dim, channel_dim=-1, + min_positive=0.3, max_positive=0.7, + min_abs=ScheduledFloat((0.0, 0.0), (4000.0, 0.2), default=0.0), + max_abs=4.0, + prob=0.05, + ) + + self.whiten = Whiten(num_groups=1, + whitening_limit=_whitening_schedule(4.0, ratio=3.0), + prob=(0.025, 0.25), + grad_scale=0.01) + + self.balancer2 = Balancer( + embed_dim, channel_dim=-1, + min_positive=0.45, max_positive=0.55, + min_abs=0.1, max_abs=4.0, + ) + + def get_sequence_dropout_mask(self, x: Tensor, dropout_rate: float) -> Optional[Tensor]: + if dropout_rate == 0.0 or not self.training or torch.jit.is_scripting(): + return None + batch_size = x.shape[1] + mask = (torch.rand(batch_size, 1, device=x.device) > dropout_rate).to(x.dtype) + return mask + + def sequence_dropout(self, x: Tensor, dropout_rate: float) -> Tensor: + """ + Apply sequence-level dropout to x. + x shape: (seq_len, batch_size, embed_dim) + """ + dropout_mask = self.get_sequence_dropout_mask(x, dropout_rate) + if dropout_mask is None: + return x + else: + return x * dropout_mask + + def forward( + self, + src: Tensor, + pos_emb: Tensor, + chunk_size: int = -1, + attn_mask: Optional[Tensor] = None, + src_key_padding_mask: Optional[Tensor] = None, + ) -> Tensor: + """ + Pass the input through the encoder layer. + Args: + src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). + pos_emb: (1, 2*seq_len-1, pos_emb_dim) or (batch_size, 2*seq_len-1, pos_emb_dim) + chunk_size: the number of frames per chunk, of >= 0; if -1, no chunking. + feature_mask: something that broadcasts with src, that we'll multiply `src` + by at every layer: if a Tensor, likely of shape (seq_len, batch_size, embedding_dim) + attn_mask: the attention mask, of shape (batch_size, seq_len, seq_len) or (seq_len, seq_len), + interpreted as (batch_size, tgt_seq_len, src_seq_len) or (tgt_seq_len, src_seq_len). + True means masked position. May be None. + src_key_padding_mask: the mask for padding, of shape (batch_size, seq_len); True means + masked position. May be None. + + Returns: + A tensor which has the same shape as src + """ + src_orig = src + + # dropout rate for non-feedforward submodules + if torch.jit.is_scripting(): + attention_skip_rate = 0.0 + else: + attention_skip_rate = float(self.attention_skip_rate) if self.training else 0.0 + + # attn_weights: (num_heads, batch_size, seq_len, seq_len) + attn_weights = self.self_attn_weights( + src, + pos_emb=pos_emb, + attn_mask=attn_mask, + key_padding_mask=src_key_padding_mask, + ) + + src = src + self.feed_forward1(src) + + self_attn_dropout_mask = self.get_sequence_dropout_mask(src, attention_skip_rate) + + selected_attn_weights = attn_weights[0:1] + if torch.jit.is_scripting(): + pass + elif not self.training and random.random() < float(self.const_attention_rate): + # Make attention weights constant. The intention is to + # encourage these modules to do something similar to an + # averaging-over-time operation. + # only need the mask, can just use the 1st one and expand later + selected_attn_weights = selected_attn_weights[0:1] + selected_attn_weights = (selected_attn_weights > 0.0).to(selected_attn_weights.dtype) + selected_attn_weights = selected_attn_weights * (1.0 / selected_attn_weights.sum(dim=-1, keepdim=True)) + + na = self.balancer_na(self.nonlin_attention(src, selected_attn_weights)) + + src = src + (na if self_attn_dropout_mask is None else na * self_attn_dropout_mask) + + self_attn = self.self_attn1(src, attn_weights) + + src = src + (self_attn if self_attn_dropout_mask is None else self_attn * self_attn_dropout_mask) + + if torch.jit.is_scripting(): + conv_skip_rate = 0.0 + else: + conv_skip_rate = float(self.conv_skip_rate) if self.training else 0.0 + src = src + self.sequence_dropout(self.conv_module1(src, chunk_size=chunk_size, + src_key_padding_mask=src_key_padding_mask), + conv_skip_rate) + + if torch.jit.is_scripting(): + ff2_skip_rate = 0.0 + else: + ff2_skip_rate = float(self.ff2_skip_rate) if self.training else 0.0 + src = src + self.sequence_dropout(self.balancer_ff2(self.feed_forward2(src)), + ff2_skip_rate) + + # bypass in the middle of the layer. + src = self.bypass_mid(src_orig, src) + + self_attn = self.self_attn2(src, attn_weights) + + src = src + (self_attn if self_attn_dropout_mask is None else self_attn * self_attn_dropout_mask) + + if torch.jit.is_scripting(): + conv_skip_rate = 0.0 + else: + conv_skip_rate = float(self.conv_skip_rate) if self.training else 0.0 + src = src + self.sequence_dropout(self.conv_module2(src, chunk_size=chunk_size, + src_key_padding_mask=src_key_padding_mask), + conv_skip_rate) + + if torch.jit.is_scripting(): + ff3_skip_rate = 0.0 + else: + ff3_skip_rate = float(self.ff3_skip_rate) if self.training else 0.0 + src = src + self.sequence_dropout(self.balancer_ff3(self.feed_forward3(src)), + ff3_skip_rate) + + src = self.balancer1(src) + src = self.norm(src) + + src = self.bypass(src_orig, src) + + src = self.balancer2(src) + src = self.whiten(src) + + return src + + def streaming_forward( + self, + src: Tensor, + pos_emb: Tensor, + cached_key: Tensor, + cached_nonlin_attn: Tensor, + cached_val1: Tensor, + cached_val2: Tensor, + cached_conv1: Tensor, + cached_conv2: Tensor, + left_context_len: int, + src_key_padding_mask: Tensor, + ) -> Tuple[Tensor, Tensor, Tensor, Tensor, Tensor, Tensor, Tensor]: + """Pass the input through the encoder layer in streaming forward mode. + + Args: + src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). + pos_emb: (1, left_context_len+2*seq_len-1, pos_emb_dim) or + (batch_size, left_context_len+2*seq_len-1, pos_emb_dim) + cached_key: cached attention key tensor of left context, + of shape (left_context_len, batch_size, key_dim) + cached_nonlin_attn: left context for nonlin_attention module, a Tensor of shape + (num_heads, batch_size, left_context_len, head_dim) + cached_val1: cached left context for the first attention module, + of shape (left_context_len, batch_size, value_dim) + cached_val2: cached left context for the second attention module, + of shape (left_context_len, batch_size, value_dim) + cached_conv1: cached left context for the first convolution module, + of shape (batch_size, channels, left_pad) + cached_conv2: cached left context for the second convolution module, + of shape (batch_size, channels, left_pad) + left_context_len: number of left context frames. + src_key_padding_mask: the mask for padding, of shape + (batch_size, left_context_len + seq_len); True means masked position. + May be None. + + Returns: + - x, with the same shape as src + - updated cached_key + - updated cached_nonlin_attn + - updated cached_val1 + - updated cached_val2 + - updated cached_conv1 + - updated cached_conv2 + """ + src_orig = src + + # attn_weights: (num_heads, batch_size, seq_len, seq_len) + attn_weights, cached_key = self.self_attn_weights.streaming_forward( + src, + pos_emb=pos_emb, + cached_key=cached_key, + left_context_len=left_context_len, + key_padding_mask=src_key_padding_mask, + ) + + src = src + self.feed_forward1(src) + + na, cached_nonlin_attn = self.nonlin_attention.streaming_forward( + src, + attn_weights[0:1], + cached_x=cached_nonlin_attn, + left_context_len=left_context_len, + ) + src = src + na + + self_attn, cached_val1 = self.self_attn1.streaming_forward( + src, + attn_weights=attn_weights, + cached_val=cached_val1, + left_context_len=left_context_len, + ) + src = src + self_attn + + src_conv, cached_conv1 = self.conv_module1.streaming_forward( + src, + cache=cached_conv1, + src_key_padding_mask=src_key_padding_mask[:, left_context_len:], + ) + src = src + src_conv + + src = src + self.feed_forward2(src) + + # bypass in the middle of the layer. + src = self.bypass_mid(src_orig, src) + + self_attn, cached_val2 = self.self_attn2.streaming_forward( + src, + attn_weights=attn_weights, + cached_val=cached_val2, + left_context_len=left_context_len, + ) + src = src + self_attn + + src_conv, cached_conv2 = self.conv_module2.streaming_forward( + src, + cache=cached_conv2, + src_key_padding_mask=src_key_padding_mask[:, left_context_len:], + ) + src = src + src_conv + + src = src + self.feed_forward3(src) + + src = self.norm(src) + + src = self.bypass(src_orig, src) + + return ( + src, + cached_key, + cached_nonlin_attn, + cached_val1, + cached_val2, + cached_conv1, + cached_conv2, + ) + + +class Zipformer2Encoder(nn.Module): + r"""Zipformer2Encoder is a stack of N encoder layers + + Args: + encoder_layer: an instance of the Zipformer2EncoderLayer() class (required). + num_layers: the number of sub-encoder-layers in the encoder (required). + pos_dim: the dimension for the relative positional encoding + + Examples:: + >>> encoder_layer = Zipformer2EncoderLayer(embed_dim=512, nhead=8) + >>> zipformer_encoder = Zipformer2Encoder(encoder_layer, num_layers=6) + >>> src = torch.rand(10, 32, 512) + >>> out = zipformer_encoder(src) + """ + def __init__( + self, + encoder_layer: nn.Module, + num_layers: int, + pos_dim: int, + dropout: float, + warmup_begin: float, + warmup_end: float, + initial_layerdrop_rate: float = 0.5, + final_layerdrop_rate: float = 0.05, + ) -> None: + super().__init__() + self.encoder_pos = CompactRelPositionalEncoding(pos_dim, dropout_rate=0.15, + length_factor=1.0) + + self.layers = nn.ModuleList( + [copy.deepcopy(encoder_layer) for i in range(num_layers)] + ) + self.num_layers = num_layers + + assert 0 <= warmup_begin <= warmup_end + + delta = (1. / num_layers) * (warmup_end - warmup_begin) + cur_begin = warmup_begin # interpreted as a training batch index + for i in range(num_layers): + cur_end = cur_begin + delta + self.layers[i].bypass.skip_rate = ScheduledFloat((cur_begin, initial_layerdrop_rate), + (cur_end, final_layerdrop_rate), + default=0.0) + cur_begin = cur_end + + def forward( + self, + src: Tensor, + chunk_size: int = -1, + feature_mask: Union[Tensor, float] = 1.0, + attn_mask: Optional[Tensor] = None, + src_key_padding_mask: Optional[Tensor] = None, + ) -> Tensor: + r"""Pass the input through the encoder layers in turn. + + Args: + src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). + chunk_size: the number of frames per chunk, of >= 0; if -1, no chunking. + feature_mask: something that broadcasts with src, that we'll multiply `src` + by at every layer: if a Tensor, likely of shape (seq_len, batch_size, embedding_dim) + attn_mask: the attention mask, of shape (batch_size, seq_len, seq_len) or (seq_len, seq_len), + interpreted as (batch_size, tgt_seq_len, src_seq_len) or (tgt_seq_len, src_seq_len). + True means masked position. May be None. + src_key_padding_mask: the mask for padding, of shape (batch_size, seq_len); True means + masked position. May be None. + + Returns: a Tensor with the same shape as src. + """ + pos_emb = self.encoder_pos(src) + output = src + + if not torch.jit.is_scripting(): + output = output * feature_mask + + for i, mod in enumerate(self.layers): + output = mod( + output, + pos_emb, + chunk_size=chunk_size, + attn_mask=attn_mask, + src_key_padding_mask=src_key_padding_mask, + ) + + if not torch.jit.is_scripting(): + output = output * feature_mask + + return output + + def streaming_forward( + self, + src: Tensor, + states: List[Tensor], + left_context_len: int, + src_key_padding_mask: Tensor, + ) -> Tuple[Tensor, List[Tensor]]: + r"""Pass the input through the encoder layers in turn. + + Args: + src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). + states: list of cached tensors of N encoder layers. For layer-i, states[i*6:(i+1)*6] is + (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + left_context_len: Number of left context frames. + src_key_padding_mask: the mask for padding, of shape + (batch_size, left_context_len + seq_len); True means masked position. + May be None. + + Returns: + - output, a Tensor with the same shape as src. + - updated states + """ + pos_emb = self.encoder_pos(src, left_context_len) + output = src + + new_states = [] + for i, mod in enumerate(self.layers): + ( + cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2 + ) = states[i * 6: (i + 1) * 6] + ( + output, + new_cached_key, + new_cached_nonlin_attn, + new_cached_val1, + new_cached_val2, + new_cached_conv1, + new_cached_conv2 + ) = mod.streaming_forward( + output, + pos_emb, + cached_key=cached_key, + cached_nonlin_attn=cached_nonlin_attn, + cached_val1=cached_val1, + cached_val2=cached_val2, + cached_conv1=cached_conv1, + cached_conv2=cached_conv2, + left_context_len=left_context_len, + src_key_padding_mask=src_key_padding_mask, + ) + new_states += [ + new_cached_key, + new_cached_nonlin_attn, + new_cached_val1, + new_cached_val2, + new_cached_conv1, + new_cached_conv2, + ] + + return output, new_states + + +class BypassModule(nn.Module): + """ + An nn.Module that implements a learnable bypass scale, and also randomized per-sequence + layer-skipping. The bypass is limited during early stages of training to be close to + "straight-through", i.e. to not do the bypass operation much initially, in order to + force all the modules to learn something. + """ + def __init__( + self, + embed_dim: int, + skip_rate: FloatLike = 0.0, + straight_through_rate: FloatLike = 0.0, + scale_min: FloatLike = ScheduledFloat((0.0, 0.9), (20000.0, 0.2), default=0), + scale_max: FloatLike = 1.0): + super().__init__() + self.bypass_scale = nn.Parameter(torch.full((embed_dim,), 0.5)) + self.skip_rate = copy.deepcopy(skip_rate) + self.straight_through_rate = copy.deepcopy(straight_through_rate) + self.scale_min = copy.deepcopy(scale_min) + self.scale_max = copy.deepcopy(scale_max) + + def _get_bypass_scale(self, batch_size: int): + # returns bypass-scale of shape (num_channels,), + # or (batch_size, num_channels,). This is actually the + # scale on the non-residual term, so 0 correponds to bypassing + # this module. + if torch.jit.is_scripting() or not self.training: + return self.bypass_scale + else: + ans = limit_param_value(self.bypass_scale, + min=float(self.scale_min), + max=float(self.scale_max)) + skip_rate = float(self.skip_rate) + if skip_rate != 0.0: + mask = torch.rand((batch_size, 1), device=ans.device) > skip_rate + ans = ans * mask + # now ans is of shape (batch_size, num_channels), and is zero for sequences + # on which we have randomly chosen to do layer-skipping. + straight_through_rate = float(self.straight_through_rate) + if straight_through_rate != 0.0: + mask = torch.rand((batch_size, 1), device=ans.device) < straight_through_rate + ans = torch.maximum(ans, mask.to(ans.dtype)) + return ans + + def forward(self, + src_orig: Tensor, + src: Tensor): + """ + Args: src_orig and src are both of shape (seq_len, batch_size, num_channels) + Returns: something with the same shape as src and src_orig + """ + bypass_scale = self._get_bypass_scale(src.shape[1]) + return src_orig + (src - src_orig) * bypass_scale + + +class DownsampledZipformer2Encoder(nn.Module): + r""" + DownsampledZipformer2Encoder is a zipformer encoder evaluated at a reduced frame rate, + after convolutional downsampling, and then upsampled again at the output, and combined + with the origin input, so that the output has the same shape as the input. + """ + def __init__(self, + encoder: nn.Module, + dim: int, + downsample: int, + dropout: FloatLike): + super(DownsampledZipformer2Encoder, self).__init__() + self.downsample_factor = downsample + self.downsample = SimpleDownsample(dim, + downsample, dropout) + self.num_layers = encoder.num_layers + self.encoder = encoder + self.upsample = SimpleUpsample(dim, downsample) + self.out_combiner = BypassModule(dim, straight_through_rate=0) + + def forward( + self, + src: Tensor, + chunk_size: int = -1, + feature_mask: Union[Tensor, float] = 1.0, + attn_mask: Optional[Tensor] = None, + src_key_padding_mask: Optional[Tensor] = None, + ) -> Tensor: + r"""Downsample, go through encoder, upsample. + + Args: + src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). + feature_mask: something that broadcasts with src, that we'll multiply `src` + by at every layer: if a Tensor, likely of shape (seq_len, batch_size, embedding_dim) + attn_mask: the attention mask, of shape (batch_size, seq_len, seq_len) or (seq_len, seq_len), + interpreted as (batch_size, tgt_seq_len, src_seq_len) or (tgt_seq_len, src_seq_len). + True means masked position. May be None. + src_key_padding_mask: the mask for padding, of shape (batch_size, seq_len); True means + masked position. May be None. + + Returns: a Tensor with the same shape as src. + """ + src_orig = src + src = self.downsample(src) + ds = self.downsample_factor + if attn_mask is not None: + attn_mask = attn_mask[::ds,::ds] + + src = self.encoder( + src, + chunk_size=chunk_size // ds, + feature_mask=feature_mask, + attn_mask=attn_mask, + src_key_padding_mask=src_key_padding_mask, + ) + src = self.upsample(src) + # remove any extra frames that are not a multiple of downsample_factor + src = src[:src_orig.shape[0]] + + return self.out_combiner(src_orig, src) + + def streaming_forward( + self, + src: Tensor, + states: List[Tensor], + left_context_len: int, + src_key_padding_mask: Tensor, + ) -> Tuple[Tensor, List[Tensor]]: + r"""Downsample, go through encoder, upsample, in streaming forward mode. + + Args: + src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). + states: list of cached tensors of N encoder layers. For layer-i, states[i*6:(i+1)*6] is + (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + left_context_len: Number of left context frames. + src_key_padding_mask: the mask for padding, of shape (batch_size, left_context_len+seq_len); + True means masked position. May be None. + + Returns: + - output, a Tensor with the same shape as src. + - updated states + """ + src_orig = src + src = self.downsample(src) + + src, new_states = self.encoder.streaming_forward( + src, + states=states, + left_context_len=left_context_len, + src_key_padding_mask=src_key_padding_mask, + ) + src = self.upsample(src) + # remove any extra frames that are not a multiple of downsample_factor + src = src[:src_orig.shape[0]] + + return self.out_combiner(src_orig, src), new_states + + +class SimpleDownsample(torch.nn.Module): + """ + Does downsampling with attention, by weighted sum, and a projection.. + """ + def __init__(self, + channels: int, + downsample: int, + dropout: FloatLike): + super(SimpleDownsample, self).__init__() + + self.bias = nn.Parameter(torch.zeros(downsample)) + + self.name = None # will be set from training code + self.dropout = copy.deepcopy(dropout) + + self.downsample = downsample + + def forward(self, + src: Tensor) -> Tensor: + """ + x: (seq_len, batch_size, in_channels) + Returns a tensor of shape + ( (seq_len+downsample-1)//downsample, batch_size, channels) + """ + (seq_len, batch_size, in_channels) = src.shape + ds = self.downsample + d_seq_len = (seq_len + ds - 1) // ds + + # Pad to an exact multiple of self.downsample + if seq_len != d_seq_len * ds: + # right-pad src, repeating the last element. + pad = d_seq_len * ds - seq_len + src_extra = src[src.shape[0]-1:].expand(pad, src.shape[1], src.shape[2]) + src = torch.cat((src, src_extra), dim=0) + assert src.shape[0] == d_seq_len * ds + + src = src.reshape(d_seq_len, ds, batch_size, in_channels) + + weights = self.bias.softmax(dim=0) + # weights: (downsample, 1, 1) + weights = weights.unsqueeze(-1).unsqueeze(-1) + + # ans1 is the first `in_channels` channels of the output + ans = (src * weights).sum(dim=1) + + return ans + + +class SimpleUpsample(torch.nn.Module): + """ + A very simple form of upsampling that mostly just repeats the input, but + also adds a position-specific bias. + """ + def __init__(self, + num_channels: int, + upsample: int): + super(SimpleUpsample, self).__init__() + self.upsample = upsample + + def forward(self, + src: Tensor) -> Tensor: + """ + x: (seq_len, batch_size, num_channels) + Returns a tensor of shape + ( (seq_len*upsample), batch_size, num_channels) + """ + upsample = self.upsample + (seq_len, batch_size, num_channels) = src.shape + src = src.unsqueeze(1).expand(seq_len, upsample, batch_size, num_channels) + src = src.reshape(seq_len * upsample, batch_size, num_channels) + return src + + +class CompactRelPositionalEncoding(torch.nn.Module): + """ + Relative positional encoding module. This version is "compact" meaning it is able to encode + the important information about the relative position in a relatively small number of dimensions. + The goal is to make it so that small differences between large relative offsets (e.g. 1000 vs. 1001) + make very little difference to the embedding. Such differences were potentially important + when encoding absolute position, but not important when encoding relative position because there + is now no need to compare two large offsets with each other. + + Our embedding works done by projecting the interval [-infinity,infinity] to a finite interval + using the atan() function, before doing the fourier transform of that fixed interval. The + atan() function would compress the "long tails" too small, + making it hard to distinguish between different magnitudes of large offsets, so we use a logarithmic + function to compress large offsets to a smaller range before applying atan(). + Scalings are chosen in such a way that the embedding can clearly distinguish invidual offsets as long + as they are quite close to the origin, e.g. abs(offset) <= about sqrt(embedding_dim) + + + Args: + embed_dim: Embedding dimension. + dropout_rate: Dropout rate. + max_len: Maximum input length: just a heuristic for initialization. + length_factor: a heuristic scale (should be >= 1.0) which, if larger, gives + less weight to small differences of offset near the origin. + """ + def __init__( + self, embed_dim: int, + dropout_rate: FloatLike, + max_len: int = 1000, + length_factor: float = 1.0, + ) -> None: + """Construct a CompactRelPositionalEncoding object.""" + super(CompactRelPositionalEncoding, self).__init__() + self.embed_dim = embed_dim + assert embed_dim % 2 == 0 + self.dropout = Dropout2(dropout_rate) + self.pe = None + assert length_factor >= 1.0 + self.length_factor = length_factor + self.extend_pe(torch.tensor(0.0).expand(max_len)) + + def extend_pe(self, x: Tensor, left_context_len: int = 0) -> None: + """Reset the positional encodings.""" + T = x.size(0) + left_context_len + + if self.pe is not None: + # self.pe contains both positive and negative parts + # the length of self.pe is 2 * input_len - 1 + if self.pe.size(0) >= T * 2 - 1: + # Note: TorchScript doesn't implement operator== for torch.Device + if self.pe.dtype != x.dtype or str(self.pe.device) != str( + x.device + ): + self.pe = self.pe.to(dtype=x.dtype, device=x.device) + return + + # if T == 4, x would contain [ -3, -2, 1, 0, 1, 2, 3 ] + x = torch.arange(-(T-1), T, + device=x.device).to(torch.float32).unsqueeze(1) + + freqs = 1 + torch.arange(self.embed_dim // 2, device=x.device) + + # `compression_length` this is arbitrary/heuristic, if it is larger we have more resolution + # for small time offsets but less resolution for large time offsets. + compression_length = (self.embed_dim ** 0.5) + # x_compressed, like X, goes from -infinity to infinity as T goes from -infinity to infinity; + # but it does so more slowly than T for large absolute values of T. + # The formula is chosen so that d(x_compressed )/dx is 1 around x == 0, which + # is important. + x_compressed = compression_length * x.sign() * ((x.abs() + compression_length).log() - math.log(compression_length)) + + # if self.length_factor == 1.0, then length_scale is chosen so that the + # FFT can exactly separate points close to the origin (T == 0). So this + # part of the formulation is not really heuristic. + # But empirically, for ASR at least, length_factor > 1.0 seems to work better. + length_scale = self.length_factor * self.embed_dim / (2.0 * math.pi) + + # note for machine implementations: if atan is not available, we can use: + # x.sign() * ((1 / (x.abs() + 1)) - 1) * (-math.pi/2) + # check on wolframalpha.com: plot(sign(x) * (1 / ( abs(x) + 1) - 1 ) * -pi/2 , atan(x)) + x_atan = (x_compressed / length_scale).atan() # results between -pi and pi + + cosines = (x_atan * freqs).cos() + sines = (x_atan * freqs).sin() + + pe = torch.zeros(x.shape[0], self.embed_dim, device=x.device) + pe[:, 0::2] = cosines + pe[:, 1::2] = sines + pe[:, -1] = 1.0 # for bias. + + self.pe = pe.to(dtype=x.dtype) + + def forward(self, x: Tensor, left_context_len: int = 0) -> Tensor: + """Create positional encoding. + + Args: + x (Tensor): Input tensor (time, batch, `*`). + left_context_len: (int): Length of cached left context. + + Returns: + positional embedding, of shape (batch, left_context_len + 2*time-1, `*`). + """ + self.extend_pe(x, left_context_len) + x_size_left = x.size(0) + left_context_len + # length of positive side: x.size(0) + left_context_len + # length of negative side: x.size(0) + pos_emb = self.pe[ + self.pe.size(0) // 2 + - x_size_left + + 1 : self.pe.size(0) // 2 # noqa E203 + + x.size(0), + : + ] + pos_emb = pos_emb.unsqueeze(0) + return self.dropout(pos_emb) + + +class RelPositionMultiheadAttentionWeights(nn.Module): + r"""Module that computes multi-head attention weights with relative position encoding. + Various other modules consume the resulting attention weights: see, for example, the + SimpleAttention module which allows you to compute conventional attention. + + This is a quite heavily modified from: "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context", + we have to write up the differences. + + + Args: + embed_dim: number of channels at the input to this module, e.g. 256 + pos_dim: dimension of the positional encoding vectors, e.g. 128. + num_heads: number of heads to compute weights for, e.g. 8 + query_head_dim: dimension of the query (and key), per head. e.g. 24. + pos_head_dim: dimension of the projected positional encoding per head, e.g. 4. + dropout: dropout probability for attn_output_weights. Default: 0.0. + pos_emb_skip_rate: probability for skipping the pos_emb part of the scores on + any given call to forward(), in training time. + """ + + def __init__( + self, + embed_dim: int, + pos_dim: int, + num_heads: int, + query_head_dim: int, + pos_head_dim: int, + dropout: float = 0.0, + pos_emb_skip_rate: FloatLike = ScheduledFloat((0.0, 0.5), + (4000.0, 0.0)) + ) -> None: + super().__init__() + self.embed_dim = embed_dim + self.num_heads = num_heads + self.query_head_dim = query_head_dim + self.pos_head_dim = pos_head_dim + self.dropout = dropout + self.pos_emb_skip_rate = copy.deepcopy(pos_emb_skip_rate) + self.name = None # will be overwritten in training code; for diagnostics. + + key_head_dim = query_head_dim + in_proj_dim = (query_head_dim + key_head_dim + pos_head_dim) * num_heads + + # the initial_scale is supposed to take over the "scaling" factor of + # head_dim ** -0.5 that has been used in previous forms of attention, + # dividing it between the query and key. Note: this module is intended + # to be used with the ScaledAdam optimizer; with most other optimizers, + # it would be necessary to apply the scaling factor in the forward function. + self.in_proj = ScaledLinear(embed_dim, in_proj_dim, bias=True, + initial_scale=query_head_dim**-0.25) + + self.whiten_keys = Whiten(num_groups=num_heads, + whitening_limit=_whitening_schedule(3.0), + prob=(0.025, 0.25), + grad_scale=0.025) + + # add a balancer for the keys that runs with very small probability, and + # tries to enforce that all dimensions have mean around zero. The + # weights produced by this module are invariant to adding a constant to + # the keys, so the derivative of the bias is mathematically zero; but + # due to how Adam/ScaledAdam work, it can learn a fairly large nonzero + # bias because the small numerical roundoff tends to have a non-random + # sign. This module is intended to prevent that. Use a very small + # probability; that should be suffixient to fix the problem. + self.balance_keys = Balancer(key_head_dim * num_heads, + channel_dim=-1, + min_positive=0.4, + max_positive=0.6, + min_abs=0.0, + max_abs=100.0, + prob=0.025) + + # linear transformation for positional encoding. + self.linear_pos = ScaledLinear(pos_dim, + num_heads * pos_head_dim, + bias=False, + initial_scale=0.05) + + # the following are for diagnosics only, see --print-diagnostics option + self.copy_pos_query = Identity() + self.copy_query = Identity() + + def forward( + self, + x: Tensor, + pos_emb: Tensor, + key_padding_mask: Optional[Tensor] = None, + attn_mask: Optional[Tensor] = None, + ) -> Tensor: + r""" + Args: + x: input of shape (seq_len, batch_size, embed_dim) + pos_emb: Positional embedding tensor, of shape (1, 2*seq_len - 1, pos_dim) + key_padding_mask: a bool tensor of shape (batch_size, seq_len). Positions that + are True in this mask will be ignored as sources in the attention weighting. + attn_mask: mask of shape (seq_len, seq_len) or (batch_size, seq_len, seq_len), + interpreted as ([batch_size,] tgt_seq_len, src_seq_len) + saying which positions are allowed to attend to which other positions. + Returns: + a tensor of attention weights, of shape (hum_heads, batch_size, seq_len, seq_len) + interpreted as (hum_heads, batch_size, tgt_seq_len, src_seq_len). + """ + x = self.in_proj(x) + query_head_dim = self.query_head_dim + pos_head_dim = self.pos_head_dim + num_heads = self.num_heads + + seq_len, batch_size, _ = x.shape + + query_dim = query_head_dim * num_heads + + # self-attention + q = x[...,0:query_dim] + k = x[...,query_dim:2*query_dim] + # p is the position-encoding query + p = x[...,2*query_dim:] + assert p.shape[-1] == num_heads * pos_head_dim + + q = self.copy_query(q) # for diagnostics only, does nothing. + k = self.whiten_keys(self.balance_keys(k)) # does nothing in the forward pass. + p = self.copy_pos_query(p) # for diagnostics only, does nothing. + + q = q.reshape(seq_len, batch_size, num_heads, query_head_dim) + p = p.reshape(seq_len, batch_size, num_heads, pos_head_dim) + k = k.reshape(seq_len, batch_size, num_heads, query_head_dim) + + # time1 refers to target, time2 refers to source. + q = q.permute(2, 1, 0, 3) # (head, batch, time1, query_head_dim) + p = p.permute(2, 1, 0, 3) # (head, batch, time1, pos_head_dim) + k = k.permute(2, 1, 3, 0) # (head, batch, d_k, time2) + + attn_scores = torch.matmul(q, k) + + use_pos_scores = False + if torch.jit.is_scripting(): + # We can't put random.random() in the same line + use_pos_scores = True + elif not self.training or random.random() >= float(self.pos_emb_skip_rate): + use_pos_scores = True + + if use_pos_scores: + pos_emb = self.linear_pos(pos_emb) + seq_len2 = 2 * seq_len - 1 + pos_emb = pos_emb.reshape(-1, seq_len2, num_heads, pos_head_dim).permute(2, 0, 3, 1) + # pos shape now: (head, {1 or batch_size}, pos_dim, seq_len2) + + # (head, batch, time1, pos_dim) x (head, 1, pos_dim, seq_len2) -> (head, batch, time1, seq_len2) + # [where seq_len2 represents relative position.] + pos_scores = torch.matmul(p, pos_emb) + # the following .as_strided() expression converts the last axis of pos_scores from relative + # to absolute position. I don't know whether I might have got the time-offsets backwards or + # not, but let this code define which way round it is supposed to be. + pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, seq_len), + (pos_scores.stride(0), + pos_scores.stride(1), + pos_scores.stride(2)-pos_scores.stride(3), + pos_scores.stride(3)), + storage_offset=pos_scores.stride(3) * (seq_len - 1)) + + attn_scores = attn_scores + pos_scores + + if torch.jit.is_scripting(): + pass + elif self.training and random.random() < 0.1: + # This is a harder way of limiting the attention scores to not be + # too large. It incurs a penalty if any of them has an absolute + # value greater than 50.0. this should be outside the normal range + # of the attention scores. We use this mechanism instead of, say, + # something added to the loss function involving the entropy, + # because once the entropy gets very small gradients through the + # softmax can become very small, and we'd get zero derivatives. The + # choices of 1.0e-04 as the scale on the penalty makes this + # mechanism vulnerable to the absolute scale of the loss function, + # but we view this as a failsafe to avoid "implausible" parameter + # values rather than a regularization method that should be active + # under normal circumstances. + attn_scores = penalize_abs_values_gt(attn_scores, + limit=25.0, + penalty=1.0e-04, + name=self.name) + + assert attn_scores.shape == (num_heads, batch_size, seq_len, seq_len) + + if attn_mask is not None: + assert attn_mask.dtype == torch.bool + # use -1000 to avoid nan's where attn_mask and key_padding_mask make + # all scores zero. It's important that this be large enough that exp(-1000) + # is exactly zero, for reasons related to const_attention_rate, it + # compares the final weights with zero. + attn_scores = attn_scores.masked_fill(attn_mask, -1000) + + if key_padding_mask is not None: + assert key_padding_mask.shape == (batch_size, seq_len), key_padding_mask.shape + attn_scores = attn_scores.masked_fill( + key_padding_mask.unsqueeze(1), + -1000, + ) + + # We use our own version of softmax, defined in scaling.py, which should + # save a little of the memory used in backprop by, if we are in + # automatic mixed precision mode (amp / autocast), by only storing the + # half-precision output for backprop purposes. + attn_weights = softmax(attn_scores, dim=-1) + + if torch.jit.is_scripting(): + pass + elif random.random() < 0.001 and not self.training: + self._print_attn_entropy(attn_weights) + + attn_weights = nn.functional.dropout( + attn_weights, p=self.dropout, training=self.training + ) + + return attn_weights + + def streaming_forward( + self, + x: Tensor, + pos_emb: Tensor, + cached_key: Tensor, + left_context_len: int, + key_padding_mask: Tensor, + ) -> Tuple[Tensor, Tensor]: + r""" + Args: + x: input of shape (seq_len, batch_size, embed_dim) + pos_emb: Positional embedding tensor, of shape (1, left_context_len+2*seq_len-1, pos_dim) + cached_key: cached attention key tensor of left context, + of shape (left_context_len, batch_size, key_dim) + left_context_len: number of left context frames. + key_padding_mask: a bool tensor of shape (batch_size, seq_len). Positions that + are True in this mask will be ignored as sources in the attention weighting. + + Returns: + - attention weights, of shape (hum_heads, batch_size, seq_len, seq_len2), + interpreted as (hum_heads, batch_size, tgt_seq_len, src_seq_len). + - updated cached attention key tensor of left context. + """ + x = self.in_proj(x) + query_head_dim = self.query_head_dim + pos_head_dim = self.pos_head_dim + num_heads = self.num_heads + + seq_len, batch_size, _ = x.shape + + query_dim = query_head_dim * num_heads + + # self-attention + q = x[...,0:query_dim] + k = x[...,query_dim:2*query_dim] + # p is the position-encoding query + p = x[...,2*query_dim:] + assert p.shape[-1] == num_heads * pos_head_dim + + # Pad cached left contexts + assert cached_key.shape[0] == left_context_len, (cached_key.shape[0], left_context_len) + k = torch.cat([cached_key, k], dim=0) + # Update cached left contexts + cached_key = k[-left_context_len:, ...] + + # The length of key + k_len = k.shape[0] + + q = q.reshape(seq_len, batch_size, num_heads, query_head_dim) + p = p.reshape(seq_len, batch_size, num_heads, pos_head_dim) + k = k.reshape(k_len, batch_size, num_heads, query_head_dim) + + # time1 refers to target, time2 refers to source. + q = q.permute(2, 1, 0, 3) # (head, batch, time1, query_head_dim) + p = p.permute(2, 1, 0, 3) # (head, batch, time1, pos_head_dim) + k = k.permute(2, 1, 3, 0) # (head, batch, d_k, time2) + + attn_scores = torch.matmul(q, k) + + pos_emb = self.linear_pos(pos_emb) + seq_len2 = 2 * seq_len - 1 + left_context_len + pos_emb = pos_emb.reshape(-1, seq_len2, num_heads, pos_head_dim).permute(2, 0, 3, 1) + # pos shape now: (head, {1 or batch_size}, pos_dim, seq_len2) + + # (head, batch, time1, pos_dim) x (head, 1, pos_dim, seq_len2) -> (head, batch, time1, seq_len2) + # [where seq_len2 represents relative position.] + pos_scores = torch.matmul(p, pos_emb) + # the following .as_strided() expression converts the last axis of pos_scores from relative + # to absolute position. I don't know whether I might have got the time-offsets backwards or + # not, but let this code define which way round it is supposed to be. + pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, k_len), + (pos_scores.stride(0), + pos_scores.stride(1), + pos_scores.stride(2)-pos_scores.stride(3), + pos_scores.stride(3)), + storage_offset=pos_scores.stride(3) * (seq_len - 1)) + + attn_scores = attn_scores + pos_scores + + assert attn_scores.shape == (num_heads, batch_size, seq_len, k_len), attn_scores.shape + + if key_padding_mask is not None: + assert key_padding_mask.shape == (batch_size, k_len), key_padding_mask.shape + attn_scores = attn_scores.masked_fill( + key_padding_mask.unsqueeze(1), + -1000, + ) + + attn_weights = attn_scores.softmax(dim=-1) + + return attn_weights, cached_key + + def _print_attn_entropy( + self, + attn_weights: Tensor): + # attn_weights: (num_heads, batch_size, seq_len, seq_len) + (num_heads, batch_size, seq_len, seq_len) = attn_weights.shape + + with torch.no_grad(): + with torch.cuda.amp.autocast(enabled=False): + attn_weights = attn_weights.to(torch.float32) + attn_weights_entropy = -((attn_weights + 1.0e-20).log() * attn_weights).sum( + dim=-1).mean(dim=(1,2)) + logging.info(f"name={self.name}, attn_weights_entropy = {attn_weights_entropy}") + + +class SelfAttention(nn.Module): + """ + The simplest possible attention module. This one works with already-computed attention + weights, e.g. as computed by RelPositionMultiheadAttentionWeights. + + Args: + embed_dim: the input and output embedding dimension + num_heads: the number of attention heads + value_head_dim: the value dimension per head + """ + def __init__( + self, + embed_dim: int, + num_heads: int, + value_head_dim: int, + ) -> None: + super().__init__() + self.in_proj = nn.Linear(embed_dim, + num_heads * value_head_dim, + bias=True) + + self.out_proj = ScaledLinear(num_heads * value_head_dim, + embed_dim, bias=True, + initial_scale=0.05) + + self.whiten = Whiten(num_groups=1, + whitening_limit=_whitening_schedule(7.5, ratio=3.0), + prob=(0.025, 0.25), + grad_scale=0.01) + + def forward( + self, + x: Tensor, + attn_weights: Tensor, + ) -> Tensor: + """ + Args: + x: input tensor, of shape (seq_len, batch_size, embed_dim) + attn_weights: a tensor of shape (num_heads, batch_size, seq_len, seq_len), + with seq_len being interpreted as (tgt_seq_len, src_seq_len). Expect + attn_weights.sum(dim=-1) == 1. + Returns: + a tensor with the same shape as x. + """ + (seq_len, batch_size, embed_dim) = x.shape + num_heads = attn_weights.shape[0] + assert attn_weights.shape == (num_heads, batch_size, seq_len, seq_len) + + x = self.in_proj(x) # (seq_len, batch_size, num_heads * value_head_dim) + x = x.reshape(seq_len, batch_size, num_heads, -1).permute(2, 1, 0, 3) + # now x: (num_heads, batch_size, seq_len, value_head_dim) + value_head_dim = x.shape[-1] + + # todo: see whether there is benefit in overriding matmul + x = torch.matmul(attn_weights, x) + # v: (num_heads, batch_size, seq_len, value_head_dim) + + x = x.permute(2, 1, 0, 3).contiguous().view( + seq_len, batch_size, num_heads * value_head_dim) + + # returned value is of shape (seq_len, batch_size, embed_dim), like the input. + x = self.out_proj(x) + x = self.whiten(x) + + return x + + def streaming_forward( + self, + x: Tensor, + attn_weights: Tensor, + cached_val: Tensor, + left_context_len: int, + ) -> Tuple[Tensor, Tensor]: + """ + Args: + x: input tensor, of shape (seq_len, batch_size, embed_dim) + attn_weights: a tensor of shape (num_heads, batch_size, seq_len, seq_len), + with seq_len being interpreted as (tgt_seq_len, src_seq_len). Expect + attn_weights.sum(dim=-1) == 1. + cached_val: cached attention value tensor of left context, + of shape (left_context_len, batch_size, value_dim) + left_context_len: number of left context frames. + + Returns: + - attention weighted output, a tensor with the same shape as x. + - updated cached attention value tensor of left context. + """ + (seq_len, batch_size, embed_dim) = x.shape + num_heads = attn_weights.shape[0] + seq_len2 = seq_len + left_context_len + assert attn_weights.shape == (num_heads, batch_size, seq_len, seq_len2) + + x = self.in_proj(x) # (seq_len, batch_size, num_heads * value_head_dim) + + # Pad cached left contexts + assert cached_val.shape[0] == left_context_len, (cached_val.shape[0], left_context_len) + x = torch.cat([cached_val, x], dim=0) + # Update cached left contexts + cached_val = x[-left_context_len:, ...] + + x = x.reshape(seq_len2, batch_size, num_heads, -1).permute(2, 1, 0, 3) + # now x: (num_heads, batch_size, seq_len, value_head_dim) + value_head_dim = x.shape[-1] + + # todo: see whether there is benefit in overriding matmul + x = torch.matmul(attn_weights, x) + # v: (num_heads, batch_size, seq_len, value_head_dim) + + x = x.permute(2, 1, 0, 3).contiguous().view( + seq_len, batch_size, num_heads * value_head_dim) + + # returned value is of shape (seq_len, batch_size, embed_dim), like the input. + x = self.out_proj(x) + + return x, cached_val + + +class FeedforwardModule(nn.Module): + """Feedforward module in Zipformer2 model. + """ + def __init__(self, + embed_dim: int, + feedforward_dim: int, + dropout: FloatLike): + super(FeedforwardModule, self).__init__() + self.in_proj = nn.Linear(embed_dim, feedforward_dim) + + self.hidden_balancer = Balancer(feedforward_dim, + channel_dim=-1, + min_positive=0.3, + max_positive=1.0, + min_abs=0.75, + max_abs=5.0) + + # shared_dim=0 means we share the dropout mask along the time axis + self.out_proj = ActivationDropoutAndLinear(feedforward_dim, embed_dim, + activation='SwooshL', + dropout_p=dropout, + dropout_shared_dim=0, bias=True, + initial_scale=0.1) + + self.out_whiten = Whiten(num_groups=1, + whitening_limit=_whitening_schedule(7.5), + prob=(0.025, 0.25), + grad_scale=0.01) + + def forward(self, x: Tensor): + x = self.in_proj(x) + x = self.hidden_balancer(x) + # out_proj contains SwooshL activation, then dropout, then linear. + x = self.out_proj(x) + x = self.out_whiten(x) + return x + + +class NonlinAttention(nn.Module): + """This is like the ConvolutionModule, but refactored so that we use multiplication by attention weights (borrowed + from the attention module) in place of actual convolution. We also took out the second nonlinearity, the + one after the attention mechanism. + + Args: + channels (int): The number of channels of conv layers. + """ + + def __init__( + self, + channels: int, + hidden_channels: int, + ) -> None: + super().__init__() + + self.hidden_channels = hidden_channels + + self.in_proj = nn.Linear(channels, hidden_channels * 3, bias=True) + + # balancer that goes before the sigmoid. Have quite a large min_abs value, at 2.0, + # because we noticed that well-trained instances of this module have abs-value before the sigmoid + # starting from about 3, and poorly-trained instances of the module have smaller abs values + # before the sigmoid. + self.balancer = Balancer( + hidden_channels, channel_dim=-1, + min_positive=ScheduledFloat((0.0, 0.25), (20000.0, 0.05)), + max_positive=ScheduledFloat((0.0, 0.75), (20000.0, 0.95)), + min_abs=0.5, + max_abs=5.0, + ) + self.tanh = nn.Tanh() + + self.identity1 = Identity() # for diagnostics. + self.identity2 = Identity() # for diagnostics. + self.identity3 = Identity() # for diagnostics. + + self.out_proj = ScaledLinear(hidden_channels, channels, + bias=True, + initial_scale=0.05) + + self.whiten1 = Whiten(num_groups=1, + whitening_limit=_whitening_schedule(5.0), + prob=(0.025, 0.25), + grad_scale=0.01) + + self.whiten2 = Whiten(num_groups=1, + whitening_limit=_whitening_schedule(5.0, ratio=3.0), + prob=(0.025, 0.25), + grad_scale=0.01) + + def forward( + self, + x: Tensor, + attn_weights: Tensor, + ) -> Tensor: + """. + Args: + x: a Tensor of shape (seq_len, batch_size, num_channels) +attn_weights: a Tensor of shape (num_heads, batch_size, seq_len, seq_len) + Returns: + a Tensor with the same shape as x + """ + x = self.in_proj(x) + + (seq_len, batch_size, _) = x.shape + hidden_channels = self.hidden_channels + + s, x, y = x.chunk(3, dim=-1) + + # s will go through tanh. + + s = self.balancer(s) + s = self.tanh(s) + + s = s.unsqueeze(-1).reshape(seq_len, batch_size, hidden_channels) + x = self.whiten1(x) + x = x * s + x = self.identity1(x) # diagnostics only, it's the identity. + + (seq_len, batch_size, embed_dim) = x.shape + num_heads = attn_weights.shape[0] + assert attn_weights.shape == (num_heads, batch_size, seq_len, seq_len) + + x = x.reshape(seq_len, batch_size, num_heads, -1).permute(2, 1, 0, 3) + # now x: (num_heads, batch_size, seq_len, head_dim) + x = torch.matmul(attn_weights, x) + # now x: (num_heads, batch_size, seq_len, head_dim) + x = x.permute(2, 1, 0, 3).reshape(seq_len, batch_size, -1) + + y = self.identity2(y) + x = x * y + x = self.identity3(x) + + x = self.out_proj(x) + x = self.whiten2(x) + return x + + def streaming_forward( + self, + x: Tensor, + attn_weights: Tensor, + cached_x: Tensor, + left_context_len: int, + ) -> Tuple[Tensor, Tensor]: + """. + Args: + x: a Tensor of shape (seq_len, batch_size, num_channels) + attn_weights: a Tensor of shape (num_heads, batch_size, seq_len, seq_len) + cached_x: left context, a Tensor of shape + (num_heads, batch_size, left_context_len, head_dim) + left_context_len: number of left context frames. + Returns: + - a Tensor with the same shape as x + - updated left context with same shape as cached_x + """ + x = self.in_proj(x) + + (seq_len, batch_size, _) = x.shape + hidden_channels = self.hidden_channels + + s, x, y = x.chunk(3, dim=-1) + + # s will go through tanh. + s = self.tanh(s) + + s = s.unsqueeze(-1).reshape(seq_len, batch_size, hidden_channels) + x = x * s + + (seq_len, batch_size, embed_dim) = x.shape + num_heads = attn_weights.shape[0] + assert attn_weights.shape == (num_heads, batch_size, seq_len, left_context_len + seq_len) + + x = x.reshape(seq_len, batch_size, num_heads, -1).permute(2, 1, 0, 3) + # now x: (num_heads, batch_size, seq_len, head_dim) + + # Pad cached tensor + assert cached_x.shape[2] == left_context_len, (cached_x.shape[2], left_context_len) + x_pad = torch.cat([cached_x, x], dim=2) + # Update cached tensor + cached_x = x_pad[:, :, -left_context_len:, :] + + x = torch.matmul(attn_weights, x_pad) + # now x: (num_heads, batch_size, seq_len, head_dim) + x = x.permute(2, 1, 0, 3).reshape(seq_len, batch_size, -1) + + x = x * y + + x = self.out_proj(x) + return x, cached_x + + +class ConvolutionModule(nn.Module): + """ConvolutionModule in Zipformer2 model. + Modified from https://github.com/espnet/espnet/blob/master/espnet/nets/pytorch_backend/zipformer/convolution.py + + Args: + channels (int): The number of channels of conv layers. + kernel_size (int): Kernerl size of conv layers. + bias (bool): Whether to use bias in conv layers (default=True). + + """ + def __init__( + self, channels: int, kernel_size: int, causal: bool, + ) -> None: + """Construct a ConvolutionModule object.""" + super(ConvolutionModule, self).__init__() + # kernerl_size should be a odd number for 'SAME' padding + assert (kernel_size - 1) % 2 == 0 + + bottleneck_dim = channels + self.causal = causal + + self.in_proj = nn.Linear( + channels, 2 * bottleneck_dim, + ) + # the gradients on in_proj are a little noisy, likely to do with the + # sigmoid in glu. + + # after in_proj we put x through a gated linear unit (nn.functional.glu). + # For most layers the normal rms value of channels of x seems to be in the range 1 to 4, + # but sometimes, for some reason, for layer 0 the rms ends up being very large, + # between 50 and 100 for different channels. This will cause very peaky and + # sparse derivatives for the sigmoid gating function, which will tend to make + # the loss function not learn effectively. (for most layers the average absolute values + # are in the range 0.5..9.0, and the average p(x>0), i.e. positive proportion, + # at the output of pointwise_conv1.output is around 0.35 to 0.45 for different + # layers, which likely breaks down as 0.5 for the "linear" half and + # 0.2 to 0.3 for the part that goes into the sigmoid. The idea is that if we + # constrain the rms values to a reasonable range via a constraint of max_abs=10.0, + # it will be in a better position to start learning something, i.e. to latch onto + # the correct range. + self.balancer1 = Balancer( + bottleneck_dim, channel_dim=-1, + min_positive=ScheduledFloat((0.0, 0.05), (8000.0, 0.025)), + max_positive=1.0, + min_abs=1.5, + max_abs=ScheduledFloat((0.0, 5.0), (8000.0, 10.0), default=1.0), + ) + + self.activation1 = Identity() # for diagnostics + + self.sigmoid = nn.Sigmoid() + + self.activation2 = Identity() # for diagnostics + + assert kernel_size % 2 == 1 + + self.depthwise_conv = ChunkCausalDepthwiseConv1d( + channels=bottleneck_dim, + kernel_size=kernel_size) if causal else nn.Conv1d( + in_channels=bottleneck_dim, + out_channels=bottleneck_dim, + groups=bottleneck_dim, + kernel_size=kernel_size, + padding=kernel_size // 2) + + self.balancer2 = Balancer( + bottleneck_dim, channel_dim=1, + min_positive=ScheduledFloat((0.0, 0.1), (8000.0, 0.05)), + max_positive=1.0, + min_abs=ScheduledFloat((0.0, 0.2), (20000.0, 0.5)), + max_abs=10.0, + ) + + self.whiten = Whiten(num_groups=1, + whitening_limit=_whitening_schedule(7.5), + prob=(0.025, 0.25), + grad_scale=0.01) + + self.out_proj = ActivationDropoutAndLinear( + bottleneck_dim, channels, activation='SwooshR', + dropout_p=0.0, initial_scale=0.05, + ) + + def forward( + self, + x: Tensor, + src_key_padding_mask: Optional[Tensor] = None, + chunk_size: int = -1, + ) -> Tensor: + """Compute convolution module. + + Args: + x: Input tensor (#time, batch, channels). + src_key_padding_mask: the mask for the src keys per batch (optional): + (batch, #time), contains True in masked positions. + + Returns: + Tensor: Output tensor (#time, batch, channels). + + """ + + x = self.in_proj(x) # (time, batch, 2*channels) + + x, s = x.chunk(2, dim=-1) + s = self.balancer1(s) + s = self.sigmoid(s) + x = self.activation1(x) # identity. + x = x * s + x = self.activation2(x) # identity + + # (time, batch, channels) + + # exchange the temporal dimension and the feature dimension + x = x.permute(1, 2, 0) # (#batch, channels, time). + + if src_key_padding_mask is not None: + x = x.masked_fill(src_key_padding_mask.unsqueeze(1).expand_as(x), 0.0) + + if not torch.jit.is_scripting() and chunk_size >= 0: + # Not support exporting a model for simulated streaming decoding + assert self.causal, "Must initialize model with causal=True if you use chunk_size" + x = self.depthwise_conv(x, chunk_size=chunk_size) + else: + x = self.depthwise_conv(x) + + x = self.balancer2(x) + x = x.permute(2, 0, 1) # (time, batch, channels) + + x = self.whiten(x) # (time, batch, channels) + x = self.out_proj(x) # (time, batch, channels) + + return x + + def streaming_forward( + self, + x: Tensor, + cache: Tensor, + src_key_padding_mask: Tensor, + ) -> Tuple[Tensor, Tensor]: + """Compute convolution module in streaming forward mode. + + Args: + x: Input tensor (#time, batch, channels). + cache: cached left context for depthwise_conv of shape + (#batch, channels, left_pad) + src_key_padding_mask: the mask for the src keys per batch (optional): + (batch, #time), contains True in masked positions. + + Returns: + - Output tensor (#time, batch, channels). + - Updated cache (#batch, channels, left_pad) + """ + + x = self.in_proj(x) # (time, batch, 2*channels) + + x, s = x.chunk(2, dim=-1) + s = self.sigmoid(s) + x = x * s + # (time, batch, channels) + + # exchange the temporal dimension and the feature dimension + x = x.permute(1, 2, 0) # (#batch, channels, time). + + if src_key_padding_mask is not None: + x = x.masked_fill(src_key_padding_mask.unsqueeze(1).expand_as(x), 0.0) + + x, cache = self.depthwise_conv.streaming_forward(x, cache=cache) + + x = x.permute(2, 0, 1) # (time, batch, channels) + + x = self.out_proj(x) # (time, batch, channels) + + return x, cache + + +class ScalarMultiply(nn.Module): + def __init__(self, scale: float): + super().__init__() + self.scale = scale + + def forward(self, x): + return x * self.scale + + +def _test_zipformer_main(causal: bool = False): + batch_size = 5 + seq_len = 20 + # Just make sure the forward pass runs. + + c = Zipformer2( + encoder_dim=(64, 96), encoder_unmasked_dim=(48, 64), num_heads=(4, 4), + causal=causal, + chunk_size=(4,) if causal else (-1,), + left_context_frames=(64,) + ) + batch_size = 5 + seq_len = 20 + # Just make sure the forward pass runs. + f = c( + torch.randn(seq_len, batch_size, 64), + torch.full((batch_size,), seq_len, dtype=torch.int64), + ) + f[0].sum().backward() + c.eval() + f = c( + torch.randn(seq_len, batch_size, 64), + torch.full((batch_size,), seq_len, dtype=torch.int64), + ) + f # to remove flake8 warnings + + +if __name__ == "__main__": + logging.getLogger().setLevel(logging.INFO) + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + _test_zipformer_main(False) + _test_zipformer_main(True) diff --git a/icefall/diagnostics.py b/icefall/diagnostics.py index 6589579d1..51e816105 100644 --- a/icefall/diagnostics.py +++ b/icefall/diagnostics.py @@ -16,15 +16,13 @@ # See the License for the specific language governing permissions and # limitations under the License. - import random from dataclasses import dataclass -from typing import List, Optional, Tuple +from typing import Optional, Tuple, List import torch from torch import Tensor, nn - class TensorDiagnosticOptions(object): """Options object for tensor diagnostics: @@ -60,7 +58,8 @@ def get_tensor_stats( "abs" -> take abs() before summing "positive" -> take (x > 0) before summing "rms" -> square before summing, we'll take sqrt later - "value -> just sum x itself + "value" -> just sum x itself + "max", "min" -> take the maximum or minimum [over all other dims but dim] instead of summing Returns: stats: a Tensor of shape (x.shape[dim],). count: an integer saying how many items were counted in each element @@ -78,11 +77,11 @@ def get_tensor_stats( elif stats_type == "abs": x = x.abs() elif stats_type == "rms": - x = x**2 + x = x ** 2 elif stats_type == "positive": x = (x > 0).to(dtype=torch.float) else: - assert stats_type in ["value", "max", "min"] + assert stats_type in [ "value", "max", "min" ] sum_dims = [d for d in range(x.ndim) if d != dim] if len(sum_dims) > 0: @@ -94,7 +93,7 @@ def get_tensor_stats( x = torch.min(x, dim=dim)[0] else: x = torch.sum(x, dim=sum_dims) - x = x.flatten() + x = x.flatten().clone() return x, count @@ -106,7 +105,7 @@ class TensorAndCount: class TensorDiagnostic(object): """This class is not directly used by the user, it is responsible for - collecting diagnostics for a single parameter tensor of a torch.nn.Module. + collecting diagnostics for a module or parameter tensor of a torch.nn.Module. Args: opts: @@ -121,9 +120,14 @@ class TensorDiagnostic(object): self.name = name self.class_name = None # will assign in accumulate() - self.stats = ( - None # we'll later assign a list to this data member. It's a list of dict. - ) + self.stats = None # we'll later assign a list to self.stats. + # It's a list of dicts, indexed by dim (i.e. by the + # axis of the tensor). The dicts, in turn, are + # indexed by `stats-type` which are strings in + # ["abs", "max", "min", "positive", "value", "rms"]. + + # scalar_stats contains some analysis of the activations and gradients, + self.scalar_stats = None # the keys into self.stats[dim] are strings, whose values can be # "abs", "max", "min" ,"value", "positive", "rms", "value". @@ -135,6 +139,7 @@ class TensorDiagnostic(object): # only adding a new element to the list if there was a different dim. # if the string in the key is "eigs", if we detect a length mismatch we put None as the value. + def accumulate(self, x, class_name: Optional[str] = None): """ Accumulate tensors. @@ -178,20 +183,27 @@ class TensorDiagnostic(object): if s.tensor.shape == stats.shape: if stats_type == "max": s.tensor = torch.maximum(s.tensor, stats) + elif stats_type == "min": s.tensor = torch.minimum(s.tensor, stats) else: + assert stats_type != "max" s.tensor += stats s.count += count done = True break if not done: - if this_dim_stats[stats_type] != [] and stats_type == "eigs": + if ( + this_dim_stats[stats_type] != [] + and stats_type == "eigs" + ): # >1 size encountered on this dim, e.g. it's a batch or time dimension, # don't accumulat "eigs" stats type, it uses too much memory this_dim_stats[stats_type] = None else: - this_dim_stats[stats_type].append(TensorAndCount(stats, count)) + this_dim_stats[stats_type].append( + TensorAndCount(stats, count) + ) def print_diagnostics(self): """Print diagnostics for each dimension of the tensor.""" @@ -199,14 +211,28 @@ class TensorDiagnostic(object): print(f"Warning: the stats of {self.name} is None.") return for dim, this_dim_stats in enumerate(self.stats): + if "rms" in this_dim_stats and "value" in this_dim_stats: + # produce "stddev" stats, which is centered RMS. + rms_stats_list = this_dim_stats["rms"] + value_stats_list = this_dim_stats["value"] + if len(rms_stats_list) == len(value_stats_list): + stddev_stats_list = [] + for r, v in zip(rms_stats_list, value_stats_list): + stddev_stats_list.append( + # r.count and v.count should be the same, but we don't check this. + TensorAndCount(r.tensor - v.tensor * v.tensor / (v.count + 1.0e-20), + r.count)) + this_dim_stats["stddev"] = stddev_stats_list + for stats_type, stats_list in this_dim_stats.items(): - # stats_type could be "rms", "value", "abs", "eigs", "positive". + # stats_type could be "rms", "value", "abs", "eigs", "positive", "min" or "max". # "stats_list" could be a list of TensorAndCount (one list per distinct tensor # shape of the stats), or None if stats_list is None: assert stats_type == "eigs" continue + def get_count(count): return 1 if stats_type in ["max", "min"] else count @@ -224,20 +250,22 @@ class TensorDiagnostic(object): eigs, _ = torch.symeig(stats) stats = eigs.abs().sqrt() except: # noqa - print("Error getting eigenvalues, trying another method.") + print( + "Error getting eigenvalues, trying another method." + ) eigs, _ = torch.eig(stats) - stats = eigs.abs().sqrt() + stats = eigs.norm(dim=1).sqrt() # sqrt so it reflects data magnitude, like stddev- not variance - if stats_type == "rms": + if stats_type in [ "rms", "stddev" ]: # we stored the square; after aggregation we need to take sqrt. stats = stats.sqrt() # if `summarize` we print percentiles of the stats; else, # we print out individual elements. - summarize = (len(stats_list) > 1) or self.opts.dim_is_summarized( - stats.numel() - ) + summarize = ( + len(stats_list) > 1 + ) or self.opts.dim_is_summarized(stats.numel()) if summarize: # usually `summarize` will be true # print out percentiles. stats = stats.sort()[0] @@ -254,32 +282,192 @@ class TensorDiagnostic(object): ans = stats.tolist() ans = ["%.2g" % x for x in ans] ans = "[" + " ".join(ans) + "]" - if stats_type in ["value", "rms", "eigs"]: + if stats_type in [ "value", "rms", "stddev", "eigs" ]: # This norm is useful because it is strictly less than the largest # sqrt(eigenvalue) of the variance, which we print out, and shows, # speaking in an approximate way, how much of that largest eigenvalue # can be attributed to the mean of the distribution. - norm = (stats**2).sum().sqrt().item() + norm = (stats ** 2).sum().sqrt().item() ans += f", norm={norm:.2g}" mean = stats.mean().item() - rms = (stats**2).mean().sqrt().item() - ans += f", mean={mean:.2g}, rms={rms:.2g}" + rms = (stats ** 2).mean().sqrt().item() + ans += f", mean={mean:.3g}, rms={rms:.3g}" # OK, "ans" contains the actual stats, e.g. # ans = "percentiles: [0.43 0.46 0.48 0.49 0.49 0.5 0.51 0.52 0.53 0.54 0.59], mean=0.5, rms=0.5" sizes = [x.tensor.shape[0] for x in stats_list] size_str = ( - f"{sizes[0]}" if len(sizes) == 1 else f"{min(sizes)}..{max(sizes)}" - ) - maybe_class_name = ( - f" type={self.class_name}," if self.class_name is not None else "" + f"{sizes[0]}" + if len(sizes) == 1 + else f"{min(sizes)}..{max(sizes)}" ) + maybe_class_name = f" type={self.class_name}," if self.class_name is not None else "" print( f"module={self.name},{maybe_class_name} dim={dim}, size={size_str}, {stats_type} {ans}" ) +class ScalarDiagnostic(object): + """This class is not directly used by the user, it is responsible for + collecting diagnostics for a single module (subclass of torch.nn.Module) that + represents some kind of nonlinearity, e.g. ReLU, sigmoid, etc. + """ + + def __init__(self, opts: TensorDiagnosticOptions, name: str): + self.opts = opts + self.name = name + self.class_name = None # will assign in accumulate() + self.is_forward_pass = True + + self.tick_scale = None + + self.saved_inputs = [] + self.is_ok = True + + self.counts = None + self.sum_grad = None + self.sum_gradsq = None + self.sum_abs_grad = None + + + def accumulate_input(self, x: Tensor, class_name: Optional[str] = None): + """ + Called in forward pass. + """ + if not self.is_forward_pass: + # in case we did a forward pass without a backward pass, for some reason. + self.saved_inputs = [] + self.is_forward_pass = True + + if class_name is not None: + self.class_name = class_name + if not self.is_ok: + return + + limit = 10 + if len(self.saved_inputs) > limit: + print(f"ERROR: forward pass called for this module over {limit} times with no backward pass. " + f" Will not accumulate scalar stats.") + self.is_ok = False + return + self.saved_inputs.append(x) + + def accumulate_output_grad(self, grad: Tensor): + if not self.is_ok: + return + if self.is_forward_pass: + self.is_forward_pass = False + + last_shape = 'n/a' if len(self.saved_inputs) == 0 else self.saved_inputs[-1].shape + if len(self.saved_inputs) == 0 or grad.shape != last_shape: + print(f"ERROR: shape mismatch or no forward activation present when backward " + f"pass called: grad shape ={tuple(grad.shape)}, num-saved-inputs={len(self.saved_inputs)}" + f", shape-of-last-saved-input={last_shape}") + self.is_ok = False + return + + x = self.saved_inputs.pop() + self.process_input_and_grad(x, grad) + + def process_input_and_grad(self, x: Tensor, grad: Tensor): + assert x.shape == grad.shape + x = x.flatten() + grad = grad.flatten() + + num_ticks_per_side = 256 + + if self.tick_scale is None: + x_abs_sorted = x.abs().sort()[0] + # take the 98th percentile as the largest value we count separately. + index = int(x.numel() * 0.98) + self.tick_scale = float(x_abs_sorted[index] / num_ticks_per_side) + + # integerize from tick * (-num ticks_per_side .. num_ticks_per_side - 1] + self.counts = torch.zeros(2 * num_ticks_per_side, dtype=torch.long, device=x.device) + self.sum_grad = torch.zeros(2 * num_ticks_per_side, dtype=torch.double, device=x.device) + # sum_gradsq is for getting error bars. + self.sum_gradsq = torch.zeros(2 * num_ticks_per_side, dtype=torch.double, device=x.device) + self.sum_abs_grad = torch.zeros(2 * num_ticks_per_side, dtype=torch.double, device=x.device) + + # this will round down. + x = (x / self.tick_scale).to(torch.long) + x = x.clamp_(min=-num_ticks_per_side, max=num_ticks_per_side - 1) + x = x + num_ticks_per_side + + self.counts.index_add_(dim=0, index=x, source=torch.ones_like(x)) + self.sum_grad.index_add_(dim=0, index=x, source=grad.to(torch.double)) + self.sum_gradsq.index_add_(dim=0, index=x, source=(grad*grad).to(torch.double)) + self.sum_abs_grad.index_add_(dim=0, index=x, source=grad.abs().to(torch.double)) + + + def print_diagnostics(self): + """Print diagnostics.""" + if self.is_ok is False or self.counts is None: + print(f"Warning: no stats accumulated for {self.name}, is_ok={self.is_ok}") + return + + counts = self.counts.to('cpu') + sum_grad = self.sum_grad.to(device='cpu', dtype=torch.float32) + sum_gradsq = self.sum_gradsq.to(device='cpu', dtype=torch.float32) + sum_abs_grad = self.sum_abs_grad.to(device='cpu', dtype=torch.float32) + + counts_cumsum = counts.cumsum(dim=0) + counts_tot = counts_cumsum[-1] + + # subdivide the distribution up into `num_bins` intervals for analysis, for greater + # statistical significance. each bin corresponds to multiple of the original 'tick' intervals. + num_bins = 20 + + # integer division + counts_per_bin = (counts_tot // num_bins) + 1 + bin_indexes = counts_cumsum // counts_per_bin + bin_indexes = bin_indexes.clamp(min=0, max=num_bins).to(torch.long) + + bin_counts = torch.zeros(num_bins, dtype=torch.long) + bin_counts.index_add_(dim=0, index=bin_indexes, source=counts) + bin_grad = torch.zeros(num_bins) + bin_grad.index_add_(dim=0, index=bin_indexes, source=sum_grad) + bin_gradsq = torch.zeros(num_bins) + bin_gradsq.index_add_(dim=0, index=bin_indexes, source=sum_gradsq) + bin_abs_grad = torch.zeros(num_bins) + bin_abs_grad.index_add_(dim=0, index=bin_indexes, source=sum_abs_grad) + + avg_grad = (bin_grad / bin_counts) + avg_grad_stddev = (bin_gradsq / bin_counts).sqrt() + + bin_boundary_counts = torch.arange(num_bins + 1, dtype=torch.long) * counts_per_bin + bin_tick_indexes = torch.searchsorted(counts_cumsum, bin_boundary_counts) + # boundaries are the "x" values between the bins, e.g. corresponding to the + # locations of percentiles of the distribution. + num_ticks_per_side = counts.numel() // 2 + bin_boundaries = (bin_tick_indexes - num_ticks_per_side) * self.tick_scale + + + bin_grad = bin_grad / (bin_counts + 1) + bin_conf_interval = bin_gradsq.sqrt() / (bin_counts + 1) # consider this a standard deviation. + # bin_grad / bin_abs_grad will give us a sense for how important in a practical sense, + # the gradients are. + bin_abs_grad = bin_abs_grad / (bin_counts + 1) + + bin_rel_grad = bin_grad / (bin_abs_grad + 1.0e-20) + bin_conf = bin_grad / (bin_conf_interval + 1.0e-20) + + def tensor_to_str(x: Tensor): + x = ["%.2g" % f for f in x] + x = "[" + " ".join(x) + "]" + return x + + + maybe_class_name = f" type={self.class_name}," if self.class_name is not None else "" + + print( + f"module={self.name},{maybe_class_name} bin-boundaries={tensor_to_str(bin_boundaries)}, " + f"rel_grad={tensor_to_str(bin_rel_grad)}, grad_conf={tensor_to_str(bin_conf)}" + ) + + + class ModelDiagnostic(object): """This class stores diagnostics for all tensors in the torch.nn.Module. @@ -297,9 +485,11 @@ class ModelDiagnostic(object): self.opts = opts self.diagnostics = dict() + def __getitem__(self, name: str): + T = ScalarDiagnostic if name[-7:] == '.scalar' else TensorDiagnostic if name not in self.diagnostics: - self.diagnostics[name] = TensorDiagnostic(self.opts, name) + self.diagnostics[name] = T(self.opts, name) return self.diagnostics[name] def print_diagnostics(self): @@ -332,41 +522,73 @@ def attach_diagnostics( if name == "": name = "" + + # Setting model_diagnostic=ans and n=name below, instead of trying to # capture the variables, ensures that we use the current values. - # (matters for name, since the variable gets overwritten). + # (this matters for `name`, since the variable gets overwritten). # These closures don't really capture by value, only by # "the final value the variable got in the function" :-( - def forward_hook(_module, _input, _output, _model_diagnostic=ans, _name=name): + def forward_hook( + _module, _input, _output, _model_diagnostic=ans, _name=name + ): if isinstance(_output, tuple) and len(_output) == 1: _output = _output[0] - if isinstance(_output, Tensor): - _model_diagnostic[f"{_name}.output"].accumulate( - _output, class_name=type(_module).__name__ - ) + if isinstance(_output, Tensor) and _output.dtype in ( torch.float32, torch.float16, torch.float64 ): + _model_diagnostic[f"{_name}.output"].accumulate(_output, + class_name=type(_module).__name__) elif isinstance(_output, tuple): for i, o in enumerate(_output): - _model_diagnostic[f"{_name}.output[{i}]"].accumulate( - o, class_name=type(_module).__name__ - ) + if o.dtype in ( torch.float32, torch.float16, torch.float64 ): + _model_diagnostic[f"{_name}.output[{i}]"].accumulate(o, + class_name=type(_module).__name__) - def backward_hook(_module, _input, _output, _model_diagnostic=ans, _name=name): + def backward_hook( + _module, _input, _output, _model_diagnostic=ans, _name=name + ): if isinstance(_output, tuple) and len(_output) == 1: _output = _output[0] - if isinstance(_output, Tensor): - _model_diagnostic[f"{_name}.grad"].accumulate( - _output, class_name=type(_module).__name__ - ) + if isinstance(_output, Tensor) and _output.dtype in ( torch.float32, torch.float16, torch.float64 ): + _model_diagnostic[f"{_name}.grad"].accumulate(_output, + class_name=type(_module).__name__) elif isinstance(_output, tuple): for i, o in enumerate(_output): - _model_diagnostic[f"{_name}.grad[{i}]"].accumulate( - o, class_name=type(_module).__name__ - ) + if o.dtype in ( torch.float32, torch.float16, torch.float64 ): + _model_diagnostic[f"{_name}.grad[{i}]"].accumulate(o, + class_name=type(_module).__name__) + module.register_forward_hook(forward_hook) module.register_backward_hook(backward_hook) + if type(module).__name__ in ["Sigmoid", "Tanh", "ReLU", "TanSwish", "Swish", "DoubleSwish", "Swoosh"]: + # For these specific module types, accumulate some additional diagnostics + # that can help us improve the activation function. These require a lot of memory, + # to save the forward activations, so limit this to some select classes. + # Note: this will not work correctly for all model types. + def scalar_forward_hook( + _module, _input, _output, _model_diagnostic=ans, _name=name + ): + if isinstance(_input, tuple): + _input, = _input + assert isinstance(_input, Tensor) + _model_diagnostic[f"{_name}.scalar"].accumulate_input(_input, + class_name=type(_module).__name__) + + def scalar_backward_hook( + _module, _input, _output, _model_diagnostic=ans, _name=name + ): + if isinstance(_output, tuple): + _output, = _output + assert isinstance(_output, Tensor) + _model_diagnostic[f"{_name}.scalar"].accumulate_output_grad(_output) + + module.register_forward_hook(scalar_forward_hook) + module.register_backward_hook(scalar_backward_hook) + + + for name, parameter in model.named_parameters(): def param_backward_hook( @@ -390,7 +612,7 @@ def _test_tensor_diagnostic(): diagnostic.print_diagnostics() - model = nn.Sequential(nn.Linear(100, 50), nn.Linear(50, 80)) + model = nn.Sequential(nn.Linear(100, 50), nn.ReLU(), nn.Linear(50, 80)) diagnostic = attach_diagnostics(model, opts) for _ in range(10): diff --git a/icefall/utils.py b/icefall/utils.py index 4aa8197ad..eba95ee11 100644 --- a/icefall/utils.py +++ b/icefall/utils.py @@ -1195,6 +1195,64 @@ def measure_gradient_norms(model: nn.Module, norm: str = "l1") -> Dict[str, floa return norms +def get_parameter_groups_with_lrs( + model: nn.Module, lr: float, include_names: bool = False +) -> List[dict]: + """ + This is for use with the ScaledAdam optimizers (more recent versions that accept lists of + named-parameters; we can, if needed, create a version without the names). + + It provides a way to specifiy learning-rate scales inside the module, so that if + any nn.Module in the hierarchy has a floating-point parameter 'lr_scale', it will + scale the LR of any parameters inside that module or its submodules. Note: you + can set module parameters outside the __init__ function, e.g.: + >>> a = nn.Linear(10, 10) + >>> a.lr_scale = 0.5 + + Returns: a list of dicts, of the following form: + if include_names == False: + [ { 'params': [ tensor1, tensor2, ... ], 'lr': 0.01 }, + { 'params': [ tensor3, tensor4, ... ], 'lr': 0.005 }, + ... ] + if include_names == true: + [ { 'named_params': [ (name1, tensor1, (name2, tensor2), ... ], 'lr': 0.01 }, + { 'named_params': [ (name3, tensor3), (name4, tensor4), ... ], 'lr': 0.005 }, + ... ] + + """ + # flat_lr_scale just contains the lr_scale explicitly specified + # for each prefix of the name, e.g. 'encoder.layers.3', these need + # to be multiplied for all prefix of the name of any given parameter. + flat_lr_scale = defaultdict(lambda: 1.0) + names = [] + for name, m in model.named_modules(): + names.append(name) + if hasattr(m, "lr_scale"): + flat_lr_scale[name] = m.lr_scale + + # lr_to_parames is a dict from learning rate (floating point) to: if + # include_names == true, a list of (name, parameter) for that learning rate; + # otherwise a list of parameters for that learning rate. + lr_to_params = defaultdict(list) + + for name, parameter in model.named_parameters(): + split_name = name.split(".") + # caution: as a special case, if the name is '', split_name will be [ '' ]. + prefix = split_name[0] + cur_lr = lr * flat_lr_scale[prefix] + if prefix != "": + cur_lr *= flat_lr_scale[""] + for part in split_name[1:]: + prefix = ".".join([prefix, part]) + cur_lr *= flat_lr_scale[prefix] + lr_to_params[cur_lr].append((name, parameter) if include_names else parameter) + + if include_names: + return [{"named_params": pairs, "lr": lr} for lr, pairs in lr_to_params.items()] + else: + return [{"params": params, "lr": lr} for lr, params in lr_to_params.items()] + + def optim_step_and_measure_param_change( model: nn.Module, old_parameters: Dict[str, nn.parameter.Parameter], diff --git a/pyproject.toml b/pyproject.toml index 3183055d4..650167e2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,4 +11,6 @@ exclude = ''' )/ | make_kn_lm.py | icefall\/__init__\.py + | icefall\/diagnostics\.py + | egs\/librispeech\/ASR\/zipformer ''' diff --git a/requirements-ci.txt b/requirements-ci.txt index 50d4e5e3f..0c9705a58 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -11,7 +11,7 @@ graphviz==0.19.1 -f https://download.pytorch.org/whl/cpu/torch_stable.html torch==1.10.0+cpu -f https://download.pytorch.org/whl/cpu/torch_stable.html torchaudio==0.10.0+cpu --f https://k2-fsa.org/nightly/ k2==1.15.1.dev20220426+cpu.torch1.10.0 +-f https://k2-fsa.org/nightly/ k2==1.23.4.dev20230316+cpu.torch1.10.0 git+https://github.com/lhotse-speech/lhotse kaldilm==1.11 From a7e142b7ff7a34be59889bac855331b142ca65eb Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Fri, 19 May 2023 20:27:55 +0800 Subject: [PATCH 006/100] Support long audios recognition (#980) * support long file transcription * rename recipe as long_file_recog * add docs * support multi-gpu decoding * style fix --- egs/librispeech/ASR/long_file_recog.sh | 94 +++ .../ASR/long_file_recog/asr_datamodule.py | 189 ++++++ .../ASR/long_file_recog/beam_search.py | 613 ++++++++++++++++++ .../ASR/long_file_recog/merge_chunks.py | 240 +++++++ .../ASR/long_file_recog/recognize.py | 435 +++++++++++++ .../ASR/long_file_recog/split_into_chunks.py | 100 +++ .../beam_search.py | 6 + icefall/utils.py | 5 +- 8 files changed, 1681 insertions(+), 1 deletion(-) create mode 100755 egs/librispeech/ASR/long_file_recog.sh create mode 100644 egs/librispeech/ASR/long_file_recog/asr_datamodule.py create mode 100644 egs/librispeech/ASR/long_file_recog/beam_search.py create mode 100755 egs/librispeech/ASR/long_file_recog/merge_chunks.py create mode 100755 egs/librispeech/ASR/long_file_recog/recognize.py create mode 100755 egs/librispeech/ASR/long_file_recog/split_into_chunks.py diff --git a/egs/librispeech/ASR/long_file_recog.sh b/egs/librispeech/ASR/long_file_recog.sh new file mode 100755 index 000000000..acd1b1253 --- /dev/null +++ b/egs/librispeech/ASR/long_file_recog.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash + +# fix segmentation fault reported in https://github.com/k2-fsa/icefall/issues/674 +export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +set -eou pipefail + +# This script is used to recogize long audios. The process is as follows: +# 1) Split long audios into chunks with overlaps. +# 2) Perform speech recognition on chunks, getting tokens and timestamps. +# 3) Merge the overlapped chunks into utterances acording to the timestamps. + +# Each chunk (except the first and the last) is padded with extra left side and right side. +# The chunk length is: left_side + chunk_size + right_side. +chunk=30.0 +extra=2.0 + +stage=1 +stop_stage=4 + +# We assume that you have downloaded the LibriLight dataset +# with audio files in $corpus_dir and texts in $text_dir +corpus_dir=$PWD/download/libri-light +text_dir=$PWD/download/librilight_text +# Path to save the manifests +output_dir=$PWD/data/librilight + +world_size=4 + + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + # We will get librilight_recodings_{subset}.jsonl.gz and librilight_supervisions_{subset}.jsonl.gz + # saved in $output_dir/manifests + log "Stage 1: Prepare LibriLight manifest" + lhotse prepare librilight $corpus_dir $text_dir $output_dir/manifests -j 10 +fi + +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + # Chunk manifests are saved to $output_dir/manifests_chunk/librilight_cuts_{subset}.jsonl.gz + log "Stage 2: Split long audio into chunks" + ./long_file_recog/split_into_chunks.py \ + --manifest-in-dir $output_dir/manifests \ + --manifest-out-dir $output_dir/manifests_chunk \ + --chunk $chunk \ + --extra $extra # Extra duration (in seconds) at both sides +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + # Recognized tokens and timestamps are saved to $output_dir/manifests_chunk_recog/librilight_cuts_{subset}.jsonl.gz + + # This script loads torchscript models, exported by `torch.jit.script()`, + # and uses it to decode waves. + # You can download the jit model from https://huggingface.co/csukuangfj/icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11 + + log "Stage 3: Perform speech recognition on splitted chunks" + for subset in small median large; do + ./long_file_recog/recognize.py \ + --world-size $world_size \ + --num-workers 8 \ + --subset $subset \ + --manifest-in-dir $output_dir/manifests_chunk \ + --manifest-out-dir $output_dir/manifests_chunk_recog \ + --nn-model-filename long_file_recog/exp/jit_model.pt \ + --bpe-model data/lang_bpe_500/bpe.model \ + --max-duration 2400 \ + --decoding-method greedy_search + --master 12345 + + if [ $world_size -gt 1 ]; then + # Combine manifests from different jobs + lhotse combine $(find $output_dir/manifests_chunk_recog -name librilight_cuts_${subset}_job_*.jsonl.gz | tr "\n" " ") $output_dir/manifests_chunk_recog/librilight_cuts_${subset}.jsonl.gz + fi + done +fi + +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + # Final results are saved in $output_dir/manifests/librilight_cuts_{subset}.jsonl.gz + log "Stage 4: Merge splitted chunks into utterances." + ./long_file_recog/merge_chunks.py \ + --manifest-in-dir $output_dir/manifests_chunk_recog \ + --manifest-out-dir $output_dir/manifests \ + --bpe-model data/lang_bpe_500/bpe.model \ + --extra $extra +fi + + diff --git a/egs/librispeech/ASR/long_file_recog/asr_datamodule.py b/egs/librispeech/ASR/long_file_recog/asr_datamodule.py new file mode 100644 index 000000000..eddce7213 --- /dev/null +++ b/egs/librispeech/ASR/long_file_recog/asr_datamodule.py @@ -0,0 +1,189 @@ +# Copyright 2021 Piotr Żelasko +# Copyright 2022 Xiaomi Corporation (Author: Mingshuang Luo) +# Copyright 2023 Xiaomi Corporation (Author: Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 +from functools import lru_cache +from pathlib import Path +from typing import Dict, List, Union + +import torch +from lhotse import CutSet, Fbank, FbankConfig, load_manifest_lazy +from lhotse.cut import Cut +from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures + CutConcatenate, + CutMix, + DynamicBucketingSampler, + K2SpeechRecognitionDataset, + PrecomputedFeatures, + SimpleCutSampler, + SpecAugment, +) +from lhotse.dataset.input_strategies import ( # noqa F401 For AudioSamples + AudioSamples, + BatchIO, + OnTheFlyFeatures, +) +from torch.utils.data import DataLoader + +from icefall.utils import str2bool + + +class SpeechRecognitionDataset(K2SpeechRecognitionDataset): + def __init__( + self, + return_cuts: bool = False, + input_strategy: BatchIO = PrecomputedFeatures(), + ): + super().__init__(return_cuts=return_cuts, input_strategy=input_strategy) + + def __getitem__(self, cuts: CutSet) -> Dict[str, Union[torch.Tensor, List[Cut]]]: + """ + Return a new batch, with the batch size automatically determined using the constraints + of max_frames and max_cuts. + """ + self.hdf5_fix.update() + + # Note: don't sort cuts here + # Sort the cuts by duration so that the first one determines the batch time dimensions. + # cuts = cuts.sort_by_duration(ascending=False) + + # Get a tensor with batched feature matrices, shape (B, T, F) + # Collation performs auto-padding, if necessary. + input_tpl = self.input_strategy(cuts) + if len(input_tpl) == 3: + # An input strategy with fault tolerant audio reading mode. + # "cuts" may be a subset of the original "cuts" variable, + # that only has cuts for which we succesfully read the audio. + inputs, _, cuts = input_tpl + else: + inputs, _ = input_tpl + + # Get a dict of tensors that encode the positional information about supervisions + # in the batch of feature matrices. The tensors are named "sequence_idx", + # "start_frame/sample" and "num_frames/samples". + supervision_intervals = self.input_strategy.supervision_intervals(cuts) + + batch = {"inputs": inputs, "supervisions": supervision_intervals} + if self.return_cuts: + batch["supervisions"]["cut"] = [cut for cut in cuts] + + return batch + + +class AsrDataModule: + """ + DataModule for k2 ASR experiments. + It assumes there is always one train and valid dataloader, + but there can be multiple test dataloaders (e.g. LibriSpeech test-clean + and test-other). + + It contains all the common data pipeline modules used in ASR + experiments, e.g.: + - dynamic batch size, + - bucketing samplers, + - cut concatenation, + - augmentation, + - on-the-fly feature extraction + + This class should be derived for specific corpora used in ASR tasks. + """ + + def __init__(self, args: argparse.Namespace): + self.args = args + + @classmethod + def add_arguments(cls, parser: argparse.ArgumentParser): + group = parser.add_argument_group( + title="ASR data related options", + description="These options are used for the preparation of " + "PyTorch DataLoaders from Lhotse CutSet's -- they control the " + "effective batch sizes, sampling strategies, applied data " + "augmentations, etc.", + ) + group.add_argument( + "--manifest-dir", + type=Path, + default=Path("data/manifests_chunk"), + help="Path to directory with train/valid/test cuts.", + ) + group.add_argument( + "--max-duration", + type=int, + default=600.0, + help="Maximum pooled recordings duration (seconds) in a " + "single batch. You can reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--bucketing-sampler", + type=str2bool, + default=True, + help="When enabled, the batches will come from buckets of " + "similar duration (saves padding frames).", + ) + group.add_argument( + "--return-cuts", + type=str2bool, + default=True, + help="When enabled, each batch will have the " + "field: batch['supervisions']['cut'] with the cuts that " + "were used to construct it.", + ) + group.add_argument( + "--num-workers", + type=int, + default=8, + help="The number of training dataloader workers that " + "collect the batches.", + ) + + group.add_argument( + "--input-strategy", + type=str, + default="PrecomputedFeatures", + help="AudioSamples or PrecomputedFeatures", + ) + + def dataloaders(self, cuts: CutSet) -> DataLoader: + logging.debug("About to create test dataset") + test = SpeechRecognitionDataset( + input_strategy=OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))), + return_cuts=self.args.return_cuts, + ) + + sampler = SimpleCutSampler( + cuts, + max_duration=self.args.max_duration, + shuffle=False, + drop_last=False, + ) + + logging.debug("About to create test dataloader") + test_dl = DataLoader( + test, + batch_size=None, + sampler=sampler, + num_workers=self.args.num_workers, + persistent_workers=False, + ) + return test_dl + + @lru_cache() + def load_subset(self, cuts_filename: Path) -> CutSet: + return load_manifest_lazy(cuts_filename) diff --git a/egs/librispeech/ASR/long_file_recog/beam_search.py b/egs/librispeech/ASR/long_file_recog/beam_search.py new file mode 100644 index 000000000..f8c31861c --- /dev/null +++ b/egs/librispeech/ASR/long_file_recog/beam_search.py @@ -0,0 +1,613 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang +# Xiaoyu Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 warnings +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Union + +import k2 +import torch + +from icefall.decode import one_best_decoding +from icefall.utils import DecodingResults, get_texts, get_texts_with_timestamp + + +def fast_beam_search( + model: torch.nn.Module, + decoding_graph: k2.Fsa, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + beam: float, + max_states: int, + max_contexts: int, + temperature: float = 1.0, +) -> k2.Fsa: + """It limits the maximum number of symbols per frame to 1. + + Args: + model: + An instance of `Transducer`. + decoding_graph: + Decoding graph used for decoding, may be a TrivialGraph or a LG. + encoder_out: + A tensor of shape (N, T, C) from the encoder. + encoder_out_lens: + A tensor of shape (N,) containing the number of frames in `encoder_out` + before padding. + beam: + Beam value, similar to the beam used in Kaldi.. + max_states: + Max states per stream per frame. + max_contexts: + Max contexts pre stream per frame. + temperature: + Softmax temperature. + Returns: + Return an FsaVec with axes [utt][state][arc] containing the decoded + lattice. Note: When the input graph is a TrivialGraph, the returned + lattice is actually an acceptor. + """ + assert encoder_out.ndim == 3 + + context_size = model.decoder.context_size + vocab_size = model.decoder.vocab_size + + B, T, C = encoder_out.shape + + config = k2.RnntDecodingConfig( + vocab_size=vocab_size, + decoder_history_len=context_size, + beam=beam, + max_contexts=max_contexts, + max_states=max_states, + ) + individual_streams = [] + for i in range(B): + individual_streams.append(k2.RnntDecodingStream(decoding_graph)) + decoding_streams = k2.RnntDecodingStreams(individual_streams, config) + + encoder_out = model.joiner.encoder_proj(encoder_out) + + for t in range(T): + # shape is a RaggedShape of shape (B, context) + # contexts is a Tensor of shape (shape.NumElements(), context_size) + shape, contexts = decoding_streams.get_contexts() + # `nn.Embedding()` in torch below v1.7.1 supports only torch.int64 + contexts = contexts.to(torch.int64) + # decoder_out is of shape (shape.NumElements(), 1, decoder_out_dim) + decoder_out = model.decoder(contexts, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + # current_encoder_out is of shape + # (shape.NumElements(), 1, joiner_dim) + # fmt: off + current_encoder_out = torch.index_select( + encoder_out[:, t:t + 1, :], 0, shape.row_ids(1).to(torch.int64) + ) + # fmt: on + logits = model.joiner( + current_encoder_out.unsqueeze(2), + decoder_out.unsqueeze(1), + project_input=False, + ) + logits = logits.squeeze(1).squeeze(1) + log_probs = (logits / temperature).log_softmax(dim=-1) + decoding_streams.advance(log_probs) + decoding_streams.terminate_and_flush_to_streams() + lattice = decoding_streams.format_output(encoder_out_lens.tolist()) + + return lattice + + +def fast_beam_search_one_best( + model: torch.nn.Module, + decoding_graph: k2.Fsa, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + beam: float, + max_states: int, + max_contexts: int, + temperature: float = 1.0, + return_timestamps: bool = False, +) -> Union[List[List[int]], DecodingResults]: + """It limits the maximum number of symbols per frame to 1. + + A lattice is first obtained using fast beam search, and then + the shortest path within the lattice is used as the final output. + + Args: + model: + An instance of `Transducer`. + decoding_graph: + Decoding graph used for decoding, may be a TrivialGraph or a LG. + encoder_out: + A tensor of shape (N, T, C) from the encoder. + encoder_out_lens: + A tensor of shape (N,) containing the number of frames in `encoder_out` + before padding. + beam: + Beam value, similar to the beam used in Kaldi.. + max_states: + Max states per stream per frame. + max_contexts: + Max contexts pre stream per frame. + temperature: + Softmax temperature. + return_timestamps: + Whether to return timestamps. + Returns: + If return_timestamps is False, return the decoded result. + Else, return a DecodingResults object containing + decoded result and corresponding timestamps. + """ + lattice = fast_beam_search( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=beam, + max_states=max_states, + max_contexts=max_contexts, + temperature=temperature, + ) + + best_path = one_best_decoding(lattice) + + if not return_timestamps: + return get_texts(best_path) + else: + return get_texts_with_timestamp(best_path) + + +def greedy_search_batch( + model: torch.nn.Module, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + return_timestamps: bool = False, +) -> Union[List[List[int]], DecodingResults]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + Output from the encoder. Its shape is (N, T, C), where N >= 1. + encoder_out_lens: + A 1-D tensor of shape (N,), containing number of valid frames in + encoder_out before padding. + return_timestamps: + Whether to return timestamps. + Returns: + If return_timestamps is False, return the decoded result. + Else, return a DecodingResults object containing + decoded result and corresponding timestamps. + """ + assert encoder_out.ndim == 3 + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + device = next(model.parameters()).device + + blank_id = model.decoder.blank_id + unk_id = getattr(model, "unk_id", blank_id) + context_size = model.decoder.context_size + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + hyps = [[-1] * (context_size - 1) + [blank_id] for _ in range(N)] + + # timestamp[n][i] is the frame index after subsampling + # on which hyp[n][i] is decoded + timestamps = [[] for _ in range(N)] + # scores[n][i] is the logits on which hyp[n][i] is decoded + scores = [[] for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + device=device, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + # decoder_out: (N, 1, decoder_out_dim) + + encoder_out = model.joiner.encoder_proj(packed_encoder_out.data) + + offset = 0 + for (t, batch_size) in enumerate(batch_size_list): + start = offset + end = offset + batch_size + current_encoder_out = encoder_out.data[start:end] + current_encoder_out = current_encoder_out.unsqueeze(1).unsqueeze(1) + # current_encoder_out's shape: (batch_size, 1, 1, encoder_out_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + + logits = model.joiner( + current_encoder_out, decoder_out.unsqueeze(1), project_input=False + ) + # logits'shape (batch_size, 1, 1, vocab_size) + + logits = logits.squeeze(1).squeeze(1) # (batch_size, vocab_size) + log_probs = logits.log_softmax(dim=-1) + assert log_probs.ndim == 2, log_probs.shape + y = log_probs.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v not in (blank_id, unk_id): + hyps[i].append(v) + timestamps[i].append(t) + scores[i].append(log_probs[i, v].item()) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + device=device, + dtype=torch.int64, + ) + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + ans_timestamps = [] + ans_scores = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + ans_timestamps.append(timestamps[unsorted_indices[i]]) + ans_scores.append(scores[unsorted_indices[i]]) + + if not return_timestamps: + return ans + else: + return DecodingResults( + hyps=ans, + timestamps=ans_timestamps, + scores=ans_scores, + ) + + +@dataclass +class Hypothesis: + # The predicted tokens so far. + # Newly predicted tokens are appended to `ys`. + ys: List[int] + + # The log prob of ys. + # It contains only one entry. + log_prob: torch.Tensor + + # timestamp[i] is the frame index after subsampling + # on which ys[i] is decoded + timestamp: List[int] = field(default_factory=list) + + @property + def key(self) -> str: + """Return a string representation of self.ys""" + return "_".join(map(str, self.ys)) + + +class HypothesisList(object): + def __init__(self, data: Optional[Dict[str, Hypothesis]] = None) -> None: + """ + Args: + data: + A dict of Hypotheses. Its key is its `value.key`. + """ + if data is None: + self._data = {} + else: + self._data = data + + @property + def data(self) -> Dict[str, Hypothesis]: + return self._data + + def add(self, hyp: Hypothesis) -> None: + """Add a Hypothesis to `self`. + + If `hyp` already exists in `self`, its probability is updated using + `log-sum-exp` with the existed one. + + Args: + hyp: + The hypothesis to be added. + """ + key = hyp.key + if key in self: + old_hyp = self._data[key] # shallow copy + torch.logaddexp(old_hyp.log_prob, hyp.log_prob, out=old_hyp.log_prob) + else: + self._data[key] = hyp + + def get_most_probable(self, length_norm: bool = False) -> Hypothesis: + """Get the most probable hypothesis, i.e., the one with + the largest `log_prob`. + + Args: + length_norm: + If True, the `log_prob` of a hypothesis is normalized by the + number of tokens in it. + Returns: + Return the hypothesis that has the largest `log_prob`. + """ + if length_norm: + return max(self._data.values(), key=lambda hyp: hyp.log_prob / len(hyp.ys)) + else: + return max(self._data.values(), key=lambda hyp: hyp.log_prob) + + def remove(self, hyp: Hypothesis) -> None: + """Remove a given hypothesis. + + Caution: + `self` is modified **in-place**. + + Args: + hyp: + The hypothesis to be removed from `self`. + Note: It must be contained in `self`. Otherwise, + an exception is raised. + """ + key = hyp.key + assert key in self, f"{key} does not exist" + del self._data[key] + + def filter(self, threshold: torch.Tensor) -> "HypothesisList": + """Remove all Hypotheses whose log_prob is less than threshold. + + Caution: + `self` is not modified. Instead, a new HypothesisList is returned. + + Returns: + Return a new HypothesisList containing all hypotheses from `self` + with `log_prob` being greater than the given `threshold`. + """ + ans = HypothesisList() + for _, hyp in self._data.items(): + if hyp.log_prob > threshold: + ans.add(hyp) # shallow copy + return ans + + def topk(self, k: int) -> "HypothesisList": + """Return the top-k hypothesis.""" + hyps = list(self._data.items()) + + hyps = sorted(hyps, key=lambda h: h[1].log_prob, reverse=True)[:k] + + ans = HypothesisList(dict(hyps)) + return ans + + def __contains__(self, key: str): + return key in self._data + + def __iter__(self): + return iter(self._data.values()) + + def __len__(self) -> int: + return len(self._data) + + def __str__(self) -> str: + s = [] + for key in self: + s.append(key) + return ", ".join(s) + + +def get_hyps_shape(hyps: List[HypothesisList]) -> k2.RaggedShape: + """Return a ragged shape with axes [utt][num_hyps]. + + Args: + hyps: + len(hyps) == batch_size. It contains the current hypothesis for + each utterance in the batch. + Returns: + Return a ragged shape with 2 axes [utt][num_hyps]. Note that + the shape is on CPU. + """ + num_hyps = [len(h) for h in hyps] + + # torch.cumsum() is inclusive sum, so we put a 0 at the beginning + # to get exclusive sum later. + num_hyps.insert(0, 0) + + num_hyps = torch.tensor(num_hyps) + row_splits = torch.cumsum(num_hyps, dim=0, dtype=torch.int32) + ans = k2.ragged.create_ragged_shape2( + row_splits=row_splits, cached_tot_size=row_splits[-1].item() + ) + return ans + + +def modified_beam_search( + model: torch.nn.Module, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + beam: int = 4, + temperature: float = 1.0, + return_timestamps: bool = False, +) -> Union[List[List[int]], DecodingResults]: + """Beam search in batch mode with --max-sym-per-frame=1 being hardcoded. + + Args: + model: + The transducer model. + encoder_out: + Output from the encoder. Its shape is (N, T, C). + encoder_out_lens: + A 1-D tensor of shape (N,), containing number of valid frames in + encoder_out before padding. + beam: + Number of active paths during the beam search. + temperature: + Softmax temperature. + return_timestamps: + Whether to return timestamps. + Returns: + If return_timestamps is False, return the decoded result. + Else, return a DecodingResults object containing + decoded result and corresponding timestamps. + """ + assert encoder_out.ndim == 3, encoder_out.shape + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + blank_id = model.decoder.blank_id + unk_id = getattr(model, "unk_id", blank_id) + context_size = model.decoder.context_size + device = next(model.parameters()).device + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + B = [HypothesisList() for _ in range(N)] + for i in range(N): + B[i].add( + Hypothesis( + ys=[blank_id] * context_size, + log_prob=torch.zeros(1, dtype=torch.float32, device=device), + timestamp=[], + ) + ) + + encoder_out = model.joiner.encoder_proj(packed_encoder_out.data) + + offset = 0 + finalized_B = [] + for (t, batch_size) in enumerate(batch_size_list): + start = offset + end = offset + batch_size + current_encoder_out = encoder_out.data[start:end] + current_encoder_out = current_encoder_out.unsqueeze(1).unsqueeze(1) + # current_encoder_out's shape is (batch_size, 1, 1, encoder_out_dim) + offset = end + + finalized_B = B[batch_size:] + finalized_B + B = B[:batch_size] + + hyps_shape = get_hyps_shape(B).to(device) + + A = [list(b) for b in B] + B = [HypothesisList() for _ in range(batch_size)] + + ys_log_probs = torch.cat( + [hyp.log_prob.reshape(1, 1) for hyps in A for hyp in hyps] + ) # (num_hyps, 1) + + decoder_input = torch.tensor( + [hyp.ys[-context_size:] for hyps in A for hyp in hyps], + device=device, + dtype=torch.int64, + ) # (num_hyps, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False).unsqueeze(1) + decoder_out = model.joiner.decoder_proj(decoder_out) + # decoder_out is of shape (num_hyps, 1, 1, joiner_dim) + + # Note: For torch 1.7.1 and below, it requires a torch.int64 tensor + # as index, so we use `to(torch.int64)` below. + current_encoder_out = torch.index_select( + current_encoder_out, + dim=0, + index=hyps_shape.row_ids(1).to(torch.int64), + ) # (num_hyps, 1, 1, encoder_out_dim) + + logits = model.joiner( + current_encoder_out, + decoder_out, + project_input=False, + ) # (num_hyps, 1, 1, vocab_size) + + logits = logits.squeeze(1).squeeze(1) # (num_hyps, vocab_size) + + log_probs = (logits / temperature).log_softmax(dim=-1) # (num_hyps, vocab_size) + + log_probs.add_(ys_log_probs) + + vocab_size = log_probs.size(-1) + + log_probs = log_probs.reshape(-1) + + row_splits = hyps_shape.row_splits(1) * vocab_size + log_probs_shape = k2.ragged.create_ragged_shape2( + row_splits=row_splits, cached_tot_size=log_probs.numel() + ) + ragged_log_probs = k2.RaggedTensor(shape=log_probs_shape, value=log_probs) + + for i in range(batch_size): + topk_log_probs, topk_indexes = ragged_log_probs[i].topk(beam) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + topk_hyp_indexes = (topk_indexes // vocab_size).tolist() + topk_token_indexes = (topk_indexes % vocab_size).tolist() + + for k in range(len(topk_hyp_indexes)): + hyp_idx = topk_hyp_indexes[k] + hyp = A[i][hyp_idx] + + new_ys = hyp.ys[:] + new_token = topk_token_indexes[k] + new_timestamp = hyp.timestamp[:] + if new_token not in (blank_id, unk_id): + new_ys.append(new_token) + new_timestamp.append(t) + + new_log_prob = topk_log_probs[k] + new_hyp = Hypothesis( + ys=new_ys, log_prob=new_log_prob, timestamp=new_timestamp + ) + B[i].add(new_hyp) + + B = B + finalized_B + best_hyps = [b.get_most_probable(length_norm=True) for b in B] + + sorted_ans = [h.ys[context_size:] for h in best_hyps] + sorted_timestamps = [h.timestamp for h in best_hyps] + ans = [] + ans_timestamps = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + ans_timestamps.append(sorted_timestamps[unsorted_indices[i]]) + + if not return_timestamps: + return ans + else: + return DecodingResults( + hyps=ans, + timestamps=ans_timestamps, + ) diff --git a/egs/librispeech/ASR/long_file_recog/merge_chunks.py b/egs/librispeech/ASR/long_file_recog/merge_chunks.py new file mode 100755 index 000000000..d38d9c86a --- /dev/null +++ b/egs/librispeech/ASR/long_file_recog/merge_chunks.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file merge overlapped chunks into utterances accroding to recording ids. +""" + +import argparse +import logging +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import List + +import sentencepiece as spm +from lhotse import ( + CutSet, + MonoCut, + SupervisionSegment, + SupervisionSet, + load_manifest, + load_manifest_lazy, +) +from lhotse.cut import Cut +from lhotse.serialization import SequentialJsonlWriter + + +def get_parser(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--manifest-in-dir", + type=Path, + default=Path("data/librilight/manifests_chunk_recog"), + help="Path to directory of chunk cuts with recognition results.", + ) + + parser.add_argument( + "--manifest-out-dir", + type=Path, + default=Path("data/manifests"), + help="Path to directory to save full utterance by merging overlapped chunks.", + ) + + parser.add_argument( + "--extra", + type=float, + default=2.0, + help="""Extra duration (in seconds) at both sides.""", + ) + + return parser.parse_args() + + +def merge_chunks( + cuts_chunk: CutSet, + supervisions: SupervisionSet, + cuts_writer: SequentialJsonlWriter, + sp: spm.SentencePieceProcessor, + extra: float, +) -> int: + """Merge chunk-wise cuts accroding to recording ids. + + Args: + cuts_chunk: + The chunk-wise cuts opened in a lazy mode. + supervisions: + The supervision manifest containing text file path, opened in a lazy mode. + cuts_writer: + Writer to save the cuts with recognition results. + sp: + The BPE model. + extra: + Extra duration (in seconds) to drop at both sides of each chunk. + """ + + # Background worker to add alignemnt and save cuts to disk. + def _save_worker(utt_cut: Cut, flush=False): + cuts_writer.write(utt_cut, flush=flush) + + def _merge(cut_list: List[Cut], rec_id: str, utt_idx: int): + """Merge chunks with same recording_id.""" + for cut in cut_list: + assert cut.recording.id == rec_id, (cut.recording.id, rec_id) + + # For each group with a same recording, sort it accroding to the start time + # In fact, we don't need to do this since the cuts have been sorted + # according to the start time + cut_list = sorted(cut_list, key=(lambda cut: cut.start)) + + rec = cut_list[0].recording + alignments = [] + cur_end = 0 + for cut in cut_list: + # Get left and right borders + left = cut.start + extra if cut.start > 0 else 0 + chunk_end = cut.start + cut.duration + right = chunk_end - extra if chunk_end < rec.duration else rec.duration + + # Assert the chunks are continuous + assert left == cur_end, (left, cur_end) + cur_end = right + + assert len(cut.supervisions) == 1, len(cut.supervisions) + for ali in cut.supervisions[0].alignment["symbol"]: + t = ali.start + cut.start + if left <= t < right: + alignments.append(ali.with_offset(cut.start)) + + old_sup = supervisions[rec_id] + # Assuming the supervisions are sorted with the same recoding order as in cuts_chunk + # old_sup = supervisions[utt_idx] + assert old_sup.recording_id == rec_id, (old_sup.recording_id, rec_id) + + new_sup = SupervisionSegment( + id=rec_id, + recording_id=rec_id, + start=0, + duration=rec.duration, + alignment={"symbol": alignments}, + language=old_sup.language, + speaker=old_sup.speaker, + ) + + utt_cut = MonoCut( + id=rec_id, + start=0, + duration=rec.duration, + channel=0, + recording=rec, + supervisions=[new_sup], + ) + # Set a custom attribute to the cut + utt_cut.text_path = old_sup.book + + return utt_cut + + last_rec_id = None + cut_list = [] + utt_idx = 0 + + futures = [] + with ThreadPoolExecutor(max_workers=1) as executor: + + for cut in cuts_chunk: + cur_rec_id = cut.recording.id + if len(cut_list) == 0: + # Case of the first cut + last_rec_id = cur_rec_id + cut_list.append(cut) + elif cur_rec_id == last_rec_id: + cut_list.append(cut) + else: + # Case of a cut belonging to a new recording + utt_cut = _merge(cut_list, last_rec_id, utt_idx) + utt_idx += 1 + + futures.append(executor.submit(_save_worker, utt_cut)) + + last_rec_id = cur_rec_id + cut_list = [cut] + + if utt_idx % 5000 == 0: + logging.info(f"Procesed {utt_idx} utterances.") + + # For the cuts belonging to the last recording + if len(cut_list) != 0: + utt_cut = _merge(cut_list, last_rec_id, utt_idx) + utt_idx += 1 + + futures.append(executor.submit(_save_worker, utt_cut)) + logging.info("Finished") + + for f in futures: + f.result() + + return utt_idx + + +def main(): + args = get_parser() + + sp = spm.SentencePieceProcessor() + sp.load(args.bpe_model) + + # It contains "librilight_recordings_*.jsonl.gz" and "librilight_supervisions_small.jsonl.gz" + manifest_out_dir = args.manifest_out_dir + + subsets = ["small", "median", "large"] + + for subset in subsets: + logging.info(f"Processing {subset} subset") + + manifest_out = manifest_out_dir / f"librilight_cuts_{subset}.jsonl.gz" + if manifest_out.is_file(): + logging.info(f"{manifest_out} already exists - skipping.") + continue + + supervisions = load_manifest( + manifest_out_dir / f"librilight_supervisions_{subset}.jsonl.gz" + ) # We will use the text path from supervisions + + cuts_chunk = load_manifest_lazy( + args.manifest_in_dir / f"librilight_cuts_{subset}.jsonl.gz" + ) + + cuts_writer = CutSet.open_writer(manifest_out, overwrite=True) + num_utt = merge_chunks( + cuts_chunk, supervisions, cuts_writer=cuts_writer, sp=sp, extra=args.extra + ) + cuts_writer.close() + logging.info(f"{num_utt} cuts saved to {manifest_out}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/ASR/long_file_recog/recognize.py b/egs/librispeech/ASR/long_file_recog/recognize.py new file mode 100755 index 000000000..96c83f859 --- /dev/null +++ b/egs/librispeech/ASR/long_file_recog/recognize.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads torchscript models, exported by `torch.jit.script()`, +and uses them to decode waves. +You can use the following command to get the exported models: + +./pruned_transducer_stateless7/export.py \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 20 \ + --avg 10 \ + --jit 1 + +You can also download the jit model from +https://huggingface.co/csukuangfj/icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11 +""" + +import argparse +import torch.multiprocessing as mp +import torch +import torch.nn as nn +import logging +from concurrent.futures import ThreadPoolExecutor +from typing import List, Optional, Tuple + +from pathlib import Path + +import k2 +import sentencepiece as spm +from asr_datamodule import AsrDataModule +from beam_search import ( + fast_beam_search_one_best, + greedy_search_batch, + modified_beam_search, +) +from icefall.utils import AttributeDict, convert_timestamp, setup_logger +from lhotse import CutSet, load_manifest_lazy +from lhotse.cut import Cut +from lhotse.supervision import AlignmentItem +from lhotse.serialization import SequentialJsonlWriter + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + parser.add_argument( + "--subset", + type=str, + default="small", + help="Subset to process. Possible values are 'small', 'medium', 'large'", + ) + + parser.add_argument( + "--manifest-in-dir", + type=Path, + default=Path("data/librilight/manifests_chunk"), + help="Path to directory with chunks cuts.", + ) + + parser.add_argument( + "--manifest-out-dir", + type=Path, + default=Path("data/librilight/manifests_chunk_recog"), + help="Path to directory to save the chunk cuts with recognition results.", + ) + + parser.add_argument( + "--log-dir", + type=Path, + default=Path("long_file_recog/log"), + help="Path to directory to save logs.", + ) + + parser.add_argument( + "--nn-model-filename", + type=str, + required=True, + help="Path to the torchscript model cpu_jit.pt", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - modified_beam_search + - fast_beam_search + """, + ) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing decoding parameters.""" + params = AttributeDict( + { + "subsampling_factor": 4, + "frame_shift_ms": 10, + # Used only when --method is beam_search or modified_beam_search. + "beam_size": 4, + # Used only when --method is beam_search or fast_beam_search. + # A floating point value to calculate the cutoff score during beam + # search (i.e., `cutoff = max-score - beam`), which is the same as the + # `beam` in Kaldi. + "beam": 4, + "max_contexts": 4, + "max_states": 8, + } + ) + return params + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + batch: dict, + decoding_graph: Optional[k2.Fsa] = None, +) -> Tuple[List[List[str]], List[List[float]], List[List[float]]]: + """Decode one batch. + + Args: + params: + It's the return value of :func:`get_params`. + paramsmodel: + The neural model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or LG, Used + only when --decoding_method is fast_beam_search. + + Returns: + Return the decoding result, timestamps, and scores. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + encoder_out, encoder_out_lens = model.encoder(x=feature, x_lens=feature_lens) + + if params.decoding_method == "fast_beam_search": + res = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + return_timestamps=True, + ) + elif params.decoding_method == "greedy_search": + res = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + return_timestamps=True, + ) + elif params.decoding_method == "modified_beam_search": + res = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + return_timestamps=True, + ) + else: + raise ValueError(f"Unsupported decoding method: {params.decoding_method}") + + hyps = [] + timestamps = [] + scores = [] + for i in range(feature.shape[0]): + hyps.append(res.hyps[i]) + timestamps.append( + convert_timestamp( + res.timestamps[i], params.subsampling_factor, params.frame_shift_ms + ) + ) + scores.append(res.scores[i]) + + return hyps, timestamps, scores + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + cuts_writer: SequentialJsonlWriter, + decoding_graph: Optional[k2.Fsa] = None, +) -> None: + """Decode dataset and store the recognition results to manifest. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + cuts_writer: + Writer to save the cuts with recognition results. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or LG, Used + only when --decoding_method is fast_beam_search. + + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains five elements: + - cut_id + - reference transcript + - predicted result + - timestamps of reference transcript + - timestamps of predicted result + """ + # Background worker to add alignemnt and save cuts to disk. + def _save_worker( + cuts: List[Cut], + hyps: List[List[str]], + timestamps: List[List[float]], + scores: List[List[float]], + ): + for cut, symbol_list, time_list, score_list in zip( + cuts, hyps, timestamps, scores + ): + symbol_list = sp.id_to_piece(symbol_list) + ali = [ + AlignmentItem(symbol=symbol, start=start, duration=None, score=score) + for symbol, start, score in zip(symbol_list, time_list, score_list) + ] + assert len(cut.supervisions) == 1, len(cut.supervisions) + cut.supervisions[0].alignment = {"symbol": ali} + cuts_writer.write(cut, flush=True) + + num_cuts = 0 + log_interval = 10 + futures = [] + with ThreadPoolExecutor(max_workers=1) as executor: + # We only want one background worker so that serialization is deterministic. + + for batch_idx, batch in enumerate(dl): + cuts = batch["supervisions"]["cut"] + + hyps, timestamps, scores = decode_one_batch( + params=params, + model=model, + decoding_graph=decoding_graph, + batch=batch, + ) + + futures.append( + executor.submit(_save_worker, cuts, hyps, timestamps, scores) + ) + + num_cuts += len(cuts) + if batch_idx % log_interval == 0: + logging.info(f"cuts processed until now is {num_cuts}") + + for f in futures: + f.result() + + +@torch.no_grad() +def run(rank, world_size, args, in_cuts): + """ + Args: + rank: + It is a value between 0 and `world_size-1`. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + setup_logger(f"{params.log_dir}/log-decode") + logging.info("Decoding started") + + assert params.decoding_method in ( + "greedy_search", + "fast_beam_search", + "modified_beam_search", + ), params.decoding_method + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"device: {device}") + + logging.info("Loading jit model") + model = torch.jit.load(params.nn_model_filename) + model.to(device) + model.eval() + + if params.decoding_method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + else: + decoding_graph = None + + # we will store new cuts with recognition results. + args.return_cuts = True + asr_data_module = AsrDataModule(args) + + if world_size > 1: + in_cuts = in_cuts[rank] + out_cuts_filename = params.manifest_out_dir / ( + f"{params.cuts_filename}_job_{rank}" + params.suffix + ) + else: + out_cuts_filename = params.manifest_out_dir / ( + f"{params.cuts_filename}" + params.suffix + ) + + dl = asr_data_module.dataloaders(in_cuts) + + cuts_writer = CutSet.open_writer(out_cuts_filename, overwrite=True) + decode_dataset( + dl=dl, + params=params, + model=model, + sp=sp, + decoding_graph=decoding_graph, + cuts_writer=cuts_writer, + ) + cuts_writer.close() + logging.info(f"Cuts saved to {out_cuts_filename}") + + logging.info("Done!") + + +def main(): + parser = get_parser() + AsrDataModule.add_arguments(parser) + args = parser.parse_args() + + subset = args.subset + assert subset in ["small", "medium", "large"], subset + + manifest_out_dir = args.manifest_out_dir + manifest_out_dir.mkdir(parents=True, exist_ok=True) + + args.suffix = ".jsonl.gz" + args.cuts_filename = f"librilight_cuts_{args.subset}" + + out_cuts_filename = manifest_out_dir / (args.cuts_filename + args.suffix) + if out_cuts_filename.is_file(): + logging.info(f"{out_cuts_filename} already exists - skipping.") + return + + in_cuts_filename = args.manifest_in_dir / (args.cuts_filename + args.suffix) + in_cuts = load_manifest_lazy(in_cuts_filename) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + chunk_size = (len(in_cuts) + (world_size - 1)) // world_size + # Each manifest is saved at: ``{output_dir}/{prefix}.{split_idx}.jsonl.gz`` + splits = in_cuts.split_lazy( + output_dir=args.manifest_in_dir / "split", + chunk_size=chunk_size, + prefix=args.cuts_filename, + ) + assert len(splits) == world_size, (len(splits), world_size) + mp.spawn(run, args=(world_size, args, splits), nprocs=world_size, join=True) + else: + run(rank=0, world_size=world_size, args=args, in_cuts=in_cuts) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/long_file_recog/split_into_chunks.py b/egs/librispeech/ASR/long_file_recog/split_into_chunks.py new file mode 100755 index 000000000..4a900831c --- /dev/null +++ b/egs/librispeech/ASR/long_file_recog/split_into_chunks.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script splits long utterances into chunks with overlaps. +Each chunk (except the first and the last) is padded with extra left side and right side. +The chunk length is: left_side + chunk_size + right_side. +""" + +import argparse +import logging +from pathlib import Path + +from lhotse import CutSet, load_manifest + + +def get_args(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--manifest-in-dir", + type=Path, + default=Path("data/librilight/manifests"), + help="Path to directory of full utterances.", + ) + + parser.add_argument( + "--manifest-out-dir", + type=Path, + default=Path("data/librilight/manifests_chunk"), + help="Path to directory to save splitted chunks.", + ) + + parser.add_argument( + "--chunk", + type=float, + default=300.0, + help="""Duration (in seconds) of each chunk.""", + ) + + parser.add_argument( + "--extra", + type=float, + default=2.0, + help="""Extra duration (in seconds) at both sides.""", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + logging.info(vars(args)) + + manifest_out_dir = args.manifest_out_dir + manifest_out_dir.mkdir(parents=True, exist_ok=True) + + subsets = ["small", "medium", "large"] + + for subset in subsets: + logging.info(f"Processing {subset} subset") + + manifest_out = manifest_out_dir / f"librilight_cuts_{subset}.jsonl.gz" + if manifest_out.is_file(): + logging.info(f"{manifest_out} already exists - skipping.") + continue + + manifest_in = args.manifest_in_dir / f"librilight_recordings_{subset}.jsonl.gz" + recordings = load_manifest(manifest_in) + + cuts = CutSet.from_manifests(recordings=recordings) + cuts = cuts.cut_into_windows( + duration=args.chunk, hop=args.chunk - args.extra * 2 + ) + cuts = cuts.fill_supervisions() + + cuts.to_file(manifest_out) + logging.info(f"Cuts saved to {manifest_out}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index 0280193ca..f5f15808d 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -670,6 +670,8 @@ def greedy_search_batch( # timestamp[n][i] is the frame index after subsampling # on which hyp[n][i] is decoded timestamps = [[] for _ in range(N)] + # scores[n][i] is the logits on which hyp[n][i] is decoded + scores = [[] for _ in range(N)] decoder_input = torch.tensor( hyps, @@ -707,6 +709,7 @@ def greedy_search_batch( if v not in (blank_id, unk_id): hyps[i].append(v) timestamps[i].append(t) + scores[i].append(logits[i, v].item()) emitted = True if emitted: # update decoder output @@ -722,10 +725,12 @@ def greedy_search_batch( sorted_ans = [h[context_size:] for h in hyps] ans = [] ans_timestamps = [] + ans_scores = [] unsorted_indices = packed_encoder_out.unsorted_indices.tolist() for i in range(N): ans.append(sorted_ans[unsorted_indices[i]]) ans_timestamps.append(timestamps[unsorted_indices[i]]) + ans_scores.append(scores[unsorted_indices[i]]) if not return_timestamps: return ans @@ -733,6 +738,7 @@ def greedy_search_batch( return DecodingResults( hyps=ans, timestamps=ans_timestamps, + scores=ans_scores, ) diff --git a/icefall/utils.py b/icefall/utils.py index eba95ee11..fb350a73f 100644 --- a/icefall/utils.py +++ b/icefall/utils.py @@ -272,6 +272,9 @@ class DecodingResults: # for the i-th utterance with fast_beam_search_nbest_LG. hyps: Union[List[List[int]], k2.RaggedTensor] + # scores[i][k] contains the log-prob of tokens[i][k] + scores: Optional[List[List[float]]] = None + def get_texts_with_timestamp( best_paths: k2.Fsa, return_ragged: bool = False @@ -1442,7 +1445,7 @@ def convert_timestamp( frame_shift = frame_shift_ms / 1000.0 time = [] for f in frames: - time.append(f * subsampling_factor * frame_shift) + time.append(round(f * subsampling_factor * frame_shift, ndigits=3)) return time From 30fcd16c7d98cd6738902eb63b5dbd2d104dc60d Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Sat, 20 May 2023 23:12:11 +0800 Subject: [PATCH 007/100] rm zipformer/__init__.py (#1075) --- egs/librispeech/ASR/zipformer/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 egs/librispeech/ASR/zipformer/__init__.py diff --git a/egs/librispeech/ASR/zipformer/__init__.py b/egs/librispeech/ASR/zipformer/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 8070258ec54d246516f7cd459b623dbbb7849866 Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Sun, 21 May 2023 20:31:54 +0800 Subject: [PATCH 008/100] fix conv_emformer2, when using right_context_length=0 (#1076) --- .../ASR/conv_emformer_transducer_stateless2/emformer.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/emformer.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/emformer.py index 3cedf99b6..d9ef5a2da 100644 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/emformer.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/emformer.py @@ -1358,12 +1358,7 @@ class EmformerEncoder(nn.Module): output_lengths = torch.clamp(lengths - self.right_context_length, min=0) attention_mask = self._gen_attention_mask(utterance) - M = ( - right_context.size(0) // self.right_context_length - 1 - if self.use_memory - else 0 - ) - padding_mask = make_pad_mask(M + right_context.size(0) + output_lengths) + padding_mask = make_pad_mask(attention_mask.shape[1] - U + output_lengths) output = utterance for layer in self.emformer_layers: From 3883e362ad3e66c50ed04f904cd401f2d5c006f2 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 22 May 2023 12:29:51 +0800 Subject: [PATCH 009/100] Fix yesno CI test (#1077) --- .github/workflows/run-yesno-recipe.yml | 2 ++ requirements-ci.txt | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-yesno-recipe.yml b/.github/workflows/run-yesno-recipe.yml index 83a1d5462..9bc7b8299 100644 --- a/.github/workflows/run-yesno-recipe.yml +++ b/.github/workflows/run-yesno-recipe.yml @@ -69,6 +69,8 @@ jobs: pip uninstall -y protobuf pip install --no-binary protobuf protobuf==3.20.* + pip install --force-reinstall https://huggingface.co/csukuangfj/k2/resolve/main/cpu/k2-1.24.3.dev20230508+cpu.torch1.13.1-cp38-cp38-linux_x86_64.whl + - name: Run yesno recipe shell: bash working-directory: ${{github.workspace}} diff --git a/requirements-ci.txt b/requirements-ci.txt index 0c9705a58..3c2eb5f65 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -8,10 +8,10 @@ numpy==1.19 pytest==7.1.0 graphviz==0.19.1 --f https://download.pytorch.org/whl/cpu/torch_stable.html torch==1.10.0+cpu --f https://download.pytorch.org/whl/cpu/torch_stable.html torchaudio==0.10.0+cpu +-f https://download.pytorch.org/whl/cpu/torch_stable.html torch==1.13.1+cpu +-f https://download.pytorch.org/whl/cpu/torch_stable.html torchaudio==0.13.1+cpu --f https://k2-fsa.org/nightly/ k2==1.23.4.dev20230316+cpu.torch1.10.0 +-f https://k2-fsa.org/nightly/ k2==1.23.4.dev20230319+cpu.torch1.13.1 git+https://github.com/lhotse-speech/lhotse kaldilm==1.11 From 90c392b7b37c65cb7e8be59dda1b0a932ee0a81d Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Mon, 22 May 2023 12:39:51 +0800 Subject: [PATCH 010/100] Add docs for Fine-tune with mux (#1074) * Update RESULTS.md --- egs/librispeech/ASR/RESULTS.md | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/egs/librispeech/ASR/RESULTS.md b/egs/librispeech/ASR/RESULTS.md index ed456a617..28361afdd 100644 --- a/egs/librispeech/ASR/RESULTS.md +++ b/egs/librispeech/ASR/RESULTS.md @@ -244,6 +244,87 @@ for m in greedy_search modified_beam_search fast_beam_search; do done ``` +### pruned_transducer_stateless7 (Fine-tune with mux) + +See for more details. + +[pruned_transducer_stateless7](./pruned_transducer_stateless7) + +The tensorboard log can be found at + + +You can find the pretrained model and bpe model needed for fine-tuning at: + + +You can find a fine-tuned model, fine-tuning logs, decoding logs, and decoding +results at: + + +You can use to deploy it. + +Number of model parameters: 70369391, i.e., 70.37 M + +| decoding method | dev | test | test-clean | test-other | comment | +|----------------------|------------|------------|------------|------------|--------------------| +| greedy_search | 14.27 | 14.22 | 2.08 | 4.79 | --epoch 20 --avg 5 | +| modified_beam_search | 14.22 | 14.08 | 2.06 | 4.72 | --epoch 20 --avg 5 | +| fast_beam_search | 14.23 | 14.17 | 2.08 | 4.09 | --epoch 20 --avg 5 | + +The training commands are: +```bash +export CUDA_VISIBLE_DEVICES="0,1" + +./pruned_transducer_stateless7/finetune.py \ + --world-size 2 \ + --num-epochs 20 \ + --start-epoch 1 \ + --exp-dir pruned_transducer_stateless7/exp_giga_finetune \ + --subset S \ + --use-fp16 1 \ + --base-lr 0.005 \ + --lr-epochs 100 \ + --lr-batches 100000 \ + --bpe-model icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11/data/lang_bpe_500/bpe.model \ + --do-finetune True \ + --use-mux True \ + --finetune-ckpt icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11/exp/pretrain.pt \ + --max-duration 500 +``` + +The decoding commands are: +```bash +# greedy_search +./pruned_transducer_stateless7/decode.py \ + --epoch 20 \ + --avg 5 \ + --use-averaged-model 1 \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ + --max-duration 600 \ + --decoding-method greedy_search + +# modified_beam_search +./pruned_transducer_stateless7/decode.py \ + --epoch 20 \ + --avg 5 \ + --use-averaged-model 1 \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +# fast_beam_search +./pruned_transducer_stateless7/decode.py \ + --epoch 20 \ + --avg 5 \ + --use-averaged-model 1 \ + --exp-dir ./pruned_transducer_stateless7/exp_giga_finetune \ + --max-duration 600 \ + --decoding-method fast_beam_search \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 +``` + ### pruned_transducer_stateless7 (zipformer + multidataset(LibriSpeech + GigaSpeech + CommonVoice 13.0)) See for more details. From 7c4ff66a3dc840267c5c475dfe90268b8513268f Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Mon, 22 May 2023 12:46:43 +0800 Subject: [PATCH 011/100] Fix yesno Cl test (#1078) --- .github/workflows/run-aishell-2022-06-20.yml | 2 +- .github/workflows/run-gigaspeech-2022-05-13.yml | 2 +- .github/workflows/run-librispeech-2022-03-12.yml | 2 +- .github/workflows/run-librispeech-2022-04-29.yml | 2 +- .github/workflows/run-librispeech-2022-05-13.yml | 2 +- .github/workflows/run-librispeech-2022-11-11-stateless7.yml | 2 +- .github/workflows/run-librispeech-2022-11-14-stateless8.yml | 2 +- .github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml | 2 +- .github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml | 2 +- .../workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml | 2 +- .../run-librispeech-2022-12-29-stateless7-streaming.yml | 2 +- .github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml | 2 +- .../run-librispeech-lstm-transducer-stateless2-2022-09-03.yml | 2 +- .../run-librispeech-pruned-transducer-stateless3-2022-05-13.yml | 2 +- ...n-librispeech-streaming-transducer-stateless2-2022-06-26.yml | 2 +- .../run-librispeech-streaming-zipformer-2023-05-18.yml | 2 +- .../run-librispeech-transducer-stateless2-2022-04-19.yml | 2 +- .github/workflows/run-librispeech-zipformer-2023-05-18.yml | 2 +- .github/workflows/run-pretrained-conformer-ctc.yml | 2 +- .../run-pretrained-transducer-stateless-librispeech-100h.yml | 2 +- ...etrained-transducer-stateless-librispeech-multi-datasets.yml | 2 +- .../run-pretrained-transducer-stateless-modified-2-aishell.yml | 2 +- .../run-pretrained-transducer-stateless-modified-aishell.yml | 2 +- .github/workflows/run-pretrained-transducer-stateless.yml | 2 +- .github/workflows/run-pretrained-transducer.yml | 2 +- .../workflows/run-wenetspeech-pruned-transducer-stateless2.yml | 2 +- .github/workflows/run-yesno-recipe.yml | 2 +- .github/workflows/test-ncnn-export.yml | 2 +- .github/workflows/test-onnx-export.yml | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/.github/workflows/run-aishell-2022-06-20.yml b/.github/workflows/run-aishell-2022-06-20.yml index f5ba73195..c46cea0f6 100644 --- a/.github/workflows/run-aishell-2022-06-20.yml +++ b/.github/workflows/run-aishell-2022-06-20.yml @@ -73,7 +73,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-gigaspeech-2022-05-13.yml b/.github/workflows/run-gigaspeech-2022-05-13.yml index c7b9cc79d..f8ee25cc4 100644 --- a/.github/workflows/run-gigaspeech-2022-05-13.yml +++ b/.github/workflows/run-gigaspeech-2022-05-13.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-03-12.yml b/.github/workflows/run-librispeech-2022-03-12.yml index 9c7cd1228..d42202b79 100644 --- a/.github/workflows/run-librispeech-2022-03-12.yml +++ b/.github/workflows/run-librispeech-2022-03-12.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-04-29.yml b/.github/workflows/run-librispeech-2022-04-29.yml index 78c9e759f..f42c8f27a 100644 --- a/.github/workflows/run-librispeech-2022-04-29.yml +++ b/.github/workflows/run-librispeech-2022-04-29.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-05-13.yml b/.github/workflows/run-librispeech-2022-05-13.yml index 04799bf52..1fbd96157 100644 --- a/.github/workflows/run-librispeech-2022-05-13.yml +++ b/.github/workflows/run-librispeech-2022-05-13.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-11-11-stateless7.yml b/.github/workflows/run-librispeech-2022-11-11-stateless7.yml index 6dfc23920..596596bd9 100644 --- a/.github/workflows/run-librispeech-2022-11-11-stateless7.yml +++ b/.github/workflows/run-librispeech-2022-11-11-stateless7.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-11-14-stateless8.yml b/.github/workflows/run-librispeech-2022-11-14-stateless8.yml index 0544e68b3..dca7d6d25 100644 --- a/.github/workflows/run-librispeech-2022-11-14-stateless8.yml +++ b/.github/workflows/run-librispeech-2022-11-14-stateless8.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml b/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml index 62e1f2a01..cd41e988e 100644 --- a/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml +++ b/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml @@ -68,7 +68,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml b/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml index 7dc33aaa9..91242c401 100644 --- a/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml +++ b/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml b/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml index de55847ad..e0130a636 100644 --- a/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml +++ b/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml @@ -68,7 +68,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml b/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml index feb5c6fd0..8490a62fc 100644 --- a/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml +++ b/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml b/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml index c95ed8b9a..40a37da57 100644 --- a/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml +++ b/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml b/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml index e14d4e92f..aba29d066 100644 --- a/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml +++ b/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml @@ -55,7 +55,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml b/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml index 73d91fcd4..fd497601d 100644 --- a/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml +++ b/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml b/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml index 8a690393e..57fe5b999 100644 --- a/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml +++ b/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml b/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml index fa0bb3971..ed934d56d 100644 --- a/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml +++ b/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml b/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml index 217dbdfa1..515122a66 100644 --- a/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml +++ b/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-librispeech-zipformer-2023-05-18.yml b/.github/workflows/run-librispeech-zipformer-2023-05-18.yml index febb55026..7ecf0d2a0 100644 --- a/.github/workflows/run-librispeech-zipformer-2023-05-18.yml +++ b/.github/workflows/run-librispeech-zipformer-2023-05-18.yml @@ -72,7 +72,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-pretrained-conformer-ctc.yml b/.github/workflows/run-pretrained-conformer-ctc.yml index 4e8e7b8db..8aaea35f6 100644 --- a/.github/workflows/run-pretrained-conformer-ctc.yml +++ b/.github/workflows/run-pretrained-conformer-ctc.yml @@ -62,7 +62,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml b/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml index ddde4f1d6..03a1df48e 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml @@ -71,7 +71,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml b/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml index 00ea97b2a..8da4ff56a 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml @@ -71,7 +71,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml b/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml index b3cfc9efd..0b3e70d77 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml @@ -62,7 +62,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml b/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml index ab598541d..a6a59d339 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml @@ -62,7 +62,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-pretrained-transducer-stateless.yml b/.github/workflows/run-pretrained-transducer-stateless.yml index d663d49dd..98d84bf96 100644 --- a/.github/workflows/run-pretrained-transducer-stateless.yml +++ b/.github/workflows/run-pretrained-transducer-stateless.yml @@ -71,7 +71,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-pretrained-transducer.yml b/.github/workflows/run-pretrained-transducer.yml index 9cb9d3b59..8c1a652e0 100644 --- a/.github/workflows/run-pretrained-transducer.yml +++ b/.github/workflows/run-pretrained-transducer.yml @@ -62,7 +62,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml b/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml index 14fb96ec8..6c70c646b 100644 --- a/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml +++ b/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml @@ -62,7 +62,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/run-yesno-recipe.yml b/.github/workflows/run-yesno-recipe.yml index 9bc7b8299..f997e634a 100644 --- a/.github/workflows/run-yesno-recipe.yml +++ b/.github/workflows/run-yesno-recipe.yml @@ -69,7 +69,7 @@ jobs: pip uninstall -y protobuf pip install --no-binary protobuf protobuf==3.20.* - pip install --force-reinstall https://huggingface.co/csukuangfj/k2/resolve/main/cpu/k2-1.24.3.dev20230508+cpu.torch1.13.1-cp38-cp38-linux_x86_64.whl + pip install --no-deps --force-reinstall https://huggingface.co/csukuangfj/k2/resolve/main/cpu/k2-1.24.3.dev20230508+cpu.torch1.13.1-cp38-cp38-linux_x86_64.whl - name: Run yesno recipe shell: bash diff --git a/.github/workflows/test-ncnn-export.yml b/.github/workflows/test-ncnn-export.yml index cdea54854..5709f8ebb 100644 --- a/.github/workflows/test-ncnn-export.yml +++ b/.github/workflows/test-ncnn-export.yml @@ -54,7 +54,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' diff --git a/.github/workflows/test-onnx-export.yml b/.github/workflows/test-onnx-export.yml index 3dc4261ab..c05cde3ba 100644 --- a/.github/workflows/test-onnx-export.yml +++ b/.github/workflows/test-onnx-export.yml @@ -54,7 +54,7 @@ jobs: with: path: | ~/tmp/kaldifeat - key: cache-tmp-${{ matrix.python-version }}-2022-09-25 + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 - name: Install kaldifeat if: steps.my-cache.outputs.cache-hit != 'true' From 585e7b224fddeae5a45bef3fc1cf8fb30dcef503 Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Tue, 23 May 2023 11:04:33 +0800 Subject: [PATCH 012/100] Aishell pruned_transducer_stateless7 (#962) * Add pruned_transducer_stateless7 for Aishell * update README.md * update comments and small fixes --- egs/aishell/ASR/README.md | 1 + egs/aishell/ASR/RESULTS.md | 52 + .../pruned_transducer_stateless7/aishell.py | 1 + .../asr_datamodule.py | 1 + .../beam_search.py | 1 + .../pruned_transducer_stateless7/decode.py | 623 ++++++++ .../pruned_transducer_stateless7/decoder.py | 1 + .../pruned_transducer_stateless7/decoder2.py | 87 ++ .../encoder_interface.py | 1 + .../export-onnx.py | 589 ++++++++ .../pruned_transducer_stateless7/export.py | 321 +++++ .../jit_pretrained.py | 278 ++++ .../pruned_transducer_stateless7/joiner.py | 1 + .../ASR/pruned_transducer_stateless7/model.py | 1 + .../onnx_check.py | 1 + .../onnx_pretrained.py | 419 ++++++ .../ASR/pruned_transducer_stateless7/optim.py | 1 + .../pretrained.py | 348 +++++ .../pruned_transducer_stateless7/scaling.py | 1 + .../scaling_converter.py | 1 + .../ASR/pruned_transducer_stateless7/train.py | 1266 +++++++++++++++++ .../pruned_transducer_stateless7/train2.py | 1266 +++++++++++++++++ .../pruned_transducer_stateless7/zipformer.py | 1 + .../asr_datamodule.py | 7 +- 24 files changed, 5268 insertions(+), 1 deletion(-) create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/aishell.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/asr_datamodule.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/beam_search.py create mode 100755 egs/aishell/ASR/pruned_transducer_stateless7/decode.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/decoder.py create mode 100644 egs/aishell/ASR/pruned_transducer_stateless7/decoder2.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/encoder_interface.py create mode 100755 egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py create mode 100755 egs/aishell/ASR/pruned_transducer_stateless7/export.py create mode 100755 egs/aishell/ASR/pruned_transducer_stateless7/jit_pretrained.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/joiner.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/model.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/onnx_check.py create mode 100755 egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/optim.py create mode 100644 egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/scaling.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/scaling_converter.py create mode 100755 egs/aishell/ASR/pruned_transducer_stateless7/train.py create mode 100755 egs/aishell/ASR/pruned_transducer_stateless7/train2.py create mode 120000 egs/aishell/ASR/pruned_transducer_stateless7/zipformer.py diff --git a/egs/aishell/ASR/README.md b/egs/aishell/ASR/README.md index f4a59e552..b9064cede 100644 --- a/egs/aishell/ASR/README.md +++ b/egs/aishell/ASR/README.md @@ -17,6 +17,7 @@ The following table lists the differences among them. | `transducer_stateless_modified` | Conformer | Embedding + Conv1d | with modified transducer from `optimized_transducer` | | `transducer_stateless_modified-2` | Conformer | Embedding + Conv1d | with modified transducer from `optimized_transducer` + extra data | | `pruned_transducer_stateless3` | Conformer (reworked) | Embedding + Conv1d | pruned RNN-T + reworked model with random combiner + using aidatatang_20zh as extra data| +| `pruned_transducer_stateless7` | Zipformer | Embedding | pruned RNN-T + zipformer encoder + stateless decoder with context-size 1 | The decoder in `transducer_stateless` is modified from the paper [Rnn-Transducer with Stateless Prediction Network](https://ieeexplore.ieee.org/document/9054419/). diff --git a/egs/aishell/ASR/RESULTS.md b/egs/aishell/ASR/RESULTS.md index aa18502c2..5088497a1 100644 --- a/egs/aishell/ASR/RESULTS.md +++ b/egs/aishell/ASR/RESULTS.md @@ -2,6 +2,58 @@ ### Aishell training result(Stateless Transducer) +#### Pruned transducer stateless 7 + +[./pruned_transducer_stateless7](./pruned_transducer_stateless7) + +It's Zipformer with Pruned RNNT loss. + +| | test | dev | comment | +|------------------------|------|------|---------------------------------------| +| greedy search | 5.02 | 4.61 | --epoch 42 --avg 6 --max-duration 600 | +| modified beam search | 4.81 | 4.4 | --epoch 42 --avg 6 --max-duration 600 | +| fast beam search | 4.91 | 4.52 | --epoch 42 --avg 6 --max-duration 600 | + +Training command is: + +```bash +./prepare.sh + +export CUDA_VISIBLE_DEVICES="0,1" + +./pruned_transducer_stateless7/train.py \ + --world-size 2 \ + --num-epochs 50 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir pruned_transducer_stateless7/exp \ + --context-size 1 \ + --max-duration 300 +``` + +**Caution**: It uses `--context-size=1`. + +The tensorboard log is available at + + +The decoding command is: +```bash +for m in greedy_search modified_beam_search fast_beam_search ; do + ./pruned_transducer_stateless7/decode.py \ + --epoch 42 \ + --avg 6 \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --lang-dir data/lang_char \ + --max-duration 300 \ + --context-size 1 \ + --decoding-method $m + +done +``` + +Pretrained models, training logs, decoding logs, and decoding results +are available at + #### Pruned transducer stateless 7 (zipformer) See diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/aishell.py b/egs/aishell/ASR/pruned_transducer_stateless7/aishell.py new file mode 120000 index 000000000..ce581b950 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/aishell.py @@ -0,0 +1 @@ +../pruned_transducer_stateless3/aishell.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/asr_datamodule.py b/egs/aishell/ASR/pruned_transducer_stateless7/asr_datamodule.py new file mode 120000 index 000000000..ae3bdd1e0 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/asr_datamodule.py @@ -0,0 +1 @@ +../transducer_stateless_modified-2/asr_datamodule.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/beam_search.py b/egs/aishell/ASR/pruned_transducer_stateless7/beam_search.py new file mode 120000 index 000000000..e9bbcf2a9 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/beam_search.py @@ -0,0 +1 @@ +../pruned_transducer_stateless3/beam_search.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/decode.py b/egs/aishell/ASR/pruned_transducer_stateless7/decode.py new file mode 100755 index 000000000..af54af8da --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/decode.py @@ -0,0 +1,623 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2022 Xiaomi Corporation (Author: Xiaoyu Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./pruned_transducer_stateless7/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) beam search (not recommended) +./pruned_transducer_stateless7/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --max-duration 600 \ + --decoding-method beam_search \ + --beam-size 4 + +(3) modified beam search +./pruned_transducer_stateless7/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +(4) fast beam search +./pruned_transducer_stateless7/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search \ + --beam 4 \ + --max-contexts 4 \ + --max-states 8 +""" + + +import argparse +import logging +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import torch +import torch.nn as nn +from aishell import AIShell +from asr_datamodule import AsrDataModule +from beam_search import ( + beam_search, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=False, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless3/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="The lang dir", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + - fast_beam_search + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=4, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search""", + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=4, + help="""Used only when --decoding-method is + fast_beam_search""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=8, + help="""Used only when --decoding-method is + fast_beam_search""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=1, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + token_table: k2.SymbolTable, + batch: dict, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + token_table: + It maps token ID to a string. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + encoder_out, encoder_out_lens = model.encoder(x=feature, x_lens=feature_lens) + + if params.decoding_method == "fast_beam_search": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + elif params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + else: + hyp_tokens = [] + batch_size = encoder_out.size(0) + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyp_tokens.append(hyp) + + hyps = [[token_table[t] for t in tokens] for tokens in hyp_tokens] + + if params.decoding_method == "greedy_search": + return {"greedy_search": hyps} + elif params.decoding_method == "fast_beam_search": + return { + ( + f"beam_{params.beam}_" + f"max_contexts_{params.max_contexts}_" + f"max_states_{params.max_states}" + ): hyps + } + else: + return {f"beam_size_{params.beam_size}": hyps} + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + token_table: k2.SymbolTable, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + token_table: + It maps a token ID to a string. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + token_table=token_table, + decoding_graph=decoding_graph, + batch=batch, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = ref_text.split() + this_batch.append((cut_id, ref_words, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = params.res_dir / f"recogs-{test_set_name}-{params.suffix}.txt" + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = params.res_dir / f"errs-{test_set_name}-{params.suffix}.txt" + # we compute CER for aishell dataset. + results_char = [] + for res in results: + results_char.append((res[0], list("".join(res[1])), list("".join(res[2])))) + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results_char, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = params.res_dir / f"wer-summary-{test_set_name}-{params.suffix}.txt" + with open(errs_info, "w") as f: + print("settings\tCER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, CER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + AsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + args.lang_dir = Path(args.lang_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "fast_beam_search", + "modified_beam_search", + ) + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if "fast_beam_search" in params.decoding_method: + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + elif "beam_search" in params.decoding_method: + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict( + average_checkpoints(filenames, device=device), strict=False + ) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict( + average_checkpoints(filenames, device=device), strict=False + ) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ), + strict=False, + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ), + strict=False, + ) + + model.to(device) + model.eval() + + if params.decoding_method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + else: + decoding_graph = None + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + asr_datamodule = AsrDataModule(args) + aishell = AIShell(manifest_dir=args.manifest_dir) + test_cuts = aishell.test_cuts() + dev_cuts = aishell.valid_cuts() + test_dl = asr_datamodule.test_dataloaders(test_cuts) + dev_dl = asr_datamodule.test_dataloaders(dev_cuts) + + test_sets = ["test", "dev"] + test_dls = [test_dl, dev_dl] + + for test_set, test_dl in zip(test_sets, test_dls): + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + token_table=lexicon.token_table, + decoding_graph=decoding_graph, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/decoder.py b/egs/aishell/ASR/pruned_transducer_stateless7/decoder.py new file mode 120000 index 000000000..8283d8c5a --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/decoder.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/decoder.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/decoder2.py b/egs/aishell/ASR/pruned_transducer_stateless7/decoder2.py new file mode 100644 index 000000000..0345db0a8 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/decoder2.py @@ -0,0 +1,87 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 Decoder(nn.Module): + """This class modifies the stateless decoder from the following paper: + + RNN-transducer with stateless prediction network + https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9054419 + + It removes the recurrent connection from the decoder, i.e., the prediction + network. Different from the above paper, it adds an extra Conv1d + right after the embedding layer. + + TODO: Implement https://arxiv.org/pdf/2109.07513.pdf + """ + + def __init__( + self, + vocab_size: int, + decoder_dim: int, + blank_id: int, + context_size: int, + ): + """ + Args: + vocab_size: + Number of tokens of the modeling unit including blank. + decoder_dim: + Dimension of the input embedding, and of the decoder output. + blank_id: + The ID of the blank symbol. + context_size: + Number of previous words to use to predict the next word. + 1 means bigram; 2 means trigram. n means (n+1)-gram. + """ + super().__init__() + + self.embedding = nn.Embedding( + num_embeddings=vocab_size, + embedding_dim=decoder_dim, + ) + self.blank_id = blank_id + + assert context_size == 1, context_size + self.context_size = context_size + self.vocab_size = vocab_size + + def forward(self, y: torch.Tensor, need_pad: bool = True) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, U). + need_pad: + True to left pad the input. Should be True during training. + False to not pad the input. Should be False during inference. + Returns: + Return a tensor of shape (N, U, decoder_dim). + """ + y = y.to(torch.int64) + # this stuff about clamp() is a temporary fix for a mismatch + # at utterance start, we use negative ids in beam_search.py + if torch.jit.is_tracing(): + # This is for exporting to PNNX via ONNX + embedding_out = self.embedding(y) + else: + embedding_out = self.embedding(y.clamp(min=0)) * (y >= 0).unsqueeze(-1) + + embedding_out = F.relu(embedding_out) + return embedding_out diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/encoder_interface.py b/egs/aishell/ASR/pruned_transducer_stateless7/encoder_interface.py new file mode 120000 index 000000000..0c2673d46 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/encoder_interface.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/encoder_interface.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py b/egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py new file mode 100755 index 000000000..e8211500a --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py @@ -0,0 +1,589 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang +# Xiaoyu Yang) + +""" +This script exports a transducer model from PyTorch to ONNX. + +We use the pre-trained model from +https://huggingface.co/marcoyang/icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/marcoyang/icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21/ +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless7/export-onnx.py \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --feedforward-dims "1024,1024,2048,2048,1024" + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +See ./onnx_pretrained.py and ./onnx_check.py for how to +use the exported ONNX models. +""" + +import argparse +import logging +from pathlib import Path +from typing import Dict, Tuple + +import onnx +import sentencepiece as spm +import torch +import torch.nn as nn +from decoder2 import Decoder +from onnxruntime.quantization import QuantType, quantize_dynamic +from scaling_converter import convert_scaled_to_non_scaled +from train2 import add_model_arguments, get_params, get_transducer_model +from zipformer import Zipformer + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import setup_logger, str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for averaging. + Note: Epoch counts from 0. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless5/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=1, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +def add_meta_data(filename: str, meta_data: Dict[str, str]): + """Add meta data to an ONNX model. It is changed in-place. + + Args: + filename: + Filename of the ONNX model to be changed. + meta_data: + Key-value pairs. + """ + model = onnx.load(filename) + for key, value in meta_data.items(): + meta = model.metadata_props.add() + meta.key = key + meta.value = value + + onnx.save(model, filename) + + +class OnnxEncoder(nn.Module): + """A wrapper for Zipformer and the encoder_proj from the joiner""" + + def __init__(self, encoder: Zipformer, encoder_proj: nn.Linear): + """ + Args: + encoder: + A Zipformer encoder. + encoder_proj: + The projection layer for encoder from the joiner. + """ + super().__init__() + self.encoder = encoder + self.encoder_proj = encoder_proj + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Please see the help information of Zipformer.forward + + Args: + x: + A 3-D tensor of shape (N, T, C) + x_lens: + A 1-D tensor of shape (N,). Its dtype is torch.int64 + Returns: + Return a tuple containing: + - encoder_out, A 3-D tensor of shape (N, T', joiner_dim) + - encoder_out_lens, A 1-D tensor of shape (N,) + """ + encoder_out, encoder_out_lens = self.encoder(x, x_lens) + + encoder_out = self.encoder_proj(encoder_out) + # Now encoder_out is of shape (N, T, joiner_dim) + + return encoder_out, encoder_out_lens + + +class OnnxDecoder(nn.Module): + """A wrapper for Decoder and the decoder_proj from the joiner""" + + def __init__(self, decoder: Decoder, decoder_proj: nn.Linear): + super().__init__() + self.decoder = decoder + self.decoder_proj = decoder_proj + + def forward(self, y: torch.Tensor) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, context_size). + Returns + Return a 2-D tensor of shape (N, joiner_dim) + """ + need_pad = False + decoder_output = self.decoder(y, need_pad=need_pad) + decoder_output = decoder_output.squeeze(1) + output = self.decoder_proj(decoder_output) + + return output + + +class OnnxJoiner(nn.Module): + """A wrapper for the joiner""" + + def __init__(self, output_linear: nn.Linear): + super().__init__() + self.output_linear = output_linear + + def forward( + self, + encoder_out: torch.Tensor, + decoder_out: torch.Tensor, + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + logit = encoder_out + decoder_out + logit = self.output_linear(torch.tanh(logit)) + return logit + + +def export_encoder_model_onnx( + encoder_model: OnnxEncoder, + encoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the given encoder model to ONNX format. + The exported model has two inputs: + + - x, a tensor of shape (N, T, C); dtype is torch.float32 + - x_lens, a tensor of shape (N,); dtype is torch.int64 + + and it has two outputs: + + - encoder_out, a tensor of shape (N, T', joiner_dim) + - encoder_out_lens, a tensor of shape (N,) + + Args: + encoder_model: + The input encoder model + encoder_filename: + The filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + x = torch.zeros(1, 100, 80, dtype=torch.float32) + x_lens = torch.tensor([100], dtype=torch.int64) + + torch.onnx.export( + encoder_model, + (x, x_lens), + encoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["x", "x_lens"], + output_names=["encoder_out", "encoder_out_lens"], + dynamic_axes={ + "x": {0: "N", 1: "T"}, + "x_lens": {0: "N"}, + "encoder_out": {0: "N", 1: "T"}, + "encoder_out_lens": {0: "N"}, + }, + ) + + +def export_decoder_model_onnx( + decoder_model: OnnxDecoder, + decoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the decoder model to ONNX format. + + The exported model has one input: + + - y: a torch.int64 tensor of shape (N, decoder_model.context_size) + + and has one output: + + - decoder_out: a torch.float32 tensor of shape (N, joiner_dim) + + Args: + decoder_model: + The decoder model to be exported. + decoder_filename: + Filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + context_size = decoder_model.decoder.context_size + vocab_size = decoder_model.decoder.vocab_size + + y = torch.zeros(10, context_size, dtype=torch.int64) + torch.onnx.export( + decoder_model, + y, + decoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["y"], + output_names=["decoder_out"], + dynamic_axes={ + "y": {0: "N"}, + "decoder_out": {0: "N"}, + }, + ) + + meta_data = { + "context_size": str(context_size), + "vocab_size": str(vocab_size), + } + add_meta_data(filename=decoder_filename, meta_data=meta_data) + + +def export_joiner_model_onnx( + joiner_model: nn.Module, + joiner_filename: str, + opset_version: int = 11, +) -> None: + """Export the joiner model to ONNX format. + The exported joiner model has two inputs: + + - encoder_out: a tensor of shape (N, joiner_dim) + - decoder_out: a tensor of shape (N, joiner_dim) + + and produces one output: + + - logit: a tensor of shape (N, vocab_size) + """ + joiner_dim = joiner_model.output_linear.weight.shape[1] + logging.info(f"joiner dim: {joiner_dim}") + + projected_encoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + projected_decoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + + torch.onnx.export( + joiner_model, + (projected_encoder_out, projected_decoder_out), + joiner_filename, + verbose=False, + opset_version=opset_version, + input_names=[ + "encoder_out", + "decoder_out", + ], + output_names=["logit"], + dynamic_axes={ + "encoder_out": {0: "N"}, + "decoder_out": {0: "N"}, + "logit": {0: "N"}, + }, + ) + meta_data = { + "joiner_dim": str(joiner_dim), + } + add_meta_data(filename=joiner_filename, meta_data=meta_data) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + setup_logger(f"{params.exp_dir}/log-export/log-export-onnx") + + logging.info(f"device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + convert_scaled_to_non_scaled(model, inplace=True) + + encoder = OnnxEncoder( + encoder=model.encoder, + encoder_proj=model.joiner.encoder_proj, + ) + + decoder = OnnxDecoder( + decoder=model.decoder, + decoder_proj=model.joiner.decoder_proj, + ) + + joiner = OnnxJoiner(output_linear=model.joiner.output_linear) + + encoder_num_param = sum([p.numel() for p in encoder.parameters()]) + decoder_num_param = sum([p.numel() for p in decoder.parameters()]) + joiner_num_param = sum([p.numel() for p in joiner.parameters()]) + total_num_param = encoder_num_param + decoder_num_param + joiner_num_param + logging.info(f"encoder parameters: {encoder_num_param}") + logging.info(f"decoder parameters: {decoder_num_param}") + logging.info(f"joiner parameters: {joiner_num_param}") + logging.info(f"total parameters: {total_num_param}") + + if params.iter > 0: + suffix = f"iter-{params.iter}" + else: + suffix = f"epoch-{params.epoch}" + + suffix += f"-avg-{params.avg}" + + opset_version = 13 + + logging.info("Exporting encoder") + encoder_filename = params.exp_dir / f"encoder-{suffix}.onnx" + export_encoder_model_onnx( + encoder, + encoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported encoder to {encoder_filename}") + + logging.info("Exporting decoder") + decoder_filename = params.exp_dir / f"decoder-{suffix}.onnx" + export_decoder_model_onnx( + decoder, + decoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported decoder to {decoder_filename}") + + logging.info("Exporting joiner") + joiner_filename = params.exp_dir / f"joiner-{suffix}.onnx" + export_joiner_model_onnx( + joiner, + joiner_filename, + opset_version=opset_version, + ) + logging.info(f"Exported joiner to {joiner_filename}") + + # Generate int8 quantization models + # See https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html#data-type-selection + + logging.info("Generate int8 quantization models") + + encoder_filename_int8 = params.exp_dir / f"encoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=encoder_filename, + model_output=encoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + decoder_filename_int8 = params.exp_dir / f"decoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=decoder_filename, + model_output=decoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + joiner_filename_int8 = params.exp_dir / f"joiner-{suffix}.int8.onnx" + quantize_dynamic( + model_input=joiner_filename, + model_output=joiner_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/export.py b/egs/aishell/ASR/pruned_transducer_stateless7/export.py new file mode 100755 index 000000000..1b0e8d3b9 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/export.py @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 +# +# Copyright 2021 Xiaomi Corporation (Author: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script converts several saved checkpoints +# to a single one using model averaging. +""" + +Usage: + +(1) Export to torchscript model using torch.jit.script() + +./pruned_transducer_stateless7/export.py \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --lang-dir data/lang_char \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +It will generate a file `cpu_jit.pt` in the given `exp_dir`. You can later +load it by `torch.jit.load("cpu_jit.pt")`. + +Note `cpu` in the name `cpu_jit.pt` means the parameters when loaded into Python +are on CPU. You can use `to("cuda")` to move them to a CUDA device. + +Check +https://github.com/k2-fsa/sherpa +for how to use the exported models outside of icefall. + +(2) Export `model.state_dict()` + +./pruned_transducer_stateless7/export.py \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --lang-dir data/lang_char \ + --epoch 20 \ + --avg 10 + +It will generate a file `pretrained.pt` in the given `exp_dir`. You can later +load it by `icefall.checkpoint.load_checkpoint()`. + +To use the generated file with `pruned_transducer_stateless7/decode.py`, +you can do: + + cd /path/to/exp_dir + ln -s pretrained.pt epoch-9999.pt + + cd /path/to/egs/librispeech/ASR + ./pruned_transducer_stateless7/decode.py \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --decoding-method greedy_search \ + --lang-dir data/lang_char + +Check ./pretrained.py for its usage. + +Note: If you don't want to train a model from scratch, we have +provided one for you. You can get it at + +https://huggingface.co/marcoyang/icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21 + +with the following commands: + + sudo apt-get install git-lfs + git lfs install + git clone https://huggingface.co/marcoyang/icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21 + # You will find the pre-trained model in icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21exp +""" + +import argparse +import logging +from pathlib import Path + +import sentencepiece as spm +import torch +import torch.nn as nn +from scaling_converter import convert_scaled_to_non_scaled +from train2 import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless7/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "--jit", + type=str2bool, + default=False, + help="""True to save a model after applying torch.jit.script. + It will generate a file named cpu_jit.pt + + Check ./jit_pretrained.py for how to use it. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=1, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + if params.jit is True: + convert_scaled_to_non_scaled(model, inplace=True) + # We won't use the forward() method of the model in C++, so just ignore + # it here. + # Otherwise, one of its arguments is a ragged tensor and is not + # torch scriptabe. + model.__class__.forward = torch.jit.ignore(model.__class__.forward) + logging.info("Using torch.jit.script") + model = torch.jit.script(model) + filename = params.exp_dir / "cpu_jit.pt" + model.save(str(filename)) + logging.info(f"Saved to {filename}") + else: + logging.info("Not using torchscript. Export model.state_dict()") + # Save it using a format so that it can be loaded + # by :func:`load_checkpoint` + filename = params.exp_dir / "pretrained.pt" + torch.save({"model": model.state_dict()}, str(filename)) + logging.info(f"Saved to {filename}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/jit_pretrained.py b/egs/aishell/ASR/pruned_transducer_stateless7/jit_pretrained.py new file mode 100755 index 000000000..e61190649 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/jit_pretrained.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads torchscript models, exported by `torch.jit.script()` +and uses them to decode waves. +You can use the following command to get the exported models: + +./pruned_transducer_stateless7/export.py \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --lang-dir ./data/lang_char \ + --epoch 20 \ + --avg 10 \ + --jit 1 + +Usage of this script: + +./pruned_transducer_stateless7/jit_pretrained.py \ + --nn-model-filename ./pruned_transducer_stateless7/exp/cpu_jit.pt \ + --lang-dir ./data/lang_char \ + /path/to/foo.wav \ + /path/to/bar.wav +""" + +import argparse +import logging +import math +from typing import List + +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence + +from icefall.lexicon import Lexicon + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model-filename", + type=str, + required=True, + help="Path to the torchscript model cpu_jit.pt", + ) + + parser.add_argument( + "--lang-dir", + type=str, + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float = 16000 +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +def greedy_search( + model: torch.jit.ScriptModule, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, +) -> List[List[int]]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (N, T, C) + encoder_out_lens: + A 1-D tensor of shape (N,). + Returns: + Return the decoded results for each utterance. + """ + assert encoder_out.ndim == 3 + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + device = encoder_out.device + blank_id = 0 # hard-code to 0 + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + context_size = model.decoder.context_size + hyps = [[blank_id] * context_size for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + device=device, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.decoder( + decoder_input, + need_pad=torch.tensor([False]), + ).squeeze(1) + + offset = 0 + for batch_size in batch_size_list: + start = offset + end = offset + batch_size + current_encoder_out = packed_encoder_out.data[start:end] + current_encoder_out = current_encoder_out + # current_encoder_out's shape: (batch_size, encoder_out_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + + logits = model.joiner( + current_encoder_out, + decoder_out, + ) + # logits'shape (batch_size, vocab_size) + + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v != blank_id: + hyps[i].append(v) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + device=device, + dtype=torch.int64, + ) + decoder_out = model.decoder( + decoder_input, + need_pad=torch.tensor([False]), + ) + decoder_out = decoder_out.squeeze(1) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + model = torch.jit.load(args.nn_model_filename) + + model.eval() + + model.to(device) + + lexicon = Lexicon(args.lang_dir) + token_table = lexicon.token_table + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = 16000 + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence( + features, + batch_first=True, + padding_value=math.log(1e-10), + ) + + feature_lengths = torch.tensor(feature_lengths, device=device) + + encoder_out, encoder_out_lens = model.encoder( + x=features, + x_lens=feature_lengths, + ) + + hyps = greedy_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + hyps = [[token_table[t] for t in tokens] for tokens in hyps] + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/joiner.py b/egs/aishell/ASR/pruned_transducer_stateless7/joiner.py new file mode 120000 index 000000000..0f0c3c90a --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/joiner.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/joiner.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/model.py b/egs/aishell/ASR/pruned_transducer_stateless7/model.py new file mode 120000 index 000000000..0d8bc665b --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/model.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/onnx_check.py b/egs/aishell/ASR/pruned_transducer_stateless7/onnx_check.py new file mode 120000 index 000000000..e97d1c0aa --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/onnx_check.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/onnx_check.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py b/egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py new file mode 100755 index 000000000..5adb6c16a --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads ONNX models and uses them to decode waves. +You can use the following command to get the exported models: + +We use the pre-trained model from +https://huggingface.co/csukuangfj/icefall-asr-librispeech-pruned-transducer-stateless3-2022-05-13 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/csukuangfj/icefall-asr-librispeech-pruned-transducer-stateless3-2022-05-13 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "exp/pretrained-iter-1224000-avg-14.pt" + +cd exp +ln -s pretrained-iter-1224000-avg-14.pt epoch-9999.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless3/export-onnx.py \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --epoch 9999 \ + --avg 1 \ + --exp-dir $repo/exp/ + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-9999-avg-1.onnx + - decoder-epoch-9999-avg-1.onnx + - joiner-epoch-9999-avg-1.onnx + +3. Run this file + +./pruned_transducer_stateless3/onnx_pretrained.py \ + --encoder-model-filename $repo/exp/encoder-epoch-9999-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-9999-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-9999-avg-1.onnx \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav +""" + +import argparse +import logging +import math +from typing import List, Tuple + +import k2 +import kaldifeat +import numpy as np +import onnxruntime as ort +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--encoder-model-filename", + type=str, + required=True, + help="Path to the encoder onnx model. ", + ) + + parser.add_argument( + "--decoder-model-filename", + type=str, + required=True, + help="Path to the decoder onnx model. ", + ) + + parser.add_argument( + "--joiner-model-filename", + type=str, + required=True, + help="Path to the joiner onnx model. ", + ) + + parser.add_argument( + "--tokens", + type=str, + help="""Path to tokens.txt.""", + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + return parser + + +class OnnxModel: + def __init__( + self, + encoder_model_filename: str, + decoder_model_filename: str, + joiner_model_filename: str, + ): + session_opts = ort.SessionOptions() + session_opts.inter_op_num_threads = 1 + session_opts.intra_op_num_threads = 1 + + self.session_opts = session_opts + + self.init_encoder(encoder_model_filename) + self.init_decoder(decoder_model_filename) + self.init_joiner(joiner_model_filename) + + def init_encoder(self, encoder_model_filename: str): + self.encoder = ort.InferenceSession( + encoder_model_filename, + sess_options=self.session_opts, + ) + + def init_decoder(self, decoder_model_filename: str): + self.decoder = ort.InferenceSession( + decoder_model_filename, + sess_options=self.session_opts, + ) + + decoder_meta = self.decoder.get_modelmeta().custom_metadata_map + self.context_size = int(decoder_meta["context_size"]) + self.vocab_size = int(decoder_meta["vocab_size"]) + + logging.info(f"context_size: {self.context_size}") + logging.info(f"vocab_size: {self.vocab_size}") + + def init_joiner(self, joiner_model_filename: str): + self.joiner = ort.InferenceSession( + joiner_model_filename, + sess_options=self.session_opts, + ) + + joiner_meta = self.joiner.get_modelmeta().custom_metadata_map + self.joiner_dim = int(joiner_meta["joiner_dim"]) + + logging.info(f"joiner_dim: {self.joiner_dim}") + + def run_encoder( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Args: + x: + A 3-D tensor of shape (N, T, C) + x_lens: + A 2-D tensor of shape (N,). Its dtype is torch.int64 + Returns: + Return a tuple containing: + - encoder_out, its shape is (N, T', joiner_dim) + - encoder_out_lens, its shape is (N,) + """ + out = self.encoder.run( + [ + self.encoder.get_outputs()[0].name, + self.encoder.get_outputs()[1].name, + ], + { + self.encoder.get_inputs()[0].name: x.numpy(), + self.encoder.get_inputs()[1].name: x_lens.numpy(), + }, + ) + return torch.from_numpy(out[0]), torch.from_numpy(out[1]) + + def run_decoder(self, decoder_input: torch.Tensor) -> torch.Tensor: + """ + Args: + decoder_input: + A 2-D tensor of shape (N, context_size) + Returns: + Return a 2-D tensor of shape (N, joiner_dim) + """ + out = self.decoder.run( + [self.decoder.get_outputs()[0].name], + {self.decoder.get_inputs()[0].name: decoder_input.numpy()}, + )[0] + + return torch.from_numpy(out) + + def run_joiner( + self, encoder_out: torch.Tensor, decoder_out: torch.Tensor + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + out = self.joiner.run( + [self.joiner.get_outputs()[0].name], + { + self.joiner.get_inputs()[0].name: encoder_out.numpy(), + self.joiner.get_inputs()[1].name: decoder_out.numpy(), + }, + )[0] + + return torch.from_numpy(out) + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +def greedy_search( + model: OnnxModel, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, +) -> List[List[int]]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (N, T, joiner_dim) + encoder_out_lens: + A 1-D tensor of shape (N,). + Returns: + Return the decoded results for each utterance. + """ + assert encoder_out.ndim == 3, encoder_out.shape + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + blank_id = 0 # hard-code to 0 + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + context_size = model.context_size + hyps = [[blank_id] * context_size for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.run_decoder(decoder_input) + + offset = 0 + for batch_size in batch_size_list: + start = offset + end = offset + batch_size + current_encoder_out = packed_encoder_out.data[start:end] + # current_encoder_out's shape: (batch_size, joiner_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + logits = model.run_joiner(current_encoder_out, decoder_out) + + # logits'shape (batch_size, vocab_size) + + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v != blank_id: + hyps[i].append(v) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + dtype=torch.int64, + ) + decoder_out = model.run_decoder(decoder_input) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + model = OnnxModel( + encoder_model_filename=args.encoder_model_filename, + decoder_model_filename=args.decoder_model_filename, + joiner_model_filename=args.joiner_model_filename, + ) + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = "cpu" + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = args.sample_rate + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, + expected_sample_rate=args.sample_rate, + ) + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence( + features, + batch_first=True, + padding_value=math.log(1e-10), + ) + + feature_lengths = torch.tensor(feature_lengths, dtype=torch.int64) + encoder_out, encoder_out_lens = model.run_encoder(features, feature_lengths) + + hyps = greedy_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + s = "\n" + + symbol_table = k2.SymbolTable.from_file(args.tokens) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += symbol_table[i] + return text.replace("▁", " ").strip() + + context_size = model.context_size + for filename, hyp in zip(args.sound_files, hyps): + words = token_ids_to_words(hyp[context_size:]) + s += f"{filename}:\n{words}\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/optim.py b/egs/aishell/ASR/pruned_transducer_stateless7/optim.py new file mode 120000 index 000000000..8a05abb5f --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/optim.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/optim.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py b/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py new file mode 100644 index 000000000..cc54027d6 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads a checkpoint and uses it to decode waves. +You can generate the checkpoint with the following command: + +./pruned_transducer_stateless7/export.py \ + --exp-dir ./pruned_transducer_stateless7/exp \ + --lang-dir data/lang_char \ + --epoch 20 \ + --avg 10 + +Usage of this script: + +(1) greedy search +./pruned_transducer_stateless7/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ + --lang-dir ./data/lang_char \ + --method greedy_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) beam search +./pruned_transducer_stateless7/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ + --lang-dir ./data/lang_char \ + --method beam_search \ + --beam-size 4 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) modified beam search +./pruned_transducer_stateless7/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ + --lang-dir ./data/lang_char \ + --method modified_beam_search \ + --beam-size 4 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(4) fast beam search +./pruned_transducer_stateless7/pretrained.py \ + --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ + --lang-dir ./data/lang_char \ + --method fast_beam_search \ + --beam-size 4 \ + /path/to/foo.wav \ + /path/to/bar.wav + +You can also use `./pruned_transducer_stateless7/exp/epoch-xx.pt`. + +Note: ./pruned_transducer_stateless7/exp/pretrained.pt is generated by +./pruned_transducer_stateless7/export.py +""" + + +import argparse +import logging +import math +from typing import List + +import k2 +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from beam_search import ( + beam_search, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from torch.nn.utils.rnn import pad_sequence +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.lexicon import Lexicon +from icefall.utils import str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--checkpoint", + type=str, + required=True, + help="Path to the checkpoint. " + "The checkpoint is assumed to be saved by " + "icefall.checkpoint.save_checkpoint().", + ) + + parser.add_argument( + "--lang-dir", + type=str, + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "--method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + - fast_beam_search + """, + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=4, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=4, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=8, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=1, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. Used only when + --method is greedy_search. + """, + ) + + add_model_arguments(parser) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + + params.update(vars(args)) + + lexicon = Lexicon(params.lang_dir) + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + token_table = lexicon.token_table + + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + logging.info("Creating model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + checkpoint = torch.load(args.checkpoint, map_location="cpu") + model.load_state_dict(checkpoint["model"], strict=False) + model.to(device) + model.eval() + model.device = device + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + feature_lengths = torch.tensor(feature_lengths, device=device) + + encoder_out, encoder_out_lens = model.encoder(x=features, x_lens=feature_lengths) + + num_waves = encoder_out.size(0) + hyps = [] + msg = f"Using {params.method}" + if params.method == "beam_search": + msg += f" with beam size {params.beam_size}" + logging.info(msg) + + if params.method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + elif params.method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + elif params.method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + else: + for i in range(num_waves): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.method == "greedy_search": + hyp_tokens = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.method == "beam_search": + hyp_tokens = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError(f"Unsupported method: {params.method}") + + hyps = [[token_table[t] for t in tokens] for tokens in hyp_tokens] + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/scaling.py b/egs/aishell/ASR/pruned_transducer_stateless7/scaling.py new file mode 120000 index 000000000..5f9be9fe0 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/scaling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/scaling.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/scaling_converter.py b/egs/aishell/ASR/pruned_transducer_stateless7/scaling_converter.py new file mode 120000 index 000000000..f9960e5c6 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/scaling_converter.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/scaling_converter.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train.py b/egs/aishell/ASR/pruned_transducer_stateless7/train.py new file mode 100755 index 000000000..a8779f80f --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train.py @@ -0,0 +1,1266 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Xiaoyu Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +./prepare.sh + +If you use --datatang-prob=0, then you don't need to run the above script. + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + + +./pruned_transducer_stateless7/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --exp-dir pruned_transducer_stateless7/exp \ + --full-libri 1 \ + --max-duration 300 + +# For mix precision training: + +./pruned_transducer_stateless7/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir pruned_transducer_stateless7/exp \ + --full-libri 1 \ + --max-duration 550 +""" + + +import argparse +import copy +import logging +import random +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from aishell import AIShell +from asr_datamodule import AsrDataModule +from decoder import Decoder +from joiner import Joiner +from lhotse import CutSet, load_manifest +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from model import Transducer +from optim import Eden, ScaledAdam +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer + +from icefall import diagnostics +from icefall.char_graph_compiler import CharCtcTrainingGraphCompiler +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.hooks import register_inf_check_hooks +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + MetricsTracker, + filter_uneven_sized_batch, + setup_logger, + str2bool, +) + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for module in model.modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,4,3,2,4", + help="Number of zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--feedforward-dims", + type=str, + default="1024,1024,2048,2048,1024", + help="Feedforward dimension of the zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--nhead", + type=str, + default="8,8,8,8,8", + help="Number of attention heads in the zipformer encoder layers.", + ) + + parser.add_argument( + "--encoder-dims", + type=str, + default="384,384,384,384,384", + help="Embedding dimension in the 2 blocks of zipformer encoder layers, comma separated", + ) + + parser.add_argument( + "--attention-dims", + type=str, + default="192,192,192,192,192", + help="""Attention dimension in the 2 blocks of zipformer encoder layers, comma separated; + not the same as embedding dimension.""", + ) + + parser.add_argument( + "--encoder-unmasked-dims", + type=str, + default="256,256,256,256,256", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance " + " worse.", + ) + + parser.add_argument( + "--zipformer-downsampling-factors", + type=str, + default="1,2,4,8,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--cnn-module-kernels", + type=str, + default="31,31,31,31,31", + help="Sizes of kernels in convolution modules", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless3/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "--base-lr", type=float, default=0.05, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=5000, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=6, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=1, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network) part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--inf-check", + type=str2bool, + default=False, + help="Add hooks to check for infinite module outputs and gradients.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=4000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=30, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=200, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warmup period that dictates the decay of the + scale on "simple" (un-pruned) loss. + """ + params = AttributeDict( + { + "frame_shift_ms": 10.0, + "allowed_excess_duration_ratio": 0.1, + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 3000, # For the 100h subset, use 800 + # parameters for zipformer + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed. + "warm_step": 2000, + "env_info": get_env_info(), + } + ) + + return params + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + # TODO: We can add an option to switch between Zipformer and Transformer + def to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + encoder = Zipformer( + num_features=params.feature_dim, + output_downsampling_factor=2, + zipformer_downsampling_factors=to_int_tuple( + params.zipformer_downsampling_factors + ), + encoder_dims=to_int_tuple(params.encoder_dims), + attention_dim=to_int_tuple(params.attention_dims), + encoder_unmasked_dims=to_int_tuple(params.encoder_unmasked_dims), + nhead=to_int_tuple(params.nhead), + feedforward_dim=to_int_tuple(params.feedforward_dims), + cnn_module_kernels=to_int_tuple(params.cnn_module_kernels), + num_encoder_layers=to_int_tuple(params.num_encoder_layers), + ) + return encoder + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_transducer_model(params: AttributeDict) -> nn.Module: + encoder = get_encoder_model(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = Transducer( + encoder=encoder, + decoder=decoder, + joiner=joiner, + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + if "cur_batch_idx" in saved_params: + params["cur_batch_idx"] = saved_params["cur_batch_idx"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + graph_compiler: CharCtcTrainingGraphCompiler, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute RNN-T loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Zipformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + # For the uneven-sized batch, the total duration after padding would possibly + # cause OOM. Hence, for each batch, which is sorted descendingly by length, + # we simply drop the last few shortest samples, so that the retained total frames + # (after padding) would not exceed `allowed_max_frames`: + # `allowed_max_frames = int(max_frames * (1.0 + allowed_excess_duration_ratio))`, + # where `max_frames = max_duration * 1000 // frame_shift_ms`. + # We set allowed_excess_duration_ratio=0.1. + max_frames = params.max_duration * 1000 // params.frame_shift_ms + allowed_max_frames = int(max_frames * (1.0 + params.allowed_excess_duration_ratio)) + batch = filter_uneven_sized_batch(batch, allowed_max_frames) + + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.warm_step + + texts = batch["supervisions"]["text"] + y = graph_compiler.texts_to_ids(texts) + y = k2.RaggedTensor(y).to(device) + + with torch.set_grad_enabled(is_training): + simple_loss, pruned_loss = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + ) + + s = params.simple_loss_scale + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss = params.simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + graph_compiler: CharCtcTrainingGraphCompiler, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + graph_compiler: CharCtcTrainingGraphCompiler, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + set_batch_count(model, params.batch_idx_train) + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + display_and_save_batch(batch, params=params, graph_compiler=graph_compiler) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + if cur_grad_scale < 1.0 or (cur_grad_scale < 8.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", + cur_grad_scale, + params.batch_idx_train, + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + lexicon = Lexicon(params.lang_dir) + graph_compiler = CharCtcTrainingGraphCompiler( + lexicon=lexicon, + device=device, + oov="", + ) + + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model).to(torch.float64) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + opts = diagnostics.TensorDiagnosticOptions( + 2**22 + ) # allow 4 megabytes per sub-module + diagnostic = diagnostics.attach_diagnostics(model, opts) + + if params.inf_check: + register_inf_check_hooks(model) + + def remove_short_and_long_utt(c: Cut): + # Keep only utterances with duration between 1 second and 20 seconds + # + # Caution: There is a reason to select 20.0 here. Please see + # ../local/display_manifest_statistics.py + # + # You should use ../local/display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + if c.duration < 1.0 or c.duration > 12.0: + logging.warning( + f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + ) + return False + + # In pruned RNN-T, we require that T >= S + # where T is the number of feature frames after subsampling + # and S is the number of tokens in the utterance + + # In ./zipformer.py, the conv module uses the following expression + # for subsampling + # T = ((c.num_frames - 7) // 2 + 1) // 2 + # tokens = sp.encode(c.supervisions[0].text, out_type=str) + + # if T < len(tokens): + # logging.warning( + # f"Exclude cut with ID {c.id} from training. " + # f"Number of frames (before subsampling): {c.num_frames}. " + # f"Number of frames (after subsampling): {T}. " + # f"Text: {c.supervisions[0].text}. " + # f"Tokens: {tokens}. " + # f"Number of tokens: {len(tokens)}" + # ) + # return False + + return True + + aishell = AIShell(manifest_dir=args.manifest_dir) + train_cuts = aishell.train_cuts() + train_cuts = train_cuts.filter(remove_short_and_long_utt) + + if args.enable_musan: + cuts_musan = load_manifest(Path(args.manifest_dir) / "musan_cuts.jsonl.gz") + else: + cuts_musan = None + + asr_datamodule = AsrDataModule(args) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = asr_datamodule.train_dataloaders( + train_cuts, + on_the_fly_feats=False, + cuts_musan=cuts_musan, + sampler_state_dict=sampler_state_dict, + ) + + valid_cuts = aishell.valid_cuts() + valid_dl = asr_datamodule.valid_dataloaders(valid_cuts) + # if not params.print_diagnostics: + # scan_pessimistic_batches_for_oom( + # model=model, + # train_dl=train_dl, + # optimizer=optimizer, + # graph_compiler=graph_compiler, + # params=params, + # ) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + logging.info(f"start training from epoch {params.start_epoch}") + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + graph_compiler=graph_compiler, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + graph_compiler: CharCtcTrainingGraphCompiler, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + supervisions = batch["supervisions"] + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = graph_compiler.texts_to_ids(supervisions["text"]) + num_tokens = sum(len(i) for i in y) + logging.info(f"num tokens: {num_tokens}") + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + graph_compiler: CharCtcTrainingGraphCompiler, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=True, + ) + loss.backward() + optimizer.zero_grad() + except Exception as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + display_and_save_batch(batch, params=params, graph_compiler=graph_compiler) + raise + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + + +def main(): + parser = get_parser() + AsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + args.lang_dir = Path(args.lang_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train2.py b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py new file mode 100755 index 000000000..d9eb9a0f4 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py @@ -0,0 +1,1266 @@ +#!/usr/bin/env python3 +# Copyright 2021-2022 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo,) +# Zengwei Yao) +# Copyright 2021 (Pingfeng Luo) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +./prepare.sh + +If you use --datatang-prob=0, then you don't need to run the above script. + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + + +./pruned_transducer_stateless7/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --exp-dir pruned_transducer_stateless7/exp \ + --full-libri 1 \ + --max-duration 300 + +# For mix precision training: + +./pruned_transducer_stateless7/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir pruned_transducer_stateless7/exp \ + --full-libri 1 \ + --max-duration 550 +""" + + +import argparse +import copy +import logging +import random +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from aishell import AIShell +from asr_datamodule import AsrDataModule +from decoder2 import Decoder +from joiner import Joiner +from lhotse import CutSet, load_manifest +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from model import Transducer +from optim import Eden, ScaledAdam +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer + +from icefall import diagnostics +from icefall.char_graph_compiler import CharCtcTrainingGraphCompiler +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + MetricsTracker, + filter_uneven_sized_batch, + setup_logger, + str2bool, +) + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for module in model.modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,4,3,2,4", + help="Number of zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--feedforward-dims", + type=str, + default="1024,1024,2048,2048,1024", + help="Feedforward dimension of the zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--nhead", + type=str, + default="8,8,8,8,8", + help="Number of attention heads in the zipformer encoder layers.", + ) + + parser.add_argument( + "--encoder-dims", + type=str, + default="384,384,384,384,384", + help="Embedding dimension in the 2 blocks of zipformer encoder layers, comma separated", + ) + + parser.add_argument( + "--attention-dims", + type=str, + default="192,192,192,192,192", + help="""Attention dimension in the 2 blocks of zipformer encoder layers, comma separated; + not the same as embedding dimension.""", + ) + + parser.add_argument( + "--encoder-unmasked-dims", + type=str, + default="256,256,256,256,256", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance " + " worse.", + ) + + parser.add_argument( + "--zipformer-downsampling-factors", + type=str, + default="1,2,4,8,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--cnn-module-kernels", + type=str, + default="31,31,31,31,31", + help="Sizes of kernels in convolution modules", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless3/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "--base-lr", type=float, default=0.05, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=5000, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=6, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=1, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network) part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--inf-check", + type=str2bool, + default=False, + help="Add hooks to check for infinite module outputs and gradients.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=4000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=30, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=200, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warmup period that dictates the decay of the + scale on "simple" (un-pruned) loss. + """ + params = AttributeDict( + { + "frame_shift_ms": 10.0, + "allowed_excess_duration_ratio": 0.1, + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 3000, # For the 100h subset, use 800 + # parameters for zipformer + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed. + "warm_step": 2000, + "env_info": get_env_info(), + } + ) + + return params + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + # TODO: We can add an option to switch between Zipformer and Transformer + def to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + encoder = Zipformer( + num_features=params.feature_dim, + output_downsampling_factor=2, + zipformer_downsampling_factors=to_int_tuple( + params.zipformer_downsampling_factors + ), + encoder_dims=to_int_tuple(params.encoder_dims), + attention_dim=to_int_tuple(params.attention_dims), + encoder_unmasked_dims=to_int_tuple(params.encoder_unmasked_dims), + nhead=to_int_tuple(params.nhead), + feedforward_dim=to_int_tuple(params.feedforward_dims), + cnn_module_kernels=to_int_tuple(params.cnn_module_kernels), + num_encoder_layers=to_int_tuple(params.num_encoder_layers), + ) + return encoder + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_transducer_model(params: AttributeDict) -> nn.Module: + encoder = get_encoder_model(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = Transducer( + encoder=encoder, + decoder=decoder, + joiner=joiner, + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + if "cur_batch_idx" in saved_params: + params["cur_batch_idx"] = saved_params["cur_batch_idx"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + graph_compiler: CharCtcTrainingGraphCompiler, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute RNN-T loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Zipformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + # For the uneven-sized batch, the total duration after padding would possibly + # cause OOM. Hence, for each batch, which is sorted descendingly by length, + # we simply drop the last few shortest samples, so that the retained total frames + # (after padding) would not exceed `allowed_max_frames`: + # `allowed_max_frames = int(max_frames * (1.0 + allowed_excess_duration_ratio))`, + # where `max_frames = max_duration * 1000 // frame_shift_ms`. + # We set allowed_excess_duration_ratio=0.1. + max_frames = params.max_duration * 1000 // params.frame_shift_ms + allowed_max_frames = int(max_frames * (1.0 + params.allowed_excess_duration_ratio)) + batch = filter_uneven_sized_batch(batch, allowed_max_frames) + + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.warm_step + + texts = batch["supervisions"]["text"] + y = graph_compiler.texts_to_ids(texts) + y = k2.RaggedTensor(y).to(device) + + with torch.set_grad_enabled(is_training): + simple_loss, pruned_loss = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + ) + + s = params.simple_loss_scale + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss = params.simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + graph_compiler: CharCtcTrainingGraphCompiler, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + graph_compiler: CharCtcTrainingGraphCompiler, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + set_batch_count(model, params.batch_idx_train) + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + display_and_save_batch(batch, params=params, graph_compiler=graph_compiler) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + if cur_grad_scale < 1.0 or (cur_grad_scale < 8.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", + cur_grad_scale, + params.batch_idx_train, + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + lexicon = Lexicon(params.lang_dir) + graph_compiler = CharCtcTrainingGraphCompiler( + lexicon=lexicon, + device=device, + oov="", + ) + + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model).to(torch.float64) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + opts = diagnostics.TensorDiagnosticOptions( + 2**22 + ) # allow 4 megabytes per sub-module + diagnostic = diagnostics.attach_diagnostics(model, opts) + + def remove_short_and_long_utt(c: Cut): + # Keep only utterances with duration between 1 second and 20 seconds + # + # Caution: There is a reason to select 20.0 here. Please see + # ../local/display_manifest_statistics.py + # + # You should use ../local/display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + if c.duration < 1.0 or c.duration > 12.0: + logging.warning( + f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + ) + return False + + # In pruned RNN-T, we require that T >= S + # where T is the number of feature frames after subsampling + # and S is the number of tokens in the utterance + + # In ./zipformer.py, the conv module uses the following expression + # for subsampling + # T = ((c.num_frames - 7) // 2 + 1) // 2 + # tokens = sp.encode(c.supervisions[0].text, out_type=str) + + # if T < len(tokens): + # logging.warning( + # f"Exclude cut with ID {c.id} from training. " + # f"Number of frames (before subsampling): {c.num_frames}. " + # f"Number of frames (after subsampling): {T}. " + # f"Text: {c.supervisions[0].text}. " + # f"Tokens: {tokens}. " + # f"Number of tokens: {len(tokens)}" + # ) + # return False + + return True + + aishell = AIShell(manifest_dir=args.manifest_dir) + train_cuts = aishell.train_cuts() + train_cuts = train_cuts.filter(remove_short_and_long_utt) + + if args.enable_musan: + cuts_musan = load_manifest(Path(args.manifest_dir) / "musan_cuts.jsonl.gz") + else: + cuts_musan = None + + asr_datamodule = AsrDataModule(args) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = asr_datamodule.train_dataloaders( + train_cuts, + on_the_fly_feats=False, + cuts_musan=cuts_musan, + sampler_state_dict=sampler_state_dict, + ) + + valid_cuts = aishell.valid_cuts() + valid_dl = asr_datamodule.valid_dataloaders(valid_cuts) + # if not params.print_diagnostics: + # scan_pessimistic_batches_for_oom( + # model=model, + # train_dl=train_dl, + # optimizer=optimizer, + # graph_compiler=graph_compiler, + # params=params, + # ) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + logging.info(f"start training from epoch {params.start_epoch}") + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + graph_compiler=graph_compiler, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + graph_compiler: CharCtcTrainingGraphCompiler, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + supervisions = batch["supervisions"] + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = graph_compiler.texts_to_ids(supervisions["text"]) + num_tokens = sum(len(i) for i in y) + logging.info(f"num tokens: {num_tokens}") + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + graph_compiler: CharCtcTrainingGraphCompiler, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=True, + ) + loss.backward() + optimizer.zero_grad() + except Exception as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + display_and_save_batch(batch, params=params, graph_compiler=graph_compiler) + raise + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + + +def main(): + parser = get_parser() + AsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + args.lang_dir = Path(args.lang_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/zipformer.py b/egs/aishell/ASR/pruned_transducer_stateless7/zipformer.py new file mode 120000 index 000000000..f2f66041e --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/zipformer.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/zipformer.py \ No newline at end of file diff --git a/egs/aishell/ASR/transducer_stateless_modified-2/asr_datamodule.py b/egs/aishell/ASR/transducer_stateless_modified-2/asr_datamodule.py index 5d49d7338..9c6021a19 100644 --- a/egs/aishell/ASR/transducer_stateless_modified-2/asr_datamodule.py +++ b/egs/aishell/ASR/transducer_stateless_modified-2/asr_datamodule.py @@ -20,7 +20,7 @@ import argparse import inspect import logging from pathlib import Path -from typing import Optional +from typing import Any, Dict, Optional from lhotse import CutSet, Fbank, FbankConfig from lhotse.dataset import ( @@ -144,6 +144,7 @@ class AsrDataModule: cuts_train: CutSet, on_the_fly_feats: bool, cuts_musan: Optional[CutSet] = None, + sampler_state_dict: Optional[Dict[str, Any]] = None, ) -> DataLoader: """ Args: @@ -228,6 +229,10 @@ class AsrDataModule: drop_last=True, ) + if sampler_state_dict is not None: + logging.info("Loading sampler state dict") + train_sampler.load_state_dict(sampler_state_dict) + logging.info("About to create train dataloader") train_dl = DataLoader( train, From dbcf0b41dbfd36648c0242eec82add35c124d840 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 23 May 2023 12:52:02 +0800 Subject: [PATCH 013/100] Fix stateless7 training error (#1082) --- .../pruned_transducer_stateless7/finetune.py | 25 ++++++++++--------- .../ASR/pruned_transducer_stateless7/train.py | 5 ++-- icefall/utils.py | 12 ++++----- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py index 02e8bd9eb..8151f3ba0 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py @@ -56,8 +56,8 @@ import sentencepiece as spm import torch import torch.multiprocessing as mp import torch.nn as nn -from decoder import Decoder from asr_datamodule import LibriSpeechAsrDataModule +from decoder import Decoder from gigaspeech import GigaSpeechAsrDataModule from joiner import Joiner from lhotse.cut import Cut, CutSet @@ -124,9 +124,9 @@ def add_finetune_arguments(parser: argparse.ArgumentParser): default=None, help=""" Modules to be initialized. It matches all parameters starting with - a specific key. The keys are given with Comma seperated. If None, - all modules will be initialised. For example, if you only want to - initialise all parameters staring with "encoder", use "encoder"; + a specific key. The keys are given with Comma seperated. If None, + all modules will be initialised. For example, if you only want to + initialise all parameters staring with "encoder", use "encoder"; if you want to initialise parameters starting with encoder or decoder, use "encoder,joiner". """, @@ -185,7 +185,7 @@ def add_model_arguments(parser: argparse.ArgumentParser): type=str, default="256,256,256,256,256", help="""Unmasked dimensions in the encoders, relates to augmentation - during training. Must be <= each of encoder_dims. Empirically, less + during training. Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance worse. """, ) @@ -288,7 +288,7 @@ def get_parser(): "--bpe-model", type=str, default="data/lang_bpe_500/bpe.model", - help="""Path to the BPE model. + help="""Path to the BPE model. This should be the bpe model of the original model """, ) @@ -302,8 +302,8 @@ def get_parser(): type=float, default=100000, help="""Number of steps that affects how rapidly the learning rate - decreases. During fine-tuning, we set this very large so that the - learning rate slowly decays with number of batches. You may tune + decreases. During fine-tuning, we set this very large so that the + learning rate slowly decays with number of batches. You may tune its value by yourself. """, ) @@ -312,9 +312,9 @@ def get_parser(): "--lr-epochs", type=float, default=100, - help="""Number of epochs that affects how rapidly the learning rate - decreases. During fine-tuning, we set this very large so that the - learning rate slowly decays with number of batches. You may tune + help="""Number of epochs that affects how rapidly the learning rate + decreases. During fine-tuning, we set this very large so that the + learning rate slowly decays with number of batches. You may tune its value by yourself. """, ) @@ -753,7 +753,8 @@ def compute_loss( # We set allowed_excess_duration_ratio=0.1. max_frames = params.max_duration * 1000 // params.frame_shift_ms allowed_max_frames = int(max_frames * (1.0 + params.allowed_excess_duration_ratio)) - batch = filter_uneven_sized_batch(batch, allowed_max_frames) + if is_training: + batch = filter_uneven_sized_batch(batch, allowed_max_frames) device = model.device if isinstance(model, DDP) else next(model.parameters()).device feature = batch["inputs"] diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py index ed6dfc28f..9c2816ae1 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py @@ -660,7 +660,7 @@ def compute_loss( values >= 1.0 are fully warmed up and have all modules present. """ # For the uneven-sized batch, the total duration after padding would possibly - # cause OOM. Hence, for each batch, which is sorted descendingly by length, + # cause OOM. Hence, for each batch, which is sorted in descending order by length, # we simply drop the last few shortest samples, so that the retained total frames # (after padding) would not exceed `allowed_max_frames`: # `allowed_max_frames = int(max_frames * (1.0 + allowed_excess_duration_ratio))`, @@ -668,7 +668,8 @@ def compute_loss( # We set allowed_excess_duration_ratio=0.1. max_frames = params.max_duration * 1000 // params.frame_shift_ms allowed_max_frames = int(max_frames * (1.0 + params.allowed_excess_duration_ratio)) - batch = filter_uneven_sized_batch(batch, allowed_max_frames) + if is_training: + batch = filter_uneven_sized_batch(batch, allowed_max_frames) device = model.device if isinstance(model, DDP) else next(model.parameters()).device feature = batch["inputs"] diff --git a/icefall/utils.py b/icefall/utils.py index fb350a73f..d002982ec 100644 --- a/icefall/utils.py +++ b/icefall/utils.py @@ -1551,7 +1551,7 @@ def is_module_available(*modules: str) -> bool: def filter_uneven_sized_batch(batch: dict, allowed_max_frames: int): """For the uneven-sized batch, the total duration after padding would possibly - cause OOM. Hence, for each batch, which is sorted descendingly by length, + cause OOM. Hence, for each batch, which is sorted in descending order by length, we simply drop the last few shortest samples, so that the retained total frames (after padding) would not exceed the given allow_max_frames. @@ -1567,20 +1567,20 @@ def filter_uneven_sized_batch(batch: dict, allowed_max_frames: int): N, T, _ = features.size() assert T == supervisions["num_frames"].max(), (T, supervisions["num_frames"].max()) - keep_num_utt = allowed_max_frames // T + kept_num_utt = allowed_max_frames // T - if keep_num_utt >= N: + if kept_num_utt >= N or kept_num_utt == 0: return batch # Note: we assume the samples in batch is sorted descendingly by length logging.info( f"Filtering uneven-sized batch, original batch size is {N}, " - f"retained batch size is {keep_num_utt}." + f"retained batch size is {kept_num_utt}." ) - batch["inputs"] = features[:keep_num_utt] + batch["inputs"] = features[:kept_num_utt] for k, v in supervisions.items(): assert len(v) == N, (len(v), N) - batch["supervisions"][k] = v[:keep_num_utt] + batch["supervisions"][k] = v[:kept_num_utt] return batch From ea8b15309fe1c4b6fcfeb04344a9eefdd3a76532 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 23 May 2023 13:32:14 +0800 Subject: [PATCH 014/100] Add onnx export scripts for wenetspeech recipe. (#1085) --- .../export-onnx-streaming.py | 678 ++++++++++++++++++ .../export-onnx.py | 602 ++++++++++++++++ .../onnx_check.py | 235 ++++++ .../onnx_pretrained-streaming.py | 457 ++++++++++++ .../onnx_pretrained.py | 425 +++++++++++ 5 files changed, 2397 insertions(+) create mode 100755 egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py create mode 100755 egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py create mode 100755 egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_check.py create mode 100755 egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py create mode 100755 egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py new file mode 100755 index 000000000..9a926d7e5 --- /dev/null +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py @@ -0,0 +1,678 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) + +""" +This script exports a transducer model from PyTorch to ONNX. + +We use the pre-trained model from +https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_streaming +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/wenetspeech/ASR + +repo_url=https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_streaming +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_char/Linv.pt" +git lfs pull --include "exp/pretrained_epoch_7_avg_1.pt" + +cd exp +ln -s pretrained_epoch_7_avg_1.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless5/export-onnx-streaming.py \ + --lang-dir $repo/data/lang_char \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model 0 \ + --exp-dir $repo/exp \ + --num-encoder-layers 24 \ + --dim-feedforward 1536 \ + --nhead 8 \ + --encoder-dim 384 \ + --decoder-dim 512 \ + --joiner-dim 512 + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +See ./onnx_pretrained-streaming.py for how to +use the exported ONNX models. + +You can find the exported models in +https://huggingface.co/csukuangfj/sherpa-onnx-streaming-conformer-zh-2023-05-23 +""" + +import argparse +import logging +from pathlib import Path +from typing import Dict, Tuple + +import onnx +from icefall.lexicon import Lexicon +import torch +import torch.nn as nn +from conformer import Conformer +from onnxruntime.quantization import QuantType, quantize_dynamic +from decoder import Decoder +from scaling_converter import convert_scaled_to_non_scaled +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import setup_logger, str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for averaging. + Note: Epoch counts from 0. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless5/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="The lang dir", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +def add_meta_data(filename: str, meta_data: Dict[str, str]): + """Add meta data to an ONNX model. It is changed in-place. + + Args: + filename: + Filename of the ONNX model to be changed. + meta_data: + Key-value pairs. + """ + model = onnx.load(filename) + for key, value in meta_data.items(): + meta = model.metadata_props.add() + meta.key = key + meta.value = value + + onnx.save(model, filename) + + +class OnnxEncoder(nn.Module): + """A wrapper for Conformer and the encoder_proj from the joiner""" + + def __init__(self, encoder: Conformer, encoder_proj: nn.Linear): + """ + Args: + encoder: + A Conformer encoder. + encoder_proj: + The projection layer for encoder from the joiner. + """ + super().__init__() + self.encoder = encoder + self.encoder_proj = encoder_proj + + self.num_encoder_layers = encoder.encoder_layers + self.encoder_dim = encoder.d_model + self.cnn_module_kernel = encoder.cnn_module_kernel + + # Note you can tune these values + self.left_context = 64 # after subsampling + self.chunk_size = 16 # after subsampling + self.right_context = 0 # after subsampling + + subsampling_factor = 4 + self.pad_length = (self.right_context + 2) * subsampling_factor + 3 + + self.T = (self.chunk_size * subsampling_factor) + self.pad_length + self.decode_chunk_len = self.chunk_size * subsampling_factor + + def forward( + self, + x: torch.Tensor, + cached_attn: torch.Tensor, + cached_conv: torch.Tensor, + processed_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Please see the help information of Conformer.forward + + Args: + x: + A 3-D tensor of shape (N, self.T, C) + cached_attn: + A 3-D tensor of shape + (num_encoder_layers, self.left_context, N, self.encoder_dim) + cached_conv: + A 3-D tensor of shape + (num_encoder_layers, self.cnn_module_kernel-1, N, self.encoder_dim) + processed_lens: + A 1-D tensor of shape (N,). It contains number of processed frames + after subsampling. Its dtype is torch.int64. + Returns: + Return a tuple containing: + - encoder_out, A 3-D tensor of shape (N, self.chunk_size, joiner_dim) + - new_cached_attn, it has the same shape as cached_attn + - new_cached_conv, it has the same shape as cached_conv + """ + assert x.size(1) == self.T, (x.shape, self.T) + N = x.size(0) + x_lens = torch.full((N,), fill_value=self.T, device=x.device, dtype=torch.int64) + + ( + encoder_out, + _, + [new_cached_attn, new_cached_conv], + ) = self.encoder.streaming_forward( + x, + x_lens, + states=[cached_attn, cached_conv], + processed_lens=processed_lens, + left_context=self.left_context, + right_context=self.right_context, + chunk_size=self.chunk_size, + ) + + encoder_out = self.encoder_proj(encoder_out) + # Now encoder_out is of shape (N, T, joiner_dim) + + return encoder_out, new_cached_attn, new_cached_conv + + +class OnnxDecoder(nn.Module): + """A wrapper for Decoder and the decoder_proj from the joiner""" + + def __init__(self, decoder: Decoder, decoder_proj: nn.Linear): + super().__init__() + self.decoder = decoder + self.decoder_proj = decoder_proj + + def forward(self, y: torch.Tensor) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, context_size). + Returns + Return a 2-D tensor of shape (N, joiner_dim) + """ + need_pad = False + decoder_output = self.decoder(y, need_pad=need_pad) + decoder_output = decoder_output.squeeze(1) + output = self.decoder_proj(decoder_output) + + return output + + +class OnnxJoiner(nn.Module): + """A wrapper for the joiner""" + + def __init__(self, output_linear: nn.Linear): + super().__init__() + self.output_linear = output_linear + + def forward( + self, + encoder_out: torch.Tensor, + decoder_out: torch.Tensor, + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + logit = encoder_out + decoder_out + logit = self.output_linear(torch.tanh(logit)) + return logit + + +def export_encoder_model_onnx( + encoder_model: OnnxEncoder, + encoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the given encoder model to ONNX format. + The exported model has two inputs: + + - x, a tensor of shape (N, T, C); dtype is torch.float32 + - x_lens, a tensor of shape (N,); dtype is torch.int64 + + and it has two outputs: + + - encoder_out, a tensor of shape (N, T', joiner_dim) + - encoder_out_lens, a tensor of shape (N,) + + Args: + encoder_model: + The input encoder model + encoder_filename: + The filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + N = 1 + x = torch.zeros(N, encoder_model.T, 80, dtype=torch.float32) + cached_attn = torch.zeros( + encoder_model.num_encoder_layers, + encoder_model.left_context, + N, + encoder_model.encoder_dim, + ) + cached_conv = torch.zeros( + encoder_model.num_encoder_layers, + encoder_model.cnn_module_kernel - 1, + N, + encoder_model.encoder_dim, + ) + processed_lens = torch.zeros((N,), dtype=torch.int64) + + torch.onnx.export( + encoder_model, + (x, cached_attn, cached_conv, processed_lens), + encoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["x", "cached_attn", "cached_conv", "processed_lens"], + output_names=["encoder_out", "new_cached_attn", "new_cached_conv"], + dynamic_axes={ + "x": {0: "N"}, + "cached_attn": {2: "N"}, + "cached_conv": {2: "N"}, + "processed_lens": {0: "N"}, + "encoder_out": {0: "N"}, + "new_cached_attn": {2: "N"}, + "new_cached_conv": {2: "N"}, + }, + ) + + meta_data = { + "model_type": "conformer", + "version": "1", + "model_author": "k2-fsa", + "comment": "stateless5", + "pad_length": str(encoder_model.pad_length), + "decode_chunk_len": str(encoder_model.decode_chunk_len), + "encoder_dim": str(encoder_model.encoder_dim), + "num_encoder_layers": str(encoder_model.num_encoder_layers), + "cnn_module_kernel": str(encoder_model.cnn_module_kernel), + "left_context": str(encoder_model.left_context), + "right_context": str(encoder_model.right_context), + "chunk_size": str(encoder_model.chunk_size), + "T": str(encoder_model.T), + } + logging.info(f"meta_data: {meta_data}") + + add_meta_data(filename=encoder_filename, meta_data=meta_data) + + +def export_decoder_model_onnx( + decoder_model: OnnxDecoder, + decoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the decoder model to ONNX format. + + The exported model has one input: + + - y: a torch.int64 tensor of shape (N, decoder_model.context_size) + + and has one output: + + - decoder_out: a torch.float32 tensor of shape (N, joiner_dim) + + Args: + decoder_model: + The decoder model to be exported. + decoder_filename: + Filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + context_size = decoder_model.decoder.context_size + vocab_size = decoder_model.decoder.vocab_size + + y = torch.zeros(10, context_size, dtype=torch.int64) + torch.onnx.export( + decoder_model, + y, + decoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["y"], + output_names=["decoder_out"], + dynamic_axes={ + "y": {0: "N"}, + "decoder_out": {0: "N"}, + }, + ) + + meta_data = { + "context_size": str(context_size), + "vocab_size": str(vocab_size), + } + add_meta_data(filename=decoder_filename, meta_data=meta_data) + + +def export_joiner_model_onnx( + joiner_model: nn.Module, + joiner_filename: str, + opset_version: int = 11, +) -> None: + """Export the joiner model to ONNX format. + The exported joiner model has two inputs: + + - encoder_out: a tensor of shape (N, joiner_dim) + - decoder_out: a tensor of shape (N, joiner_dim) + + and produces one output: + + - logit: a tensor of shape (N, vocab_size) + """ + joiner_dim = joiner_model.output_linear.weight.shape[1] + logging.info(f"joiner dim: {joiner_dim}") + + projected_encoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + projected_decoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + + torch.onnx.export( + joiner_model, + (projected_encoder_out, projected_decoder_out), + joiner_filename, + verbose=False, + opset_version=opset_version, + input_names=[ + "encoder_out", + "decoder_out", + ], + output_names=["logit"], + dynamic_axes={ + "encoder_out": {0: "N"}, + "decoder_out": {0: "N"}, + "logit": {0: "N"}, + }, + ) + meta_data = { + "joiner_dim": str(joiner_dim), + } + add_meta_data(filename=joiner_filename, meta_data=meta_data) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + if not params.causal_convolution: + logging.info("Seting causal_convolution to True for exporting streaming models") + params.causal_convolution = True + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + setup_logger(f"{params.exp_dir}/log-export/log-export-onnx") + + logging.info(f"device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + num_param = sum(p.numel() for p in model.parameters()) + logging.info(f"Number of model parameters: {num_param}") + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + convert_scaled_to_non_scaled(model, inplace=True) + + encoder = OnnxEncoder( + encoder=model.encoder, + encoder_proj=model.joiner.encoder_proj, + ) + + decoder = OnnxDecoder( + decoder=model.decoder, + decoder_proj=model.joiner.decoder_proj, + ) + + joiner = OnnxJoiner(output_linear=model.joiner.output_linear) + + encoder_num_param = sum([p.numel() for p in encoder.parameters()]) + decoder_num_param = sum([p.numel() for p in decoder.parameters()]) + joiner_num_param = sum([p.numel() for p in joiner.parameters()]) + total_num_param = encoder_num_param + decoder_num_param + joiner_num_param + logging.info(f"encoder parameters: {encoder_num_param}") + logging.info(f"decoder parameters: {decoder_num_param}") + logging.info(f"joiner parameters: {joiner_num_param}") + logging.info(f"total parameters: {total_num_param}") + + if params.iter > 0: + suffix = f"iter-{params.iter}" + else: + suffix = f"epoch-{params.epoch}" + + suffix += f"-avg-{params.avg}" + + opset_version = 13 + + logging.info("Exporting encoder") + encoder_filename = params.exp_dir / f"encoder-{suffix}.onnx" + export_encoder_model_onnx( + encoder, + encoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported encoder to {encoder_filename}") + + logging.info("Exporting decoder") + decoder_filename = params.exp_dir / f"decoder-{suffix}.onnx" + export_decoder_model_onnx( + decoder, + decoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported decoder to {decoder_filename}") + + logging.info("Exporting joiner") + joiner_filename = params.exp_dir / f"joiner-{suffix}.onnx" + export_joiner_model_onnx( + joiner, + joiner_filename, + opset_version=opset_version, + ) + logging.info(f"Exported joiner to {joiner_filename}") + + # Generate int8 quantization models + # See https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html#data-type-selection + + logging.info("Generate int8 quantization models") + + encoder_filename_int8 = params.exp_dir / f"encoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=encoder_filename, + model_output=encoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + decoder_filename_int8 = params.exp_dir / f"decoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=decoder_filename, + model_output=decoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + joiner_filename_int8 = params.exp_dir / f"joiner-{suffix}.int8.onnx" + quantize_dynamic( + model_input=joiner_filename, + model_output=joiner_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + main() diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py new file mode 100755 index 000000000..68c7cc352 --- /dev/null +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py @@ -0,0 +1,602 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) + +""" +This script exports a transducer model from PyTorch to ONNX. + +We use the pre-trained model from +https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_offline/ +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/wenetspeech/ASR + +repo_url=https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_offline/ +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_char/Linv.pt" +git lfs pull --include "exp/pretrained_epoch_9_avg_1.pt" + +cd exp +ln -s pretrained_epoch_9_avg_1.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless5/export-onnx.py \ + --lang-dir $repo/data/lang_char \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model 0 \ + --exp-dir $repo/exp \ + --num-encoder-layers 24 \ + --dim-feedforward 1536 \ + --nhead 8 \ + --encoder-dim 384 \ + --decoder-dim 512 \ + --joiner-dim 512 + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +See ./onnx_pretrained.py and ./onnx_check.py for how to +use the exported ONNX models. +""" + +import argparse +import logging +from pathlib import Path +from typing import Dict, Tuple + +import onnx +import torch +import torch.nn as nn +from conformer import Conformer +from decoder import Decoder +from onnxruntime.quantization import QuantType, quantize_dynamic +from scaling_converter import convert_scaled_to_non_scaled +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import setup_logger, str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for averaging. + Note: Epoch counts from 0. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless5/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="The lang dir", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +def add_meta_data(filename: str, meta_data: Dict[str, str]): + """Add meta data to an ONNX model. It is changed in-place. + + Args: + filename: + Filename of the ONNX model to be changed. + meta_data: + Key-value pairs. + """ + model = onnx.load(filename) + for key, value in meta_data.items(): + meta = model.metadata_props.add() + meta.key = key + meta.value = value + + onnx.save(model, filename) + + +class OnnxEncoder(nn.Module): + """A wrapper for Conformer and the encoder_proj from the joiner""" + + def __init__(self, encoder: Conformer, encoder_proj: nn.Linear): + """ + Args: + encoder: + A Conformer encoder. + encoder_proj: + The projection layer for encoder from the joiner. + """ + super().__init__() + self.encoder = encoder + self.encoder_proj = encoder_proj + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Please see the help information of Conformer.forward + + Args: + x: + A 3-D tensor of shape (N, T, C) + x_lens: + A 1-D tensor of shape (N,). Its dtype is torch.int64 + Returns: + Return a tuple containing: + - encoder_out, A 3-D tensor of shape (N, T', joiner_dim) + - encoder_out_lens, A 1-D tensor of shape (N,) + """ + encoder_out, encoder_out_lens = self.encoder(x, x_lens) + + encoder_out = self.encoder_proj(encoder_out) + # Now encoder_out is of shape (N, T, joiner_dim) + + return encoder_out, encoder_out_lens + + +class OnnxDecoder(nn.Module): + """A wrapper for Decoder and the decoder_proj from the joiner""" + + def __init__(self, decoder: Decoder, decoder_proj: nn.Linear): + super().__init__() + self.decoder = decoder + self.decoder_proj = decoder_proj + + def forward(self, y: torch.Tensor) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, context_size). + Returns + Return a 2-D tensor of shape (N, joiner_dim) + """ + need_pad = False + decoder_output = self.decoder(y, need_pad=need_pad) + decoder_output = decoder_output.squeeze(1) + output = self.decoder_proj(decoder_output) + + return output + + +class OnnxJoiner(nn.Module): + """A wrapper for the joiner""" + + def __init__(self, output_linear: nn.Linear): + super().__init__() + self.output_linear = output_linear + + def forward( + self, + encoder_out: torch.Tensor, + decoder_out: torch.Tensor, + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + logit = encoder_out + decoder_out + logit = self.output_linear(torch.tanh(logit)) + return logit + + +def export_encoder_model_onnx( + encoder_model: OnnxEncoder, + encoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the given encoder model to ONNX format. + The exported model has two inputs: + + - x, a tensor of shape (N, T, C); dtype is torch.float32 + - x_lens, a tensor of shape (N,); dtype is torch.int64 + + and it has two outputs: + + - encoder_out, a tensor of shape (N, T', joiner_dim) + - encoder_out_lens, a tensor of shape (N,) + + Args: + encoder_model: + The input encoder model + encoder_filename: + The filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + x = torch.zeros(1, 100, 80, dtype=torch.float32) + x_lens = torch.tensor([100], dtype=torch.int64) + + torch.onnx.export( + encoder_model, + (x, x_lens), + encoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["x", "x_lens"], + output_names=["encoder_out", "encoder_out_lens"], + dynamic_axes={ + "x": {0: "N", 1: "T"}, + "x_lens": {0: "N"}, + "encoder_out": {0: "N", 1: "T"}, + "encoder_out_lens": {0: "N"}, + }, + ) + + meta_data = { + "model_type": "conformer", + "version": "1", + "model_author": "k2-fsa", + "comment": "stateless5", + } + logging.info(f"meta_data: {meta_data}") + + add_meta_data(filename=encoder_filename, meta_data=meta_data) + + +def export_decoder_model_onnx( + decoder_model: OnnxDecoder, + decoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the decoder model to ONNX format. + + The exported model has one input: + + - y: a torch.int64 tensor of shape (N, decoder_model.context_size) + + and has one output: + + - decoder_out: a torch.float32 tensor of shape (N, joiner_dim) + + Args: + decoder_model: + The decoder model to be exported. + decoder_filename: + Filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + context_size = decoder_model.decoder.context_size + vocab_size = decoder_model.decoder.vocab_size + + y = torch.zeros(10, context_size, dtype=torch.int64) + torch.onnx.export( + decoder_model, + y, + decoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["y"], + output_names=["decoder_out"], + dynamic_axes={ + "y": {0: "N"}, + "decoder_out": {0: "N"}, + }, + ) + + meta_data = { + "context_size": str(context_size), + "vocab_size": str(vocab_size), + } + add_meta_data(filename=decoder_filename, meta_data=meta_data) + + +def export_joiner_model_onnx( + joiner_model: nn.Module, + joiner_filename: str, + opset_version: int = 11, +) -> None: + """Export the joiner model to ONNX format. + The exported joiner model has two inputs: + + - encoder_out: a tensor of shape (N, joiner_dim) + - decoder_out: a tensor of shape (N, joiner_dim) + + and produces one output: + + - logit: a tensor of shape (N, vocab_size) + """ + joiner_dim = joiner_model.output_linear.weight.shape[1] + logging.info(f"joiner dim: {joiner_dim}") + + projected_encoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + projected_decoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + + torch.onnx.export( + joiner_model, + (projected_encoder_out, projected_decoder_out), + joiner_filename, + verbose=False, + opset_version=opset_version, + input_names=[ + "encoder_out", + "decoder_out", + ], + output_names=["logit"], + dynamic_axes={ + "encoder_out": {0: "N"}, + "decoder_out": {0: "N"}, + "logit": {0: "N"}, + }, + ) + meta_data = { + "joiner_dim": str(joiner_dim), + } + add_meta_data(filename=joiner_filename, meta_data=meta_data) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + setup_logger(f"{params.exp_dir}/log-export/log-export-onnx") + + logging.info(f"device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + convert_scaled_to_non_scaled(model, inplace=True) + + encoder = OnnxEncoder( + encoder=model.encoder, + encoder_proj=model.joiner.encoder_proj, + ) + + decoder = OnnxDecoder( + decoder=model.decoder, + decoder_proj=model.joiner.decoder_proj, + ) + + joiner = OnnxJoiner(output_linear=model.joiner.output_linear) + + encoder_num_param = sum([p.numel() for p in encoder.parameters()]) + decoder_num_param = sum([p.numel() for p in decoder.parameters()]) + joiner_num_param = sum([p.numel() for p in joiner.parameters()]) + total_num_param = encoder_num_param + decoder_num_param + joiner_num_param + logging.info(f"encoder parameters: {encoder_num_param}") + logging.info(f"decoder parameters: {decoder_num_param}") + logging.info(f"joiner parameters: {joiner_num_param}") + logging.info(f"total parameters: {total_num_param}") + + if params.iter > 0: + suffix = f"iter-{params.iter}" + else: + suffix = f"epoch-{params.epoch}" + + suffix += f"-avg-{params.avg}" + + opset_version = 13 + + logging.info("Exporting encoder") + encoder_filename = params.exp_dir / f"encoder-{suffix}.onnx" + export_encoder_model_onnx( + encoder, + encoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported encoder to {encoder_filename}") + + logging.info("Exporting decoder") + decoder_filename = params.exp_dir / f"decoder-{suffix}.onnx" + export_decoder_model_onnx( + decoder, + decoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported decoder to {decoder_filename}") + + logging.info("Exporting joiner") + joiner_filename = params.exp_dir / f"joiner-{suffix}.onnx" + export_joiner_model_onnx( + joiner, + joiner_filename, + opset_version=opset_version, + ) + logging.info(f"Exported joiner to {joiner_filename}") + + # Generate int8 quantization models + # See https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html#data-type-selection + + logging.info("Generate int8 quantization models") + + encoder_filename_int8 = params.exp_dir / f"encoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=encoder_filename, + model_output=encoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + decoder_filename_int8 = params.exp_dir / f"decoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=decoder_filename, + model_output=decoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + joiner_filename_int8 = params.exp_dir / f"joiner-{suffix}.int8.onnx" + quantize_dynamic( + model_input=joiner_filename, + model_output=joiner_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + main() diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_check.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_check.py new file mode 100755 index 000000000..ee8252a90 --- /dev/null +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_check.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# +# Copyright 2022 Xiaomi Corporation (Author: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script checks that exported onnx models produce the same output +with the given torchscript model for the same input. + +We use the pre-trained model from +https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_offline/ +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/wenetspeech/ASR + +repo_url=https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_offline/ +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_char/Linv.pt" +git lfs pull --include "exp/pretrained_epoch_4_avg_1.pt" +git lfs pull --include "exp/cpu_jit_epoch_4_avg_1_torch.1.7.1.pt" + +cd exp +ln -s pretrained_epoch_9_avg_1_torch.1.7.1.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless5/export-onnx.py \ + --lang-dir $repo/data/lang_char \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model 0 \ + --exp-dir $repo/exp \ + --num-encoder-layers 24 \ + --dim-feedforward 1536 \ + --nhead 8 \ + --encoder-dim 384 \ + --decoder-dim 512 \ + --joiner-dim 512 + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +4. Run this file + +./pruned_transducer_stateless5/onnx_check.py \ + --jit-filename $repo/exp/cpu_jit_epoch_4_avg_1_torch.1.7.1.pt \ + --onnx-encoder-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --onnx-decoder-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --onnx-joiner-filename $repo/exp/joiner-epoch-99-avg-1.onnx +""" + +import argparse +import logging + +from icefall import is_module_available +from onnx_pretrained import OnnxModel + +import torch + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--jit-filename", + required=True, + type=str, + help="Path to the torchscript model", + ) + + parser.add_argument( + "--onnx-encoder-filename", + required=True, + type=str, + help="Path to the onnx encoder model", + ) + + parser.add_argument( + "--onnx-decoder-filename", + required=True, + type=str, + help="Path to the onnx decoder model", + ) + + parser.add_argument( + "--onnx-joiner-filename", + required=True, + type=str, + help="Path to the onnx joiner model", + ) + + return parser + + +def test_encoder( + torch_model: torch.jit.ScriptModule, + onnx_model: OnnxModel, +): + C = 80 + for i in range(3): + N = torch.randint(low=1, high=20, size=(1,)).item() + T = torch.randint(low=30, high=50, size=(1,)).item() + logging.info(f"test_encoder: iter {i}, N={N}, T={T}") + + x = torch.rand(N, T, C) + x_lens = torch.randint(low=30, high=T + 1, size=(N,)) + x_lens[0] = T + + torch_encoder_out, torch_encoder_out_lens = torch_model.encoder(x, x_lens) + torch_encoder_out = torch_model.joiner.encoder_proj(torch_encoder_out) + + onnx_encoder_out, onnx_encoder_out_lens = onnx_model.run_encoder(x, x_lens) + + assert torch.allclose(torch_encoder_out, onnx_encoder_out, atol=1e-05), ( + (torch_encoder_out - onnx_encoder_out).abs().max() + ) + + +def test_decoder( + torch_model: torch.jit.ScriptModule, + onnx_model: OnnxModel, +): + context_size = onnx_model.context_size + vocab_size = onnx_model.vocab_size + for i in range(10): + N = torch.randint(1, 100, size=(1,)).item() + logging.info(f"test_decoder: iter {i}, N={N}") + x = torch.randint( + low=1, + high=vocab_size, + size=(N, context_size), + dtype=torch.int64, + ) + torch_decoder_out = torch_model.decoder(x, need_pad=torch.tensor([False])) + torch_decoder_out = torch_model.joiner.decoder_proj(torch_decoder_out) + torch_decoder_out = torch_decoder_out.squeeze(1) + + onnx_decoder_out = onnx_model.run_decoder(x) + assert torch.allclose(torch_decoder_out, onnx_decoder_out, atol=1e-4), ( + (torch_decoder_out - onnx_decoder_out).abs().max() + ) + + +def test_joiner( + torch_model: torch.jit.ScriptModule, + onnx_model: OnnxModel, +): + encoder_dim = torch_model.joiner.encoder_proj.weight.shape[1] + decoder_dim = torch_model.joiner.decoder_proj.weight.shape[1] + for i in range(10): + N = torch.randint(1, 100, size=(1,)).item() + logging.info(f"test_joiner: iter {i}, N={N}") + encoder_out = torch.rand(N, encoder_dim) + decoder_out = torch.rand(N, decoder_dim) + + projected_encoder_out = torch_model.joiner.encoder_proj(encoder_out) + projected_decoder_out = torch_model.joiner.decoder_proj(decoder_out) + + torch_joiner_out = torch_model.joiner(encoder_out, decoder_out) + onnx_joiner_out = onnx_model.run_joiner( + projected_encoder_out, projected_decoder_out + ) + + assert torch.allclose(torch_joiner_out, onnx_joiner_out, atol=1e-4), ( + (torch_joiner_out - onnx_joiner_out).abs().max() + ) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + logging.info(vars(args)) + + torch_model = torch.jit.load(args.jit_filename) + + onnx_model = OnnxModel( + encoder_model_filename=args.onnx_encoder_filename, + decoder_model_filename=args.onnx_decoder_filename, + joiner_model_filename=args.onnx_joiner_filename, + ) + + logging.info("Test encoder") + test_encoder(torch_model, onnx_model) + + logging.info("Test decoder") + test_decoder(torch_model, onnx_model) + + logging.info("Test joiner") + test_joiner(torch_model, onnx_model) + logging.info("Finished checking ONNX models") + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +# See https://github.com/pytorch/pytorch/issues/38342 +# and https://github.com/pytorch/pytorch/issues/33354 +# +# If we don't do this, the delay increases whenever there is +# a new request that changes the actual batch size. +# If you use `py-spy dump --pid --native`, you will +# see a lot of time is spent in re-compiling the torch script model. +torch._C._jit_set_profiling_executor(False) +torch._C._jit_set_profiling_mode(False) +torch._C._set_graph_executor_optimize(False) +if __name__ == "__main__": + torch.manual_seed(20220727) + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py new file mode 100755 index 000000000..facfc2258 --- /dev/null +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +""" +This script loads ONNX models exported by ./export-onnx.py +and uses them to decode waves. + +We use the pre-trained model from +https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_streaming +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/wenetspeech/ASR + +repo_url=https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_streaming +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_char/Linv.pt" +git lfs pull --include "exp/pretrained_epoch_7_avg_1.pt" + +cd exp +ln -s pretrained_epoch_7_avg_1.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless5/export-onnx-streaming.py \ + --lang-dir $repo/data/lang_char \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model 0 \ + --exp-dir $repo/exp \ + --num-encoder-layers 24 \ + --dim-feedforward 1536 \ + --nhead 8 \ + --encoder-dim 384 \ + --decoder-dim 512 \ + --joiner-dim 512 + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +3. Run this file with the exported ONNX models + +./pruned_transducer_stateless5/onnx_pretrained-streaming.py \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ + --tokens $repo/data/lang_char/tokens.txt \ + $repo/test_wavs/DEV_T0000000000.wav + +Note: Even though this script only supports decoding a single file, +the exported ONNX models do support batch processing. + +You can find the exported models in +https://huggingface.co/csukuangfj/sherpa-onnx-streaming-conformer-zh-2023-05-23 +""" + +import argparse +import logging +from typing import Dict, List, Optional, Tuple + +import k2 +import numpy as np +import onnxruntime as ort +import torch +import torchaudio +from kaldifeat import FbankOptions, OnlineFbank, OnlineFeature + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--encoder-model-filename", + type=str, + required=True, + help="Path to the encoder onnx model. ", + ) + + parser.add_argument( + "--decoder-model-filename", + type=str, + required=True, + help="Path to the decoder onnx model. ", + ) + + parser.add_argument( + "--joiner-model-filename", + type=str, + required=True, + help="Path to the joiner onnx model. ", + ) + + parser.add_argument( + "--tokens", + type=str, + help="""Path to tokens.txt.""", + ) + + parser.add_argument( + "sound_file", + type=str, + help="The input sound file to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + return parser + + +class OnnxModel: + def __init__( + self, + encoder_model_filename: str, + decoder_model_filename: str, + joiner_model_filename: str, + ): + session_opts = ort.SessionOptions() + session_opts.inter_op_num_threads = 1 + session_opts.intra_op_num_threads = 1 + + self.session_opts = session_opts + + self.init_encoder(encoder_model_filename) + self.init_decoder(decoder_model_filename) + self.init_joiner(joiner_model_filename) + + def init_encoder(self, encoder_model_filename: str): + self.encoder = ort.InferenceSession( + encoder_model_filename, + sess_options=self.session_opts, + ) + self.init_encoder_states() + + def init_encoder_states(self, batch_size: int = 1): + encoder_meta = self.encoder.get_modelmeta().custom_metadata_map + print(encoder_meta) + + model_type = encoder_meta["model_type"] + assert model_type == "conformer", model_type + + decode_chunk_len = int(encoder_meta["decode_chunk_len"]) + T = int(encoder_meta["T"]) + pad_length = int(encoder_meta["pad_length"]) + + encoder_dim = int(encoder_meta["encoder_dim"]) + cnn_module_kernel = int(encoder_meta["cnn_module_kernel"]) + left_context = int(encoder_meta["left_context"]) + num_encoder_layers = int(encoder_meta["num_encoder_layers"]) + + self.cached_attn = torch.zeros( + num_encoder_layers, + left_context, + batch_size, + encoder_dim, + ).numpy() + self.cached_conv = torch.zeros( + num_encoder_layers, + cnn_module_kernel - 1, + batch_size, + encoder_dim, + ).numpy() + + logging.info(f"decode_chunk_len: {decode_chunk_len}") + logging.info(f"T: {T}") + logging.info(f"pad_length: {pad_length}") + logging.info(f"encoder_dim: {encoder_dim}") + logging.info(f"cnn_module_kernel: {cnn_module_kernel}") + logging.info(f"left_context: {left_context}") + logging.info(f"num_encoder_layers: {num_encoder_layers}") + + self.segment = T + self.offset = decode_chunk_len + + def init_decoder(self, decoder_model_filename: str): + self.decoder = ort.InferenceSession( + decoder_model_filename, + sess_options=self.session_opts, + ) + + decoder_meta = self.decoder.get_modelmeta().custom_metadata_map + self.context_size = int(decoder_meta["context_size"]) + self.vocab_size = int(decoder_meta["vocab_size"]) + + logging.info(f"context_size: {self.context_size}") + logging.info(f"vocab_size: {self.vocab_size}") + + def init_joiner(self, joiner_model_filename: str): + self.joiner = ort.InferenceSession( + joiner_model_filename, + sess_options=self.session_opts, + ) + + joiner_meta = self.joiner.get_modelmeta().custom_metadata_map + self.joiner_dim = int(joiner_meta["joiner_dim"]) + + logging.info(f"joiner_dim: {self.joiner_dim}") + + def _build_encoder_input_output( + self, x: torch.Tensor, processed_lens: int + ) -> Tuple[Dict[str, np.ndarray], List[str]]: + assert x.size(0) == 1 + encoder_input = { + "x": x.numpy(), + "cached_attn": self.cached_attn, + "cached_conv": self.cached_conv, + "processed_lens": torch.full( + (1,), fill_value=processed_lens, dtype=torch.int64 + ).numpy(), + } + encoder_output = ["encoder_out", "new_cached_attn", "new_cached_conv"] + + return encoder_input, encoder_output + + def _update_states(self, states: List[np.ndarray]): + self.cached_attn = states[0] + self.cached_conv = states[1] + + def run_encoder(self, x: torch.Tensor, num_processed_frames: int) -> torch.Tensor: + """ + Args: + x: + A 3-D tensor of shape (N, self.T, C). It only implements N == 1 + num_processed_frames: + Number of processed frames before subsampling. + Returns: + Return a 3-D tensor of shape (N, chunk_size, joiner_dim) + """ + # assume subsampling_factor is 4 + num_processed_frames = num_processed_frames // 4 + encoder_input, encoder_output_names = self._build_encoder_input_output( + x, num_processed_frames + ) + out = self.encoder.run(encoder_output_names, encoder_input) + + self._update_states(out[1:]) + + return torch.from_numpy(out[0]) + + def run_decoder(self, decoder_input: torch.Tensor) -> torch.Tensor: + """ + Args: + decoder_input: + A 2-D tensor of shape (N, context_size) + Returns: + Return a 2-D tensor of shape (N, joiner_dim) + """ + out = self.decoder.run( + [self.decoder.get_outputs()[0].name], + {self.decoder.get_inputs()[0].name: decoder_input.numpy()}, + )[0] + + return torch.from_numpy(out) + + def run_joiner( + self, encoder_out: torch.Tensor, decoder_out: torch.Tensor + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + out = self.joiner.run( + [self.joiner.get_outputs()[0].name], + { + self.joiner.get_inputs()[0].name: encoder_out.numpy(), + self.joiner.get_inputs()[1].name: decoder_out.numpy(), + }, + )[0] + + return torch.from_numpy(out) + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def create_streaming_feature_extractor() -> OnlineFeature: + """Create a CPU streaming feature extractor. + + At present, we assume it returns a fbank feature extractor with + fixed options. In the future, we will support passing in the options + from outside. + + Returns: + Return a CPU streaming feature extractor. + """ + opts = FbankOptions() + opts.device = "cpu" + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = 16000 + opts.mel_opts.num_bins = 80 + return OnlineFbank(opts) + + +def greedy_search( + model: OnnxModel, + encoder_out: torch.Tensor, + context_size: int, + decoder_out: Optional[torch.Tensor] = None, + hyp: Optional[List[int]] = None, +) -> List[int]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (1, T, joiner_dim) + context_size: + The context size of the decoder model. + decoder_out: + Optional. Decoder output of the previous chunk. + hyp: + Decoding results for previous chunks. + Returns: + Return the decoded results so far. + """ + + blank_id = 0 + + if decoder_out is None: + assert hyp is None, hyp + hyp = [blank_id] * context_size + decoder_input = torch.tensor([hyp], dtype=torch.int64) + decoder_out = model.run_decoder(decoder_input) + else: + assert hyp is not None, hyp + + encoder_out = encoder_out.squeeze(0) + T = encoder_out.size(0) + for t in range(T): + cur_encoder_out = encoder_out[t : t + 1] + joiner_out = model.run_joiner(cur_encoder_out, decoder_out).squeeze(0) + y = joiner_out.argmax(dim=0).item() + if y != blank_id: + hyp.append(y) + decoder_input = hyp[-context_size:] + decoder_input = torch.tensor([decoder_input], dtype=torch.int64) + decoder_out = model.run_decoder(decoder_input) + + return hyp, decoder_out + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + + model = OnnxModel( + encoder_model_filename=args.encoder_model_filename, + decoder_model_filename=args.decoder_model_filename, + joiner_model_filename=args.joiner_model_filename, + ) + + sample_rate = 16000 + + logging.info("Constructing Fbank computer") + online_fbank = create_streaming_feature_extractor() + + logging.info(f"Reading sound files: {args.sound_file}") + waves = read_sound_files( + filenames=[args.sound_file], + expected_sample_rate=sample_rate, + )[0] + + tail_padding = torch.zeros(int(1.0 * sample_rate), dtype=torch.float32) + wave_samples = torch.cat([waves, tail_padding]) + + num_processed_frames = 0 + segment = model.segment + offset = model.offset + + context_size = model.context_size + hyp = None + decoder_out = None + + chunk = int(1 * sample_rate) # 1 second + start = 0 + while start < wave_samples.numel(): + end = min(start + chunk, wave_samples.numel()) + samples = wave_samples[start:end] + start += chunk + + online_fbank.accept_waveform( + sampling_rate=sample_rate, + waveform=samples, + ) + + while online_fbank.num_frames_ready - num_processed_frames >= segment: + frames = [] + for i in range(segment): + frames.append(online_fbank.get_frame(num_processed_frames + i)) + num_processed_frames += offset + frames = torch.cat(frames, dim=0) + frames = frames.unsqueeze(0) + encoder_out = model.run_encoder(frames, num_processed_frames) + hyp, decoder_out = greedy_search( + model, + encoder_out, + context_size, + decoder_out, + hyp, + ) + + symbol_table = k2.SymbolTable.from_file(args.tokens) + + text = "" + for i in hyp[context_size:]: + text += symbol_table[i] + text = text.replace("▁", " ").strip() + + logging.info(args.sound_file) + logging.info(text) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py new file mode 100755 index 000000000..e7c8b4556 --- /dev/null +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py @@ -0,0 +1,425 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads ONNX models and uses them to decode waves. +You can use the following command to get the exported models: + +We use the pre-trained model from +https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_offline/ +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/wenetspeech/ASR + +repo_url=https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless5_offline/ +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_char/Linv.pt" +git lfs pull --include "exp/pretrained_epoch_4_avg_1.pt" +git lfs pull --include "exp/cpu_jit_epoch_4_avg_1_torch.1.7.1.pt" + +cd exp +ln -s pretrained_epoch_9_avg_1_torch.1.7.1.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless5/export-onnx.py \ + --lang-dir $repo/data/lang_char \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model 0 \ + --exp-dir $repo/exp \ + --num-encoder-layers 24 \ + --dim-feedforward 1536 \ + --nhead 8 \ + --encoder-dim 384 \ + --decoder-dim 512 \ + --joiner-dim 512 + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +3. Run this file + +./pruned_transducer_stateless5/onnx_pretrained.py \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ + --tokens $repo/data/lang_char/tokens.txt \ + $repo/test_wavs/DEV_T0000000000.wav \ + $repo/test_wavs/DEV_T0000000001.wav \ + $repo/test_wavs/DEV_T0000000002.wav +""" + +import argparse +import logging +import math +from typing import List, Tuple + +import k2 +import kaldifeat +import onnxruntime as ort +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--encoder-model-filename", + type=str, + required=True, + help="Path to the encoder onnx model. ", + ) + + parser.add_argument( + "--decoder-model-filename", + type=str, + required=True, + help="Path to the decoder onnx model. ", + ) + + parser.add_argument( + "--joiner-model-filename", + type=str, + required=True, + help="Path to the joiner onnx model. ", + ) + + parser.add_argument( + "--tokens", + type=str, + help="""Path to tokens.txt.""", + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + return parser + + +class OnnxModel: + def __init__( + self, + encoder_model_filename: str, + decoder_model_filename: str, + joiner_model_filename: str, + ): + session_opts = ort.SessionOptions() + session_opts.inter_op_num_threads = 1 + session_opts.intra_op_num_threads = 4 + + self.session_opts = session_opts + + self.init_encoder(encoder_model_filename) + self.init_decoder(decoder_model_filename) + self.init_joiner(joiner_model_filename) + + def init_encoder(self, encoder_model_filename: str): + self.encoder = ort.InferenceSession( + encoder_model_filename, + sess_options=self.session_opts, + ) + + def init_decoder(self, decoder_model_filename: str): + self.decoder = ort.InferenceSession( + decoder_model_filename, + sess_options=self.session_opts, + ) + + decoder_meta = self.decoder.get_modelmeta().custom_metadata_map + self.context_size = int(decoder_meta["context_size"]) + self.vocab_size = int(decoder_meta["vocab_size"]) + + logging.info(f"context_size: {self.context_size}") + logging.info(f"vocab_size: {self.vocab_size}") + + def init_joiner(self, joiner_model_filename: str): + self.joiner = ort.InferenceSession( + joiner_model_filename, + sess_options=self.session_opts, + ) + + joiner_meta = self.joiner.get_modelmeta().custom_metadata_map + self.joiner_dim = int(joiner_meta["joiner_dim"]) + + logging.info(f"joiner_dim: {self.joiner_dim}") + + def run_encoder( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Args: + x: + A 3-D tensor of shape (N, T, C) + x_lens: + A 2-D tensor of shape (N,). Its dtype is torch.int64 + Returns: + Return a tuple containing: + - encoder_out, its shape is (N, T', joiner_dim) + - encoder_out_lens, its shape is (N,) + """ + out = self.encoder.run( + [ + self.encoder.get_outputs()[0].name, + self.encoder.get_outputs()[1].name, + ], + { + self.encoder.get_inputs()[0].name: x.numpy(), + self.encoder.get_inputs()[1].name: x_lens.numpy(), + }, + ) + return torch.from_numpy(out[0]), torch.from_numpy(out[1]) + + def run_decoder(self, decoder_input: torch.Tensor) -> torch.Tensor: + """ + Args: + decoder_input: + A 2-D tensor of shape (N, context_size) + Returns: + Return a 2-D tensor of shape (N, joiner_dim) + """ + out = self.decoder.run( + [self.decoder.get_outputs()[0].name], + {self.decoder.get_inputs()[0].name: decoder_input.numpy()}, + )[0] + + return torch.from_numpy(out) + + def run_joiner( + self, encoder_out: torch.Tensor, decoder_out: torch.Tensor + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + out = self.joiner.run( + [self.joiner.get_outputs()[0].name], + { + self.joiner.get_inputs()[0].name: encoder_out.numpy(), + self.joiner.get_inputs()[1].name: decoder_out.numpy(), + }, + )[0] + + return torch.from_numpy(out) + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +def greedy_search( + model: OnnxModel, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, +) -> List[List[int]]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (N, T, joiner_dim) + encoder_out_lens: + A 1-D tensor of shape (N,). + Returns: + Return the decoded results for each utterance. + """ + assert encoder_out.ndim == 3, encoder_out.shape + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + blank_id = 0 # hard-code to 0 + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + context_size = model.context_size + hyps = [[blank_id] * context_size for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.run_decoder(decoder_input) + + offset = 0 + for batch_size in batch_size_list: + start = offset + end = offset + batch_size + current_encoder_out = packed_encoder_out.data[start:end] + # current_encoder_out's shape: (batch_size, joiner_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + logits = model.run_joiner(current_encoder_out, decoder_out) + + # logits'shape (batch_size, vocab_size) + + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v != blank_id: + hyps[i].append(v) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + dtype=torch.int64, + ) + decoder_out = model.run_decoder(decoder_input) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + model = OnnxModel( + encoder_model_filename=args.encoder_model_filename, + decoder_model_filename=args.decoder_model_filename, + joiner_model_filename=args.joiner_model_filename, + ) + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = "cpu" + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = args.sample_rate + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, + expected_sample_rate=args.sample_rate, + ) + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence( + features, + batch_first=True, + padding_value=math.log(1e-10), + ) + + feature_lengths = torch.tensor(feature_lengths, dtype=torch.int64) + encoder_out, encoder_out_lens = model.run_encoder(features, feature_lengths) + + hyps = greedy_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + s = "\n" + + symbol_table = k2.SymbolTable.from_file(args.tokens) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += symbol_table[i] + return text.replace("▁", " ").strip() + + for filename, hyp in zip(args.sound_files, hyps): + words = token_ids_to_words(hyp) + s += f"{filename}:\n{words}\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() From 1df71a6b3801c228554d42d691ec43058bddaa39 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 23 May 2023 16:11:00 +0800 Subject: [PATCH 015/100] add onnx export for stateless2 (#1086) --- .../export-onnx.py | 517 ++++++++++++++++++ .../pruned_transducer_stateless2/export.py | 261 +-------- .../onnx_pretrained.py | 391 +------------ 3 files changed, 520 insertions(+), 649 deletions(-) create mode 100755 egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py mode change 100755 => 120000 egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_pretrained.py diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py new file mode 100755 index 000000000..fad66986b --- /dev/null +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) + +""" +This script exports a transducer model from PyTorch to ONNX. + +We use the pre-trained model from +https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless2 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/wenetspeech/ASR + +repo_url=icefall_asr_wenetspeech_pruned_transducer_stateless2 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_char/Linv.pt" +git lfs pull --include "exp/pretrained_epoch_10_avg_2.pt" + +cd exp +ln -s pretrained_epoch_10_avg_2.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./pruned_transducer_stateless2/export-onnx.py \ + --lang-dir $repo/data/lang_char \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +See ./onnx_pretrained.py for how to +use the exported ONNX models. +""" + +import argparse +import logging +from pathlib import Path +from typing import Dict, Tuple + +import onnx +import torch +import torch.nn as nn +from conformer import Conformer +from decoder import Decoder +from onnxruntime.quantization import QuantType, quantize_dynamic +from scaling_converter import convert_scaled_to_non_scaled +from train import get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import setup_logger, str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for averaging. + Note: Epoch counts from 0. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless5/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="The lang dir", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + return parser + + +def add_meta_data(filename: str, meta_data: Dict[str, str]): + """Add meta data to an ONNX model. It is changed in-place. + + Args: + filename: + Filename of the ONNX model to be changed. + meta_data: + Key-value pairs. + """ + model = onnx.load(filename) + for key, value in meta_data.items(): + meta = model.metadata_props.add() + meta.key = key + meta.value = value + + onnx.save(model, filename) + + +class OnnxEncoder(nn.Module): + """A wrapper for Conformer and the encoder_proj from the joiner""" + + def __init__(self, encoder: Conformer, encoder_proj: nn.Linear): + """ + Args: + encoder: + A Conformer encoder. + encoder_proj: + The projection layer for encoder from the joiner. + """ + super().__init__() + self.encoder = encoder + self.encoder_proj = encoder_proj + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Please see the help information of Conformer.forward + + Args: + x: + A 3-D tensor of shape (N, T, C) + x_lens: + A 1-D tensor of shape (N,). Its dtype is torch.int64 + Returns: + Return a tuple containing: + - encoder_out, A 3-D tensor of shape (N, T', joiner_dim) + - encoder_out_lens, A 1-D tensor of shape (N,) + """ + encoder_out, encoder_out_lens = self.encoder(x, x_lens) + + encoder_out = self.encoder_proj(encoder_out) + # Now encoder_out is of shape (N, T, joiner_dim) + + return encoder_out, encoder_out_lens + + +class OnnxDecoder(nn.Module): + """A wrapper for Decoder and the decoder_proj from the joiner""" + + def __init__(self, decoder: Decoder, decoder_proj: nn.Linear): + super().__init__() + self.decoder = decoder + self.decoder_proj = decoder_proj + + def forward(self, y: torch.Tensor) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, context_size). + Returns + Return a 2-D tensor of shape (N, joiner_dim) + """ + need_pad = False + decoder_output = self.decoder(y, need_pad=need_pad) + decoder_output = decoder_output.squeeze(1) + output = self.decoder_proj(decoder_output) + + return output + + +class OnnxJoiner(nn.Module): + """A wrapper for the joiner""" + + def __init__(self, output_linear: nn.Linear): + super().__init__() + self.output_linear = output_linear + + def forward( + self, + encoder_out: torch.Tensor, + decoder_out: torch.Tensor, + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + logit = encoder_out + decoder_out + logit = self.output_linear(torch.tanh(logit)) + return logit + + +def export_encoder_model_onnx( + encoder_model: OnnxEncoder, + encoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the given encoder model to ONNX format. + The exported model has two inputs: + + - x, a tensor of shape (N, T, C); dtype is torch.float32 + - x_lens, a tensor of shape (N,); dtype is torch.int64 + + and it has two outputs: + + - encoder_out, a tensor of shape (N, T', joiner_dim) + - encoder_out_lens, a tensor of shape (N,) + + Args: + encoder_model: + The input encoder model + encoder_filename: + The filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + x = torch.zeros(1, 100, 80, dtype=torch.float32) + x_lens = torch.tensor([100], dtype=torch.int64) + + torch.onnx.export( + encoder_model, + (x, x_lens), + encoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["x", "x_lens"], + output_names=["encoder_out", "encoder_out_lens"], + dynamic_axes={ + "x": {0: "N", 1: "T"}, + "x_lens": {0: "N"}, + "encoder_out": {0: "N", 1: "T"}, + "encoder_out_lens": {0: "N"}, + }, + ) + + meta_data = { + "model_type": "conformer", + "version": "1", + "model_author": "k2-fsa", + "comment": "stateless5", + } + logging.info(f"meta_data: {meta_data}") + + add_meta_data(filename=encoder_filename, meta_data=meta_data) + + +def export_decoder_model_onnx( + decoder_model: OnnxDecoder, + decoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the decoder model to ONNX format. + + The exported model has one input: + + - y: a torch.int64 tensor of shape (N, decoder_model.context_size) + + and has one output: + + - decoder_out: a torch.float32 tensor of shape (N, joiner_dim) + + Args: + decoder_model: + The decoder model to be exported. + decoder_filename: + Filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + context_size = decoder_model.decoder.context_size + vocab_size = decoder_model.decoder.vocab_size + + y = torch.zeros(10, context_size, dtype=torch.int64) + torch.onnx.export( + decoder_model, + y, + decoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["y"], + output_names=["decoder_out"], + dynamic_axes={ + "y": {0: "N"}, + "decoder_out": {0: "N"}, + }, + ) + + meta_data = { + "context_size": str(context_size), + "vocab_size": str(vocab_size), + } + add_meta_data(filename=decoder_filename, meta_data=meta_data) + + +def export_joiner_model_onnx( + joiner_model: nn.Module, + joiner_filename: str, + opset_version: int = 11, +) -> None: + """Export the joiner model to ONNX format. + The exported joiner model has two inputs: + + - encoder_out: a tensor of shape (N, joiner_dim) + - decoder_out: a tensor of shape (N, joiner_dim) + + and produces one output: + + - logit: a tensor of shape (N, vocab_size) + """ + joiner_dim = joiner_model.output_linear.weight.shape[1] + logging.info(f"joiner dim: {joiner_dim}") + + projected_encoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + projected_decoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + + torch.onnx.export( + joiner_model, + (projected_encoder_out, projected_decoder_out), + joiner_filename, + verbose=False, + opset_version=opset_version, + input_names=[ + "encoder_out", + "decoder_out", + ], + output_names=["logit"], + dynamic_axes={ + "encoder_out": {0: "N"}, + "decoder_out": {0: "N"}, + "logit": {0: "N"}, + }, + ) + meta_data = { + "joiner_dim": str(joiner_dim), + } + add_meta_data(filename=joiner_filename, meta_data=meta_data) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + setup_logger(f"{params.exp_dir}/log-export/log-export-onnx") + + logging.info(f"device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = 0 + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + model.to(device) + + if params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if start >= 0: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + + model.to("cpu") + model.eval() + + convert_scaled_to_non_scaled(model, inplace=True) + + encoder = OnnxEncoder( + encoder=model.encoder, + encoder_proj=model.joiner.encoder_proj, + ) + + decoder = OnnxDecoder( + decoder=model.decoder, + decoder_proj=model.joiner.decoder_proj, + ) + + joiner = OnnxJoiner(output_linear=model.joiner.output_linear) + + encoder_num_param = sum([p.numel() for p in encoder.parameters()]) + decoder_num_param = sum([p.numel() for p in decoder.parameters()]) + joiner_num_param = sum([p.numel() for p in joiner.parameters()]) + total_num_param = encoder_num_param + decoder_num_param + joiner_num_param + logging.info(f"encoder parameters: {encoder_num_param}") + logging.info(f"decoder parameters: {decoder_num_param}") + logging.info(f"joiner parameters: {joiner_num_param}") + logging.info(f"total parameters: {total_num_param}") + + if params.iter > 0: + suffix = f"iter-{params.iter}" + else: + suffix = f"epoch-{params.epoch}" + + suffix += f"-avg-{params.avg}" + + opset_version = 13 + + logging.info("Exporting encoder") + encoder_filename = params.exp_dir / f"encoder-{suffix}.onnx" + export_encoder_model_onnx( + encoder, + encoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported encoder to {encoder_filename}") + + logging.info("Exporting decoder") + decoder_filename = params.exp_dir / f"decoder-{suffix}.onnx" + export_decoder_model_onnx( + decoder, + decoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported decoder to {decoder_filename}") + + logging.info("Exporting joiner") + joiner_filename = params.exp_dir / f"joiner-{suffix}.onnx" + export_joiner_model_onnx( + joiner, + joiner_filename, + opset_version=opset_version, + ) + logging.info(f"Exported joiner to {joiner_filename}") + + # Generate int8 quantization models + # See https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html#data-type-selection + + logging.info("Generate int8 quantization models") + + encoder_filename_int8 = params.exp_dir / f"encoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=encoder_filename, + model_output=encoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + decoder_filename_int8 = params.exp_dir / f"decoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=decoder_filename, + model_output=decoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + joiner_filename_int8 = params.exp_dir / f"joiner-{suffix}.int8.onnx" + quantize_dynamic( + model_input=joiner_filename, + model_output=joiner_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + main() diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/export.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export.py index 8c4fbdd47..5d25daf5e 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/export.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export.py @@ -59,23 +59,7 @@ It will generate the following files: Check ./jit_pretrained.py for usage. -(3) Export to ONNX format - -./pruned_transducer_stateless2/export.py \ - --exp-dir ./pruned_transducer_stateless2/exp \ - --lang-dir data/lang_char \ - --epoch 10 \ - --avg 2 \ - --onnx 1 - -Refer to ./onnx_check.py and ./onnx_pretrained.py -for usage. - -Check -https://github.com/k2-fsa/sherpa-onnx -for how to use the exported models outside of icefall. - -(4) Export `model.state_dict()` +(3) Export `model.state_dict()` ./pruned_transducer_stateless2/export.py \ --exp-dir ./pruned_transducer_stateless2/exp \ @@ -184,23 +168,6 @@ def get_parser(): """, ) - parser.add_argument( - "--onnx", - type=str2bool, - default=False, - help="""If True, --jit is ignored and it exports the model - to onnx format. It will generate the following files: - - - encoder.onnx - - decoder.onnx - - joiner.onnx - - joiner_encoder_proj.onnx - - joiner_decoder_proj.onnx - - Refer to ./onnx_check.py and ./onnx_pretrained.py for how to use them. - """, - ) - parser.add_argument( "--context-size", type=int, @@ -333,206 +300,6 @@ def export_joiner_model_jit_trace( logging.info(f"Saved to {joiner_filename}") -def export_encoder_model_onnx( - encoder_model: nn.Module, - encoder_filename: str, - opset_version: int = 11, -) -> None: - """Export the given encoder model to ONNX format. - The exported model has two inputs: - - - x, a tensor of shape (N, T, C); dtype is torch.float32 - - x_lens, a tensor of shape (N,); dtype is torch.int64 - - and it has two outputs: - - - encoder_out, a tensor of shape (N, T, C) - - encoder_out_lens, a tensor of shape (N,) - - Note: The warmup argument is fixed to 1. - - Args: - encoder_model: - The input encoder model - encoder_filename: - The filename to save the exported ONNX model. - opset_version: - The opset version to use. - """ - x = torch.zeros(1, 100, 80, dtype=torch.float32) - x_lens = torch.tensor([100], dtype=torch.int64) - - # encoder_model = torch.jit.script(encoder_model) - # It throws the following error for the above statement - # - # RuntimeError: Exporting the operator __is_ to ONNX opset version - # 11 is not supported. Please feel free to request support or - # submit a pull request on PyTorch GitHub. - # - # I cannot find which statement causes the above error. - # torch.onnx.export() will use torch.jit.trace() internally, which - # works well for the current reworked model - warmup = 1.0 - torch.onnx.export( - encoder_model, - (x, x_lens, warmup), - encoder_filename, - verbose=False, - opset_version=opset_version, - input_names=["x", "x_lens", "warmup"], - output_names=["encoder_out", "encoder_out_lens"], - dynamic_axes={ - "x": {0: "N", 1: "T"}, - "x_lens": {0: "N"}, - "encoder_out": {0: "N", 1: "T"}, - "encoder_out_lens": {0: "N"}, - }, - ) - logging.info(f"Saved to {encoder_filename}") - - -def export_decoder_model_onnx( - decoder_model: nn.Module, - decoder_filename: str, - opset_version: int = 11, -) -> None: - """Export the decoder model to ONNX format. - - The exported model has one input: - - - y: a torch.int64 tensor of shape (N, decoder_model.context_size) - - and has one output: - - - decoder_out: a torch.float32 tensor of shape (N, 1, C) - - Note: The argument need_pad is fixed to False. - - Args: - decoder_model: - The decoder model to be exported. - decoder_filename: - Filename to save the exported ONNX model. - opset_version: - The opset version to use. - """ - y = torch.zeros(10, decoder_model.context_size, dtype=torch.int64) - need_pad = False # Always False, so we can use torch.jit.trace() here - # Note(fangjun): torch.jit.trace() is more efficient than torch.jit.script() - # in this case - torch.onnx.export( - decoder_model, - (y, need_pad), - decoder_filename, - verbose=False, - opset_version=opset_version, - input_names=["y", "need_pad"], - output_names=["decoder_out"], - dynamic_axes={ - "y": {0: "N"}, - "decoder_out": {0: "N"}, - }, - ) - logging.info(f"Saved to {decoder_filename}") - - -def export_joiner_model_onnx( - joiner_model: nn.Module, - joiner_filename: str, - opset_version: int = 11, -) -> None: - """Export the joiner model to ONNX format. - The exported joiner model has two inputs: - - - projected_encoder_out: a tensor of shape (N, joiner_dim) - - projected_decoder_out: a tensor of shape (N, joiner_dim) - - and produces one output: - - - logit: a tensor of shape (N, vocab_size) - - The exported encoder_proj model has one input: - - - encoder_out: a tensor of shape (N, encoder_out_dim) - - and produces one output: - - - projected_encoder_out: a tensor of shape (N, joiner_dim) - - The exported decoder_proj model has one input: - - - decoder_out: a tensor of shape (N, decoder_out_dim) - - and produces one output: - - - projected_decoder_out: a tensor of shape (N, joiner_dim) - """ - encoder_proj_filename = str(joiner_filename).replace(".onnx", "_encoder_proj.onnx") - - decoder_proj_filename = str(joiner_filename).replace(".onnx", "_decoder_proj.onnx") - - encoder_out_dim = joiner_model.encoder_proj.weight.shape[1] - decoder_out_dim = joiner_model.decoder_proj.weight.shape[1] - joiner_dim = joiner_model.decoder_proj.weight.shape[0] - - projected_encoder_out = torch.rand(1, joiner_dim, dtype=torch.float32) - projected_decoder_out = torch.rand(1, joiner_dim, dtype=torch.float32) - - project_input = False - # Note: It uses torch.jit.trace() internally - torch.onnx.export( - joiner_model, - (projected_encoder_out, projected_decoder_out, project_input), - joiner_filename, - verbose=False, - opset_version=opset_version, - input_names=[ - "projected_encoder_out", - "projected_decoder_out", - "project_input", - ], - output_names=["logit"], - dynamic_axes={ - "projected_encoder_out": {0: "N"}, - "projected_decoder_out": {0: "N"}, - "logit": {0: "N"}, - }, - ) - logging.info(f"Saved to {joiner_filename}") - - encoder_out = torch.rand(1, encoder_out_dim, dtype=torch.float32) - torch.onnx.export( - joiner_model.encoder_proj, - encoder_out, - encoder_proj_filename, - verbose=False, - opset_version=opset_version, - input_names=["encoder_out"], - output_names=["projected_encoder_out"], - dynamic_axes={ - "encoder_out": {0: "N"}, - "projected_encoder_out": {0: "N"}, - }, - ) - logging.info(f"Saved to {encoder_proj_filename}") - - decoder_out = torch.rand(1, decoder_out_dim, dtype=torch.float32) - torch.onnx.export( - joiner_model.decoder_proj, - decoder_out, - decoder_proj_filename, - verbose=False, - opset_version=opset_version, - input_names=["decoder_out"], - output_names=["projected_decoder_out"], - dynamic_axes={ - "decoder_out": {0: "N"}, - "projected_decoder_out": {0: "N"}, - }, - ) - logging.info(f"Saved to {decoder_proj_filename}") - - def main(): args = get_parser().parse_args() args.exp_dir = Path(args.exp_dir) @@ -573,31 +340,7 @@ def main(): model.to("cpu") model.eval() - if params.onnx is True: - convert_scaled_to_non_scaled(model, inplace=True) - opset_version = 11 - logging.info("Exporting to onnx format") - encoder_filename = params.exp_dir / "encoder.onnx" - export_encoder_model_onnx( - model.encoder, - encoder_filename, - opset_version=opset_version, - ) - - decoder_filename = params.exp_dir / "decoder.onnx" - export_decoder_model_onnx( - model.decoder, - decoder_filename, - opset_version=opset_version, - ) - - joiner_filename = params.exp_dir / "joiner.onnx" - export_joiner_model_onnx( - model.joiner, - joiner_filename, - opset_version=opset_version, - ) - elif params.jit: + if params.jit: convert_scaled_to_non_scaled(model, inplace=True) logging.info("Using torch.jit.script") # We won't use the forward() method of the model in C++, so just ignore diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_pretrained.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_pretrained.py deleted file mode 100755 index 9e34b4427..000000000 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_pretrained.py +++ /dev/null @@ -1,390 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) -# -# See ../../../../LICENSE for clarification regarding multiple authors -# -# 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 script loads ONNX models and uses them to decode waves. -You can use the following command to get the exported models: - -./pruned_transducer_stateless2/export.py \ - --exp-dir ./pruned_transducer_stateless3/exp \ - --lang-dir data/lang_char \ - --epoch 20 \ - --avg 10 \ - --onnx 1 - -Usage of this script: - -./pruned_transducer_stateless3/onnx_pretrained.py \ - --encoder-model-filename ./pruned_transducer_stateless3/exp/encoder.onnx \ - --decoder-model-filename ./pruned_transducer_stateless3/exp/decoder.onnx \ - --joiner-model-filename ./pruned_transducer_stateless3/exp/joiner.onnx \ - --joiner-encoder-proj-model-filename ./pruned_transducer_stateless3/exp/joiner_encoder_proj.onnx \ - --joiner-decoder-proj-model-filename ./pruned_transducer_stateless3/exp/joiner_decoder_proj.onnx \ - --tokens data/lang_char/tokens.txt \ - /path/to/foo.wav \ - /path/to/bar.wav - -We provide pretrained models at: -https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless2/tree/main/exp -""" - -import argparse -import logging -import math -from typing import List - -import k2 -import kaldifeat -import numpy as np - -from icefall import is_module_available - -if not is_module_available("onnxruntime"): - raise ValueError("Please 'pip install onnxruntime' first.") - -import onnxruntime as ort -import torch -import torchaudio -from torch.nn.utils.rnn import pad_sequence - - -def get_parser(): - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) - - parser.add_argument( - "--encoder-model-filename", - type=str, - required=True, - help="Path to the encoder onnx model. ", - ) - - parser.add_argument( - "--decoder-model-filename", - type=str, - required=True, - help="Path to the decoder onnx model. ", - ) - - parser.add_argument( - "--joiner-model-filename", - type=str, - required=True, - help="Path to the joiner onnx model. ", - ) - - parser.add_argument( - "--joiner-encoder-proj-model-filename", - type=str, - required=True, - help="Path to the joiner encoder_proj onnx model. ", - ) - - parser.add_argument( - "--joiner-decoder-proj-model-filename", - type=str, - required=True, - help="Path to the joiner decoder_proj onnx model. ", - ) - - parser.add_argument( - "--tokens", - type=str, - help="""Path to tokens.txt""", - ) - - parser.add_argument( - "sound_files", - type=str, - nargs="+", - help="The input sound file(s) to transcribe. " - "Supported formats are those supported by torchaudio.load(). " - "For example, wav and flac are supported. " - "The sample rate has to be 16kHz.", - ) - - parser.add_argument( - "--sample-rate", - type=int, - default=16000, - help="The sample rate of the input sound file", - ) - - parser.add_argument( - "--context-size", - type=int, - default=2, - help="Context size of the decoder model", - ) - - return parser - - -def read_sound_files( - filenames: List[str], expected_sample_rate: float -) -> List[torch.Tensor]: - """Read a list of sound files into a list 1-D float32 torch tensors. - Args: - filenames: - A list of sound filenames. - expected_sample_rate: - The expected sample rate of the sound files. - Returns: - Return a list of 1-D float32 torch tensors. - """ - ans = [] - for f in filenames: - wave, sample_rate = torchaudio.load(f) - assert ( - sample_rate == expected_sample_rate - ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" - # We use only the first channel - ans.append(wave[0]) - return ans - - -def greedy_search( - decoder: ort.InferenceSession, - joiner: ort.InferenceSession, - joiner_encoder_proj: ort.InferenceSession, - joiner_decoder_proj: ort.InferenceSession, - encoder_out: np.ndarray, - encoder_out_lens: np.ndarray, - context_size: int, -) -> List[List[int]]: - """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. - Args: - decoder: - The decoder model. - joiner: - The joiner model. - joiner_encoder_proj: - The joiner encoder projection model. - joiner_decoder_proj: - The joiner decoder projection model. - encoder_out: - A 3-D tensor of shape (N, T, C) - encoder_out_lens: - A 1-D tensor of shape (N,). - context_size: - The context size of the decoder model. - Returns: - Return the decoded results for each utterance. - """ - encoder_out = torch.from_numpy(encoder_out) - encoder_out_lens = torch.from_numpy(encoder_out_lens) - assert encoder_out.ndim == 3 - assert encoder_out.size(0) >= 1, encoder_out.size(0) - - packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( - input=encoder_out, - lengths=encoder_out_lens.cpu(), - batch_first=True, - enforce_sorted=False, - ) - - projected_encoder_out = joiner_encoder_proj.run( - [joiner_encoder_proj.get_outputs()[0].name], - {joiner_encoder_proj.get_inputs()[0].name: packed_encoder_out.data.numpy()}, - )[0] - - blank_id = 0 # hard-code to 0 - - batch_size_list = packed_encoder_out.batch_sizes.tolist() - N = encoder_out.size(0) - - assert torch.all(encoder_out_lens > 0), encoder_out_lens - assert N == batch_size_list[0], (N, batch_size_list) - - hyps = [[blank_id] * context_size for _ in range(N)] - - decoder_input_nodes = decoder.get_inputs() - decoder_output_nodes = decoder.get_outputs() - - joiner_input_nodes = joiner.get_inputs() - joiner_output_nodes = joiner.get_outputs() - - decoder_input = torch.tensor( - hyps, - dtype=torch.int64, - ) # (N, context_size) - - decoder_out = decoder.run( - [decoder_output_nodes[0].name], - { - decoder_input_nodes[0].name: decoder_input.numpy(), - }, - )[0].squeeze(1) - projected_decoder_out = joiner_decoder_proj.run( - [joiner_decoder_proj.get_outputs()[0].name], - {joiner_decoder_proj.get_inputs()[0].name: decoder_out}, - )[0] - - projected_decoder_out = torch.from_numpy(projected_decoder_out) - - offset = 0 - for batch_size in batch_size_list: - start = offset - end = offset + batch_size - current_encoder_out = projected_encoder_out[start:end] - # current_encoder_out's shape: (batch_size, encoder_out_dim) - offset = end - - projected_decoder_out = projected_decoder_out[:batch_size] - - logits = joiner.run( - [joiner_output_nodes[0].name], - { - joiner_input_nodes[0].name: current_encoder_out, - joiner_input_nodes[1].name: projected_decoder_out.numpy(), - }, - )[0] - logits = torch.from_numpy(logits).squeeze(1).squeeze(1) - # logits'shape (batch_size, vocab_size) - - assert logits.ndim == 2, logits.shape - y = logits.argmax(dim=1).tolist() - emitted = False - for i, v in enumerate(y): - if v != blank_id: - hyps[i].append(v) - emitted = True - if emitted: - # update decoder output - decoder_input = [h[-context_size:] for h in hyps[:batch_size]] - decoder_input = torch.tensor( - decoder_input, - dtype=torch.int64, - ) - decoder_out = decoder.run( - [decoder_output_nodes[0].name], - { - decoder_input_nodes[0].name: decoder_input.numpy(), - }, - )[0].squeeze(1) - projected_decoder_out = joiner_decoder_proj.run( - [joiner_decoder_proj.get_outputs()[0].name], - {joiner_decoder_proj.get_inputs()[0].name: decoder_out}, - )[0] - projected_decoder_out = torch.from_numpy(projected_decoder_out) - - sorted_ans = [h[context_size:] for h in hyps] - ans = [] - unsorted_indices = packed_encoder_out.unsorted_indices.tolist() - for i in range(N): - ans.append(sorted_ans[unsorted_indices[i]]) - - return ans - - -@torch.no_grad() -def main(): - parser = get_parser() - args = parser.parse_args() - logging.info(vars(args)) - - session_opts = ort.SessionOptions() - session_opts.inter_op_num_threads = 1 - session_opts.intra_op_num_threads = 1 - - encoder = ort.InferenceSession( - args.encoder_model_filename, - sess_options=session_opts, - ) - - decoder = ort.InferenceSession( - args.decoder_model_filename, - sess_options=session_opts, - ) - - joiner = ort.InferenceSession( - args.joiner_model_filename, - sess_options=session_opts, - ) - - joiner_encoder_proj = ort.InferenceSession( - args.joiner_encoder_proj_model_filename, - sess_options=session_opts, - ) - - joiner_decoder_proj = ort.InferenceSession( - args.joiner_decoder_proj_model_filename, - sess_options=session_opts, - ) - - logging.info("Constructing Fbank computer") - opts = kaldifeat.FbankOptions() - opts.device = "cpu" - opts.frame_opts.dither = 0 - opts.frame_opts.snip_edges = False - opts.frame_opts.samp_freq = args.sample_rate - opts.mel_opts.num_bins = 80 - - fbank = kaldifeat.Fbank(opts) - - logging.info(f"Reading sound files: {args.sound_files}") - waves = read_sound_files( - filenames=args.sound_files, - expected_sample_rate=args.sample_rate, - ) - - logging.info("Decoding started") - features = fbank(waves) - feature_lengths = [f.size(0) for f in features] - - features = pad_sequence( - features, - batch_first=True, - padding_value=math.log(1e-10), - ) - - feature_lengths = torch.tensor(feature_lengths, dtype=torch.int64) - - encoder_input_nodes = encoder.get_inputs() - encoder_out_nodes = encoder.get_outputs() - encoder_out, encoder_out_lens = encoder.run( - [encoder_out_nodes[0].name, encoder_out_nodes[1].name], - { - encoder_input_nodes[0].name: features.numpy(), - encoder_input_nodes[1].name: feature_lengths.numpy(), - }, - ) - - hyps = greedy_search( - decoder=decoder, - joiner=joiner, - joiner_encoder_proj=joiner_encoder_proj, - joiner_decoder_proj=joiner_decoder_proj, - encoder_out=encoder_out, - encoder_out_lens=encoder_out_lens, - context_size=args.context_size, - ) - symbol_table = k2.SymbolTable.from_file(args.tokens) - s = "\n" - for filename, hyp in zip(args.sound_files, hyps): - words = "".join([symbol_table[i] for i in hyp]) - s += f"{filename}:\n{words}\n\n" - logging.info(s) - - logging.info("Decoding Done") - - -if __name__ == "__main__": - formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" - - logging.basicConfig(format=formatter, level=logging.INFO) - main() diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_pretrained.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_pretrained.py new file mode 120000 index 000000000..f1bfbee49 --- /dev/null +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_pretrained.py @@ -0,0 +1 @@ +../pruned_transducer_stateless5/onnx_pretrained.py \ No newline at end of file From 6826b076d4fd965c345cd5642aa941592f98ff58 Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Wed, 24 May 2023 19:10:45 +0800 Subject: [PATCH 016/100] add flops profiler, support for Zipformer encoder and Conformer encoder (#1093) * add flops profiler, support for Zipformer encoder and Conformer encoder * support for reworked conformer and old zipformer * skip black check --- .../pruned_transducer_stateless/profile.py | 94 ++ .../pruned_transducer_stateless4/profile.py | 146 +++ .../pruned_transducer_stateless7/profile.py | 146 +++ egs/librispeech/ASR/zipformer/profile.py | 176 ++++ icefall/profiler.py | 941 ++++++++++++++++++ pyproject.toml | 1 + 6 files changed, 1504 insertions(+) create mode 100755 egs/librispeech/ASR/pruned_transducer_stateless/profile.py create mode 100755 egs/librispeech/ASR/pruned_transducer_stateless4/profile.py create mode 100755 egs/librispeech/ASR/pruned_transducer_stateless7/profile.py create mode 100755 egs/librispeech/ASR/zipformer/profile.py create mode 100644 icefall/profiler.py diff --git a/egs/librispeech/ASR/pruned_transducer_stateless/profile.py b/egs/librispeech/ASR/pruned_transducer_stateless/profile.py new file mode 100755 index 000000000..09e4a7af4 --- /dev/null +++ b/egs/librispeech/ASR/pruned_transducer_stateless/profile.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + +""" +Usage: ./pruned_transducer_stateless/profile.py +""" + +import argparse +import logging +import sentencepiece as spm +import torch + +from icefall.profiler import get_model_profile +from train import get_encoder_model, add_model_arguments, get_params + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + add_model_arguments(parser) + + return parser + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + + # We only profile the encoder part + model = get_encoder_model(params) + model.eval() + model.to(device) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # for 30-second input + B, T, D = 1, 3000, 80 + feature = torch.ones(B, T, D, dtype=torch.float32).to(device) + feature_lens = torch.full((B,), T, dtype=torch.int64).to(device) + + flops, params = get_model_profile(model=model, args=(feature, feature_lens)) + logging.info(f"For the encoder part, params: {params}, flops: {flops}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/ASR/pruned_transducer_stateless4/profile.py b/egs/librispeech/ASR/pruned_transducer_stateless4/profile.py new file mode 100755 index 000000000..252bdf060 --- /dev/null +++ b/egs/librispeech/ASR/pruned_transducer_stateless4/profile.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + +""" +Usage: ./pruned_transducer_stateless4/profile.py +""" + +import argparse +import logging +import sentencepiece as spm +import torch + +from typing import Tuple +from torch import Tensor, nn + +from icefall.profiler import get_model_profile +from scaling import BasicNorm, DoubleSwish +from train import get_encoder_model, get_joiner_model, add_model_arguments, get_params + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + add_model_arguments(parser) + + return parser + + +def _basic_norm_flops_compute(module, input, output): + assert len(input) == 1, len(input) + # estimate as layer_norm, see icefall/profiler.py + flops = input[0].numel() * 5 + module.__flops__ += int(flops) + + +def _doubleswish_module_flops_compute(module, input, output): + # For DoubleSwish + assert len(input) == 1, len(input) + # estimate as swish/silu, see icefall/profiler.py + flops = input[0].numel() + module.__flops__ += int(flops) + + +MODULE_HOOK_MAPPING = { + BasicNorm: _basic_norm_flops_compute, + DoubleSwish: _doubleswish_module_flops_compute, +} + + +class Model(nn.Module): + """A Wrapper for encoder and encoder_proj""" + + def __init__( + self, + encoder: nn.Module, + encoder_proj: nn.Module, + ) -> None: + super().__init__() + self.encoder = encoder + self.encoder_proj = encoder_proj + + def forward(self, feature: Tensor, feature_lens: Tensor) -> Tuple[Tensor, Tensor]: + encoder_out, encoder_out_lens = self.encoder(feature, feature_lens) + + logits = self.encoder_proj(encoder_out) + + return logits, encoder_out_lens + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + + # We only profile the encoder part + model = Model( + encoder=get_encoder_model(params), + encoder_proj=get_joiner_model(params).encoder_proj, + ) + model.eval() + model.to(device) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # for 30-second input + B, T, D = 1, 3000, 80 + feature = torch.ones(B, T, D, dtype=torch.float32).to(device) + feature_lens = torch.full((B,), T, dtype=torch.int64).to(device) + + flops, params = get_model_profile( + model=model, + args=(feature, feature_lens), + module_hoop_mapping=MODULE_HOOK_MAPPING, + ) + logging.info(f"For the encoder part, params: {params}, flops: {flops}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/profile.py b/egs/librispeech/ASR/pruned_transducer_stateless7/profile.py new file mode 100755 index 000000000..0d308e966 --- /dev/null +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/profile.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + +""" +Usage: ./pruned_transducer_stateless7/profile.py +""" + +import argparse +import logging +import sentencepiece as spm +import torch + +from typing import Tuple +from torch import Tensor, nn + +from icefall.profiler import get_model_profile +from scaling import BasicNorm, DoubleSwish +from train import get_encoder_model, get_joiner_model, add_model_arguments, get_params + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + add_model_arguments(parser) + + return parser + + +def _basic_norm_flops_compute(module, input, output): + assert len(input) == 1, len(input) + # estimate as layer_norm, see icefall/profiler.py + flops = input[0].numel() * 5 + module.__flops__ += int(flops) + + +def _doubleswish_module_flops_compute(module, input, output): + # For DoubleSwish + assert len(input) == 1, len(input) + # estimate as swish/silu, see icefall/profiler.py + flops = input[0].numel() + module.__flops__ += int(flops) + + +MODULE_HOOK_MAPPING = { + BasicNorm: _basic_norm_flops_compute, + DoubleSwish: _doubleswish_module_flops_compute, +} + + +class Model(nn.Module): + """A Wrapper for encoder and encoder_proj""" + + def __init__( + self, + encoder: nn.Module, + encoder_proj: nn.Module, + ) -> None: + super().__init__() + self.encoder = encoder + self.encoder_proj = encoder_proj + + def forward(self, feature: Tensor, feature_lens: Tensor) -> Tuple[Tensor, Tensor]: + encoder_out, encoder_out_lens = self.encoder(feature, feature_lens) + + logits = self.encoder_proj(encoder_out) + + return logits, encoder_out_lens + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + + # We only profile the encoder part + model = Model( + encoder=get_encoder_model(params), + encoder_proj=get_joiner_model(params).encoder_proj, + ) + model.eval() + model.to(device) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # for 30-second input + B, T, D = 1, 3000, 80 + feature = torch.ones(B, T, D, dtype=torch.float32).to(device) + feature_lens = torch.full((B,), T, dtype=torch.int64).to(device) + + flops, params = get_model_profile( + model=model, + args=(feature, feature_lens), + module_hoop_mapping=MODULE_HOOK_MAPPING, + ) + logging.info(f"For the encoder part, params: {params}, flops: {flops}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/ASR/zipformer/profile.py b/egs/librispeech/ASR/zipformer/profile.py new file mode 100755 index 000000000..b460b5338 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/profile.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + +""" +Usage: ./zipformer/profile.py +""" + +import argparse +import logging +import sentencepiece as spm +import torch + +from typing import Tuple +from torch import Tensor, nn + +from icefall.utils import make_pad_mask +from icefall.profiler import get_model_profile +from scaling import BiasNorm +from train import ( + get_encoder_embed, + get_encoder_model, + get_joiner_model, + add_model_arguments, + get_params, +) +from zipformer import BypassModule + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + add_model_arguments(parser) + + return parser + + +def _bias_norm_flops_compute(module, input, output): + assert len(input) == 1, len(input) + # estimate as layer_norm, see icefall/profiler.py + flops = input[0].numel() * 5 + module.__flops__ += int(flops) + + +def _swoosh_module_flops_compute(module, input, output): + # For SwooshL and SwooshR modules + assert len(input) == 1, len(input) + # estimate as swish/silu, see icefall/profiler.py + flops = input[0].numel() + module.__flops__ += int(flops) + + +def _bypass_module_flops_compute(module, input, output): + # For Bypass module + assert len(input) == 2, len(input) + flops = input[0].numel() * 2 + module.__flops__ += int(flops) + + +MODULE_HOOK_MAPPING = { + BiasNorm: _bias_norm_flops_compute, + BypassModule: _bypass_module_flops_compute, +} + + +class Model(nn.Module): + """A Wrapper for encoder, encoder_embed, and encoder_proj""" + + def __init__( + self, + encoder: nn.Module, + encoder_embed: nn.Module, + encoder_proj: nn.Module, + ) -> None: + super().__init__() + self.encoder = encoder + self.encoder_embed = encoder_embed + self.encoder_proj = encoder_proj + + def forward( + self, feature: Tensor, feature_lens: Tensor + ) -> Tuple[Tensor, Tensor]: + x, x_lens = self.encoder_embed(feature, feature_lens) + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = self.encoder( + x, x_lens, src_key_padding_mask + ) + + encoder_out = encoder_out.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + logits = self.encoder_proj(encoder_out) + + return logits, encoder_out_lens + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + + # We only profile the encoder part + model = Model( + encoder=get_encoder_model(params), + encoder_embed=get_encoder_embed(params), + encoder_proj=get_joiner_model(params).encoder_proj, + ) + model.eval() + model.to(device) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # for 30-second input + B, T, D = 1, 3000, 80 + feature = torch.ones(B, T, D, dtype=torch.float32).to(device) + feature_lens = torch.full((B,), T, dtype=torch.int64).to(device) + + flops, params = get_model_profile( + model=model, + args=(feature, feature_lens), + module_hoop_mapping=MODULE_HOOK_MAPPING, + ) + logging.info(f"For the encoder part, params: {params}, flops: {flops}") + + +if __name__ == "__main__": + formatter = ( + "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + ) + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/icefall/profiler.py b/icefall/profiler.py new file mode 100644 index 000000000..dc76ebebc --- /dev/null +++ b/icefall/profiler.py @@ -0,0 +1,941 @@ +# Copyright (c) Microsoft Corporation. +# SPDX-License-Identifier: Apache-2.0 + +# DeepSpeed Team + +# This is modified from https://github.com/microsoft/DeepSpeed/blob/master/deepspeed/profiling/flops_profiler/profiler.py + +import k2 +import torch +import torch.nn as nn +import torch.nn.functional as F +from functools import partial +from typing import List, Optional +from collections import OrderedDict +import numpy as np + +Tensor = torch.Tensor + +module_flop_count = [] +old_functions = {} + + +class FlopsProfiler(object): + """Measures the latency, number of estimated floating-point operations and parameters of each module in a PyTorch model. + + The flops-profiler profiles the forward pass of a PyTorch model and prints the model graph with the measured profile attached to each module. It shows how latency, flops and parameters are spent in the model and which modules or layers could be the bottleneck. It also outputs the names of the top k modules in terms of aggregated latency, flops, and parameters at depth l with k and l specified by the user. The output profile is computed for each batch of input. + + To profile a trained model in inference, use the `get_model_profile` API. + + Args: + object (torch.nn.Module): The PyTorch model to profile. + """ + + def __init__(self, model, module_hoop_mapping=None): + self.model = model + self.started = False + self.func_patched = False + self.module_hoop_mapping = ( + module_hoop_mapping + if module_hoop_mapping is not None + else MODULE_HOOK_MAPPING + ) + + def start_profile(self, ignore_list=None): + """Starts profiling. + + Extra attributes are added recursively to all the modules and the profiled torch.nn.functionals are monkey patched. + + Args: + ignore_list (list, optional): the list of modules to ignore while profiling. Defaults to None. + """ + self.reset_profile() + _patch_functionals() + _patch_tensor_methods() + + def register_module_hooks(module, ignore_list): + if ignore_list and type(module) in ignore_list: + return + + # if computing the flops of a module directly + if type(module) in self.module_hoop_mapping: + if not hasattr(module, "__flops_handle__"): + module.__flops_handle__ = module.register_forward_hook( + self.module_hoop_mapping[type(module)] + ) + return + + # if computing the flops of the functionals in a module + def pre_hook(module, input): + module_flop_count.append([]) + + if not hasattr(module, "__pre_hook_handle__"): + module.__pre_hook_handle__ = module.register_forward_pre_hook( + pre_hook + ) + + def post_hook(module, input, output): + if module_flop_count: + module.__flops__ += sum( + [elem[1] for elem in module_flop_count[-1]] + ) + module_flop_count.pop() + + if not hasattr(module, "__post_hook_handle__"): + module.__post_hook_handle__ = module.register_forward_hook( + post_hook + ) + + self.model.apply( + partial(register_module_hooks, ignore_list=ignore_list) + ) + self.started = True + self.func_patched = True + + def stop_profile(self): + """Stop profiling. + + All torch.nn.functionals are restored to their originals. + """ + if self.started and self.func_patched: + _reload_functionals() + _reload_tensor_methods() + self.func_patched = False + + def remove_profile_attrs(module): + if hasattr(module, "__pre_hook_handle__"): + module.__pre_hook_handle__.remove() + del module.__pre_hook_handle__ + if hasattr(module, "__post_hook_handle__"): + module.__post_hook_handle__.remove() + del module.__post_hook_handle__ + if hasattr(module, "__flops_handle__"): + module.__flops_handle__.remove() + del module.__flops_handle__ + + self.model.apply(remove_profile_attrs) + + def reset_profile(self): + """Resets the profiling. + + Adds or resets the extra attributes. + """ + + def add_or_reset_attrs(module): + module.__flops__ = 0 + module.__params__ = sum(p.numel() for p in module.parameters()) + + self.model.apply(add_or_reset_attrs) + + def end_profile(self): + """Ends profiling. + + The added attributes and handles are removed recursively on all the modules. + """ + if not self.started: + return + self.stop_profile() + self.started = False + + def remove_profile_attrs(module): + if hasattr(module, "__flops__"): + del module.__flops__ + if hasattr(module, "__params__"): + del module.__params__ + + self.model.apply(remove_profile_attrs) + + def get_total_flops(self, as_string=False): + """Returns the total flops of the model. + + Args: + as_string (bool, optional): whether to output the flops as string. Defaults to False. + + Returns: + The number of multiply-accumulate operations of the model forward pass. + """ + total_flops = get_module_flops(self.model) + return num_to_string(total_flops) if as_string else total_flops + + def get_total_params(self, as_string=False): + """Returns the total parameters of the model. + + Args: + as_string (bool, optional): whether to output the parameters as string. Defaults to False. + + Returns: + The number of parameters in the model. + """ + return ( + params_to_string(self.model.__params__) + if as_string + else self.model.__params__ + ) + + +def _prod(dims): + p = 1 + for v in dims: + p *= v + return p + + +def _linear_flops_compute(input, weight, bias=None): + out_features = weight.shape[0] + macs = input.numel() * out_features + return 2 * macs + + +def _relu_flops_compute(input, inplace=False): + return input.numel() + + +def _prelu_flops_compute(input: Tensor, weight: Tensor): + return input.numel() + + +def _elu_flops_compute( + input: Tensor, alpha: float = 1.0, inplace: bool = False +): + return input.numel() + + +def _leaky_relu_flops_compute( + input: Tensor, negative_slope: float = 0.01, inplace: bool = False +): + return input.numel() + + +def _relu6_flops_compute(input: Tensor, inplace: bool = False): + return input.numel() + + +def _silu_flops_compute(input: Tensor, inplace: bool = False): + return input.numel() + + +def _gelu_flops_compute(input, **kwargs): + return input.numel() + + +def _pool_flops_compute( + input, + kernel_size, + stride=None, + padding=0, + dilation=None, + ceil_mode=False, + count_include_pad=True, + divisor_override=None, + return_indices=None, +): + return input.numel() + + +def _conv_flops_compute( + input, weight, bias=None, stride=1, padding=0, dilation=1, groups=1 +): + assert weight.shape[1] * groups == input.shape[1] + + batch_size = input.shape[0] + in_channels = input.shape[1] + out_channels = weight.shape[0] + kernel_dims = list(weight.shape[2:]) + input_dims = list(input.shape[2:]) + + length = len(input_dims) + + paddings = padding if type(padding) is tuple else (padding,) * length + strides = stride if type(stride) is tuple else (stride,) * length + dilations = dilation if type(dilation) is tuple else (dilation,) * length + + output_dims = [] + for idx, input_dim in enumerate(input_dims): + output_dim = ( + input_dim + + 2 * paddings[idx] + - (dilations[idx] * (kernel_dims[idx] - 1) + 1) + ) // strides[idx] + 1 + output_dims.append(output_dim) + + filters_per_channel = out_channels // groups + conv_per_position_macs = ( + int(_prod(kernel_dims)) * in_channels * filters_per_channel + ) + active_elements_count = batch_size * int(_prod(output_dims)) + overall_conv_macs = conv_per_position_macs * active_elements_count + overall_conv_flops = 2 * overall_conv_macs + + bias_flops = 0 + if bias is not None: + bias_flops = out_channels * active_elements_count + + return int(overall_conv_flops + bias_flops) + + +def _conv_trans_flops_compute( + input, + weight, + bias=None, + stride=1, + padding=0, + output_padding=0, + groups=1, + dilation=1, +): + batch_size = input.shape[0] + in_channels = input.shape[1] + out_channels = weight.shape[0] + kernel_dims = list(weight.shape[2:]) + input_dims = list(input.shape[2:]) + + length = len(input_dims) + + paddings = padding if type(padding) is tuple else (padding,) * length + strides = stride if type(stride) is tuple else (stride,) * length + dilations = dilation if type(dilation) is tuple else (dilation,) * length + + output_dims = [] + for idx, input_dim in enumerate(input_dims): + + output_dim = ( + input_dim + + 2 * paddings[idx] + - (dilations[idx] * (kernel_dims[idx] - 1) + 1) + ) // strides[idx] + 1 + output_dims.append(output_dim) + + paddings = padding if type(padding) is tuple else (padding, padding) + strides = stride if type(stride) is tuple else (stride, stride) + dilations = dilation if type(dilation) is tuple else (dilation, dilation) + + filters_per_channel = out_channels // groups + conv_per_position_macs = ( + int(_prod(kernel_dims)) * in_channels * filters_per_channel + ) + active_elements_count = batch_size * int(_prod(input_dims)) + overall_conv_macs = conv_per_position_macs * active_elements_count + overall_conv_flops = 2 * overall_conv_macs + + bias_flops = 0 + if bias is not None: + bias_flops = out_channels * batch_size * int(_prod(output_dims)) + + return int(overall_conv_flops + bias_flops) + + +def _batch_norm_flops_compute( + input, + running_mean, + running_var, + weight=None, + bias=None, + training=False, + momentum=0.1, + eps=1e-05, +): + has_affine = weight is not None + if training: + # estimation + return input.numel() * (5 if has_affine else 4), 0 + flops = input.numel() * (2 if has_affine else 1) + return flops + + +def _layer_norm_flops_compute( + input: Tensor, + normalized_shape: List[int], + weight: Optional[Tensor] = None, + bias: Optional[Tensor] = None, + eps: float = 1e-5, +): + has_affine = weight is not None + # estimation + return input.numel() * (5 if has_affine else 4) + + +def _group_norm_flops_compute( + input: Tensor, + num_groups: int, + weight: Optional[Tensor] = None, + bias: Optional[Tensor] = None, + eps: float = 1e-5, +): + has_affine = weight is not None + # estimation + return input.numel() * (5 if has_affine else 4) + + +def _instance_norm_flops_compute( + input: Tensor, + running_mean: Optional[Tensor] = None, + running_var: Optional[Tensor] = None, + weight: Optional[Tensor] = None, + bias: Optional[Tensor] = None, + use_input_stats: bool = True, + momentum: float = 0.1, + eps: float = 1e-5, +): + has_affine = weight is not None + # estimation + return input.numel() * (5 if has_affine else 4) + + +def _upsample_flops_compute(input, **kwargs): + size = kwargs.get("size", None) + if size is not None: + if isinstance(size, tuple) or isinstance(size, list): + return int(_prod(size)), 0 + else: + return int(size), 0 + scale_factor = kwargs.get("scale_factor", None) + assert ( + scale_factor is not None + ), "either size or scale_factor should be defined" + flops = input.numel() + if isinstance(scale_factor, tuple) and len(scale_factor) == len(input): + flops * int(_prod(scale_factor)) + else: + flops * scale_factor ** len(input) + return flops + + +def _softmax_flops_compute(input, dim=None, _stacklevel=3, dtype=None): + return input.numel() + + +def _sigmoid_flops_compute(input): + return input.numel() + + +def _embedding_flops_compute( + input, + weight, + padding_idx=None, + max_norm=None, + norm_type=2.0, + scale_grad_by_freq=False, + sparse=False, +): + return 0 + + +def _dropout_flops_compute(input, p=0.5, training=True, inplace=False): + return 0 + + +def _matmul_flops_compute(input, other, *, out=None): + """ + Count flops for the matmul operation. + """ + macs = _prod(input.shape) * other.shape[-1] + return 2 * macs + + +def _addmm_flops_compute(input, mat1, mat2, *, beta=1, alpha=1, out=None): + """ + Count flops for the addmm operation. + """ + macs = _prod(mat1.shape) * mat2.shape[-1] + return 2 * macs + _prod(input.shape) + + +def _einsum_flops_compute(equation, *operands): + """ + Count flops for the einsum operation. + """ + equation = equation.replace(" ", "") + input_shapes = [o.shape for o in operands] + + # Re-map equation so that same equation with different alphabet + # representations will look the same. + letter_order = OrderedDict((k, 0) for k in equation if k.isalpha()).keys() + mapping = {ord(x): 97 + i for i, x in enumerate(letter_order)} + equation = equation.translate(mapping) + + np_arrs = [np.zeros(s) for s in input_shapes] + optim = np.einsum_path(equation, *np_arrs, optimize="optimal")[1] + for line in optim.split("\n"): + if "optimized flop" in line.lower(): + flop = int(float(line.split(":")[-1])) + return flop + raise NotImplementedError("Unsupported einsum operation.") + + +def _tensor_addmm_flops_compute(self, mat1, mat2, *, beta=1, alpha=1, out=None): + """ + Count flops for the tensor addmm operation. + """ + macs = _prod(mat1.shape) * mat2.shape[-1] + return 2 * macs + _prod(self.shape) + + +def _mul_flops_compute(input, other, *, out=None): + print("mul") + return _elementwise_flops_compute(input, other) + + +def _add_flops_compute(input, other, *, alpha=1, out=None): + print("add") + return _elementwise_flops_compute(input, other) + + +def _sum_flops_compute(input, dim, keepdim=False): + return input.numel() + + +def _elementwise_flops_compute(input, other): + if not torch.is_tensor(input): + if torch.is_tensor(other): + return _prod(other.shape) + else: + return 1 + elif not torch.is_tensor(other): + return _prod(input.shape) + else: + dim_input = len(input.shape) + dim_other = len(other.shape) + max_dim = max(dim_input, dim_other) + + final_shape = [] + for i in range(max_dim): + in_i = input.shape[i] if i < dim_input else 1 + ot_i = other.shape[i] if i < dim_other else 1 + if in_i > ot_i: + final_shape.append(in_i) + else: + final_shape.append(ot_i) + flops = _prod(final_shape) + return flops + + +def _tanh_flops_compute(input): + return input.numel() + + +def _k2_swoosh_flops_compute(input): + # For SwooshLForward and SwooshRForward + # estimate as swish/silu + return input.numel() + + +def wrapFunc(func, funcFlopCompute): + oldFunc = func + name = func.__str__ + old_functions[name] = oldFunc + + def newFunc(*args, **kwds): + flops = funcFlopCompute(*args, **kwds) + if module_flop_count: + module_flop_count[-1].append((name, flops)) + return oldFunc(*args, **kwds) + + newFunc.__str__ = func.__str__ + + return newFunc + + +def _patch_functionals(): + # FC + F.linear = wrapFunc(F.linear, _linear_flops_compute) + + # convolutions + F.conv1d = wrapFunc(F.conv1d, _conv_flops_compute) + F.conv2d = wrapFunc(F.conv2d, _conv_flops_compute) + F.conv3d = wrapFunc(F.conv3d, _conv_flops_compute) + + # conv transposed + F.conv_transpose1d = wrapFunc(F.conv_transpose1d, _conv_trans_flops_compute) + F.conv_transpose2d = wrapFunc(F.conv_transpose2d, _conv_trans_flops_compute) + F.conv_transpose3d = wrapFunc(F.conv_transpose3d, _conv_trans_flops_compute) + + # activations + F.relu = wrapFunc(F.relu, _relu_flops_compute) + F.prelu = wrapFunc(F.prelu, _prelu_flops_compute) + F.elu = wrapFunc(F.elu, _elu_flops_compute) + F.leaky_relu = wrapFunc(F.leaky_relu, _leaky_relu_flops_compute) + F.relu6 = wrapFunc(F.relu6, _relu6_flops_compute) + if hasattr(F, "silu"): + F.silu = wrapFunc(F.silu, _silu_flops_compute) + F.gelu = wrapFunc(F.gelu, _gelu_flops_compute) + + # Normalizations + F.batch_norm = wrapFunc(F.batch_norm, _batch_norm_flops_compute) + F.layer_norm = wrapFunc(F.layer_norm, _layer_norm_flops_compute) + F.instance_norm = wrapFunc(F.instance_norm, _instance_norm_flops_compute) + F.group_norm = wrapFunc(F.group_norm, _group_norm_flops_compute) + + # poolings + F.avg_pool1d = wrapFunc(F.avg_pool1d, _pool_flops_compute) + F.avg_pool2d = wrapFunc(F.avg_pool2d, _pool_flops_compute) + F.avg_pool3d = wrapFunc(F.avg_pool3d, _pool_flops_compute) + F.max_pool1d = wrapFunc(F.max_pool1d, _pool_flops_compute) + F.max_pool2d = wrapFunc(F.max_pool2d, _pool_flops_compute) + F.max_pool3d = wrapFunc(F.max_pool3d, _pool_flops_compute) + F.adaptive_avg_pool1d = wrapFunc(F.adaptive_avg_pool1d, _pool_flops_compute) + F.adaptive_avg_pool2d = wrapFunc(F.adaptive_avg_pool2d, _pool_flops_compute) + F.adaptive_avg_pool3d = wrapFunc(F.adaptive_avg_pool3d, _pool_flops_compute) + F.adaptive_max_pool1d = wrapFunc(F.adaptive_max_pool1d, _pool_flops_compute) + F.adaptive_max_pool2d = wrapFunc(F.adaptive_max_pool2d, _pool_flops_compute) + F.adaptive_max_pool3d = wrapFunc(F.adaptive_max_pool3d, _pool_flops_compute) + + # upsample + F.upsample = wrapFunc(F.upsample, _upsample_flops_compute) + F.interpolate = wrapFunc(F.interpolate, _upsample_flops_compute) + + # softmax + F.softmax = wrapFunc(F.softmax, _softmax_flops_compute) + + # sigmoid + F.sigmoid = wrapFunc(F.sigmoid, _sigmoid_flops_compute) + + # embedding + F.embedding = wrapFunc(F.embedding, _embedding_flops_compute) + + # swoosh functions in k2 + k2.swoosh_l_forward = wrapFunc( + k2.swoosh_l_forward, _k2_swoosh_flops_compute + ) + k2.swoosh_r_forward = wrapFunc( + k2.swoosh_r_forward, _k2_swoosh_flops_compute + ) + k2.swoosh_l = wrapFunc(k2.swoosh_l, _k2_swoosh_flops_compute) + k2.swoosh_r = wrapFunc(k2.swoosh_r, _k2_swoosh_flops_compute) + + +def _patch_tensor_methods(): + torch.matmul = wrapFunc(torch.matmul, _matmul_flops_compute) + torch.Tensor.matmul = wrapFunc(torch.Tensor.matmul, _matmul_flops_compute) + torch.mm = wrapFunc(torch.mm, _matmul_flops_compute) + torch.Tensor.mm = wrapFunc(torch.Tensor.mm, _matmul_flops_compute) + torch.bmm = wrapFunc(torch.bmm, _matmul_flops_compute) + torch.Tensor.bmm = wrapFunc(torch.Tensor.bmm, _matmul_flops_compute) + + torch.addmm = wrapFunc(torch.addmm, _addmm_flops_compute) + torch.Tensor.addmm = wrapFunc( + torch.Tensor.addmm, _tensor_addmm_flops_compute + ) + + torch.mul = wrapFunc(torch.mul, _mul_flops_compute) + torch.Tensor.mul = wrapFunc(torch.Tensor.mul, _mul_flops_compute) + + torch.add = wrapFunc(torch.add, _add_flops_compute) + torch.Tensor.add = wrapFunc(torch.Tensor.add, _add_flops_compute) + + torch.sum = wrapFunc(torch.sum, _sum_flops_compute) + torch.Tensor.sum = wrapFunc(torch.Tensor.sum, _sum_flops_compute) + + torch.einsum = wrapFunc(torch.einsum, _einsum_flops_compute) + + torch.baddbmm = wrapFunc(torch.baddbmm, _tensor_addmm_flops_compute) + + torch.tanh = wrapFunc(torch.tanh, _tanh_flops_compute) + + torch.Tensor.softmax = wrapFunc( + torch.Tensor.softmax, _softmax_flops_compute + ) + + torch.sigmoid = wrapFunc(torch.sigmoid, _sigmoid_flops_compute) + torch.Tensor.sigmoid = wrapFunc( + torch.Tensor.sigmoid, _sigmoid_flops_compute + ) + + +def _reload_functionals(): + # torch.nn.functional does not support importlib.reload() + F.linear = old_functions[F.linear.__str__] + F.conv1d = old_functions[F.conv1d.__str__] + F.conv2d = old_functions[F.conv2d.__str__] + F.conv3d = old_functions[F.conv3d.__str__] + F.conv_transpose1d = old_functions[F.conv_transpose1d.__str__] + F.conv_transpose2d = old_functions[F.conv_transpose2d.__str__] + F.conv_transpose3d = old_functions[F.conv_transpose3d.__str__] + F.relu = old_functions[F.relu.__str__] + F.prelu = old_functions[F.prelu.__str__] + F.elu = old_functions[F.elu.__str__] + F.leaky_relu = old_functions[F.leaky_relu.__str__] + F.relu6 = old_functions[F.relu6.__str__] + if hasattr(F, "silu"): + F.silu = old_functions[F.silu.__str__] + F.gelu = old_functions[F.gelu.__str__] + F.batch_norm = old_functions[F.batch_norm.__str__] + F.layer_norm = old_functions[F.layer_norm.__str__] + F.instance_norm = old_functions[F.instance_norm.__str__] + F.group_norm = old_functions[F.group_norm.__str__] + F.avg_pool1d = old_functions[F.avg_pool1d.__str__] + F.avg_pool2d = old_functions[F.avg_pool2d.__str__] + F.avg_pool3d = old_functions[F.avg_pool3d.__str__] + F.max_pool1d = old_functions[F.max_pool1d.__str__] + F.max_pool2d = old_functions[F.max_pool2d.__str__] + F.max_pool3d = old_functions[F.max_pool3d.__str__] + F.adaptive_avg_pool1d = old_functions[F.adaptive_avg_pool1d.__str__] + F.adaptive_avg_pool2d = old_functions[F.adaptive_avg_pool2d.__str__] + F.adaptive_avg_pool3d = old_functions[F.adaptive_avg_pool3d.__str__] + F.adaptive_max_pool1d = old_functions[F.adaptive_max_pool1d.__str__] + F.adaptive_max_pool2d = old_functions[F.adaptive_max_pool2d.__str__] + F.adaptive_max_pool3d = old_functions[F.adaptive_max_pool3d.__str__] + F.upsample = old_functions[F.upsample.__str__] + F.interpolate = old_functions[F.interpolate.__str__] + F.softmax = old_functions[F.softmax.__str__] + F.sigmoid = old_functions[F.sigmoid.__str__] + F.embedding = old_functions[F.embedding.__str__] + # swoosh functions in k2 + k2.swoosh_l = old_functions[k2.swoosh_l.__str__] + k2.swoosh_r = old_functions[k2.swoosh_r.__str__] + k2.swoosh_l_forward = old_functions[k2.swoosh_l_forward.__str__] + k2.swoosh_r_forward = old_functions[k2.swoosh_r_forward.__str__] + + +def _reload_tensor_methods(): + torch.matmul = old_functions[torch.matmul.__str__] + torch.Tensor.matmul = old_functions[torch.Tensor.matmul.__str__] + torch.mm = old_functions[torch.mm.__str__] + torch.Tensor.mm = old_functions[torch.Tensor.mm.__str__] + torch.bmm = old_functions[torch.matmul.__str__] + torch.Tensor.bmm = old_functions[torch.Tensor.bmm.__str__] + torch.addmm = old_functions[torch.addmm.__str__] + torch.Tensor.addmm = old_functions[torch.Tensor.addmm.__str__] + torch.mul = old_functions[torch.mul.__str__] + torch.Tensor.mul = old_functions[torch.Tensor.mul.__str__] + torch.add = old_functions[torch.add.__str__] + torch.Tensor.add = old_functions[torch.Tensor.add.__str__] + torch.sum = old_functions[torch.sum.__str__] + torch.Tensor.sum = old_functions[torch.Tensor.sum.__str__] + + torch.einsum = old_functions[torch.einsum.__str__] + + torch.baddbmm = old_functions[torch.baddbmm.__str__] + + torch.Tensor.softmax = old_functions[torch.Tensor.softmax.__str__] + + torch.sigmoid = old_functions[torch.sigmoid.__str__] + torch.Tensor.sigmoid = old_functions[torch.Tensor.sigmoid.__str__] + + +def _rnn_flops(flops, rnn_module, w_ih, w_hh, input_size): + # matrix matrix mult ih state and internal state + flops += w_ih.shape[0] * w_ih.shape[1] + # matrix matrix mult hh state and internal state + flops += w_hh.shape[0] * w_hh.shape[1] + if isinstance(rnn_module, (nn.RNN, nn.RNNCell)): + # add both operations + flops += rnn_module.hidden_size + elif isinstance(rnn_module, (nn.GRU, nn.GRUCell)): + # hadamard of r + flops += rnn_module.hidden_size + # adding operations from both states + flops += rnn_module.hidden_size * 3 + # last two hadamard _product and add + flops += rnn_module.hidden_size * 3 + elif isinstance(rnn_module, (nn.LSTM, nn.LSTMCell)): + # adding operations from both states + flops += rnn_module.hidden_size * 4 + # two hadamard _product and add for C state + flops += ( + rnn_module.hidden_size + + rnn_module.hidden_size + + rnn_module.hidden_size + ) + # final hadamard + flops += ( + rnn_module.hidden_size + + rnn_module.hidden_size + + rnn_module.hidden_size + ) + return flops + + +def _rnn_forward_hook(rnn_module, input, output): + flops = 0 + # input is a tuple containing a sequence to process and (optionally) hidden state + inp = input[0] + batch_size = inp.shape[0] + seq_length = inp.shape[1] + num_layers = rnn_module.num_layers + + for i in range(num_layers): + w_ih = rnn_module.__getattr__("weight_ih_l" + str(i)) + w_hh = rnn_module.__getattr__("weight_hh_l" + str(i)) + if i == 0: + input_size = rnn_module.input_size + else: + input_size = rnn_module.hidden_size + flops = _rnn_flops(flops, rnn_module, w_ih, w_hh, input_size) + if rnn_module.bias: + b_ih = rnn_module.__getattr__("bias_ih_l" + str(i)) + b_hh = rnn_module.__getattr__("bias_hh_l" + str(i)) + flops += b_ih.shape[0] + b_hh.shape[0] + + flops *= batch_size + flops *= seq_length + if rnn_module.bidirectional: + flops *= 2 + rnn_module.__flops__ += int(flops) + + +def _rnn_cell_forward_hook(rnn_cell_module, input, output): + flops = 0 + inp = input[0] + batch_size = inp.shape[0] + w_ih = rnn_cell_module.__getattr__("weight_ih") + w_hh = rnn_cell_module.__getattr__("weight_hh") + input_size = inp.shape[1] + flops = _rnn_flops(flops, rnn_cell_module, w_ih, w_hh, input_size) + if rnn_cell_module.bias: + b_ih = rnn_cell_module.__getattr__("bias_ih") + b_hh = rnn_cell_module.__getattr__("bias_hh") + flops += b_ih.shape[0] + b_hh.shape[0] + + flops *= batch_size + rnn_cell_module.__flops__ += int(flops) + + +MODULE_HOOK_MAPPING = { + # RNN + nn.RNN: _rnn_forward_hook, + nn.GRU: _rnn_forward_hook, + nn.LSTM: _rnn_forward_hook, + nn.RNNCell: _rnn_cell_forward_hook, + nn.LSTMCell: _rnn_cell_forward_hook, + nn.GRUCell: _rnn_cell_forward_hook, +} + + +def num_to_string(num, precision=2): + if num // 10**9 > 0: + return str(round(num / 10.0**9, precision)) + " G" + elif num // 10**6 > 0: + return str(round(num / 10.0**6, precision)) + " M" + elif num // 10**3 > 0: + return str(round(num / 10.0**3, precision)) + " K" + else: + return str(num) + + +def number_to_string(num, units=None, precision=2): + if units is None: + if num // 10**9 > 0: + return str(round(num / 10.0**9, precision)) + " G" + elif num // 10**6 > 0: + return str(round(num / 10.0**6, precision)) + " M" + elif num // 10**3 > 0: + return str(round(num / 10.0**3, precision)) + " K" + else: + return str(num) + " " + else: + if units == "G": + return str(round(num / 10.0**9, precision)) + " " + units + elif units == "M": + return str(round(num / 10.0**6, precision)) + " " + units + elif units == "K": + return str(round(num / 10.0**3, precision)) + " " + units + else: + return str(num) + " " + + +def flops_to_string(flops, units=None, precision=2): + if units is None: + if flops // 10**12 > 0: + return str(round(flops / 10.0**12, precision)) + " TFLOPS" + if flops // 10**9 > 0: + return str(round(flops / 10.0**9, precision)) + " GFLOPS" + elif flops // 10**6 > 0: + return str(round(flops / 10.0**6, precision)) + " MFLOPS" + elif flops // 10**3 > 0: + return str(round(flops / 10.0**3, precision)) + " KFLOPS" + else: + return str(flops) + " FLOPS" + else: + if units == "TFLOPS": + return str(round(flops / 10.0**12, precision)) + " " + units + if units == "GFLOPS": + return str(round(flops / 10.0**9, precision)) + " " + units + elif units == "MFLOPS": + return str(round(flops / 10.0**6, precision)) + " " + units + elif units == "KFLOPS": + return str(round(flops / 10.0**3, precision)) + " " + units + else: + return str(flops) + " FLOPS" + + +def params_to_string(params_num, units=None, precision=2): + if units is None: + if params_num // 10**6 > 0: + return str(round(params_num / 10**6, 2)) + " M" + elif params_num // 10**3: + return str(round(params_num / 10**3, 2)) + " k" + else: + return str(params_num) + else: + if units == "M": + return str(round(params_num / 10.0**6, precision)) + " " + units + elif units == "K": + return str(round(params_num / 10.0**3, precision)) + " " + units + else: + return str(params_num) + + +def get_module_flops(module): + sum = module.__flops__ + # iterate over immediate children modules + for child in module.children(): + sum += get_module_flops(child) + return sum + + +def get_module_duration(module): + duration = module.__duration__ + if duration == 0: # e.g. ModuleList + for m in module.children(): + duration += m.__duration__ + return duration + + +def get_model_profile( + model, + args=[], + as_string=True, + ignore_modules=None, + module_hoop_mapping=None, +): + """Returns the total floating-point operations, MACs, and parameters of a model. + + Example: + + .. code-block:: python + + model = torchvision.models.alexnet() + batch_size = 256 + flops, params = get_model_profile(model=model, args=(feature, feature_lens)) + + Args: + model ([torch.nn.Module]): the PyTorch model to be profiled. + args (list): list of positional arguments to the model. + top_modules (int, optional): the number of top modules to print in the aggregated profile. Defaults to 3. + as_string (bool, optional): whether to print the output as string. Defaults to True. + ignore_modules ([type], optional): the list of modules to ignore during profiling. Defaults to None. + + Returns: + The number of floating-point operations, multiply-accumulate operations (MACs), and parameters in the model. + """ + assert isinstance(model, nn.Module), "model must be a PyTorch module" + prof = FlopsProfiler(model, module_hoop_mapping=module_hoop_mapping) + model.eval() + + assert len(args) > 0, "input args must be specified" + + prof.start_profile(ignore_list=ignore_modules) + + _ = model(*args) + + flops = prof.get_total_flops() + params = prof.get_total_params() + + prof.end_profile() + if as_string: + return ( + number_to_string(flops), + params_to_string(params), + ) + + return flops, params diff --git a/pyproject.toml b/pyproject.toml index 650167e2f..c40143fb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,5 +12,6 @@ exclude = ''' | make_kn_lm.py | icefall\/__init__\.py | icefall\/diagnostics\.py + | icefall\/profiler\.py | egs\/librispeech\/ASR\/zipformer ''' From af8907e1ec56a1946161711e8dee37ebd20e4efa Mon Sep 17 00:00:00 2001 From: Peter Ross Date: Wed, 24 May 2023 21:57:37 +1000 Subject: [PATCH 017/100] Update pre-commit isort package to v5.11.5 (#1095) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cb213327..1bb38f6ba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: # E121,E123,E126,E226,E24,E704,W503,W504 - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.11.5 hooks: - id: isort args: ["--profile=black"] From 1aeffa73bce3d4803ec52f0d17287ff65e280430 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 25 May 2023 07:47:38 +0800 Subject: [PATCH 018/100] remove outdated code in train.py (#1096) --- egs/librispeech/ASR/transducer/train.py | 8 -------- egs/librispeech/ASR/transducer_stateless/train.py | 14 -------------- egs/librispeech/ASR/transducer_stateless2/train.py | 14 -------------- 3 files changed, 36 deletions(-) diff --git a/egs/librispeech/ASR/transducer/train.py b/egs/librispeech/ASR/transducer/train.py index 29625754e..f2a09346c 100755 --- a/egs/librispeech/ASR/transducer/train.py +++ b/egs/librispeech/ASR/transducer/train.py @@ -627,14 +627,6 @@ def run(rank, world_size, args): train_cuts = train_cuts.filter(remove_short_and_long_utt) - num_left = len(train_cuts) - num_removed = num_in_total - num_left - removed_percent = num_removed / num_in_total * 100 - - logging.info(f"Before removing short and long utterances: {num_in_total}") - logging.info(f"After removing short and long utterances: {num_left}") - logging.info(f"Removed {num_removed} utterances ({removed_percent:.5f}%)") - train_dl = librispeech.train_dataloaders(train_cuts) valid_cuts = librispeech.dev_clean_cuts() diff --git a/egs/librispeech/ASR/transducer_stateless/train.py b/egs/librispeech/ASR/transducer_stateless/train.py index 8db9b59e7..baeff6016 100755 --- a/egs/librispeech/ASR/transducer_stateless/train.py +++ b/egs/librispeech/ASR/transducer_stateless/train.py @@ -654,20 +654,6 @@ def run(rank, world_size, args): train_cuts = train_cuts.filter(remove_short_and_long_utt) - try: - num_left = len(train_cuts) - num_removed = num_in_total - num_left - removed_percent = num_removed / num_in_total * 100 - - logging.info(f"Before removing short and long utterances: {num_in_total}") - logging.info(f"After removing short and long utterances: {num_left}") - logging.info(f"Removed {num_removed} utterances ({removed_percent:.5f}%)") - except TypeError as e: - # You can ignore this error as previous versions of Lhotse work fine - # for the above code. In recent versions of Lhotse, it uses - # lazy filter, producing cutsets that don't have the __len__ method - logging.info(str(e)) - train_dl = librispeech.train_dataloaders(train_cuts) valid_cuts = librispeech.dev_clean_cuts() diff --git a/egs/librispeech/ASR/transducer_stateless2/train.py b/egs/librispeech/ASR/transducer_stateless2/train.py index 1c3a33870..cca0d0e27 100755 --- a/egs/librispeech/ASR/transducer_stateless2/train.py +++ b/egs/librispeech/ASR/transducer_stateless2/train.py @@ -642,20 +642,6 @@ def run(rank, world_size, args): train_cuts = train_cuts.filter(remove_short_and_long_utt) - try: - num_left = len(train_cuts) - num_removed = num_in_total - num_left - removed_percent = num_removed / num_in_total * 100 - - logging.info(f"Before removing short and long utterances: {num_in_total}") - logging.info(f"After removing short and long utterances: {num_left}") - logging.info(f"Removed {num_removed} utterances ({removed_percent:.5f}%)") - except TypeError as e: - # You can ignore this error as previous versions of Lhotse work fine - # for the above code. In recent versions of Lhotse, it uses - # lazy filter, producing cutsets that don't have the __len__ method - logging.info(str(e)) - train_dl = librispeech.train_dataloaders(train_cuts) valid_cuts = librispeech.dev_clean_cuts() From 7b0afbdc16066701759e088f7edbb648a0b879f0 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 30 May 2023 14:49:54 +0800 Subject: [PATCH 019/100] Remove cur_batch_idx (#1102) --- egs/aishell/ASR/pruned_transducer_stateless7/train.py | 11 ----------- .../ASR/pruned_transducer_stateless7/train2.py | 11 ----------- .../ASR/pruned_transducer_stateless7_bbpe/train.py | 11 ----------- .../ASR/pruned_transducer_stateless5/train.py | 11 ----------- .../ASR_v2/pruned_transducer_stateless7/train.py | 11 ----------- egs/ami/ASR/pruned_transducer_stateless7/train.py | 11 ----------- .../ASR/pruned_transducer_stateless7/train.py | 11 ----------- .../pruned_transducer_stateless7_streaming/train.py | 11 ----------- .../pruned_transducer_stateless7_streaming/train2.py | 11 ----------- .../ASR/pruned_transducer_stateless2/train.py | 11 ----------- egs/librispeech/ASR/conformer_ctc2/train.py | 11 ----------- .../ASR/conv_emformer_transducer_stateless/train.py | 11 ----------- .../ASR/conv_emformer_transducer_stateless2/train.py | 11 ----------- .../ASR/conv_emformer_transducer_stateless2/train2.py | 11 ----------- egs/librispeech/ASR/pruned2_knowledge/train.py | 11 ----------- .../ASR/pruned_stateless_emformer_rnnt2/train.py | 11 ----------- .../ASR/pruned_transducer_stateless7/finetune.py | 11 ----------- .../ASR/pruned_transducer_stateless7/train.py | 11 ----------- .../ASR/pruned_transducer_stateless7_ctc/train.py | 11 ----------- .../ASR/pruned_transducer_stateless7_ctc_bs/train.py | 11 ----------- .../pruned_transducer_stateless7_streaming/train.py | 11 ----------- .../pruned_transducer_stateless7_streaming/train2.py | 11 ----------- .../train.py | 5 ----- egs/librispeech/ASR/zipformer/train.py | 10 ---------- egs/librispeech/ASR/zipformer_mmi/train.py | 11 ----------- egs/mgb2/ASR/pruned_transducer_stateless5/train.py | 11 ----------- .../ASR/pruned_transducer_stateless2/train.py | 11 ----------- .../ASR/pruned_transducer_stateless5/train.py | 11 ----------- .../ASR/pruned_transducer_stateless7_bbpe/train.py | 11 ----------- .../ASR/pruned_transducer_stateless5/train.py | 11 ----------- .../ASR/pruned_transducer_stateless7/train.py | 11 ----------- 31 files changed, 334 deletions(-) diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train.py b/egs/aishell/ASR/pruned_transducer_stateless7/train.py index a8779f80f..ef536c035 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/train.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train.py @@ -577,9 +577,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -806,13 +803,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -859,7 +850,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -872,7 +862,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train2.py b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py index d9eb9a0f4..fb35a6c95 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/train2.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py @@ -580,9 +580,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -809,13 +806,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -862,7 +853,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -875,7 +865,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py b/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py index 499badb14..4e52f9573 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py @@ -567,9 +567,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -799,13 +796,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -852,7 +843,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -865,7 +855,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/aishell4/ASR/pruned_transducer_stateless5/train.py b/egs/aishell4/ASR/pruned_transducer_stateless5/train.py index d7c69f226..47015cbe7 100755 --- a/egs/aishell4/ASR/pruned_transducer_stateless5/train.py +++ b/egs/aishell4/ASR/pruned_transducer_stateless5/train.py @@ -512,9 +512,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -725,13 +722,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) # print(batch["supervisions"]) @@ -774,7 +765,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -787,7 +777,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py b/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py index 757d6535e..45d777922 100755 --- a/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py +++ b/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py @@ -554,9 +554,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -779,13 +776,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -832,7 +823,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -845,7 +835,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/ami/ASR/pruned_transducer_stateless7/train.py b/egs/ami/ASR/pruned_transducer_stateless7/train.py index 81823ced2..8c8d9593b 100755 --- a/egs/ami/ASR/pruned_transducer_stateless7/train.py +++ b/egs/ami/ASR/pruned_transducer_stateless7/train.py @@ -549,9 +549,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -770,13 +767,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -823,7 +814,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -836,7 +826,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py b/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py index 73a29a90a..4bd5b83a2 100755 --- a/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py +++ b/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py @@ -567,9 +567,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -799,13 +796,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -852,7 +843,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -865,7 +855,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py index 601de2c41..18cb75c37 100755 --- a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py +++ b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py @@ -606,9 +606,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -835,13 +832,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -889,7 +880,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -902,7 +892,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py index d1913d718..bc4bcf253 100755 --- a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py +++ b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py @@ -607,9 +607,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -836,13 +833,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -890,7 +881,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -903,7 +893,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/gigaspeech/ASR/pruned_transducer_stateless2/train.py b/egs/gigaspeech/ASR/pruned_transducer_stateless2/train.py index 578bd9218..a7772b62f 100755 --- a/egs/gigaspeech/ASR/pruned_transducer_stateless2/train.py +++ b/egs/gigaspeech/ASR/pruned_transducer_stateless2/train.py @@ -462,9 +462,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -674,13 +671,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -712,7 +703,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -725,7 +715,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/conformer_ctc2/train.py b/egs/librispeech/ASR/conformer_ctc2/train.py index 121fdb256..3366af13e 100755 --- a/egs/librispeech/ASR/conformer_ctc2/train.py +++ b/egs/librispeech/ASR/conformer_ctc2/train.py @@ -410,9 +410,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -675,13 +672,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) batch_name = batch["supervisions"]["uttid"] @@ -736,7 +727,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -749,7 +739,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py index 6bb5505aa..c5a05d349 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py @@ -550,9 +550,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -771,13 +768,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -819,7 +810,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -832,7 +822,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py index 8462ae92a..6bb37b017 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py @@ -550,9 +550,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -771,13 +768,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -819,7 +810,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -832,7 +822,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py index dd0a60736..36067510c 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py @@ -552,9 +552,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -773,13 +770,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -821,7 +812,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -834,7 +824,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned2_knowledge/train.py b/egs/librispeech/ASR/pruned2_knowledge/train.py index 123d448bb..77e06d3b7 100755 --- a/egs/librispeech/ASR/pruned2_knowledge/train.py +++ b/egs/librispeech/ASR/pruned2_knowledge/train.py @@ -444,9 +444,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -649,13 +646,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -686,7 +677,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -698,7 +688,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/train.py b/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/train.py index 3601e1e11..2b872f1d5 100755 --- a/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/train.py +++ b/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/train.py @@ -487,9 +487,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -692,13 +689,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -738,7 +729,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -750,7 +740,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py index 8151f3ba0..bac25d7b2 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py @@ -620,9 +620,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -896,13 +893,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -953,7 +944,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -966,7 +956,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py index 9c2816ae1..5be8c481d 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py @@ -578,9 +578,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -811,13 +808,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -864,7 +855,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -877,7 +867,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py index 718381baa..44823dd20 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py @@ -577,9 +577,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -830,13 +827,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -883,7 +874,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -896,7 +886,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py index ea280e642..8d6dc0881 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py @@ -570,9 +570,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -819,13 +816,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -872,7 +863,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -885,7 +875,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py index 90428133d..e58f4f820 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py @@ -586,9 +586,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -807,13 +804,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -860,7 +851,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -873,7 +863,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py index 5437e961e..dcdd2ed65 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py @@ -587,9 +587,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -808,13 +805,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -861,7 +852,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -874,7 +864,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py index 09e8a512f..37b89a626 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py @@ -604,9 +604,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -921,7 +918,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -934,7 +930,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/zipformer/train.py b/egs/librispeech/ASR/zipformer/train.py index 5af4c9b78..1f0741ba4 100755 --- a/egs/librispeech/ASR/zipformer/train.py +++ b/egs/librispeech/ASR/zipformer/train.py @@ -667,9 +667,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -895,8 +892,6 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - saved_bad_model = False def save_bad_model(suffix: str = ""): @@ -913,9 +908,6 @@ def train_one_epoch( for batch_idx, batch in enumerate(train_dl): if batch_idx % 10 == 0: set_batch_count(model, get_adjusted_batch_count(params)) - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -963,7 +955,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -976,7 +967,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/librispeech/ASR/zipformer_mmi/train.py b/egs/librispeech/ASR/zipformer_mmi/train.py index b2784e47c..c1b3ea3e0 100755 --- a/egs/librispeech/ASR/zipformer_mmi/train.py +++ b/egs/librispeech/ASR/zipformer_mmi/train.py @@ -503,9 +503,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -741,13 +738,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -797,7 +788,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -810,7 +800,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/mgb2/ASR/pruned_transducer_stateless5/train.py b/egs/mgb2/ASR/pruned_transducer_stateless5/train.py index e1b623353..a68702776 100755 --- a/egs/mgb2/ASR/pruned_transducer_stateless5/train.py +++ b/egs/mgb2/ASR/pruned_transducer_stateless5/train.py @@ -506,9 +506,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -748,15 +745,9 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): if batch["inputs"].shape[0] == len(batch["supervisions"]["text"]): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -805,7 +796,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -818,7 +808,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/spgispeech/ASR/pruned_transducer_stateless2/train.py b/egs/spgispeech/ASR/pruned_transducer_stateless2/train.py index d943180b1..a9146a0fe 100755 --- a/egs/spgispeech/ASR/pruned_transducer_stateless2/train.py +++ b/egs/spgispeech/ASR/pruned_transducer_stateless2/train.py @@ -443,9 +443,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -648,13 +645,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -685,7 +676,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -697,7 +687,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py b/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py index 43f3231ba..417515968 100755 --- a/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py @@ -513,9 +513,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -726,13 +723,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) # print(batch["supervisions"]) @@ -775,7 +766,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -788,7 +778,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py index 32db5c801..d80e0147c 100755 --- a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py @@ -566,9 +566,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -798,13 +795,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -851,7 +842,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -864,7 +854,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py index 34a72be8f..8e1b12dba 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py @@ -578,9 +578,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -794,13 +791,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -846,7 +837,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -859,7 +849,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, diff --git a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py index 1332bafd8..f8dd7b287 100755 --- a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py +++ b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py @@ -561,9 +561,6 @@ def load_checkpoint_if_available( if "cur_epoch" in saved_params: params["start_epoch"] = saved_params["cur_epoch"] - if "cur_batch_idx" in saved_params: - params["cur_batch_idx"] = saved_params["cur_batch_idx"] - return saved_params @@ -782,13 +779,7 @@ def train_one_epoch( tot_loss = MetricsTracker() - cur_batch_idx = params.get("cur_batch_idx", 0) - for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: - continue - cur_batch_idx = batch_idx - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -835,7 +826,6 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): - params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -848,7 +838,6 @@ def train_one_epoch( scaler=scaler, rank=rank, ) - del params.cur_batch_idx remove_checkpoints( out_dir=params.exp_dir, topk=params.keep_last_k, From 03853f1ee5bc71b7ff5e36289682a430a196f75e Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Wed, 31 May 2023 12:46:17 +0800 Subject: [PATCH 020/100] Add peoples_speech (#1101) * update * Small fix * Update egs/peoples_speech/ASR/prepare.sh Co-authored-by: Fangjun Kuang * limit normalize log * Update egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_valid_test.py Co-authored-by: Fangjun Kuang * Update compute_fbank_peoples_speech_splits.py * Update compute_fbank_peoples_speech_valid_test.py --------- Co-authored-by: Fangjun Kuang --- .../ASR/local/compute_fbank_musan.py | 1 + .../compute_fbank_peoples_speech_splits.py | 154 +++++++++++ ...compute_fbank_peoples_speech_valid_test.py | 93 +++++++ egs/peoples_speech/ASR/local/filter_cuts.py | 1 + .../ASR/local/prepare_lang_bpe.py | 1 + .../ASR/local/preprocess_peoples_speech.py | 123 +++++++++ .../ASR/local/train_bpe_model.py | 1 + .../ASR/local/validate_bpe_lexicon.py | 1 + egs/peoples_speech/ASR/prepare.sh | 247 ++++++++++++++++++ egs/peoples_speech/ASR/shared | 1 + 10 files changed, 623 insertions(+) create mode 120000 egs/peoples_speech/ASR/local/compute_fbank_musan.py create mode 100755 egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_splits.py create mode 100755 egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_valid_test.py create mode 120000 egs/peoples_speech/ASR/local/filter_cuts.py create mode 120000 egs/peoples_speech/ASR/local/prepare_lang_bpe.py create mode 100755 egs/peoples_speech/ASR/local/preprocess_peoples_speech.py create mode 120000 egs/peoples_speech/ASR/local/train_bpe_model.py create mode 120000 egs/peoples_speech/ASR/local/validate_bpe_lexicon.py create mode 100755 egs/peoples_speech/ASR/prepare.sh create mode 120000 egs/peoples_speech/ASR/shared diff --git a/egs/peoples_speech/ASR/local/compute_fbank_musan.py b/egs/peoples_speech/ASR/local/compute_fbank_musan.py new file mode 120000 index 000000000..5833f2484 --- /dev/null +++ b/egs/peoples_speech/ASR/local/compute_fbank_musan.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/compute_fbank_musan.py \ No newline at end of file diff --git a/egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_splits.py b/egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_splits.py new file mode 100755 index 000000000..c2ab3d07d --- /dev/null +++ b/egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_splits.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (Yifan Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 +from datetime import datetime +from pathlib import Path + +import torch +from lhotse import ( + CutSet, + KaldifeatFbank, + KaldifeatFbankConfig, + LilcomChunkyWriter, + set_audio_duration_mismatch_tolerance, + set_caching_enabled, +) + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def get_args(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--num-workers", + type=int, + default=20, + help="Number of dataloading workers used for reading the audio.", + ) + + parser.add_argument( + "--batch-duration", + type=float, + default=600.0, + help="The maximum number of audio seconds in a batch." + "Determines batch size dynamically.", + ) + + parser.add_argument( + "--num-splits", + type=int, + required=True, + help="The number of splits of the train subset", + ) + + parser.add_argument( + "--start", + type=int, + default=0, + help="Process pieces starting from this number (inclusive).", + ) + + parser.add_argument( + "--stop", + type=int, + default=-1, + help="Stop processing pieces until this number (exclusive).", + ) + + return parser.parse_args() + + +def compute_fbank_peoples_speech_splits(args): + subsets = ("dirty", "dirty_sa", "clean", "clean_sa") + num_splits = args.num_splits + output_dir = f"data/fbank/peoples_speech_train_split" + output_dir = Path(output_dir) + assert output_dir.exists(), f"{output_dir} does not exist!" + + num_digits = 8 + + start = args.start + stop = args.stop + if stop < start: + stop = num_splits + + stop = min(stop, num_splits) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + extractor = KaldifeatFbank(KaldifeatFbankConfig(device=device)) + logging.info(f"device: {device}") + + set_audio_duration_mismatch_tolerance(0.01) # 10ms tolerance + set_caching_enabled(False) + + for partition in subsets: + for i in range(start, stop): + idx = f"{i + 1}".zfill(num_digits) + logging.info(f"Processing {partition}: {idx}") + + cuts_path = output_dir / f"peoples_speech_cuts_{partition}.{idx}.jsonl.gz" + if cuts_path.is_file(): + logging.info(f"{cuts_path} exists - skipping") + continue + + raw_cuts_path = ( + output_dir / f"peoples_speech_cuts_{partition}_raw.{idx}.jsonl.gz" + ) + + logging.info(f"Loading {raw_cuts_path}") + cut_set = CutSet.from_file(raw_cuts_path) + + logging.info("Splitting cuts into smaller chunks.") + cut_set = cut_set.trim_to_supervisions( + keep_overlapping=False, min_duration=None + ) + + logging.info("Computing features") + cut_set = cut_set.compute_and_store_features_batch( + extractor=extractor, + storage_path=f"{output_dir}/peoples_speech_feats_{partition}_{idx}", + num_workers=args.num_workers, + batch_duration=args.batch_duration, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + logging.info(f"Saving to {cuts_path}") + cut_set.to_file(cuts_path) + + +def main(): + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + args = get_args() + logging.info(vars(args)) + compute_fbank_peoples_speech_splits(args) + + +if __name__ == "__main__": + main() diff --git a/egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_valid_test.py b/egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_valid_test.py new file mode 100755 index 000000000..89f43a674 --- /dev/null +++ b/egs/peoples_speech/ASR/local/compute_fbank_peoples_speech_valid_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Yifan Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the People's Speech dataset. +It looks for manifests in the directory data/manifests. + +The generated fbank features are saved in data/fbank. +""" + +import argparse +import logging +import os +from pathlib import Path +from typing import Optional + +import torch +from filter_cuts import filter_cuts +from lhotse import CutSet, KaldifeatFbank, KaldifeatFbankConfig, LilcomChunkyWriter + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def compute_fbank_peoples_speech_valid_test(): + src_dir = Path(f"data/manifests") + output_dir = Path(f"data/fbank") + num_workers = 42 + batch_duration = 600 + + subsets = ("validation", "test") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + extractor = KaldifeatFbank(KaldifeatFbankConfig(device=device)) + + logging.info(f"device: {device}") + + for partition in subsets: + cuts_path = output_dir / f"peoples_speech_cuts_{partition}.jsonl.gz" + if cuts_path.is_file(): + logging.info(f"{partition} already exists - skipping.") + continue + + raw_cuts_path = output_dir / f"peoples_speech_cuts_{partition}_raw.jsonl.gz" + + logging.info(f"Loading {raw_cuts_path}") + cut_set = CutSet.from_file(raw_cuts_path) + + logging.info("Splitting cuts into smaller chunks") + cut_set = cut_set.trim_to_supervisions( + keep_overlapping=False, min_duration=None + ) + + logging.info("Computing features") + cut_set = cut_set.compute_and_store_features_batch( + extractor=extractor, + storage_path=f"{output_dir}/peoples_speech_feats_{partition}", + num_workers=num_workers, + batch_duration=batch_duration, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + logging.info(f"Saving to {cuts_path}") + cut_set.to_file(cuts_path) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + compute_fbank_peoples_speech_valid_test() diff --git a/egs/peoples_speech/ASR/local/filter_cuts.py b/egs/peoples_speech/ASR/local/filter_cuts.py new file mode 120000 index 000000000..27aca1729 --- /dev/null +++ b/egs/peoples_speech/ASR/local/filter_cuts.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/filter_cuts.py \ No newline at end of file diff --git a/egs/peoples_speech/ASR/local/prepare_lang_bpe.py b/egs/peoples_speech/ASR/local/prepare_lang_bpe.py new file mode 120000 index 000000000..36b40e7fc --- /dev/null +++ b/egs/peoples_speech/ASR/local/prepare_lang_bpe.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/prepare_lang_bpe.py \ No newline at end of file diff --git a/egs/peoples_speech/ASR/local/preprocess_peoples_speech.py b/egs/peoples_speech/ASR/local/preprocess_peoples_speech.py new file mode 100755 index 000000000..c5417049f --- /dev/null +++ b/egs/peoples_speech/ASR/local/preprocess_peoples_speech.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Yifan Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 re +from pathlib import Path +from typing import Optional + +from lhotse import CutSet, SupervisionSegment +from lhotse.recipes.utils import read_manifests_if_cached + + +def get_args(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--dataset", + type=str, + help="""Dataset parts to compute fbank. If None, we will use all""", + ) + + return parser.parse_args() + + +def normalize_text(utt: str) -> str: + utt = re.sub(r"[{0}]+".format("-"), " ", utt) + return re.sub(r"[^a-zA-Z\s]", "", utt).upper() + + +def preprocess_peoples_speech(dataset: Optional[str] = None): + src_dir = Path(f"data/manifests") + output_dir = Path(f"data/fbank") + output_dir.mkdir(exist_ok=True) + + if dataset is None: + dataset_parts = ( + "validation", + "test", + "dirty", + "dirty_sa", + "clean", + "clean_sa", + ) + else: + dataset_parts = dataset.split(" ", -1) + + logging.info("Loading manifest, it may takes 8 minutes") + prefix = f"peoples_speech" + suffix = "jsonl.gz" + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + suffix=suffix, + prefix=prefix, + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + + for partition, m in manifests.items(): + logging.info(f"Processing {partition}") + raw_cuts_path = output_dir / f"{prefix}_cuts_{partition}_raw.{suffix}" + if raw_cuts_path.is_file(): + logging.info(f"{partition} already exists - skipping") + continue + + logging.info(f"Normalizing text in {partition}") + i = 0 + for sup in m["supervisions"]: + text = str(sup.text) + orig_text = text + sup.text = normalize_text(sup.text) + text = str(sup.text) + if i < 10 and len(orig_text) != len(text): + logging.info( + f"\nOriginal text vs normalized text:\n{orig_text}\n{text}" + ) + i += 1 + + # Create long-recording cut manifests. + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ).resample(16000) + + # Run data augmentation that needs to be done in the + # time domain. + logging.info(f"Saving to {raw_cuts_path}") + cut_set.to_file(raw_cuts_path) + + +def main(): + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + args = get_args() + logging.info(vars(args)) + preprocess_peoples_speech(dataset=args.dataset) + logging.info("Done") + + +if __name__ == "__main__": + main() diff --git a/egs/peoples_speech/ASR/local/train_bpe_model.py b/egs/peoples_speech/ASR/local/train_bpe_model.py new file mode 120000 index 000000000..6fad36421 --- /dev/null +++ b/egs/peoples_speech/ASR/local/train_bpe_model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/train_bpe_model.py \ No newline at end of file diff --git a/egs/peoples_speech/ASR/local/validate_bpe_lexicon.py b/egs/peoples_speech/ASR/local/validate_bpe_lexicon.py new file mode 120000 index 000000000..721bb48e7 --- /dev/null +++ b/egs/peoples_speech/ASR/local/validate_bpe_lexicon.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/validate_bpe_lexicon.py \ No newline at end of file diff --git a/egs/peoples_speech/ASR/prepare.sh b/egs/peoples_speech/ASR/prepare.sh new file mode 100755 index 000000000..3787858d9 --- /dev/null +++ b/egs/peoples_speech/ASR/prepare.sh @@ -0,0 +1,247 @@ +#!/usr/bin/env bash + +set -eou pipefail + +nj=32 +stage=-1 +stop_stage=100 + +# Split data/set to a number of pieces +# This is to avoid OOM during feature extraction. +num_per_split=4000 + +# We assume dl_dir (download dir) contains the following +# directories and files. If not, they will be downloaded +# by this script automatically. +# +# - $dl_dir/peoples_speech +# This directory contains the following files downloaded from +# https://huggingface.co/datasets/MLCommons/peoples_speech +# +# - test +# - train +# - validation +# +# - $dl_dir/musan +# This directory contains the following directories downloaded from +# http://www.openslr.org/17/ +# +# - music +# - noise +# - speech + +dl_dir=$PWD/download + +. shared/parse_options.sh || exit 1 + +# vocab size for sentence piece models. +# It will generate data/lang_bpe_xxx, +# data/lang_bpe_yyy if the array contains xxx, yyy +vocab_sizes=( + # 5000 + # 2000 + # 1000 + 500 +) + +# All files generated by this script are saved in "data". +# You can safely remove "data" and rerun this script to regenerate it. +mkdir -p data + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +log "dl_dir: $dl_dir" + +if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then + log "Stage 0: Download data" + + # If you have pre-downloaded it to /path/to/peoples_speech, + # you can create a symlink + # + # ln -sfv /path/to/peoples_speech $dl_dir/peoples_speech + # + if [ ! -d $dl_dir/peoples_speech/train ]; then + git lfs install + git clone https://huggingface.co/datasets/MLCommons/peoples_speech + fi + + # If you have pre-downloaded it to /path/to/musan, + # you can create a symlink + # + # ln -sfv /path/to/musan $dl_dir/ + # + if [ ! -d $dl_dir/musan ]; then + lhotse download musan $dl_dir + fi +fi + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + log "Stage 1: Prepare People's Speech manifest" + # We assume that you have downloaded the People's Speech corpus + # to $dl_dir/peoples_speech + mkdir -p data/manifests + if [ ! -e data/manifests/.peoples_speech.done ]; then + lhotse prepare peoples-speech -j $nj $dl_dir/peoples_speech data/manifests + touch data/manifests/.peoples_speech.done + fi +fi + +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + log "Stage 2: Prepare musan manifest" + # We assume that you have downloaded the musan corpus + # to data/musan + mkdir -p data/manifests + if [ ! -e data/manifests/.musan.done ]; then + lhotse prepare musan $dl_dir/musan data/manifests + touch data/manifests/.musan.done + fi +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + log "Stage 3: Preprocess People's Speech manifest" + mkdir -p data/fbank + if [ ! -e data/fbank/.preprocess_complete ]; then + ./local/preprocess_peoples_speech.py + touch data/fbank/.preprocess_complete + fi +fi + +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + log "Stage 4: Compute fbank for valid and test subsets of People's Speech" + if [ ! -e data/fbank/.peoples_speech_valid_test.done ]; then + ./local/compute_fbank_peoples_speech_valid_test.py + touch data/fbank/.peoples_speech_valid_test.done + fi +fi + +if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then + log "Stage 5: Split train subset into pieces" + split_dir=data/fbank/peoples_speech_train_split + if [ ! -e $split_dir/.peoples_speech_dirty_split.done ]; then + lhotse split-lazy ./data/fbank/peoples_speech_cuts_dirty_raw.jsonl.gz $split_dir $num_per_split + touch $split_dir/.peoples_speech_dirty_split.done + fi + + if [ ! -e $split_dir/.peoples_speech_dirty_sa_split.done ]; then + lhotse split-lazy ./data/fbank/peoples_speech_cuts_dirty_sa_raw.jsonl.gz $split_dir $num_per_split + touch $split_dir/.peoples_speech_dirty_sa_split.done + fi + + if [ ! -e $split_dir/.peoples_speech_clean_split.done ]; then + lhotse split-lazy ./data/fbank/peoples_speech_cuts_clean_raw.jsonl.gz $split_dir $num_per_split + touch $split_dir/.peoples_speech_clean_split.done + fi + + if [ ! -e $split_dir/.peoples_speech_clean_sa_split.done ]; then + lhotse split-lazy ./data/fbank/peoples_speech_cuts_clean_sa_raw.jsonl.gz $split_dir $num_per_split + touch $split_dir/.peoples_speech_clean_sa_split.done + fi +fi + +if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then + log "Stage 6: Compute features for train subset of People's Speech" + if [ ! -e data/fbank/.peoples_speech_train.done ]; then + ./local/compute_fbank_peoples_speech_splits.py \ + --num-workers $nj \ + --batch-duration 600 \ + --start 0 \ + --num-splits 2000 + touch data/fbank/.peoples_speech_train.done + fi +fi + +if [ $stage -le 7 ] && [ $stop_stage -ge 7 ]; then + log "Stage 7: Compute fbank for musan" + mkdir -p data/fbank + if [ ! -e data/fbank/.musan.done ]; then + ./local/compute_fbank_musan.py + touch data/fbank/.musan.done + fi +fi + +if [ $stage -le 8 ] && [ $stop_stage -ge 8 ]; then + log "Stage 8: Prepare BPE based lang" + + for vocab_size in ${vocab_sizes[@]}; do + lang_dir=data/lang_bpe_${vocab_size} + mkdir -p $lang_dir + + if [ ! -f $lang_dir/transcript_words.txt ]; then + log "Generate data for BPE training" + file=$( + find "data/fbank/peoples_speech_cuts_dirty_raw.jsonl.gz" + find "data/fbank/peoples_speech_cuts_dirty_sa_raw.jsonl.gz" + find "data/fbank/peoples_speech_cuts_clean_raw.jsonl.gz" + find "data/fbank/peoples_speech_cuts_clean_sa_raw.jsonl.gz" + ) + gunzip -c ${file} | awk -F '"' '{print $30}' > $lang_dir/transcript_words.txt + + # Ensure space only appears once + sed -i 's/\t/ /g' $lang_dir/transcript_words.txt + sed -i 's/ +/ /g' $lang_dir/transcript_words.txt + fi + + if [ ! -f $lang_dir/words.txt ]; then + cat $lang_dir/transcript_words.txt | sed 's/ /\n/g' \ + | sort -u | sed '/^$/d' > $lang_dir/words.txt + (echo '!SIL'; echo ''; echo ''; ) | + cat - $lang_dir/words.txt | sort | uniq | awk ' + BEGIN { + print " 0"; + } + { + if ($1 == "") { + print " is in the vocabulary!" | "cat 1>&2" + exit 1; + } + if ($1 == "") { + print " is in the vocabulary!" | "cat 1>&2" + exit 1; + } + printf("%s %d\n", $1, NR); + } + END { + printf("#0 %d\n", NR+1); + printf(" %d\n", NR+2); + printf(" %d\n", NR+3); + }' > $lang_dir/words || exit 1; + mv $lang_dir/words $lang_dir/words.txt + fi + + if [ ! -f $lang_dir/bpe.model ]; then + ./local/train_bpe_model.py \ + --lang-dir $lang_dir \ + --vocab-size $vocab_size \ + --transcript $lang_dir/transcript_words.txt + fi + + if [ ! -f $lang_dir/L_disambig.pt ]; then + ./local/prepare_lang_bpe.py --lang-dir $lang_dir + + log "Validating $lang_dir/lexicon.txt" + ./local/validate_bpe_lexicon.py \ + --lexicon $lang_dir/lexicon.txt \ + --bpe-model $lang_dir/bpe.model + fi + + if [ ! -f $lang_dir/L.fst ]; then + log "Converting L.pt to L.fst" + ./shared/convert-k2-to-openfst.py \ + --olabels aux_labels \ + $lang_dir/L.pt \ + $lang_dir/L.fst + fi + + if [ ! -f $lang_dir/L_disambig.fst ]; then + log "Converting L_disambig.pt to L_disambig.fst" + ./shared/convert-k2-to-openfst.py \ + --olabels aux_labels \ + $lang_dir/L_disambig.pt \ + $lang_dir/L_disambig.fst + fi + done +fi diff --git a/egs/peoples_speech/ASR/shared b/egs/peoples_speech/ASR/shared new file mode 120000 index 000000000..4c5e91438 --- /dev/null +++ b/egs/peoples_speech/ASR/shared @@ -0,0 +1 @@ +../../../icefall/shared/ \ No newline at end of file From 7a604057f9c99dc5f064ed6557d17f2ba95c518a Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Thu, 1 Jun 2023 14:24:19 +0800 Subject: [PATCH 021/100] update diagnostics, print limits in Balancer, merge changes from Dan's branch zlm59 (#1109) --- icefall/diagnostics.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/icefall/diagnostics.py b/icefall/diagnostics.py index 51e816105..98870684e 100644 --- a/icefall/diagnostics.py +++ b/icefall/diagnostics.py @@ -498,6 +498,22 @@ class ModelDiagnostic(object): self.diagnostics[k].print_diagnostics() +def get_class_name(module: nn.Module): + ans = type(module).__name__ + # we put the below in try blocks in case anyone is using a different version of these modules that + # might have different member names. + if ans == 'Balancer' or ans == 'ActivationBalancer': + try: + ans += f'[{float(module.min_positive)},{float(module.max_positive)},{float(module.min_abs)},{float(module.max_abs)}]' + except: + pass + elif ans == 'AbsValuePenalizer': + try: + ans += f'[{module.limit}]' + except: + pass + return ans + def attach_diagnostics( model: nn.Module, opts: Optional[TensorDiagnosticOptions] = None ) -> ModelDiagnostic: @@ -537,12 +553,12 @@ def attach_diagnostics( if isinstance(_output, Tensor) and _output.dtype in ( torch.float32, torch.float16, torch.float64 ): _model_diagnostic[f"{_name}.output"].accumulate(_output, - class_name=type(_module).__name__) + class_name=get_class_name(_module)) elif isinstance(_output, tuple): for i, o in enumerate(_output): if o.dtype in ( torch.float32, torch.float16, torch.float64 ): _model_diagnostic[f"{_name}.output[{i}]"].accumulate(o, - class_name=type(_module).__name__) + class_name=get_class_name(_module)) def backward_hook( _module, _input, _output, _model_diagnostic=ans, _name=name @@ -551,12 +567,12 @@ def attach_diagnostics( _output = _output[0] if isinstance(_output, Tensor) and _output.dtype in ( torch.float32, torch.float16, torch.float64 ): _model_diagnostic[f"{_name}.grad"].accumulate(_output, - class_name=type(_module).__name__) + class_name=get_class_name(_module)) elif isinstance(_output, tuple): for i, o in enumerate(_output): if o.dtype in ( torch.float32, torch.float16, torch.float64 ): _model_diagnostic[f"{_name}.grad[{i}]"].accumulate(o, - class_name=type(_module).__name__) + class_name=get_class_name(_module)) module.register_forward_hook(forward_hook) @@ -574,7 +590,7 @@ def attach_diagnostics( _input, = _input assert isinstance(_input, Tensor) _model_diagnostic[f"{_name}.scalar"].accumulate_input(_input, - class_name=type(_module).__name__) + class_name=get_class_name(_module)) def scalar_backward_hook( _module, _input, _output, _model_diagnostic=ans, _name=name From 82f34a238875188ae86552c2655026cc14b41c76 Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Thu, 1 Jun 2023 18:45:20 +0800 Subject: [PATCH 022/100] Remove multidataset from librispeech/pruned_transducer_stateless7 (#1105) * Add People's Speech to multidataset * update * remove multi from librispeech --- egs/librispeech/ASR/prepare_common_voice.sh | 117 ------- egs/librispeech/ASR/prepare_giga_speech.sh | 159 --------- egs/librispeech/ASR/prepare_multidataset.sh | 330 ------------------ .../multidataset.py | 77 ---- .../ASR/pruned_transducer_stateless7/train.py | 24 +- egs/librispeech/ASR/zipformer/train.py | 147 ++++---- 6 files changed, 74 insertions(+), 780 deletions(-) delete mode 100755 egs/librispeech/ASR/prepare_common_voice.sh delete mode 100755 egs/librispeech/ASR/prepare_giga_speech.sh delete mode 100755 egs/librispeech/ASR/prepare_multidataset.sh delete mode 100644 egs/librispeech/ASR/pruned_transducer_stateless7/multidataset.py diff --git a/egs/librispeech/ASR/prepare_common_voice.sh b/egs/librispeech/ASR/prepare_common_voice.sh deleted file mode 100755 index 6f9c4fb2f..000000000 --- a/egs/librispeech/ASR/prepare_common_voice.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash - -set -eou pipefail - -nj=16 -stage=-1 -stop_stage=100 - -# Split data/${lang}set to this number of pieces -# This is to avoid OOM during feature extraction. -num_splits=1000 - -# We assume dl_dir (download dir) contains the following -# directories and files. If not, they will be downloaded -# by this script automatically. -# -# - $dl_dir/$release/$lang -# This directory contains the following files downloaded from -# https://mozilla-common-voice-datasets.s3.dualstack.us-west-2.amazonaws.com/${release}/${release}-${lang}.tar.gz -# -# - clips -# - dev.tsv -# - invalidated.tsv -# - other.tsv -# - reported.tsv -# - test.tsv -# - train.tsv -# - validated.tsv - -dl_dir=$PWD/download -release=cv-corpus-13.0-2023-03-09 -lang=en - -. shared/parse_options.sh || exit 1 - -# All files generated by this script are saved in "data/${lang}". -# You can safely remove "data/${lang}" and rerun this script to regenerate it. -mkdir -p data/${lang} - -log() { - # This function is from espnet - local fname=${BASH_SOURCE[1]##*/} - echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" -} - -log "dl_dir: $dl_dir" - -if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then - log "Stage 0: Download data" - - # If you have pre-downloaded it to /path/to/$release, - # you can create a symlink - # - # ln -sfv /path/to/$release $dl_dir/$release - # - if [ ! -d $dl_dir/$release/$lang/clips ]; then - lhotse download commonvoice --languages $lang --release $release $dl_dir - fi -fi - -if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then - log "Stage 1: Prepare CommonVoice manifest" - # We assume that you have downloaded the CommonVoice corpus - # to $dl_dir/$release - mkdir -p data/${lang}/manifests - if [ ! -e data/${lang}/manifests/.cv-${lang}.done ]; then - lhotse prepare commonvoice --language $lang -j $nj $dl_dir/$release data/${lang}/manifests - touch data/${lang}/manifests/.cv-${lang}.done - fi -fi - -if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then - log "Stage 2: Preprocess CommonVoice manifest" - if [ ! -e data/${lang}/fbank/.preprocess_complete ]; then - ./local/preprocess_commonvoice.py --language $lang - touch data/${lang}/fbank/.preprocess_complete - fi -fi - -if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then - log "Stage 3: Compute fbank for dev and test subsets of CommonVoice" - mkdir -p data/${lang}/fbank - if [ ! -e data/${lang}/fbank/.cv-${lang}_dev_test.done ]; then - ./local/compute_fbank_commonvoice_dev_test.py --language $lang - touch data/${lang}/fbank/.cv-${lang}_dev_test.done - fi -fi - -if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then - log "Stage 4: Split train subset into ${num_splits} pieces" - split_dir=data/${lang}/fbank/cv-${lang}_train_split_${num_splits} - if [ ! -e $split_dir/.cv-${lang}_train_split.done ]; then - lhotse split $num_splits ./data/${lang}/fbank/cv-${lang}_cuts_train_raw.jsonl.gz $split_dir - touch $split_dir/.cv-${lang}_train_split.done - fi -fi - -if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then - log "Stage 5: Compute features for train subset of CommonVoice" - if [ ! -e data/${lang}/fbank/.cv-${lang}_train.done ]; then - ./local/compute_fbank_commonvoice_splits.py \ - --num-workers $nj \ - --batch-duration 600 \ - --start 0 \ - --num-splits $num_splits \ - --language $lang - touch data/${lang}/fbank/.cv-${lang}_train.done - fi -fi - -if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then - log "Stage 6: Combine features for train" - if [ ! -f data/${lang}/fbank/cv-${lang}_cuts_train.jsonl.gz ]; then - pieces=$(find data/${lang}/fbank/cv-${lang}_train_split_${num_splits} -name "cv-${lang}_cuts_train.*.jsonl.gz") - lhotse combine $pieces data/${lang}/fbank/cv-${lang}_cuts_train.jsonl.gz - fi -fi diff --git a/egs/librispeech/ASR/prepare_giga_speech.sh b/egs/librispeech/ASR/prepare_giga_speech.sh deleted file mode 100755 index b077aaf3a..000000000 --- a/egs/librispeech/ASR/prepare_giga_speech.sh +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env bash - -set -eou pipefail - -nj=15 -stage=-1 -stop_stage=100 - -# We assume dl_dir (download dir) contains the following -# directories and files. If not, they will be downloaded -# by this script automatically. -# -# - $dl_dir/GigaSpeech -# You can find audio, dict, GigaSpeech.json inside it. -# You can apply for the download credentials by following -# https://github.com/SpeechColab/GigaSpeech#download - -# Number of hours for GigaSpeech subsets -# XL 10k hours -# L 2.5k hours -# M 1k hours -# S 250 hours -# XS 10 hours -# DEV 12 hours -# Test 40 hours - -# Split XL subset to this number of pieces -# This is to avoid OOM during feature extraction. -num_splits=2000 -# We use lazy split from lhotse. -# The XL subset (10k hours) contains 37956 cuts without speed perturbing. -# We want to split it into 2000 splits, so each split -# contains about 37956 / 2000 = 19 cuts. As a result, there will be 1998 splits. -chunk_size=19 # number of cuts in each split. The last split may contain fewer cuts. - -dl_dir=$PWD/download - -. shared/parse_options.sh || exit 1 - -# All files generated by this script are saved in "data". -# You can safely remove "data" and rerun this script to regenerate it. -mkdir -p data - -log() { - # This function is from espnet - local fname=${BASH_SOURCE[1]##*/} - echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" -} - -log "dl_dir: $dl_dir" - -if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then - log "Stage 0: Download data" - - [ ! -e $dl_dir/GigaSpeech ] && mkdir -p $dl_dir/GigaSpeech - - # If you have pre-downloaded it to /path/to/GigaSpeech, - # you can create a symlink - # - # ln -sfv /path/to/GigaSpeech $dl_dir/GigaSpeech - # - if [ ! -d $dl_dir/GigaSpeech/audio ] && [ ! -f $dl_dir/GigaSpeech.json ]; then - # Check credentials. - if [ ! -f $dl_dir/password ]; then - echo -n "$0: Please apply for the download credentials by following" - echo -n "https://github.com/SpeechColab/GigaSpeech#dataset-download" - echo " and save it to $dl_dir/password." - exit 1; - fi - PASSWORD=`cat $dl_dir/password 2>/dev/null` - if [ -z "$PASSWORD" ]; then - echo "$0: Error, $dl_dir/password is empty." - exit 1; - fi - PASSWORD_MD5=`echo $PASSWORD | md5sum | cut -d ' ' -f 1` - if [[ $PASSWORD_MD5 != "dfbf0cde1a3ce23749d8d81e492741b8" ]]; then - echo "$0: Error, invalid $dl_dir/password." - exit 1; - fi - # Download XL, DEV and TEST sets by default. - lhotse download gigaspeech \ - --subset XL \ - --subset L \ - --subset M \ - --subset S \ - --subset XS \ - --subset DEV \ - --subset TEST \ - --host tsinghua \ - $dl_dir/password $dl_dir/GigaSpeech - fi -fi - -if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then - log "Stage 1: Prepare GigaSpeech manifest (may take 30 minutes)" - # We assume that you have downloaded the GigaSpeech corpus - # to $dl_dir/GigaSpeech - if [ ! -f data/manifests/.gigaspeech.done ]; then - mkdir -p data/manifests - lhotse prepare gigaspeech \ - --subset XL \ - --subset L \ - --subset M \ - --subset S \ - --subset XS \ - --subset DEV \ - --subset TEST \ - -j $nj \ - $dl_dir/GigaSpeech data/manifests - touch data/manifests/.gigaspeech.done - fi -fi - -if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then - log "Stage 2: Preprocess GigaSpeech manifest" - if [ ! -f data/fbank/.gigaspeech_preprocess.done ]; then - log "It may take 2 hours for this stage" - ./local/preprocess_gigaspeech.py - touch data/fbank/.gigaspeech_preprocess.done - fi -fi - -if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then - log "Stage 3: Compute features for DEV and TEST subsets of GigaSpeech (may take 2 minutes)" - if [ ! -f data/fbank/.gigaspeech_dev_test.done ]; then - ./local/compute_fbank_gigaspeech_dev_test.py - touch data/fbank/.gigaspeech_dev_test.done - fi -fi - -if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then - log "Stage 4: Split XL subset into ${num_splits} pieces" - split_dir=data/fbank/gigaspeech_XL_split_${num_splits} - if [ ! -f $split_dir/.gigaspeech_XL_split.done ]; then - lhotse split-lazy ./data/fbank/gigaspeech_cuts_XL_raw.jsonl.gz $split_dir $chunk_size - touch $split_dir/.gigaspeech_XL_split.done - fi -fi - -if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then - log "Stage 5: Compute features for XL" - # Note: The script supports --start and --stop options. - # You can use several machines to compute the features in parallel. - if [ ! -f data/fbank/.gigaspeech_XL.done ]; then - ./local/compute_fbank_gigaspeech_splits.py \ - --num-workers $nj \ - --batch-duration 600 \ - --num-splits $num_splits - touch data/fbank/.gigaspeech_XL.done - fi -fi - -if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then - log "Stage 6: Combine features for XL (may take 15 hours)" - if [ ! -f data/fbank/gigaspeech_cuts_XL.jsonl.gz ]; then - pieces=$(find data/fbank/gigaspeech_XL_split_${num_splits} -name "gigaspeech_cuts_XL.*.jsonl.gz") - lhotse combine $pieces data/fbank/gigaspeech_cuts_XL.jsonl.gz - fi -fi diff --git a/egs/librispeech/ASR/prepare_multidataset.sh b/egs/librispeech/ASR/prepare_multidataset.sh deleted file mode 100755 index c95b4d039..000000000 --- a/egs/librispeech/ASR/prepare_multidataset.sh +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env bash - -# fix segmentation fault reported in https://github.com/k2-fsa/icefall/issues/674 -export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python - -set -eou pipefail - -nj=16 -stage=-1 -stop_stage=100 - -# We assume dl_dir (download dir) contains the following -# directories and files. If not, they will be downloaded -# by this script automatically. -# -# - $dl_dir/LibriSpeech -# You can find BOOKS.TXT, test-clean, train-clean-360, etc, inside it. -# You can download them from https://www.openslr.org/12 -# -# - $dl_dir/lm -# This directory contains the following files downloaded from -# http://www.openslr.org/resources/11 -# -# - 3-gram.pruned.1e-7.arpa.gz -# - 3-gram.pruned.1e-7.arpa -# - 4-gram.arpa.gz -# - 4-gram.arpa -# - librispeech-vocab.txt -# - librispeech-lexicon.txt -# - librispeech-lm-norm.txt.gz -# -# - $dl_dir/musan -# This directory contains the following directories downloaded from -# http://www.openslr.org/17/ -# -# - music -# - noise -# - speech - -# Split all dataset to this number of pieces and mix each dataset pieces -# into multidataset pieces with shuffling. -num_splits=1998 - -dl_dir=$PWD/download - -. shared/parse_options.sh || exit 1 - -# vocab size for sentence piece models. -# It will generate data/lang_bpe_xxx, -# data/lang_bpe_yyy if the array contains xxx, yyy -vocab_sizes=( - # 5000 - # 2000 - # 1000 - 500 -) - -# multidataset list. -# LibriSpeech and musan are required. -# The others are optional. -multidataset=( - "gigaspeech", - "commonvoice", -) - -# All files generated by this script are saved in "data". -# You can safely remove "data" and rerun this script to regenerate it. -mkdir -p data - -log() { - # This function is from espnet - local fname=${BASH_SOURCE[1]##*/} - echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" -} - -log "dl_dir: $dl_dir" - -log "Dataset: LibriSpeech and musan" -if [ $stage -le -1 ] && [ $stop_stage -ge -1 ]; then - log "Stage -1: Download LM" - mkdir -p $dl_dir/lm - if [ ! -e $dl_dir/lm/.done ]; then - ./local/download_lm.py --out-dir=$dl_dir/lm - touch $dl_dir/lm/.done - fi -fi - -if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then - log "Stage 0: Download data" - - # If you have pre-downloaded it to /path/to/LibriSpeech, - # you can create a symlink - # - # ln -sfv /path/to/LibriSpeech $dl_dir/LibriSpeech - # - if [ ! -d $dl_dir/LibriSpeech/train-other-500 ]; then - lhotse download librispeech --full $dl_dir - fi - - # If you have pre-downloaded it to /path/to/musan, - # you can create a symlink - # - # ln -sfv /path/to/musan $dl_dir/ - # - if [ ! -d $dl_dir/musan ]; then - lhotse download musan $dl_dir - fi -fi - -if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then - log "Stage 1: Prepare LibriSpeech manifest" - # We assume that you have downloaded the LibriSpeech corpus - # to $dl_dir/LibriSpeech - mkdir -p data/manifests - if [ ! -e data/manifests/.librispeech.done ]; then - lhotse prepare librispeech -j $nj $dl_dir/LibriSpeech data/manifests - touch data/manifests/.librispeech.done - fi -fi - -if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then - log "Stage 2: Prepare musan manifest" - # We assume that you have downloaded the musan corpus - # to data/musan - mkdir -p data/manifests - if [ ! -e data/manifests/.musan.done ]; then - lhotse prepare musan $dl_dir/musan data/manifests - touch data/manifests/.musan.done - fi -fi - -if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then - log "Stage 3: Compute fbank for librispeech" - mkdir -p data/fbank - if [ ! -e data/fbank/.librispeech.done ]; then - ./local/compute_fbank_librispeech.py --perturb-speed False - touch data/fbank/.librispeech.done - fi - - if [ ! -f data/fbank/librispeech_cuts_train-all-shuf.jsonl.gz ]; then - cat <(gunzip -c data/fbank/librispeech_cuts_train-clean-100.jsonl.gz) \ - <(gunzip -c data/fbank/librispeech_cuts_train-clean-360.jsonl.gz) \ - <(gunzip -c data/fbank/librispeech_cuts_train-other-500.jsonl.gz) | \ - shuf | gzip -c > data/fbank/librispeech_cuts_train-all-shuf.jsonl.gz - fi - - if [ ! -e data/fbank/.librispeech-validated.done ]; then - log "Validating data/fbank for LibriSpeech" - parts=( - train-clean-100 - train-clean-360 - train-other-500 - test-clean - test-other - dev-clean - dev-other - ) - for part in ${parts[@]}; do - python3 ./local/validate_manifest.py \ - data/fbank/librispeech_cuts_${part}.jsonl.gz - done - touch data/fbank/.librispeech-validated.done - fi -fi - -if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then - log "Stage 4: Compute fbank for musan" - mkdir -p data/fbank - if [ ! -e data/fbank/.musan.done ]; then - ./local/compute_fbank_musan.py - touch data/fbank/.musan.done - fi -fi - -if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then - log "Stage 5: Prepare phone based lang" - lang_dir=data/lang_phone - mkdir -p $lang_dir - - (echo '!SIL SIL'; echo ' SPN'; echo ' SPN'; ) | - cat - $dl_dir/lm/librispeech-lexicon.txt | - sort | uniq > $lang_dir/lexicon.txt - - if [ ! -f $lang_dir/L_disambig.pt ]; then - ./local/prepare_lang.py --lang-dir $lang_dir - fi - - if [ ! -f $lang_dir/L.fst ]; then - log "Converting L.pt to L.fst" - ./shared/convert-k2-to-openfst.py \ - --olabels aux_labels \ - $lang_dir/L.pt \ - $lang_dir/L.fst - fi - - if [ ! -f $lang_dir/L_disambig.fst ]; then - log "Converting L_disambig.pt to L_disambig.fst" - ./shared/convert-k2-to-openfst.py \ - --olabels aux_labels \ - $lang_dir/L_disambig.pt \ - $lang_dir/L_disambig.fst - fi -fi - -if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then - log "Stage 6: Prepare BPE based lang" - - for vocab_size in ${vocab_sizes[@]}; do - lang_dir=data/lang_bpe_${vocab_size} - mkdir -p $lang_dir - # We reuse words.txt from phone based lexicon - # so that the two can share G.pt later. - cp data/lang_phone/words.txt $lang_dir - - if [ ! -f $lang_dir/transcript_words.txt ]; then - log "Generate data for BPE training" - files=$( - find "$dl_dir/LibriSpeech/train-clean-100" -name "*.trans.txt" - find "$dl_dir/LibriSpeech/train-clean-360" -name "*.trans.txt" - find "$dl_dir/LibriSpeech/train-other-500" -name "*.trans.txt" - ) - for f in ${files[@]}; do - cat $f | cut -d " " -f 2- - done > $lang_dir/transcript_words.txt - fi - - if [ ! -f $lang_dir/bpe.model ]; then - ./local/train_bpe_model.py \ - --lang-dir $lang_dir \ - --vocab-size $vocab_size \ - --transcript $lang_dir/transcript_words.txt - fi - - if [ ! -f $lang_dir/L_disambig.pt ]; then - ./local/prepare_lang_bpe.py --lang-dir $lang_dir - - log "Validating $lang_dir/lexicon.txt" - ./local/validate_bpe_lexicon.py \ - --lexicon $lang_dir/lexicon.txt \ - --bpe-model $lang_dir/bpe.model - fi - - if [ ! -f $lang_dir/L.fst ]; then - log "Converting L.pt to L.fst" - ./shared/convert-k2-to-openfst.py \ - --olabels aux_labels \ - $lang_dir/L.pt \ - $lang_dir/L.fst - fi - - if [ ! -f $lang_dir/L_disambig.fst ]; then - log "Converting L_disambig.pt to L_disambig.fst" - ./shared/convert-k2-to-openfst.py \ - --olabels aux_labels \ - $lang_dir/L_disambig.pt \ - $lang_dir/L_disambig.fst - fi - done -fi - -if [ $stage -le 7 ] && [ $stop_stage -ge 7 ]; then - log "Stage 7: Prepare G" - # We assume you have install kaldilm, if not, please install - # it using: pip install kaldilm - - mkdir -p data/lm - if [ ! -f data/lm/G_3_gram.fst.txt ]; then - # It is used in building HLG - python3 -m kaldilm \ - --read-symbol-table="data/lang_phone/words.txt" \ - --disambig-symbol='#0' \ - --max-order=3 \ - $dl_dir/lm/3-gram.pruned.1e-7.arpa > data/lm/G_3_gram.fst.txt - fi - - if [ ! -f data/lm/G_4_gram.fst.txt ]; then - # It is used for LM rescoring - python3 -m kaldilm \ - --read-symbol-table="data/lang_phone/words.txt" \ - --disambig-symbol='#0' \ - --max-order=4 \ - $dl_dir/lm/4-gram.arpa > data/lm/G_4_gram.fst.txt - fi -fi - -if [ $stage -le 8 ] && [ $stop_stage -ge 8 ]; then - log "Stage 8: Compile HLG" - ./local/compile_hlg.py --lang-dir data/lang_phone - - # Note If ./local/compile_hlg.py throws OOM, - # please switch to the following command - # - # ./local/compile_hlg_using_openfst.py --lang-dir data/lang_phone - - for vocab_size in ${vocab_sizes[@]}; do - lang_dir=data/lang_bpe_${vocab_size} - ./local/compile_hlg.py --lang-dir $lang_dir - - # Note If ./local/compile_hlg.py throws OOM, - # please switch to the following command - # - # ./local/compile_hlg_using_openfst.py --lang-dir $lang_dir - done -fi - -# Compile LG for RNN-T fast_beam_search decoding -if [ $stage -le 9 ] && [ $stop_stage -ge 9 ]; then - log "Stage 9: Compile LG" - ./local/compile_lg.py --lang-dir data/lang_phone - - for vocab_size in ${vocab_sizes[@]}; do - lang_dir=data/lang_bpe_${vocab_size} - ./local/compile_lg.py --lang-dir $lang_dir - done -fi - -if [ $stage -le 10 ] && [ $stop_stage -ge 10 ]; then - log "Stage 10: Prepare the other datasets" - # GigaSpeech - if [[ "${multidataset[@]}" =~ "gigaspeech" ]]; then - log "Dataset: GigaSpeech" - ./prepare_giga_speech.sh --stop_stage 5 - fi - - # CommonVoice - if [[ "${multidataset[@]}" =~ "commonvoice" ]]; then - log "Dataset: CommonVoice" - ./prepare_common_voice.sh - fi -fi diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/multidataset.py b/egs/librispeech/ASR/pruned_transducer_stateless7/multidataset.py deleted file mode 100644 index 07c7126fa..000000000 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/multidataset.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2023 Xiaomi Corp. (authors: Yifan Yang) -# -# See ../../../../LICENSE for clarification regarding multiple authors -# -# 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 glob -import logging -import re -from pathlib import Path - -import lhotse -from lhotse import CutSet, load_manifest_lazy - - -class MultiDataset: - def __init__(self, manifest_dir: str, cv_manifest_dir: str): - """ - Args: - manifest_dir: - It is expected to contain the following files: - - - librispeech_cuts_train-all-shuf.jsonl.gz - - gigaspeech_XL_split_2000/gigaspeech_cuts_XL.*.jsonl.gz - - cv_manifest_dir: - It is expected to contain the following files: - - - cv-en_cuts_train.jsonl.gz - """ - self.manifest_dir = Path(manifest_dir) - self.cv_manifest_dir = Path(cv_manifest_dir) - - def train_cuts(self) -> CutSet: - logging.info("About to get multidataset train cuts") - - # LibriSpeech - logging.info(f"Loading LibriSpeech in lazy mode") - librispeech_cuts = load_manifest_lazy( - self.manifest_dir / "librispeech_cuts_train-all-shuf.jsonl.gz" - ) - - # GigaSpeech - filenames = glob.glob( - f"{self.manifest_dir}/gigaspeech_XL_split_2000/gigaspeech_cuts_XL.*.jsonl.gz" - ) - - pattern = re.compile(r"gigaspeech_cuts_XL.([0-9]+).jsonl.gz") - idx_filenames = ((int(pattern.search(f).group(1)), f) for f in filenames) - idx_filenames = sorted(idx_filenames, key=lambda x: x[0]) - - sorted_filenames = [f[1] for f in idx_filenames] - - logging.info(f"Loading GigaSpeech {len(sorted_filenames)} splits in lazy mode") - - gigaspeech_cuts = lhotse.combine( - lhotse.load_manifest_lazy(p) for p in sorted_filenames - ) - - # CommonVoice - logging.info(f"Loading CommonVoice in lazy mode") - commonvoice_cuts = load_manifest_lazy( - self.cv_manifest_dir / f"cv-en_cuts_train.jsonl.gz" - ) - - return CutSet.mux(librispeech_cuts, gigaspeech_cuts, commonvoice_cuts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py index 5be8c481d..a67e5174e 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py @@ -66,7 +66,6 @@ from lhotse.cut import Cut from lhotse.dataset.sampling.base import CutSampler from lhotse.utils import fix_random_seed from model import Transducer -from multidataset import MultiDataset from optim import Eden, ScaledAdam from torch import Tensor from torch.cuda.amp import GradScaler @@ -376,13 +375,6 @@ def get_parser(): help="Whether to use half precision training.", ) - parser.add_argument( - "--use-multidataset", - type=str2bool, - default=False, - help="Whether to use multidataset to train.", - ) - add_model_arguments(parser) return parser @@ -1042,16 +1034,12 @@ def run(rank, world_size, args): librispeech = LibriSpeechAsrDataModule(args) - if params.use_multidataset: - multidataset = MultiDataset(params.manifest_dir, params.cv_manifest_dir) - train_cuts = multidataset.train_cuts() + if params.mini_libri: + train_cuts = librispeech.train_clean_5_cuts() + elif params.full_libri: + train_cuts = librispeech.train_all_shuf_cuts() else: - if params.mini_libri: - train_cuts = librispeech.train_clean_5_cuts() - elif params.full_libri: - train_cuts = librispeech.train_all_shuf_cuts() - else: - train_cuts = librispeech.train_clean_100_cuts() + train_cuts = librispeech.train_clean_100_cuts() def remove_short_and_long_utt(c: Cut): # Keep only utterances with duration between 1 second and 20 seconds @@ -1107,7 +1095,7 @@ def run(rank, world_size, args): valid_cuts += librispeech.dev_other_cuts() valid_dl = librispeech.valid_dataloaders(valid_cuts) - if not params.use_multidataset and not params.print_diagnostics: + if not params.print_diagnostics: scan_pessimistic_batches_for_oom( model=model, train_dl=train_dl, diff --git a/egs/librispeech/ASR/zipformer/train.py b/egs/librispeech/ASR/zipformer/train.py index 1f0741ba4..9788220c9 100755 --- a/egs/librispeech/ASR/zipformer/train.py +++ b/egs/librispeech/ASR/zipformer/train.py @@ -62,20 +62,20 @@ import torch import torch.multiprocessing as mp import torch.nn as nn from asr_datamodule import LibriSpeechAsrDataModule -from zipformer import Zipformer2 -from scaling import ScheduledFloat from decoder import Decoder from joiner import Joiner -from subsampling import Conv2dSubsampling from lhotse.cut import Cut from lhotse.dataset.sampling.base import CutSampler from lhotse.utils import fix_random_seed from model import Transducer from optim import Eden, ScaledAdam +from scaling import ScheduledFloat +from subsampling import Conv2dSubsampling from torch import Tensor from torch.cuda.amp import GradScaler from torch.nn.parallel import DistributedDataParallel as DDP from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer2 from icefall import diagnostics from icefall.checkpoint import load_checkpoint, remove_checkpoints @@ -84,40 +84,38 @@ from icefall.checkpoint import ( save_checkpoint_with_global_batch_idx, update_averaged_model, ) -from icefall.hooks import register_inf_check_hooks from icefall.dist import cleanup_dist, setup_dist from icefall.env import get_env_info +from icefall.hooks import register_inf_check_hooks from icefall.utils import ( AttributeDict, MetricsTracker, + get_parameter_groups_with_lrs, setup_logger, str2bool, - get_parameter_groups_with_lrs ) -LRSchedulerType = Union[ - torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler -] +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] -def get_adjusted_batch_count( - params: AttributeDict) -> float: +def get_adjusted_batch_count(params: AttributeDict) -> float: # returns the number of batches we would have used so far if we had used the reference # duration. This is for purposes of set_batch_count(). - return (params.batch_idx_train * (params.max_duration * params.world_size) / - params.ref_duration) + return ( + params.batch_idx_train + * (params.max_duration * params.world_size) + / params.ref_duration + ) -def set_batch_count( - model: Union[nn.Module, DDP], batch_count: float -) -> None: +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: if isinstance(model, DDP): # get underlying nn.Module model = model.module for name, module in model.named_modules(): - if hasattr(module, 'batch_count'): + if hasattr(module, "batch_count"): module.batch_count = batch_count - if hasattr(module, 'name'): + if hasattr(module, "name"): module.name = name @@ -154,35 +152,35 @@ def add_model_arguments(parser: argparse.ArgumentParser): "--encoder-dim", type=str, default="192,256,384,512,384,256", - help="Embedding dimension in encoder stacks: a single int or comma-separated list." + help="Embedding dimension in encoder stacks: a single int or comma-separated list.", ) parser.add_argument( "--query-head-dim", type=str, default="32", - help="Query/key dimension per head in encoder stacks: a single int or comma-separated list." + help="Query/key dimension per head in encoder stacks: a single int or comma-separated list.", ) parser.add_argument( "--value-head-dim", type=str, default="12", - help="Value dimension per head in encoder stacks: a single int or comma-separated list." + help="Value dimension per head in encoder stacks: a single int or comma-separated list.", ) parser.add_argument( "--pos-head-dim", type=str, default="4", - help="Positional-encoding dimension per head in encoder stacks: a single int or comma-separated list." + help="Positional-encoding dimension per head in encoder stacks: a single int or comma-separated list.", ) parser.add_argument( "--pos-dim", type=int, default="48", - help="Positional-encoding embedding dimension" + help="Positional-encoding embedding dimension", ) parser.add_argument( @@ -190,7 +188,7 @@ def add_model_arguments(parser: argparse.ArgumentParser): type=str, default="192,192,256,256,256,192", help="Unmasked dimensions in the encoders, relates to augmentation during training. " - "A single int or comma-separated list. Must be <= each corresponding encoder_dim." + "A single int or comma-separated list. Must be <= each corresponding encoder_dim.", ) parser.add_argument( @@ -230,7 +228,7 @@ def add_model_arguments(parser: argparse.ArgumentParser): type=str, default="16,32,64,-1", help="Chunk sizes (at 50Hz frame rate) will be chosen randomly from this list during training. " - " Must be just -1 if --causal=False" + " Must be just -1 if --causal=False", ) parser.add_argument( @@ -239,7 +237,7 @@ def add_model_arguments(parser: argparse.ArgumentParser): default="64,128,256,-1", help="Maximum left-contexts for causal training, measured in frames which will " "be converted to a number of chunks. If splitting into chunks, " - "chunk left-context frames will be chosen randomly from this list; else not relevant." + "chunk left-context frames will be chosen randomly from this list; else not relevant.", ) @@ -313,10 +311,7 @@ def get_parser(): ) parser.add_argument( - "--base-lr", - type=float, - default=0.045, - help="The base learning rate." + "--base-lr", type=float, default=0.045, help="The base learning rate." ) parser.add_argument( @@ -340,15 +335,14 @@ def get_parser(): type=float, default=600, help="Reference batch duration for purposes of adjusting batch counts for setting various " - "schedules inside the model" + "schedules inside the model", ) parser.add_argument( "--context-size", type=int, default=2, - help="The context size in the decoder. 1 means bigram; " - "2 means tri-gram", + help="The context size in the decoder. 1 means bigram; " "2 means tri-gram", ) parser.add_argument( @@ -371,8 +365,7 @@ def get_parser(): "--am-scale", type=float, default=0.0, - help="The scale to smooth the loss with am (output of encoder network)" - "part.", + help="The scale to smooth the loss with am (output of encoder network)" "part.", ) parser.add_argument( @@ -522,7 +515,7 @@ def get_params() -> AttributeDict: def _to_int_tuple(s: str): - return tuple(map(int, s.split(','))) + return tuple(map(int, s.split(","))) def get_encoder_embed(params: AttributeDict) -> nn.Module: @@ -537,7 +530,7 @@ def get_encoder_embed(params: AttributeDict) -> nn.Module: encoder_embed = Conv2dSubsampling( in_channels=params.feature_dim, out_channels=_to_int_tuple(params.encoder_dim)[0], - dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)) + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), ) return encoder_embed @@ -596,7 +589,7 @@ def get_transducer_model(params: AttributeDict) -> nn.Module: encoder=encoder, decoder=decoder, joiner=joiner, - encoder_dim=int(max(params.encoder_dim.split(','))), + encoder_dim=int(max(params.encoder_dim.split(","))), decoder_dim=params.decoder_dim, joiner_dim=params.joiner_dim, vocab_size=params.vocab_size, @@ -745,11 +738,7 @@ def compute_loss( warmup: a floating point value which increases throughout training; values >= 1.0 are fully warmed up and have all modules present. """ - device = ( - model.device - if isinstance(model, DDP) - else next(model.parameters()).device - ) + device = model.device if isinstance(model, DDP) else next(model.parameters()).device feature = batch["inputs"] # at entry, feature is (N, T, C) assert feature.ndim == 3 @@ -779,27 +768,24 @@ def compute_loss( # take down the scale on the simple loss from 1.0 at the start # to params.simple_loss scale by warm_step. simple_loss_scale = ( - s if batch_idx_train >= warm_step + s + if batch_idx_train >= warm_step else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) ) pruned_loss_scale = ( - 1.0 if batch_idx_train >= warm_step + 1.0 + if batch_idx_train >= warm_step else 0.1 + 0.9 * (batch_idx_train / warm_step) ) - loss = ( - simple_loss_scale * simple_loss + - pruned_loss_scale * pruned_loss - ) + loss = simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss assert loss.requires_grad == is_training info = MetricsTracker() with warnings.catch_warnings(): warnings.simplefilter("ignore") - info["frames"] = ( - (feature_lens // params.subsampling_factor).sum().item() - ) + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() # Note: We use reduction=sum while computing the loss. info["loss"] = loss.detach().cpu().item() @@ -895,15 +881,17 @@ def train_one_epoch( saved_bad_model = False def save_bad_model(suffix: str = ""): - save_checkpoint_impl(filename=params.exp_dir / f"bad-model{suffix}-{rank}.pt", - model=model, - model_avg=model_avg, - params=params, - optimizer=optimizer, - scheduler=scheduler, - sampler=train_dl.sampler, - scaler=scaler, - rank=0) + save_checkpoint_impl( + filename=params.exp_dir / f"bad-model{suffix}-{rank}.pt", + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=0, + ) for batch_idx, batch in enumerate(train_dl): if batch_idx % 10 == 0: @@ -988,7 +976,9 @@ def train_one_epoch( logging.warning(f"Grad scale is small: {cur_grad_scale}") if cur_grad_scale < 1.0e-05: save_bad_model() - raise RuntimeError(f"grad_scale is too small, exiting: {cur_grad_scale}") + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) if batch_idx % params.log_interval == 0: cur_lr = max(scheduler.get_last_lr()) @@ -998,8 +988,8 @@ def train_one_epoch( f"Epoch {params.cur_epoch}, " f"batch {batch_idx}, loss[{loss_info}], " f"tot_loss[{tot_loss}], batch size: {batch_size}, " - f"lr: {cur_lr:.2e}, " + - (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") ) if tb_writer is not None: @@ -1010,9 +1000,7 @@ def train_one_epoch( loss_info.write_summary( tb_writer, "train/current_", params.batch_idx_train ) - tot_loss.write_summary( - tb_writer, "train/tot_", params.batch_idx_train - ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) if params.use_fp16: tb_writer.add_scalar( "train/grad_scale", cur_grad_scale, params.batch_idx_train @@ -1029,7 +1017,9 @@ def train_one_epoch( ) model.train() logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") - logging.info(f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) if tb_writer is not None: valid_info.write_summary( tb_writer, "train/valid_", params.batch_idx_train @@ -1103,13 +1093,11 @@ def run(rank, world_size, args): model.to(device) if world_size > 1: logging.info("Using DDP") - model = DDP(model, device_ids=[rank], - find_unused_parameters=True) + model = DDP(model, device_ids=[rank], find_unused_parameters=True) optimizer = ScaledAdam( - get_parameter_groups_with_lrs( - model, lr=params.base_lr, include_names=True), - lr=params.base_lr, # should have no effect + get_parameter_groups_with_lrs(model, lr=params.base_lr, include_names=True), + lr=params.base_lr, # should have no effect clipping_scale=2.0, ) @@ -1129,7 +1117,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2 ** 22 + 2**22 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) @@ -1153,9 +1141,9 @@ def run(rank, world_size, args): # an utterance duration distribution for your dataset to select # the threshold if c.duration < 1.0 or c.duration > 20.0: - logging.warning( - f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" - ) + # logging.warning( + # f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + # ) return False # In pruned RNN-T, we require that T >= S @@ -1206,8 +1194,7 @@ def run(rank, world_size, args): params=params, ) - scaler = GradScaler(enabled=params.use_fp16, - init_scale=1.0) + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) if checkpoints and "grad_scaler" in checkpoints: logging.info("Loading grad scaler state dict") scaler.load_state_dict(checkpoints["grad_scaler"]) @@ -1328,7 +1315,9 @@ def scan_pessimistic_batches_for_oom( ) display_and_save_batch(batch, params=params, sp=sp) raise - logging.info(f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) def main(): From ca60ced213b2567a650d61f1a1bc867eee3c8e23 Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:12:42 +0800 Subject: [PATCH 023/100] Fix typo (#1114) * Fix typo for zipformer * Fix typo for pruned_transducer_stateless7 * Fix typo for pruned_transducer_stateless7_ctc * Fix typo for pruned_transducer_stateless7_ctc_bs * Fix typo for pruned_transducer_stateless7_streaming * Fix typo for pruned_transducer_stateless7_streaming_multi * Fix file permissions for pruned_transducer_stateless7_streaming_multi * Fix typo for pruned_transducer_stateless8 * Fix typo for pruned_transducer_stateless6 * Fix typo for pruned_transducer_stateless5 * Fix typo for pruned_transducer_stateless4 * Fix typo for pruned_transducer_stateless3 --- .../streaming_decode.py | 2 +- .../streaming_decode.py | 4 ++-- .../ASR/pruned_transducer_stateless4/train.py | 8 ++++---- .../export-onnx-streaming.py | 2 +- .../pruned_transducer_stateless5/export-onnx.py | 2 +- .../streaming_decode.py | 4 ++-- .../ASR/pruned_transducer_stateless5/test_model.py | 2 +- .../ASR/pruned_transducer_stateless5/train.py | 2 +- .../ASR/pruned_transducer_stateless6/export.py | 14 +++++++------- .../ASR/pruned_transducer_stateless6/train.py | 2 +- .../ASR/pruned_transducer_stateless7/decode.py | 8 ++++---- .../pruned_transducer_stateless7/export-onnx.py | 4 ++-- .../ASR/pruned_transducer_stateless7/finetune.py | 2 +- .../ASR/pruned_transducer_stateless7/train.py | 2 +- .../ASR/pruned_transducer_stateless7_ctc/train.py | 2 +- .../pruned_transducer_stateless7_ctc_bs/train.py | 2 +- .../export-for-ncnn-zh.py | 2 +- .../export-for-ncnn.py | 2 +- .../jit_trace_export.py | 4 ++-- .../streaming_decode.py | 4 ++-- .../train.py | 2 +- .../train2.py | 2 +- .../decode_gigaspeech.py | 0 .../export-for-ncnn.py | 2 +- .../streaming_decode.py | 4 ++-- .../train.py | 2 +- .../ASR/pruned_transducer_stateless8/train.py | 2 +- egs/librispeech/ASR/zipformer/streaming_decode.py | 2 +- egs/librispeech/ASR/zipformer/train.py | 2 +- 29 files changed, 46 insertions(+), 46 deletions(-) mode change 100644 => 100755 egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/decode_gigaspeech.py mode change 100644 => 100755 egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/streaming_decode.py diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/streaming_decode.py b/egs/librispeech/ASR/pruned_transducer_stateless3/streaming_decode.py index 3a1ecb7ed..e7c1affc2 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/streaming_decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/streaming_decode.py @@ -99,7 +99,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless3/exp", help="The experiment dir", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless4/streaming_decode.py b/egs/librispeech/ASR/pruned_transducer_stateless4/streaming_decode.py index ca3a023ce..e966aa4b1 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless4/streaming_decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless4/streaming_decode.py @@ -78,7 +78,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for decoding. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) @@ -115,7 +115,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless4/exp", help="The experiment dir", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless4/train.py b/egs/librispeech/ASR/pruned_transducer_stateless4/train.py index 9bd7df401..875b03f7f 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless4/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless4/train.py @@ -26,7 +26,7 @@ export CUDA_VISIBLE_DEVICES="0,1,2,3" --world-size 4 \ --num-epochs 30 \ --start-epoch 1 \ - --exp-dir pruned_transducer_stateless2/exp \ + --exp-dir pruned_transducer_stateless4/exp \ --full-libri 1 \ --max-duration 300 @@ -37,7 +37,7 @@ export CUDA_VISIBLE_DEVICES="0,1,2,3" --num-epochs 30 \ --start-epoch 1 \ --use-fp16 1 \ - --exp-dir pruned_transducer_stateless2/exp \ + --exp-dir pruned_transducer_stateless4/exp \ --full-libri 1 \ --max-duration 550 @@ -195,7 +195,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless4/exp", help="""The experiment dir. It specifies the directory where all training related files, e.g., checkpoints, log, etc, are saved @@ -296,7 +296,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py index 32eb9eda3..938ff2f16 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py @@ -87,7 +87,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py index e89d94d82..20fd8dff8 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py @@ -84,7 +84,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/streaming_decode.py b/egs/librispeech/ASR/pruned_transducer_stateless5/streaming_decode.py index 5b15dcee7..f65f47fc2 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/streaming_decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/streaming_decode.py @@ -78,7 +78,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for decoding. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) @@ -115,7 +115,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless5/exp", help="The experiment dir", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/test_model.py b/egs/librispeech/ASR/pruned_transducer_stateless5/test_model.py index 9aad32014..71b36e029 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/test_model.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/test_model.py @@ -20,7 +20,7 @@ To run this file, do: cd icefall/egs/librispeech/ASR - python ./pruned_transducer_stateless4/test_model.py + python ./pruned_transducer_stateless5/test_model.py """ from train import get_params, get_transducer_model diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/train.py b/egs/librispeech/ASR/pruned_transducer_stateless5/train.py index 847c80ab0..3b5a635e4 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/train.py @@ -328,7 +328,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless6/export.py b/egs/librispeech/ASR/pruned_transducer_stateless6/export.py index b6190e8a6..4d0d8326c 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless6/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless6/export.py @@ -20,23 +20,23 @@ # to a single one using model averaging. """ Usage: -./pruned_transducer_stateless2/export.py \ - --exp-dir ./pruned_transducer_stateless2/exp \ +./pruned_transducer_stateless6/export.py \ + --exp-dir ./pruned_transducer_stateless6/exp \ --bpe-model data/lang_bpe_500/bpe.model \ --epoch 20 \ --avg 10 It will generate a file exp_dir/pretrained.pt -To use the generated file with `pruned_transducer_stateless2/decode.py`, +To use the generated file with `pruned_transducer_stateless6/decode.py`, you can do: cd /path/to/exp_dir ln -s pretrained.pt epoch-9999.pt cd /path/to/egs/librispeech/ASR - ./pruned_transducer_stateless2/decode.py \ - --exp-dir ./pruned_transducer_stateless2/exp \ + ./pruned_transducer_stateless6/decode.py \ + --exp-dir ./pruned_transducer_stateless6/exp \ --epoch 9999 \ --avg 1 \ --max-duration 100 \ @@ -65,7 +65,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) @@ -91,7 +91,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless6/exp", help="""It specifies the directory where all training related files, e.g., checkpoints, log, etc, are saved """, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless6/train.py b/egs/librispeech/ASR/pruned_transducer_stateless6/train.py index 57753599a..8f033cb9a 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless6/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless6/train.py @@ -267,7 +267,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/decode.py b/egs/librispeech/ASR/pruned_transducer_stateless7/decode.py index 55a2493e9..eb8841cc4 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/decode.py @@ -94,10 +94,10 @@ Usage: --max-states 64 (8) modified beam search with RNNLM shallow fusion -./pruned_transducer_stateless5/decode.py \ +./pruned_transducer_stateless7/decode.py \ --epoch 35 \ --avg 15 \ - --exp-dir ./pruned_transducer_stateless5/exp \ + --exp-dir ./pruned_transducer_stateless7/exp \ --max-duration 600 \ --decoding-method modified_beam_search_lm_shallow_fusion \ --beam-size 4 \ @@ -110,11 +110,11 @@ Usage: --rnn-lm-tie-weights 1 (9) modified beam search with LM shallow fusion + LODR -./pruned_transducer_stateless5/decode.py \ +./pruned_transducer_stateless7/decode.py \ --epoch 28 \ --avg 15 \ --max-duration 600 \ - --exp-dir ./pruned_transducer_stateless5/exp \ + --exp-dir ./pruned_transducer_stateless7/exp \ --decoding-method modified_beam_search_LODR \ --beam-size 4 \ --lm-type rnn \ diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py index 2f5d9e338..d2db92820 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py @@ -79,7 +79,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) @@ -116,7 +116,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless5/exp", + default="pruned_transducer_stateless7/exp", help="""It specifies the directory where all training related files, e.g., checkpoints, log, etc, are saved """, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py index bac25d7b2..3ee2b7d65 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py @@ -389,7 +389,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py index a67e5174e..5ec71ec3f 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py @@ -340,7 +340,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py index 44823dd20..b387968a9 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py @@ -346,7 +346,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py index 8d6dc0881..23fb6f497 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py @@ -342,7 +342,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py index 1f870ca5a..e196f8b7d 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py @@ -90,7 +90,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py index f5589d1b2..4a16a97fb 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py @@ -88,7 +88,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_trace_export.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_trace_export.py index a164f3f69..4af742316 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_trace_export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_trace_export.py @@ -39,7 +39,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) @@ -65,7 +65,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless7_streaming/exp", help="""It specifies the directory where all training related files, e.g., checkpoints, log, etc, are saved """, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py index c272ed641..b76272e66 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py @@ -77,7 +77,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for decoding. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) @@ -114,7 +114,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless7_streaming/exp", help="The experiment dir", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py index e58f4f820..99090b2c1 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py @@ -355,7 +355,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py index dcdd2ed65..9be629149 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py @@ -355,7 +355,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/decode_gigaspeech.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/decode_gigaspeech.py old mode 100644 new mode 100755 diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py index f5589d1b2..4a16a97fb 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py @@ -88,7 +88,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for averaging. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/streaming_decode.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/streaming_decode.py old mode 100644 new mode 100755 index 78713f920..2904f086c --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/streaming_decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/streaming_decode.py @@ -78,7 +78,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for decoding. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) @@ -115,7 +115,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless2/exp", + default="pruned_transducer_stateless7_streaming_multi/exp", help="The experiment dir", ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py index 37b89a626..b494253d6 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py @@ -366,7 +366,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless8/train.py b/egs/librispeech/ASR/pruned_transducer_stateless8/train.py index b0abad5ae..41adda012 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless8/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless8/train.py @@ -348,7 +348,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) diff --git a/egs/librispeech/ASR/zipformer/streaming_decode.py b/egs/librispeech/ASR/zipformer/streaming_decode.py index c2d58cb1e..3f140b4fa 100755 --- a/egs/librispeech/ASR/zipformer/streaming_decode.py +++ b/egs/librispeech/ASR/zipformer/streaming_decode.py @@ -81,7 +81,7 @@ def get_parser(): type=int, default=28, help="""It specifies the checkpoint to use for decoding. - Note: Epoch counts from 0. + Note: Epoch counts from 1. You can specify --avg to use more checkpoints for model averaging.""", ) diff --git a/egs/librispeech/ASR/zipformer/train.py b/egs/librispeech/ASR/zipformer/train.py index 9788220c9..bec9a3986 100755 --- a/egs/librispeech/ASR/zipformer/train.py +++ b/egs/librispeech/ASR/zipformer/train.py @@ -408,7 +408,7 @@ def get_parser(): params.batch_idx_train % save_every_n == 0. The checkpoint filename has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the - end of each epoch where `xxx` is the epoch number counting from 0. + end of each epoch where `xxx` is the epoch number counting from 1. """, ) From ba257efbcddf990edeee615e4af541ae378da3e2 Mon Sep 17 00:00:00 2001 From: Wei Kang Date: Sat, 3 Jun 2023 21:28:49 +0800 Subject: [PATCH 024/100] Add Context biasing (#1038) * Add context biasing for librispeech * Add context biasing for wenetspeech * fix bugs * Implement Aho-Corasick context graph * fix some bugs * Fixes to forward_one_step; add draw to context graph * add output arc; fix black * Fix wenetspeech tokenizer * Minor fixes to the decode.py --- .../pruned_transducer_stateless7/decode.py | 74 +++- .../beam_search.py | 44 +- .../pruned_transducer_stateless4/decode.py | 57 ++- .../asr_datamodule.py | 4 +- .../pruned_transducer_stateless5/decode.py | 85 +++- icefall/__init__.py | 2 + icefall/context_graph.py | 412 ++++++++++++++++++ 7 files changed, 652 insertions(+), 26 deletions(-) create mode 100644 icefall/context_graph.py diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/decode.py b/egs/aishell/ASR/pruned_transducer_stateless7/decode.py index af54af8da..be58c4e43 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/decode.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/decode.py @@ -58,6 +58,7 @@ Usage: import argparse import logging +import os from collections import defaultdict from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -76,6 +77,8 @@ from beam_search import ( ) from train import add_model_arguments, get_params, get_transducer_model +from icefall import ContextGraph, LmScorer, NgramLm +from icefall.char_graph_compiler import CharCtcTrainingGraphCompiler from icefall.checkpoint import ( average_checkpoints, average_checkpoints_with_averaged_model, @@ -211,6 +214,26 @@ def get_parser(): Used only when --decoding_method is greedy_search""", ) + parser.add_argument( + "--context-score", + type=float, + default=2, + help=""" + The bonus score of each token for the context biasing words/phrases. + Used only when --decoding_method is modified_beam_search. + """, + ) + + parser.add_argument( + "--context-file", + type=str, + default="", + help=""" + The path of the context biasing lists, one word/phrase each line + Used only when --decoding_method is modified_beam_search. + """, + ) + add_model_arguments(parser) return parser @@ -222,6 +245,7 @@ def decode_one_batch( token_table: k2.SymbolTable, batch: dict, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, ) -> Dict[str, List[List[str]]]: """Decode one batch and return the result in a dict. The dict has the following format: @@ -285,6 +309,7 @@ def decode_one_batch( encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, beam=params.beam_size, + context_graph=context_graph, ) else: hyp_tokens = [] @@ -324,7 +349,12 @@ def decode_one_batch( ): hyps } else: - return {f"beam_size_{params.beam_size}": hyps} + key = f"beam_size_{params.beam_size}" + if params.has_contexts: + key += f"-context-score-{params.context_score}" + else: + key += "-no-context-words" + return {key: hyps} def decode_dataset( @@ -333,6 +363,7 @@ def decode_dataset( model: nn.Module, token_table: k2.SymbolTable, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, ) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: """Decode dataset. @@ -377,6 +408,7 @@ def decode_dataset( model=model, token_table=token_table, decoding_graph=decoding_graph, + context_graph=context_graph, batch=batch, ) @@ -407,16 +439,17 @@ def save_results( for key, results in results_dict.items(): recog_path = params.res_dir / f"recogs-{test_set_name}-{params.suffix}.txt" results = sorted(results) - store_transcripts(filename=recog_path, texts=results) + # we compute CER for aishell dataset. + results_char = [] + for res in results: + results_char.append((res[0], list("".join(res[1])), list("".join(res[2])))) + + store_transcripts(filename=recog_path, texts=results_char) logging.info(f"The transcripts are stored in {recog_path}") # The following prints out WERs, per-word error statistics and aligned # ref/hyp pairs. errs_filename = params.res_dir / f"errs-{test_set_name}-{params.suffix}.txt" - # we compute CER for aishell dataset. - results_char = [] - for res in results: - results_char.append((res[0], list("".join(res[1])), list("".join(res[2])))) with open(errs_filename, "w") as f: wer = write_error_stats( f, f"{test_set_name}-{key}", results_char, enable_log=True @@ -457,6 +490,12 @@ def main(): "fast_beam_search", "modified_beam_search", ) + + if os.path.exists(params.context_file): + params.has_contexts = True + else: + params.has_contexts = False + params.res_dir = params.exp_dir / params.decoding_method if params.iter > 0: @@ -470,6 +509,10 @@ def main(): params.suffix += f"-max-states-{params.max_states}" elif "beam_search" in params.decoding_method: params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + if params.has_contexts: + params.suffix += f"-context-score-{params.context_score}" + else: + params.suffix += "-no-contexts-words" else: params.suffix += f"-context-{params.context_size}" params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" @@ -490,6 +533,11 @@ def main(): params.blank_id = 0 params.vocab_size = max(lexicon.tokens) + 1 + graph_compiler = CharCtcTrainingGraphCompiler( + lexicon=lexicon, + device=device, + ) + logging.info(params) logging.info("About to create model") @@ -586,6 +634,19 @@ def main(): else: decoding_graph = None + if params.decoding_method == "modified_beam_search": + if os.path.exists(params.context_file): + contexts_text = [] + for line in open(params.context_file).readlines(): + contexts_text.append(line.strip()) + contexts = graph_compiler.texts_to_ids(contexts_text) + context_graph = ContextGraph(params.context_score) + context_graph.build(contexts) + else: + context_graph = None + else: + context_graph = None + num_param = sum([p.numel() for p in model.parameters()]) logging.info(f"Number of model parameters: {num_param}") @@ -608,6 +669,7 @@ def main(): model=model, token_table=lexicon.token_table, decoding_graph=decoding_graph, + context_graph=context_graph, ) save_results( diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index f5f15808d..25b79d600 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -24,7 +24,7 @@ import sentencepiece as spm import torch from model import Transducer -from icefall import NgramLm, NgramLmStateCost +from icefall import ContextGraph, ContextState, NgramLm, NgramLmStateCost from icefall.decode import Nbest, one_best_decoding from icefall.lm_wrapper import LmScorer from icefall.rnn_lm.model import RnnLmModel @@ -765,6 +765,9 @@ class Hypothesis: # N-gram LM state state_cost: Optional[NgramLmStateCost] = None + # Context graph state + context_state: Optional[ContextState] = None + @property def key(self) -> str: """Return a string representation of self.ys""" @@ -917,6 +920,7 @@ def modified_beam_search( model: Transducer, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, + context_graph: Optional[ContextGraph] = None, beam: int = 4, temperature: float = 1.0, return_timestamps: bool = False, @@ -968,6 +972,7 @@ def modified_beam_search( Hypothesis( ys=[blank_id] * context_size, log_prob=torch.zeros(1, dtype=torch.float32, device=device), + context_state=None if context_graph is None else context_graph.root, timestamp=[], ) ) @@ -990,6 +995,7 @@ def modified_beam_search( hyps_shape = get_hyps_shape(B).to(device) A = [list(b) for b in B] + B = [HypothesisList() for _ in range(batch_size)] ys_log_probs = torch.cat( @@ -1047,21 +1053,51 @@ def modified_beam_search( for k in range(len(topk_hyp_indexes)): hyp_idx = topk_hyp_indexes[k] hyp = A[i][hyp_idx] - new_ys = hyp.ys[:] new_token = topk_token_indexes[k] new_timestamp = hyp.timestamp[:] + context_score = 0 + new_context_state = None if context_graph is None else hyp.context_state if new_token not in (blank_id, unk_id): new_ys.append(new_token) new_timestamp.append(t) + if context_graph is not None: + ( + context_score, + new_context_state, + ) = context_graph.forward_one_step(hyp.context_state, new_token) + + new_log_prob = topk_log_probs[k] + context_score - new_log_prob = topk_log_probs[k] new_hyp = Hypothesis( - ys=new_ys, log_prob=new_log_prob, timestamp=new_timestamp + ys=new_ys, + log_prob=new_log_prob, + timestamp=new_timestamp, + context_state=new_context_state, ) B[i].add(new_hyp) B = B + finalized_B + + # finalize context_state, if the matched contexts do not reach final state + # we need to add the score on the corresponding backoff arc + if context_graph is not None: + finalized_B = [HypothesisList() for _ in range(len(B))] + for i, hyps in enumerate(B): + for hyp in list(hyps): + context_score, new_context_state = context_graph.finalize( + hyp.context_state + ) + finalized_B[i].add( + Hypothesis( + ys=hyp.ys, + log_prob=hyp.log_prob + context_score, + timestamp=hyp.timestamp, + context_state=new_context_state, + ) + ) + B = finalized_B + best_hyps = [b.get_most_probable(length_norm=True) for b in B] sorted_ans = [h.ys[context_size:] for h in best_hyps] diff --git a/egs/librispeech/ASR/pruned_transducer_stateless4/decode.py b/egs/librispeech/ASR/pruned_transducer_stateless4/decode.py index 79d919ab1..524366068 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless4/decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless4/decode.py @@ -125,6 +125,7 @@ For example: import argparse import logging import math +import os from collections import defaultdict from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -146,6 +147,7 @@ from beam_search import ( ) from train import add_model_arguments, get_params, get_transducer_model +from icefall import ContextGraph from icefall.checkpoint import ( average_checkpoints, average_checkpoints_with_averaged_model, @@ -353,6 +355,27 @@ def get_parser(): Used only when the decoding method is fast_beam_search_nbest, fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", ) + + parser.add_argument( + "--context-score", + type=float, + default=2, + help=""" + The bonus score of each token for the context biasing words/phrases. + Used only when --decoding_method is modified_beam_search. + """, + ) + + parser.add_argument( + "--context-file", + type=str, + default="", + help=""" + The path of the context biasing lists, one word/phrase each line + Used only when --decoding_method is modified_beam_search. + """, + ) + add_model_arguments(parser) return parser @@ -365,6 +388,7 @@ def decode_one_batch( batch: dict, word_table: Optional[k2.SymbolTable] = None, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, ) -> Dict[str, Tuple[List[List[str]], List[List[float]]]]: """Decode one batch and return the result in a dict. The dict has the following format: @@ -494,6 +518,7 @@ def decode_one_batch( encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, beam=params.beam_size, + context_graph=context_graph, return_timestamps=True, ) else: @@ -548,7 +573,12 @@ def decode_one_batch( return {key: (hyps, timestamps)} else: - return {f"beam_size_{params.beam_size}": (hyps, timestamps)} + key = f"beam_size_{params.beam_size}" + if params.has_contexts: + key += f"-context-score-{params.context_score}" + else: + key += "-no-context-words" + return {key: (hyps, timestamps)} def decode_dataset( @@ -558,6 +588,7 @@ def decode_dataset( sp: spm.SentencePieceProcessor, word_table: Optional[k2.SymbolTable] = None, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, ) -> Dict[str, List[Tuple[str, List[str], List[str], List[float], List[float]]]]: """Decode dataset. @@ -622,6 +653,7 @@ def decode_dataset( decoding_graph=decoding_graph, word_table=word_table, batch=batch, + context_graph=context_graph, ) for name, (hyps, timestamps_hyp) in hyps_dict.items(): @@ -728,6 +760,12 @@ def main(): "fast_beam_search_nbest_oracle", "modified_beam_search", ) + + if os.path.exists(params.context_file): + params.has_contexts = True + else: + params.has_contexts = False + params.res_dir = params.exp_dir / params.decoding_method if params.iter > 0: @@ -750,6 +788,10 @@ def main(): params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" elif "beam_search" in params.decoding_method: params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + if params.has_contexts: + params.suffix += f"-context-score-{params.context_score}" + else: + params.suffix += "-no-context-words" else: params.suffix += f"-context-{params.context_size}" params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" @@ -881,6 +923,18 @@ def main(): decoding_graph = None word_table = None + if params.decoding_method == "modified_beam_search": + if os.path.exists(params.context_file): + contexts = [] + for line in open(params.context_file).readlines(): + contexts.append(line.strip()) + context_graph = ContextGraph(params.context_score) + context_graph.build(sp.encode(contexts)) + else: + context_graph = None + else: + context_graph = None + num_param = sum([p.numel() for p in model.parameters()]) logging.info(f"Number of model parameters: {num_param}") @@ -905,6 +959,7 @@ def main(): sp=sp, word_table=word_table, decoding_graph=decoding_graph, + context_graph=context_graph, ) save_results( diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py index c9e30e737..7cb2e1048 100644 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py @@ -106,7 +106,7 @@ class WenetSpeechAsrDataModule: group.add_argument( "--num-buckets", type=int, - default=300, + default=30, help="The number of buckets for the DynamicBucketingSampler" "(you might want to increase it for larger datasets).", ) @@ -364,7 +364,7 @@ class WenetSpeechAsrDataModule: return valid_dl def test_dataloaders(self, cuts: CutSet) -> DataLoader: - logging.debug("About to create test dataset") + logging.info("About to create test dataset") test = K2SpeechRecognitionDataset( input_strategy=OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) if self.args.on_the_fly_feats diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py index 46ba6b005..dc431578c 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py @@ -92,7 +92,7 @@ When training with the L subset, the streaming usage: --causal-convolution 1 \ --decode-chunk-size 16 \ --left-context 64 - + (4) modified beam search with RNNLM shallow fusion ./pruned_transducer_stateless5/decode.py \ --epoch 35 \ @@ -112,8 +112,10 @@ When training with the L subset, the streaming usage: import argparse +import glob import logging import math +import os from collections import defaultdict from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -133,7 +135,8 @@ from beam_search import ( ) from train import add_model_arguments, get_params, get_transducer_model -from icefall import LmScorer, NgramLm +from icefall import ContextGraph, LmScorer, NgramLm +from icefall.char_graph_compiler import CharCtcTrainingGraphCompiler from icefall.checkpoint import ( average_checkpoints, average_checkpoints_with_averaged_model, @@ -307,6 +310,26 @@ def get_parser(): help="left context can be seen during decoding (in frames after subsampling)", ) + parser.add_argument( + "--context-score", + type=float, + default=2, + help=""" + The bonus score of each token for the context biasing words/phrases. + Used only when --decoding_method is modified_beam_search. + """, + ) + + parser.add_argument( + "--context-file", + type=str, + default="", + help=""" + The path of the context biasing lists, one word/phrase each line + Used only when --decoding_method is modified_beam_search. + """, + ) + parser.add_argument( "--use-shallow-fusion", type=str2bool, @@ -362,6 +385,7 @@ def decode_one_batch( lexicon: Lexicon, batch: dict, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, ngram_lm: Optional[NgramLm] = None, ngram_lm_scale: float = 1.0, LM: Optional[LmScorer] = None, @@ -402,14 +426,13 @@ def decode_one_batch( supervisions = batch["supervisions"] feature_lens = supervisions["num_frames"].to(device) - feature_lens += params.left_context - feature = torch.nn.functional.pad( - feature, - pad=(0, 0, 0, params.left_context), - value=LOG_EPS, - ) - if params.simulate_streaming: + feature_lens += params.left_context + feature = torch.nn.functional.pad( + feature, + pad=(0, 0, 0, params.left_context), + value=LOG_EPS, + ) encoder_out, encoder_out_lens, _ = model.encoder.streaming_forward( x=feature, x_lens=feature_lens, @@ -448,6 +471,7 @@ def decode_one_batch( encoder_out=encoder_out, beam=params.beam_size, encoder_out_lens=encoder_out_lens, + context_graph=context_graph, ) for i in range(encoder_out.size(0)): hyps.append([lexicon.token_table[idx] for idx in hyp_tokens[i]]) @@ -509,7 +533,12 @@ def decode_one_batch( ): hyps } else: - return {f"beam_size_{params.beam_size}": hyps} + key = f"beam_size_{params.beam_size}" + if params.has_contexts: + key += f"-context-score-{params.context_score}" + else: + key += "-no-context-words" + return {key: hyps} def decode_dataset( @@ -518,6 +547,7 @@ def decode_dataset( model: nn.Module, lexicon: Lexicon, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, ngram_lm: Optional[NgramLm] = None, ngram_lm_scale: float = 1.0, LM: Optional[LmScorer] = None, @@ -567,6 +597,7 @@ def decode_dataset( lexicon=lexicon, decoding_graph=decoding_graph, batch=batch, + context_graph=context_graph, ngram_lm=ngram_lm, ngram_lm_scale=ngram_lm_scale, LM=LM, @@ -646,6 +677,12 @@ def main(): "modified_beam_search_lm_shallow_fusion", "modified_beam_search_LODR", ) + + if os.path.exists(params.context_file): + params.has_contexts = True + else: + params.has_contexts = False + params.res_dir = params.exp_dir / params.decoding_method params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" @@ -655,6 +692,10 @@ def main(): params.suffix += f"-max-states-{params.max_states}" elif "beam_search" in params.decoding_method: params.suffix += f"-beam-{params.beam_size}" + if params.has_contexts: + params.suffix += f"-context-score-{params.context_score}" + else: + params.suffix += "-no-contexts-words" else: params.suffix += f"-context-{params.context_size}" params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" @@ -684,11 +725,15 @@ def main(): logging.info(f"Device: {device}") - # import pdb; pdb.set_trace() lexicon = Lexicon(params.lang_dir) params.blank_id = lexicon.token_table[""] params.vocab_size = max(lexicon.tokens) + 1 + graph_compiler = CharCtcTrainingGraphCompiler( + lexicon=lexicon, + device=device, + ) + if params.simulate_streaming: assert ( params.causal_convolution @@ -816,6 +861,19 @@ def main(): else: decoding_graph = None + if params.decoding_method == "modified_beam_search": + if os.path.exists(params.context_file): + contexts_text = [] + for line in open(params.context_file).readlines(): + contexts_text.append(line.strip()) + contexts = graph_compiler.texts_to_ids(contexts_text) + context_graph = ContextGraph(params.context_score) + context_graph.build(contexts) + else: + context_graph = None + else: + context_graph = None + num_param = sum([p.numel() for p in model.parameters()]) logging.info(f"Number of model parameters: {num_param}") @@ -833,15 +891,16 @@ def main(): test_meeting_dl = wenetspeech.test_dataloaders(test_meeting_cuts) test_sets = ["DEV", "TEST_NET", "TEST_MEETING"] - test_dl = [dev_dl, test_net_dl, test_meeting_dl] + test_dls = [dev_dl, test_net_dl, test_meeting_dl] - for test_set, test_dl in zip(test_sets, test_dl): + for test_set, test_dl in zip(test_sets, test_dls): results_dict = decode_dataset( dl=test_dl, params=params, model=model, lexicon=lexicon, decoding_graph=decoding_graph, + context_graph=context_graph, ngram_lm=ngram_lm, ngram_lm_scale=ngram_lm_scale, LM=LM, diff --git a/icefall/__init__.py b/icefall/__init__.py index 5d846b41d..05e2b408c 100644 --- a/icefall/__init__.py +++ b/icefall/__init__.py @@ -23,6 +23,8 @@ from .checkpoint import ( save_checkpoint_with_global_batch_idx, ) +from .context_graph import ContextGraph, ContextState + from .decode import ( get_lattice, nbest_decoding, diff --git a/icefall/context_graph.py b/icefall/context_graph.py new file mode 100644 index 000000000..c78de30f6 --- /dev/null +++ b/icefall/context_graph.py @@ -0,0 +1,412 @@ +# Copyright 2023 Xiaomi Corp. (authors: Wei Kang) +# +# See ../LICENSE for clarification regarding multiple authors +# +# 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 shutil +from collections import deque +from typing import Dict, List, Optional, Tuple + + +class ContextState: + """The state in ContextGraph""" + + def __init__( + self, + id: int, + token: int, + token_score: float, + node_score: float, + local_node_score: float, + is_end: bool, + ): + """Create a ContextState. + + Args: + id: + The node id, only for visualization now. A node is in [0, graph.num_nodes). + The id of the root node is always 0. + token: + The token id. + score: + The bonus for each token during decoding, which will hopefully + boost the token up to survive beam search. + node_score: + The accumulated bonus from root of graph to current node, it will be + used to calculate the score for fail arc. + local_node_score: + The accumulated bonus from last ``end_node``(node with is_end true) + to current_node, it will be used to calculate the score for fail arc. + Node: The local_node_score of a ``end_node`` is 0. + is_end: + True if current token is the end of a context. + """ + self.id = id + self.token = token + self.token_score = token_score + self.node_score = node_score + self.local_node_score = local_node_score + self.is_end = is_end + self.next = {} + self.fail = None + self.output = None + + +class ContextGraph: + """The ContextGraph is modified from Aho-Corasick which is mainly + a Trie with a fail arc for each node. + See https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_algorithm for more details + of Aho-Corasick algorithm. + + A ContextGraph contains some words / phrases that we expect to boost their + scores during decoding. If the substring of a decoded sequence matches the word / phrase + in the ContextGraph, we will give the decoded sequence a bonus to make it survive + beam search. + """ + + def __init__(self, context_score: float): + """Initialize a ContextGraph with the given ``context_score``. + + A root node will be created (**NOTE:** the token of root is hardcoded to -1). + + Args: + context_score: + The bonus score for each token(note: NOT for each word/phrase, it means longer + word/phrase will have larger bonus score, they have to be matched though). + """ + self.context_score = context_score + self.num_nodes = 0 + self.root = ContextState( + id=self.num_nodes, + token=-1, + token_score=0, + node_score=0, + local_node_score=0, + is_end=False, + ) + self.root.fail = self.root + + def _fill_fail_output(self): + """This function fills the fail arc for each trie node, it can be computed + in linear time by performing a breadth-first search starting from the root. + See https://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_algorithm for the + details of the algorithm. + """ + queue = deque() + for token, node in self.root.next.items(): + node.fail = self.root + queue.append(node) + while queue: + current_node = queue.popleft() + for token, node in current_node.next.items(): + fail = current_node.fail + if token in fail.next: + fail = fail.next[token] + else: + fail = fail.fail + while token not in fail.next: + fail = fail.fail + if fail.token == -1: # root + break + if token in fail.next: + fail = fail.next[token] + node.fail = fail + # fill the output arc + output = node.fail + while not output.is_end: + output = output.fail + if output.token == -1: # root + output = None + break + node.output = output + queue.append(node) + + def build(self, token_ids: List[List[int]]): + """Build the ContextGraph from a list of token list. + It first build a trie from the given token lists, then fill the fail arc + for each trie node. + + See https://en.wikipedia.org/wiki/Trie for how to build a trie. + + Args: + token_ids: + The given token lists to build the ContextGraph, it is a list of token list, + each token list contains the token ids for a word/phrase. The token id + could be an id of a char (modeling with single Chinese char) or an id + of a BPE (modeling with BPEs). + """ + for tokens in token_ids: + node = self.root + for i, token in enumerate(tokens): + if token not in node.next: + self.num_nodes += 1 + is_end = i == len(tokens) - 1 + node.next[token] = ContextState( + id=self.num_nodes, + token=token, + token_score=self.context_score, + node_score=node.node_score + self.context_score, + local_node_score=0 + if is_end + else (node.local_node_score + self.context_score), + is_end=is_end, + ) + node = node.next[token] + self._fill_fail_output() + + def forward_one_step( + self, state: ContextState, token: int + ) -> Tuple[float, ContextState]: + """Search the graph with given state and token. + + Args: + state: + The given token containing trie node to start. + token: + The given token. + + Returns: + Return a tuple of score and next state. + """ + node = None + score = 0 + # token matched + if token in state.next: + node = state.next[token] + score = node.token_score + if state.is_end: + score += state.node_score + else: + # token not matched + # We will trace along the fail arc until it matches the token or reaching + # root of the graph. + node = state.fail + while token not in node.next: + node = node.fail + if node.token == -1: # root + break + + if token in node.next: + node = node.next[token] + + # The score of the fail path + score = node.node_score - state.local_node_score + assert node is not None + matched_score = 0 + output = node.output + while output is not None: + matched_score += output.node_score + output = output.output + return (score + matched_score, node) + + def finalize(self, state: ContextState) -> Tuple[float, ContextState]: + """When reaching the end of the decoded sequence, we need to finalize + the matching, the purpose is to subtract the added bonus score for the + state that is not the end of a word/phrase. + + Args: + state: + The given state(trie node). + + Returns: + Return a tuple of score and next state. If state is the end of a word/phrase + the score is zero, otherwise the score is the score of a implicit fail arc + to root. The next state is always root. + """ + # The score of the fail arc + score = -state.node_score + if state.is_end: + score = 0 + return (score, self.root) + + def draw( + self, + title: Optional[str] = None, + filename: Optional[str] = "", + symbol_table: Optional[Dict[int, str]] = None, + ) -> "Digraph": # noqa + + """Visualize a ContextGraph via graphviz. + + Render ContextGraph as an image via graphviz, and return the Digraph object; + and optionally save to file `filename`. + `filename` must have a suffix that graphviz understands, such as + `pdf`, `svg` or `png`. + + Note: + You need to install graphviz to use this function:: + + pip install graphviz + + Args: + title: + Title to be displayed in image, e.g. 'A simple FSA example' + filename: + Filename to (optionally) save to, e.g. 'foo.png', 'foo.svg', + 'foo.png' (must have a suffix that graphviz understands). + symbol_table: + Map the token ids to symbols. + Returns: + A Diagraph from grahpviz. + """ + + try: + import graphviz + except Exception: + print("You cannot use `to_dot` unless the graphviz package is installed.") + raise + + graph_attr = { + "rankdir": "LR", + "size": "8.5,11", + "center": "1", + "orientation": "Portrait", + "ranksep": "0.4", + "nodesep": "0.25", + } + if title is not None: + graph_attr["label"] = title + + default_node_attr = { + "shape": "circle", + "style": "bold", + "fontsize": "14", + } + + final_state_attr = { + "shape": "doublecircle", + "style": "bold", + "fontsize": "14", + } + + final_state = -1 + dot = graphviz.Digraph(name="Context Graph", graph_attr=graph_attr) + + seen = set() + queue = deque() + queue.append(self.root) + # root id is always 0 + dot.node("0", label="0", **default_node_attr) + dot.edge("0", "0", color="red") + seen.add(0) + + while len(queue): + current_node = queue.popleft() + for token, node in current_node.next.items(): + if node.id not in seen: + node_score = f"{node.node_score:.2f}".rstrip("0").rstrip(".") + local_node_score = f"{node.local_node_score:.2f}".rstrip( + "0" + ).rstrip(".") + label = f"{node.id}/({node_score},{local_node_score})" + if node.is_end: + dot.node(str(node.id), label=label, **final_state_attr) + else: + dot.node(str(node.id), label=label, **default_node_attr) + seen.add(node.id) + weight = f"{node.token_score:.2f}".rstrip("0").rstrip(".") + label = str(token) if symbol_table is None else symbol_table[token] + dot.edge(str(current_node.id), str(node.id), label=f"{label}/{weight}") + dot.edge( + str(node.id), + str(node.fail.id), + color="red", + ) + if node.output is not None: + dot.edge( + str(node.id), + str(node.output.id), + color="green", + ) + queue.append(node) + + if filename: + _, extension = os.path.splitext(filename) + if extension == "" or extension[0] != ".": + raise ValueError( + "Filename needs to have a suffix like .png, .pdf, .svg: {}".format( + filename + ) + ) + + import tempfile + + with tempfile.TemporaryDirectory() as tmp_dir: + temp_fn = dot.render( + filename="temp", + directory=tmp_dir, + format=extension[1:], + cleanup=True, + ) + + shutil.move(temp_fn, filename) + + return dot + + +if __name__ == "__main__": + contexts_str = [ + "S", + "HE", + "SHE", + "SHELL", + "HIS", + "HERS", + "HELLO", + "THIS", + "THEM", + ] + contexts = [] + for s in contexts_str: + contexts.append([ord(x) for x in s]) + + context_graph = ContextGraph(context_score=1) + context_graph.build(contexts) + + symbol_table = {} + for contexts in contexts_str: + for s in contexts: + symbol_table[ord(s)] = s + + context_graph.draw( + title="Graph for: " + " / ".join(contexts_str), + filename="context_graph.pdf", + symbol_table=symbol_table, + ) + + queries = { + "HEHERSHE": 14, # "HE", "HE", "HERS", "S", "SHE", "HE" + "HERSHE": 12, # "HE", "HERS", "S", "SHE", "HE" + "HISHE": 9, # "HIS", "S", "SHE", "HE" + "SHED": 6, # "S", "SHE", "HE" + "HELL": 2, # "HE" + "HELLO": 7, # "HE", "HELLO" + "DHRHISQ": 4, # "HIS", "S" + "THEN": 2, # "HE" + } + for query, expected_score in queries.items(): + total_scores = 0 + state = context_graph.root + for q in query: + score, state = context_graph.forward_one_step(state, ord(q)) + total_scores += score + score, state = context_graph.finalize(state) + assert state.token == -1, state.token + total_scores += score + assert total_scores == expected_score, ( + total_scores, + expected_score, + query, + ) From c0de78d3c0ea04a08d849e37a8e22ff48a5e087a Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 5 Jun 2023 15:49:41 +0800 Subject: [PATCH 025/100] Add data preparation for the MuST-C speech translation corpus (#1107) --- egs/librispeech/ASR/prepare.sh | 2 +- egs/must_c/ST/local/compute_fbank_musan.py | 1 + egs/must_c/ST/local/compute_fbank_must_c.py | 155 ++++++++++++++ egs/must_c/ST/local/get_text.py | 34 +++ egs/must_c/ST/local/get_words.py | 48 +++++ egs/must_c/ST/local/normalize_punctuation.py | 169 +++++++++++++++ egs/must_c/ST/local/prepare_lang.py | 1 + egs/must_c/ST/local/prepare_lang_bpe.py | 1 + egs/must_c/ST/local/preprocess_must_c.py | 96 +++++++++ .../ST/local/remove_non_native_characters.py | 21 ++ egs/must_c/ST/local/remove_punctuation.py | 41 ++++ .../ST/local/test_normalize_punctuation.py | 197 ++++++++++++++++++ .../test_remove_non_native_characters.py | 26 +++ .../ST/local/test_remove_punctuation.py | 17 ++ egs/must_c/ST/local/train_bpe_model.py | 1 + egs/must_c/ST/local/validate_bpe_lexicon.py | 1 + egs/must_c/ST/prepare.sh | 173 +++++++++++++++ egs/must_c/ST/shared | 1 + 18 files changed, 984 insertions(+), 1 deletion(-) create mode 120000 egs/must_c/ST/local/compute_fbank_musan.py create mode 100755 egs/must_c/ST/local/compute_fbank_must_c.py create mode 100755 egs/must_c/ST/local/get_text.py create mode 100755 egs/must_c/ST/local/get_words.py create mode 100644 egs/must_c/ST/local/normalize_punctuation.py create mode 120000 egs/must_c/ST/local/prepare_lang.py create mode 120000 egs/must_c/ST/local/prepare_lang_bpe.py create mode 100755 egs/must_c/ST/local/preprocess_must_c.py create mode 100755 egs/must_c/ST/local/remove_non_native_characters.py create mode 100644 egs/must_c/ST/local/remove_punctuation.py create mode 100755 egs/must_c/ST/local/test_normalize_punctuation.py create mode 100755 egs/must_c/ST/local/test_remove_non_native_characters.py create mode 100755 egs/must_c/ST/local/test_remove_punctuation.py create mode 120000 egs/must_c/ST/local/train_bpe_model.py create mode 120000 egs/must_c/ST/local/validate_bpe_lexicon.py create mode 100755 egs/must_c/ST/prepare.sh create mode 120000 egs/must_c/ST/shared diff --git a/egs/librispeech/ASR/prepare.sh b/egs/librispeech/ASR/prepare.sh index 8342d5212..8ce1eb478 100755 --- a/egs/librispeech/ASR/prepare.sh +++ b/egs/librispeech/ASR/prepare.sh @@ -107,7 +107,7 @@ fi if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then log "Stage 2: Prepare musan manifest" # We assume that you have downloaded the musan corpus - # to data/musan + # to $dl_dir/musan mkdir -p data/manifests if [ ! -e data/manifests/.musan.done ]; then lhotse prepare musan $dl_dir/musan data/manifests diff --git a/egs/must_c/ST/local/compute_fbank_musan.py b/egs/must_c/ST/local/compute_fbank_musan.py new file mode 120000 index 000000000..5833f2484 --- /dev/null +++ b/egs/must_c/ST/local/compute_fbank_musan.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/compute_fbank_musan.py \ No newline at end of file diff --git a/egs/must_c/ST/local/compute_fbank_must_c.py b/egs/must_c/ST/local/compute_fbank_must_c.py new file mode 100755 index 000000000..84de099d1 --- /dev/null +++ b/egs/must_c/ST/local/compute_fbank_must_c.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +""" +This file computes fbank features of the MuST-C dataset. +It looks for manifests in the directory "in_dir" and write +generated features to "out_dir". +""" +import argparse +import logging +from pathlib import Path + +import torch +from lhotse import ( + CutSet, + Fbank, + FbankConfig, + FeatureSet, + LilcomChunkyWriter, + load_manifest, +) + +from icefall.utils import str2bool + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--in-dir", + type=Path, + required=True, + help="Input manifest directory", + ) + + parser.add_argument( + "--out-dir", + type=Path, + required=True, + help="Output directory where generated fbank features are saved.", + ) + + parser.add_argument( + "--tgt-lang", + type=str, + required=True, + help="Target language, e.g., zh, de, fr.", + ) + + parser.add_argument( + "--num-jobs", + type=int, + default=1, + help="Number of jobs for computing features", + ) + + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="""True to enable speed perturb with factors 0.9 and 1.1 on + the train subset. False (by default) to disable speed perturb. + """, + ) + + return parser.parse_args() + + +def compute_fbank_must_c( + in_dir: Path, + out_dir: Path, + tgt_lang: str, + num_jobs: int, + perturb_speed: bool, +): + out_dir.mkdir(parents=True, exist_ok=True) + + extractor = Fbank(FbankConfig(num_mel_bins=80)) + + parts = ["dev", "tst-COMMON", "tst-HE", "train"] + + prefix = "must_c" + suffix = "jsonl.gz" + for p in parts: + logging.info(f"Processing {p}") + + cuts_path = f"{out_dir}/{prefix}_feats_en-{tgt_lang}_{p}" + if perturb_speed and p == "train": + cuts_path += "_sp" + + cuts_path += ".jsonl.gz" + + if Path(cuts_path).is_file(): + logging.info(f"{cuts_path} exists - skipping") + continue + + recordings_filename = in_dir / f"{prefix}_recordings_en-{tgt_lang}_{p}.jsonl.gz" + supervisions_filename = ( + in_dir / f"{prefix}_supervisions_en-{tgt_lang}_{p}_norm_rm.jsonl.gz" + ) + assert recordings_filename.is_file(), recordings_filename + assert supervisions_filename.is_file(), supervisions_filename + cut_set = CutSet.from_manifests( + recordings=load_manifest(recordings_filename), + supervisions=load_manifest(supervisions_filename), + ) + if perturb_speed and p == "train": + logging.info("Speed perturbing for the train dataset") + cut_set = cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) + storage_path = f"{out_dir}/{prefix}_feats_en-{tgt_lang}_{p}_sp" + else: + storage_path = f"{out_dir}/{prefix}_feats_en-{tgt_lang}_{p}" + + cut_set = cut_set.compute_and_store_features( + extractor=extractor, + storage_path=storage_path, + num_jobs=num_jobs, + storage_type=LilcomChunkyWriter, + ) + + logging.info("About to split cuts into smaller chunks.") + cut_set = cut_set.trim_to_supervisions( + keep_overlapping=False, min_duration=None + ) + + logging.info(f"Saving to {cuts_path}") + cut_set.to_file(cuts_path) + logging.info(f"Saved to {cuts_path}") + + +def main(): + args = get_args() + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + logging.info(vars(args)) + assert args.in_dir.is_dir(), args.in_dir + + compute_fbank_must_c( + in_dir=args.in_dir, + out_dir=args.out_dir, + tgt_lang=args.tgt_lang, + num_jobs=args.num_jobs, + perturb_speed=args.perturb_speed, + ) + + +if __name__ == "__main__": + main() diff --git a/egs/must_c/ST/local/get_text.py b/egs/must_c/ST/local/get_text.py new file mode 100755 index 000000000..558ab6de8 --- /dev/null +++ b/egs/must_c/ST/local/get_text.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) +""" +This file prints the text field of supervisions from cutset to the console +""" + +import argparse + +from lhotse import load_manifest_lazy +from pathlib import Path + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "manifest", + type=Path, + help="Input manifest", + ) + return parser.parse_args() + + +def main(): + args = get_args() + assert args.manifest.is_file(), args.manifest + + cutset = load_manifest_lazy(args.manifest) + for c in cutset: + for sup in c.supervisions: + print(sup.text) + + +if __name__ == "__main__": + main() diff --git a/egs/must_c/ST/local/get_words.py b/egs/must_c/ST/local/get_words.py new file mode 100755 index 000000000..a61f60860 --- /dev/null +++ b/egs/must_c/ST/local/get_words.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) +""" +This file generates words.txt from the given transcript file. +""" + +import argparse + +from pathlib import Path + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "transcript", + type=Path, + help="Input transcript file", + ) + return parser.parse_args() + + +def main(): + args = get_args() + assert args.transcript.is_file(), args.transcript + + word_set = set() + with open(args.transcript) as f: + for line in f: + words = line.strip().split() + for w in words: + word_set.add(w) + + # Note: reserved* should be kept in sync with ./local/prepare_lang_bpe.py + reserved1 = ["", "!SIL", "", ""] + reserved2 = ["#0", "", ""] + + for w in reserved1 + reserved2: + assert w not in word_set, w + + words = sorted(list(word_set)) + words = reserved1 + words + reserved2 + + for i, w in enumerate(words): + print(w, i) + + +if __name__ == "__main__": + main() diff --git a/egs/must_c/ST/local/normalize_punctuation.py b/egs/must_c/ST/local/normalize_punctuation.py new file mode 100644 index 000000000..efd47e091 --- /dev/null +++ b/egs/must_c/ST/local/normalize_punctuation.py @@ -0,0 +1,169 @@ +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) +import re + + +def normalize_punctuation(s: str, lang: str) -> str: + """ + This function implements + https://github.com/moses-smt/mosesdecoder/blob/master/scripts/tokenizer/normalize-punctuation.perl + + Args: + s: + A string to be normalized. + lang: + The language to which `s` belongs + Returns: + Return a normalized string. + """ + # s/\r//g; + s = re.sub("\r", "", s) + + # remove extra spaces + # s/\(/ \(/g; + s = re.sub("\(", " (", s) # add a space before ( + + # s/\)/\) /g; s/ +/ /g; + s = re.sub("\)", ") ", s) # add a space after ) + s = re.sub(" +", " ", s) # convert multiple spaces to one + + # s/\) ([\.\!\:\?\;\,])/\)$1/g; + s = re.sub("\) ([\.\!\:\?\;\,])", r")\1", s) + + # s/\( /\(/g; + s = re.sub("\( ", "(", s) # remove space after ( + + # s/ \)/\)/g; + s = re.sub(" \)", ")", s) # remove space before ) + + # s/(\d) \%/$1\%/g; + s = re.sub("(\d) \%", r"\1%", s) # remove space between a digit and % + + # s/ :/:/g; + s = re.sub(" :", ":", s) # remove space before : + + # s/ ;/;/g; + s = re.sub(" ;", ";", s) # remove space before ; + + # normalize unicode punctuation + # s/\`/\'/g; + s = re.sub("`", "'", s) # replace ` with ' + + # s/\'\'/ \" /g; + s = re.sub("''", '"', s) # replace '' with " + + # s/„/\"/g; + s = re.sub("„", '"', s) # replace „ with " + + # s/“/\"/g; + s = re.sub("“", '"', s) # replace “ with " + + # s/”/\"/g; + s = re.sub("”", '"', s) # replace ” with " + + # s/–/-/g; + s = re.sub("–", "-", s) # replace – with - + + # s/—/ - /g; s/ +/ /g; + s = re.sub("—", " - ", s) + s = re.sub(" +", " ", s) # convert multiple spaces to one + + # s/´/\'/g; + s = re.sub("´", "'", s) + + # s/([a-z])‘([a-z])/$1\'$2/gi; + s = re.sub("([a-z])‘([a-z])", r"\1'\2", s, flags=re.IGNORECASE) + + # s/([a-z])’([a-z])/$1\'$2/gi; + s = re.sub("([a-z])’([a-z])", r"\1'\2", s, flags=re.IGNORECASE) + + # s/‘/\'/g; + s = re.sub("‘", "'", s) + + # s/‚/\'/g; + s = re.sub("‚", "'", s) + + # s/’/\"/g; + s = re.sub("’", '"', s) + + # s/''/\"/g; + s = re.sub("''", '"', s) + + # s/´´/\"/g; + s = re.sub("´´", '"', s) + + # s/…/.../g; + s = re.sub("…", "...", s) + + # French quotes + + # s/ « / \"/g; + s = re.sub(" « ", ' "', s) + + # s/« /\"/g; + s = re.sub("« ", '"', s) + + # s/«/\"/g; + s = re.sub("«", '"', s) + + # s/ » /\" /g; + s = re.sub(" » ", '" ', s) + + # s/ »/\"/g; + s = re.sub(" »", '"', s) + + # s/»/\"/g; + s = re.sub("»", '"', s) + + # handle pseudo-spaces + + # s/ \%/\%/g; + s = re.sub(" %", r"%", s) + + # s/nº /nº /g; + s = re.sub("nº ", "nº ", s) + + # s/ :/:/g; + s = re.sub(" :", ":", s) + + # s/ ºC/ ºC/g; + s = re.sub(" ºC", " ºC", s) + + # s/ cm/ cm/g; + s = re.sub(" cm", " cm", s) + + # s/ \?/\?/g; + s = re.sub(" \?", "\?", s) + + # s/ \!/\!/g; + s = re.sub(" \!", "\!", s) + + # s/ ;/;/g; + s = re.sub(" ;", ";", s) + + # s/, /, /g; s/ +/ /g; + s = re.sub(", ", ", ", s) + s = re.sub(" +", " ", s) + + if lang == "en": + # English "quotation," followed by comma, style + # s/\"([,\.]+)/$1\"/g; + s = re.sub('"([,\.]+)', r'\1"', s) + elif lang in ("cs", "cz"): + # Czech is confused + pass + else: + # German/Spanish/French "quotation", followed by comma, style + # s/,\"/\",/g; + s = re.sub(',"', '",', s) + + # s/(\.+)\"(\s*[^<])/\"$1$2/g; # don't fix period at end of sentence + s = re.sub('(\.+)"(\s*[^<])', r'"\1\2', s) + + if lang in ("de", "es", "cz", "cs", "fr"): + # s/(\d) (\d)/$1,$2/g; + s = re.sub("(\d) (\d)", r"\1,\2", s) + else: + # s/(\d) (\d)/$1.$2/g; + s = re.sub("(\d) (\d)", r"\1.\2", s) + + return s diff --git a/egs/must_c/ST/local/prepare_lang.py b/egs/must_c/ST/local/prepare_lang.py new file mode 120000 index 000000000..747f2ab39 --- /dev/null +++ b/egs/must_c/ST/local/prepare_lang.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/prepare_lang.py \ No newline at end of file diff --git a/egs/must_c/ST/local/prepare_lang_bpe.py b/egs/must_c/ST/local/prepare_lang_bpe.py new file mode 120000 index 000000000..36b40e7fc --- /dev/null +++ b/egs/must_c/ST/local/prepare_lang_bpe.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/prepare_lang_bpe.py \ No newline at end of file diff --git a/egs/must_c/ST/local/preprocess_must_c.py b/egs/must_c/ST/local/preprocess_must_c.py new file mode 100755 index 000000000..1ba282bf4 --- /dev/null +++ b/egs/must_c/ST/local/preprocess_must_c.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) +""" +This script normalizes transcripts from supervisions. + +Usage: + ./local/preprocess_must_c.py \ + --manifest-dir ./data/manifests/v1.0/ \ + --tgt-lang de +""" + +import argparse +import logging +import re +from functools import partial +from pathlib import Path + +from lhotse.recipes.utils import read_manifests_if_cached +from normalize_punctuation import normalize_punctuation +from remove_non_native_characters import remove_non_native_characters +from remove_punctuation import remove_punctuation + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--manifest-dir", + type=Path, + required=True, + help="Manifest directory", + ) + parser.add_argument( + "--tgt-lang", + type=str, + required=True, + help="Target language, e.g., zh, de, fr.", + ) + return parser.parse_args() + + +def preprocess_must_c(manifest_dir: Path, tgt_lang: str): + normalize_punctuation_lang = partial(normalize_punctuation, lang=tgt_lang) + remove_non_native_characters_lang = partial( + remove_non_native_characters, lang=tgt_lang + ) + + prefix = "must_c" + suffix = "jsonl.gz" + parts = ["dev", "tst-COMMON", "tst-HE", "train"] + for p in parts: + logging.info(f"Processing {p}") + name = f"en-{tgt_lang}_{p}" + + # norm: normalization + # rm: remove punctuation + dst_name = manifest_dir / f"must_c_supervisions_{name}_norm_rm.jsonl.gz" + if dst_name.is_file(): + logging.info(f"{dst_name} exists - skipping") + continue + + manifests = read_manifests_if_cached( + dataset_parts=name, + output_dir=manifest_dir, + prefix=prefix, + suffix=suffix, + types=("supervisions",), + ) + if name not in manifests: + raise RuntimeError(f"Processing {p} failed.") + + supervisions = manifests[name]["supervisions"] + supervisions = supervisions.transform_text(normalize_punctuation_lang) + supervisions = supervisions.transform_text(remove_punctuation) + supervisions = supervisions.transform_text(lambda x: x.lower()) + supervisions = supervisions.transform_text(remove_non_native_characters_lang) + supervisions = supervisions.transform_text(lambda x: re.sub(" +", " ", x)) + + supervisions.to_file(dst_name) + + +def main(): + args = get_args() + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + logging.info(vars(args)) + assert args.manifest_dir.is_dir(), args.manifest_dir + + preprocess_must_c( + manifest_dir=args.manifest_dir, + tgt_lang=args.tgt_lang, + ) + + +if __name__ == "__main__": + main() diff --git a/egs/must_c/ST/local/remove_non_native_characters.py b/egs/must_c/ST/local/remove_non_native_characters.py new file mode 100755 index 000000000..f61fbd16b --- /dev/null +++ b/egs/must_c/ST/local/remove_non_native_characters.py @@ -0,0 +1,21 @@ +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +import re + + +def remove_non_native_characters(s: str, lang: str): + if lang == "de": + # ä -> ae + # ö -> oe + # ü -> ue + # ß -> ss + + s = re.sub("ä", "ae", s) + s = re.sub("ö", "oe", s) + s = re.sub("ü", "ue", s) + s = re.sub("ß", "ss", s) + # keep only a-z and spaces + # note: ' is removed + s = re.sub(r"[^a-z\s]", "", s) + + return s diff --git a/egs/must_c/ST/local/remove_punctuation.py b/egs/must_c/ST/local/remove_punctuation.py new file mode 100644 index 000000000..723946ec3 --- /dev/null +++ b/egs/must_c/ST/local/remove_punctuation.py @@ -0,0 +1,41 @@ +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) +import re +import string + + +def remove_punctuation(s: str) -> str: + """ + It implements https://github.com/espnet/espnet/blob/master/utils/remove_punctuation.pl + """ + + # Remove punctuation except apostrophe + # s//spacemark/g; # for scoring + s = re.sub("", "spacemark", s) + + # s/'/apostrophe/g; + s = re.sub("'", "apostrophe", s) + + # s/[[:punct:]]//g; + s = s.translate(str.maketrans("", "", string.punctuation)) + # string punctuation returns the following string + # !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ + # See + # https://stackoverflow.com/questions/265960/best-way-to-strip-punctuation-from-a-string + + # s/apostrophe/'/g; + s = re.sub("apostrophe", "'", s) + + # s/spacemark//g; # for scoring + s = re.sub("spacemark", "", s) + + # remove whitespace + # s/\s+/ /g; + s = re.sub("\s+", " ", s) + + # s/^\s+//; + s = re.sub("^\s+", "", s) + + # s/\s+$//; + s = re.sub("\s+$", "", s) + + return s diff --git a/egs/must_c/ST/local/test_normalize_punctuation.py b/egs/must_c/ST/local/test_normalize_punctuation.py new file mode 100755 index 000000000..9079858c8 --- /dev/null +++ b/egs/must_c/ST/local/test_normalize_punctuation.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +from normalize_punctuation import normalize_punctuation + + +def test_normalize_punctuation(): + # s/\r//g; + s = "a\r\nb\r\n" + n = normalize_punctuation(s, lang="en") + assert "\r" not in n + assert len(s) - 2 == len(n), (len(s), len(n)) + + # s/\(/ \(/g; + s = "(ab (c" + n = normalize_punctuation(s, lang="en") + assert n == " (ab (c", n + + # s/\)/\) /g; + s = "a)b c)" + n = normalize_punctuation(s, lang="en") + assert n == "a) b c) " + + # s/ +/ /g; + s = " a b c d " + n = normalize_punctuation(s, lang="en") + assert n == " a b c d " + + # s/\) ([\.\!\:\?\;\,])/\)$1/g; + for i in ".!:?;,": + s = f"a) {i}" + n = normalize_punctuation(s, lang="en") + assert n == f"a){i}" + + # s/\( /\(/g; + s = "a( b" + n = normalize_punctuation(s, lang="en") + assert n == "a (b", n + + # s/ \)/\)/g; + s = "ab ) a" + n = normalize_punctuation(s, lang="en") + assert n == "ab) a", n + + # s/(\d) \%/$1\%/g; + s = "1 %a" + n = normalize_punctuation(s, lang="en") + assert n == "1%a", n + + # s/ :/:/g; + s = "a :" + n = normalize_punctuation(s, lang="en") + assert n == "a:", n + + # s/ ;/;/g; + s = "a ;" + n = normalize_punctuation(s, lang="en") + assert n == "a;", n + + # s/\`/\'/g; + s = "`a`" + n = normalize_punctuation(s, lang="en") + assert n == "'a'", n + + # s/\'\'/ \" /g; + s = "''a''" + n = normalize_punctuation(s, lang="en") + assert n == '"a"', n + + # s/„/\"/g; + s = '„a"' + n = normalize_punctuation(s, lang="en") + assert n == '"a"', n + + # s/“/\"/g; + s = "“a„" + n = normalize_punctuation(s, lang="en") + assert n == '"a"', n + + # s/”/\"/g; + s = "“a”" + n = normalize_punctuation(s, lang="en") + assert n == '"a"', n + + # s/–/-/g; + s = "a–b" + n = normalize_punctuation(s, lang="en") + assert n == "a-b", n + + # s/—/ - /g; s/ +/ /g; + s = "a—b" + n = normalize_punctuation(s, lang="en") + assert n == "a - b", n + + # s/´/\'/g; + s = "a´b" + n = normalize_punctuation(s, lang="en") + assert n == "a'b", n + + # s/([a-z])‘([a-z])/$1\'$2/gi; + for i in "‘’": + s = f"a{i}B" + n = normalize_punctuation(s, lang="en") + assert n == "a'B", n + + s = f"A{i}B" + n = normalize_punctuation(s, lang="en") + assert n == "A'B", n + + s = f"A{i}b" + n = normalize_punctuation(s, lang="en") + assert n == "A'b", n + + # s/‘/\'/g; + # s/‚/\'/g; + for i in "‘‚": + s = f"a{i}b" + n = normalize_punctuation(s, lang="en") + assert n == "a'b", n + + # s/’/\"/g; + s = "’" + n = normalize_punctuation(s, lang="en") + assert n == '"', n + + # s/''/\"/g; + s = "''" + n = normalize_punctuation(s, lang="en") + assert n == '"', n + + # s/´´/\"/g; + s = "´´" + n = normalize_punctuation(s, lang="en") + assert n == '"', n + + # s/…/.../g; + s = "…" + n = normalize_punctuation(s, lang="en") + assert n == "...", n + + # s/ « / \"/g; + s = "a « b" + n = normalize_punctuation(s, lang="en") + assert n == 'a "b', n + + # s/« /\"/g; + s = "a « b" + n = normalize_punctuation(s, lang="en") + assert n == 'a "b', n + + # s/«/\"/g; + s = "a«b" + n = normalize_punctuation(s, lang="en") + assert n == 'a"b', n + + # s/ » /\" /g; + s = " » " + n = normalize_punctuation(s, lang="en") + assert n == '" ', n + + # s/ »/\"/g; + s = " »" + n = normalize_punctuation(s, lang="en") + assert n == '"', n + + # s/»/\"/g; + s = "»" + n = normalize_punctuation(s, lang="en") + assert n == '"', n + + # s/ \%/\%/g; + s = " %" + n = normalize_punctuation(s, lang="en") + assert n == "%", n + + # s/ :/:/g; + s = " :" + n = normalize_punctuation(s, lang="en") + assert n == ":", n + + # s/(\d) (\d)/$1.$2/g; + s = "2 3" + n = normalize_punctuation(s, lang="en") + assert n == "2.3", n + + # s/(\d) (\d)/$1,$2/g; + s = "2 3" + n = normalize_punctuation(s, lang="de") + assert n == "2,3", n + + +def main(): + test_normalize_punctuation() + + +if __name__ == "__main__": + main() diff --git a/egs/must_c/ST/local/test_remove_non_native_characters.py b/egs/must_c/ST/local/test_remove_non_native_characters.py new file mode 100755 index 000000000..ecf8569cf --- /dev/null +++ b/egs/must_c/ST/local/test_remove_non_native_characters.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +from remove_non_native_characters import remove_non_native_characters + + +def test_remove_non_native_characters(): + s = "Ich heiße xxx好的01 fangjun".lower() + n = remove_non_native_characters(s, lang="de") + assert n == "ich heisse xxx fangjun", n + + s = "äÄ".lower() + n = remove_non_native_characters(s, lang="de") + assert n == "aeae", n + + s = "öÖ".lower() + n = remove_non_native_characters(s, lang="de") + assert n == "oeoe", n + + s = "üÜ".lower() + n = remove_non_native_characters(s, lang="de") + assert n == "ueue", n + + +if __name__ == "__main__": + test_remove_non_native_characters() diff --git a/egs/must_c/ST/local/test_remove_punctuation.py b/egs/must_c/ST/local/test_remove_punctuation.py new file mode 100755 index 000000000..a4f318550 --- /dev/null +++ b/egs/must_c/ST/local/test_remove_punctuation.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +from remove_punctuation import remove_punctuation + + +def test_remove_punctuation(): + s = "a,b'c!#" + n = remove_punctuation(s) + assert n == "ab'c", n + + s = " ab " # remove leading and trailing spaces + n = remove_punctuation(s) + assert n == "ab", n + + +if __name__ == "__main__": + test_remove_punctuation() diff --git a/egs/must_c/ST/local/train_bpe_model.py b/egs/must_c/ST/local/train_bpe_model.py new file mode 120000 index 000000000..6fad36421 --- /dev/null +++ b/egs/must_c/ST/local/train_bpe_model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/train_bpe_model.py \ No newline at end of file diff --git a/egs/must_c/ST/local/validate_bpe_lexicon.py b/egs/must_c/ST/local/validate_bpe_lexicon.py new file mode 120000 index 000000000..721bb48e7 --- /dev/null +++ b/egs/must_c/ST/local/validate_bpe_lexicon.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/validate_bpe_lexicon.py \ No newline at end of file diff --git a/egs/must_c/ST/prepare.sh b/egs/must_c/ST/prepare.sh new file mode 100755 index 000000000..d16bb3d0b --- /dev/null +++ b/egs/must_c/ST/prepare.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +# fix segmentation fault reported in https://github.com/k2-fsa/icefall/issues/674 +export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python + +set -eou pipefail + +nj=10 +stage=0 +stop_stage=100 + +version=v1.0 +tgt_lang=de +dl_dir=$PWD/download + +must_c_dir=$dl_dir/must-c/$version/en-$tgt_lang/data + +# We assume dl_dir (download dir) contains the following +# directories and files. +# - $dl_dir/must-c/$version/en-$tgt_lang/data/{dev,train,tst-COMMON,tst-HE} +# +# Please go to https://ict.fbk.eu/must-c-releases/ +# to download and untar the dataset if you have not already done this. + +# - $dl_dir/musan +# This directory contains the following directories downloaded from +# http://www.openslr.org/17/ +# +# - music +# - noise +# - speech + +. shared/parse_options.sh || exit 1 + +# vocab size for sentence piece models. +# It will generate +# data/lang_bpe_${tgt_lang}_xxx +# data/lang_bpe_${tgt_lang}_yyy +# if the array contains xxx, yyy +vocab_sizes=( + # 5000 + # 2000 + # 1000 + 500 +) + +# All files generated by this script are saved in "data". +# You can safely remove "data" and rerun this script to regenerate it. +mkdir -p data + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +log "dl_dir: $dl_dir" + +if [ ! -d $must_c_dir ]; then + log "$must_c_dir does not exist" + exit 1 +fi + +for d in dev train tst-COMMON tst-HE; do + if [ ! -d $must_c_dir/$d ]; then + log "$must_c_dir/$d does not exist!" + exit 1 + fi +done + +if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then + log "Stage 0: Download musan" + if [ ! -d $dl_dir/musan ]; then + lhotse download musan $dl_dir + fi +fi + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + log "Stage 1: Prepare musan manifest" + # We assume that you have downloaded the musan corpus + # to $dl_dir/musan + mkdir -p data/manifests + if [ ! -e data/manifests/.musan.done ]; then + lhotse prepare musan $dl_dir/musan data/manifests + touch data/manifests/.musan.done + fi +fi + +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + log "Stage 2: Prepare must-c $version manifest for target language $tgt_lang" + mkdir -p data/manifests/$version + if [ ! -e data/manifests/$version/.${tgt_lang}.manifests.done ]; then + lhotse prepare must-c \ + -j $nj \ + --tgt-lang $tgt_lang \ + $dl_dir/must-c/$version/ \ + data/manifests/$version/ + + touch data/manifests/$version/.${tgt_lang}.manifests.done + fi +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + log "Stage 3: Text normalization for $version with target language $tgt_lang" + if [ ! -f ./data/manifests/$version/.$tgt_lang.norm.done ]; then + ./local/preprocess_must_c.py \ + --manifest-dir ./data/manifests/$version/ \ + --tgt-lang $tgt_lang + touch ./data/manifests/$version/.$tgt_lang.norm.done + fi +fi + +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + log "Stage 4: Compute fbank for musan" + mkdir -p data/fbank + if [ ! -e data/fbank/.musan.done ]; then + ./local/compute_fbank_musan.py + touch data/fbank/.musan.done + fi +fi + +if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then + log "Stage 5: Compute fbank for $version with target language $tgt_lang" + mkdir -p data/fbank/$version/ + if [ ! -e data/fbank/$version/.$tgt_lang.done ]; then + ./local/compute_fbank_must_c.py \ + --in-dir ./data/manifests/$version/ \ + --out-dir ./data/fbank/$version/ \ + --tgt-lang $tgt_lang \ + --num-jobs $nj + + ./local/compute_fbank_must_c.py \ + --in-dir ./data/manifests/$version/ \ + --out-dir ./data/fbank/$version/ \ + --tgt-lang $tgt_lang \ + --num-jobs $nj \ + --perturb-speed 1 + + touch data/fbank/$version/.$tgt_lang.done + fi +fi + +if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then + log "Stage 6: Prepare BPE based lang for $version with target language $tgt_lang" + + for vocab_size in ${vocab_sizes[@]}; do + lang_dir=data/lang_bpe_${vocab_size}/$version/$tgt_lang/ + mkdir -p $lang_dir + if [ ! -f $lang_dir/transcript_words.txt ]; then + ./local/get_text.py ./data/fbank/$version/must_c_feats_en-${tgt_lang}_train.jsonl.gz > $lang_dir/transcript_words.txt + fi + + if [ ! -f $lang_dir/words.txt ]; then + ./local/get_words.py $lang_dir/transcript_words.txt > $lang_dir/words.txt + fi + + if [ ! -f $lang_dir/bpe.model ]; then + ./local/train_bpe_model.py \ + --lang-dir $lang_dir \ + --vocab-size $vocab_size \ + --transcript $lang_dir/transcript_words.txt + fi + + if [ ! -f $lang_dir/L_disambig.pt ]; then + ./local/prepare_lang_bpe.py --lang-dir $lang_dir + + log "Validating $lang_dir/lexicon.txt" + ./local/validate_bpe_lexicon.py \ + --lexicon $lang_dir/lexicon.txt \ + --bpe-model $lang_dir/bpe.model + fi + done +fi diff --git a/egs/must_c/ST/shared b/egs/must_c/ST/shared new file mode 120000 index 000000000..4cbd91a7e --- /dev/null +++ b/egs/must_c/ST/shared @@ -0,0 +1 @@ +../../../icefall/shared \ No newline at end of file From 3ae47a494058bf8d8b5aaaf6853e9047044b1de8 Mon Sep 17 00:00:00 2001 From: SarahSmitho <56591685+SarahSmitho@users.noreply.github.com> Date: Wed, 7 Jun 2023 11:17:38 +0800 Subject: [PATCH 026/100] verify have installed ffmpeg (#1117) --- egs/commonvoice/ASR/prepare.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/egs/commonvoice/ASR/prepare.sh b/egs/commonvoice/ASR/prepare.sh index 7a583f9c8..3946908c6 100755 --- a/egs/commonvoice/ASR/prepare.sh +++ b/egs/commonvoice/ASR/prepare.sh @@ -63,6 +63,14 @@ log() { log "dl_dir: $dl_dir" +if ! command -v ffmpeg &> /dev/null; then + echo "This dataset requires ffmpeg" + echo "Please install ffmpeg first" + echo "" + echo " sudo apt-get install ffmpeg" + exit 1 +fi + if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then log "Stage 0: Download data" From dca21c2a17b6e62f687e49398517cb57f62203b0 Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Thu, 8 Jun 2023 16:54:05 +0800 Subject: [PATCH 027/100] Fix parameters_names in train.py (#1121) --- .../ASR/pruned_transducer_stateless8/train.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless8/train.py b/egs/librispeech/ASR/pruned_transducer_stateless8/train.py index 41adda012..bee414292 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless8/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless8/train.py @@ -1127,7 +1127,16 @@ def run(rank, world_size, args): logging.info("Using DDP") model = DDP(model, device_ids=[rank], find_unused_parameters=True) - optimizer = ScaledAdam(model.parameters(), lr=params.base_lr, clipping_scale=2.0) + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) From b4c38d754745d0127ec728b4ab717c090777b6f3 Mon Sep 17 00:00:00 2001 From: Peter Ross Date: Mon, 12 Jun 2023 15:51:46 +1000 Subject: [PATCH 028/100] Use symlinks for best epochs (#1123) * utils: add symlink_or_copyfile * pruned_transducer_stateless7: use symlinks (when possible) to output best epochs * Rename function --------- Co-authored-by: Yifan Yang <64255737+yfyeung@users.noreply.github.com> --- .../ASR/pruned_transducer_stateless7/train.py | 15 +++++++++------ icefall/utils.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py index 5ec71ec3f..2b4d51089 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py @@ -50,7 +50,6 @@ import copy import logging import warnings from pathlib import Path -from shutil import copyfile from typing import Any, Dict, Optional, Tuple, Union import k2 @@ -89,6 +88,7 @@ from icefall.utils import ( filter_uneven_sized_batch, setup_logger, str2bool, + symlink_or_copy, ) LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] @@ -601,7 +601,8 @@ def save_checkpoint( """ if rank != 0: return - filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + epoch_basename = f"epoch-{params.cur_epoch}.pt" + filename = params.exp_dir / epoch_basename save_checkpoint_impl( filename=filename, model=model, @@ -615,12 +616,14 @@ def save_checkpoint( ) if params.best_train_epoch == params.cur_epoch: - best_train_filename = params.exp_dir / "best-train-loss.pt" - copyfile(src=filename, dst=best_train_filename) + symlink_or_copy( + exp_dir=params.exp_dir, src=epoch_basename, dst="best-train-loss.pt" + ) if params.best_valid_epoch == params.cur_epoch: - best_valid_filename = params.exp_dir / "best-valid-loss.pt" - copyfile(src=filename, dst=best_valid_filename) + symlink_or_copy( + exp_dir=params.exp_dir, src=epoch_basename, dst="best-valid-loss.pt" + ) def compute_loss( diff --git a/icefall/utils.py b/icefall/utils.py index d002982ec..dfe9a7b42 100644 --- a/icefall/utils.py +++ b/icefall/utils.py @@ -28,6 +28,7 @@ from contextlib import contextmanager from dataclasses import dataclass from datetime import datetime from pathlib import Path +from shutil import copyfile from typing import Dict, Iterable, List, Optional, TextIO, Tuple, Union import k2 @@ -1881,3 +1882,20 @@ def is_cjk(character): ] ] ) + + +def symlink_or_copy(exp_dir: Path, src: str, dst: str): + """ + In the experiment directory, create a symlink pointing to src named dst. + If symlink creation fails (Windows?), fall back to copyfile.""" + + dir_fd = os.open(exp_dir, os.O_RDONLY) + try: + os.remove(dst, dir_fd=dir_fd) + except FileNotFoundError: + pass + try: + os.symlink(src=src, dst=dst, dir_fd=dir_fd) + except OSError: + copyfile(src=exp_dir / src, dst=exp_dir / dst) + os.close(dir_fd) From 0cb71ad3bcb6497788efed5ea2bdedb988b029be Mon Sep 17 00:00:00 2001 From: danfu Date: Mon, 12 Jun 2023 14:02:23 +0800 Subject: [PATCH 029/100] add updated zipformer onnx export (#1108) Co-authored-by: Fangjun Kuang --- .../ASR/zipformer/export-onnx-streaming.py | 775 ++++++++++++++++++ egs/librispeech/ASR/zipformer/export-onnx.py | 624 ++++++++++++++ egs/librispeech/ASR/zipformer/model.py | 2 +- .../zipformer/onnx_pretrained-streaming.py | 544 ++++++++++++ .../ASR/zipformer/onnx_pretrained.py | 1 + egs/librispeech/ASR/zipformer/scaling.py | 30 +- .../ASR/zipformer/scaling_converter.py | 9 + egs/librispeech/ASR/zipformer/subsampling.py | 4 +- egs/librispeech/ASR/zipformer/zipformer.py | 103 ++- 9 files changed, 2037 insertions(+), 55 deletions(-) create mode 100755 egs/librispeech/ASR/zipformer/export-onnx-streaming.py create mode 100755 egs/librispeech/ASR/zipformer/export-onnx.py create mode 100755 egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py create mode 120000 egs/librispeech/ASR/zipformer/onnx_pretrained.py diff --git a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py new file mode 100755 index 000000000..356935657 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py @@ -0,0 +1,775 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) +# Copyright 2023 Danqing Fu (danqing.fu@gmail.com) + +""" +This script exports a transducer model from PyTorch to ONNX. + +We use the pre-trained model from +https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./zipformer/export-onnx-streaming.py \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --num-encoder-layers "2,2,3,4,3,2" \ + --downsampling-factor "1,2,4,8,4,2" \ + --feedforward-dim "512,768,1024,1536,1024,768" \ + --num-heads "4,4,4,8,4,4" \ + --encoder-dim "192,256,384,512,384,256" \ + --query-head-dim 32 \ + --value-head-dim 12 \ + --pos-head-dim 4 \ + --pos-dim 48 \ + --encoder-unmasked-dim "192,192,256,256,256,192" \ + --cnn-module-kernel "31,31,15,15,15,31" \ + --decoder-dim 512 \ + --joiner-dim 512 \ + --causal True \ + --chunk-size 16 \ + --left-context-frames 64 + +The --chunk-size in training is "16,32,64,-1", so we select one of them +(excluding -1) during streaming export. The same applies to `--left-context`, +whose value is "64,128,256,-1". + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +See ./onnx_pretrained-streaming.py for how to use the exported ONNX models. +""" + +import argparse +import logging +from pathlib import Path +from typing import Dict, List, Tuple + +import onnx +import sentencepiece as spm +import torch +import torch.nn as nn +from decoder import Decoder +from onnxruntime.quantization import QuantType, quantize_dynamic +from scaling_converter import convert_scaled_to_non_scaled +from train import add_model_arguments, get_params, get_transducer_model +from zipformer import Zipformer2 + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import str2bool, make_pad_mask + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for averaging. + Note: Epoch counts from 0. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +def add_meta_data(filename: str, meta_data: Dict[str, str]): + """Add meta data to an ONNX model. It is changed in-place. + + Args: + filename: + Filename of the ONNX model to be changed. + meta_data: + Key-value pairs. + """ + model = onnx.load(filename) + for key, value in meta_data.items(): + meta = model.metadata_props.add() + meta.key = key + meta.value = value + + onnx.save(model, filename) + + +class OnnxEncoder(nn.Module): + """A wrapper for Zipformer and the encoder_proj from the joiner""" + + def __init__( + self, encoder: Zipformer2, encoder_embed: nn.Module, encoder_proj: nn.Linear + ): + """ + Args: + encoder: + A Zipformer encoder. + encoder_proj: + The projection layer for encoder from the joiner. + """ + super().__init__() + self.encoder = encoder + self.encoder_embed = encoder_embed + self.encoder_proj = encoder_proj + self.chunk_size = encoder.chunk_size[0] + self.left_context_len = encoder.left_context_frames[0] + self.pad_length = 7 + 2 * 3 + + def forward( + self, + x: torch.Tensor, + states: List[torch.Tensor], + ) -> Tuple[torch.Tensor, torch.Tensor, List[torch.Tensor]]: + N = x.size(0) + T = self.chunk_size * 2 + self.pad_length + x_lens = torch.tensor([T] * N, device=x.device) + left_context_len = self.left_context_len + + cached_embed_left_pad = states[-2] + x, x_lens, new_cached_embed_left_pad = self.encoder_embed.streaming_forward( + x=x, + x_lens=x_lens, + cached_left_pad=cached_embed_left_pad, + ) + assert x.size(1) == self.chunk_size, (x.size(1), self.chunk_size) + + src_key_padding_mask = make_pad_mask(x_lens) + + # processed_mask is used to mask out initial states + processed_mask = torch.arange(left_context_len, device=x.device).expand( + x.size(0), left_context_len + ) + processed_lens = states[-1] # (batch,) + # (batch, left_context_size) + processed_mask = (processed_lens.unsqueeze(1) <= processed_mask).flip(1) + # Update processed lengths + new_processed_lens = processed_lens + x_lens + # (batch, left_context_size + chunk_size) + src_key_padding_mask = torch.cat([processed_mask, src_key_padding_mask], dim=1) + + x = x.permute(1, 0, 2) + encoder_states = states[:-2] + logging.info(f"len_encoder_states={len(encoder_states)}") + ( + encoder_out, + encoder_out_lens, + new_encoder_states, + ) = self.encoder.streaming_forward( + x=x, + x_lens=x_lens, + states=encoder_states, + src_key_padding_mask=src_key_padding_mask, + ) + encoder_out = encoder_out.permute(1, 0, 2) + encoder_out = self.encoder_proj(encoder_out) + # Now encoder_out is of shape (N, T, joiner_dim) + + new_states = new_encoder_states + [ + new_cached_embed_left_pad, + new_processed_lens, + ] + + return encoder_out, new_states + + def get_init_states( + self, + batch_size: int = 1, + device: torch.device = torch.device("cpu"), + ) -> List[torch.Tensor]: + """ + Returns a list of cached tensors of all encoder layers. For layer-i, states[i*6:(i+1)*6] + is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + states[-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + states[-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + """ + states = self.encoder.get_init_states(batch_size, device) + + embed_states = self.encoder_embed.get_init_states(batch_size, device) + states.append(embed_states) + + processed_lens = torch.zeros(batch_size, dtype=torch.int64, device=device) + states.append(processed_lens) + + return states + + +class OnnxDecoder(nn.Module): + """A wrapper for Decoder and the decoder_proj from the joiner""" + + def __init__(self, decoder: Decoder, decoder_proj: nn.Linear): + super().__init__() + self.decoder = decoder + self.decoder_proj = decoder_proj + + def forward(self, y: torch.Tensor) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, context_size). + Returns + Return a 2-D tensor of shape (N, joiner_dim) + """ + need_pad = False + decoder_output = self.decoder(y, need_pad=need_pad) + decoder_output = decoder_output.squeeze(1) + output = self.decoder_proj(decoder_output) + + return output + + +class OnnxJoiner(nn.Module): + """A wrapper for the joiner""" + + def __init__(self, output_linear: nn.Linear): + super().__init__() + self.output_linear = output_linear + + def forward( + self, + encoder_out: torch.Tensor, + decoder_out: torch.Tensor, + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + logit = encoder_out + decoder_out + logit = self.output_linear(torch.tanh(logit)) + return logit + + +def export_encoder_model_onnx( + encoder_model: OnnxEncoder, + encoder_filename: str, + opset_version: int = 11, +) -> None: + encoder_model.encoder.__class__.forward = ( + encoder_model.encoder.__class__.streaming_forward + ) + + decode_chunk_len = encoder_model.chunk_size * 2 + # The encoder_embed subsample features (T - 7) // 2 + # The ConvNeXt module needs (7 - 1) // 2 = 3 frames of right padding after subsampling + T = decode_chunk_len + encoder_model.pad_length + + x = torch.rand(1, T, 80, dtype=torch.float32) + init_state = encoder_model.get_init_states() + num_encoders = len(encoder_model.encoder.encoder_dim) + logging.info(f"num_encoders: {num_encoders}") + logging.info(f"len(init_state): {len(init_state)}") + + inputs = {} + input_names = ["x"] + + outputs = {} + output_names = ["encoder_out"] + + def build_inputs_outputs(tensors, i): + assert len(tensors) == 6, len(tensors) + + # (downsample_left, batch_size, key_dim) + name = f"cached_key_{i}" + logging.info(f"{name}.shape: {tensors[0].shape}") + inputs[name] = {1: "N"} + outputs[f"new_{name}"] = {1: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + # (1, batch_size, downsample_left, nonlin_attn_head_dim) + name = f"cached_nonlin_attn_{i}" + logging.info(f"{name}.shape: {tensors[1].shape}") + inputs[name] = {1: "N"} + outputs[f"new_{name}"] = {1: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + # (downsample_left, batch_size, value_dim) + name = f"cached_val1_{i}" + logging.info(f"{name}.shape: {tensors[2].shape}") + inputs[name] = {1: "N"} + outputs[f"new_{name}"] = {1: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + # (downsample_left, batch_size, value_dim) + name = f"cached_val2_{i}" + logging.info(f"{name}.shape: {tensors[3].shape}") + inputs[name] = {1: "N"} + outputs[f"new_{name}"] = {1: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + # (batch_size, embed_dim, conv_left_pad) + name = f"cached_conv1_{i}" + logging.info(f"{name}.shape: {tensors[4].shape}") + inputs[name] = {0: "N"} + outputs[f"new_{name}"] = {0: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + # (batch_size, embed_dim, conv_left_pad) + name = f"cached_conv2_{i}" + logging.info(f"{name}.shape: {tensors[5].shape}") + inputs[name] = {0: "N"} + outputs[f"new_{name}"] = {0: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + num_encoder_layers = ",".join(map(str, encoder_model.encoder.num_encoder_layers)) + encoder_dims = ",".join(map(str, encoder_model.encoder.encoder_dim)) + cnn_module_kernels = ",".join(map(str, encoder_model.encoder.cnn_module_kernel)) + ds = encoder_model.encoder.downsampling_factor + left_context_len = encoder_model.left_context_len + left_context_len = [left_context_len // k for k in ds] + left_context_len = ",".join(map(str, left_context_len)) + query_head_dims = ",".join(map(str, encoder_model.encoder.query_head_dim)) + value_head_dims = ",".join(map(str, encoder_model.encoder.value_head_dim)) + num_heads = ",".join(map(str, encoder_model.encoder.num_heads)) + + meta_data = { + "model_type": "zipformer2", + "version": "1", + "model_author": "k2-fsa", + "comment": "streaming zipformer2", + "decode_chunk_len": str(decode_chunk_len), # 32 + "T": str(T), # 32+7+2*3=45 + "num_encoder_layers": num_encoder_layers, + "encoder_dims": encoder_dims, + "cnn_module_kernels": cnn_module_kernels, + "left_context_len": left_context_len, + "query_head_dims": query_head_dims, + "value_head_dims": value_head_dims, + "num_heads": num_heads, + } + logging.info(f"meta_data: {meta_data}") + + for i in range(len(init_state[:-2]) // 6): + build_inputs_outputs(init_state[i * 6 : (i + 1) * 6], i) + + # (batch_size, channels, left_pad, freq) + embed_states = init_state[-2] + name = "embed_states" + logging.info(f"{name}.shape: {embed_states.shape}") + inputs[name] = {0: "N"} + outputs[f"new_{name}"] = {0: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + # (batch_size,) + processed_lens = init_state[-1] + name = "processed_lens" + logging.info(f"{name}.shape: {processed_lens.shape}") + inputs[name] = {0: "N"} + outputs[f"new_{name}"] = {0: "N"} + input_names.append(name) + output_names.append(f"new_{name}") + + logging.info(inputs) + logging.info(outputs) + logging.info(input_names) + logging.info(output_names) + + torch.onnx.export( + encoder_model, + (x, init_state), + encoder_filename, + verbose=False, + opset_version=opset_version, + input_names=input_names, + output_names=output_names, + dynamic_axes={ + "x": {0: "N"}, + "encoder_out": {0: "N"}, + **inputs, + **outputs, + }, + ) + + add_meta_data(filename=encoder_filename, meta_data=meta_data) + + +def export_decoder_model_onnx( + decoder_model: OnnxDecoder, + decoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the decoder model to ONNX format. + + The exported model has one input: + + - y: a torch.int64 tensor of shape (N, decoder_model.context_size) + + and has one output: + + - decoder_out: a torch.float32 tensor of shape (N, joiner_dim) + + Args: + decoder_model: + The decoder model to be exported. + decoder_filename: + Filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + context_size = decoder_model.decoder.context_size + vocab_size = decoder_model.decoder.vocab_size + + y = torch.zeros(10, context_size, dtype=torch.int64) + torch.onnx.export( + decoder_model, + y, + decoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["y"], + output_names=["decoder_out"], + dynamic_axes={ + "y": {0: "N"}, + "decoder_out": {0: "N"}, + }, + ) + + meta_data = { + "context_size": str(context_size), + "vocab_size": str(vocab_size), + } + add_meta_data(filename=decoder_filename, meta_data=meta_data) + + +def export_joiner_model_onnx( + joiner_model: nn.Module, + joiner_filename: str, + opset_version: int = 11, +) -> None: + """Export the joiner model to ONNX format. + The exported joiner model has two inputs: + + - encoder_out: a tensor of shape (N, joiner_dim) + - decoder_out: a tensor of shape (N, joiner_dim) + + and produces one output: + + - logit: a tensor of shape (N, vocab_size) + """ + joiner_dim = joiner_model.output_linear.weight.shape[1] + logging.info(f"joiner dim: {joiner_dim}") + + projected_encoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + projected_decoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + + torch.onnx.export( + joiner_model, + (projected_encoder_out, projected_decoder_out), + joiner_filename, + verbose=False, + opset_version=opset_version, + input_names=[ + "encoder_out", + "decoder_out", + ], + output_names=["logit"], + dynamic_axes={ + "encoder_out": {0: "N"}, + "decoder_out": {0: "N"}, + "logit": {0: "N"}, + }, + ) + meta_data = { + "joiner_dim": str(joiner_dim), + } + add_meta_data(filename=joiner_filename, meta_data=meta_data) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + convert_scaled_to_non_scaled(model, inplace=True) + + encoder = OnnxEncoder( + encoder=model.encoder, + encoder_embed=model.encoder_embed, + encoder_proj=model.joiner.encoder_proj, + ) + + decoder = OnnxDecoder( + decoder=model.decoder, + decoder_proj=model.joiner.decoder_proj, + ) + + joiner = OnnxJoiner(output_linear=model.joiner.output_linear) + + encoder_num_param = sum([p.numel() for p in encoder.parameters()]) + decoder_num_param = sum([p.numel() for p in decoder.parameters()]) + joiner_num_param = sum([p.numel() for p in joiner.parameters()]) + total_num_param = encoder_num_param + decoder_num_param + joiner_num_param + logging.info(f"encoder parameters: {encoder_num_param}") + logging.info(f"decoder parameters: {decoder_num_param}") + logging.info(f"joiner parameters: {joiner_num_param}") + logging.info(f"total parameters: {total_num_param}") + + if params.iter > 0: + suffix = f"iter-{params.iter}" + else: + suffix = f"epoch-{params.epoch}" + + suffix += f"-avg-{params.avg}" + + opset_version = 13 + + logging.info("Exporting encoder") + encoder_filename = params.exp_dir / f"encoder-{suffix}.onnx" + export_encoder_model_onnx( + encoder, + encoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported encoder to {encoder_filename}") + + logging.info("Exporting decoder") + decoder_filename = params.exp_dir / f"decoder-{suffix}.onnx" + export_decoder_model_onnx( + decoder, + decoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported decoder to {decoder_filename}") + + logging.info("Exporting joiner") + joiner_filename = params.exp_dir / f"joiner-{suffix}.onnx" + export_joiner_model_onnx( + joiner, + joiner_filename, + opset_version=opset_version, + ) + logging.info(f"Exported joiner to {joiner_filename}") + + # Generate int8 quantization models + # See https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html#data-type-selection + + logging.info("Generate int8 quantization models") + + encoder_filename_int8 = params.exp_dir / f"encoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=encoder_filename, + model_output=encoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + decoder_filename_int8 = params.exp_dir / f"decoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=decoder_filename, + model_output=decoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + joiner_filename_int8 = params.exp_dir / f"joiner-{suffix}.int8.onnx" + quantize_dynamic( + model_input=joiner_filename, + model_output=joiner_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/export-onnx.py b/egs/librispeech/ASR/zipformer/export-onnx.py new file mode 100755 index 000000000..490e7c2e9 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/export-onnx.py @@ -0,0 +1,624 @@ +#!/usr/bin/env python3 +# +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) +# Copyright 2023 Danqing Fu (danqing.fu@gmail.com) + +""" +This script exports a transducer model from PyTorch to ONNX. + +We use the pre-trained model from +https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./zipformer/export-onnx.py \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + \ + --num-encoder-layers "2,2,3,4,3,2" \ + --downsampling-factor "1,2,4,8,4,2" \ + --feedforward-dim "512,768,1024,1536,1024,768" \ + --num-heads "4,4,4,8,4,4" \ + --encoder-dim "192,256,384,512,384,256" \ + --query-head-dim 32 \ + --value-head-dim 12 \ + --pos-head-dim 4 \ + --pos-dim 48 \ + --encoder-unmasked-dim "192,192,256,256,256,192" \ + --cnn-module-kernel "31,31,15,15,15,31" \ + --decoder-dim 512 \ + --joiner-dim 512 \ + --causal False \ + --chunk-size "16,32,64,-1" \ + --left-context-frames "64,128,256,-1" + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +See ./onnx_pretrained.py and ./onnx_check.py for how to +use the exported ONNX models. +""" + +import argparse +import logging +from pathlib import Path +from typing import Dict, Tuple + +import onnx +import sentencepiece as spm +import torch +import torch.nn as nn +from decoder import Decoder +from onnxruntime.quantization import QuantType, quantize_dynamic +from scaling_converter import convert_scaled_to_non_scaled +from train import add_model_arguments, get_params, get_transducer_model +from zipformer import Zipformer2 + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import str2bool, make_pad_mask + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for averaging. + Note: Epoch counts from 0. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +def add_meta_data(filename: str, meta_data: Dict[str, str]): + """Add meta data to an ONNX model. It is changed in-place. + + Args: + filename: + Filename of the ONNX model to be changed. + meta_data: + Key-value pairs. + """ + model = onnx.load(filename) + for key, value in meta_data.items(): + meta = model.metadata_props.add() + meta.key = key + meta.value = value + + onnx.save(model, filename) + + +class OnnxEncoder(nn.Module): + """A wrapper for Zipformer and the encoder_proj from the joiner""" + + def __init__( + self, encoder: Zipformer2, encoder_embed: nn.Module, encoder_proj: nn.Linear + ): + """ + Args: + encoder: + A Zipformer encoder. + encoder_proj: + The projection layer for encoder from the joiner. + """ + super().__init__() + self.encoder = encoder + self.encoder_embed = encoder_embed + self.encoder_proj = encoder_proj + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Please see the help information of Zipformer.forward + + Args: + x: + A 3-D tensor of shape (N, T, C) + x_lens: + A 1-D tensor of shape (N,). Its dtype is torch.int64 + Returns: + Return a tuple containing: + - encoder_out, A 3-D tensor of shape (N, T', joiner_dim) + - encoder_out_lens, A 1-D tensor of shape (N,) + """ + x, x_lens = self.encoder_embed(x, x_lens) + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) + encoder_out, encoder_out_lens = self.encoder(x, x_lens, src_key_padding_mask) + encoder_out = encoder_out.permute(1, 0, 2) + encoder_out = self.encoder_proj(encoder_out) + # Now encoder_out is of shape (N, T, joiner_dim) + + return encoder_out, encoder_out_lens + + +class OnnxDecoder(nn.Module): + """A wrapper for Decoder and the decoder_proj from the joiner""" + + def __init__(self, decoder: Decoder, decoder_proj: nn.Linear): + super().__init__() + self.decoder = decoder + self.decoder_proj = decoder_proj + + def forward(self, y: torch.Tensor) -> torch.Tensor: + """ + Args: + y: + A 2-D tensor of shape (N, context_size). + Returns + Return a 2-D tensor of shape (N, joiner_dim) + """ + need_pad = False + decoder_output = self.decoder(y, need_pad=need_pad) + decoder_output = decoder_output.squeeze(1) + output = self.decoder_proj(decoder_output) + + return output + + +class OnnxJoiner(nn.Module): + """A wrapper for the joiner""" + + def __init__(self, output_linear: nn.Linear): + super().__init__() + self.output_linear = output_linear + + def forward( + self, + encoder_out: torch.Tensor, + decoder_out: torch.Tensor, + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + logit = encoder_out + decoder_out + logit = self.output_linear(torch.tanh(logit)) + return logit + + +def export_encoder_model_onnx( + encoder_model: OnnxEncoder, + encoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the given encoder model to ONNX format. + The exported model has two inputs: + + - x, a tensor of shape (N, T, C); dtype is torch.float32 + - x_lens, a tensor of shape (N,); dtype is torch.int64 + + and it has two outputs: + + - encoder_out, a tensor of shape (N, T', joiner_dim) + - encoder_out_lens, a tensor of shape (N,) + + Args: + encoder_model: + The input encoder model + encoder_filename: + The filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + x = torch.zeros(1, 100, 80, dtype=torch.float32) + x_lens = torch.tensor([100], dtype=torch.int64) + + encoder_model = torch.jit.trace(encoder_model, (x, x_lens)) + + torch.onnx.export( + encoder_model, + (x, x_lens), + encoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["x", "x_lens"], + output_names=["encoder_out", "encoder_out_lens"], + dynamic_axes={ + "x": {0: "N", 1: "T"}, + "x_lens": {0: "N"}, + "encoder_out": {0: "N", 1: "T"}, + "encoder_out_lens": {0: "N"}, + }, + ) + + meta_data = { + "model_type": "zipformer2", + "version": "1", + "model_author": "k2-fsa", + "comment": "non-streaming zipformer2", + } + logging.info(f"meta_data: {meta_data}") + + add_meta_data(filename=encoder_filename, meta_data=meta_data) + + +def export_decoder_model_onnx( + decoder_model: OnnxDecoder, + decoder_filename: str, + opset_version: int = 11, +) -> None: + """Export the decoder model to ONNX format. + + The exported model has one input: + + - y: a torch.int64 tensor of shape (N, decoder_model.context_size) + + and has one output: + + - decoder_out: a torch.float32 tensor of shape (N, joiner_dim) + + Args: + decoder_model: + The decoder model to be exported. + decoder_filename: + Filename to save the exported ONNX model. + opset_version: + The opset version to use. + """ + context_size = decoder_model.decoder.context_size + vocab_size = decoder_model.decoder.vocab_size + + y = torch.zeros(10, context_size, dtype=torch.int64) + torch.onnx.export( + decoder_model, + y, + decoder_filename, + verbose=False, + opset_version=opset_version, + input_names=["y"], + output_names=["decoder_out"], + dynamic_axes={ + "y": {0: "N"}, + "decoder_out": {0: "N"}, + }, + ) + + meta_data = { + "context_size": str(context_size), + "vocab_size": str(vocab_size), + } + add_meta_data(filename=decoder_filename, meta_data=meta_data) + + +def export_joiner_model_onnx( + joiner_model: nn.Module, + joiner_filename: str, + opset_version: int = 11, +) -> None: + """Export the joiner model to ONNX format. + The exported joiner model has two inputs: + + - encoder_out: a tensor of shape (N, joiner_dim) + - decoder_out: a tensor of shape (N, joiner_dim) + + and produces one output: + + - logit: a tensor of shape (N, vocab_size) + """ + joiner_dim = joiner_model.output_linear.weight.shape[1] + logging.info(f"joiner dim: {joiner_dim}") + + projected_encoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + projected_decoder_out = torch.rand(11, joiner_dim, dtype=torch.float32) + + torch.onnx.export( + joiner_model, + (projected_encoder_out, projected_decoder_out), + joiner_filename, + verbose=False, + opset_version=opset_version, + input_names=[ + "encoder_out", + "decoder_out", + ], + output_names=["logit"], + dynamic_axes={ + "encoder_out": {0: "N"}, + "decoder_out": {0: "N"}, + "logit": {0: "N"}, + }, + ) + meta_data = { + "joiner_dim": str(joiner_dim), + } + add_meta_data(filename=joiner_filename, meta_data=meta_data) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + convert_scaled_to_non_scaled(model, inplace=True, is_onnx=True) + + encoder = OnnxEncoder( + encoder=model.encoder, + encoder_embed=model.encoder_embed, + encoder_proj=model.joiner.encoder_proj, + ) + + decoder = OnnxDecoder( + decoder=model.decoder, + decoder_proj=model.joiner.decoder_proj, + ) + + joiner = OnnxJoiner(output_linear=model.joiner.output_linear) + + encoder_num_param = sum([p.numel() for p in encoder.parameters()]) + decoder_num_param = sum([p.numel() for p in decoder.parameters()]) + joiner_num_param = sum([p.numel() for p in joiner.parameters()]) + total_num_param = encoder_num_param + decoder_num_param + joiner_num_param + logging.info(f"encoder parameters: {encoder_num_param}") + logging.info(f"decoder parameters: {decoder_num_param}") + logging.info(f"joiner parameters: {joiner_num_param}") + logging.info(f"total parameters: {total_num_param}") + + if params.iter > 0: + suffix = f"iter-{params.iter}" + else: + suffix = f"epoch-{params.epoch}" + + suffix += f"-avg-{params.avg}" + + opset_version = 13 + + logging.info("Exporting encoder") + encoder_filename = params.exp_dir / f"encoder-{suffix}.onnx" + export_encoder_model_onnx( + encoder, + encoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported encoder to {encoder_filename}") + + logging.info("Exporting decoder") + decoder_filename = params.exp_dir / f"decoder-{suffix}.onnx" + export_decoder_model_onnx( + decoder, + decoder_filename, + opset_version=opset_version, + ) + logging.info(f"Exported decoder to {decoder_filename}") + + logging.info("Exporting joiner") + joiner_filename = params.exp_dir / f"joiner-{suffix}.onnx" + export_joiner_model_onnx( + joiner, + joiner_filename, + opset_version=opset_version, + ) + logging.info(f"Exported joiner to {joiner_filename}") + + # Generate int8 quantization models + # See https://onnxruntime.ai/docs/performance/model-optimizations/quantization.html#data-type-selection + + logging.info("Generate int8 quantization models") + + encoder_filename_int8 = params.exp_dir / f"encoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=encoder_filename, + model_output=encoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + decoder_filename_int8 = params.exp_dir / f"decoder-{suffix}.int8.onnx" + quantize_dynamic( + model_input=decoder_filename, + model_output=decoder_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + joiner_filename_int8 = params.exp_dir / f"joiner-{suffix}.int8.onnx" + quantize_dynamic( + model_input=joiner_filename, + model_output=joiner_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/model.py b/egs/librispeech/ASR/zipformer/model.py index 7fcab04ae..ea2b4b721 100644 --- a/egs/librispeech/ASR/zipformer/model.py +++ b/egs/librispeech/ASR/zipformer/model.py @@ -49,7 +49,7 @@ class Transducer(nn.Module): encoder: It is the transcription network in the paper. Its accepts two inputs: `x` of (N, T, encoder_dim) and `x_lens` of shape (N,). - It returns two tensors: `logits` of shape (N, T, encoder_dm) and + It returns two tensors: `logits` of shape (N, T, encoder_dim) and `logit_lens` of shape (N,). decoder: It is the prediction network in the paper. Its input shape diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py new file mode 100755 index 000000000..273f883df --- /dev/null +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py @@ -0,0 +1,544 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) +# Copyright 2023 Danqing Fu (danqing.fu@gmail.com) + +""" +This script loads ONNX models exported by ./export-onnx-streaming.py +and uses them to decode waves. + +We use the pre-trained model from +https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./zipformer/export-onnx-streaming.py \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --num-encoder-layers "2,2,3,4,3,2" \ + --downsampling-factor "1,2,4,8,4,2" \ + --feedforward-dim "512,768,1024,1536,1024,768" \ + --num-heads "4,4,4,8,4,4" \ + --encoder-dim "192,256,384,512,384,256" \ + --query-head-dim 32 \ + --value-head-dim 12 \ + --pos-head-dim 4 \ + --pos-dim 48 \ + --encoder-unmasked-dim "192,192,256,256,256,192" \ + --cnn-module-kernel "31,31,15,15,15,31" \ + --decoder-dim 512 \ + --joiner-dim 512 \ + --causal True \ + --chunk-size 16 \ + --left-context-frames 64 + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +3. Run this file with the exported ONNX models + +./zipformer/onnx_pretrained-streaming.py \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + $repo/test_wavs/1089-134686-0001.wav + +Note: Even though this script only supports decoding a single file, +the exported ONNX models do support batch processing. +""" + +import argparse +import logging +from typing import Dict, List, Optional, Tuple + +import k2 +import numpy as np +import onnxruntime as ort +import torch +import torchaudio +from kaldifeat import FbankOptions, OnlineFbank, OnlineFeature + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--encoder-model-filename", + type=str, + required=True, + help="Path to the encoder onnx model. ", + ) + + parser.add_argument( + "--decoder-model-filename", + type=str, + required=True, + help="Path to the decoder onnx model. ", + ) + + parser.add_argument( + "--joiner-model-filename", + type=str, + required=True, + help="Path to the joiner onnx model. ", + ) + + parser.add_argument( + "--tokens", + type=str, + help="""Path to tokens.txt.""", + ) + + parser.add_argument( + "sound_file", + type=str, + help="The input sound file to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + return parser + + +class OnnxModel: + def __init__( + self, + encoder_model_filename: str, + decoder_model_filename: str, + joiner_model_filename: str, + ): + session_opts = ort.SessionOptions() + session_opts.inter_op_num_threads = 1 + session_opts.intra_op_num_threads = 1 + + self.session_opts = session_opts + + self.init_encoder(encoder_model_filename) + self.init_decoder(decoder_model_filename) + self.init_joiner(joiner_model_filename) + + def init_encoder(self, encoder_model_filename: str): + self.encoder = ort.InferenceSession( + encoder_model_filename, + sess_options=self.session_opts, + ) + self.init_encoder_states() + + def init_encoder_states(self, batch_size: int = 1): + encoder_meta = self.encoder.get_modelmeta().custom_metadata_map + logging.info(f"encoder_meta={encoder_meta}") + + model_type = encoder_meta["model_type"] + assert model_type == "zipformer2", model_type + + decode_chunk_len = int(encoder_meta["decode_chunk_len"]) + T = int(encoder_meta["T"]) + + num_encoder_layers = encoder_meta["num_encoder_layers"] + encoder_dims = encoder_meta["encoder_dims"] + cnn_module_kernels = encoder_meta["cnn_module_kernels"] + left_context_len = encoder_meta["left_context_len"] + query_head_dims = encoder_meta["query_head_dims"] + value_head_dims = encoder_meta["value_head_dims"] + num_heads = encoder_meta["num_heads"] + + def to_int_list(s): + return list(map(int, s.split(","))) + + num_encoder_layers = to_int_list(num_encoder_layers) + encoder_dims = to_int_list(encoder_dims) + cnn_module_kernels = to_int_list(cnn_module_kernels) + left_context_len = to_int_list(left_context_len) + query_head_dims = to_int_list(query_head_dims) + value_head_dims = to_int_list(value_head_dims) + num_heads = to_int_list(num_heads) + + logging.info(f"decode_chunk_len: {decode_chunk_len}") + logging.info(f"T: {T}") + logging.info(f"num_encoder_layers: {num_encoder_layers}") + logging.info(f"encoder_dims: {encoder_dims}") + logging.info(f"cnn_module_kernels: {cnn_module_kernels}") + logging.info(f"left_context_len: {left_context_len}") + logging.info(f"query_head_dims: {query_head_dims}") + logging.info(f"value_head_dims: {value_head_dims}") + logging.info(f"num_heads: {num_heads}") + + num_encoders = len(num_encoder_layers) + + self.states = [] + for i in range(num_encoders): + num_layers = num_encoder_layers[i] + key_dim = query_head_dims[i] * num_heads[i] + embed_dim = encoder_dims[i] + nonlin_attn_head_dim = 3 * embed_dim // 4 + value_dim = value_head_dims[i] * num_heads[i] + conv_left_pad = cnn_module_kernels[i] // 2 + + for layer in range(num_layers): + cached_key = torch.zeros( + left_context_len[i], batch_size, key_dim + ).numpy() + cached_nonlin_attn = torch.zeros( + 1, batch_size, left_context_len[i], nonlin_attn_head_dim + ).numpy() + cached_val1 = torch.zeros( + left_context_len[i], batch_size, value_dim + ).numpy() + cached_val2 = torch.zeros( + left_context_len[i], batch_size, value_dim + ).numpy() + cached_conv1 = torch.zeros(batch_size, embed_dim, conv_left_pad).numpy() + cached_conv2 = torch.zeros(batch_size, embed_dim, conv_left_pad).numpy() + self.states += [ + cached_key, + cached_nonlin_attn, + cached_val1, + cached_val2, + cached_conv1, + cached_conv2, + ] + embed_states = torch.zeros(batch_size, 128, 3, 19).numpy() + self.states.append(embed_states) + processed_lens = torch.zeros(batch_size, dtype=torch.int64).numpy() + self.states.append(processed_lens) + + self.num_encoders = num_encoders + + self.segment = T + self.offset = decode_chunk_len + + def init_decoder(self, decoder_model_filename: str): + self.decoder = ort.InferenceSession( + decoder_model_filename, + sess_options=self.session_opts, + ) + + decoder_meta = self.decoder.get_modelmeta().custom_metadata_map + self.context_size = int(decoder_meta["context_size"]) + self.vocab_size = int(decoder_meta["vocab_size"]) + + logging.info(f"context_size: {self.context_size}") + logging.info(f"vocab_size: {self.vocab_size}") + + def init_joiner(self, joiner_model_filename: str): + self.joiner = ort.InferenceSession( + joiner_model_filename, + sess_options=self.session_opts, + ) + + joiner_meta = self.joiner.get_modelmeta().custom_metadata_map + self.joiner_dim = int(joiner_meta["joiner_dim"]) + + logging.info(f"joiner_dim: {self.joiner_dim}") + + def _build_encoder_input_output( + self, + x: torch.Tensor, + ) -> Tuple[Dict[str, np.ndarray], List[str]]: + encoder_input = {"x": x.numpy()} + encoder_output = ["encoder_out"] + + def build_inputs_outputs(tensors, i): + assert len(tensors) == 6, len(tensors) + + # (downsample_left, batch_size, key_dim) + name = f"cached_key_{i}" + encoder_input[name] = tensors[0] + encoder_output.append(f"new_{name}") + + # (1, batch_size, downsample_left, nonlin_attn_head_dim) + name = f"cached_nonlin_attn_{i}" + encoder_input[name] = tensors[1] + encoder_output.append(f"new_{name}") + + # (downsample_left, batch_size, value_dim) + name = f"cached_val1_{i}" + encoder_input[name] = tensors[2] + encoder_output.append(f"new_{name}") + + # (downsample_left, batch_size, value_dim) + name = f"cached_val2_{i}" + encoder_input[name] = tensors[3] + encoder_output.append(f"new_{name}") + + # (batch_size, embed_dim, conv_left_pad) + name = f"cached_conv1_{i}" + encoder_input[name] = tensors[4] + encoder_output.append(f"new_{name}") + + # (batch_size, embed_dim, conv_left_pad) + name = f"cached_conv2_{i}" + encoder_input[name] = tensors[5] + encoder_output.append(f"new_{name}") + + for i in range(len(self.states[:-2]) // 6): + build_inputs_outputs(self.states[i * 6 : (i + 1) * 6], i) + + # (batch_size, channels, left_pad, freq) + name = "embed_states" + embed_states = self.states[-2] + encoder_input[name] = embed_states + encoder_output.append(f"new_{name}") + + # (batch_size,) + name = "processed_lens" + processed_lens = self.states[-1] + encoder_input[name] = processed_lens + encoder_output.append(f"new_{name}") + + return encoder_input, encoder_output + + def _update_states(self, states: List[np.ndarray]): + self.states = states + + def run_encoder(self, x: torch.Tensor) -> torch.Tensor: + """ + Args: + x: + A 3-D tensor of shape (N, T, C) + Returns: + Return a 3-D tensor of shape (N, T', joiner_dim) where + T' is usually equal to ((T-7)//2+1)//2 + """ + encoder_input, encoder_output_names = self._build_encoder_input_output(x) + + out = self.encoder.run(encoder_output_names, encoder_input) + + self._update_states(out[1:]) + + return torch.from_numpy(out[0]) + + def run_decoder(self, decoder_input: torch.Tensor) -> torch.Tensor: + """ + Args: + decoder_input: + A 2-D tensor of shape (N, context_size) + Returns: + Return a 2-D tensor of shape (N, joiner_dim) + """ + out = self.decoder.run( + [self.decoder.get_outputs()[0].name], + {self.decoder.get_inputs()[0].name: decoder_input.numpy()}, + )[0] + + return torch.from_numpy(out) + + def run_joiner( + self, encoder_out: torch.Tensor, decoder_out: torch.Tensor + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + out = self.joiner.run( + [self.joiner.get_outputs()[0].name], + { + self.joiner.get_inputs()[0].name: encoder_out.numpy(), + self.joiner.get_inputs()[1].name: decoder_out.numpy(), + }, + )[0] + + return torch.from_numpy(out) + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def create_streaming_feature_extractor() -> OnlineFeature: + """Create a CPU streaming feature extractor. + + At present, we assume it returns a fbank feature extractor with + fixed options. In the future, we will support passing in the options + from outside. + + Returns: + Return a CPU streaming feature extractor. + """ + opts = FbankOptions() + opts.device = "cpu" + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = 16000 + opts.mel_opts.num_bins = 80 + return OnlineFbank(opts) + + +def greedy_search( + model: OnnxModel, + encoder_out: torch.Tensor, + context_size: int, + decoder_out: Optional[torch.Tensor] = None, + hyp: Optional[List[int]] = None, +) -> List[int]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (1, T, joiner_dim) + context_size: + The context size of the decoder model. + decoder_out: + Optional. Decoder output of the previous chunk. + hyp: + Decoding results for previous chunks. + Returns: + Return the decoded results so far. + """ + + blank_id = 0 + + if decoder_out is None: + assert hyp is None, hyp + hyp = [blank_id] * context_size + decoder_input = torch.tensor([hyp], dtype=torch.int64) + decoder_out = model.run_decoder(decoder_input) + else: + assert hyp is not None, hyp + + encoder_out = encoder_out.squeeze(0) + T = encoder_out.size(0) + for t in range(T): + cur_encoder_out = encoder_out[t : t + 1] + joiner_out = model.run_joiner(cur_encoder_out, decoder_out).squeeze(0) + y = joiner_out.argmax(dim=0).item() + if y != blank_id: + hyp.append(y) + decoder_input = hyp[-context_size:] + decoder_input = torch.tensor([decoder_input], dtype=torch.int64) + decoder_out = model.run_decoder(decoder_input) + + return hyp, decoder_out + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + + model = OnnxModel( + encoder_model_filename=args.encoder_model_filename, + decoder_model_filename=args.decoder_model_filename, + joiner_model_filename=args.joiner_model_filename, + ) + + sample_rate = 16000 + + logging.info("Constructing Fbank computer") + online_fbank = create_streaming_feature_extractor() + + logging.info(f"Reading sound files: {args.sound_file}") + waves = read_sound_files( + filenames=[args.sound_file], + expected_sample_rate=sample_rate, + )[0] + + tail_padding = torch.zeros(int(0.3 * sample_rate), dtype=torch.float32) + wave_samples = torch.cat([waves, tail_padding]) + + num_processed_frames = 0 + segment = model.segment + offset = model.offset + + context_size = model.context_size + hyp = None + decoder_out = None + + chunk = int(1 * sample_rate) # 1 second + start = 0 + while start < wave_samples.numel(): + end = min(start + chunk, wave_samples.numel()) + samples = wave_samples[start:end] + start += chunk + + online_fbank.accept_waveform( + sampling_rate=sample_rate, + waveform=samples, + ) + + while online_fbank.num_frames_ready - num_processed_frames >= segment: + frames = [] + for i in range(segment): + frames.append(online_fbank.get_frame(num_processed_frames + i)) + num_processed_frames += offset + frames = torch.cat(frames, dim=0) + frames = frames.unsqueeze(0) + encoder_out = model.run_encoder(frames) + hyp, decoder_out = greedy_search( + model, + encoder_out, + context_size, + decoder_out, + hyp, + ) + + symbol_table = k2.SymbolTable.from_file(args.tokens) + + text = "" + for i in hyp[context_size:]: + text += symbol_table[i] + text = text.replace("▁", " ").strip() + + logging.info(args.sound_file) + logging.info(text) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained.py b/egs/librispeech/ASR/zipformer/onnx_pretrained.py new file mode 120000 index 000000000..0069288fe --- /dev/null +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained.py @@ -0,0 +1 @@ +../pruned_transducer_stateless7/onnx_pretrained.py \ No newline at end of file diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py index 908b60938..9f23eeead 100644 --- a/egs/librispeech/ASR/zipformer/scaling.py +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -26,6 +26,18 @@ import torch.nn as nn from torch import Tensor +# RuntimeError: Exporting the operator logaddexp to ONNX opset version +# 14 is not supported. Please feel free to request support or submit +# a pull request on PyTorch GitHub. +# +# The following function is to solve the above error when exporting +# models to ONNX via torch.jit.trace() +def logaddexp(x: Tensor, y: Tensor) -> Tensor: + if not torch.jit.is_tracing(): + return torch.logaddexp(x, y) + else: + return (x.exp() + y.exp()).log() + class PiecewiseLinear(object): """ Piecewise linear function, from float to float, specified as nonempty list of (x,y) pairs with @@ -162,7 +174,7 @@ class ScheduledFloat(torch.nn.Module): def __float__(self): batch_count = self.batch_count - if batch_count is None or not self.training or torch.jit.is_scripting(): + if batch_count is None or not self.training or torch.jit.is_scripting() or torch.jit.is_tracing(): return float(self.default) else: ans = self.schedule(self.batch_count) @@ -268,7 +280,7 @@ class SoftmaxFunction(torch.autograd.Function): def softmax(x: Tensor, dim: int): - if not x.requires_grad or torch.jit.is_scripting(): + if not x.requires_grad or torch.jit.is_scripting() or torch.jit.is_tracing(): return x.softmax(dim=dim) return SoftmaxFunction.apply(x, dim) @@ -1073,7 +1085,7 @@ class ScaleGrad(nn.Module): self.alpha = alpha def forward(self, x: Tensor) -> Tensor: - if torch.jit.is_scripting() or not self.training: + if torch.jit.is_scripting() or torch.jit.is_tracing() or not self.training: return x return scale_grad(x, self.alpha) @@ -1115,7 +1127,7 @@ def limit_param_value(x: Tensor, def _no_op(x: Tensor) -> Tensor: - if (torch.jit.is_scripting()): + if torch.jit.is_scripting() or torch.jit.is_tracing(): return x else: # a no-op function that will have a node in the autograd graph, @@ -1198,7 +1210,7 @@ class DoubleSwish(torch.nn.Module): """Return double-swish activation function which is an approximation to Swish(Swish(x)), that we approximate closely with x * sigmoid(x-1). """ - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): return x * torch.sigmoid(x - 1.0) return DoubleSwishFunction.apply(x) @@ -1313,9 +1325,9 @@ class SwooshL(torch.nn.Module): def forward(self, x: Tensor) -> Tensor: """Return Swoosh-L activation. """ - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) - return torch.logaddexp(zero, x - 4.0) - 0.08 * x - 0.035 + return logaddexp(zero, x - 4.0) - 0.08 * x - 0.035 if not x.requires_grad: return k2.swoosh_l_forward(x) else: @@ -1379,9 +1391,9 @@ class SwooshR(torch.nn.Module): def forward(self, x: Tensor) -> Tensor: """Return Swoosh-R activation. """ - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) - return torch.logaddexp(zero, x - 1.) - 0.08 * x - 0.313261687 + return logaddexp(zero, x - 1.) - 0.08 * x - 0.313261687 if not x.requires_grad: return k2.swoosh_r_forward(x) else: diff --git a/egs/librispeech/ASR/zipformer/scaling_converter.py b/egs/librispeech/ASR/zipformer/scaling_converter.py index 683a03461..54a5c2a6a 100644 --- a/egs/librispeech/ASR/zipformer/scaling_converter.py +++ b/egs/librispeech/ASR/zipformer/scaling_converter.py @@ -27,6 +27,7 @@ from typing import List, Tuple import torch import torch.nn as nn from scaling import Balancer, Dropout3, ScaleGrad, Whiten +from zipformer import CompactRelPositionalEncoding # Copied from https://pytorch.org/docs/1.9.0/_modules/torch/nn/modules/module.html#Module.get_submodule # noqa @@ -51,6 +52,7 @@ def convert_scaled_to_non_scaled( model: nn.Module, inplace: bool = False, is_pnnx: bool = False, + is_onnx: bool = False, ): """ Args: @@ -61,6 +63,8 @@ def convert_scaled_to_non_scaled( If False, the input model is copied and we modify the copied version. is_pnnx: True if we are going to export the model for PNNX. + is_onnx: + True if we are going to export the model for ONNX. Return: Return a model without scaled layers. """ @@ -71,6 +75,11 @@ def convert_scaled_to_non_scaled( for name, m in model.named_modules(): if isinstance(m, (Balancer, Dropout3, ScaleGrad, Whiten)): d[name] = nn.Identity() + elif is_onnx and isinstance(m, CompactRelPositionalEncoding): + # We want to recreate the positional encoding vector when + # the input changes, so we have to use torch.jit.script() + # to replace torch.jit.trace() + d[name] = torch.jit.script(m) for k, v in d.items(): if "." in k: diff --git a/egs/librispeech/ASR/zipformer/subsampling.py b/egs/librispeech/ASR/zipformer/subsampling.py index 47403f13c..d6bf57db4 100644 --- a/egs/librispeech/ASR/zipformer/subsampling.py +++ b/egs/librispeech/ASR/zipformer/subsampling.py @@ -100,7 +100,7 @@ class ConvNeXt(nn.Module): ) def forward(self, x: Tensor) -> Tensor: - if torch.jit.is_scripting() or not self.training: + if torch.jit.is_scripting() or torch.jit.is_tracing() or not self.training: return self.forward_internal(x) layerdrop_rate = float(self.layerdrop_rate) @@ -322,7 +322,7 @@ class Conv2dSubsampling(nn.Module): x = self.out_norm(x) x = self.dropout(x) - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): x_lens = (x_lens - 7) // 2 else: with warnings.catch_warnings(): diff --git a/egs/librispeech/ASR/zipformer/zipformer.py b/egs/librispeech/ASR/zipformer/zipformer.py index 8d90198fd..612356a50 100644 --- a/egs/librispeech/ASR/zipformer/zipformer.py +++ b/egs/librispeech/ASR/zipformer/zipformer.py @@ -133,6 +133,7 @@ class Zipformer2(EncoderInterface): self.encoder_dim = encoder_dim = _to_tuple(encoder_dim) # tuple self.encoder_unmasked_dim = encoder_unmasked_dim = _to_tuple(encoder_unmasked_dim) # tuple num_encoder_layers = _to_tuple(num_encoder_layers) + self.num_encoder_layers = num_encoder_layers self.query_head_dim = query_head_dim = _to_tuple(query_head_dim) self.value_head_dim = value_head_dim = _to_tuple(value_head_dim) pos_head_dim = _to_tuple(pos_head_dim) @@ -258,7 +259,7 @@ class Zipformer2(EncoderInterface): if not self.causal: return -1, -1 - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): assert len(self.chunk_size) == 1, self.chunk_size chunk_size = self.chunk_size[0] else: @@ -267,7 +268,7 @@ class Zipformer2(EncoderInterface): if chunk_size == -1: left_context_chunks = -1 else: - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): assert len(self.left_context_frames) == 1, self.left_context_frames left_context_frames = self.left_context_frames[0] else: @@ -301,14 +302,14 @@ class Zipformer2(EncoderInterface): of frames in `embeddings` before padding. """ outputs = [] - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): feature_masks = [1.0] * len(self.encoder_dim) else: feature_masks = self.get_feature_masks(x) chunk_size, left_context_chunks = self.get_chunk_info() - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): # Not support exporting a model for simulating streaming decoding attn_mask = None else: @@ -334,7 +335,7 @@ class Zipformer2(EncoderInterface): x = self.downsample_output(x) # class Downsample has this rounding behavior.. assert self.output_downsampling_factor == 2 - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): lengths = (x_lens + 1) // 2 else: with warnings.catch_warnings(): @@ -372,7 +373,7 @@ class Zipformer2(EncoderInterface): # t is frame index, shape (seq_len,) t = torch.arange(seq_len, dtype=torch.int32, device=x.device) # c is chunk index for each frame, shape (seq_len,) - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): c = t // chunk_size else: with warnings.catch_warnings(): @@ -650,7 +651,7 @@ class Zipformer2EncoderLayer(nn.Module): ) def get_sequence_dropout_mask(self, x: Tensor, dropout_rate: float) -> Optional[Tensor]: - if dropout_rate == 0.0 or not self.training or torch.jit.is_scripting(): + if dropout_rate == 0.0 or not self.training or torch.jit.is_scripting() or torch.jit.is_tracing(): return None batch_size = x.shape[1] mask = (torch.rand(batch_size, 1, device=x.device) > dropout_rate).to(x.dtype) @@ -695,7 +696,7 @@ class Zipformer2EncoderLayer(nn.Module): src_orig = src # dropout rate for non-feedforward submodules - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): attention_skip_rate = 0.0 else: attention_skip_rate = float(self.attention_skip_rate) if self.training else 0.0 @@ -713,7 +714,7 @@ class Zipformer2EncoderLayer(nn.Module): self_attn_dropout_mask = self.get_sequence_dropout_mask(src, attention_skip_rate) selected_attn_weights = attn_weights[0:1] - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): pass elif not self.training and random.random() < float(self.const_attention_rate): # Make attention weights constant. The intention is to @@ -732,7 +733,7 @@ class Zipformer2EncoderLayer(nn.Module): src = src + (self_attn if self_attn_dropout_mask is None else self_attn * self_attn_dropout_mask) - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): conv_skip_rate = 0.0 else: conv_skip_rate = float(self.conv_skip_rate) if self.training else 0.0 @@ -740,7 +741,7 @@ class Zipformer2EncoderLayer(nn.Module): src_key_padding_mask=src_key_padding_mask), conv_skip_rate) - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): ff2_skip_rate = 0.0 else: ff2_skip_rate = float(self.ff2_skip_rate) if self.training else 0.0 @@ -754,7 +755,7 @@ class Zipformer2EncoderLayer(nn.Module): src = src + (self_attn if self_attn_dropout_mask is None else self_attn * self_attn_dropout_mask) - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): conv_skip_rate = 0.0 else: conv_skip_rate = float(self.conv_skip_rate) if self.training else 0.0 @@ -762,7 +763,7 @@ class Zipformer2EncoderLayer(nn.Module): src_key_padding_mask=src_key_padding_mask), conv_skip_rate) - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): ff3_skip_rate = 0.0 else: ff3_skip_rate = float(self.ff3_skip_rate) if self.training else 0.0 @@ -968,7 +969,7 @@ class Zipformer2Encoder(nn.Module): pos_emb = self.encoder_pos(src) output = src - if not torch.jit.is_scripting(): + if not torch.jit.is_scripting() and not torch.jit.is_tracing(): output = output * feature_mask for i, mod in enumerate(self.layers): @@ -980,7 +981,7 @@ class Zipformer2Encoder(nn.Module): src_key_padding_mask=src_key_padding_mask, ) - if not torch.jit.is_scripting(): + if not torch.jit.is_scripting() and not torch.jit.is_tracing(): output = output * feature_mask return output @@ -1073,7 +1074,7 @@ class BypassModule(nn.Module): # or (batch_size, num_channels,). This is actually the # scale on the non-residual term, so 0 correponds to bypassing # this module. - if torch.jit.is_scripting() or not self.training: + if torch.jit.is_scripting() or torch.jit.is_tracing() or not self.training: return self.bypass_scale else: ans = limit_param_value(self.bypass_scale, @@ -1229,12 +1230,11 @@ class SimpleDownsample(torch.nn.Module): d_seq_len = (seq_len + ds - 1) // ds # Pad to an exact multiple of self.downsample - if seq_len != d_seq_len * ds: - # right-pad src, repeating the last element. - pad = d_seq_len * ds - seq_len - src_extra = src[src.shape[0]-1:].expand(pad, src.shape[1], src.shape[2]) - src = torch.cat((src, src_extra), dim=0) - assert src.shape[0] == d_seq_len * ds + # right-pad src, repeating the last element. + pad = d_seq_len * ds - seq_len + src_extra = src[src.shape[0]-1:].expand(pad, src.shape[1], src.shape[2]) + src = torch.cat((src, src_extra), dim=0) + assert src.shape[0] == d_seq_len * ds src = src.reshape(d_seq_len, ds, batch_size, in_channels) @@ -1322,11 +1322,7 @@ class CompactRelPositionalEncoding(torch.nn.Module): # self.pe contains both positive and negative parts # the length of self.pe is 2 * input_len - 1 if self.pe.size(0) >= T * 2 - 1: - # Note: TorchScript doesn't implement operator== for torch.Device - if self.pe.dtype != x.dtype or str(self.pe.device) != str( - x.device - ): - self.pe = self.pe.to(dtype=x.dtype, device=x.device) + self.pe = self.pe.to(dtype=x.dtype, device=x.device) return # if T == 4, x would contain [ -3, -2, 1, 0, 1, 2, 3 ] @@ -1524,7 +1520,7 @@ class RelPositionMultiheadAttentionWeights(nn.Module): attn_scores = torch.matmul(q, k) use_pos_scores = False - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): # We can't put random.random() in the same line use_pos_scores = True elif not self.training or random.random() >= float(self.pos_emb_skip_rate): @@ -1542,16 +1538,26 @@ class RelPositionMultiheadAttentionWeights(nn.Module): # the following .as_strided() expression converts the last axis of pos_scores from relative # to absolute position. I don't know whether I might have got the time-offsets backwards or # not, but let this code define which way round it is supposed to be. - pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, seq_len), - (pos_scores.stride(0), - pos_scores.stride(1), - pos_scores.stride(2)-pos_scores.stride(3), - pos_scores.stride(3)), - storage_offset=pos_scores.stride(3) * (seq_len - 1)) + if torch.jit.is_tracing(): + (num_heads, batch_size, time1, n) = pos_scores.shape + rows = torch.arange(start=time1 - 1, end=-1, step=-1) + cols = torch.arange(seq_len) + rows = rows.repeat(batch_size * num_heads).unsqueeze(-1) + indexes = rows + cols + pos_scores = pos_scores.reshape(-1, n) + pos_scores = torch.gather(pos_scores, dim=1, index=indexes) + pos_scores = pos_scores.reshape(num_heads, batch_size, time1, seq_len) + else: + pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, seq_len), + (pos_scores.stride(0), + pos_scores.stride(1), + pos_scores.stride(2)-pos_scores.stride(3), + pos_scores.stride(3)), + storage_offset=pos_scores.stride(3) * (seq_len - 1)) attn_scores = attn_scores + pos_scores - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): pass elif self.training and random.random() < 0.1: # This is a harder way of limiting the attention scores to not be @@ -1594,7 +1600,7 @@ class RelPositionMultiheadAttentionWeights(nn.Module): # half-precision output for backprop purposes. attn_weights = softmax(attn_scores, dim=-1) - if torch.jit.is_scripting(): + if torch.jit.is_scripting() or torch.jit.is_tracing(): pass elif random.random() < 0.001 and not self.training: self._print_attn_entropy(attn_weights) @@ -1672,15 +1678,26 @@ class RelPositionMultiheadAttentionWeights(nn.Module): # (head, batch, time1, pos_dim) x (head, 1, pos_dim, seq_len2) -> (head, batch, time1, seq_len2) # [where seq_len2 represents relative position.] pos_scores = torch.matmul(p, pos_emb) + + if torch.jit.is_tracing(): + (num_heads, batch_size, time1, n) = pos_scores.shape + rows = torch.arange(start=time1 - 1, end=-1, step=-1) + cols = torch.arange(k_len) + rows = rows.repeat(batch_size * num_heads).unsqueeze(-1) + indexes = rows + cols + pos_scores = pos_scores.reshape(-1, n) + pos_scores = torch.gather(pos_scores, dim=1, index=indexes) + pos_scores = pos_scores.reshape(num_heads, batch_size, time1, k_len) # the following .as_strided() expression converts the last axis of pos_scores from relative # to absolute position. I don't know whether I might have got the time-offsets backwards or # not, but let this code define which way round it is supposed to be. - pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, k_len), - (pos_scores.stride(0), - pos_scores.stride(1), - pos_scores.stride(2)-pos_scores.stride(3), - pos_scores.stride(3)), - storage_offset=pos_scores.stride(3) * (seq_len - 1)) + else: + pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, k_len), + (pos_scores.stride(0), + pos_scores.stride(1), + pos_scores.stride(2)-pos_scores.stride(3), + pos_scores.stride(3)), + storage_offset=pos_scores.stride(3) * (seq_len - 1)) attn_scores = attn_scores + pos_scores @@ -2136,7 +2153,7 @@ class ConvolutionModule(nn.Module): if src_key_padding_mask is not None: x = x.masked_fill(src_key_padding_mask.unsqueeze(1).expand_as(x), 0.0) - if not torch.jit.is_scripting() and chunk_size >= 0: + if not torch.jit.is_scripting() and not torch.jit.is_tracing() and chunk_size >= 0: # Not support exporting a model for simulated streaming decoding assert self.causal, "Must initialize model with causal=True if you use chunk_size" x = self.depthwise_conv(x, chunk_size=chunk_size) From 0ad037d0762ecdf7ebeb40d7a196cc6280e8a535 Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Wed, 14 Jun 2023 14:27:29 +0800 Subject: [PATCH 030/100] Add CTC loss option in zipformer recipe (#1111) * add CTC loss option in zipformer recipe * add ctc_decode.py * support CTC model export, add jit_pretrained_ctc.py, pretrained_ctc.py * update README.md and RESULTS.md * add CI test --- ...un-librispeech-zipformer-ctc-2023-06-14.sh | 117 +++ ...n-librispeech-zipformer-ctc-2023-06-14.yml | 155 ++++ egs/librispeech/ASR/README.md | 1 + egs/librispeech/ASR/RESULTS.md | 66 +- .../beam_search.py | 36 +- egs/librispeech/ASR/zipformer/ctc_decode.py | 847 ++++++++++++++++++ egs/librispeech/ASR/zipformer/decode.py | 14 +- .../ASR/zipformer/export-onnx-streaming.py | 4 +- egs/librispeech/ASR/zipformer/export-onnx.py | 4 +- egs/librispeech/ASR/zipformer/export.py | 4 +- .../ASR/zipformer/generate_averaged_model.py | 4 +- .../ASR/zipformer/jit_pretrained.py | 2 +- .../ASR/zipformer/jit_pretrained_ctc.py | 428 +++++++++ egs/librispeech/ASR/zipformer/model.py | 275 ++++-- egs/librispeech/ASR/zipformer/pretrained.py | 17 +- .../ASR/zipformer/pretrained_ctc.py | 446 +++++++++ .../ASR/zipformer/streaming_decode.py | 4 +- egs/librispeech/ASR/zipformer/train.py | 101 ++- 18 files changed, 2378 insertions(+), 147 deletions(-) create mode 100755 .github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh create mode 100644 .github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml create mode 100755 egs/librispeech/ASR/zipformer/ctc_decode.py create mode 100755 egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py create mode 100755 egs/librispeech/ASR/zipformer/pretrained_ctc.py diff --git a/.github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh b/.github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh new file mode 100755 index 000000000..cfa9c420c --- /dev/null +++ b/.github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +set -e + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-transducer-ctc-2023-06-13 + +log "Downloading pre-trained model from $repo_url" +git lfs install +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +log "Display test files" +tree $repo/ +ls -lh $repo/test_wavs/*.wav + +pushd $repo/exp +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "data/lang_bpe_500/HLG.pt" +git lfs pull --include "data/lang_bpe_500/L.pt" +git lfs pull --include "data/lang_bpe_500/LG.pt" +git lfs pull --include "data/lang_bpe_500/Linv.pt" +git lfs pull --include "data/lm/G_4_gram.pt" +git lfs pull --include "exp/jit_script.pt" +git lfs pull --include "exp/pretrained.pt" +ln -s pretrained.pt epoch-99.pt +ls -lh *.pt +popd + +log "Export to torchscript model" +./zipformer/export.py \ + --exp-dir $repo/exp \ + --use-transducer 1 \ + --use-ctc 1 \ + --use-averaged-model false \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --epoch 99 \ + --avg 1 \ + --jit 1 + +ls -lh $repo/exp/*.pt + +log "Decode with models exported by torch.jit.script()" + +for method in ctc-decoding 1best; do + ./zipformer/jit_pretrained_ctc.py \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --model-filename $repo/exp/jit_script.pt \ + --HLG $repo/data/lang_bpe_500/HLG.pt \ + --words-file $repo/data/lang_bpe_500/words.txt \ + --G $repo/data/lm/G_4_gram.pt \ + --method $method \ + --sample-rate 16000 \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav +done + +for method in ctc-decoding 1best; do + log "$method" + + ./zipformer/pretrained_ctc.py \ + --use-transducer 1 \ + --use-ctc 1 \ + --method $method \ + --checkpoint $repo/exp/pretrained.pt \ + --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --words-file $repo/data/lang_bpe_500/words.txt \ + --HLG $repo/data/lang_bpe_500/HLG.pt \ + --G $repo/data/lm/G_4_gram.pt \ + --words-file $repo/data/lang_bpe_500/words.txt \ + --sample-rate 16000 \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav +done + +echo "GITHUB_EVENT_NAME: ${GITHUB_EVENT_NAME}" +echo "GITHUB_EVENT_LABEL_NAME: ${GITHUB_EVENT_LABEL_NAME}" +if [[ x"${GITHUB_EVENT_NAME}" == x"schedule" || x"${GITHUB_EVENT_LABEL_NAME}" == x"run-decode" ]]; then + mkdir -p zipformer/exp + ln -s $PWD/$repo/exp/pretrained.pt zipformer/exp/epoch-999.pt + ln -s $PWD/$repo/data/lang_bpe_500 data/ + + ls -lh data + ls -lh zipformer/exp + + log "Decoding test-clean and test-other" + + # use a small value for decoding with CPU + max_duration=100 + + for method in ctc-decoding 1best; do + log "Decoding with $method" + + ./zipformer/ctc_decode.py \ + --use-transducer 1 \ + --use-ctc 1 \ + --decoding-method $method \ + --nbest-scale 1.0 \ + --hlg-scale 0.6 \ + --epoch 999 \ + --avg 1 \ + --use-averaged-model 0 \ + --max-duration $max_duration \ + --exp-dir zipformer/exp + done + + rm zipformer/exp/*.pt +fi diff --git a/.github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml b/.github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml new file mode 100644 index 000000000..569ce48fc --- /dev/null +++ b/.github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml @@ -0,0 +1,155 @@ +# Copyright 2022 Fangjun Kuang (csukuangfj@gmail.com) + +# See ../../LICENSE for clarification regarding multiple authors +# +# 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-librispeech-zipformer-ctc-2023-06-14 +# zipformer + +on: + push: + branches: + - master + pull_request: + types: [labeled] + + schedule: + # minute (0-59) + # hour (0-23) + # day of the month (1-31) + # month (1-12) + # day of the week (0-6) + # nightly build at 15:50 UTC time every day + - cron: "50 15 * * *" + +concurrency: + group: run_librispeech_2023_06_14_zipformer-ctc-${{ github.ref }} + cancel-in-progress: true + +jobs: + run_librispeech_2023_06_14_zipformer_ctc: + if: github.event.label.name == 'zipformer' ||github.event.label.name == 'ready' || github.event.label.name == 'run-decode' || github.event_name == 'push' || github.event_name == 'schedule' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.8] + + fail-fast: false + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: '**/requirements-ci.txt' + + - name: Install Python dependencies + run: | + grep -v '^#' ./requirements-ci.txt | xargs -n 1 -L 1 pip install + pip uninstall -y protobuf + pip install --no-binary protobuf protobuf==3.20.* + + - name: Cache kaldifeat + id: my-cache + uses: actions/cache@v2 + with: + path: | + ~/tmp/kaldifeat + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 + + - name: Install kaldifeat + if: steps.my-cache.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/install-kaldifeat.sh + + - name: Cache LibriSpeech test-clean and test-other datasets + id: libri-test-clean-and-test-other-data + uses: actions/cache@v2 + with: + path: | + ~/tmp/download + key: cache-libri-test-clean-and-test-other + + - name: Download LibriSpeech test-clean and test-other + if: steps.libri-test-clean-and-test-other-data.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/download-librispeech-test-clean-and-test-other-dataset.sh + + - name: Prepare manifests for LibriSpeech test-clean and test-other + shell: bash + run: | + .github/scripts/prepare-librispeech-test-clean-and-test-other-manifests.sh + + - name: Cache LibriSpeech test-clean and test-other fbank features + id: libri-test-clean-and-test-other-fbank + uses: actions/cache@v2 + with: + path: | + ~/tmp/fbank-libri + key: cache-libri-fbank-test-clean-and-test-other-v2 + + - name: Compute fbank for LibriSpeech test-clean and test-other + if: steps.libri-test-clean-and-test-other-fbank.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/compute-fbank-librispeech-test-clean-and-test-other.sh + + - name: Inference with pre-trained model + shell: bash + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_LABEL_NAME: ${{ github.event.label.name }} + run: | + mkdir -p egs/librispeech/ASR/data + ln -sfv ~/tmp/fbank-libri egs/librispeech/ASR/data/fbank + ls -lh egs/librispeech/ASR/data/* + + sudo apt-get -qq install git-lfs tree + export PYTHONPATH=$PWD:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/kaldifeat/python:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/build/lib:$PYTHONPATH + + .github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh + + - name: Display decoding results for librispeech zipformer + if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' + shell: bash + run: | + cd egs/librispeech/ASR/ + tree ./zipformer/exp + + cd zipformer + echo "results for zipformer" + echo "===ctc-decoding===" + find exp/ctc-decoding -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/ctc-decoding -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + echo "===1best===" + find exp/1best -name "log-*" -exec grep -n --color "best for test-clean" {} + | sort -n -k2 + find exp/1best -name "log-*" -exec grep -n --color "best for test-other" {} + | sort -n -k2 + + - name: Upload decoding results for librispeech zipformer + uses: actions/upload-artifact@v2 + if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' + with: + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-zipformer-2022-11-11 + path: egs/librispeech/ASR/zipformer/exp/ diff --git a/egs/librispeech/ASR/README.md b/egs/librispeech/ASR/README.md index 6f5ee7846..f42750da9 100644 --- a/egs/librispeech/ASR/README.md +++ b/egs/librispeech/ASR/README.md @@ -47,6 +47,7 @@ We place an additional Conv1d layer right after the input embedding layer. | `conformer-ctc` | Conformer | Use auxiliary attention head | | `conformer-ctc2` | Reworked Conformer | Use auxiliary attention head | | `conformer-ctc3` | Reworked Conformer | Streaming version + delay penalty | +| `zipformer` | Upgraded Zipformer | Use auxiliary transducer head | The latest recipe | # MMI diff --git a/egs/librispeech/ASR/RESULTS.md b/egs/librispeech/ASR/RESULTS.md index 28361afdd..1b8e690bd 100644 --- a/egs/librispeech/ASR/RESULTS.md +++ b/egs/librispeech/ASR/RESULTS.md @@ -1,5 +1,69 @@ ## Results +### zipformer (zipformer + pruned stateless transducer + CTC) + +See for more details. + +[zipformer](./zipformer) + +#### Non-streaming + +##### normal-scaled model, number of model parameters: 65805511, i.e., 65.81 M + +The tensorboard log can be found at + + +You can find a pretrained model, training logs, decoding logs, and decoding results at: + + +You can use to deploy it. + +Results of the CTC head: + +| decoding method | test-clean | test-other | comment | +|-------------------------|------------|------------|--------------------| +| ctc-decoding | 2.40 | 5.66 | --epoch 40 --avg 16 | +| 1best | 2.46 | 5.11 | --epoch 40 --avg 16 | +| nbest | 2.46 | 5.11 | --epoch 40 --avg 16 | +| nbest-rescoring | 2.37 | 4.93 | --epoch 40 --avg 16 | +| whole-lattice-rescoring | 2.37 | 4.88 | --epoch 40 --avg 16 | + +The training command is: +```bash +export CUDA_VISIBLE_DEVICES="0,1,2,3" +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 40 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp-ctc-rnnt \ + --causal 0 \ + --use-transducer 1 \ + --use-ctc 1 \ + --ctc-loss-scale 0.2 \ + --full-libri 1 \ + --max-duration 1000 +``` + +The decoding command is: +```bash +export CUDA_VISIBLE_DEVICES="0" +for m in ctc-decoding 1best nbest nbest-rescoring whole-lattice-rescoring; do + ./zipformer/ctc_decode.py \ + --epoch 40 \ + --avg 16 \ + --exp-dir zipformer/exp-ctc-rnnt \ + --use-transducer 1 \ + --use-ctc 1 \ + --max-duration 300 \ + --causal 0 \ + --num-paths 100 \ + --nbest-scale 1.0 \ + --hlg-scale 0.6 \ + --decoding-method $m +done +``` + ### zipformer (zipformer + pruned stateless transducer) See for more details. @@ -285,7 +349,7 @@ export CUDA_VISIBLE_DEVICES="0,1" --lr-epochs 100 \ --lr-batches 100000 \ --bpe-model icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11/data/lang_bpe_500/bpe.model \ - --do-finetune True \ + --do-finetune True \ --use-mux True \ --finetune-ckpt icefall-asr-librispeech-pruned-transducer-stateless7-2022-11-11/exp/pretrain.pt \ --max-duration 500 diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index 25b79d600..1bbad6946 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -22,7 +22,6 @@ from typing import Dict, List, Optional, Tuple, Union import k2 import sentencepiece as spm import torch -from model import Transducer from icefall import ContextGraph, ContextState, NgramLm, NgramLmStateCost from icefall.decode import Nbest, one_best_decoding @@ -36,10 +35,11 @@ from icefall.utils import ( get_texts, get_texts_with_timestamp, ) +from torch import nn def fast_beam_search_one_best( - model: Transducer, + model: nn.Module, decoding_graph: k2.Fsa, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, @@ -103,7 +103,7 @@ def fast_beam_search_one_best( def fast_beam_search_nbest_LG( - model: Transducer, + model: nn.Module, decoding_graph: k2.Fsa, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, @@ -229,7 +229,7 @@ def fast_beam_search_nbest_LG( def fast_beam_search_nbest( - model: Transducer, + model: nn.Module, decoding_graph: k2.Fsa, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, @@ -319,7 +319,7 @@ def fast_beam_search_nbest( def fast_beam_search_nbest_oracle( - model: Transducer, + model: nn.Module, decoding_graph: k2.Fsa, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, @@ -424,7 +424,7 @@ def fast_beam_search_nbest_oracle( def fast_beam_search( - model: Transducer, + model: nn.Module, decoding_graph: k2.Fsa, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, @@ -523,7 +523,7 @@ def fast_beam_search( def greedy_search( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, max_sym_per_frame: int, return_timestamps: bool = False, @@ -623,7 +623,7 @@ def greedy_search( def greedy_search_batch( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, return_timestamps: bool = False, @@ -917,7 +917,7 @@ def get_hyps_shape(hyps: List[HypothesisList]) -> k2.RaggedShape: def modified_beam_search( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, context_graph: Optional[ContextGraph] = None, @@ -1119,7 +1119,7 @@ def modified_beam_search( def modified_beam_search_lm_rescore( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, LM: LmScorer, @@ -1317,7 +1317,7 @@ def modified_beam_search_lm_rescore( def modified_beam_search_lm_rescore_LODR( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, LM: LmScorer, @@ -1533,7 +1533,7 @@ def modified_beam_search_lm_rescore_LODR( def _deprecated_modified_beam_search( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, beam: int = 4, return_timestamps: bool = False, @@ -1658,7 +1658,7 @@ def _deprecated_modified_beam_search( def beam_search( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, beam: int = 4, temperature: float = 1.0, @@ -1818,7 +1818,7 @@ def beam_search( def fast_beam_search_with_nbest_rescoring( - model: Transducer, + model: nn.Module, decoding_graph: k2.Fsa, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, @@ -1978,7 +1978,7 @@ def fast_beam_search_with_nbest_rescoring( def fast_beam_search_with_nbest_rnn_rescoring( - model: Transducer, + model: nn.Module, decoding_graph: k2.Fsa, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, @@ -2169,7 +2169,7 @@ def fast_beam_search_with_nbest_rnn_rescoring( def modified_beam_search_ngram_rescoring( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, ngram_lm: NgramLm, @@ -2333,7 +2333,7 @@ def modified_beam_search_ngram_rescoring( def modified_beam_search_LODR( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, LODR_lm: NgramLm, @@ -2604,7 +2604,7 @@ def modified_beam_search_LODR( def modified_beam_search_lm_shallow_fusion( - model: Transducer, + model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, LM: LmScorer, diff --git a/egs/librispeech/ASR/zipformer/ctc_decode.py b/egs/librispeech/ASR/zipformer/ctc_decode.py new file mode 100755 index 000000000..4db50b981 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/ctc_decode.py @@ -0,0 +1,847 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2022 Xiaomi Corporation (Author: Fangjun Kuang, +# Liyong Guo, +# Quandong Wang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +(1) ctc-decoding +./zipformer/ctc_decode.py \ + --epoch 30 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --max-duration 600 \ + --decoding-method ctc-decoding + +(2) 1best +./zipformer/ctc_decode.py \ + --epoch 30 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --max-duration 600 \ + --hlg-scale 0.6 \ + --decoding-method 1best + +(3) nbest +./zipformer/ctc_decode.py \ + --epoch 30 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --max-duration 600 \ + --hlg-scale 0.6 \ + --decoding-method nbest + +(4) nbest-rescoring +./zipformer/ctc_decode.py \ + --epoch 30 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --max-duration 600 \ + --hlg-scale 0.6 \ + --nbest-scale 1.0 \ + --lm-dir data/lm \ + --decoding-method nbest-rescoring + +(5) whole-lattice-rescoring +./zipformer/ctc_decode.py \ + --epoch 30 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --max-duration 600 \ + --hlg-scale 0.6 \ + --nbest-scale 1.0 \ + --lm-dir data/lm \ + --decoding-method whole-lattice-rescoring +""" + + +import argparse +import logging +import math +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import LibriSpeechAsrDataModule +from train import add_model_arguments, get_params, get_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.decode import ( + get_lattice, + nbest_decoding, + nbest_oracle, + one_best_decoding, + rescore_with_n_best_list, + rescore_with_whole_lattice, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + get_texts, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--lang-dir", + type=Path, + default="data/lang_bpe_500", + help="The lang dir containing word table and LG graph", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="ctc-decoding", + help="""Decoding method. + Supported values are: + - (1) ctc-decoding. Use CTC decoding. It uses a sentence piece + model, i.e., lang_dir/bpe.model, to convert word pieces to words. + It needs neither a lexicon nor an n-gram LM. + - (2) 1best. Extract the best path from the decoding lattice as the + decoding result. + - (3) nbest. Extract n paths from the decoding lattice; the path + with the highest score is the decoding result. + - (4) nbest-rescoring. Extract n paths from the decoding lattice, + rescore them with an n-gram LM (e.g., a 4-gram LM), the path with + the highest score is the decoding result. + - (5) whole-lattice-rescoring. Rescore the decoding lattice with an + n-gram LM (e.g., a 4-gram LM), the best path of rescored lattice + is the decoding result. + you have trained an RNN LM using ./rnn_lm/train.py + - (6) nbest-oracle. Its WER is the lower bound of any n-best + rescoring method can achieve. Useful for debugging n-best + rescoring method. + """, + ) + + parser.add_argument( + "--num-paths", + type=int, + default=100, + help="""Number of paths for n-best based decoding method. + Used only when "method" is one of the following values: + nbest, nbest-rescoring, and nbest-oracle + """, + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=1.0, + help="""The scale to be applied to `lattice.scores`. + It's needed if you use any kinds of n-best based rescoring. + Used only when "method" is one of the following values: + nbest, nbest-rescoring, and nbest-oracle + A smaller value results in more unique paths. + """, + ) + + parser.add_argument( + "--hlg-scale", + type=float, + default=0.6, + help="""The scale to be applied to `hlg.scores`. + """, + ) + + parser.add_argument( + "--lm-dir", + type=str, + default="data/lm", + help="""The n-gram LM dir. + It should contain either G_4_gram.pt or G_4_gram.fst.txt + """, + ) + + add_model_arguments(parser) + + return parser + + +def get_decoding_params() -> AttributeDict: + """Parameters for decoding.""" + params = AttributeDict( + { + "frame_shift_ms": 10, + "search_beam": 20, + "output_beam": 8, + "min_active_states": 30, + "max_active_states": 10000, + "use_double_scores": True, + } + ) + return params + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + HLG: Optional[k2.Fsa], + H: Optional[k2.Fsa], + bpe_model: Optional[spm.SentencePieceProcessor], + batch: dict, + word_table: k2.SymbolTable, + G: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + - key: It indicates the setting used for decoding. For example, + if no rescoring is used, the key is the string `no_rescore`. + If LM rescoring is used, the key is the string `lm_scale_xxx`, + where `xxx` is the value of `lm_scale`. An example key is + `lm_scale_0.7` + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + + Args: + params: + It's the return value of :func:`get_params`. + + - params.decoding_method is "1best", it uses 1best decoding without LM rescoring. + - params.decoding_method is "nbest", it uses nbest decoding without LM rescoring. + - params.decoding_method is "nbest-rescoring", it uses nbest LM rescoring. + - params.decoding_method is "whole-lattice-rescoring", it uses whole lattice LM + rescoring. + + model: + The neural model. + HLG: + The decoding graph. Used only when params.decoding_method is NOT ctc-decoding. + H: + The ctc topo. Used only when params.decoding_method is ctc-decoding. + bpe_model: + The BPE model. Used only when params.decoding_method is ctc-decoding. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + word_table: + The word symbol table. + G: + An LM. It is not None when params.decoding_method is "nbest-rescoring" + or "whole-lattice-rescoring". In general, the G in HLG + is a 3-gram LM, while this G is a 4-gram LM. + Returns: + Return the decoding result. See above description for the format of + the returned dict. Note: If it decodes to nothing, then return None. + """ + if HLG is not None: + device = HLG.device + else: + device = H.device + feature = batch["inputs"] + assert feature.ndim == 3 + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + if params.causal: + # this seems to cause insertions at the end of the utterance if used with zipformer. + pad_len = 30 + feature_lens += pad_len + feature = torch.nn.functional.pad( + feature, + pad=(0, 0, 0, pad_len), + value=LOG_EPS, + ) + + encoder_out, encoder_out_lens = model.forward_encoder(feature, feature_lens) + ctc_output = model.ctc_output(encoder_out) # (N, T, C) + + supervision_segments = torch.stack( + ( + supervisions["sequence_idx"], + torch.div( + supervisions["start_frame"], + params.subsampling_factor, + rounding_mode="floor", + ), + torch.div( + supervisions["num_frames"], + params.subsampling_factor, + rounding_mode="floor", + ), + ), + 1, + ).to(torch.int32) + + if H is None: + assert HLG is not None + decoding_graph = HLG + else: + assert HLG is None + assert bpe_model is not None + decoding_graph = H + + lattice = get_lattice( + nnet_output=ctc_output, + decoding_graph=decoding_graph, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + subsampling_factor=params.subsampling_factor, + ) + + if params.decoding_method == "ctc-decoding": + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + # Note: `best_path.aux_labels` contains token IDs, not word IDs + # since we are using H, not HLG here. + # + # token_ids is a lit-of-list of IDs + token_ids = get_texts(best_path) + + # hyps is a list of str, e.g., ['xxx yyy zzz', ...] + hyps = bpe_model.decode(token_ids) + + # hyps is a list of list of str, e.g., [['xxx', 'yyy', 'zzz'], ... ] + hyps = [s.split() for s in hyps] + key = "ctc-decoding" + return {key: hyps} + + if params.decoding_method == "nbest-oracle": + # Note: You can also pass rescored lattices to it. + # We choose the HLG decoded lattice for speed reasons + # as HLG decoding is faster and the oracle WER + # is only slightly worse than that of rescored lattices. + best_path = nbest_oracle( + lattice=lattice, + num_paths=params.num_paths, + ref_texts=supervisions["text"], + word_table=word_table, + nbest_scale=params.nbest_scale, + oov="", + ) + hyps = get_texts(best_path) + hyps = [[word_table[i] for i in ids] for ids in hyps] + key = f"oracle_{params.num_paths}_nbest_scale_{params.nbest_scale}" # noqa + return {key: hyps} + + if params.decoding_method in ["1best", "nbest"]: + if params.decoding_method == "1best": + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + key = "no_rescore" + else: + best_path = nbest_decoding( + lattice=lattice, + num_paths=params.num_paths, + use_double_scores=params.use_double_scores, + nbest_scale=params.nbest_scale, + ) + key = f"no_rescore-nbest-scale-{params.nbest_scale}-{params.num_paths}" # noqa + + hyps = get_texts(best_path) + hyps = [[word_table[i] for i in ids] for ids in hyps] + return {key: hyps} + + assert params.decoding_method in [ + "nbest-rescoring", + "whole-lattice-rescoring", + ] + + lm_scale_list = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7] + lm_scale_list += [0.8, 0.9, 1.0, 1.1, 1.2, 1.3] + lm_scale_list += [1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] + + if params.decoding_method == "nbest-rescoring": + best_path_dict = rescore_with_n_best_list( + lattice=lattice, + G=G, + num_paths=params.num_paths, + lm_scale_list=lm_scale_list, + nbest_scale=params.nbest_scale, + ) + elif params.decoding_method == "whole-lattice-rescoring": + best_path_dict = rescore_with_whole_lattice( + lattice=lattice, + G_with_epsilon_loops=G, + lm_scale_list=lm_scale_list, + ) + else: + assert False, f"Unsupported decoding method: {params.decoding_method}" + + ans = dict() + if best_path_dict is not None: + for lm_scale_str, best_path in best_path_dict.items(): + hyps = get_texts(best_path) + hyps = [[word_table[i] for i in ids] for ids in hyps] + ans[lm_scale_str] = hyps + else: + ans = None + return ans + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + HLG: Optional[k2.Fsa], + H: Optional[k2.Fsa], + bpe_model: Optional[spm.SentencePieceProcessor], + word_table: k2.SymbolTable, + G: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + HLG: + The decoding graph. Used only when params.decoding_method is NOT ctc-decoding. + H: + The ctc topo. Used only when params.decoding_method is ctc-decoding. + bpe_model: + The BPE model. Used only when params.decoding_method is ctc-decoding. + word_table: + It is the word symbol table. + G: + An LM. It is not None when params.decoding_method is "nbest-rescoring" + or "whole-lattice-rescoring". In general, the G in HLG + is a 3-gram LM, while this G is a 4-gram LM. + Returns: + Return a dict, whose key may be "no-rescore" if no LM rescoring + is used, or it may be "lm_scale_0.7" if LM rescoring is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + HLG=HLG, + H=H, + bpe_model=bpe_model, + batch=batch, + word_table=word_table, + G=G, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = ref_text.split() + this_batch.append((cut_id, ref_words, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % 100 == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = params.res_dir / f"recogs-{test_set_name}-{params.suffix}.txt" + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = params.res_dir / f"errs-{test_set_name}-{params.suffix}.txt" + with open(errs_filename, "w") as f: + wer = write_error_stats(f, f"{test_set_name}-{key}", results) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = params.res_dir / f"wer-summary-{test_set_name}-{params.suffix}.txt" + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + args.lang_dir = Path(args.lang_dir) + args.lm_dir = Path(args.lm_dir) + + params = get_params() + # add decoding params + params.update(get_decoding_params()) + params.update(vars(args)) + + assert params.decoding_method in ( + "ctc-decoding", + "1best", + "nbest", + "nbest-rescoring", + "whole-lattice-rescoring", + "nbest-oracle", + ) + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if params.causal: + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + params.suffix += f"-chunk-{params.chunk_size}" + params.suffix += f"-left-context-{params.left_context_frames}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + logging.info(params) + + lexicon = Lexicon(params.lang_dir) + max_token_id = max(lexicon.tokens) + num_classes = max_token_id + 1 # +1 for the blank + + params.vocab_size = num_classes + # and are defined in local/train_bpe_model.py + params.blank_id = 0 + + if params.decoding_method == "ctc-decoding": + HLG = None + H = k2.ctc_topo( + max_token=max_token_id, + modified=False, + device=device, + ) + bpe_model = spm.SentencePieceProcessor() + bpe_model.load(str(params.lang_dir / "bpe.model")) + else: + H = None + bpe_model = None + HLG = k2.Fsa.from_dict( + torch.load(f"{params.lang_dir}/HLG.pt", map_location=device) + ) + assert HLG.requires_grad is False + + HLG.scores *= params.hlg_scale + if not hasattr(HLG, "lm_scores"): + HLG.lm_scores = HLG.scores.clone() + + if params.decoding_method in ( + "nbest-rescoring", + "whole-lattice-rescoring", + ): + if not (params.lm_dir / "G_4_gram.pt").is_file(): + logging.info("Loading G_4_gram.fst.txt") + logging.warning("It may take 8 minutes.") + with open(params.lm_dir / "G_4_gram.fst.txt") as f: + first_word_disambig_id = lexicon.word_table["#0"] + + G = k2.Fsa.from_openfst(f.read(), acceptor=False) + # G.aux_labels is not needed in later computations, so + # remove it here. + del G.aux_labels + # CAUTION: The following line is crucial. + # Arcs entering the back-off state have label equal to #0. + # We have to change it to 0 here. + G.labels[G.labels >= first_word_disambig_id] = 0 + # See https://github.com/k2-fsa/k2/issues/874 + # for why we need to set G.properties to None + G.__dict__["_properties"] = None + G = k2.Fsa.from_fsas([G]).to(device) + G = k2.arc_sort(G) + # Save a dummy value so that it can be loaded in C++. + # See https://github.com/pytorch/pytorch/issues/67902 + # for why we need to do this. + G.dummy = 1 + + torch.save(G.as_dict(), params.lm_dir / "G_4_gram.pt") + else: + logging.info("Loading pre-compiled G_4_gram.pt") + d = torch.load(params.lm_dir / "G_4_gram.pt", map_location=device) + G = k2.Fsa.from_dict(d) + + if params.decoding_method == "whole-lattice-rescoring": + # Add epsilon self-loops to G as we will compose + # it with the whole lattice later + G = k2.add_epsilon_self_loops(G) + G = k2.arc_sort(G) + G = G.to(device) + + # G.lm_scores is used to replace HLG.lm_scores during + # LM rescoring. + G.lm_scores = G.scores.clone() + else: + G = None + + logging.info("About to create model") + model = get_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + librispeech = LibriSpeechAsrDataModule(args) + + test_clean_cuts = librispeech.test_clean_cuts() + test_other_cuts = librispeech.test_other_cuts() + + test_clean_dl = librispeech.test_dataloaders(test_clean_cuts) + test_other_dl = librispeech.test_dataloaders(test_other_cuts) + + test_sets = ["test-clean", "test-other"] + test_dl = [test_clean_dl, test_other_dl] + + for test_set, test_dl in zip(test_sets, test_dl): + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + HLG=HLG, + H=H, + bpe_model=bpe_model, + word_table=lexicon.word_table, + G=G, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/decode.py b/egs/librispeech/ASR/zipformer/decode.py index f4b81cfe3..93680602e 100755 --- a/egs/librispeech/ASR/zipformer/decode.py +++ b/egs/librispeech/ASR/zipformer/decode.py @@ -116,7 +116,7 @@ from beam_search import ( greedy_search_batch, modified_beam_search, ) -from train import add_model_arguments, get_params, get_transducer_model +from train import add_model_arguments, get_params, get_model from icefall.checkpoint import ( average_checkpoints, @@ -366,15 +366,7 @@ def decode_one_batch( value=LOG_EPS, ) - x, x_lens = model.encoder_embed(feature, feature_lens) - - src_key_padding_mask = make_pad_mask(x_lens) - x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) - - encoder_out, encoder_out_lens = model.encoder( - x, x_lens, src_key_padding_mask - ) - encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + encoder_out, encoder_out_lens = model.forward_encoder(feature, feature_lens) hyps = [] @@ -694,7 +686,7 @@ def main(): logging.info(params) logging.info("About to create model") - model = get_transducer_model(params) + model = get_model(params) if not params.use_averaged_model: if params.iter > 0: diff --git a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py index 356935657..8cec09869 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py +++ b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py @@ -76,7 +76,7 @@ import torch.nn as nn from decoder import Decoder from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled -from train import add_model_arguments, get_params, get_transducer_model +from train import add_model_arguments, get_params, get_model from zipformer import Zipformer2 from icefall.checkpoint import ( @@ -595,7 +595,7 @@ def main(): logging.info(params) logging.info("About to create model") - model = get_transducer_model(params) + model = get_model(params) model.to(device) diff --git a/egs/librispeech/ASR/zipformer/export-onnx.py b/egs/librispeech/ASR/zipformer/export-onnx.py index 490e7c2e9..f5b01ce71 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx.py +++ b/egs/librispeech/ASR/zipformer/export-onnx.py @@ -74,7 +74,7 @@ import torch.nn as nn from decoder import Decoder from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled -from train import add_model_arguments, get_params, get_transducer_model +from train import add_model_arguments, get_params, get_model from zipformer import Zipformer2 from icefall.checkpoint import ( @@ -444,7 +444,7 @@ def main(): logging.info(params) logging.info("About to create model") - model = get_transducer_model(params) + model = get_model(params) model.to(device) diff --git a/egs/librispeech/ASR/zipformer/export.py b/egs/librispeech/ASR/zipformer/export.py index b996470aa..a100cbb8d 100755 --- a/egs/librispeech/ASR/zipformer/export.py +++ b/egs/librispeech/ASR/zipformer/export.py @@ -161,7 +161,7 @@ from typing import List, Tuple import sentencepiece as spm import torch from torch import Tensor, nn -from train import add_model_arguments, get_params, get_transducer_model +from train import add_model_arguments, get_params, get_model from icefall.checkpoint import ( average_checkpoints, @@ -408,7 +408,7 @@ def main(): logging.info(params) logging.info("About to create model") - model = get_transducer_model(params) + model = get_model(params) if not params.use_averaged_model: if params.iter > 0: diff --git a/egs/librispeech/ASR/zipformer/generate_averaged_model.py b/egs/librispeech/ASR/zipformer/generate_averaged_model.py index fe29355f2..e0c7b52cb 100755 --- a/egs/librispeech/ASR/zipformer/generate_averaged_model.py +++ b/egs/librispeech/ASR/zipformer/generate_averaged_model.py @@ -44,7 +44,7 @@ import sentencepiece as spm import torch from asr_datamodule import LibriSpeechAsrDataModule -from train import add_model_arguments, get_params, get_transducer_model +from train import add_model_arguments, get_params, get_model from icefall.checkpoint import ( average_checkpoints_with_averaged_model, @@ -140,7 +140,7 @@ def main(): params.vocab_size = sp.get_piece_size() print("About to create model") - model = get_transducer_model(params) + model = get_model(params) if params.iter > 0: filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained.py b/egs/librispeech/ASR/zipformer/jit_pretrained.py index 4092d165e..87cd5102c 100755 --- a/egs/librispeech/ASR/zipformer/jit_pretrained.py +++ b/egs/librispeech/ASR/zipformer/jit_pretrained.py @@ -97,7 +97,7 @@ def read_sound_files( sample_rate == expected_sample_rate ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" # We use only the first channel - ans.append(wave[0]) + ans.append(wave[0].contiguous()) return ans diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py new file mode 100755 index 000000000..1ec390d5b --- /dev/null +++ b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python3 +# Copyright 2022-2023 Xiaomi Corp. (authors: Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads a checkpoint and uses it to decode waves. +You can generate the checkpoint with the following command: + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --causal 1 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +Usage of this script: + +(1) ctc-decoding +./zipformer/jit_pretrained_ctc.py \ + --model-filename ./zipformer/exp/jit_script.pt \ + --bpe-model data/lang_bpe_500/bpe.model \ + --method ctc-decoding \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) 1best +./zipformer/jit_pretrained_ctc.py \ + --model-filename ./zipformer/exp/jit_script.pt \ + --HLG data/lang_bpe_500/HLG.pt \ + --words-file data/lang_bpe_500/words.txt \ + --method 1best \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) nbest-rescoring +./zipformer/jit_pretrained_ctc.py \ + --model-filename ./zipformer/exp/jit_script.pt \ + --HLG data/lang_bpe_500/HLG.pt \ + --words-file data/lang_bpe_500/words.txt \ + --G data/lm/G_4_gram.pt \ + --method nbest-rescoring \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(4) whole-lattice-rescoring +./zipformer/jit_pretrained_ctc.py \ + --model-filename ./zipformer/exp/jit_script.pt \ + --HLG data/lang_bpe_500/HLG.pt \ + --words-file data/lang_bpe_500/words.txt \ + --G data/lm/G_4_gram.pt \ + --method whole-lattice-rescoring \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav +""" + +import argparse +import logging +import math +from typing import List + +import k2 +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from ctc_decode import get_decoding_params +from torch.nn.utils.rnn import pad_sequence +from train import get_params + +from icefall.decode import ( + get_lattice, + one_best_decoding, + rescore_with_n_best_list, + rescore_with_whole_lattice, +) +from icefall.utils import get_texts + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--model-filename", + type=str, + required=True, + help="Path to the torchscript model.", + ) + + parser.add_argument( + "--words-file", + type=str, + help="""Path to words.txt. + Used only when method is not ctc-decoding. + """, + ) + + parser.add_argument( + "--HLG", + type=str, + help="""Path to HLG.pt. + Used only when method is not ctc-decoding. + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to bpe.model. + Used only when method is ctc-decoding. + """, + ) + + parser.add_argument( + "--method", + type=str, + default="1best", + help="""Decoding method. + Possible values are: + (0) ctc-decoding - Use CTC decoding. It uses a sentence + piece model, i.e., lang_dir/bpe.model, to convert + word pieces to words. It needs neither a lexicon + nor an n-gram LM. + (1) 1best - Use the best path as decoding output. Only + the transformer encoder output is used for decoding. + We call it HLG decoding. + (2) nbest-rescoring. Extract n paths from the decoding lattice, + rescore them with an LM, the path with + the highest score is the decoding result. + We call it HLG decoding + nbest n-gram LM rescoring. + (3) whole-lattice-rescoring - Use an LM to rescore the + decoding lattice and then use 1best to decode the + rescored lattice. + We call it HLG decoding + whole-lattice n-gram LM rescoring. + """, + ) + + parser.add_argument( + "--G", + type=str, + help="""An LM for rescoring. + Used only when method is + whole-lattice-rescoring or nbest-rescoring. + It's usually a 4-gram LM. + """, + ) + + parser.add_argument( + "--num-paths", + type=int, + default=100, + help=""" + Used only when method is attention-decoder. + It specifies the size of n-best list.""", + ) + + parser.add_argument( + "--ngram-lm-scale", + type=float, + default=1.3, + help=""" + Used only when method is whole-lattice-rescoring and nbest-rescoring. + It specifies the scale for n-gram LM scores. + (Note: You need to tune it on a dataset.) + """, + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=1.0, + help=""" + Used only when method is nbest-rescoring. + It specifies the scale for lattice.scores when + extracting n-best lists. A smaller value results in + more unique number of paths with the risk of missing + the best path. + """, + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float = 16000 +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"Expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + # add decoding params + params.update(get_decoding_params()) + params.update(vars(args)) + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + params.vocab_size = sp.get_piece_size() + + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + model = torch.jit.load(args.model_filename) + model.to(device) + model.eval() + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + feature_lengths = torch.tensor(feature_lengths, device=device) + + encoder_out, encoder_out_lens = model.encoder(features, feature_lengths) + ctc_output = model.ctc_output(encoder_out) # (N, T, C) + + batch_size = ctc_output.shape[0] + supervision_segments = torch.tensor( + [ + [i, 0, feature_lengths[i].item() // params.subsampling_factor] + for i in range(batch_size) + ], + dtype=torch.int32, + ) + + if params.method == "ctc-decoding": + logging.info("Use CTC decoding") + max_token_id = params.vocab_size - 1 + + H = k2.ctc_topo( + max_token=max_token_id, + modified=False, + device=device, + ) + + lattice = get_lattice( + nnet_output=ctc_output, + decoding_graph=H, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + subsampling_factor=params.subsampling_factor, + ) + + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + token_ids = get_texts(best_path) + hyps = sp.decode(token_ids) + hyps = [s.split() for s in hyps] + elif params.method in [ + "1best", + "nbest-rescoring", + "whole-lattice-rescoring", + ]: + logging.info(f"Loading HLG from {params.HLG}") + HLG = k2.Fsa.from_dict(torch.load(params.HLG, map_location="cpu")) + HLG = HLG.to(device) + if not hasattr(HLG, "lm_scores"): + # For whole-lattice-rescoring and attention-decoder + HLG.lm_scores = HLG.scores.clone() + + if params.method in [ + "nbest-rescoring", + "whole-lattice-rescoring", + ]: + logging.info(f"Loading G from {params.G}") + G = k2.Fsa.from_dict(torch.load(params.G, map_location="cpu")) + G = G.to(device) + if params.method == "whole-lattice-rescoring": + # Add epsilon self-loops to G as we will compose + # it with the whole lattice later + G = k2.add_epsilon_self_loops(G) + G = k2.arc_sort(G) + + # G.lm_scores is used to replace HLG.lm_scores during + # LM rescoring. + G.lm_scores = G.scores.clone() + + lattice = get_lattice( + nnet_output=ctc_output, + decoding_graph=HLG, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + subsampling_factor=params.subsampling_factor, + ) + + if params.method == "1best": + logging.info("Use HLG decoding") + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + if params.method == "nbest-rescoring": + logging.info("Use HLG decoding + LM rescoring") + best_path_dict = rescore_with_n_best_list( + lattice=lattice, + G=G, + num_paths=params.num_paths, + lm_scale_list=[params.ngram_lm_scale], + nbest_scale=params.nbest_scale, + ) + best_path = next(iter(best_path_dict.values())) + elif params.method == "whole-lattice-rescoring": + logging.info("Use HLG decoding + LM rescoring") + best_path_dict = rescore_with_whole_lattice( + lattice=lattice, + G_with_epsilon_loops=G, + lm_scale_list=[params.ngram_lm_scale], + ) + best_path = next(iter(best_path_dict.values())) + + hyps = get_texts(best_path) + word_sym_table = k2.SymbolTable.from_file(params.words_file) + hyps = [[word_sym_table[i] for i in ids] for ids in hyps] + else: + raise ValueError(f"Unsupported decoding method: {params.method}") + + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/model.py b/egs/librispeech/ASR/zipformer/model.py index ea2b4b721..9b7494972 100644 --- a/egs/librispeech/ASR/zipformer/model.py +++ b/egs/librispeech/ASR/zipformer/model.py @@ -1,4 +1,6 @@ -# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, Wei Kang) +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Zengwei Yao) # # See ../../../../LICENSE for clarification regarding multiple authors # @@ -14,6 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Optional, Tuple import k2 import torch @@ -24,23 +27,25 @@ from icefall.utils import add_sos, make_pad_mask from scaling import ScaledLinear -class Transducer(nn.Module): - """It implements https://arxiv.org/pdf/1211.3711.pdf - "Sequence Transduction with Recurrent Neural Networks" - """ - +class AsrModel(nn.Module): def __init__( self, encoder_embed: nn.Module, encoder: EncoderInterface, - decoder: nn.Module, - joiner: nn.Module, - encoder_dim: int, - decoder_dim: int, - joiner_dim: int, - vocab_size: int, + decoder: Optional[nn.Module] = None, + joiner: Optional[nn.Module] = None, + encoder_dim: int = 384, + decoder_dim: int = 512, + vocab_size: int = 500, + use_transducer: bool = True, + use_ctc: bool = False, ): - """ + """A joint CTC & Transducer ASR model. + + - Connectionist temporal classification: labelling unsegmented sequence data with recurrent neural networks (http://imagine.enpc.fr/~obozinsg/teaching/mva_gm/papers/ctc.pdf) + - Sequence Transduction with Recurrent Neural Networks (https://arxiv.org/pdf/1211.3711.pdf) + - Pruned RNN-T for fast, memory-efficient ASR training (https://arxiv.org/pdf/2206.13236.pdf) + Args: encoder_embed: It is a Convolutional 2D subsampling module. It converts @@ -55,47 +60,133 @@ class Transducer(nn.Module): It is the prediction network in the paper. Its input shape is (N, U) and its output shape is (N, U, decoder_dim). It should contain one attribute: `blank_id`. + It is used when use_transducer is True. joiner: It has two inputs with shapes: (N, T, encoder_dim) and (N, U, decoder_dim). Its output shape is (N, T, U, vocab_size). Note that its output contains unnormalized probs, i.e., not processed by log-softmax. + It is used when use_transducer is True. + use_transducer: + Whether use transducer head. Default: True. + use_ctc: + Whether use CTC head. Default: False. """ super().__init__() + + assert ( + use_transducer or use_ctc + ), f"At least one of them should be True, but got use_transducer={use_transducer}, use_ctc={use_ctc}" + assert isinstance(encoder, EncoderInterface), type(encoder) - assert hasattr(decoder, "blank_id") self.encoder_embed = encoder_embed self.encoder = encoder - self.decoder = decoder - self.joiner = joiner - self.simple_am_proj = ScaledLinear( - encoder_dim, - vocab_size, - initial_scale=0.25, - ) - self.simple_lm_proj = ScaledLinear( - decoder_dim, - vocab_size, - initial_scale=0.25, - ) + self.use_transducer = use_transducer + if use_transducer: + # Modules for Transducer head + assert decoder is not None + assert hasattr(decoder, "blank_id") + assert joiner is not None - def forward( - self, - x: torch.Tensor, - x_lens: torch.Tensor, - y: k2.RaggedTensor, - prune_range: int = 5, - am_scale: float = 0.0, - lm_scale: float = 0.0, - ) -> torch.Tensor: - """ + self.decoder = decoder + self.joiner = joiner + + self.simple_am_proj = ScaledLinear( + encoder_dim, vocab_size, initial_scale=0.25 + ) + self.simple_lm_proj = ScaledLinear( + decoder_dim, vocab_size, initial_scale=0.25 + ) + else: + assert decoder is None + assert joiner is None + + self.use_ctc = use_ctc + if use_ctc: + # Modules for CTC head + self.ctc_output = nn.Sequential( + nn.Dropout(p=0.1), + nn.Linear(encoder_dim, vocab_size), + nn.LogSoftmax(dim=-1), + ) + + def forward_encoder( + self, x: torch.Tensor, x_lens: torch.Tensor + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Compute encoder outputs. Args: x: A 3-D tensor of shape (N, T, C). x_lens: A 1-D tensor of shape (N,). It contains the number of frames in `x` before padding. + + Returns: + encoder_out: + Encoder output, of shape (N, T, C). + encoder_out_lens: + Encoder output lengths, of shape (N,). + """ + # logging.info(f"Memory allocated at entry: {torch.cuda.memory_allocated() // 1000000}M") + x, x_lens = self.encoder_embed(x, x_lens) + # logging.info(f"Memory allocated after encoder_embed: {torch.cuda.memory_allocated() // 1000000}M") + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = self.encoder(x, x_lens, src_key_padding_mask) + + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + assert torch.all(encoder_out_lens > 0), (x_lens, encoder_out_lens) + + return encoder_out, encoder_out_lens + + def forward_ctc( + self, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + targets: torch.Tensor, + target_lengths: torch.Tensor, + ) -> torch.Tensor: + """Compute CTC loss. + Args: + encoder_out: + Encoder output, of shape (N, T, C). + encoder_out_lens: + Encoder output lengths, of shape (N,). + targets: + Target Tensor of shape (sum(target_lengths)). The targets are assumed + to be un-padded and concatenated within 1 dimension. + """ + # Compute CTC log-prob + ctc_output = self.ctc_output(encoder_out) # (N, T, C) + + ctc_loss = torch.nn.functional.ctc_loss( + log_probs=ctc_output.permute(1, 0, 2), # (T, N, C) + targets=targets, + input_lengths=encoder_out_lens, + target_lengths=target_lengths, + reduction="sum", + ) + return ctc_loss + + def forward_transducer( + self, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + y: k2.RaggedTensor, + y_lens: torch.Tensor, + prune_range: int = 5, + am_scale: float = 0.0, + lm_scale: float = 0.0, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """Compute Transducer loss. + Args: + encoder_out: + Encoder output, of shape (N, T, C). + encoder_out_lens: + Encoder output lengths, of shape (N,). y: A ragged tensor with 2 axes [utt][label]. It contains labels of each utterance. @@ -108,37 +199,8 @@ class Transducer(nn.Module): lm_scale: The scale to smooth the loss with lm (output of predictor network) part - Returns: - Return the transducer loss. - - Note: - Regarding am_scale & lm_scale, it will make the loss-function one of - the form: - lm_scale * lm_probs + am_scale * am_probs + - (1-lm_scale-am_scale) * combined_probs """ - assert x.ndim == 3, x.shape - assert x_lens.ndim == 1, x_lens.shape - assert y.num_axes == 2, y.num_axes - - assert x.size(0) == x_lens.size(0) == y.dim0 - - # logging.info(f"Memory allocated at entry: {torch.cuda.memory_allocated() // 1000000}M") - x, x_lens = self.encoder_embed(x, x_lens) - # logging.info(f"Memory allocated after encoder_embed: {torch.cuda.memory_allocated() // 1000000}M") - - src_key_padding_mask = make_pad_mask(x_lens) - x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) - - encoder_out, x_lens = self.encoder(x, x_lens, src_key_padding_mask) - encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) - - assert torch.all(x_lens > 0) - # Now for the decoder, i.e., the prediction network - row_splits = y.shape.row_splits(1) - y_lens = row_splits[1:] - row_splits[:-1] - blank_id = self.decoder.blank_id sos_y = add_sos(y, sos_id=blank_id) @@ -159,7 +221,7 @@ class Transducer(nn.Module): device=encoder_out.device, ) boundary[:, 2] = y_lens - boundary[:, 3] = x_lens + boundary[:, 3] = encoder_out_lens lm = self.simple_lm_proj(decoder_out) am = self.simple_am_proj(encoder_out) @@ -214,4 +276,83 @@ class Transducer(nn.Module): reduction="sum", ) - return (simple_loss, pruned_loss) + return simple_loss, pruned_loss + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + y: k2.RaggedTensor, + prune_range: int = 5, + am_scale: float = 0.0, + lm_scale: float = 0.0, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Args: + x: + A 3-D tensor of shape (N, T, C). + x_lens: + A 1-D tensor of shape (N,). It contains the number of frames in `x` + before padding. + y: + A ragged tensor with 2 axes [utt][label]. It contains labels of each + utterance. + prune_range: + The prune range for rnnt loss, it means how many symbols(context) + we are considering for each frame to compute the loss. + am_scale: + The scale to smooth the loss with am (output of encoder network) + part + lm_scale: + The scale to smooth the loss with lm (output of predictor network) + part + Returns: + Return the transducer losses and CTC loss, + in form of (simple_loss, pruned_loss, ctc_loss) + + Note: + Regarding am_scale & lm_scale, it will make the loss-function one of + the form: + lm_scale * lm_probs + am_scale * am_probs + + (1-lm_scale-am_scale) * combined_probs + """ + assert x.ndim == 3, x.shape + assert x_lens.ndim == 1, x_lens.shape + assert y.num_axes == 2, y.num_axes + + assert x.size(0) == x_lens.size(0) == y.dim0 + + # Compute encoder outputs + encoder_out, encoder_out_lens = self.forward_encoder(x, x_lens) + + row_splits = y.shape.row_splits(1) + y_lens = row_splits[1:] - row_splits[:-1] + + if self.use_transducer: + # Compute transducer loss + simple_loss, pruned_loss = self.forward_transducer( + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + y=y, + y_lens=y_lens, + prune_range=prune_range, + am_scale=am_scale, + lm_scale=lm_scale, + ) + else: + simple_loss = 0 + pruned_loss = 0 + + if self.use_ctc: + # Compute CTC loss + targets = y.values + ctc_loss = self.forward_ctc( + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + targets=targets, + target_lengths=y_lens, + ) + else: + ctc_loss = 0 + + return simple_loss, pruned_loss, ctc_loss diff --git a/egs/librispeech/ASR/zipformer/pretrained.py b/egs/librispeech/ASR/zipformer/pretrained.py index a4b7c2c36..2944f79e3 100755 --- a/egs/librispeech/ASR/zipformer/pretrained.py +++ b/egs/librispeech/ASR/zipformer/pretrained.py @@ -120,9 +120,8 @@ from beam_search import ( greedy_search_batch, modified_beam_search, ) -from icefall.utils import make_pad_mask from torch.nn.utils.rnn import pad_sequence -from train import add_model_arguments, get_params, get_transducer_model +from train import add_model_arguments, get_params, get_model def get_parser(): @@ -246,7 +245,7 @@ def read_sound_files( sample_rate == expected_sample_rate ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" # We use only the first channel - ans.append(wave[0]) + ans.append(wave[0].contiguous()) return ans @@ -284,7 +283,7 @@ def main(): ), "left_context_frames should be one value in decoding." logging.info("Creating model") - model = get_transducer_model(params) + model = get_model(params) num_param = sum([p.numel() for p in model.parameters()]) logging.info(f"Number of model parameters: {num_param}") @@ -318,15 +317,7 @@ def main(): feature_lengths = torch.tensor(feature_lengths, device=device) # model forward - x, x_lens = model.encoder_embed(features, feature_lengths) - - src_key_padding_mask = make_pad_mask(x_lens) - x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) - - encoder_out, encoder_out_lens = model.encoder( - x, x_lens, src_key_padding_mask - ) - encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + encoder_out, encoder_out_lens = model.forward_encoder(features, feature_lengths) hyps = [] msg = f"Using {params.method}" diff --git a/egs/librispeech/ASR/zipformer/pretrained_ctc.py b/egs/librispeech/ASR/zipformer/pretrained_ctc.py new file mode 100755 index 000000000..f10d95449 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/pretrained_ctc.py @@ -0,0 +1,446 @@ +#!/usr/bin/env python3 +# Copyright 2022-2023 Xiaomi Corp. (authors: Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads a checkpoint and uses it to decode waves. +You can generate the checkpoint with the following command: + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --use-ctc 1 \ + --causal 1 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 + +Usage of this script: + +(1) ctc-decoding +./zipformer/pretrained_ctc.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --bpe-model data/lang_bpe_500/bpe.model \ + --method ctc-decoding \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) 1best +./zipformer/pretrained_ctc.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --HLG data/lang_bpe_500/HLG.pt \ + --words-file data/lang_bpe_500/words.txt \ + --method 1best \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) nbest-rescoring +./zipformer/pretrained_ctc.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --HLG data/lang_bpe_500/HLG.pt \ + --words-file data/lang_bpe_500/words.txt \ + --G data/lm/G_4_gram.pt \ + --method nbest-rescoring \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav + + +(4) whole-lattice-rescoring +./zipformer/pretrained_ctc.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --HLG data/lang_bpe_500/HLG.pt \ + --words-file data/lang_bpe_500/words.txt \ + --G data/lm/G_4_gram.pt \ + --method whole-lattice-rescoring \ + --sample-rate 16000 \ + /path/to/foo.wav \ + /path/to/bar.wav +""" + +import argparse +import logging +import math +from typing import List + +import k2 +import kaldifeat +import sentencepiece as spm +import torch +import torchaudio +from ctc_decode import get_decoding_params +from torch.nn.utils.rnn import pad_sequence +from train import add_model_arguments, get_params, get_model + +from icefall.decode import ( + get_lattice, + one_best_decoding, + rescore_with_n_best_list, + rescore_with_whole_lattice, +) +from icefall.utils import get_texts + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--checkpoint", + type=str, + required=True, + help="Path to the checkpoint. " + "The checkpoint is assumed to be saved by " + "icefall.checkpoint.save_checkpoint().", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; " "2 means tri-gram", + ) + + parser.add_argument( + "--words-file", + type=str, + help="""Path to words.txt. + Used only when method is not ctc-decoding. + """, + ) + + parser.add_argument( + "--HLG", + type=str, + help="""Path to HLG.pt. + Used only when method is not ctc-decoding. + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to bpe.model. + Used only when method is ctc-decoding. + """, + ) + + parser.add_argument( + "--method", + type=str, + default="1best", + help="""Decoding method. + Possible values are: + (0) ctc-decoding - Use CTC decoding. It uses a sentence + piece model, i.e., lang_dir/bpe.model, to convert + word pieces to words. It needs neither a lexicon + nor an n-gram LM. + (1) 1best - Use the best path as decoding output. Only + the transformer encoder output is used for decoding. + We call it HLG decoding. + (2) nbest-rescoring. Extract n paths from the decoding lattice, + rescore them with an LM, the path with + the highest score is the decoding result. + We call it HLG decoding + nbest n-gram LM rescoring. + (3) whole-lattice-rescoring - Use an LM to rescore the + decoding lattice and then use 1best to decode the + rescored lattice. + We call it HLG decoding + whole-lattice n-gram LM rescoring. + """, + ) + + parser.add_argument( + "--G", + type=str, + help="""An LM for rescoring. + Used only when method is + whole-lattice-rescoring or nbest-rescoring. + It's usually a 4-gram LM. + """, + ) + + parser.add_argument( + "--num-paths", + type=int, + default=100, + help=""" + Used only when method is attention-decoder. + It specifies the size of n-best list.""", + ) + + parser.add_argument( + "--ngram-lm-scale", + type=float, + default=1.3, + help=""" + Used only when method is whole-lattice-rescoring and nbest-rescoring. + It specifies the scale for n-gram LM scores. + (Note: You need to tune it on a dataset.) + """, + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=1.0, + help=""" + Used only when method is nbest-rescoring. + It specifies the scale for lattice.scores when + extracting n-best lists. A smaller value results in + more unique number of paths with the risk of missing + the best path. + """, + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + add_model_arguments(parser) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float = 16000 +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert sample_rate == expected_sample_rate, ( + f"expected sample rate: {expected_sample_rate}. " f"Given: {sample_rate}" + ) + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + # add decoding params + params.update(get_decoding_params()) + params.update(vars(args)) + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + params.vocab_size = sp.get_piece_size() + params.blank_id = 0 + + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + logging.info("Creating model") + model = get_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + checkpoint = torch.load(args.checkpoint, map_location="cpu") + model.load_state_dict(checkpoint["model"], strict=False) + model.to(device) + model.eval() + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + feature_lengths = torch.tensor(feature_lengths, device=device) + + encoder_out, encoder_out_lens = model.forward_encoder(features, feature_lengths) + ctc_output = model.ctc_output(encoder_out) # (N, T, C) + + batch_size = ctc_output.shape[0] + supervision_segments = torch.tensor( + [ + [i, 0, feature_lengths[i].item() // params.subsampling_factor] + for i in range(batch_size) + ], + dtype=torch.int32, + ) + + if params.method == "ctc-decoding": + logging.info("Use CTC decoding") + max_token_id = params.vocab_size - 1 + + H = k2.ctc_topo( + max_token=max_token_id, + modified=False, + device=device, + ) + + lattice = get_lattice( + nnet_output=ctc_output, + decoding_graph=H, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + subsampling_factor=params.subsampling_factor, + ) + + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + token_ids = get_texts(best_path) + hyps = sp.decode(token_ids) + hyps = [s.split() for s in hyps] + elif params.method in [ + "1best", + "nbest-rescoring", + "whole-lattice-rescoring", + ]: + logging.info(f"Loading HLG from {params.HLG}") + HLG = k2.Fsa.from_dict(torch.load(params.HLG, map_location="cpu")) + HLG = HLG.to(device) + if not hasattr(HLG, "lm_scores"): + # For whole-lattice-rescoring and attention-decoder + HLG.lm_scores = HLG.scores.clone() + + if params.method in [ + "nbest-rescoring", + "whole-lattice-rescoring", + ]: + logging.info(f"Loading G from {params.G}") + G = k2.Fsa.from_dict(torch.load(params.G, map_location="cpu")) + G = G.to(device) + if params.method == "whole-lattice-rescoring": + # Add epsilon self-loops to G as we will compose + # it with the whole lattice later + G = k2.add_epsilon_self_loops(G) + G = k2.arc_sort(G) + + # G.lm_scores is used to replace HLG.lm_scores during + # LM rescoring. + G.lm_scores = G.scores.clone() + + lattice = get_lattice( + nnet_output=ctc_output, + decoding_graph=HLG, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + subsampling_factor=params.subsampling_factor, + ) + + if params.method == "1best": + logging.info("Use HLG decoding") + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + if params.method == "nbest-rescoring": + logging.info("Use HLG decoding + LM rescoring") + best_path_dict = rescore_with_n_best_list( + lattice=lattice, + G=G, + num_paths=params.num_paths, + lm_scale_list=[params.ngram_lm_scale], + nbest_scale=params.nbest_scale, + ) + best_path = next(iter(best_path_dict.values())) + elif params.method == "whole-lattice-rescoring": + logging.info("Use HLG decoding + LM rescoring") + best_path_dict = rescore_with_whole_lattice( + lattice=lattice, + G_with_epsilon_loops=G, + lm_scale_list=[params.ngram_lm_scale], + ) + best_path = next(iter(best_path_dict.values())) + + hyps = get_texts(best_path) + word_sym_table = k2.SymbolTable.from_file(params.words_file) + hyps = [[word_sym_table[i] for i in ids] for ids in hyps] + else: + raise ValueError(f"Unsupported decoding method: {params.method}") + + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/streaming_decode.py b/egs/librispeech/ASR/zipformer/streaming_decode.py index 3f140b4fa..44ff392a3 100755 --- a/egs/librispeech/ASR/zipformer/streaming_decode.py +++ b/egs/librispeech/ASR/zipformer/streaming_decode.py @@ -51,7 +51,7 @@ from streaming_beam_search import ( ) from torch import Tensor, nn from torch.nn.utils.rnn import pad_sequence -from train import add_model_arguments, get_params, get_transducer_model +from train import add_model_arguments, get_params, get_model from icefall.checkpoint import ( average_checkpoints, @@ -756,7 +756,7 @@ def main(): logging.info(params) logging.info("About to create model") - model = get_transducer_model(params) + model = get_model(params) if not params.use_averaged_model: if params.iter > 0: diff --git a/egs/librispeech/ASR/zipformer/train.py b/egs/librispeech/ASR/zipformer/train.py index bec9a3986..1d1bee947 100755 --- a/egs/librispeech/ASR/zipformer/train.py +++ b/egs/librispeech/ASR/zipformer/train.py @@ -44,6 +44,10 @@ export CUDA_VISIBLE_DEVICES="0,1,2,3" --full-libri 1 \ --max-duration 1000 +It supports training with: + - transducer loss (default), with `--use-transducer True --use-ctc False` + - ctc loss (not recommended), with `--use-transducer False --use-ctc True` + - transducer loss & ctc loss, with `--use-transducer True --use-ctc True` """ @@ -67,7 +71,7 @@ from joiner import Joiner from lhotse.cut import Cut from lhotse.dataset.sampling.base import CutSampler from lhotse.utils import fix_random_seed -from model import Transducer +from model import AsrModel from optim import Eden, ScaledAdam from scaling import ScheduledFloat from subsampling import Conv2dSubsampling @@ -240,6 +244,20 @@ def add_model_arguments(parser: argparse.ArgumentParser): "chunk left-context frames will be chosen randomly from this list; else not relevant.", ) + parser.add_argument( + "--use-transducer", + type=str2bool, + default=True, + help="If True, use Transducer head.", + ) + + parser.add_argument( + "--use-ctc", + type=str2bool, + default=False, + help="If True, use CTC head.", + ) + def get_parser(): parser = argparse.ArgumentParser( @@ -378,6 +396,13 @@ def get_parser(): "with this parameter before adding to the final loss.", ) + parser.add_argument( + "--ctc-loss-scale", + type=float, + default=0.2, + help="Scale for CTC loss.", + ) + parser.add_argument( "--seed", type=int, @@ -578,21 +603,33 @@ def get_joiner_model(params: AttributeDict) -> nn.Module: return joiner -def get_transducer_model(params: AttributeDict) -> nn.Module: +def get_model(params: AttributeDict) -> nn.Module: + assert ( + params.use_transducer or params.use_ctc + ), (f"At least one of them should be True, " + f"but got params.use_transducer={params.use_transducer}, " + f"params.use_ctc={params.use_ctc}") + encoder_embed = get_encoder_embed(params) encoder = get_encoder_model(params) - decoder = get_decoder_model(params) - joiner = get_joiner_model(params) - model = Transducer( + if params.use_transducer: + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + else: + decoder = None + joiner = None + + model = AsrModel( encoder_embed=encoder_embed, encoder=encoder, decoder=decoder, joiner=joiner, - encoder_dim=int(max(params.encoder_dim.split(","))), + encoder_dim=max(_to_int_tuple(params.encoder_dim)), decoder_dim=params.decoder_dim, - joiner_dim=params.joiner_dim, vocab_size=params.vocab_size, + use_transducer=params.use_transducer, + use_ctc=params.use_ctc, ) return model @@ -721,7 +758,7 @@ def compute_loss( is_training: bool, ) -> Tuple[Tensor, MetricsTracker]: """ - Compute CTC loss given the model and its inputs. + Compute loss given the model and its inputs. Args: params: @@ -755,7 +792,7 @@ def compute_loss( y = k2.RaggedTensor(y).to(device) with torch.set_grad_enabled(is_training): - simple_loss, pruned_loss = model( + simple_loss, pruned_loss, ctc_loss = model( x=feature, x_lens=feature_lens, y=y, @@ -764,21 +801,27 @@ def compute_loss( lm_scale=params.lm_scale, ) - s = params.simple_loss_scale - # take down the scale on the simple loss from 1.0 at the start - # to params.simple_loss scale by warm_step. - simple_loss_scale = ( - s - if batch_idx_train >= warm_step - else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) - ) - pruned_loss_scale = ( - 1.0 - if batch_idx_train >= warm_step - else 0.1 + 0.9 * (batch_idx_train / warm_step) - ) + loss = 0.0 - loss = simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss + if params.use_transducer: + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss += ( + simple_loss_scale * simple_loss + + pruned_loss_scale * pruned_loss + ) + + if params.use_ctc: + loss += params.ctc_loss_scale * ctc_loss assert loss.requires_grad == is_training @@ -789,8 +832,11 @@ def compute_loss( # Note: We use reduction=sum while computing the loss. info["loss"] = loss.detach().cpu().item() - info["simple_loss"] = simple_loss.detach().cpu().item() - info["pruned_loss"] = pruned_loss.detach().cpu().item() + if params.use_transducer: + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + if params.use_ctc: + info["ctc_loss"] = ctc_loss.detach().cpu().item() return loss, info @@ -1071,10 +1117,13 @@ def run(rank, world_size, args): params.blank_id = sp.piece_to_id("") params.vocab_size = sp.get_piece_size() + if not params.use_transducer: + params.ctc_loss_scale = 1.0 + logging.info(params) logging.info("About to create model") - model = get_transducer_model(params) + model = get_model(params) num_param = sum([p.numel() for p in model.parameters()]) logging.info(f"Number of model parameters: {num_param}") From 947f0614c972e96bb211a0399f0328027229fa2b Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 15 Jun 2023 12:25:15 +0800 Subject: [PATCH 031/100] Fix running exported model on GPU. (#1131) --- egs/librispeech/ASR/zipformer/zipformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egs/librispeech/ASR/zipformer/zipformer.py b/egs/librispeech/ASR/zipformer/zipformer.py index 612356a50..7d98dbeb1 100644 --- a/egs/librispeech/ASR/zipformer/zipformer.py +++ b/egs/librispeech/ASR/zipformer/zipformer.py @@ -2190,7 +2190,7 @@ class ConvolutionModule(nn.Module): x = self.in_proj(x) # (time, batch, 2*channels) - x, s = x.chunk(2, dim=-1) + x, s = x.chunk(2, dim=2) s = self.sigmoid(s) x = x * s # (time, batch, channels) From 0a465794a806ca42f43fb626ae8300b878b7ec43 Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Thu, 15 Jun 2023 17:52:14 +0800 Subject: [PATCH 032/100] Fix Zipformer (#1132) * Update model.py * Update train.py * Update decoder.py --- egs/librispeech/ASR/zipformer/decoder.py | 1 - egs/librispeech/ASR/zipformer/model.py | 2 +- egs/librispeech/ASR/zipformer/train.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/egs/librispeech/ASR/zipformer/decoder.py b/egs/librispeech/ASR/zipformer/decoder.py index 45432d570..e8db988f6 100644 --- a/egs/librispeech/ASR/zipformer/decoder.py +++ b/egs/librispeech/ASR/zipformer/decoder.py @@ -58,7 +58,6 @@ class Decoder(nn.Module): self.embedding = nn.Embedding( num_embeddings=vocab_size, embedding_dim=decoder_dim, - padding_idx=blank_id, ) # the balancers are to avoid any drift in the magnitude of the # embeddings, which would interact badly with parameter averaging. diff --git a/egs/librispeech/ASR/zipformer/model.py b/egs/librispeech/ASR/zipformer/model.py index 9b7494972..0c3ea6a86 100644 --- a/egs/librispeech/ASR/zipformer/model.py +++ b/egs/librispeech/ASR/zipformer/model.py @@ -333,7 +333,7 @@ class AsrModel(nn.Module): simple_loss, pruned_loss = self.forward_transducer( encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, - y=y, + y=y.to(x.device), y_lens=y_lens, prune_range=prune_range, am_scale=am_scale, diff --git a/egs/librispeech/ASR/zipformer/train.py b/egs/librispeech/ASR/zipformer/train.py index 1d1bee947..bc3e9c1ba 100755 --- a/egs/librispeech/ASR/zipformer/train.py +++ b/egs/librispeech/ASR/zipformer/train.py @@ -789,7 +789,7 @@ def compute_loss( texts = batch["supervisions"]["text"] y = sp.encode(texts, out_type=int) - y = k2.RaggedTensor(y).to(device) + y = k2.RaggedTensor(y) with torch.set_grad_enabled(is_training): simple_loss, pruned_loss, ctc_loss = model( From d667dc365b3259179c9d54bd32a1bb2bd8afa4f0 Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Fri, 16 Jun 2023 15:04:41 +0800 Subject: [PATCH 033/100] Fix for diagnostic (#1135) * CTC loss return tensor * Update model.py --- egs/librispeech/ASR/zipformer/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/egs/librispeech/ASR/zipformer/model.py b/egs/librispeech/ASR/zipformer/model.py index 0c3ea6a86..b541ee697 100644 --- a/egs/librispeech/ASR/zipformer/model.py +++ b/egs/librispeech/ASR/zipformer/model.py @@ -340,8 +340,8 @@ class AsrModel(nn.Module): lm_scale=lm_scale, ) else: - simple_loss = 0 - pruned_loss = 0 + simple_loss = torch.empty(0) + pruned_loss = torch.empty(0) if self.use_ctc: # Compute CTC loss @@ -353,6 +353,6 @@ class AsrModel(nn.Module): target_lengths=y_lens, ) else: - ctc_loss = 0 + ctc_loss = torch.empty(0) return simple_loss, pruned_loss, ctc_loss From 4d5b8369aef8bbe73c0baf5d72200dc2c1e9c0b6 Mon Sep 17 00:00:00 2001 From: frankyoujian Date: Wed, 21 Jun 2023 17:17:19 +0800 Subject: [PATCH 034/100] fix small typo (#1144) --- .../pruned_transducer_stateless7_streaming/streaming_decode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py index b76272e66..a0f54b6e1 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/streaming_decode.py @@ -22,7 +22,7 @@ Usage: --avg 15 \ --decode-chunk-len 32 \ --exp-dir ./pruned_transducer_stateless7_streaming/exp \ - --decoding_method greedy_search \ + --decoding-method greedy_search \ --num-decode-streams 2000 """ From 219bba1310fbc5f8e022817e0fee6711f62d5f54 Mon Sep 17 00:00:00 2001 From: Wei Kang Date: Mon, 26 Jun 2023 09:33:18 +0800 Subject: [PATCH 035/100] zipformer wenetspeech (#1130) * copy files * update train.py * small fixes * Add decode.py * Fix dataloader in decode.py * add blank penalty * Add blank-penalty to other decoding method * Minor fixes * add zipformer2 recipe * Minor fixes * Remove pruned7 * export and test models * Replace bpe with tokens in export.py and pretrain.py * Minor fixes * Minor fixes * Minor fixes * Fix export * Update results * Fix zipformer-ctc * Fix ci * Fix ci * Fix CI * Fix CI --------- Co-authored-by: Fangjun Kuang --- ...rispeech-streaming-zipformer-2023-05-18.sh | 7 +- .../run-librispeech-zipformer-2023-05-18.sh | 7 +- ...un-librispeech-zipformer-ctc-2023-06-14.sh | 8 +- .github/scripts/test-ncnn-export.sh | 4 +- .../decode.py | 1 - .../beam_search.py | 47 +- .../ASR/zipformer/export-onnx-streaming.py | 36 +- egs/librispeech/ASR/zipformer/export-onnx.py | 29 +- egs/librispeech/ASR/zipformer/export.py | 64 +- .../ASR/zipformer/generate_averaged_model.py | 29 +- .../ASR/zipformer/jit_pretrained.py | 30 +- .../ASR/zipformer/jit_pretrained_ctc.py | 26 +- .../ASR/zipformer/jit_pretrained_streaming.py | 28 +- egs/librispeech/ASR/zipformer/onnx_check.py | 241 +++ .../zipformer/onnx_pretrained-streaming.py | 4 +- .../ASR/zipformer/onnx_pretrained.py | 420 ++++- egs/librispeech/ASR/zipformer/pretrained.py | 60 +- .../ASR/zipformer/pretrained_ctc.py | 31 +- .../ASR/zipformer/streaming_beam_search.py | 13 + egs/wenetspeech/ASR/RESULTS.md | 85 ++ .../asr_datamodule.py | 2 +- .../pruned_transducer_stateless5/decode.py | 2 +- egs/wenetspeech/ASR/zipformer/__init__.py | 0 .../ASR/zipformer/asr_datamodule.py | 1 + egs/wenetspeech/ASR/zipformer/beam_search.py | 1 + egs/wenetspeech/ASR/zipformer/decode.py | 818 ++++++++++ .../ASR/zipformer/decode_stream.py | 1 + egs/wenetspeech/ASR/zipformer/decoder.py | 1 + .../ASR/zipformer/encoder_interface.py | 1 + .../ASR/zipformer/export-onnx-streaming.py | 1 + egs/wenetspeech/ASR/zipformer/export-onnx.py | 1 + egs/wenetspeech/ASR/zipformer/export.py | 1 + .../ASR/zipformer/jit_pretrained.py | 1 + .../ASR/zipformer/jit_pretrained_streaming.py | 1 + egs/wenetspeech/ASR/zipformer/joiner.py | 1 + egs/wenetspeech/ASR/zipformer/model.py | 1 + egs/wenetspeech/ASR/zipformer/onnx_check.py | 1 + egs/wenetspeech/ASR/zipformer/onnx_decode.py | 334 ++++ .../zipformer/onnx_pretrained-streaming.py | 1 + .../ASR/zipformer/onnx_pretrained.py | 1 + egs/wenetspeech/ASR/zipformer/optim.py | 1 + egs/wenetspeech/ASR/zipformer/pretrained.py | 1 + egs/wenetspeech/ASR/zipformer/scaling.py | 1 + .../ASR/zipformer/scaling_converter.py | 1 + .../ASR/zipformer/streaming_beam_search.py | 1 + .../ASR/zipformer/streaming_decode.py | 881 +++++++++++ egs/wenetspeech/ASR/zipformer/subsampling.py | 1 + egs/wenetspeech/ASR/zipformer/train.py | 1350 +++++++++++++++++ egs/wenetspeech/ASR/zipformer/zipformer.py | 1 + 49 files changed, 4401 insertions(+), 178 deletions(-) create mode 100755 egs/librispeech/ASR/zipformer/onnx_check.py mode change 120000 => 100755 egs/librispeech/ASR/zipformer/onnx_pretrained.py create mode 100644 egs/wenetspeech/ASR/zipformer/__init__.py create mode 120000 egs/wenetspeech/ASR/zipformer/asr_datamodule.py create mode 120000 egs/wenetspeech/ASR/zipformer/beam_search.py create mode 100755 egs/wenetspeech/ASR/zipformer/decode.py create mode 120000 egs/wenetspeech/ASR/zipformer/decode_stream.py create mode 120000 egs/wenetspeech/ASR/zipformer/decoder.py create mode 120000 egs/wenetspeech/ASR/zipformer/encoder_interface.py create mode 120000 egs/wenetspeech/ASR/zipformer/export-onnx-streaming.py create mode 120000 egs/wenetspeech/ASR/zipformer/export-onnx.py create mode 120000 egs/wenetspeech/ASR/zipformer/export.py create mode 120000 egs/wenetspeech/ASR/zipformer/jit_pretrained.py create mode 120000 egs/wenetspeech/ASR/zipformer/jit_pretrained_streaming.py create mode 120000 egs/wenetspeech/ASR/zipformer/joiner.py create mode 120000 egs/wenetspeech/ASR/zipformer/model.py create mode 120000 egs/wenetspeech/ASR/zipformer/onnx_check.py create mode 100755 egs/wenetspeech/ASR/zipformer/onnx_decode.py create mode 120000 egs/wenetspeech/ASR/zipformer/onnx_pretrained-streaming.py create mode 120000 egs/wenetspeech/ASR/zipformer/onnx_pretrained.py create mode 120000 egs/wenetspeech/ASR/zipformer/optim.py create mode 120000 egs/wenetspeech/ASR/zipformer/pretrained.py create mode 120000 egs/wenetspeech/ASR/zipformer/scaling.py create mode 120000 egs/wenetspeech/ASR/zipformer/scaling_converter.py create mode 120000 egs/wenetspeech/ASR/zipformer/streaming_beam_search.py create mode 100755 egs/wenetspeech/ASR/zipformer/streaming_decode.py create mode 120000 egs/wenetspeech/ASR/zipformer/subsampling.py create mode 100755 egs/wenetspeech/ASR/zipformer/train.py create mode 120000 egs/wenetspeech/ASR/zipformer/zipformer.py diff --git a/.github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh b/.github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh index 45324cb27..f4e2124b1 100755 --- a/.github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh +++ b/.github/scripts/run-librispeech-streaming-zipformer-2023-05-18.sh @@ -23,6 +23,7 @@ ls -lh $repo/test_wavs/*.wav pushd $repo/exp git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/jit_script_chunk_16_left_128.pt" git lfs pull --include "exp/pretrained.pt" ln -s pretrained.pt epoch-99.pt @@ -33,7 +34,7 @@ log "Export to torchscript model" ./zipformer/export.py \ --exp-dir $repo/exp \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --causal 1 \ --chunk-size 16 \ --left-context-frames 128 \ @@ -46,7 +47,7 @@ ls -lh $repo/exp/*.pt log "Decode with models exported by torch.jit.script()" ./zipformer/jit_pretrained_streaming.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --nn-model-filename $repo/exp/jit_script_chunk_16_left_128.pt \ $repo/test_wavs/1089-134686-0001.wav @@ -60,7 +61,7 @@ for method in greedy_search modified_beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-zipformer-2023-05-18.sh b/.github/scripts/run-librispeech-zipformer-2023-05-18.sh index 6aac1793e..fb1a0149d 100755 --- a/.github/scripts/run-librispeech-zipformer-2023-05-18.sh +++ b/.github/scripts/run-librispeech-zipformer-2023-05-18.sh @@ -23,6 +23,7 @@ ls -lh $repo/test_wavs/*.wav pushd $repo/exp git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/jit_script.pt" git lfs pull --include "exp/pretrained.pt" ln -s pretrained.pt epoch-99.pt @@ -33,7 +34,7 @@ log "Export to torchscript model" ./zipformer/export.py \ --exp-dir $repo/exp \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit 1 @@ -43,7 +44,7 @@ ls -lh $repo/exp/*.pt log "Decode with models exported by torch.jit.script()" ./zipformer/jit_pretrained.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --nn-model-filename $repo/exp/jit_script.pt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ @@ -56,7 +57,7 @@ for method in greedy_search modified_beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh b/.github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh index cfa9c420c..0026d2109 100755 --- a/.github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh +++ b/.github/scripts/run-librispeech-zipformer-ctc-2023-06-14.sh @@ -23,6 +23,7 @@ ls -lh $repo/test_wavs/*.wav pushd $repo/exp git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "data/lang_bpe_500/HLG.pt" git lfs pull --include "data/lang_bpe_500/L.pt" git lfs pull --include "data/lang_bpe_500/LG.pt" @@ -40,7 +41,7 @@ log "Export to torchscript model" --use-transducer 1 \ --use-ctc 1 \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit 1 @@ -51,7 +52,7 @@ log "Decode with models exported by torch.jit.script()" for method in ctc-decoding 1best; do ./zipformer/jit_pretrained_ctc.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --model-filename $repo/exp/jit_script.pt \ --HLG $repo/data/lang_bpe_500/HLG.pt \ --words-file $repo/data/lang_bpe_500/words.txt \ @@ -71,8 +72,7 @@ for method in ctc-decoding 1best; do --use-ctc 1 \ --method $method \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ - --words-file $repo/data/lang_bpe_500/words.txt \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --HLG $repo/data/lang_bpe_500/HLG.pt \ --G $repo/data/lm/G_4_gram.pt \ --words-file $repo/data/lang_bpe_500/words.txt \ diff --git a/.github/scripts/test-ncnn-export.sh b/.github/scripts/test-ncnn-export.sh index 52491d2ea..ac16131d0 100755 --- a/.github/scripts/test-ncnn-export.sh +++ b/.github/scripts/test-ncnn-export.sh @@ -195,14 +195,14 @@ git lfs pull --include "data/lang_char_bpe/Linv.pt" git lfs pull --include "exp/pretrained.pt" cd exp -ln -s pretrained.pt epoch-99.pt +ln -s pretrained.pt epoch-9999.pt popd ./pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py \ --lang-dir $repo/data/lang_char_bpe \ --exp-dir $repo/exp \ --use-averaged-model 0 \ - --epoch 99 \ + --epoch 9999 \ --avg 1 \ --decode-chunk-len 32 \ --num-encoder-layers "2,4,3,2,4" \ diff --git a/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/decode.py b/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/decode.py index fcb0ebc4e..da9000164 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/decode.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/decode.py @@ -397,7 +397,6 @@ def decode_one_batch( beam=params.beam, max_contexts=params.max_contexts, max_states=params.max_states, - subtract_ilme=True, ilme_scale=params.ilme_scale, ) for hyp in hyp_tokens: diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index 1bbad6946..17b63a659 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -22,6 +22,7 @@ from typing import Dict, List, Optional, Tuple, Union import k2 import sentencepiece as spm import torch +from torch import nn from icefall import ContextGraph, ContextState, NgramLm, NgramLmStateCost from icefall.decode import Nbest, one_best_decoding @@ -35,7 +36,6 @@ from icefall.utils import ( get_texts, get_texts_with_timestamp, ) -from torch import nn def fast_beam_search_one_best( @@ -47,8 +47,8 @@ def fast_beam_search_one_best( max_states: int, max_contexts: int, temperature: float = 1.0, - subtract_ilme: bool = False, - ilme_scale: float = 0.1, + ilme_scale: float = 0.0, + blank_penalty: float = 0.0, return_timestamps: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -90,8 +90,8 @@ def fast_beam_search_one_best( max_states=max_states, max_contexts=max_contexts, temperature=temperature, - subtract_ilme=subtract_ilme, ilme_scale=ilme_scale, + blank_penalty=blank_penalty, ) best_path = one_best_decoding(lattice) @@ -114,6 +114,8 @@ def fast_beam_search_nbest_LG( nbest_scale: float = 0.5, use_double_scores: bool = True, temperature: float = 1.0, + blank_penalty: float = 0.0, + ilme_scale: float = 0.0, return_timestamps: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -168,6 +170,8 @@ def fast_beam_search_nbest_LG( max_states=max_states, max_contexts=max_contexts, temperature=temperature, + blank_penalty=blank_penalty, + ilme_scale=ilme_scale, ) nbest = Nbest.from_lattice( @@ -240,6 +244,7 @@ def fast_beam_search_nbest( nbest_scale: float = 0.5, use_double_scores: bool = True, temperature: float = 1.0, + blank_penalty: float = 0.0, return_timestamps: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -293,6 +298,7 @@ def fast_beam_search_nbest( beam=beam, max_states=max_states, max_contexts=max_contexts, + blank_penalty=blank_penalty, temperature=temperature, ) @@ -331,6 +337,7 @@ def fast_beam_search_nbest_oracle( use_double_scores: bool = True, nbest_scale: float = 0.5, temperature: float = 1.0, + blank_penalty: float = 0.0, return_timestamps: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -389,6 +396,7 @@ def fast_beam_search_nbest_oracle( max_states=max_states, max_contexts=max_contexts, temperature=temperature, + blank_penalty=blank_penalty, ) nbest = Nbest.from_lattice( @@ -432,8 +440,8 @@ def fast_beam_search( max_states: int, max_contexts: int, temperature: float = 1.0, - subtract_ilme: bool = False, - ilme_scale: float = 0.1, + ilme_scale: float = 0.0, + blank_penalty: float = 0.0, ) -> k2.Fsa: """It limits the maximum number of symbols per frame to 1. @@ -503,8 +511,13 @@ def fast_beam_search( project_input=False, ) logits = logits.squeeze(1).squeeze(1) + + if blank_penalty != 0: + logits[:, 0] -= blank_penalty + log_probs = (logits / temperature).log_softmax(dim=-1) - if subtract_ilme: + + if ilme_scale != 0: ilme_logits = model.joiner( torch.zeros_like( current_encoder_out, device=current_encoder_out.device @@ -513,8 +526,11 @@ def fast_beam_search( project_input=False, ) ilme_logits = ilme_logits.squeeze(1).squeeze(1) + if blank_penalty != 0: + ilme_logits[:, 0] -= blank_penalty ilme_log_probs = (ilme_logits / temperature).log_softmax(dim=-1) log_probs -= ilme_scale * ilme_log_probs + decoding_streams.advance(log_probs) decoding_streams.terminate_and_flush_to_streams() lattice = decoding_streams.format_output(encoder_out_lens.tolist()) @@ -526,6 +542,7 @@ def greedy_search( model: nn.Module, encoder_out: torch.Tensor, max_sym_per_frame: int, + blank_penalty: float = 0.0, return_timestamps: bool = False, ) -> Union[List[int], DecodingResults]: """Greedy search for a single utterance. @@ -595,6 +612,9 @@ def greedy_search( ) # logits is (1, 1, 1, vocab_size) + if blank_penalty != 0: + logits[:, :, :, 0] -= blank_penalty + y = logits.argmax().item() if y not in (blank_id, unk_id): hyp.append(y) @@ -626,6 +646,7 @@ def greedy_search_batch( model: nn.Module, encoder_out: torch.Tensor, encoder_out_lens: torch.Tensor, + blank_penalty: float = 0, return_timestamps: bool = False, ) -> Union[List[List[int]], DecodingResults]: """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. @@ -703,6 +724,10 @@ def greedy_search_batch( logits = logits.squeeze(1).squeeze(1) # (batch_size, vocab_size) assert logits.ndim == 2, logits.shape + + if blank_penalty != 0: + logits[:, 0] -= blank_penalty + y = logits.argmax(dim=1).tolist() emitted = False for i, v in enumerate(y): @@ -923,6 +948,7 @@ def modified_beam_search( context_graph: Optional[ContextGraph] = None, beam: int = 4, temperature: float = 1.0, + blank_penalty: float = 0.0, return_timestamps: bool = False, ) -> Union[List[List[int]], DecodingResults]: """Beam search in batch mode with --max-sym-per-frame=1 being hardcoded. @@ -1028,6 +1054,9 @@ def modified_beam_search( logits = logits.squeeze(1).squeeze(1) # (num_hyps, vocab_size) + if blank_penalty != 0: + logits[:, 0] -= blank_penalty + log_probs = (logits / temperature).log_softmax(dim=-1) # (num_hyps, vocab_size) log_probs.add_(ys_log_probs) @@ -1662,6 +1691,7 @@ def beam_search( encoder_out: torch.Tensor, beam: int = 4, temperature: float = 1.0, + blank_penalty: float = 0.0, return_timestamps: bool = False, ) -> Union[List[int], DecodingResults]: """ @@ -1758,6 +1788,9 @@ def beam_search( project_input=False, ) + if blank_penalty != 0: + logits[:, :, :, 0] -= blank_penalty + # TODO(fangjun): Scale the blank posterior log_prob = (logits / temperature).log_softmax(dim=-1) # log_prob is (1, 1, 1, vocab_size) diff --git a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py index 8cec09869..80dc19b37 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py +++ b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang, Wei Kang) # Copyright 2023 Danqing Fu (danqing.fu@gmail.com) """ @@ -19,7 +19,7 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/pretrained.pt" cd exp @@ -29,7 +29,7 @@ popd 2. Export the model to ONNX ./zipformer/export-onnx-streaming.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -57,9 +57,9 @@ whose value is "64,128,256,-1". It will generate the following 3 files inside $repo/exp: - - encoder-epoch-99-avg-1.onnx - - decoder-epoch-99-avg-1.onnx - - joiner-epoch-99-avg-1.onnx + - encoder-epoch-99-avg-1-chunk-16-left-64.onnx + - decoder-epoch-99-avg-1-chunk-16-left-64.onnx + - joiner-epoch-99-avg-1-chunk-16-left-64.onnx See ./onnx_pretrained-streaming.py for how to use the exported ONNX models. """ @@ -69,14 +69,15 @@ import logging from pathlib import Path from typing import Dict, List, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from decoder import Decoder +from export import num_tokens from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled -from train import add_model_arguments, get_params, get_model +from train import add_model_arguments, get_model, get_params from zipformer import Zipformer2 from icefall.checkpoint import ( @@ -85,7 +86,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool, make_pad_mask +from icefall.utils import make_pad_mask, str2bool def get_parser(): @@ -142,10 +143,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", ) parser.add_argument( @@ -585,12 +586,9 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) - - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + token_table = k2.SymbolTable.from_file(params.tokens) + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 logging.info(params) @@ -709,6 +707,8 @@ def main(): suffix = f"epoch-{params.epoch}" suffix += f"-avg-{params.avg}" + suffix += f"-chunk-{params.chunk_size}" + suffix += f"-left-{params.left_context_frames}" opset_version = 13 diff --git a/egs/librispeech/ASR/zipformer/export-onnx.py b/egs/librispeech/ASR/zipformer/export-onnx.py index f5b01ce71..1bc10c896 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx.py +++ b/egs/librispeech/ASR/zipformer/export-onnx.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang, Wei Kang) # Copyright 2023 Danqing Fu (danqing.fu@gmail.com) """ @@ -19,7 +19,7 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/pretrained.pt" cd exp @@ -29,12 +29,11 @@ popd 2. Export the model to ONNX ./zipformer/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ --exp-dir $repo/exp \ - \ --num-encoder-layers "2,2,3,4,3,2" \ --downsampling-factor "1,2,4,8,4,2" \ --feedforward-dim "512,768,1024,1536,1024,768" \ @@ -67,14 +66,15 @@ import logging from pathlib import Path from typing import Dict, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from decoder import Decoder +from export import num_tokens from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled -from train import add_model_arguments, get_params, get_model +from train import add_model_arguments, get_model, get_params from zipformer import Zipformer2 from icefall.checkpoint import ( @@ -83,7 +83,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool, make_pad_mask +from icefall.utils import make_pad_mask, str2bool def get_parser(): @@ -140,10 +140,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", ) parser.add_argument( @@ -434,12 +434,9 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) - - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + token_table = k2.SymbolTable.from_file(params.tokens) + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 logging.info(params) diff --git a/egs/librispeech/ASR/zipformer/export.py b/egs/librispeech/ASR/zipformer/export.py index a100cbb8d..4a48d5bad 100755 --- a/egs/librispeech/ASR/zipformer/export.py +++ b/egs/librispeech/ASR/zipformer/export.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 # -# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, Zengwei Yao) +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao, +# Wei Kang) # # See ../../../../LICENSE for clarification regarding multiple authors # @@ -22,13 +24,16 @@ Usage: +Note: This is a example for librispeech dataset, if you are using different +dataset, you should change the argument values according to your dataset. + (1) Export to torchscript model using torch.jit.script() - For non-streaming model: ./zipformer/export.py \ --exp-dir ./zipformer/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -48,7 +53,7 @@ for how to use the exported models outside of icefall. --causal 1 \ --chunk-size 16 \ --left-context-frames 128 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -67,7 +72,7 @@ for how to use the exported models outside of icefall. ./zipformer/export.py \ --exp-dir ./zipformer/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 @@ -76,7 +81,7 @@ for how to use the exported models outside of icefall. ./zipformer/export.py \ --exp-dir ./zipformer/exp \ --causal 1 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 @@ -155,13 +160,15 @@ with the following commands: import argparse import logging +import re from pathlib import Path from typing import List, Tuple -import sentencepiece as spm +import k2 import torch +from scaling_converter import convert_scaled_to_non_scaled from torch import Tensor, nn -from train import add_model_arguments, get_params, get_model +from train import add_model_arguments, get_model, get_params from icefall.checkpoint import ( average_checkpoints, @@ -170,7 +177,26 @@ from icefall.checkpoint import ( load_checkpoint, ) from icefall.utils import make_pad_mask, str2bool -from scaling_converter import convert_scaled_to_non_scaled + + +def num_tokens( + token_table: k2.SymbolTable, disambig_pattern: str = re.compile(r"^#\d+$") +) -> int: + """Return the number of tokens excluding those from + disambiguation symbols. + + Caution: + 0 is not a token ID so it is excluded from the return value. + """ + symbols = token_table.symbols + ans = [] + for s in symbols: + if not disambig_pattern.match(s): + ans.append(token_table[s]) + num_tokens = len(ans) + if 0 in ans: + num_tokens -= 1 + return num_tokens def get_parser(): @@ -227,10 +253,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", ) parser.add_argument( @@ -238,7 +264,7 @@ def get_parser(): type=str2bool, default=False, help="""True to save a model after applying torch.jit.script. - It will generate a file named cpu_jit.pt. + It will generate a file named jit_script.pt. Check ./jit_pretrained.py for how to use it. """, ) @@ -257,6 +283,7 @@ def get_parser(): class EncoderModel(nn.Module): """A wrapper for encoder and encoder_embed""" + def __init__(self, encoder: nn.Module, encoder_embed: nn.Module) -> None: super().__init__() self.encoder = encoder @@ -275,9 +302,7 @@ class EncoderModel(nn.Module): src_key_padding_mask = make_pad_mask(x_lens) x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) - encoder_out, encoder_out_lens = self.encoder( - x, x_lens, src_key_padding_mask - ) + encoder_out, encoder_out_lens = self.encoder(x, x_lens, src_key_padding_mask) encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) return encoder_out, encoder_out_lens @@ -398,12 +423,9 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) - - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + token_table = k2.SymbolTable.from_file(params.tokens) + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 logging.info(params) diff --git a/egs/librispeech/ASR/zipformer/generate_averaged_model.py b/egs/librispeech/ASR/zipformer/generate_averaged_model.py index e0c7b52cb..68111fad7 100755 --- a/egs/librispeech/ASR/zipformer/generate_averaged_model.py +++ b/egs/librispeech/ASR/zipformer/generate_averaged_model.py @@ -40,16 +40,11 @@ You can later load it by `torch.load("iter-22000-avg-5.pt")`. import argparse from pathlib import Path -import sentencepiece as spm +import k2 import torch -from asr_datamodule import LibriSpeechAsrDataModule +from train import add_model_arguments, get_model, get_params -from train import add_model_arguments, get_params, get_model - -from icefall.checkpoint import ( - average_checkpoints_with_averaged_model, - find_checkpoints, -) +from icefall.checkpoint import average_checkpoints_with_averaged_model, find_checkpoints def get_parser(): @@ -93,10 +88,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", ) parser.add_argument( @@ -114,7 +109,6 @@ def get_parser(): @torch.no_grad() def main(): parser = get_parser() - LibriSpeechAsrDataModule.add_arguments(parser) args = parser.parse_args() args.exp_dir = Path(args.exp_dir) @@ -131,13 +125,10 @@ def main(): device = torch.device("cpu") print(f"Device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) - - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + symbol_table = k2.SymbolTable.from_file(params.tokens) + params.blank_id = symbol_table[""] + params.unk_id = symbol_table[""] + params.vocab_size = len(symbol_table) print("About to create model") model = get_model(params) diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained.py b/egs/librispeech/ASR/zipformer/jit_pretrained.py index 87cd5102c..a41fbc1c9 100755 --- a/egs/librispeech/ASR/zipformer/jit_pretrained.py +++ b/egs/librispeech/ASR/zipformer/jit_pretrained.py @@ -21,7 +21,7 @@ You can use the following command to get the exported models: ./zipformer/export.py \ --exp-dir ./zipformer/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -30,7 +30,7 @@ Usage of this script: ./zipformer/jit_pretrained.py \ --nn-model-filename ./zipformer/exp/cpu_jit.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ /path/to/foo.wav \ /path/to/bar.wav """ @@ -40,8 +40,8 @@ import logging import math from typing import List +import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from torch.nn.utils.rnn import pad_sequence @@ -60,9 +60,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -128,7 +128,7 @@ def greedy_search( ) device = encoder_out.device - blank_id = 0 # hard-code to 0 + blank_id = model.decoder.blank_id batch_size_list = packed_encoder_out.batch_sizes.tolist() N = encoder_out.size(0) @@ -215,9 +215,6 @@ def main(): model.to(device) - sp = spm.SentencePieceProcessor() - sp.load(args.bpe_model) - logging.info("Constructing Fbank computer") opts = kaldifeat.FbankOptions() opts.device = device @@ -256,10 +253,21 @@ def main(): encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) + s = "\n" + + token_table = k2.SymbolTable.from_file(args.tokens) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + for filename, hyp in zip(args.sound_files, hyps): - words = sp.decode(hyp) - s += f"{filename}:\n{words}\n\n" + words = token_ids_to_words(hyp) + s += f"{filename}:\n{words}\n" + logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py index 1ec390d5b..14faeedd1 100755 --- a/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py +++ b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py @@ -24,7 +24,7 @@ You can generate the checkpoint with the following command: ./zipformer/export.py \ --exp-dir ./zipformer/exp \ --use-ctc 1 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -35,7 +35,7 @@ You can generate the checkpoint with the following command: --exp-dir ./zipformer/exp \ --use-ctc 1 \ --causal 1 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -45,7 +45,7 @@ Usage of this script: (1) ctc-decoding ./zipformer/jit_pretrained_ctc.py \ --model-filename ./zipformer/exp/jit_script.pt \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method ctc-decoding \ --sample-rate 16000 \ /path/to/foo.wav \ @@ -91,10 +91,10 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from ctc_decode import get_decoding_params +from export import num_tokens from torch.nn.utils.rnn import pad_sequence from train import get_params @@ -136,9 +136,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model. + help="""Path to tokens.txt. Used only when method is ctc-decoding. """, ) @@ -149,8 +149,8 @@ def get_parser(): default="1best", help="""Decoding method. Possible values are: - (0) ctc-decoding - Use CTC decoding. It uses a sentence - piece model, i.e., lang_dir/bpe.model, to convert + (0) ctc-decoding - Use CTC decoding. It uses a token table, + i.e., lang_dir/token.txt, to convert word pieces to words. It needs neither a lexicon nor an n-gram LM. (1) 1best - Use the best path as decoding output. Only @@ -263,10 +263,8 @@ def main(): params.update(get_decoding_params()) params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) - - params.vocab_size = sp.get_piece_size() + token_table = k2.SymbolTable.from_file(params.tokens) + params.vocab_size = num_tokens(token_table) logging.info(f"{params}") @@ -340,8 +338,7 @@ def main(): lattice=lattice, use_double_scores=params.use_double_scores ) token_ids = get_texts(best_path) - hyps = sp.decode(token_ids) - hyps = [s.split() for s in hyps] + hyps = [[token_table[i] for i in ids] for ids in token_ids] elif params.method in [ "1best", "nbest-rescoring", @@ -415,6 +412,7 @@ def main(): s = "\n" for filename, hyp in zip(params.sound_files, hyps): words = " ".join(hyp) + words = words.replace("▁", " ").strip() s += f"{filename}:\n{words}\n\n" logging.info(s) diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py b/egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py index 58d736685..d4ceacefd 100755 --- a/egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py +++ b/egs/librispeech/ASR/zipformer/jit_pretrained_streaming.py @@ -25,7 +25,7 @@ You can use the following command to get the exported models: --causal 1 \ --chunk-size 16 \ --left-context-frames 128 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -34,7 +34,7 @@ Usage of this script: ./zipformer/jit_pretrained_streaming.py \ --nn-model-filename ./zipformer/exp-causal/jit_script_chunk_16_left_128.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ /path/to/foo.wav \ """ @@ -43,8 +43,8 @@ import logging import math from typing import List, Optional +import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from kaldifeat import FbankOptions, OnlineFbank, OnlineFeature @@ -60,13 +60,13 @@ def get_parser(): "--nn-model-filename", type=str, required=True, - help="Path to the torchscript model cpu_jit.pt", + help="Path to the torchscript model jit_script.pt", ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -120,8 +120,8 @@ def greedy_search( device: torch.device = torch.device("cpu"), ): assert encoder_out.ndim == 2 - context_size = 2 - blank_id = 0 + context_size = decoder.context_size + blank_id = decoder.blank_id if decoder_out is None: assert hyp is None, hyp @@ -190,8 +190,8 @@ def main(): decoder = model.decoder joiner = model.joiner - sp = spm.SentencePieceProcessor() - sp.load(args.bpe_model) + token_table = k2.SymbolTable.from_file(args.tokens) + context_size = decoder.context_size logging.info("Constructing Fbank computer") online_fbank = create_streaming_feature_extractor(args.sample_rate) @@ -250,9 +250,13 @@ def main(): decoder, joiner, encoder_out.squeeze(0), decoder_out, hyp, device=device ) - context_size = 2 + text = "" + for i in hyp[context_size:]: + text += token_table[i] + text = text.replace("▁", " ").strip() + logging.info(args.sound_file) - logging.info(sp.decode(hyp[context_size:])) + logging.info(text) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/zipformer/onnx_check.py b/egs/librispeech/ASR/zipformer/onnx_check.py new file mode 100755 index 000000000..b38b875d0 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/onnx_check.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +# +# Copyright 2022 Xiaomi Corporation (Author: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script checks that exported onnx models produce the same output +with the given torchscript model for the same input. + +We use the pre-trained model from +https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_bpe_500/tokens.txt" +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +2. Export the model via torchscript (torch.jit.script()) + +./zipformer/export.py \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp/ \ + --jit 1 + +It will generate the following file in $repo/exp: + - jit_script.pt + +3. Export the model to ONNX + +./zipformer/export-onnx.py \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp/ + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +4. Run this file + +./zipformer/onnx_check.py \ + --jit-filename $repo/exp/jit_script.pt \ + --onnx-encoder-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --onnx-decoder-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --onnx-joiner-filename $repo/exp/joiner-epoch-99-avg-1.onnx +""" + +import argparse +import logging + +import torch +from onnx_pretrained import OnnxModel + +from icefall import is_module_available + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--jit-filename", + required=True, + type=str, + help="Path to the torchscript model", + ) + + parser.add_argument( + "--onnx-encoder-filename", + required=True, + type=str, + help="Path to the onnx encoder model", + ) + + parser.add_argument( + "--onnx-decoder-filename", + required=True, + type=str, + help="Path to the onnx decoder model", + ) + + parser.add_argument( + "--onnx-joiner-filename", + required=True, + type=str, + help="Path to the onnx joiner model", + ) + + return parser + + +def test_encoder( + torch_model: torch.jit.ScriptModule, + onnx_model: OnnxModel, +): + C = 80 + for i in range(3): + N = torch.randint(low=1, high=20, size=(1,)).item() + T = torch.randint(low=30, high=50, size=(1,)).item() + logging.info(f"test_encoder: iter {i}, N={N}, T={T}") + + x = torch.rand(N, T, C) + x_lens = torch.randint(low=30, high=T + 1, size=(N,)) + x_lens[0] = T + + torch_encoder_out, torch_encoder_out_lens = torch_model.encoder(x, x_lens) + torch_encoder_out = torch_model.joiner.encoder_proj(torch_encoder_out) + + onnx_encoder_out, onnx_encoder_out_lens = onnx_model.run_encoder(x, x_lens) + + assert torch.allclose(torch_encoder_out, onnx_encoder_out, atol=1e-05), ( + (torch_encoder_out - onnx_encoder_out).abs().max() + ) + + +def test_decoder( + torch_model: torch.jit.ScriptModule, + onnx_model: OnnxModel, +): + context_size = onnx_model.context_size + vocab_size = onnx_model.vocab_size + for i in range(10): + N = torch.randint(1, 100, size=(1,)).item() + logging.info(f"test_decoder: iter {i}, N={N}") + x = torch.randint( + low=1, + high=vocab_size, + size=(N, context_size), + dtype=torch.int64, + ) + torch_decoder_out = torch_model.decoder(x, need_pad=torch.tensor([False])) + torch_decoder_out = torch_model.joiner.decoder_proj(torch_decoder_out) + torch_decoder_out = torch_decoder_out.squeeze(1) + + onnx_decoder_out = onnx_model.run_decoder(x) + assert torch.allclose(torch_decoder_out, onnx_decoder_out, atol=1e-4), ( + (torch_decoder_out - onnx_decoder_out).abs().max() + ) + + +def test_joiner( + torch_model: torch.jit.ScriptModule, + onnx_model: OnnxModel, +): + encoder_dim = torch_model.joiner.encoder_proj.weight.shape[1] + decoder_dim = torch_model.joiner.decoder_proj.weight.shape[1] + for i in range(10): + N = torch.randint(1, 100, size=(1,)).item() + logging.info(f"test_joiner: iter {i}, N={N}") + encoder_out = torch.rand(N, encoder_dim) + decoder_out = torch.rand(N, decoder_dim) + + projected_encoder_out = torch_model.joiner.encoder_proj(encoder_out) + projected_decoder_out = torch_model.joiner.decoder_proj(decoder_out) + + torch_joiner_out = torch_model.joiner(encoder_out, decoder_out) + onnx_joiner_out = onnx_model.run_joiner( + projected_encoder_out, projected_decoder_out + ) + + assert torch.allclose(torch_joiner_out, onnx_joiner_out, atol=1e-4), ( + (torch_joiner_out - onnx_joiner_out).abs().max() + ) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + logging.info(vars(args)) + + torch_model = torch.jit.load(args.jit_filename) + + onnx_model = OnnxModel( + encoder_model_filename=args.onnx_encoder_filename, + decoder_model_filename=args.onnx_decoder_filename, + joiner_model_filename=args.onnx_joiner_filename, + ) + + logging.info("Test encoder") + test_encoder(torch_model, onnx_model) + + logging.info("Test decoder") + test_decoder(torch_model, onnx_model) + + logging.info("Test joiner") + test_joiner(torch_model, onnx_model) + logging.info("Finished checking ONNX models") + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +# See https://github.com/pytorch/pytorch/issues/38342 +# and https://github.com/pytorch/pytorch/issues/33354 +# +# If we don't do this, the delay increases whenever there is +# a new request that changes the actual batch size. +# If you use `py-spy dump --pid --native`, you will +# see a lot of time is spent in re-compiling the torch script model. +torch._C._jit_set_profiling_executor(False) +torch._C._jit_set_profiling_mode(False) +torch._C._set_graph_executor_optimize(False) +if __name__ == "__main__": + torch.manual_seed(20220727) + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py index 273f883df..2ce4506a8 100755 --- a/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py @@ -524,11 +524,11 @@ def main(): hyp, ) - symbol_table = k2.SymbolTable.from_file(args.tokens) + token_table = k2.SymbolTable.from_file(args.tokens) text = "" for i in hyp[context_size:]: - text += symbol_table[i] + text += token_table[i] text = text.replace("▁", " ").strip() logging.info(args.sound_file) diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained.py b/egs/librispeech/ASR/zipformer/onnx_pretrained.py deleted file mode 120000 index 0069288fe..000000000 --- a/egs/librispeech/ASR/zipformer/onnx_pretrained.py +++ /dev/null @@ -1 +0,0 @@ -../pruned_transducer_stateless7/onnx_pretrained.py \ No newline at end of file diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained.py b/egs/librispeech/ASR/zipformer/onnx_pretrained.py new file mode 100755 index 000000000..b821c4e19 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads ONNX models and uses them to decode waves. +You can use the following command to get the exported models: + +We use the pre-trained model from +https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_bpe_500/tokens.txt" +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./zipformer/export-onnx.py \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --causal False + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +3. Run this file + +./pruned_transducer_stateless3/onnx_pretrained.py \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav +""" + +import argparse +import logging +import math +from typing import List, Tuple + +import k2 +import kaldifeat +import onnxruntime as ort +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--encoder-model-filename", + type=str, + required=True, + help="Path to the encoder onnx model. ", + ) + + parser.add_argument( + "--decoder-model-filename", + type=str, + required=True, + help="Path to the decoder onnx model. ", + ) + + parser.add_argument( + "--joiner-model-filename", + type=str, + required=True, + help="Path to the joiner onnx model. ", + ) + + parser.add_argument( + "--tokens", + type=str, + help="""Path to tokens.txt.""", + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + return parser + + +class OnnxModel: + def __init__( + self, + encoder_model_filename: str, + decoder_model_filename: str, + joiner_model_filename: str, + ): + session_opts = ort.SessionOptions() + session_opts.inter_op_num_threads = 1 + session_opts.intra_op_num_threads = 4 + + self.session_opts = session_opts + + self.init_encoder(encoder_model_filename) + self.init_decoder(decoder_model_filename) + self.init_joiner(joiner_model_filename) + + def init_encoder(self, encoder_model_filename: str): + self.encoder = ort.InferenceSession( + encoder_model_filename, + sess_options=self.session_opts, + ) + + def init_decoder(self, decoder_model_filename: str): + self.decoder = ort.InferenceSession( + decoder_model_filename, + sess_options=self.session_opts, + ) + + decoder_meta = self.decoder.get_modelmeta().custom_metadata_map + self.context_size = int(decoder_meta["context_size"]) + self.vocab_size = int(decoder_meta["vocab_size"]) + + logging.info(f"context_size: {self.context_size}") + logging.info(f"vocab_size: {self.vocab_size}") + + def init_joiner(self, joiner_model_filename: str): + self.joiner = ort.InferenceSession( + joiner_model_filename, + sess_options=self.session_opts, + ) + + joiner_meta = self.joiner.get_modelmeta().custom_metadata_map + self.joiner_dim = int(joiner_meta["joiner_dim"]) + + logging.info(f"joiner_dim: {self.joiner_dim}") + + def run_encoder( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + ) -> Tuple[torch.Tensor, torch.Tensor]: + """ + Args: + x: + A 3-D tensor of shape (N, T, C) + x_lens: + A 2-D tensor of shape (N,). Its dtype is torch.int64 + Returns: + Return a tuple containing: + - encoder_out, its shape is (N, T', joiner_dim) + - encoder_out_lens, its shape is (N,) + """ + out = self.encoder.run( + [ + self.encoder.get_outputs()[0].name, + self.encoder.get_outputs()[1].name, + ], + { + self.encoder.get_inputs()[0].name: x.numpy(), + self.encoder.get_inputs()[1].name: x_lens.numpy(), + }, + ) + return torch.from_numpy(out[0]), torch.from_numpy(out[1]) + + def run_decoder(self, decoder_input: torch.Tensor) -> torch.Tensor: + """ + Args: + decoder_input: + A 2-D tensor of shape (N, context_size) + Returns: + Return a 2-D tensor of shape (N, joiner_dim) + """ + out = self.decoder.run( + [self.decoder.get_outputs()[0].name], + {self.decoder.get_inputs()[0].name: decoder_input.numpy()}, + )[0] + + return torch.from_numpy(out) + + def run_joiner( + self, encoder_out: torch.Tensor, decoder_out: torch.Tensor + ) -> torch.Tensor: + """ + Args: + encoder_out: + A 2-D tensor of shape (N, joiner_dim) + decoder_out: + A 2-D tensor of shape (N, joiner_dim) + Returns: + Return a 2-D tensor of shape (N, vocab_size) + """ + out = self.joiner.run( + [self.joiner.get_outputs()[0].name], + { + self.joiner.get_inputs()[0].name: encoder_out.numpy(), + self.joiner.get_inputs()[1].name: decoder_out.numpy(), + }, + )[0] + + return torch.from_numpy(out) + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0]) + return ans + + +def greedy_search( + model: OnnxModel, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, +) -> List[List[int]]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The transducer model. + encoder_out: + A 3-D tensor of shape (N, T, joiner_dim) + encoder_out_lens: + A 1-D tensor of shape (N,). + Returns: + Return the decoded results for each utterance. + """ + assert encoder_out.ndim == 3, encoder_out.shape + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + blank_id = 0 # hard-code to 0 + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + context_size = model.context_size + hyps = [[blank_id] * context_size for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.run_decoder(decoder_input) + + offset = 0 + for batch_size in batch_size_list: + start = offset + end = offset + batch_size + current_encoder_out = packed_encoder_out.data[start:end] + # current_encoder_out's shape: (batch_size, joiner_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + logits = model.run_joiner(current_encoder_out, decoder_out) + + # logits'shape (batch_size, vocab_size) + + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v != blank_id: + hyps[i].append(v) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + dtype=torch.int64, + ) + decoder_out = model.run_decoder(decoder_input) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + model = OnnxModel( + encoder_model_filename=args.encoder_model_filename, + decoder_model_filename=args.decoder_model_filename, + joiner_model_filename=args.joiner_model_filename, + ) + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = "cpu" + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = args.sample_rate + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, + expected_sample_rate=args.sample_rate, + ) + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence( + features, + batch_first=True, + padding_value=math.log(1e-10), + ) + + feature_lengths = torch.tensor(feature_lengths, dtype=torch.int64) + encoder_out, encoder_out_lens = model.run_encoder(features, feature_lengths) + + hyps = greedy_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + s = "\n" + + token_table = k2.SymbolTable.from_file(args.tokens) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + + for filename, hyp in zip(args.sound_files, hyps): + words = token_ids_to_words(hyp) + s += f"{filename}:\n{words}\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/zipformer/pretrained.py b/egs/librispeech/ASR/zipformer/pretrained.py index 2944f79e3..3104b6084 100755 --- a/egs/librispeech/ASR/zipformer/pretrained.py +++ b/egs/librispeech/ASR/zipformer/pretrained.py @@ -18,11 +18,14 @@ This script loads a checkpoint and uses it to decode waves. You can generate the checkpoint with the following command: +Note: This is a example for librispeech dataset, if you are using different +dataset, you should change the argument values according to your dataset. + - For non-streaming model: ./zipformer/export.py \ --exp-dir ./zipformer/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 @@ -31,7 +34,7 @@ You can generate the checkpoint with the following command: ./zipformer/export.py \ --exp-dir ./zipformer/exp \ --causal 1 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 @@ -42,7 +45,7 @@ Usage of this script: (1) greedy search ./zipformer/pretrained.py \ --checkpoint ./zipformer/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -50,7 +53,7 @@ Usage of this script: (2) modified beam search ./zipformer/pretrained.py \ --checkpoint ./zipformer/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -58,7 +61,7 @@ Usage of this script: (3) fast beam search ./zipformer/pretrained.py \ --checkpoint ./zipformer/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -71,7 +74,7 @@ Usage of this script: --causal 1 \ --chunk-size 16 \ --left-context-frames 128 \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -82,7 +85,7 @@ Usage of this script: --causal 1 \ --chunk-size 16 \ --left-context-frames 128 \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -93,7 +96,7 @@ Usage of this script: --causal 1 \ --chunk-size 16 \ --left-context-frames 128 \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -112,7 +115,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -120,8 +122,11 @@ from beam_search import ( greedy_search_batch, modified_beam_search, ) +from export import num_tokens from torch.nn.utils.rnn import pad_sequence -from train import add_model_arguments, get_params, get_model +from train import add_model_arguments, get_model, get_params + +from icefall.utils import make_pad_mask def get_parser(): @@ -139,9 +144,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -258,13 +263,11 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 logging.info(f"{params}") @@ -323,6 +326,12 @@ def main(): msg = f"Using {params.method}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -334,8 +343,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -344,23 +353,22 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: raise ValueError(f"Unsupported method: {params.method}") s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/zipformer/pretrained_ctc.py b/egs/librispeech/ASR/zipformer/pretrained_ctc.py index f10d95449..be239e9c3 100755 --- a/egs/librispeech/ASR/zipformer/pretrained_ctc.py +++ b/egs/librispeech/ASR/zipformer/pretrained_ctc.py @@ -24,7 +24,7 @@ You can generate the checkpoint with the following command: ./zipformer/export.py \ --exp-dir ./zipformer/exp \ --use-ctc 1 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 @@ -34,7 +34,7 @@ You can generate the checkpoint with the following command: --exp-dir ./zipformer/exp \ --use-ctc 1 \ --causal 1 \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 @@ -43,7 +43,7 @@ Usage of this script: (1) ctc-decoding ./zipformer/pretrained_ctc.py \ --checkpoint ./zipformer/exp/pretrained.pt \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method ctc-decoding \ --sample-rate 16000 \ /path/to/foo.wav \ @@ -90,12 +90,12 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from ctc_decode import get_decoding_params +from export import num_tokens from torch.nn.utils.rnn import pad_sequence -from train import add_model_arguments, get_params, get_model +from train import add_model_arguments, get_model, get_params from icefall.decode import ( get_lattice, @@ -144,9 +144,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model. + help="""Path to tokens.txt. Used only when method is ctc-decoding. """, ) @@ -157,8 +157,8 @@ def get_parser(): default="1best", help="""Decoding method. Possible values are: - (0) ctc-decoding - Use CTC decoding. It uses a sentence - piece model, i.e., lang_dir/bpe.model, to convert + (0) ctc-decoding - Use CTC decoding. It uses a token table, + i.e., lang_dir/tokens.txt, to convert word pieces to words. It needs neither a lexicon nor an n-gram LM. (1) 1best - Use the best path as decoding output. Only @@ -273,11 +273,10 @@ def main(): params.update(get_decoding_params()) params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) - - params.vocab_size = sp.get_piece_size() - params.blank_id = 0 + token_table = k2.SymbolTable.from_file(params.tokens) + params.vocab_size = num_tokens(token_table) + params.blank_id = token_table[""] + assert params.blank_id == 0 logging.info(f"{params}") @@ -358,8 +357,7 @@ def main(): lattice=lattice, use_double_scores=params.use_double_scores ) token_ids = get_texts(best_path) - hyps = sp.decode(token_ids) - hyps = [s.split() for s in hyps] + hyps = [[token_table[i] for i in ids] for ids in token_ids] elif params.method in [ "1best", "nbest-rescoring", @@ -433,6 +431,7 @@ def main(): s = "\n" for filename, hyp in zip(params.sound_files, hyps): words = " ".join(hyp) + words = words.replace("▁", " ").strip() s += f"{filename}:\n{words}\n\n" logging.info(s) diff --git a/egs/librispeech/ASR/zipformer/streaming_beam_search.py b/egs/librispeech/ASR/zipformer/streaming_beam_search.py index e6e0fb1c8..3c8565b33 100644 --- a/egs/librispeech/ASR/zipformer/streaming_beam_search.py +++ b/egs/librispeech/ASR/zipformer/streaming_beam_search.py @@ -31,6 +31,7 @@ def greedy_search( model: nn.Module, encoder_out: torch.Tensor, streams: List[DecodeStream], + blank_penalty: float = 0.0, ) -> None: """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. @@ -71,6 +72,9 @@ def greedy_search( # logits'shape (batch_size, vocab_size) logits = logits.squeeze(1).squeeze(1) + if blank_penalty != 0.0: + logits[:, 0] -= blank_penalty + assert logits.ndim == 2, logits.shape y = logits.argmax(dim=1).tolist() emitted = False @@ -97,6 +101,7 @@ def modified_beam_search( encoder_out: torch.Tensor, streams: List[DecodeStream], num_active_paths: int = 4, + blank_penalty: float = 0.0, ) -> None: """Beam search in batch mode with --max-sym-per-frame=1 being hardcoded. @@ -158,6 +163,9 @@ def modified_beam_search( logits = logits.squeeze(1).squeeze(1) + if blank_penalty != 0.0: + logits[:, 0] -= blank_penalty + log_probs = logits.log_softmax(dim=-1) # (num_hyps, vocab_size) log_probs.add_(ys_log_probs) @@ -205,6 +213,7 @@ def fast_beam_search_one_best( beam: float, max_states: int, max_contexts: int, + blank_penalty: float = 0.0, ) -> None: """It limits the maximum number of symbols per frame to 1. @@ -269,6 +278,10 @@ def fast_beam_search_one_best( project_input=False, ) logits = logits.squeeze(1).squeeze(1) + + if blank_penalty != 0.0: + logits[:, 0] -= blank_penalty + log_probs = logits.log_softmax(dim=-1) decoding_streams.advance(log_probs) diff --git a/egs/wenetspeech/ASR/RESULTS.md b/egs/wenetspeech/ASR/RESULTS.md index 658ad4a9b..1a0e0681f 100644 --- a/egs/wenetspeech/ASR/RESULTS.md +++ b/egs/wenetspeech/ASR/RESULTS.md @@ -1,5 +1,90 @@ ## Results +### WenetSpeech char-based training results (Non-streaming and streaming) on zipformer model + +This is the [pull request](https://github.com/k2-fsa/icefall/pull/1130) in icefall. + +#### Non-streaming + +Best results (num of params : ~76M): + +Type | Greedy(dev & net & meeting) | Beam search(dev & net & meeting) |   +-- | -- | -- | -- +Non-streaming | 7.36 & 7.65 & 12.43 | 7.32 & 7.61 & 12.35 | --epoch=12 + +The training command: + +``` +./zipformer/train.py \ + --world-size 6 \ + --num-epochs 12 \ + --use-fp16 1 \ + --max-duration 450 \ + --training-subset L \ + --lr-epochs 1.5 \ + --context-size 2 \ + --exp-dir zipformer/exp_L_context_2 \ + --causal 0 \ + --num-workers 8 +``` + +Listed best results for each epoch below: + +Epoch | Greedy search(dev & net & meeting) | Modified beam search(dev & net & meeting) |   +-- | -- | -- | -- +4 | 7.83 & 8.86 &13.73 | 7.75 & 8.81 & 13.67 | avg=1;blank-penalty=2 +5 | 7.75 & 8.46 & 13.38 | 7.68 & 8.41 & 13.27 | avg=1;blank-penalty=2 +6 | 7.72 & 8.19 & 13.16 | 7.62 & 8.14 & 13.06 | avg=1;blank-penalty=2 +7 | 7.59 & 8.08 & 12.97 | 7.53 & 8.01 & 12.87 | avg=2;blank-penalty=2 +8 | 7.68 & 7.87 & 12.96 | 7.61 & 7.81 & 12.88 | avg=1;blank-penalty=2 +9 | 7.57 & 7.77 & 12.87 | 7.5 & 7.71 & 12.77 | avg=1;blank-penalty=2 +10 | 7.45 & 7.7 & 12.69 | 7.39 & 7.63 & 12.59 | avg=2;blank-penalty=2 +11 | 7.35 & 7.67 & 12.46 | 7.31 & 7.63 & 12.43 | avg=3;blank-penalty=2 +12 | 7.36 & 7.65 & 12.43 | 7.32 & 7.61 & 12.35 | avg=4;blank-penalty=2 + +The pre-trained model is available here : https://huggingface.co/pkufool/icefall-asr-zipformer-wenetspeech-20230615 + + +#### Streaming + +Best results (num of params : ~76M): + +Type | Greedy(dev & net & meeting) | Beam search(dev & net & meeting) |   +-- | -- | -- | -- +Streaming | 8.45 & 9.89 & 16.46 | 8.21 & 9.77 & 16.07 | --epoch=12; --chunk-size=16; --left-context-frames=256 +Streaming | 8.0 & 9.0 & 15.11 | 7.84 & 8.94 & 14.92 | --epoch=12; --chunk-size=32; --left-context-frames=256 + +The training command: + +``` +./zipformer/train.py \ + --world-size 8 \ + --num-epochs 12 \ + --use-fp16 1 \ + --max-duration 450 \ + --training-subset L \ + --lr-epochs 1.5 \ + --context-size 2 \ + --exp-dir zipformer/exp_L_causal_context_2 \ + --causal 1 \ + --num-workers 8 +``` + +Best results for each epoch (--chunk-size=16; --left-context-frames=128) + +Epoch | Greedy search(dev & net & meeting) | Modified beam search(dev & net & meeting) |   +-- | -- | -- | -- +6 | 9.14 & 10.75 & 18.15 | 8.79 & 10.54 & 17.64 | avg=1;blank-penalty=1.5 +7 | 9.11 & 10.61 & 17.86 | 8.8 & 10.42 & 17.29 | avg=1;blank-penalty=1.5 +8 | 8.89 & 10.32 & 17.44 | 8.59 & 10.09 & 16.9 | avg=1;blank-penalty=1.5 +9 | 8.86 & 10.11 & 17.35 | 8.55 & 9.87 & 16.76 | avg=1;blank-penalty=1.5 +10 | 8.66 & 10.0 & 16.94 | 8.39 & 9.83 & 16.47 | avg=2;blank-penalty=1.5 +11 | 8.58 & 9.92 & 16.67 | 8.32 & 9.77 & 16.27 | avg=3;blank-penalty=1.5 +12 | 8.45 & 9.89 & 16.46 | 8.21 & 9.77 & 16.07 | avg=4;blank-penalty=1.5 + +The pre-trained model is available here: https://huggingface.co/pkufool/icefall-asr-zipformer-streaming-wenetspeech-20230615 + + ### WenetSpeech char-based training results (offline and streaming) (Pruned Transducer 5) #### 2022-07-22 diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py index 7cb2e1048..746b212ff 100644 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py @@ -292,7 +292,7 @@ class WenetSpeechAsrDataModule: max_duration=self.args.max_duration, shuffle=self.args.shuffle, num_buckets=self.args.num_buckets, - buffer_size=30000, + buffer_size=300000, drop_last=True, ) else: diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py index dc431578c..36b8a4b67 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/decode.py @@ -588,7 +588,7 @@ def decode_dataset( results = defaultdict(list) for batch_idx, batch in enumerate(dl): texts = batch["supervisions"]["text"] - texts = [list(str(text)) for text in texts] + texts = [list("".join(text.split())) for text in texts] cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] hyps_dict = decode_one_batch( diff --git a/egs/wenetspeech/ASR/zipformer/__init__.py b/egs/wenetspeech/ASR/zipformer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/egs/wenetspeech/ASR/zipformer/asr_datamodule.py b/egs/wenetspeech/ASR/zipformer/asr_datamodule.py new file mode 120000 index 000000000..a074d6085 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/asr_datamodule.py @@ -0,0 +1 @@ +../pruned_transducer_stateless2/asr_datamodule.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/beam_search.py b/egs/wenetspeech/ASR/zipformer/beam_search.py new file mode 120000 index 000000000..8554e44cc --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/beam_search.py @@ -0,0 +1 @@ +../pruned_transducer_stateless2/beam_search.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/decode.py b/egs/wenetspeech/ASR/zipformer/decode.py new file mode 100755 index 000000000..0fbc8244b --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/decode.py @@ -0,0 +1,818 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2022 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao +# Mingshuang Luo) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./zipformer/decode.py \ + --epoch 35 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --lang-dir data/lang_char \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) modified beam search +./zipformer/decode.py \ + --epoch 35 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --lang-dir data/lang_char \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +(3) fast beam search (trivial_graph) +./zipformer/decode.py \ + --epoch 35 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --lang-dir data/lang_char \ + --max-duration 600 \ + --decoding-method fast_beam_search \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 + +(4) fast beam search (LG) +./zipformer/decode.py \ + --epoch 30 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --lang-dir data/lang_char \ + --max-duration 600 \ + --decoding-method fast_beam_search_LG \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 + +(5) fast beam search (nbest oracle WER) +./zipformer/decode.py \ + --epoch 35 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --lang-dir data/lang_char \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_oracle \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 +""" + + +import argparse +import logging +import math +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import torch +import torch.nn as nn +from asr_datamodule import WenetSpeechAsrDataModule +from beam_search import ( + beam_search, + fast_beam_search_nbest, + fast_beam_search_nbest_LG, + fast_beam_search_nbest_oracle, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from lhotse.cut import Cut +from train import add_model_arguments, get_model, get_params + +from icefall.char_graph_compiler import CharCtcTrainingGraphCompiler +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + make_pad_mask, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--lang-dir", + type=Path, + default="data/lang_char", + help="The lang dir containing word table and LG graph", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - modified_beam_search + - fast_beam_search + - fast_beam_search_LG + - fast_beam_search_nbest_oracle + If you use fast_beam_search_LG, you have to specify + `--lang-dir`, which should contain `LG.pt`. + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=20.0, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search, + fast_beam_search, fast_beam_search_LG, + and fast_beam_search_nbest_oracle + """, + ) + + parser.add_argument( + "--ngram-lm-scale", + type=float, + default=0.01, + help=""" + Used only when --decoding_method is fast_beam_search_LG. + It specifies the scale for n-gram LM scores. + """, + ) + + parser.add_argument( + "--ilme-scale", + type=float, + default=0.2, + help=""" + Used only when --decoding_method is fast_beam_search_LG. + It specifies the scale for the internal language model estimation. + """, + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=8, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search, fast_beam_search_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=64, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search, fast_beam_search_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + parser.add_argument( + "--num-paths", + type=int, + default=200, + help="""Number of paths for nbest decoding. + Used only when the decoding method is fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=0.5, + help="""Scale applied to lattice scores when computing nbest paths. + Used only when the decoding method is and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--blank-penalty", + type=float, + default=0.0, + help=""" + The penalty applied on blank symbol during decoding. + Note: It is a positive value that would be applied to logits like + this `logits[:, 0] -= blank_penalty` (suppose logits.shape is + [batch_size, vocab] and blank id is 0). + """, + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + lexicon: Lexicon, + graph_compiler: CharCtcTrainingGraphCompiler, + batch: dict, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or LG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + if params.causal: + # this seems to cause insertions at the end of the utterance if used with zipformer. + pad_len = 30 + feature_lens += pad_len + feature = torch.nn.functional.pad( + feature, + pad=(0, 0, 0, pad_len), + value=LOG_EPS, + ) + + x, x_lens = model.encoder_embed(feature, feature_lens) + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = model.encoder(x, x_lens, src_key_padding_mask) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + hyps = [] + + if params.decoding_method == "fast_beam_search": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + blank_penalty=params.blank_penalty, + ) + for i in range(encoder_out.size(0)): + hyps.append([lexicon.token_table[idx] for idx in hyp_tokens[i]]) + elif params.decoding_method == "fast_beam_search_LG": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + blank_penalty=params.blank_penalty, + ilme_scale=params.ilme_scale, + ) + for hyp in hyp_tokens: + sentence = "".join([lexicon.word_table[i] for i in hyp]) + hyps.append(list(sentence)) + elif params.decoding_method == "fast_beam_search_nbest_oracle": + hyp_tokens = fast_beam_search_nbest_oracle( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + ref_texts=graph_compiler.texts_to_ids(supervisions["text"]), + nbest_scale=params.nbest_scale, + blank_penalty=params.blank_penalty, + ) + for i in range(encoder_out.size(0)): + hyps.append([lexicon.token_table[idx] for idx in hyp_tokens[i]]) + elif params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + blank_penalty=params.blank_penalty, + ) + for i in range(encoder_out.size(0)): + hyps.append([lexicon.token_table[idx] for idx in hyp_tokens[i]]) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + blank_penalty=params.blank_penalty, + beam=params.beam_size, + ) + for i in range(encoder_out.size(0)): + hyps.append([lexicon.token_table[idx] for idx in hyp_tokens[i]]) + else: + batch_size = encoder_out.size(0) + + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i + 1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + blank_penalty=params.blank_penalty, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + blank_penalty=params.blank_penalty, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyps.append([lexicon.token_table[idx] for idx in hyp]) + + key = f"blank_penalty_{params.blank_penalty}" + if params.decoding_method == "greedy_search": + return {"greedy_search_" + key: hyps} + elif "fast_beam_search" in params.decoding_method: + key += f"_beam_{params.beam}_" + key += f"max_contexts_{params.max_contexts}_" + key += f"max_states_{params.max_states}" + if "nbest" in params.decoding_method: + key += f"_num_paths_{params.num_paths}_" + key += f"nbest_scale_{params.nbest_scale}" + if "LG" in params.decoding_method: + key += f"_ilme_scale_{params.ilme_scale}" + key += f"_ngram_lm_scale_{params.ngram_lm_scale}" + + return {key: hyps} + else: + return {f"beam_size_{params.beam_size}_" + key: hyps} + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + lexicon: Lexicon, + graph_compiler: CharCtcTrainingGraphCompiler, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or LG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + texts = [list("".join(text.split())) for text in texts] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + lexicon=lexicon, + graph_compiler=graph_compiler, + decoding_graph=decoding_graph, + batch=batch, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + this_batch.append((cut_id, ref_text, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[List[int], List[int]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + WenetSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "modified_beam_search", + "fast_beam_search", + "fast_beam_search_LG", + "fast_beam_search_nbest_oracle", + ) + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if params.causal: + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + params.suffix += f"-chunk-{params.chunk_size}" + params.suffix += f"-left-context-{params.left_context_frames}" + + if "fast_beam_search" in params.decoding_method: + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + if "nbest" in params.decoding_method: + params.suffix += f"-nbest-scale-{params.nbest_scale}" + params.suffix += f"-num-paths-{params.num_paths}" + if "LG" in params.decoding_method: + params.suffix += f"_ilme_scale_{params.ilme_scale}" + params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" + elif "beam_search" in params.decoding_method: + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + params.suffix += f"-blank-penalty-{params.blank_penalty}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = lexicon.token_table[""] + params.vocab_size = max(lexicon.tokens) + 1 + + graph_compiler = CharCtcTrainingGraphCompiler( + lexicon=lexicon, + device=device, + ) + + logging.info(params) + + logging.info("About to create model") + model = get_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + if "fast_beam_search" in params.decoding_method: + if "LG" in params.decoding_method: + lexicon = Lexicon(params.lang_dir) + lg_filename = params.lang_dir / "LG.pt" + logging.info(f"Loading {lg_filename}") + decoding_graph = k2.Fsa.from_dict( + torch.load(lg_filename, map_location=device) + ) + decoding_graph.scores *= params.ngram_lm_scale + else: + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + else: + decoding_graph = None + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + wenetspeech = WenetSpeechAsrDataModule(args) + + def remove_short_utt(c: Cut): + T = ((c.num_frames - 7) // 2 + 1) // 2 + if T <= 0: + logging.warning( + f"Exclude cut with ID {c.id} from decoding, num_frames : {c.num_frames}." + ) + return T > 0 + + dev_cuts = wenetspeech.valid_cuts() + dev_cuts = dev_cuts.filter(remove_short_utt) + dev_dl = wenetspeech.valid_dataloaders(dev_cuts) + + test_net_cuts = wenetspeech.test_net_cuts() + test_net_cuts = test_net_cuts.filter(remove_short_utt) + test_net_dl = wenetspeech.test_dataloaders(test_net_cuts) + + test_meeting_cuts = wenetspeech.test_meeting_cuts() + test_meeting_cuts = test_meeting_cuts.filter(remove_short_utt) + test_meeting_dl = wenetspeech.test_dataloaders(test_meeting_cuts) + + test_sets = ["DEV", "TEST_NET", "TEST_MEETING"] + test_dls = [dev_dl, test_net_dl, test_meeting_dl] + + for test_set, test_dl in zip(test_sets, test_dls): + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + lexicon=lexicon, + graph_compiler=graph_compiler, + decoding_graph=decoding_graph, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/wenetspeech/ASR/zipformer/decode_stream.py b/egs/wenetspeech/ASR/zipformer/decode_stream.py new file mode 120000 index 000000000..b8d8ddfc4 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/decode_stream.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/decode_stream.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/decoder.py b/egs/wenetspeech/ASR/zipformer/decoder.py new file mode 120000 index 000000000..5a8018680 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/decoder.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/decoder.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/encoder_interface.py b/egs/wenetspeech/ASR/zipformer/encoder_interface.py new file mode 120000 index 000000000..b9aa0ae08 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/encoder_interface.py @@ -0,0 +1 @@ +../pruned_transducer_stateless2/encoder_interface.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/export-onnx-streaming.py b/egs/wenetspeech/ASR/zipformer/export-onnx-streaming.py new file mode 120000 index 000000000..2962eb784 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/export-onnx-streaming.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/export-onnx-streaming.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/export-onnx.py b/egs/wenetspeech/ASR/zipformer/export-onnx.py new file mode 120000 index 000000000..70a15683c --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/export-onnx.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/export-onnx.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/export.py b/egs/wenetspeech/ASR/zipformer/export.py new file mode 120000 index 000000000..dfc1bec08 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/export.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/export.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/jit_pretrained.py b/egs/wenetspeech/ASR/zipformer/jit_pretrained.py new file mode 120000 index 000000000..25108391f --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/jit_pretrained.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/jit_pretrained.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/jit_pretrained_streaming.py b/egs/wenetspeech/ASR/zipformer/jit_pretrained_streaming.py new file mode 120000 index 000000000..1962351e9 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/jit_pretrained_streaming.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/jit_pretrained_streaming.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/joiner.py b/egs/wenetspeech/ASR/zipformer/joiner.py new file mode 120000 index 000000000..5b8a36332 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/joiner.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/joiner.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/model.py b/egs/wenetspeech/ASR/zipformer/model.py new file mode 120000 index 000000000..cd7e07d72 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/model.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/onnx_check.py b/egs/wenetspeech/ASR/zipformer/onnx_check.py new file mode 120000 index 000000000..f3dd42004 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/onnx_check.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/onnx_check.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/onnx_decode.py b/egs/wenetspeech/ASR/zipformer/onnx_decode.py new file mode 100755 index 000000000..ed5f6db08 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/onnx_decode.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao, +# Xiaoyu Yang, +# Wei Kang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads ONNX exported models and uses them to decode the test sets. + +We use the pre-trained model from +https://huggingface.co/pkufool/icefall-asr-zipformer-wenetspeech-20230615 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/wenetspeech/ASR + +repo_url=https://huggingface.co/pkufool/icefall-asr-zipformer-wenetspeech-20230615 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_char/tokens.txt" +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-9999.pt +popd + +2. Export the model to ONNX + +./zipformer/export-onnx.py \ + --tokens $repo/data/lang_char/tokens.txt \ + --epoch 9999 \ + --avg 1 \ + --exp-dir $repo/exp/ + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-9999-avg-1.onnx + - decoder-epoch-9999-avg-1.onnx + - joiner-epoch-9999-avg-1.onnx + +2. Run this file + +./zipformer/onnx_decode.py \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --encoder-model-filename $repo/exp/encoder-epoch-9999-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-9999-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-9999-avg-1.onnx \ +""" + + +import argparse +import logging +import time +from pathlib import Path +from typing import List, Tuple + +import k2 +import torch +import torch.nn as nn +from asr_datamodule import WenetSpeechAsrDataModule +from lhotse.cut import Cut +from onnx_pretrained import OnnxModel, greedy_search + +from icefall.utils import setup_logger, store_transcripts, write_error_stats + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--encoder-model-filename", + type=str, + required=True, + help="Path to the encoder onnx model. ", + ) + + parser.add_argument( + "--decoder-model-filename", + type=str, + required=True, + help="Path to the decoder onnx model. ", + ) + + parser.add_argument( + "--joiner-model-filename", + type=str, + required=True, + help="Path to the joiner onnx model. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="pruned_transducer_stateless7/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--tokens", + type=str, + default="data/lang_char/tokens.txt", + help="Path to the tokens.txt", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="Valid values are greedy_search and modified_beam_search", + ) + + return parser + + +def decode_one_batch( + model: OnnxModel, token_table: k2.SymbolTable, batch: dict +) -> List[List[str]]: + """Decode one batch and return the result. + Currently it only greedy_search is supported. + + Args: + model: + The neural model. + token_table: + Mapping ids to tokens. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + + Returns: + Return the decoded results for each utterance. + """ + feature = batch["inputs"] + assert feature.ndim == 3 + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(dtype=torch.int64) + + encoder_out, encoder_out_lens = model.run_encoder(x=feature, x_lens=feature_lens) + + hyps = greedy_search( + model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens + ) + + hyps = [[token_table[h] for h in hyp] for hyp in hyps] + return hyps + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + model: nn.Module, + token_table: k2.SymbolTable, +) -> Tuple[List[Tuple[str, List[str], List[str]]], float]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + model: + The neural model. + token_table: + Mapping ids to tokens. + + Returns: + - A list of tuples. Each tuple contains three elements: + - cut_id, + - reference transcript, + - predicted result. + - The total duration (in seconds) of the dataset. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + log_interval = 10 + total_duration = 0 + + results = [] + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + total_duration += sum([cut.duration for cut in batch["supervisions"]["cut"]]) + + hyps = decode_one_batch(model=model, token_table=token_table, batch=batch) + + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = list(ref_text) + this_batch.append((cut_id, ref_words, hyp_words)) + + results.extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + + return results, total_duration + + +def save_results( + res_dir: Path, + test_set_name: str, + results: List[Tuple[str, List[str], List[str]]], +): + recog_path = res_dir / f"recogs-{test_set_name}.txt" + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = res_dir / f"errs-{test_set_name}.txt" + with open(errs_filename, "w") as f: + wer = write_error_stats(f, f"{test_set_name}", results, enable_log=True) + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + errs_info = res_dir / f"wer-summary-{test_set_name}.txt" + with open(errs_info, "w") as f: + print("WER", file=f) + print(wer, file=f) + + s = "\nFor {}, WER is {}:\n".format(test_set_name, wer) + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + WenetSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + + assert ( + args.decoding_method == "greedy_search" + ), "Only supports greedy_search currently." + res_dir = Path(args.exp_dir) / f"onnx-{args.decoding_method}" + + setup_logger(f"{res_dir}/log-decode") + logging.info("Decoding started") + + device = torch.device("cpu") + logging.info(f"Device: {device}") + + token_table = k2.SymbolTable.from_file(args.tokens) + assert token_table[0] == "" + + logging.info(vars(args)) + + logging.info("About to create model") + model = OnnxModel( + encoder_model_filename=args.encoder_model_filename, + decoder_model_filename=args.decoder_model_filename, + joiner_model_filename=args.joiner_model_filename, + ) + + # we need cut ids to display recognition results. + args.return_cuts = True + + wenetspeech = WenetSpeechAsrDataModule(args) + + def remove_short_utt(c: Cut): + T = ((c.num_frames - 7) // 2 + 1) // 2 + if T <= 0: + logging.warning( + f"Exclude cut with ID {c.id} from decoding, num_frames : {c.num_frames}." + ) + return T > 0 + + dev_cuts = wenetspeech.valid_cuts() + dev_cuts = dev_cuts.filter(remove_short_utt) + dev_dl = wenetspeech.valid_dataloaders(dev_cuts) + + test_net_cuts = wenetspeech.test_net_cuts() + test_net_cuts = test_net_cuts.filter(remove_short_utt) + test_net_dl = wenetspeech.test_dataloaders(test_net_cuts) + + test_meeting_cuts = wenetspeech.test_meeting_cuts() + test_meeting_cuts = test_meeting_cuts.filter(remove_short_utt) + test_meeting_dl = wenetspeech.test_dataloaders(test_meeting_cuts) + + test_sets = ["DEV", "TEST_NET", "TEST_MEETING"] + test_dl = [dev_dl, test_net_dl, test_meeting_dl] + + for test_set, test_dl in zip(test_sets, test_dl): + start_time = time.time() + results, total_duration = decode_dataset( + dl=test_dl, model=model, token_table=token_table + ) + end_time = time.time() + elapsed_seconds = end_time - start_time + rtf = elapsed_seconds / total_duration + + logging.info(f"Elapsed time: {elapsed_seconds:.3f} s") + logging.info(f"Wave duration: {total_duration:.3f} s") + logging.info( + f"Real time factor (RTF): {elapsed_seconds:.3f}/{total_duration:.3f} = {rtf:.3f}" + ) + + save_results(res_dir=res_dir, test_set_name=test_set, results=results) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/wenetspeech/ASR/zipformer/onnx_pretrained-streaming.py b/egs/wenetspeech/ASR/zipformer/onnx_pretrained-streaming.py new file mode 120000 index 000000000..cfea104c2 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/onnx_pretrained-streaming.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/onnx_pretrained-streaming.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/onnx_pretrained.py b/egs/wenetspeech/ASR/zipformer/onnx_pretrained.py new file mode 120000 index 000000000..8f32f4ee7 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/onnx_pretrained.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/onnx_pretrained.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/optim.py b/egs/wenetspeech/ASR/zipformer/optim.py new file mode 120000 index 000000000..5eaa3cffd --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/optim.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/optim.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/pretrained.py b/egs/wenetspeech/ASR/zipformer/pretrained.py new file mode 120000 index 000000000..0bd71dde4 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/pretrained.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/pretrained.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/scaling.py b/egs/wenetspeech/ASR/zipformer/scaling.py new file mode 120000 index 000000000..6f398f431 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/scaling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/scaling.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/scaling_converter.py b/egs/wenetspeech/ASR/zipformer/scaling_converter.py new file mode 120000 index 000000000..b0ecee05e --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/scaling_converter.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/scaling_converter.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/streaming_beam_search.py b/egs/wenetspeech/ASR/zipformer/streaming_beam_search.py new file mode 120000 index 000000000..b1ed54557 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/streaming_beam_search.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/streaming_beam_search.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/streaming_decode.py b/egs/wenetspeech/ASR/zipformer/streaming_decode.py new file mode 100755 index 000000000..94c5fae5f --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/streaming_decode.py @@ -0,0 +1,881 @@ +#!/usr/bin/env python3 +# Copyright 2022-2023 Xiaomi Corporation (Authors: Wei Kang, +# Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + +""" +Usage: +./zipformer/streaming_decode.py \ + --epoch 28 \ + --avg 15 \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 256 \ + --exp-dir ./zipformer/exp \ + --decoding-method greedy_search \ + --num-decode-streams 2000 +""" + +import argparse +import logging +import math +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import numpy as np +import torch +from asr_datamodule import WenetSpeechAsrDataModule +from decode_stream import DecodeStream +from kaldifeat import Fbank, FbankOptions +from lhotse import CutSet +from streaming_beam_search import ( + fast_beam_search_one_best, + greedy_search, + modified_beam_search, +) +from torch import Tensor, nn +from torch.nn.utils.rnn import pad_sequence +from train import add_model_arguments, get_model, get_params + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + make_pad_mask, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=28, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="Path to the lang dir(containing lexicon, tokens, etc.)", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Supported decoding methods are: + greedy_search + modified_beam_search + fast_beam_search + """, + ) + + parser.add_argument( + "--num_active_paths", + type=int, + default=4, + help="""An interger indicating how many candidates we will keep for each + frame. Used only when --decoding-method is modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=4, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search""", + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=4, + help="""Used only when --decoding-method is + fast_beam_search""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=32, + help="""Used only when --decoding-method is + fast_beam_search""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--blank-penalty", + type=float, + default=0.0, + help=""" + The penalty applied on blank symbol during decoding. + Note: It is a positive value that would be applied to logits like + this `logits[:, 0] -= blank_penalty` (suppose logits.shape is + [batch_size, vocab] and blank id is 0). + """, + ) + + parser.add_argument( + "--num-decode-streams", + type=int, + default=2000, + help="The number of streams that can be decoded parallel.", + ) + + add_model_arguments(parser) + + return parser + + +def get_init_states( + model: nn.Module, + batch_size: int = 1, + device: torch.device = torch.device("cpu"), +) -> List[torch.Tensor]: + """ + Returns a list of cached tensors of all encoder layers. For layer-i, states[i*6:(i+1)*6] + is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + states[-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + states[-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + """ + states = model.encoder.get_init_states(batch_size, device) + + embed_states = model.encoder_embed.get_init_states(batch_size, device) + states.append(embed_states) + + processed_lens = torch.zeros(batch_size, dtype=torch.int32, device=device) + states.append(processed_lens) + + return states + + +def stack_states(state_list: List[List[torch.Tensor]]) -> List[torch.Tensor]: + """Stack list of zipformer states that correspond to separate utterances + into a single emformer state, so that it can be used as an input for + zipformer when those utterances are formed into a batch. + + Args: + state_list: + Each element in state_list corresponding to the internal state + of the zipformer model for a single utterance. For element-n, + state_list[n] is a list of cached tensors of all encoder layers. For layer-i, + state_list[n][i*6:(i+1)*6] is (cached_key, cached_nonlin_attn, cached_val1, + cached_val2, cached_conv1, cached_conv2). + state_list[n][-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + state_list[n][-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + + Note: + It is the inverse of :func:`unstack_states`. + """ + batch_size = len(state_list) + assert (len(state_list[0]) - 2) % 6 == 0, len(state_list[0]) + tot_num_layers = (len(state_list[0]) - 2) // 6 + + batch_states = [] + for layer in range(tot_num_layers): + layer_offset = layer * 6 + # cached_key: (left_context_len, batch_size, key_dim) + cached_key = torch.cat( + [state_list[i][layer_offset] for i in range(batch_size)], dim=1 + ) + # cached_nonlin_attn: (num_heads, batch_size, left_context_len, head_dim) + cached_nonlin_attn = torch.cat( + [state_list[i][layer_offset + 1] for i in range(batch_size)], dim=1 + ) + # cached_val1: (left_context_len, batch_size, value_dim) + cached_val1 = torch.cat( + [state_list[i][layer_offset + 2] for i in range(batch_size)], dim=1 + ) + # cached_val2: (left_context_len, batch_size, value_dim) + cached_val2 = torch.cat( + [state_list[i][layer_offset + 3] for i in range(batch_size)], dim=1 + ) + # cached_conv1: (#batch, channels, left_pad) + cached_conv1 = torch.cat( + [state_list[i][layer_offset + 4] for i in range(batch_size)], dim=0 + ) + # cached_conv2: (#batch, channels, left_pad) + cached_conv2 = torch.cat( + [state_list[i][layer_offset + 5] for i in range(batch_size)], dim=0 + ) + batch_states += [ + cached_key, + cached_nonlin_attn, + cached_val1, + cached_val2, + cached_conv1, + cached_conv2, + ] + + cached_embed_left_pad = torch.cat( + [state_list[i][-2] for i in range(batch_size)], dim=0 + ) + batch_states.append(cached_embed_left_pad) + + processed_lens = torch.cat([state_list[i][-1] for i in range(batch_size)], dim=0) + batch_states.append(processed_lens) + + return batch_states + + +def unstack_states(batch_states: List[Tensor]) -> List[List[Tensor]]: + """Unstack the zipformer state corresponding to a batch of utterances + into a list of states, where the i-th entry is the state from the i-th + utterance in the batch. + + Note: + It is the inverse of :func:`stack_states`. + + Args: + batch_states: A list of cached tensors of all encoder layers. For layer-i, + states[i*6:(i+1)*6] is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, + cached_conv1, cached_conv2). + state_list[-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + states[-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + + Returns: + state_list: A list of list. Each element in state_list corresponding to the internal state + of the zipformer model for a single utterance. + """ + assert (len(batch_states) - 2) % 6 == 0, len(batch_states) + tot_num_layers = (len(batch_states) - 2) // 6 + + processed_lens = batch_states[-1] + batch_size = processed_lens.shape[0] + + state_list = [[] for _ in range(batch_size)] + + for layer in range(tot_num_layers): + layer_offset = layer * 6 + # cached_key: (left_context_len, batch_size, key_dim) + cached_key_list = batch_states[layer_offset].chunk(chunks=batch_size, dim=1) + # cached_nonlin_attn: (num_heads, batch_size, left_context_len, head_dim) + cached_nonlin_attn_list = batch_states[layer_offset + 1].chunk( + chunks=batch_size, dim=1 + ) + # cached_val1: (left_context_len, batch_size, value_dim) + cached_val1_list = batch_states[layer_offset + 2].chunk( + chunks=batch_size, dim=1 + ) + # cached_val2: (left_context_len, batch_size, value_dim) + cached_val2_list = batch_states[layer_offset + 3].chunk( + chunks=batch_size, dim=1 + ) + # cached_conv1: (#batch, channels, left_pad) + cached_conv1_list = batch_states[layer_offset + 4].chunk( + chunks=batch_size, dim=0 + ) + # cached_conv2: (#batch, channels, left_pad) + cached_conv2_list = batch_states[layer_offset + 5].chunk( + chunks=batch_size, dim=0 + ) + for i in range(batch_size): + state_list[i] += [ + cached_key_list[i], + cached_nonlin_attn_list[i], + cached_val1_list[i], + cached_val2_list[i], + cached_conv1_list[i], + cached_conv2_list[i], + ] + + cached_embed_left_pad_list = batch_states[-2].chunk(chunks=batch_size, dim=0) + for i in range(batch_size): + state_list[i].append(cached_embed_left_pad_list[i]) + + processed_lens_list = batch_states[-1].chunk(chunks=batch_size, dim=0) + for i in range(batch_size): + state_list[i].append(processed_lens_list[i]) + + return state_list + + +def streaming_forward( + features: Tensor, + feature_lens: Tensor, + model: nn.Module, + states: List[Tensor], + chunk_size: int, + left_context_len: int, +) -> Tuple[Tensor, Tensor, List[Tensor]]: + """ + Returns encoder outputs, output lengths, and updated states. + """ + cached_embed_left_pad = states[-2] + (x, x_lens, new_cached_embed_left_pad,) = model.encoder_embed.streaming_forward( + x=features, + x_lens=feature_lens, + cached_left_pad=cached_embed_left_pad, + ) + assert x.size(1) == chunk_size, (x.size(1), chunk_size) + + src_key_padding_mask = make_pad_mask(x_lens) + + # processed_mask is used to mask out initial states + processed_mask = torch.arange(left_context_len, device=x.device).expand( + x.size(0), left_context_len + ) + processed_lens = states[-1] # (batch,) + # (batch, left_context_size) + processed_mask = (processed_lens.unsqueeze(1) <= processed_mask).flip(1) + # Update processed lengths + new_processed_lens = processed_lens + x_lens + + # (batch, left_context_size + chunk_size) + src_key_padding_mask = torch.cat([processed_mask, src_key_padding_mask], dim=1) + + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + encoder_states = states[:-2] + ( + encoder_out, + encoder_out_lens, + new_encoder_states, + ) = model.encoder.streaming_forward( + x=x, + x_lens=x_lens, + states=encoder_states, + src_key_padding_mask=src_key_padding_mask, + ) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + new_states = new_encoder_states + [ + new_cached_embed_left_pad, + new_processed_lens, + ] + return encoder_out, encoder_out_lens, new_states + + +def decode_one_chunk( + params: AttributeDict, + model: nn.Module, + decode_streams: List[DecodeStream], +) -> List[int]: + """Decode one chunk frames of features for each decode_streams and + return the indexes of finished streams in a List. + + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + decode_streams: + A List of DecodeStream, each belonging to a utterance. + Returns: + Return a List containing which DecodeStreams are finished. + """ + device = model.device + chunk_size = int(params.chunk_size) + left_context_len = int(params.left_context_frames) + + features = [] + feature_lens = [] + states = [] + processed_lens = [] # Used in fast-beam-search + + for stream in decode_streams: + feat, feat_len = stream.get_feature_frames(chunk_size * 2) + features.append(feat) + feature_lens.append(feat_len) + states.append(stream.states) + processed_lens.append(stream.done_frames) + + feature_lens = torch.tensor(feature_lens, device=device) + features = pad_sequence(features, batch_first=True, padding_value=LOG_EPS) + + # Make sure the length after encoder_embed is at least 1. + # The encoder_embed subsample features (T - 7) // 2 + # The ConvNeXt module needs (7 - 1) // 2 = 3 frames of right padding after subsampling + tail_length = chunk_size * 2 + 7 + 2 * 3 + if features.size(1) < tail_length: + pad_length = tail_length - features.size(1) + feature_lens += pad_length + features = torch.nn.functional.pad( + features, + (0, 0, 0, pad_length), + mode="constant", + value=LOG_EPS, + ) + + states = stack_states(states) + + encoder_out, encoder_out_lens, new_states = streaming_forward( + features=features, + feature_lens=feature_lens, + model=model, + states=states, + chunk_size=chunk_size, + left_context_len=left_context_len, + ) + + encoder_out = model.joiner.encoder_proj(encoder_out) + + if params.decoding_method == "greedy_search": + greedy_search( + model=model, + encoder_out=encoder_out, + streams=decode_streams, + blank_penalty=params.blank_penalty, + ) + elif params.decoding_method == "fast_beam_search": + processed_lens = torch.tensor(processed_lens, device=device) + processed_lens = processed_lens + encoder_out_lens + fast_beam_search_one_best( + model=model, + encoder_out=encoder_out, + processed_lens=processed_lens, + streams=decode_streams, + beam=params.beam, + max_states=params.max_states, + max_contexts=params.max_contexts, + blank_penalty=params.blank_penalty, + ) + elif params.decoding_method == "modified_beam_search": + modified_beam_search( + model=model, + streams=decode_streams, + encoder_out=encoder_out, + num_active_paths=params.num_active_paths, + blank_penalty=params.blank_penalty, + ) + else: + raise ValueError(f"Unsupported decoding method: {params.decoding_method}") + + states = unstack_states(new_states) + + finished_streams = [] + for i in range(len(decode_streams)): + decode_streams[i].states = states[i] + decode_streams[i].done_frames += encoder_out_lens[i] + if decode_streams[i].done: + finished_streams.append(i) + + return finished_streams + + +def decode_dataset( + cuts: CutSet, + params: AttributeDict, + model: nn.Module, + lexicon: Lexicon, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[List[str], List[str]]]]: + """Decode dataset. + + Args: + cuts: + Lhotse Cutset containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + lexicon: + The Lexicon. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + device = model.device + + opts = FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = 16000 + opts.mel_opts.num_bins = 80 + + log_interval = 100 + + decode_results = [] + # Contain decode streams currently running. + decode_streams = [] + for num, cut in enumerate(cuts): + # each utterance has a DecodeStream. + initial_states = get_init_states(model=model, batch_size=1, device=device) + decode_stream = DecodeStream( + params=params, + cut_id=cut.id, + initial_states=initial_states, + decoding_graph=decoding_graph, + device=device, + ) + + audio: np.ndarray = cut.load_audio() + # audio.shape: (1, num_samples) + assert len(audio.shape) == 2 + assert audio.shape[0] == 1, "Should be single channel" + assert audio.dtype == np.float32, audio.dtype + + # The trained model is using normalized samples + if audio.max() > 1: + logging.warning( + f"The audio should be normalized to [-1, 1], audio.max : {audio.max()}." + f"Clipping to [-1, 1]." + ) + audio = np.clip(audio, -1, 1) + + samples = torch.from_numpy(audio).squeeze(0) + + fbank = Fbank(opts) + feature = fbank(samples.to(device)) + decode_stream.set_features(feature, tail_pad_len=30) + decode_stream.ground_truth = cut.supervisions[0].text + + decode_streams.append(decode_stream) + + while len(decode_streams) >= params.num_decode_streams: + finished_streams = decode_one_chunk( + params=params, model=model, decode_streams=decode_streams + ) + for i in sorted(finished_streams, reverse=True): + decode_results.append( + ( + decode_streams[i].id, + list(decode_streams[i].ground_truth.strip()), + [ + lexicon.token_table[idx] + for idx in decode_streams[i].decoding_result() + ], + ) + ) + del decode_streams[i] + + if num % log_interval == 0: + logging.info(f"Cuts processed until now is {num}.") + + # decode final chunks of last sequences + while len(decode_streams): + finished_streams = decode_one_chunk( + params=params, model=model, decode_streams=decode_streams + ) + for i in sorted(finished_streams, reverse=True): + decode_results.append( + ( + decode_streams[i].id, + decode_streams[i].ground_truth.split(), + [ + lexicon.token_table[idx] + for idx in decode_streams[i].decoding_result() + ], + ) + ) + del decode_streams[i] + + key = f"blank_penalty_{params.blank_penalty}" + if params.decoding_method == "greedy_search": + key = f"greedy_search_{key}" + elif params.decoding_method == "fast_beam_search": + key = ( + f"beam_{params.beam}_" + f"max_contexts_{params.max_contexts}_" + f"max_states_{params.max_states}_{key}" + ) + elif params.decoding_method == "modified_beam_search": + key = f"num_active_paths_{params.num_active_paths}_{key}" + else: + raise ValueError(f"Unsupported decoding method: {params.decoding_method}") + return {key: decode_results} + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + WenetSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + params.res_dir = params.exp_dir / "streaming" / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + assert params.causal, params.causal + assert "," not in params.chunk_size, "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + params.suffix += f"-chunk-{params.chunk_size}" + params.suffix += f"-left-context-{params.left_context_frames}" + params.suffix += f"-blank-penalty-{params.blank_penalty}" + + # for fast_beam_search + if params.decoding_method == "fast_beam_search": + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + lexicon = Lexicon(params.lang_dir) + params.blank_id = lexicon.token_table[""] + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if start >= 0: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + model.device = device + + decoding_graph = None + if params.decoding_method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + wenetspeech = WenetSpeechAsrDataModule(args) + + dev_cuts = wenetspeech.valid_cuts() + test_net_cuts = wenetspeech.test_net_cuts() + test_meeting_cuts = wenetspeech.test_meeting_cuts() + + test_sets = ["DEV", "TEST_NET", "TEST_MEETING"] + test_cuts = [dev_cuts, test_net_cuts, test_meeting_cuts] + + for test_set, test_cut in zip(test_sets, test_cuts): + results_dict = decode_dataset( + cuts=test_cut, + params=params, + model=model, + lexicon=lexicon, + decoding_graph=decoding_graph, + ) + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/wenetspeech/ASR/zipformer/subsampling.py b/egs/wenetspeech/ASR/zipformer/subsampling.py new file mode 120000 index 000000000..01ae9002c --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/subsampling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/subsampling.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/zipformer/train.py b/egs/wenetspeech/ASR/zipformer/train.py new file mode 100755 index 000000000..83dbfa22f --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/train.py @@ -0,0 +1,1350 @@ +#!/usr/bin/env python3 +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo, +# Zengwei Yao, +# Daniel Povey) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3,4,5,6,7" + +./zipformer/train.py \ + --world-size 8 \ + --num-epochs 12 \ + --start-epoch 1 \ + --exp-dir zipformer/exp \ + --training-subset L + --lr-epochs 1.5 \ + --max-duration 350 + +# For mix precision training: + +./zipformer/train.py \ + --world-size 8 \ + --num-epochs 12 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --training-subset L \ + --lr-epochs 1.5 \ + --max-duration 750 + +""" + + +import argparse +import copy +import logging +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import WenetSpeechAsrDataModule +from decoder import Decoder +from joiner import Joiner +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from model import AsrModel +from optim import Eden, ScaledAdam +from scaling import ScheduledFloat +from subsampling import Conv2dSubsampling +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer2 + +from icefall import diagnostics +from icefall.char_graph_compiler import CharCtcTrainingGraphCompiler +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.hooks import register_inf_check_hooks +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + MetricsTracker, + get_parameter_groups_with_lrs, + setup_logger, + str2bool, +) + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def get_adjusted_batch_count(params: AttributeDict) -> float: + # returns the number of batches we would have used so far if we had used the reference + # duration. This is for purposes of set_batch_count(). + return ( + params.batch_idx_train + * (params.max_duration * params.world_size) + / params.ref_duration + ) + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for name, module in model.named_modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + if hasattr(module, "name"): + module.name = name + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,3,4,3,2", + help="Number of zipformer encoder layers per stack, comma separated.", + ) + + parser.add_argument( + "--downsampling-factor", + type=str, + default="1,2,4,8,4,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--feedforward-dim", + type=str, + default="512,768,1024,1536,1024,768", + help="""Feedforward dimension of the zipformer encoder layers, per stack, comma separated.""", + ) + + parser.add_argument( + "--num-heads", + type=str, + default="4,4,4,8,4,4", + help="""Number of attention heads in the zipformer encoder layers: a single int or comma-separated list.""", + ) + + parser.add_argument( + "--encoder-dim", + type=str, + default="192,256,384,512,384,256", + help="""Embedding dimension in encoder stacks: a single int or comma-separated list.""", + ) + + parser.add_argument( + "--query-head-dim", + type=str, + default="32", + help="""Query/key dimension per head in encoder stacks: a single int or comma-separated list.""", + ) + + parser.add_argument( + "--value-head-dim", + type=str, + default="12", + help="""Value dimension per head in encoder stacks: a single int or comma-separated list.""", + ) + + parser.add_argument( + "--pos-head-dim", + type=str, + default="4", + help="""Positional-encoding dimension per head in encoder stacks: a single int or comma-separated list.""", + ) + + parser.add_argument( + "--pos-dim", + type=int, + default="48", + help="Positional-encoding embedding dimension", + ) + + parser.add_argument( + "--encoder-unmasked-dim", + type=str, + default="192,192,256,256,256,192", + help="""Unmasked dimensions in the encoders, relates to augmentation during training. A single int or comma-separated list. Must be <= each corresponding encoder_dim.""", + ) + + parser.add_argument( + "--cnn-module-kernel", + type=str, + default="31,31,15,15,15,31", + help="""Sizes of convolutional kernels in convolution modules in each encoder stack: a single int or comma-separated list.""", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--causal", + type=str2bool, + default=False, + help="If True, use causal version of model.", + ) + + parser.add_argument( + "--chunk-size", + type=str, + default="16,32,64,-1", + help="""Chunk sizes (at 50Hz frame rate) will be chosen randomly from this list during training. Must be just -1 if --causal=False""", + ) + + parser.add_argument( + "--left-context-frames", + type=str, + default="64,128,256,-1", + help="""Maximum left-contexts for causal training, measured in frames which will + be converted to a number of chunks. If splitting into chunks, + chunk left-context frames will be chosen randomly from this list; else not relevant.""", + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_char", + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "--base-lr", type=float, default=0.045, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=7500, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=3.5, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--ref-duration", + type=float, + default=600, + help="""Reference batch duration for purposes of adjusting batch counts for setting various schedules inside the model""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="""The context size in the decoder. 1 means bigram; 2 means tri-gram""", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="""The prune range for rnnt loss, it means how many symbols(context) + we are using to compute the loss""", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="""The scale to smooth the loss with lm + (output of prediction network) part.""", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="""The scale to smooth the loss with am (output of encoder network) part.""", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="""To get pruning ranges, we will calculate a simple version + loss(joiner is just addition), this simple loss also uses for + training (as a regularization item). We will scale the simple loss + with this parameter before adding to the final loss.""", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--inf-check", + type=str2bool, + default=False, + help="Add hooks to check for infinite module outputs and gradients.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=4000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=30, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=200, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warmup period that dictates the decay of the + scale on "simple" (un-pruned) loss. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 3000, + # parameters for zipformer + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed. + "warm_step": 2000, + "env_info": get_env_info(), + } + ) + + return params + + +def _to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + +def get_encoder_embed(params: AttributeDict) -> nn.Module: + # encoder_embed converts the input of shape (N, T, num_features) + # to the shape (N, (T - 7) // 2, encoder_dims). + # That is, it does two things simultaneously: + # (1) subsampling: T -> (T - 7) // 2 + # (2) embedding: num_features -> encoder_dims + # In the normal configuration, we will downsample once more at the end + # by a factor of 2, and most of the encoder stacks will run at a lower + # sampling rate. + encoder_embed = Conv2dSubsampling( + in_channels=params.feature_dim, + out_channels=_to_int_tuple(params.encoder_dim)[0], + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + ) + return encoder_embed + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + encoder = Zipformer2( + output_downsampling_factor=2, + downsampling_factor=_to_int_tuple(params.downsampling_factor), + num_encoder_layers=_to_int_tuple(params.num_encoder_layers), + encoder_dim=_to_int_tuple(params.encoder_dim), + encoder_unmasked_dim=_to_int_tuple(params.encoder_unmasked_dim), + query_head_dim=_to_int_tuple(params.query_head_dim), + pos_head_dim=_to_int_tuple(params.pos_head_dim), + value_head_dim=_to_int_tuple(params.value_head_dim), + pos_dim=params.pos_dim, + num_heads=_to_int_tuple(params.num_heads), + feedforward_dim=_to_int_tuple(params.feedforward_dim), + cnn_module_kernel=_to_int_tuple(params.cnn_module_kernel), + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + warmup_batches=4000.0, + causal=params.causal, + chunk_size=_to_int_tuple(params.chunk_size), + left_context_frames=_to_int_tuple(params.left_context_frames), + ) + return encoder + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=max(_to_int_tuple(params.encoder_dim)), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_model(params: AttributeDict) -> nn.Module: + encoder_embed = get_encoder_embed(params) + encoder = get_encoder_model(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = AsrModel( + encoder_embed=encoder_embed, + encoder=encoder, + decoder=decoder, + joiner=joiner, + encoder_dim=int(max(params.encoder_dim.split(","))), + decoder_dim=params.decoder_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + if "cur_batch_idx" in saved_params: + params["cur_batch_idx"] = saved_params["cur_batch_idx"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + graph_compiler: CharCtcTrainingGraphCompiler, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute CTC loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Zipformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.warm_step + + texts = batch["supervisions"]["text"] + y = graph_compiler.texts_to_ids(texts) + y = k2.RaggedTensor(y).to(device) + + with torch.set_grad_enabled(is_training): + simple_loss, pruned_loss, _ = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + ) + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + + loss = simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + graph_compiler: CharCtcTrainingGraphCompiler, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + graph_compiler: CharCtcTrainingGraphCompiler, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + saved_bad_model = False + + def save_bad_model(suffix: str = ""): + save_checkpoint_impl( + filename=params.exp_dir / f"bad-model{suffix}-{rank}.pt", + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=0, + ) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx % 10 == 0: + set_batch_count(model, get_adjusted_batch_count(params)) + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + save_bad_model() + display_and_save_batch(batch, params=params, graph_compiler=graph_compiler) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + + if cur_grad_scale < 8.0 or (cur_grad_scale < 32.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + if not saved_bad_model: + save_bad_model(suffix="-first-warning") + saved_bad_model = True + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + save_bad_model() + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = max(scheduler.get_last_lr()) + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + lexicon = Lexicon(params.lang_dir) + graph_compiler = CharCtcTrainingGraphCompiler( + lexicon=lexicon, + device=device, + ) + + params.blank_id = lexicon.token_table[""] + params.vocab_size = max(lexicon.tokens) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model).to(torch.float64) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + optimizer = ScaledAdam( + get_parameter_groups_with_lrs(model, lr=params.base_lr, include_names=True), + lr=params.base_lr, # should have no effect + clipping_scale=2.0, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + opts = diagnostics.TensorDiagnosticOptions( + 2**22 + ) # allow 4 megabytes per sub-module + diagnostic = diagnostics.attach_diagnostics(model, opts) + + if params.inf_check: + register_inf_check_hooks(model) + + wenetspeech = WenetSpeechAsrDataModule(args) + + train_cuts = wenetspeech.train_cuts() + valid_cuts = wenetspeech.valid_cuts() + + def remove_short_and_long_utt(c: Cut): + # Keep only utterances with duration between 1 second and 15 seconds + # + # Caution: There is a reason to select 15.0 here. Please see + # ../local/display_manifest_statistics.py + # + # You should use ../local/display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + if c.duration < 1.0 or c.duration > 15.0: + # logging.warning( + # f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + # ) + return False + + # In pruned RNN-T, we require that T >= S + # where T is the number of feature frames after subsampling + # and S is the number of tokens in the utterance + + # In ./zipformer.py, the conv module uses the following expression + # for subsampling + T = ((c.num_frames - 7) // 2 + 1) // 2 + tokens = graph_compiler.texts_to_ids([c.supervisions[0].text])[0] + + if T < len(tokens): + logging.warning( + f"Exclude cut with ID {c.id} from training. " + f"Number of frames (before subsampling): {c.num_frames}. " + f"Number of frames (after subsampling): {T}. " + f"Text: {c.supervisions[0].text}. " + f"Tokens: {tokens}. " + f"Number of tokens: {len(tokens)}" + ) + return False + + return True + + train_cuts = train_cuts.filter(remove_short_and_long_utt) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = wenetspeech.train_dataloaders( + train_cuts, sampler_state_dict=sampler_state_dict + ) + + valid_dl = wenetspeech.valid_dataloaders(valid_cuts) + + if False and not params.print_diagnostics: + scan_pessimistic_batches_for_oom( + model=model, + train_dl=train_dl, + optimizer=optimizer, + graph_compiler=graph_compiler, + params=params, + ) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + graph_compiler=graph_compiler, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + graph_compiler: CharCtcTrainingGraphCompiler, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + graph_compiler: + The compiler to encode texts to ids. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + supervisions = batch["supervisions"] + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + texts = supervisions["text"] + y = graph_compiler.texts_to_ids(texts) + num_tokens = sum(len(i) for i in y) + logging.info(f"num tokens: {num_tokens}") + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + graph_compiler: CharCtcTrainingGraphCompiler, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + batch=batch, + is_training=True, + ) + loss.backward() + optimizer.zero_grad() + except Exception as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + display_and_save_batch(batch, params=params, graph_compiler=graph_compiler) + raise + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + + +def main(): + parser = get_parser() + WenetSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.lang_dir = Path(args.lang_dir) + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/wenetspeech/ASR/zipformer/zipformer.py b/egs/wenetspeech/ASR/zipformer/zipformer.py new file mode 120000 index 000000000..23011dda7 --- /dev/null +++ b/egs/wenetspeech/ASR/zipformer/zipformer.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/zipformer.py \ No newline at end of file From 968ebd236b4a03c95421d47dfb673aa718028080 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 27 Jun 2023 14:35:59 +0800 Subject: [PATCH 036/100] Fix ONNX export of the latest streaming zipformer model. (#1148) --- egs/librispeech/ASR/zipformer/export-onnx-streaming.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py index 80dc19b37..ff3e46433 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py +++ b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py @@ -86,7 +86,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import make_pad_mask, str2bool +from icefall.utils import str2bool def get_parser(): @@ -218,7 +218,7 @@ class OnnxEncoder(nn.Module): ) assert x.size(1) == self.chunk_size, (x.size(1), self.chunk_size) - src_key_padding_mask = make_pad_mask(x_lens) + src_key_padding_mask = torch.zeros(N, self.chunk_size, dtype=torch.bool) # processed_mask is used to mask out initial states processed_mask = torch.arange(left_context_len, device=x.device).expand( @@ -272,6 +272,7 @@ class OnnxEncoder(nn.Module): states = self.encoder.get_init_states(batch_size, device) embed_states = self.encoder_embed.get_init_states(batch_size, device) + states.append(embed_states) processed_lens = torch.zeros(batch_size, dtype=torch.int64, device=device) From 9c2172c1c42486c35cf98c8ee586347b57908925 Mon Sep 17 00:00:00 2001 From: Desh Raj Date: Wed, 28 Jun 2023 10:43:49 +0200 Subject: [PATCH 037/100] Zipformer for TedLium (#1125) * initial commit for zipformer tedlium * fix unk decoding * add pretrained model and logs * update for new AsrModel * add option for choosing rnnt type * add results with modified rnnt --- .../beam_search.py | 16 +- egs/tedlium3/ASR/RESULTS.md | 128 ++ egs/tedlium3/ASR/zipformer/__init__.py | 0 egs/tedlium3/ASR/zipformer/asr_datamodule.py | 1 + egs/tedlium3/ASR/zipformer/beam_search.py | 1 + egs/tedlium3/ASR/zipformer/decode.py | 833 +++++++++++ egs/tedlium3/ASR/zipformer/decoder.py | 1 + .../ASR/zipformer/encoder_interface.py | 1 + egs/tedlium3/ASR/zipformer/export.py | 1 + egs/tedlium3/ASR/zipformer/joiner.py | 1 + egs/tedlium3/ASR/zipformer/model.py | 223 +++ egs/tedlium3/ASR/zipformer/optim.py | 1 + egs/tedlium3/ASR/zipformer/pretrained.py | 1 + egs/tedlium3/ASR/zipformer/profile.py | 1 + egs/tedlium3/ASR/zipformer/scaling.py | 1 + .../ASR/zipformer/scaling_converter.py | 1 + egs/tedlium3/ASR/zipformer/subsampling.py | 1 + egs/tedlium3/ASR/zipformer/train.py | 1308 +++++++++++++++++ egs/tedlium3/ASR/zipformer/zipformer.py | 1 + 19 files changed, 2519 insertions(+), 2 deletions(-) create mode 100644 egs/tedlium3/ASR/zipformer/__init__.py create mode 120000 egs/tedlium3/ASR/zipformer/asr_datamodule.py create mode 120000 egs/tedlium3/ASR/zipformer/beam_search.py create mode 100755 egs/tedlium3/ASR/zipformer/decode.py create mode 120000 egs/tedlium3/ASR/zipformer/decoder.py create mode 120000 egs/tedlium3/ASR/zipformer/encoder_interface.py create mode 120000 egs/tedlium3/ASR/zipformer/export.py create mode 120000 egs/tedlium3/ASR/zipformer/joiner.py create mode 100644 egs/tedlium3/ASR/zipformer/model.py create mode 120000 egs/tedlium3/ASR/zipformer/optim.py create mode 120000 egs/tedlium3/ASR/zipformer/pretrained.py create mode 120000 egs/tedlium3/ASR/zipformer/profile.py create mode 120000 egs/tedlium3/ASR/zipformer/scaling.py create mode 120000 egs/tedlium3/ASR/zipformer/scaling_converter.py create mode 120000 egs/tedlium3/ASR/zipformer/subsampling.py create mode 100755 egs/tedlium3/ASR/zipformer/train.py create mode 120000 egs/tedlium3/ASR/zipformer/zipformer.py diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index 17b63a659..fd59d4b7f 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -50,6 +50,7 @@ def fast_beam_search_one_best( ilme_scale: float = 0.0, blank_penalty: float = 0.0, return_timestamps: bool = False, + allow_partial: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -91,6 +92,7 @@ def fast_beam_search_one_best( max_contexts=max_contexts, temperature=temperature, ilme_scale=ilme_scale, + allow_partial=allow_partial, blank_penalty=blank_penalty, ) @@ -117,6 +119,7 @@ def fast_beam_search_nbest_LG( blank_penalty: float = 0.0, ilme_scale: float = 0.0, return_timestamps: bool = False, + allow_partial: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -170,6 +173,7 @@ def fast_beam_search_nbest_LG( max_states=max_states, max_contexts=max_contexts, temperature=temperature, + allow_partial=allow_partial, blank_penalty=blank_penalty, ilme_scale=ilme_scale, ) @@ -246,6 +250,7 @@ def fast_beam_search_nbest( temperature: float = 1.0, blank_penalty: float = 0.0, return_timestamps: bool = False, + allow_partial: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -300,6 +305,7 @@ def fast_beam_search_nbest( max_contexts=max_contexts, blank_penalty=blank_penalty, temperature=temperature, + allow_partial=allow_partial, ) nbest = Nbest.from_lattice( @@ -339,6 +345,7 @@ def fast_beam_search_nbest_oracle( temperature: float = 1.0, blank_penalty: float = 0.0, return_timestamps: bool = False, + allow_partial: bool = False, ) -> Union[List[List[int]], DecodingResults]: """It limits the maximum number of symbols per frame to 1. @@ -396,6 +403,7 @@ def fast_beam_search_nbest_oracle( max_states=max_states, max_contexts=max_contexts, temperature=temperature, + allow_partial=allow_partial, blank_penalty=blank_penalty, ) @@ -440,7 +448,9 @@ def fast_beam_search( max_states: int, max_contexts: int, temperature: float = 1.0, - ilme_scale: float = 0.0, + subtract_ilme: bool = False, + ilme_scale: float = 0.1, + allow_partial: bool = False, blank_penalty: float = 0.0, ) -> k2.Fsa: """It limits the maximum number of symbols per frame to 1. @@ -533,7 +543,9 @@ def fast_beam_search( decoding_streams.advance(log_probs) decoding_streams.terminate_and_flush_to_streams() - lattice = decoding_streams.format_output(encoder_out_lens.tolist()) + lattice = decoding_streams.format_output( + encoder_out_lens.tolist(), allow_partial=allow_partial + ) return lattice diff --git a/egs/tedlium3/ASR/RESULTS.md b/egs/tedlium3/ASR/RESULTS.md index 38eaa8f44..cda77073d 100644 --- a/egs/tedlium3/ASR/RESULTS.md +++ b/egs/tedlium3/ASR/RESULTS.md @@ -1,5 +1,133 @@ ## Results +### TedLium3 BPE training results (Zipformer) + +#### 2023-06-15 + +Using the codes from this PR https://github.com/k2-fsa/icefall/pull/1125. + +Number of model parameters: 65549011, i.e., 65.5 M + +The WERs are + +| | dev | test | comment | +|------------------------------------|------------|------------|------------------------------------------| +| greedy search | 6.74 | 6.16 | --epoch 50, --avg 22, --max-duration 500 | +| beam search (beam size 4) | 6.56 | 5.95 | --epoch 50, --avg 22, --max-duration 500 | +| modified beam search (beam size 4) | 6.54 | 6.00 | --epoch 50, --avg 22, --max-duration 500 | +| fast beam search (set as default) | 6.91 | 6.28 | --epoch 50, --avg 22, --max-duration 500 | + +The training command for reproducing is given below: + +``` +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +./zipformer/train.py \ + --use-fp16 true \ + --world-size 4 \ + --num-epochs 50 \ + --start-epoch 0 \ + --exp-dir zipformer/exp \ + --max-duration 1000 +``` + +The tensorboard training log can be found at +https://tensorboard.dev/experiment/AKXbJha0S9aXyfmuvG4h5A/#scalars + +The decoding command is: +``` +epoch=50 +avg=22 + +## greedy search +./zipformer/decode.py \ + --epoch $epoch \ + --avg $avg \ + --exp-dir zipformer/exp \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --max-duration 500 + +## beam search +./zipformer/decode.py \ + --epoch $epoch \ + --avg $avg \ + --exp-dir zipformer/exp \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --max-duration 500 \ + --decoding-method beam_search \ + --beam-size 4 + +## modified beam search +./zipformer/decode.py \ + --epoch $epoch \ + --avg $avg \ + --exp-dir zipformer/exp \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --max-duration 500 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +## fast beam search +./zipformer/decode.py \ + --epoch $epoch \ + --avg $avg \ + --exp-dir ./zipformer/exp \ + --bpe-model ./data/lang_bpe_500/bpe.model \ + --max-duration 1500 \ + --decoding-method fast_beam_search \ + --beam 4 \ + --max-contexts 4 \ + --max-states 8 +``` + +A pre-trained model and decoding logs can be found at + +#### 2023-06-26 (transducer topology) + +**Modified transducer** + +``` +./zipformer/train.py \ + --use-fp16 true \ + --world-size 4 \ + --num-epochs 50 \ + --start-epoch 0 \ + --exp-dir zipformer/exp \ + --max-duration 1000 \ + --rnnt-type modified +``` + +| | dev | test | comment | +|------------------------------------|------------|------------|------------------------------------------| +| greedy search | 6.32 | 5.83 | --epoch 50, --avg 22, --max-duration 500 | +| beam search (beam size 4) | | | --epoch 50, --avg 22, --max-duration 500 | +| modified beam search (beam size 4) | 6.16 | 5.79 | --epoch 50, --avg 22, --max-duration 500 | +| fast beam search (set as default) | 6.30 ß | 5.89 | --epoch 50, --avg 22, --max-duration 500 | + +A pre-trained model and decoding logs can be found at . + +**Constrained transducer** + +``` +./zipformer/train.py \ + --use-fp16 true \ + --world-size 4 \ + --num-epochs 50 \ + --start-epoch 0 \ + --exp-dir zipformer/exp \ + --max-duration 1000 \ + --rnnt-type constrained +``` + +| | dev | test | comment | +|------------------------------------|------------|------------|------------------------------------------| +| greedy search | 6.58 | 6.20 | --epoch 50, --avg 22, --max-duration 500 | +| beam search (beam size 4) | 6.34 | 5.92 | --epoch 50, --avg 22, --max-duration 500 | +| modified beam search (beam size 4) | 6.38 | 5.84 | --epoch 50, --avg 22, --max-duration 500 | +| fast beam search (set as default) | 6.68 | 6.29 | --epoch 50, --avg 22, --max-duration 500 | + +A pre-trained model and decoding logs can be found at . + ### TedLium3 BPE training results (Conformer-CTC 2) #### [conformer_ctc2](./conformer_ctc2) diff --git a/egs/tedlium3/ASR/zipformer/__init__.py b/egs/tedlium3/ASR/zipformer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/egs/tedlium3/ASR/zipformer/asr_datamodule.py b/egs/tedlium3/ASR/zipformer/asr_datamodule.py new file mode 120000 index 000000000..49b2ee483 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/asr_datamodule.py @@ -0,0 +1 @@ +../transducer_stateless/asr_datamodule.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/beam_search.py b/egs/tedlium3/ASR/zipformer/beam_search.py new file mode 120000 index 000000000..e24eca39f --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/beam_search.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless2/beam_search.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/decode.py b/egs/tedlium3/ASR/zipformer/decode.py new file mode 100755 index 000000000..ea1cbba1b --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/decode.py @@ -0,0 +1,833 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) beam search (not recommended) +./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method beam_search \ + --beam-size 4 + +(3) modified beam search +./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +(4) fast beam search (one best) +./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 + +(5) fast beam search (nbest) +./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(6) fast beam search (nbest oracle WER) +./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_oracle \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(7) fast beam search (with LG) +./zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_LG \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 +""" + + +import argparse +import logging +import math +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import TedLiumAsrDataModule +from beam_search import ( + beam_search, + fast_beam_search_nbest, + fast_beam_search_nbest_LG, + fast_beam_search_nbest_oracle, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + make_pad_mask, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--lang-dir", + type=Path, + default="data/lang_bpe_500", + help="The lang dir containing word table and LG graph", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + - fast_beam_search + - fast_beam_search_nbest + - fast_beam_search_nbest_oracle + - fast_beam_search_nbest_LG + If you use fast_beam_search_nbest_LG, you have to specify + `--lang-dir`, which should contain `LG.pt`. + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=20.0, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search, + fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle + """, + ) + + parser.add_argument( + "--ngram-lm-scale", + type=float, + default=0.01, + help=""" + Used only when --decoding_method is fast_beam_search_nbest_LG. + It specifies the scale for n-gram LM scores. + """, + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=8, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=64, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; " "2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + parser.add_argument( + "--num-paths", + type=int, + default=200, + help="""Number of paths for nbest decoding. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=0.5, + help="""Scale applied to lattice scores when computing nbest paths. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + batch: dict, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + if params.causal: + # this seems to cause insertions at the end of the utterance if used with zipformer. + pad_len = 30 + feature_lens += pad_len + feature = torch.nn.functional.pad( + feature, + pad=(0, 0, 0, pad_len), + value=LOG_EPS, + ) + + x, x_lens = model.encoder_embed(feature, feature_lens) + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = model.encoder(x, x_lens, src_key_padding_mask) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + hyps = [] + unk = sp.decode(sp.unk_id()).strip() + + if params.decoding_method == "fast_beam_search": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + allow_partial=True, + ) + for hyp in sp.decode(hyp_tokens): + hyp = [w for w in hyp.split() if w != unk] + hyps.append(hyp) + elif params.decoding_method == "fast_beam_search_nbest_LG": + hyp_tokens = fast_beam_search_nbest_LG( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + nbest_scale=params.nbest_scale, + allow_partial=True, + ) + for hyp in hyp_tokens: + hyp = [word_table[i] for i in hyp if word_table[i] != unk] + hyps.append(hyp) + elif params.decoding_method == "fast_beam_search_nbest": + hyp_tokens = fast_beam_search_nbest( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + nbest_scale=params.nbest_scale, + allow_partial=True, + ) + for hyp in sp.decode(hyp_tokens): + hyp = [w for w in hyp.split() if w != unk] + hyps.append(hyp) + elif params.decoding_method == "fast_beam_search_nbest_oracle": + hyp_tokens = fast_beam_search_nbest_oracle( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + ref_texts=sp.encode(supervisions["text"]), + nbest_scale=params.nbest_scale, + allow_partial=True, + ) + for hyp in sp.decode(hyp_tokens): + hyp = [w for w in hyp.split() if w != unk] + hyps.append(hyp) + elif params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyp = [w for w in hyp.split() if w != unk] + hyps.append(hyp) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + for hyp in sp.decode(hyp_tokens): + hyp = [w for w in hyp.split() if w != unk] + hyps.append(hyp) + else: + batch_size = encoder_out.size(0) + + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyp = [w for w in sp.decode(hyp).split() if w != unk] + hyps.append(hyp) + + if params.decoding_method == "greedy_search": + return {"greedy_search": hyps} + elif "fast_beam_search" in params.decoding_method: + key = f"beam_{params.beam}_" + key += f"max_contexts_{params.max_contexts}_" + key += f"max_states_{params.max_states}" + if "nbest" in params.decoding_method: + key += f"_num_paths_{params.num_paths}_" + key += f"nbest_scale_{params.nbest_scale}" + if "LG" in params.decoding_method: + key += f"_ngram_lm_scale_{params.ngram_lm_scale}" + + return {key: hyps} + else: + return {f"beam_size_{params.beam_size}": hyps} + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + sp=sp, + decoding_graph=decoding_graph, + word_table=word_table, + batch=batch, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = ref_text.split() + this_batch.append((cut_id, ref_words, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + TedLiumAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "fast_beam_search", + "fast_beam_search_nbest", + "fast_beam_search_nbest_LG", + "fast_beam_search_nbest_oracle", + "modified_beam_search", + ) + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if params.causal: + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + params.suffix += f"-chunk-{params.chunk_size}" + params.suffix += f"-left-context-{params.left_context_frames}" + + if "fast_beam_search" in params.decoding_method: + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + if "nbest" in params.decoding_method: + params.suffix += f"-nbest-scale-{params.nbest_scale}" + params.suffix += f"-num-paths-{params.num_paths}" + if "LG" in params.decoding_method: + params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" + elif "beam_search" in params.decoding_method: + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # and are defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + if "fast_beam_search" in params.decoding_method: + if params.decoding_method == "fast_beam_search_nbest_LG": + lexicon = Lexicon(params.lang_dir) + word_table = lexicon.word_table + lg_filename = params.lang_dir / "LG.pt" + logging.info(f"Loading {lg_filename}") + decoding_graph = k2.Fsa.from_dict( + torch.load(lg_filename, map_location=device) + ) + decoding_graph.scores *= params.ngram_lm_scale + else: + word_table = None + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + else: + decoding_graph = None + word_table = None + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + tedlium = TedLiumAsrDataModule(args) + + dev_cuts = tedlium.dev_cuts() + test_cuts = tedlium.test_cuts() + + dev_dl = tedlium.test_dataloaders(dev_cuts) + test_dl = tedlium.test_dataloaders(test_cuts) + + test_sets = ["dev", "test"] + test_dls = [dev_dl, test_dl] + + for name, dl in zip(test_sets, test_dls): + results_dict = decode_dataset( + dl=dl, + params=params, + model=model, + sp=sp, + word_table=word_table, + decoding_graph=decoding_graph, + ) + + save_results( + params=params, + test_set_name=name, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/tedlium3/ASR/zipformer/decoder.py b/egs/tedlium3/ASR/zipformer/decoder.py new file mode 120000 index 000000000..5a8018680 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/decoder.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/decoder.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/encoder_interface.py b/egs/tedlium3/ASR/zipformer/encoder_interface.py new file mode 120000 index 000000000..653c5b09a --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/encoder_interface.py @@ -0,0 +1 @@ +../../../librispeech/ASR/transducer_stateless/encoder_interface.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/export.py b/egs/tedlium3/ASR/zipformer/export.py new file mode 120000 index 000000000..dfc1bec08 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/export.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/export.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/joiner.py b/egs/tedlium3/ASR/zipformer/joiner.py new file mode 120000 index 000000000..5b8a36332 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/joiner.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/joiner.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/model.py b/egs/tedlium3/ASR/zipformer/model.py new file mode 100644 index 000000000..90ec7e7aa --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/model.py @@ -0,0 +1,223 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, Wei Kang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 k2 +import torch +import torch.nn as nn +from encoder_interface import EncoderInterface + +from icefall.utils import add_sos, make_pad_mask +from scaling import ScaledLinear + + +class Transducer(nn.Module): + """It implements https://arxiv.org/pdf/1211.3711.pdf + "Sequence Transduction with Recurrent Neural Networks" + """ + + def __init__( + self, + encoder_embed: nn.Module, + encoder: EncoderInterface, + decoder: nn.Module, + joiner: nn.Module, + encoder_dim: int, + decoder_dim: int, + joiner_dim: int, + vocab_size: int, + ): + """ + Args: + encoder_embed: + It is a Convolutional 2D subsampling module. It converts + an input of shape (N, T, idim) to an output of of shape + (N, T', odim), where T' = (T-3)//2-2 = (T-7)//2. + encoder: + It is the transcription network in the paper. Its accepts + two inputs: `x` of (N, T, encoder_dim) and `x_lens` of shape (N,). + It returns two tensors: `logits` of shape (N, T, encoder_dim) and + `logit_lens` of shape (N,). + decoder: + It is the prediction network in the paper. Its input shape + is (N, U) and its output shape is (N, U, decoder_dim). + It should contain one attribute: `blank_id`. + joiner: + It has two inputs with shapes: (N, T, encoder_dim) and (N, U, decoder_dim). + Its output shape is (N, T, U, vocab_size). Note that its output contains + unnormalized probs, i.e., not processed by log-softmax. + """ + super().__init__() + assert isinstance(encoder, EncoderInterface), type(encoder) + assert hasattr(decoder, "blank_id") + + self.encoder_embed = encoder_embed + self.encoder = encoder + self.decoder = decoder + self.joiner = joiner + + self.simple_am_proj = ScaledLinear( + encoder_dim, + vocab_size, + initial_scale=0.25, + ) + self.simple_lm_proj = ScaledLinear( + decoder_dim, + vocab_size, + initial_scale=0.25, + ) + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + y: k2.RaggedTensor, + prune_range: int = 5, + am_scale: float = 0.0, + lm_scale: float = 0.0, + rnnt_type: str = "regular", + ) -> torch.Tensor: + """ + Args: + x: + A 3-D tensor of shape (N, T, C). + x_lens: + A 1-D tensor of shape (N,). It contains the number of frames in `x` + before padding. + y: + A ragged tensor with 2 axes [utt][label]. It contains labels of each + utterance. + prune_range: + The prune range for rnnt loss, it means how many symbols(context) + we are considering for each frame to compute the loss. + am_scale: + The scale to smooth the loss with am (output of encoder network) + part + lm_scale: + The scale to smooth the loss with lm (output of predictor network) + part + rnnt_type: + The type of label topology to use for the transducer loss. One of "regular", + "modified", or "constrained". + Returns: + Return the transducer loss. + + Note: + Regarding am_scale & lm_scale, it will make the loss-function one of + the form: + lm_scale * lm_probs + am_scale * am_probs + + (1-lm_scale-am_scale) * combined_probs + """ + assert x.ndim == 3, x.shape + assert x_lens.ndim == 1, x_lens.shape + assert y.num_axes == 2, y.num_axes + + assert x.size(0) == x_lens.size(0) == y.dim0 + + # logging.info(f"Memory allocated at entry: {torch.cuda.memory_allocated() // 1000000}M") + x, x_lens = self.encoder_embed(x, x_lens) + # logging.info(f"Memory allocated after encoder_embed: {torch.cuda.memory_allocated() // 1000000}M") + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, x_lens = self.encoder(x, x_lens, src_key_padding_mask) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + assert torch.all(x_lens > 0) + + # Now for the decoder, i.e., the prediction network + row_splits = y.shape.row_splits(1) + y_lens = row_splits[1:] - row_splits[:-1] + + blank_id = self.decoder.blank_id + sos_y = add_sos(y, sos_id=blank_id) + + # sos_y_padded: [B, S + 1], start with SOS. + sos_y_padded = sos_y.pad(mode="constant", padding_value=blank_id) + + # decoder_out: [B, S + 1, decoder_dim] + decoder_out = self.decoder(sos_y_padded) + + # Note: y does not start with SOS + # y_padded : [B, S] + y_padded = y.pad(mode="constant", padding_value=0) + + y_padded = y_padded.to(torch.int64) + boundary = torch.zeros( + (encoder_out.size(0), 4), + dtype=torch.int64, + device=encoder_out.device, + ) + boundary[:, 2] = y_lens + boundary[:, 3] = x_lens + + lm = self.simple_lm_proj(decoder_out) + am = self.simple_am_proj(encoder_out) + + # if self.training and random.random() < 0.25: + # lm = penalize_abs_values_gt(lm, 100.0, 1.0e-04) + # if self.training and random.random() < 0.25: + # am = penalize_abs_values_gt(am, 30.0, 1.0e-04) + + with torch.cuda.amp.autocast(enabled=False): + simple_loss, (px_grad, py_grad) = k2.rnnt_loss_smoothed( + lm=lm.float(), + am=am.float(), + symbols=y_padded, + termination_symbol=blank_id, + lm_only_scale=lm_scale, + am_only_scale=am_scale, + boundary=boundary, + reduction="sum", + return_grad=True, + rnnt_type=rnnt_type, + ) + + # ranges : [B, T, prune_range] + ranges = k2.get_rnnt_prune_ranges( + px_grad=px_grad, + py_grad=py_grad, + boundary=boundary, + s_range=prune_range, + ) + + # am_pruned : [B, T, prune_range, encoder_dim] + # lm_pruned : [B, T, prune_range, decoder_dim] + am_pruned, lm_pruned = k2.do_rnnt_pruning( + am=self.joiner.encoder_proj(encoder_out), + lm=self.joiner.decoder_proj(decoder_out), + ranges=ranges, + ) + + # logits : [B, T, prune_range, vocab_size] + + # project_input=False since we applied the decoder's input projections + # prior to do_rnnt_pruning (this is an optimization for speed). + logits = self.joiner(am_pruned, lm_pruned, project_input=False) + + with torch.cuda.amp.autocast(enabled=False): + pruned_loss = k2.rnnt_loss_pruned( + logits=logits.float(), + symbols=y_padded, + ranges=ranges, + termination_symbol=blank_id, + boundary=boundary, + reduction="sum", + rnnt_type=rnnt_type, + ) + + return (simple_loss, pruned_loss) diff --git a/egs/tedlium3/ASR/zipformer/optim.py b/egs/tedlium3/ASR/zipformer/optim.py new file mode 120000 index 000000000..5eaa3cffd --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/optim.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/optim.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/pretrained.py b/egs/tedlium3/ASR/zipformer/pretrained.py new file mode 120000 index 000000000..0bd71dde4 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/pretrained.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/pretrained.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/profile.py b/egs/tedlium3/ASR/zipformer/profile.py new file mode 120000 index 000000000..c93adbd14 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/profile.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/profile.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/scaling.py b/egs/tedlium3/ASR/zipformer/scaling.py new file mode 120000 index 000000000..6f398f431 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/scaling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/scaling.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/scaling_converter.py b/egs/tedlium3/ASR/zipformer/scaling_converter.py new file mode 120000 index 000000000..b0ecee05e --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/scaling_converter.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/scaling_converter.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/subsampling.py b/egs/tedlium3/ASR/zipformer/subsampling.py new file mode 120000 index 000000000..01ae9002c --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/subsampling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/subsampling.py \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/train.py b/egs/tedlium3/ASR/zipformer/train.py new file mode 100755 index 000000000..9271c8438 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/train.py @@ -0,0 +1,1308 @@ +#!/usr/bin/env python3 +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo, +# Zengwei Yao, +# Daniel Povey) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# For non-streaming model training: +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --full-libri 1 \ + --max-duration 1000 + +# For streaming model training: +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --causal 1 \ + --full-libri 1 \ + --max-duration 1000 + +""" + + +import argparse +import copy +import logging +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import TedLiumAsrDataModule +from decoder import Decoder +from joiner import Joiner +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from local.convert_transcript_words_to_bpe_ids import convert_texts_into_ids +from model import Transducer +from optim import Eden, ScaledAdam +from scaling import ScheduledFloat +from subsampling import Conv2dSubsampling +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer2 + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.hooks import register_inf_check_hooks +from icefall.utils import ( + AttributeDict, + MetricsTracker, + get_parameter_groups_with_lrs, + setup_logger, + str2bool, +) + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def get_adjusted_batch_count(params: AttributeDict) -> float: + # returns the number of batches we would have used so far if we had used the reference + # duration. This is for purposes of set_batch_count(). + return ( + params.batch_idx_train + * (params.max_duration * params.world_size) + / params.ref_duration + ) + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for name, module in model.named_modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + if hasattr(module, "name"): + module.name = name + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,3,4,3,2", + help="Number of zipformer encoder layers per stack, comma separated.", + ) + + parser.add_argument( + "--downsampling-factor", + type=str, + default="1,2,4,8,4,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--feedforward-dim", + type=str, + default="512,768,1024,1536,1024,768", + help="Feedforward dimension of the zipformer encoder layers, per stack, comma separated.", + ) + + parser.add_argument( + "--num-heads", + type=str, + default="4,4,4,8,4,4", + help="Number of attention heads in the zipformer encoder layers: a single int or comma-separated list.", + ) + + parser.add_argument( + "--encoder-dim", + type=str, + default="192,256,384,512,384,256", + help="Embedding dimension in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--query-head-dim", + type=str, + default="32", + help="Query/key dimension per head in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--value-head-dim", + type=str, + default="12", + help="Value dimension per head in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--pos-head-dim", + type=str, + default="4", + help="Positional-encoding dimension per head in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--pos-dim", + type=int, + default="48", + help="Positional-encoding embedding dimension", + ) + + parser.add_argument( + "--encoder-unmasked-dim", + type=str, + default="192,192,256,256,256,192", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "A single int or comma-separated list. Must be <= each corresponding encoder_dim.", + ) + + parser.add_argument( + "--cnn-module-kernel", + type=str, + default="31,31,15,15,15,31", + help="Sizes of convolutional kernels in convolution modules in each encoder stack: " + "a single int or comma-separated list.", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--causal", + type=str2bool, + default=False, + help="If True, use causal version of model.", + ) + + parser.add_argument( + "--chunk-size", + type=str, + default="16,32,64,-1", + help="Chunk sizes (at 50Hz frame rate) will be chosen randomly from this list during training. " + " Must be just -1 if --causal=False", + ) + + parser.add_argument( + "--left-context-frames", + type=str, + default="64,128,256,-1", + help="Maximum left-contexts for causal training, measured in frames which will " + "be converted to a number of chunks. If splitting into chunks, " + "chunk left-context frames will be chosen randomly from this list; else not relevant.", + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=50, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--base-lr", type=float, default=0.04, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=7500, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=5, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--ref-duration", + type=float, + default=600, + help="Reference batch duration for purposes of adjusting batch counts for setting various " + "schedules inside the model", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; " "2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--rnnt-type", + type=str, + default="regular", + choices=["regular", "modified", "constrained"], + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network)" "part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--inf-check", + type=str2bool, + default=False, + help="Add hooks to check for infinite module outputs and gradients.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=4000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 1. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=1, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=200, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warmup period that dictates the decay of the + scale on "simple" (un-pruned) loss. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 3000, # For the 100h subset, use 800 + # parameters for zipformer + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed. + "warm_step": 2000, + "env_info": get_env_info(), + } + ) + + return params + + +def _to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + +def get_encoder_embed(params: AttributeDict) -> nn.Module: + # encoder_embed converts the input of shape (N, T, num_features) + # to the shape (N, (T - 7) // 2, encoder_dims). + # That is, it does two things simultaneously: + # (1) subsampling: T -> (T - 7) // 2 + # (2) embedding: num_features -> encoder_dims + # In the normal configuration, we will downsample once more at the end + # by a factor of 2, and most of the encoder stacks will run at a lower + # sampling rate. + encoder_embed = Conv2dSubsampling( + in_channels=params.feature_dim, + out_channels=_to_int_tuple(params.encoder_dim)[0], + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + ) + return encoder_embed + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + encoder = Zipformer2( + output_downsampling_factor=2, + downsampling_factor=_to_int_tuple(params.downsampling_factor), + num_encoder_layers=_to_int_tuple(params.num_encoder_layers), + encoder_dim=_to_int_tuple(params.encoder_dim), + encoder_unmasked_dim=_to_int_tuple(params.encoder_unmasked_dim), + query_head_dim=_to_int_tuple(params.query_head_dim), + pos_head_dim=_to_int_tuple(params.pos_head_dim), + value_head_dim=_to_int_tuple(params.value_head_dim), + pos_dim=params.pos_dim, + num_heads=_to_int_tuple(params.num_heads), + feedforward_dim=_to_int_tuple(params.feedforward_dim), + cnn_module_kernel=_to_int_tuple(params.cnn_module_kernel), + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + warmup_batches=4000.0, + causal=params.causal, + chunk_size=_to_int_tuple(params.chunk_size), + left_context_frames=_to_int_tuple(params.left_context_frames), + ) + return encoder + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=max(_to_int_tuple(params.encoder_dim)), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_transducer_model(params: AttributeDict) -> nn.Module: + encoder_embed = get_encoder_embed(params) + encoder = get_encoder_model(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = Transducer( + encoder_embed=encoder_embed, + encoder=encoder, + decoder=decoder, + joiner=joiner, + encoder_dim=int(max(params.encoder_dim.split(","))), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute RNNT loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Zipformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.warm_step + + texts = batch["supervisions"]["text"] + y = convert_texts_into_ids(texts, sp) + y = k2.RaggedTensor(y).to(device) + + with torch.set_grad_enabled(is_training): + simple_loss, pruned_loss = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + rnnt_type=params.rnnt_type, + ) + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + + loss = simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + saved_bad_model = False + + def save_bad_model(suffix: str = ""): + save_checkpoint_impl( + filename=params.exp_dir / f"bad-model{suffix}-{rank}.pt", + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=0, + ) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx % 10 == 0: + set_batch_count(model, get_adjusted_batch_count(params)) + + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + save_bad_model() + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + + if cur_grad_scale < 8.0 or (cur_grad_scale < 32.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + if not saved_bad_model: + save_bad_model(suffix="-first-warning") + saved_bad_model = True + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + save_bad_model() + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = max(scheduler.get_last_lr()) + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_transducer_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model).to(torch.float64) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + optimizer = ScaledAdam( + get_parameter_groups_with_lrs(model, lr=params.base_lr, include_names=True), + lr=params.base_lr, # should have no effect + clipping_scale=2.0, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + opts = diagnostics.TensorDiagnosticOptions( + 2**22 + ) # allow 4 megabytes per sub-module + diagnostic = diagnostics.attach_diagnostics(model, opts) + + if params.inf_check: + register_inf_check_hooks(model) + + tedlium = TedLiumAsrDataModule(args) + + train_cuts = tedlium.train_cuts() + train_cuts = train_cuts.filter(lambda c: 1.0 <= c.duration <= 20.0) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = tedlium.train_dataloaders( + train_cuts, sampler_state_dict=sampler_state_dict + ) + + valid_cuts = tedlium.dev_cuts() + valid_dl = tedlium.valid_dataloaders(valid_cuts) + + if not params.print_diagnostics: + scan_pessimistic_batches_for_oom( + model=model, + train_dl=train_dl, + optimizer=optimizer, + sp=sp, + params=params, + ) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + supervisions = batch["supervisions"] + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = sp.encode(supervisions["text"], out_type=int) + num_tokens = sum(len(i) for i in y) + logging.info(f"num tokens: {num_tokens}") + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + sp: spm.SentencePieceProcessor, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + loss.backward() + optimizer.zero_grad() + except Exception as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + display_and_save_batch(batch, params=params, sp=sp) + raise + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + + +def main(): + parser = get_parser() + TedLiumAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/tedlium3/ASR/zipformer/zipformer.py b/egs/tedlium3/ASR/zipformer/zipformer.py new file mode 120000 index 000000000..23011dda7 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/zipformer.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/zipformer.py \ No newline at end of file From db71b0302651d0fd6d0e1c742591f35f2ab224ac Mon Sep 17 00:00:00 2001 From: Wei Kang Date: Thu, 29 Jun 2023 16:48:59 +0800 Subject: [PATCH 038/100] Support int8 quantization in decoder (#1152) --- egs/librispeech/ASR/zipformer/export-onnx-streaming.py | 2 +- egs/librispeech/ASR/zipformer/export-onnx.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py index ff3e46433..3eb06f68c 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py +++ b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py @@ -757,7 +757,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/zipformer/export-onnx.py b/egs/librispeech/ASR/zipformer/export-onnx.py index 1bc10c896..724fdd2a6 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx.py +++ b/egs/librispeech/ASR/zipformer/export-onnx.py @@ -602,7 +602,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) From c59c89fc1323ed4d809bad6445d480437206e75a Mon Sep 17 00:00:00 2001 From: Desh Raj Date: Thu, 29 Jun 2023 13:09:01 +0200 Subject: [PATCH 039/100] Minor fix in tedlium results file (#1153) --- egs/tedlium3/ASR/RESULTS.md | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/egs/tedlium3/ASR/RESULTS.md b/egs/tedlium3/ASR/RESULTS.md index cda77073d..bd8a5b43f 100644 --- a/egs/tedlium3/ASR/RESULTS.md +++ b/egs/tedlium3/ASR/RESULTS.md @@ -2,7 +2,7 @@ ### TedLium3 BPE training results (Zipformer) -#### 2023-06-15 +#### 2023-06-15 (Regular transducer) Using the codes from this PR https://github.com/k2-fsa/icefall/pull/1125. @@ -82,9 +82,7 @@ avg=22 A pre-trained model and decoding logs can be found at -#### 2023-06-26 (transducer topology) - -**Modified transducer** +#### 2023-06-26 (Modified transducer) ``` ./zipformer/train.py \ @@ -97,36 +95,16 @@ A pre-trained model and decoding logs can be found at . ### TedLium3 BPE training results (Conformer-CTC 2) From ccd8c624dd19c23b3ef576df3329092a78522e6f Mon Sep 17 00:00:00 2001 From: Zengwei Yao Date: Fri, 30 Jun 2023 12:05:37 +0800 Subject: [PATCH 040/100] support testing onnx exported model on the test sets (#1150) * support testing onnx exported model on the test sets * use token_table instead --- egs/librispeech/ASR/zipformer/onnx_decode.py | 323 ++++++++++++++++++ .../ASR/zipformer/onnx_pretrained.py | 2 +- 2 files changed, 324 insertions(+), 1 deletion(-) create mode 100755 egs/librispeech/ASR/zipformer/onnx_decode.py diff --git a/egs/librispeech/ASR/zipformer/onnx_decode.py b/egs/librispeech/ASR/zipformer/onnx_decode.py new file mode 100755 index 000000000..2aca36ca9 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/onnx_decode.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao, +# Xiaoyu Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads ONNX exported models and uses them to decode the test sets. + +We use the pre-trained model from +https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +as an example to show how to use this file. + +1. Download the pre-trained model + +cd egs/librispeech/ASR + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +2. Export the model to ONNX + +./zipformer/export-onnx.py \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --causal False + +It will generate the following 3 files inside $repo/exp: + + - encoder-epoch-99-avg-1.onnx + - decoder-epoch-99-avg-1.onnx + - joiner-epoch-99-avg-1.onnx + +2. Run this file + +./zipformer/onnx_decode.py \ + --exp-dir $repo/exp \ + --max-duration 600 \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ +""" + + +import argparse +import logging +import time +from pathlib import Path +from typing import List, Tuple + +import torch +import torch.nn as nn +from asr_datamodule import LibriSpeechAsrDataModule + +from onnx_pretrained import greedy_search, OnnxModel + +from icefall.utils import setup_logger, store_transcripts, write_error_stats +from k2 import SymbolTable + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--encoder-model-filename", + type=str, + required=True, + help="Path to the encoder onnx model. ", + ) + + parser.add_argument( + "--decoder-model-filename", + type=str, + required=True, + help="Path to the decoder onnx model. ", + ) + + parser.add_argument( + "--joiner-model-filename", + type=str, + required=True, + help="Path to the joiner onnx model. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--tokens", + type=str, + help="""Path to tokens.txt.""", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="Valid values are greedy_search and modified_beam_search", + ) + + return parser + + +def decode_one_batch( + model: OnnxModel, token_table: SymbolTable, batch: dict +) -> List[List[str]]: + """Decode one batch and return the result. + Currently it only greedy_search is supported. + + Args: + model: + The neural model. + token_table: + The token table. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + + Returns: + Return the decoded results for each utterance. + """ + feature = batch["inputs"] + assert feature.ndim == 3 + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(dtype=torch.int64) + + encoder_out, encoder_out_lens = model.run_encoder(x=feature, x_lens=feature_lens) + + hyps = greedy_search( + model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens + ) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + + hyps = [token_ids_to_words(h).split() for h in hyps] + return hyps + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + model: nn.Module, + token_table: SymbolTable, +) -> Tuple[List[Tuple[str, List[str], List[str]]], float]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + model: + The neural model. + token_table: + The token table. + + Returns: + - A list of tuples. Each tuple contains three elements: + - cut_id, + - reference transcript, + - predicted result. + - The total duration (in seconds) of the dataset. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + log_interval = 10 + total_duration = 0 + + results = [] + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + total_duration += sum([cut.duration for cut in batch["supervisions"]["cut"]]) + + hyps = decode_one_batch(model=model, token_table=token_table, batch=batch) + + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = ref_text.split() + this_batch.append((cut_id, ref_words, hyp_words)) + + results.extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + + return results, total_duration + + +def save_results( + res_dir: Path, + test_set_name: str, + results: List[Tuple[str, List[str], List[str]]], +): + recog_path = res_dir / f"recogs-{test_set_name}.txt" + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = res_dir / f"errs-{test_set_name}.txt" + with open(errs_filename, "w") as f: + wer = write_error_stats(f, f"{test_set_name}", results, enable_log=True) + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + errs_info = res_dir / f"wer-summary-{test_set_name}.txt" + with open(errs_info, "w") as f: + print("WER", file=f) + print(wer, file=f) + + s = "\nFor {}, WER is {}:\n".format(test_set_name, wer) + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + + assert ( + args.decoding_method == "greedy_search" + ), "Only supports greedy_search currently." + res_dir = Path(args.exp_dir) / f"onnx-{args.decoding_method}" + + setup_logger(f"{res_dir}/log-decode") + logging.info("Decoding started") + + device = torch.device("cpu") + logging.info(f"Device: {device}") + + token_table = SymbolTable.from_file(args.tokens) + + logging.info(vars(args)) + + logging.info("About to create model") + model = OnnxModel( + encoder_model_filename=args.encoder_model_filename, + decoder_model_filename=args.decoder_model_filename, + joiner_model_filename=args.joiner_model_filename, + ) + + # we need cut ids to display recognition results. + args.return_cuts = True + librispeech = LibriSpeechAsrDataModule(args) + + test_clean_cuts = librispeech.test_clean_cuts() + test_other_cuts = librispeech.test_other_cuts() + + test_clean_dl = librispeech.test_dataloaders(test_clean_cuts) + test_other_dl = librispeech.test_dataloaders(test_other_cuts) + + test_sets = ["test-clean", "test-other"] + test_dl = [test_clean_dl, test_other_dl] + + for test_set, test_dl in zip(test_sets, test_dl): + start_time = time.time() + results, total_duration = decode_dataset(dl=test_dl, model=model, token_table=token_table) + end_time = time.time() + elapsed_seconds = end_time - start_time + rtf = elapsed_seconds / total_duration + + logging.info(f"Elapsed time: {elapsed_seconds:.3f} s") + logging.info(f"Wave duration: {total_duration:.3f} s") + logging.info( + f"Real time factor (RTF): {elapsed_seconds:.3f}/{total_duration:.3f} = {rtf:.3f}" + ) + + save_results(res_dir=res_dir, test_set_name=test_set, results=results) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained.py b/egs/librispeech/ASR/zipformer/onnx_pretrained.py index b821c4e19..e8a521460 100755 --- a/egs/librispeech/ASR/zipformer/onnx_pretrained.py +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained.py @@ -56,7 +56,7 @@ It will generate the following 3 files inside $repo/exp: 3. Run this file -./pruned_transducer_stateless3/onnx_pretrained.py \ +./zipformer/onnx_pretrained.py \ --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ From 98d89463f6840439e5c4902b98df218a45359198 Mon Sep 17 00:00:00 2001 From: MicKot Date: Fri, 30 Jun 2023 15:16:40 +0200 Subject: [PATCH 041/100] zipformer2 logaddexp onnx safe (#1157) --- egs/librispeech/ASR/zipformer/scaling.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py index 9f23eeead..78c4efdc1 100644 --- a/egs/librispeech/ASR/zipformer/scaling.py +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -36,7 +36,9 @@ def logaddexp(x: Tensor, y: Tensor) -> Tensor: if not torch.jit.is_tracing(): return torch.logaddexp(x, y) else: - return (x.exp() + y.exp()).log() + max_value = torch.max(x, y) + diff = torch.abs(x - y) + return max_value + torch.log1p(torch.exp(-diff)) class PiecewiseLinear(object): """ From c3e23ec8d2a3ed2547bd94dee7280bd3f193a47e Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sun, 2 Jul 2023 10:30:09 +0800 Subject: [PATCH 042/100] Fix logaddexp for ONNX export (#1158) --- egs/librispeech/ASR/zipformer/scaling.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py index 78c4efdc1..885f8f143 100644 --- a/egs/librispeech/ASR/zipformer/scaling.py +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -33,12 +33,24 @@ from torch import Tensor # The following function is to solve the above error when exporting # models to ONNX via torch.jit.trace() def logaddexp(x: Tensor, y: Tensor) -> Tensor: - if not torch.jit.is_tracing(): + # Caution(fangjun): Put torch.jit.is_scripting() before + # torch.onnx.is_in_onnx_export(); + # otherwise, it will cause errors for torch.jit.script(). + # + # torch.logaddexp() works for both torch.jit.script() and + # torch.jit.trace() but it causes errors for ONNX export. + # + if torch.jit.is_scripting(): + # Note: We cannot use torch.jit.is_tracing() here as it also + # matches torch.onnx.export(). return torch.logaddexp(x, y) - else: + elif torch.onnx.is_in_onnx_export(): max_value = torch.max(x, y) diff = torch.abs(x - y) return max_value + torch.log1p(torch.exp(-diff)) + else: + # for torch.jit.trace() + return torch.logaddexp(x, y) class PiecewiseLinear(object): """ From 9009d028a07b0b394b150692f973d3ca9a98cfa3 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Mon, 3 Jul 2023 23:56:51 +0800 Subject: [PATCH 043/100] Fix ONNX export for the latest non-streaming zipformer. (#1160) --- egs/librispeech/ASR/zipformer/scaling.py | 23 ++++++++++++++++--- .../ASR/zipformer/scaling_converter.py | 15 +++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py index 885f8f143..4ee7b7826 100644 --- a/egs/librispeech/ASR/zipformer/scaling.py +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -25,6 +25,11 @@ import math import torch.nn as nn from torch import Tensor +def logaddexp_onnx(x: Tensor, y: Tensor) -> Tensor: + max_value = torch.max(x, y) + diff = torch.abs(x - y) + return max_value + torch.log1p(torch.exp(-diff)) + # RuntimeError: Exporting the operator logaddexp to ONNX opset version # 14 is not supported. Please feel free to request support or submit @@ -45,9 +50,7 @@ def logaddexp(x: Tensor, y: Tensor) -> Tensor: # matches torch.onnx.export(). return torch.logaddexp(x, y) elif torch.onnx.is_in_onnx_export(): - max_value = torch.max(x, y) - diff = torch.abs(x - y) - return max_value + torch.log1p(torch.exp(-diff)) + return logaddexp_onnx(x, y) else: # for torch.jit.trace() return torch.logaddexp(x, y) @@ -1348,6 +1351,13 @@ class SwooshL(torch.nn.Module): return k2.swoosh_l(x) # return SwooshLFunction.apply(x) +class SwooshLOnnx(torch.nn.Module): + def forward(self, x: Tensor) -> Tensor: + """Return Swoosh-L activation. + """ + zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) + return logaddexp_onnx(zero, x - 4.0) - 0.08 * x - 0.035 + class SwooshRFunction(torch.autograd.Function): """ @@ -1414,6 +1424,13 @@ class SwooshR(torch.nn.Module): return k2.swoosh_r(x) # return SwooshRFunction.apply(x) +class SwooshROnnx(torch.nn.Module): + def forward(self, x: Tensor) -> Tensor: + """Return Swoosh-R activation. + """ + zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) + return logaddexp_onnx(zero, x - 1.) - 0.08 * x - 0.313261687 + # simple version of SwooshL that does not redefine the backprop, used in # ActivationDropoutAndLinearFunction. diff --git a/egs/librispeech/ASR/zipformer/scaling_converter.py b/egs/librispeech/ASR/zipformer/scaling_converter.py index 54a5c2a6a..76622fa12 100644 --- a/egs/librispeech/ASR/zipformer/scaling_converter.py +++ b/egs/librispeech/ASR/zipformer/scaling_converter.py @@ -26,7 +26,16 @@ from typing import List, Tuple import torch import torch.nn as nn -from scaling import Balancer, Dropout3, ScaleGrad, Whiten +from scaling import ( + Balancer, + Dropout3, + ScaleGrad, + SwooshL, + SwooshLOnnx, + SwooshR, + SwooshROnnx, + Whiten, +) from zipformer import CompactRelPositionalEncoding @@ -75,6 +84,10 @@ def convert_scaled_to_non_scaled( for name, m in model.named_modules(): if isinstance(m, (Balancer, Dropout3, ScaleGrad, Whiten)): d[name] = nn.Identity() + elif is_onnx and isinstance(m, SwooshR): + d[name] = SwooshROnnx() + elif is_onnx and isinstance(m, SwooshL): + d[name] = SwooshLOnnx() elif is_onnx and isinstance(m, CompactRelPositionalEncoding): # We want to recreate the positional encoding vector when # the input changes, so we have to use torch.jit.script() From eca020263214bffaaf6997c62b031c355101a4db Mon Sep 17 00:00:00 2001 From: "Nickolay V. Shmyrev" Date: Tue, 4 Jul 2023 05:13:25 +0300 Subject: [PATCH 044/100] Add start-batch option for RNNLM training (#1161) * Add start-batch option for RNNLM training * Also set epoch * Skip batches on load --- icefall/rnn_lm/train.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/icefall/rnn_lm/train.py b/icefall/rnn_lm/train.py index 0f0887859..3d206d139 100755 --- a/icefall/rnn_lm/train.py +++ b/icefall/rnn_lm/train.py @@ -99,6 +99,15 @@ def get_parser(): """, ) + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + parser.add_argument( "--exp-dir", type=str, @@ -242,7 +251,9 @@ def load_checkpoint_if_available( ) -> None: """Load checkpoint from file. - If params.start_epoch is positive, it will load the checkpoint from + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from `params.start_epoch - 1`. Otherwise, this function does nothing. Apart from loading state dict for `model`, `optimizer` and `scheduler`, @@ -261,10 +272,14 @@ def load_checkpoint_if_available( Returns: Return None. """ - if params.start_epoch <= 0: - return - filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + logging.info(f"Loading checkpoint: {filename}") saved_params = load_checkpoint( filename, @@ -283,6 +298,13 @@ def load_checkpoint_if_available( for k in keys: params[k] = saved_params[k] + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + if "cur_batch_idx" in saved_params: + params["cur_batch_idx"] = saved_params["cur_batch_idx"] + return saved_params @@ -438,7 +460,14 @@ def train_one_epoch( tot_loss = MetricsTracker() + cur_batch_idx = params.get("cur_batch_idx", 0) + for batch_idx, batch in enumerate(train_dl): + + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + params.batch_idx_train += 1 x, y, sentence_lengths = batch batch_size = x.size(0) @@ -463,6 +492,7 @@ def train_one_epoch( params.batch_idx_train > 0 and params.batch_idx_train % params.save_every_n == 0 ): + params.cur_batch_idx = batch_idx save_checkpoint_with_global_batch_idx( out_dir=params.exp_dir, global_batch_idx=params.batch_idx_train, @@ -471,6 +501,7 @@ def train_one_epoch( optimizer=optimizer, rank=rank, ) + del params.cur_batch_idx if batch_idx % params.log_interval == 0: # Note: "frames" here means "num_tokens" From 856c0f2a60cf2e157cc46013665e6053117efd4f Mon Sep 17 00:00:00 2001 From: zr_jin <60612200+JinZr@users.noreply.github.com> Date: Tue, 4 Jul 2023 19:12:39 +0800 Subject: [PATCH 045/100] fixed default param for an aishell recipe (#1159) --- egs/aishell/ASR/pruned_transducer_stateless7/train.py | 2 +- egs/aishell/ASR/pruned_transducer_stateless7/train2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train.py b/egs/aishell/ASR/pruned_transducer_stateless7/train.py index ef536c035..cbb7db086 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/train.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train.py @@ -240,7 +240,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless3/exp", + default="pruned_transducer_stateless7/exp", help="""The experiment dir. It specifies the directory where all training related files, e.g., checkpoints, log, etc, are saved diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train2.py b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py index fb35a6c95..c30f6f960 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/train2.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py @@ -243,7 +243,7 @@ def get_parser(): parser.add_argument( "--exp-dir", type=str, - default="pruned_transducer_stateless3/exp", + default="pruned_transducer_stateless7/exp", help="""The experiment dir. It specifies the directory where all training related files, e.g., checkpoints, log, etc, are saved From a4402b88e6748d7ad8afe756f909f9da78bb1742 Mon Sep 17 00:00:00 2001 From: Desh Raj Date: Tue, 4 Jul 2023 13:25:58 +0200 Subject: [PATCH 046/100] SURT multi-talker ASR recipe (#1126) * merge upstream * add SURT model and training * add libricss decoding * add chunk width randomization * decode SURT with libricss * initial commit for zipformer_ctc * remove unwanted changes * remove changes to other recipe * fix zipformer softlink * fix for JIT export * add missing file * fix symbolic links * update results * clean commit for SURT recipe * training libricss surt model * remove unwanted files * remove unwanted changes * remove changes in librispeech * change some files to symlinks * remove unwanted changes in utils * add export script * add README * minor fix in README * add assets for README * replace some files with symlinks * remove unused decoding methods * fix symlink * address comments from @csukuangfj --- egs/libricss/SURT/README.md | 249 +++ .../SURT/dprnn_zipformer/asr_datamodule.py | 372 +++++ .../SURT/dprnn_zipformer/beam_search.py | 730 +++++++++ egs/libricss/SURT/dprnn_zipformer/decode.py | 654 ++++++++ egs/libricss/SURT/dprnn_zipformer/decoder.py | 1 + egs/libricss/SURT/dprnn_zipformer/dprnn.py | 305 ++++ .../SURT/dprnn_zipformer/encoder_interface.py | 1 + egs/libricss/SURT/dprnn_zipformer/export.py | 306 ++++ egs/libricss/SURT/dprnn_zipformer/joiner.py | 1 + egs/libricss/SURT/dprnn_zipformer/model.py | 316 ++++ egs/libricss/SURT/dprnn_zipformer/optim.py | 1 + egs/libricss/SURT/dprnn_zipformer/scaling.py | 1 + .../SURT/dprnn_zipformer/scaling_converter.py | 1 + egs/libricss/SURT/dprnn_zipformer/train.py | 1452 +++++++++++++++++ .../SURT/dprnn_zipformer/train_adapt.py | 1343 +++++++++++++++ .../SURT/dprnn_zipformer/zipformer.py | 1 + egs/libricss/SURT/heat.png | Bin 0 -> 305340 bytes egs/libricss/SURT/local/add_source_feats.py | 85 + .../SURT/local/compute_fbank_libricss.py | 105 ++ .../SURT/local/compute_fbank_librispeech.py | 111 ++ .../SURT/local/compute_fbank_lsmix.py | 188 +++ .../SURT/local/compute_fbank_musan.py | 114 ++ egs/libricss/SURT/prepare.sh | 204 +++ egs/libricss/SURT/shared | 1 + egs/libricss/SURT/surt.png | Bin 0 -> 114318 bytes icefall/utils.py | 163 +- 26 files changed, 6704 insertions(+), 1 deletion(-) create mode 100644 egs/libricss/SURT/README.md create mode 100644 egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py create mode 100644 egs/libricss/SURT/dprnn_zipformer/beam_search.py create mode 100755 egs/libricss/SURT/dprnn_zipformer/decode.py create mode 120000 egs/libricss/SURT/dprnn_zipformer/decoder.py create mode 100644 egs/libricss/SURT/dprnn_zipformer/dprnn.py create mode 120000 egs/libricss/SURT/dprnn_zipformer/encoder_interface.py create mode 100755 egs/libricss/SURT/dprnn_zipformer/export.py create mode 120000 egs/libricss/SURT/dprnn_zipformer/joiner.py create mode 100644 egs/libricss/SURT/dprnn_zipformer/model.py create mode 120000 egs/libricss/SURT/dprnn_zipformer/optim.py create mode 120000 egs/libricss/SURT/dprnn_zipformer/scaling.py create mode 120000 egs/libricss/SURT/dprnn_zipformer/scaling_converter.py create mode 100755 egs/libricss/SURT/dprnn_zipformer/train.py create mode 100755 egs/libricss/SURT/dprnn_zipformer/train_adapt.py create mode 120000 egs/libricss/SURT/dprnn_zipformer/zipformer.py create mode 100644 egs/libricss/SURT/heat.png create mode 100755 egs/libricss/SURT/local/add_source_feats.py create mode 100755 egs/libricss/SURT/local/compute_fbank_libricss.py create mode 100755 egs/libricss/SURT/local/compute_fbank_librispeech.py create mode 100755 egs/libricss/SURT/local/compute_fbank_lsmix.py create mode 100755 egs/libricss/SURT/local/compute_fbank_musan.py create mode 100755 egs/libricss/SURT/prepare.sh create mode 120000 egs/libricss/SURT/shared create mode 100644 egs/libricss/SURT/surt.png diff --git a/egs/libricss/SURT/README.md b/egs/libricss/SURT/README.md new file mode 100644 index 000000000..10a1aaad1 --- /dev/null +++ b/egs/libricss/SURT/README.md @@ -0,0 +1,249 @@ +# Introduction + +This is a multi-talker ASR recipe for the LibriCSS dataset. We train a Streaming +Unmixing and Recognition Transducer (SURT) model for the task. In this README, +we will describe the task, the model, and the training process. We will also +provide links to pre-trained models and training logs. + +## Task + +LibriCSS is a multi-talker meeting corpus formed from mixing together LibriSpeech utterances +and replaying in a real meeting room. It consists of 10 1-hour sessions of audio, each +recorded on a 7-channel microphone. The sessions are recorded at a sampling rate of 16 kHz. +For more information, refer to the paper: +Z. Chen et al., "Continuous speech separation: dataset and analysis," +ICASSP 2020 - 2020 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP), +Barcelona, Spain, 2020 + +In this recipe, we perform the "continuous, streaming, multi-talker ASR" task on LibriCSS. + +* By "continuous", we mean that the model should be able to transcribe unsegmented audio +without the need of an external VAD. +* By "streaming", we mean that the model has limited right context. We use a right-context +of at most 32 frames (320 ms). +* By "multi-talker", we mean that the model should be able to transcribe overlapping speech +from multiple speakers. + +For now, we do not care about speaker attribution, i.e., the transcription is speaker +agnostic. The evaluation depends on the particular model type. In this case, we use +the optimal reference combination WER (ORC-WER) metric as implemented in the +[meeteval](https://github.com/fgnt/meeteval) toolkit. + +## Model + +We use the Streaming Unmixing and Recognition Transducer (SURT) model for this task. +The model is based on the papers: + +- Lu, Liang et al. “Streaming End-to-End Multi-Talker Speech Recognition.” IEEE Signal Processing Letters 28 (2020): 803-807. +- Raj, Desh et al. “Continuous Streaming Multi-Talker ASR with Dual-Path Transducers.” ICASSP 2022 - 2022 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP) (2021): 7317-7321. + +The model is a combination of a speech separation model and a speech recognition model, +but trained end-to-end with a single loss function. The overall architecture is shown +in the figure below. Note that this architecture is slightly different from the one +in the above papers. A detailed description of the model can be found in the following +paper: [SURT 2.0: Advanced in transducer-based multi-talker ASR](https://arxiv.org/abs/2306.10559). + +

+ + + Streaming Unmixing and Recognition Transducer + +

+ +In the [dprnn_zipformer](./dprnn_zipformer) recipe, for example, we use a DPRNN-based masking network +and a Zipfomer-based recognition network. But other combinations are possible as well. + +## Training objective + +We train the model using the pruned transducer loss, similar to other ASR recipes in +icefall. However, an important consideration is how to assign references to the output +channels (2 in this case). For this, we use the heuristic error assignment training (HEAT) +strategy, which assigns references to the first available channel based on their start +times. An illustrative example is shown in the figure below: + +

+ + + Illustration of HEAT-based reference assignment. + +

+ +## Description of the recipe + +### Pre-requisites + +The recipes in this directory need the following packages to be installed: + +- [meeteval](https://github.com/fgnt/meeteval) +- [einops](https://github.com/arogozhnikov/einops) + +Additionally, we initialize the "recognition" transducer with a pre-trained model, +trained on LibriSpeech. For this, please run the following from within `egs/librispeech/ASR`: + +```bash +./prepare.sh + +export CUDA_VISIBLE_DEVICES="0,1,2,3" +python pruned_transducer_stateless7_streaming/train.py \ + --use-fp16 True \ + --exp-dir pruned_transducer_stateless7_streaming/exp \ + --world-size 4 \ + --max-duration 800 \ + --num-epochs 10 \ + --keep-last-k 1 \ + --manifest-dir data/manifests \ + --enable-musan true \ + --master-port 54321 \ + --bpe-model data/lang_bpe_500/bpe.model \ + --num-encoder-layers 2,2,2,2,2 \ + --feedforward-dims 768,768,768,768,768 \ + --nhead 8,8,8,8,8 \ + --encoder-dims 256,256,256,256,256 \ + --attention-dims 192,192,192,192,192 \ + --encoder-unmasked-dims 192,192,192,192,192 \ + --zipformer-downsampling-factors 1,2,4,8,2 \ + --cnn-module-kernels 31,31,31,31,31 \ + --decoder-dim 512 \ + --joiner-dim 512 +``` + +The above is for SURT-base (~26M). For SURT-large (~38M), use `--num-encoder-layers 2,4,3,2,4`. + +Once the above model is trained for 10 epochs, copy it to `egs/libricss/SURT/exp`: + +```bash +cp -r pruned_transducer_stateless7_streaming/exp/epoch-10.pt exp/zipformer_base.pt +``` + +**NOTE:** We also provide this pre-trained checkpoint (see the section below), so you can skip +the above step if you want. + +### Training + +To train the model, run the following from within `egs/libricss/SURT`: + +```bash +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +python dprnn_zipformer/train.py \ + --use-fp16 True \ + --exp-dir dprnn_zipformer/exp/surt_base \ + --world-size 4 \ + --max-duration 500 \ + --max-duration-valid 250 \ + --max-cuts 200 \ + --num-buckets 50 \ + --num-epochs 30 \ + --enable-spec-aug True \ + --enable-musan False \ + --ctc-loss-scale 0.2 \ + --heat-loss-scale 0.2 \ + --base-lr 0.004 \ + --model-init-ckpt exp/zipformer_base.pt \ + --chunk-width-randomization True \ + --num-mask-encoder-layers 4 \ + --num-encoder-layers 2,2,2,2,2 +``` + +The above is for SURT-base (~26M). For SURT-large (~38M), use: + +```bash + --num-mask-encoder-layers 6 \ + --num-encoder-layers 2,4,3,2,4 \ + --model-init-ckpt exp/zipformer_large.pt \ +``` + +**NOTE:** You may need to decrease the `--max-duration` for SURT-large to avoid OOM. + +### Adaptation + +The training step above only trains on simulated mixtures. For best results, we also +adapt the final model on the LibriCSS dev set. For this, run the following from within +`egs/libricss/SURT`: + +```bash +export CUDA_VISIBLE_DEVICES="0" + +python dprnn_zipformer/train_adapt.py \ + --use-fp16 True \ + --exp-dir dprnn_zipformer/exp/surt_base_adapt \ + --world-size 1 \ + --max-duration 500 \ + --max-duration-valid 250 \ + --max-cuts 200 \ + --num-buckets 50 \ + --num-epochs 8 \ + --lr-epochs 2 \ + --enable-spec-aug True \ + --enable-musan False \ + --ctc-loss-scale 0.2 \ + --base-lr 0.0004 \ + --model-init-ckpt dprnn_zipformer/exp/surt_base/epoch-30.pt \ + --chunk-width-randomization True \ + --num-mask-encoder-layers 4 \ + --num-encoder-layers 2,2,2,2,2 +``` + +For SURT-large, use the following config: + +```bash + --num-mask-encoder-layers 6 \ + --num-encoder-layers 2,4,3,2,4 \ + --model-init-ckpt dprnn_zipformer/exp/surt_large/epoch-30.pt \ + --num-epochs 15 \ + --lr-epochs 4 \ +``` + + +### Decoding + +To decode the model, run the following from within `egs/libricss/SURT`: + +#### Greedy search + +```bash +export CUDA_VISIBLE_DEVICES="0" + +python dprnn_zipformer/decode.py \ + --epoch 8 --avg 1 --use-averaged-model False \ + --exp-dir dprnn_zipformer/exp/surt_base_adapt \ + --max-duration 250 \ + --decoding-method greedy_search +``` + +#### Beam search + +```bash +python dprnn_zipformer/decode.py \ + --epoch 8 --avg 1 --use-averaged-model False \ + --exp-dir dprnn_zipformer/exp/surt_base_adapt \ + --max-duration 250 \ + --decoding-method modified_beam_search \ + --beam-size 4 +``` + +## Results (using beam search) + +#### IHM-Mix + +| Model | # params | 0L | 0S | OV10 | OV20 | OV30 | OV40 | Avg. | +|------------|:-------:|:----:|:---:|----:|:----:|:----:|:----:|:----:| +| dprnn_zipformer (base) | 26.7 | 5.1 | 4.2 | 13.7 | 18.7 | 20.5 | 20.6 | 13.8 | +| dprnn_zipformer (large) | 37.9 | 4.6 | 3.8 | 12.7 | 14.3 | 16.7 | 21.2 | 12.2 | + +#### SDM + +| Model | # params | 0L | 0S | OV10 | OV20 | OV30 | OV40 | Avg. | +|------------|:-------:|:----:|:---:|----:|:----:|:----:|:----:|:----:| +| dprnn_zipformer (base) | 26.7 | 6.8 | 7.2 | 21.4 | 24.5 | 28.6 | 31.2 | 20.0 | +| dprnn_zipformer (large) | 37.9 | 6.4 | 6.9 | 17.9 | 19.7 | 25.2 | 25.5 | 16.9 | + +## Pre-trained models and logs + +* Pre-trained models: + +* Training logs: + - surt_base: + - surt_base_adapt: + - surt_large: + - surt_large_adapt: diff --git a/egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py b/egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py new file mode 100644 index 000000000..51df91598 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py @@ -0,0 +1,372 @@ +# Copyright 2021 Piotr Żelasko +# Copyright 2022 Xiaomi Corporation (Author: Mingshuang Luo) +# Copyright 2023 Johns Hopkins Univrtsity (Author: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 inspect +import logging +from functools import lru_cache +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +import torch +from lhotse import CutSet, Fbank, FbankConfig, load_manifest, load_manifest_lazy +from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures + CutMix, + DynamicBucketingSampler, + K2SurtDataset, + PrecomputedFeatures, + SimpleCutSampler, + SpecAugment, +) +from lhotse.dataset.input_strategies import OnTheFlyFeatures +from lhotse.utils import fix_random_seed +from torch.utils.data import DataLoader + +from icefall.utils import str2bool + + +class _SeedWorkers: + def __init__(self, seed: int): + self.seed = seed + + def __call__(self, worker_id: int): + fix_random_seed(self.seed + worker_id) + + +class LibriCssAsrDataModule: + """ + DataModule for k2 ASR experiments. + It assumes there is always one train and valid dataloader, + but there can be multiple test dataloaders (e.g. LibriSpeech test-clean + and test-other). + + It contains all the common data pipeline modules used in ASR + experiments, e.g.: + - dynamic batch size, + - bucketing samplers, + - augmentation, + - on-the-fly feature extraction + + This class should be derived for specific corpora used in ASR tasks. + """ + + def __init__(self, args: argparse.Namespace): + self.args = args + + @classmethod + def add_arguments(cls, parser: argparse.ArgumentParser): + group = parser.add_argument_group( + title="ASR data related options", + description="These options are used for the preparation of " + "PyTorch DataLoaders from Lhotse CutSet's -- they control the " + "effective batch sizes, sampling strategies, applied data " + "augmentations, etc.", + ) + group.add_argument( + "--manifest-dir", + type=Path, + default=Path("data/manifests"), + help="Path to directory with train/valid/test cuts.", + ) + group.add_argument( + "--max-duration", + type=int, + default=200.0, + help="Maximum pooled recordings duration (seconds) in a " + "single batch. You can reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--max-duration-valid", + type=int, + default=200.0, + help="Maximum pooled recordings duration (seconds) in a " + "single batch. You can reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--max-cuts", + type=int, + default=100, + help="Maximum number of cuts in a single batch. You can " + "reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--bucketing-sampler", + type=str2bool, + default=True, + help="When enabled, the batches will come from buckets of " + "similar duration (saves padding frames).", + ) + group.add_argument( + "--num-buckets", + type=int, + default=30, + help="The number of buckets for the DynamicBucketingSampler" + "(you might want to increase it for larger datasets).", + ) + group.add_argument( + "--on-the-fly-feats", + type=str2bool, + default=False, + help=( + "When enabled, use on-the-fly cut mixing and feature " + "extraction. Will drop existing precomputed feature manifests " + "if available." + ), + ) + group.add_argument( + "--shuffle", + type=str2bool, + default=True, + help="When enabled (=default), the examples will be " + "shuffled for each epoch.", + ) + group.add_argument( + "--drop-last", + type=str2bool, + default=True, + help="Whether to drop last batch. Used by sampler.", + ) + group.add_argument( + "--return-cuts", + type=str2bool, + default=True, + help="When enabled, each batch will have the " + "field: batch['supervisions']['cut'] with the cuts that " + "were used to construct it.", + ) + + group.add_argument( + "--num-workers", + type=int, + default=2, + help="The number of training dataloader workers that " + "collect the batches.", + ) + + group.add_argument( + "--enable-spec-aug", + type=str2bool, + default=True, + help="When enabled, use SpecAugment for training dataset.", + ) + + group.add_argument( + "--spec-aug-time-warp-factor", + type=int, + default=80, + help="Used only when --enable-spec-aug is True. " + "It specifies the factor for time warping in SpecAugment. " + "Larger values mean more warping. " + "A value less than 1 means to disable time warp.", + ) + + group.add_argument( + "--enable-musan", + type=str2bool, + default=True, + help="When enabled, select noise from MUSAN and mix it" + "with training dataset. ", + ) + + def train_dataloaders( + self, + cuts_train: CutSet, + sampler_state_dict: Optional[Dict[str, Any]] = None, + return_sources: bool = True, + strict: bool = True, + ) -> DataLoader: + """ + Args: + cuts_train: + CutSet for training. + sampler_state_dict: + The state dict for the training sampler. + """ + transforms = [] + if self.args.enable_musan: + logging.info("Enable MUSAN") + logging.info("About to get Musan cuts") + cuts_musan = load_manifest(self.args.manifest_dir / "musan_cuts.jsonl.gz") + transforms.append( + CutMix(cuts=cuts_musan, prob=0.5, snr=(10, 20), preserve_id=True) + ) + else: + logging.info("Disable MUSAN") + + input_transforms = [] + if self.args.enable_spec_aug: + logging.info("Enable SpecAugment") + logging.info(f"Time warp factor: {self.args.spec_aug_time_warp_factor}") + # Set the value of num_frame_masks according to Lhotse's version. + # In different Lhotse's versions, the default of num_frame_masks is + # different. + num_frame_masks = 10 + num_frame_masks_parameter = inspect.signature( + SpecAugment.__init__ + ).parameters["num_frame_masks"] + if num_frame_masks_parameter.default == 1: + num_frame_masks = 2 + logging.info(f"Num frame mask: {num_frame_masks}") + input_transforms.append( + SpecAugment( + time_warp_factor=self.args.spec_aug_time_warp_factor, + num_frame_masks=num_frame_masks, + features_mask_size=27, + num_feature_masks=2, + frames_mask_size=100, + ) + ) + else: + logging.info("Disable SpecAugment") + + logging.info("About to create train dataset") + train = K2SurtDataset( + input_strategy=OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) + if self.args.on_the_fly_feats + else PrecomputedFeatures(), + cut_transforms=transforms, + input_transforms=input_transforms, + return_cuts=self.args.return_cuts, + return_sources=return_sources, + strict=strict, + ) + + if self.args.bucketing_sampler: + logging.info("Using DynamicBucketingSampler.") + train_sampler = DynamicBucketingSampler( + cuts_train, + max_duration=self.args.max_duration, + quadratic_duration=30.0, + max_cuts=self.args.max_cuts, + shuffle=self.args.shuffle, + num_buckets=self.args.num_buckets, + drop_last=self.args.drop_last, + ) + else: + logging.info("Using SingleCutSampler.") + train_sampler = SimpleCutSampler( + cuts_train, + max_duration=self.args.max_duration, + max_cuts=self.args.max_cuts, + shuffle=self.args.shuffle, + ) + logging.info("About to create train dataloader") + + if sampler_state_dict is not None: + logging.info("Loading sampler state dict") + train_sampler.load_state_dict(sampler_state_dict) + + # 'seed' is derived from the current random state, which will have + # previously been set in the main process. + seed = torch.randint(0, 100000, ()).item() + worker_init_fn = _SeedWorkers(seed) + + train_dl = DataLoader( + train, + sampler=train_sampler, + batch_size=None, + num_workers=self.args.num_workers, + persistent_workers=False, + worker_init_fn=worker_init_fn, + ) + + return train_dl + + def valid_dataloaders(self, cuts_valid: CutSet) -> DataLoader: + transforms = [] + + logging.info("About to create dev dataset") + validate = K2SurtDataset( + input_strategy=OnTheFlyFeatures( + OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) + ) + if self.args.on_the_fly_feats + else PrecomputedFeatures(), + cut_transforms=transforms, + return_cuts=self.args.return_cuts, + return_sources=False, + strict=False, + ) + valid_sampler = DynamicBucketingSampler( + cuts_valid, + max_duration=self.args.max_duration_valid, + max_cuts=self.args.max_cuts, + shuffle=False, + ) + logging.info("About to create dev dataloader") + valid_dl = DataLoader( + validate, + sampler=valid_sampler, + batch_size=None, + num_workers=2, + persistent_workers=False, + ) + + return valid_dl + + def test_dataloaders(self, cuts: CutSet) -> DataLoader: + logging.debug("About to create test dataset") + test = K2SurtDataset( + input_strategy=OnTheFlyFeatures( + OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) + ) + if self.args.on_the_fly_feats + else PrecomputedFeatures(), + return_cuts=self.args.return_cuts, + return_sources=False, + strict=False, + ) + sampler = DynamicBucketingSampler( + cuts, + max_duration=self.args.max_duration_valid, + max_cuts=self.args.max_cuts, + shuffle=False, + ) + logging.debug("About to create test dataloader") + test_dl = DataLoader( + test, + batch_size=None, + sampler=sampler, + num_workers=self.args.num_workers, + ) + return test_dl + + @lru_cache() + def lsmix_cuts( + self, + rvb_affix: str = "clean", + type_affix: str = "full", + sources: bool = True, + ) -> CutSet: + logging.info("About to get train cuts") + source_affix = "_sources" if sources else "" + cs = load_manifest_lazy( + self.args.manifest_dir + / f"cuts_train_{rvb_affix}_{type_affix}{source_affix}.jsonl.gz" + ) + cs = cs.filter(lambda c: c.duration >= 1.0 and c.duration <= 30.0) + return cs + + @lru_cache() + def libricss_cuts(self, split="dev", type="sdm") -> CutSet: + logging.info(f"About to get LibriCSS {split} {type} cuts") + cs = load_manifest_lazy( + self.args.manifest_dir / f"cuts_{split}_libricss-{type}.jsonl.gz" + ) + return cs diff --git a/egs/libricss/SURT/dprnn_zipformer/beam_search.py b/egs/libricss/SURT/dprnn_zipformer/beam_search.py new file mode 100644 index 000000000..c8e4643d0 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/beam_search.py @@ -0,0 +1,730 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang +# Xiaoyu Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 warnings +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Tuple, Union + +import k2 +import torch +from model import SURT + +from icefall import NgramLmStateCost +from icefall.utils import DecodingResults + + +def greedy_search( + model: SURT, + encoder_out: torch.Tensor, + max_sym_per_frame: int, + return_timestamps: bool = False, +) -> Union[List[int], DecodingResults]: + """Greedy search for a single utterance. + Args: + model: + An instance of `SURT`. + encoder_out: + A tensor of shape (N, T, C) from the encoder. Support only N==1 for now. + max_sym_per_frame: + Maximum number of symbols per frame. If it is set to 0, the WER + would be 100%. + return_timestamps: + Whether to return timestamps. + Returns: + If return_timestamps is False, return the decoded result. + Else, return a DecodingResults object containing + decoded result and corresponding timestamps. + """ + assert encoder_out.ndim == 4 + + # support only batch_size == 1 for now + assert encoder_out.size(0) == 1, encoder_out.size(0) + + blank_id = model.decoder.blank_id + context_size = model.decoder.context_size + unk_id = getattr(model, "unk_id", blank_id) + + device = next(model.parameters()).device + + decoder_input = torch.tensor( + [-1] * (context_size - 1) + [blank_id], device=device, dtype=torch.int64 + ).reshape(1, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + + encoder_out = model.joiner.encoder_proj(encoder_out) + + T = encoder_out.size(1) + t = 0 + hyp = [blank_id] * context_size + + # timestamp[i] is the frame index after subsampling + # on which hyp[i] is decoded + timestamp = [] + + # Maximum symbols per utterance. + max_sym_per_utt = 1000 + + # symbols per frame + sym_per_frame = 0 + + # symbols per utterance decoded so far + sym_per_utt = 0 + + while t < T and sym_per_utt < max_sym_per_utt: + if sym_per_frame >= max_sym_per_frame: + sym_per_frame = 0 + t += 1 + continue + + # fmt: off + current_encoder_out = encoder_out[:, t:t+1, :].unsqueeze(2) + # fmt: on + logits = model.joiner( + current_encoder_out, decoder_out.unsqueeze(1), project_input=False + ) + # logits is (1, 1, 1, vocab_size) + + y = logits.argmax().item() + if y not in (blank_id, unk_id): + hyp.append(y) + timestamp.append(t) + decoder_input = torch.tensor([hyp[-context_size:]], device=device).reshape( + 1, context_size + ) + + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + + sym_per_utt += 1 + sym_per_frame += 1 + else: + sym_per_frame = 0 + t += 1 + hyp = hyp[context_size:] # remove blanks + + if not return_timestamps: + return hyp + else: + return DecodingResults( + hyps=[hyp], + timestamps=[timestamp], + ) + + +def greedy_search_batch( + model: SURT, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + return_timestamps: bool = False, +) -> Union[List[List[int]], DecodingResults]: + """Greedy search in batch mode. It hardcodes --max-sym-per-frame=1. + Args: + model: + The SURT model. + encoder_out: + Output from the encoder. Its shape is (N, T, C), where N >= 1. + encoder_out_lens: + A 1-D tensor of shape (N,), containing number of valid frames in + encoder_out before padding. + return_timestamps: + Whether to return timestamps. + Returns: + If return_timestamps is False, return the decoded result. + Else, return a DecodingResults object containing + decoded result and corresponding timestamps. + """ + assert encoder_out.ndim == 3 + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + device = next(model.parameters()).device + + blank_id = model.decoder.blank_id + unk_id = getattr(model, "unk_id", blank_id) + context_size = model.decoder.context_size + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + hyps = [[-1] * (context_size - 1) + [blank_id] for _ in range(N)] + + # timestamp[n][i] is the frame index after subsampling + # on which hyp[n][i] is decoded + timestamps = [[] for _ in range(N)] + + decoder_input = torch.tensor( + hyps, + device=device, + dtype=torch.int64, + ) # (N, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + # decoder_out: (N, 1, decoder_out_dim) + + encoder_out = model.joiner.encoder_proj(packed_encoder_out.data) + + offset = 0 + for (t, batch_size) in enumerate(batch_size_list): + start = offset + end = offset + batch_size + current_encoder_out = encoder_out.data[start:end] + current_encoder_out = current_encoder_out.unsqueeze(1).unsqueeze(1) + # current_encoder_out's shape: (batch_size, 1, 1, encoder_out_dim) + offset = end + + decoder_out = decoder_out[:batch_size] + + logits = model.joiner( + current_encoder_out, decoder_out.unsqueeze(1), project_input=False + ) + # logits'shape (batch_size, 1, 1, vocab_size) + + logits = logits.squeeze(1).squeeze(1) # (batch_size, vocab_size) + assert logits.ndim == 2, logits.shape + y = logits.argmax(dim=1).tolist() + emitted = False + for i, v in enumerate(y): + if v not in (blank_id, unk_id): + hyps[i].append(v) + timestamps[i].append(t) + emitted = True + if emitted: + # update decoder output + decoder_input = [h[-context_size:] for h in hyps[:batch_size]] + decoder_input = torch.tensor( + decoder_input, + device=device, + dtype=torch.int64, + ) + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + + sorted_ans = [h[context_size:] for h in hyps] + ans = [] + ans_timestamps = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + ans_timestamps.append(timestamps[unsorted_indices[i]]) + + if not return_timestamps: + return ans + else: + return DecodingResults( + hyps=ans, + timestamps=ans_timestamps, + ) + + +def modified_beam_search( + model: SURT, + encoder_out: torch.Tensor, + encoder_out_lens: torch.Tensor, + beam: int = 4, + temperature: float = 1.0, + return_timestamps: bool = False, +) -> Union[List[List[int]], DecodingResults]: + """Beam search in batch mode with --max-sym-per-frame=1 being hardcoded. + + Args: + model: + The SURT model. + encoder_out: + Output from the encoder. Its shape is (N, T, C). + encoder_out_lens: + A 1-D tensor of shape (N,), containing number of valid frames in + encoder_out before padding. + beam: + Number of active paths during the beam search. + temperature: + Softmax temperature. + return_timestamps: + Whether to return timestamps. + Returns: + If return_timestamps is False, return the decoded result. + Else, return a DecodingResults object containing + decoded result and corresponding timestamps. + """ + assert encoder_out.ndim == 3, encoder_out.shape + assert encoder_out.size(0) >= 1, encoder_out.size(0) + + packed_encoder_out = torch.nn.utils.rnn.pack_padded_sequence( + input=encoder_out, + lengths=encoder_out_lens.cpu(), + batch_first=True, + enforce_sorted=False, + ) + + blank_id = model.decoder.blank_id + unk_id = getattr(model, "unk_id", blank_id) + context_size = model.decoder.context_size + device = next(model.parameters()).device + + batch_size_list = packed_encoder_out.batch_sizes.tolist() + N = encoder_out.size(0) + assert torch.all(encoder_out_lens > 0), encoder_out_lens + assert N == batch_size_list[0], (N, batch_size_list) + + B = [HypothesisList() for _ in range(N)] + for i in range(N): + B[i].add( + Hypothesis( + ys=[blank_id] * context_size, + log_prob=torch.zeros(1, dtype=torch.float32, device=device), + timestamp=[], + ) + ) + + encoder_out = model.joiner.encoder_proj(packed_encoder_out.data) + + offset = 0 + finalized_B = [] + for (t, batch_size) in enumerate(batch_size_list): + start = offset + end = offset + batch_size + current_encoder_out = encoder_out.data[start:end] + current_encoder_out = current_encoder_out.unsqueeze(1).unsqueeze(1) + # current_encoder_out's shape is (batch_size, 1, 1, encoder_out_dim) + offset = end + + finalized_B = B[batch_size:] + finalized_B + B = B[:batch_size] + + hyps_shape = get_hyps_shape(B).to(device) + + A = [list(b) for b in B] + B = [HypothesisList() for _ in range(batch_size)] + + ys_log_probs = torch.cat( + [hyp.log_prob.reshape(1, 1) for hyps in A for hyp in hyps] + ) # (num_hyps, 1) + + decoder_input = torch.tensor( + [hyp.ys[-context_size:] for hyps in A for hyp in hyps], + device=device, + dtype=torch.int64, + ) # (num_hyps, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False).unsqueeze(1) + decoder_out = model.joiner.decoder_proj(decoder_out) + # decoder_out is of shape (num_hyps, 1, 1, joiner_dim) + + # Note: For torch 1.7.1 and below, it requires a torch.int64 tensor + # as index, so we use `to(torch.int64)` below. + current_encoder_out = torch.index_select( + current_encoder_out, + dim=0, + index=hyps_shape.row_ids(1).to(torch.int64), + ) # (num_hyps, 1, 1, encoder_out_dim) + + logits = model.joiner( + current_encoder_out, + decoder_out, + project_input=False, + ) # (num_hyps, 1, 1, vocab_size) + + logits = logits.squeeze(1).squeeze(1) # (num_hyps, vocab_size) + + log_probs = (logits / temperature).log_softmax(dim=-1) # (num_hyps, vocab_size) + + log_probs.add_(ys_log_probs) + + vocab_size = log_probs.size(-1) + + log_probs = log_probs.reshape(-1) + + row_splits = hyps_shape.row_splits(1) * vocab_size + log_probs_shape = k2.ragged.create_ragged_shape2( + row_splits=row_splits, cached_tot_size=log_probs.numel() + ) + ragged_log_probs = k2.RaggedTensor(shape=log_probs_shape, value=log_probs) + + for i in range(batch_size): + topk_log_probs, topk_indexes = ragged_log_probs[i].topk(beam) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + topk_hyp_indexes = (topk_indexes // vocab_size).tolist() + topk_token_indexes = (topk_indexes % vocab_size).tolist() + + for k in range(len(topk_hyp_indexes)): + hyp_idx = topk_hyp_indexes[k] + hyp = A[i][hyp_idx] + + new_ys = hyp.ys[:] + new_token = topk_token_indexes[k] + new_timestamp = hyp.timestamp[:] + if new_token not in (blank_id, unk_id): + new_ys.append(new_token) + new_timestamp.append(t) + + new_log_prob = topk_log_probs[k] + new_hyp = Hypothesis( + ys=new_ys, log_prob=new_log_prob, timestamp=new_timestamp + ) + B[i].add(new_hyp) + + B = B + finalized_B + best_hyps = [b.get_most_probable(length_norm=True) for b in B] + + sorted_ans = [h.ys[context_size:] for h in best_hyps] + sorted_timestamps = [h.timestamp for h in best_hyps] + ans = [] + ans_timestamps = [] + unsorted_indices = packed_encoder_out.unsorted_indices.tolist() + for i in range(N): + ans.append(sorted_ans[unsorted_indices[i]]) + ans_timestamps.append(sorted_timestamps[unsorted_indices[i]]) + + if not return_timestamps: + return ans + else: + return DecodingResults( + hyps=ans, + timestamps=ans_timestamps, + ) + + +def beam_search( + model: SURT, + encoder_out: torch.Tensor, + beam: int = 4, + temperature: float = 1.0, + return_timestamps: bool = False, +) -> Union[List[int], DecodingResults]: + """ + It implements Algorithm 1 in https://arxiv.org/pdf/1211.3711.pdf + + espnet/nets/beam_search_SURT.py#L247 is used as a reference. + + Args: + model: + An instance of `SURT`. + encoder_out: + A tensor of shape (N, T, C) from the encoder. Support only N==1 for now. + beam: + Beam size. + temperature: + Softmax temperature. + return_timestamps: + Whether to return timestamps. + + Returns: + If return_timestamps is False, return the decoded result. + Else, return a DecodingResults object containing + decoded result and corresponding timestamps. + """ + assert encoder_out.ndim == 3 + + # support only batch_size == 1 for now + assert encoder_out.size(0) == 1, encoder_out.size(0) + blank_id = model.decoder.blank_id + unk_id = getattr(model, "unk_id", blank_id) + context_size = model.decoder.context_size + + device = next(model.parameters()).device + + decoder_input = torch.tensor( + [blank_id] * context_size, + device=device, + dtype=torch.int64, + ).reshape(1, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + + encoder_out = model.joiner.encoder_proj(encoder_out) + + T = encoder_out.size(1) + t = 0 + + B = HypothesisList() + B.add(Hypothesis(ys=[blank_id] * context_size, log_prob=0.0, timestamp=[])) + + max_sym_per_utt = 20000 + + sym_per_utt = 0 + + decoder_cache: Dict[str, torch.Tensor] = {} + + while t < T and sym_per_utt < max_sym_per_utt: + # fmt: off + current_encoder_out = encoder_out[:, t:t+1, :].unsqueeze(2) + # fmt: on + A = B + B = HypothesisList() + + joint_cache: Dict[str, torch.Tensor] = {} + + # TODO(fangjun): Implement prefix search to update the `log_prob` + # of hypotheses in A + + while True: + y_star = A.get_most_probable() + A.remove(y_star) + + cached_key = y_star.key + + if cached_key not in decoder_cache: + decoder_input = torch.tensor( + [y_star.ys[-context_size:]], + device=device, + dtype=torch.int64, + ).reshape(1, context_size) + + decoder_out = model.decoder(decoder_input, need_pad=False) + decoder_out = model.joiner.decoder_proj(decoder_out) + decoder_cache[cached_key] = decoder_out + else: + decoder_out = decoder_cache[cached_key] + + cached_key += f"-t-{t}" + if cached_key not in joint_cache: + logits = model.joiner( + current_encoder_out, + decoder_out.unsqueeze(1), + project_input=False, + ) + + # TODO(fangjun): Scale the blank posterior + log_prob = (logits / temperature).log_softmax(dim=-1) + # log_prob is (1, 1, 1, vocab_size) + log_prob = log_prob.squeeze() + # Now log_prob is (vocab_size,) + joint_cache[cached_key] = log_prob + else: + log_prob = joint_cache[cached_key] + + # First, process the blank symbol + skip_log_prob = log_prob[blank_id] + new_y_star_log_prob = y_star.log_prob + skip_log_prob + + # ys[:] returns a copy of ys + B.add( + Hypothesis( + ys=y_star.ys[:], + log_prob=new_y_star_log_prob, + timestamp=y_star.timestamp[:], + ) + ) + + # Second, process other non-blank labels + values, indices = log_prob.topk(beam + 1) + for i, v in zip(indices.tolist(), values.tolist()): + if i in (blank_id, unk_id): + continue + new_ys = y_star.ys + [i] + new_log_prob = y_star.log_prob + v + new_timestamp = y_star.timestamp + [t] + A.add( + Hypothesis( + ys=new_ys, + log_prob=new_log_prob, + timestamp=new_timestamp, + ) + ) + + # Check whether B contains more than "beam" elements more probable + # than the most probable in A + A_most_probable = A.get_most_probable() + + kept_B = B.filter(A_most_probable.log_prob) + + if len(kept_B) >= beam: + B = kept_B.topk(beam) + break + + t += 1 + + best_hyp = B.get_most_probable(length_norm=True) + ys = best_hyp.ys[context_size:] # [context_size:] to remove blanks + + if not return_timestamps: + return ys + else: + return DecodingResults(hyps=[ys], timestamps=[best_hyp.timestamp]) + + +@dataclass +class Hypothesis: + # The predicted tokens so far. + # Newly predicted tokens are appended to `ys`. + ys: List[int] + + # The log prob of ys. + # It contains only one entry. + log_prob: torch.Tensor + + # timestamp[i] is the frame index after subsampling + # on which ys[i] is decoded + timestamp: List[int] = field(default_factory=list) + + # the lm score for next token given the current ys + lm_score: Optional[torch.Tensor] = None + + # the RNNLM states (h and c in LSTM) + state: Optional[Tuple[torch.Tensor, torch.Tensor]] = None + + # N-gram LM state + state_cost: Optional[NgramLmStateCost] = None + + @property + def key(self) -> str: + """Return a string representation of self.ys""" + return "_".join(map(str, self.ys)) + + +class HypothesisList(object): + def __init__(self, data: Optional[Dict[str, Hypothesis]] = None) -> None: + """ + Args: + data: + A dict of Hypotheses. Its key is its `value.key`. + """ + if data is None: + self._data = {} + else: + self._data = data + + @property + def data(self) -> Dict[str, Hypothesis]: + return self._data + + def add(self, hyp: Hypothesis) -> None: + """Add a Hypothesis to `self`. + + If `hyp` already exists in `self`, its probability is updated using + `log-sum-exp` with the existed one. + + Args: + hyp: + The hypothesis to be added. + """ + key = hyp.key + if key in self: + old_hyp = self._data[key] # shallow copy + torch.logaddexp(old_hyp.log_prob, hyp.log_prob, out=old_hyp.log_prob) + else: + self._data[key] = hyp + + def get_most_probable(self, length_norm: bool = False) -> Hypothesis: + """Get the most probable hypothesis, i.e., the one with + the largest `log_prob`. + + Args: + length_norm: + If True, the `log_prob` of a hypothesis is normalized by the + number of tokens in it. + Returns: + Return the hypothesis that has the largest `log_prob`. + """ + if length_norm: + return max(self._data.values(), key=lambda hyp: hyp.log_prob / len(hyp.ys)) + else: + return max(self._data.values(), key=lambda hyp: hyp.log_prob) + + def remove(self, hyp: Hypothesis) -> None: + """Remove a given hypothesis. + + Caution: + `self` is modified **in-place**. + + Args: + hyp: + The hypothesis to be removed from `self`. + Note: It must be contained in `self`. Otherwise, + an exception is raised. + """ + key = hyp.key + assert key in self, f"{key} does not exist" + del self._data[key] + + def filter(self, threshold: torch.Tensor) -> "HypothesisList": + """Remove all Hypotheses whose log_prob is less than threshold. + + Caution: + `self` is not modified. Instead, a new HypothesisList is returned. + + Returns: + Return a new HypothesisList containing all hypotheses from `self` + with `log_prob` being greater than the given `threshold`. + """ + ans = HypothesisList() + for _, hyp in self._data.items(): + if hyp.log_prob > threshold: + ans.add(hyp) # shallow copy + return ans + + def topk(self, k: int) -> "HypothesisList": + """Return the top-k hypothesis.""" + hyps = list(self._data.items()) + + hyps = sorted(hyps, key=lambda h: h[1].log_prob, reverse=True)[:k] + + ans = HypothesisList(dict(hyps)) + return ans + + def __contains__(self, key: str): + return key in self._data + + def __iter__(self): + return iter(self._data.values()) + + def __len__(self) -> int: + return len(self._data) + + def __str__(self) -> str: + s = [] + for key in self: + s.append(key) + return ", ".join(s) + + +def get_hyps_shape(hyps: List[HypothesisList]) -> k2.RaggedShape: + """Return a ragged shape with axes [utt][num_hyps]. + + Args: + hyps: + len(hyps) == batch_size. It contains the current hypothesis for + each utterance in the batch. + Returns: + Return a ragged shape with 2 axes [utt][num_hyps]. Note that + the shape is on CPU. + """ + num_hyps = [len(h) for h in hyps] + + # torch.cumsum() is inclusive sum, so we put a 0 at the beginning + # to get exclusive sum later. + num_hyps.insert(0, 0) + + num_hyps = torch.tensor(num_hyps) + row_splits = torch.cumsum(num_hyps, dim=0, dtype=torch.int32) + ans = k2.ragged.create_ragged_shape2( + row_splits=row_splits, cached_tot_size=row_splits[-1].item() + ) + return ans diff --git a/egs/libricss/SURT/dprnn_zipformer/decode.py b/egs/libricss/SURT/dprnn_zipformer/decode.py new file mode 100755 index 000000000..6abbffe00 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/decode.py @@ -0,0 +1,654 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2022 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./dprnn_zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --use-averaged-model true \ + --exp-dir ./dprnn_zipformer/exp \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) modified beam search +./dprnn_zipformer/decode.py \ + --epoch 30 \ + --avg 9 \ + --use-averaged-model true \ + --exp-dir ./dprnn_zipformer/exp \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 +""" + + +import argparse +import logging +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import LibriCssAsrDataModule +from beam_search import ( + beam_search, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from lhotse.utils import EPSILON +from train import add_model_arguments, get_params, get_surt_model + +from icefall import LmScorer, NgramLm +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + setup_logger, + store_transcripts, + str2bool, + write_surt_error_stats, +) + +OVERLAP_RATIOS = ["0L", "0S", "OV10", "OV20", "OV30", "OV40"] + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="dprnn_zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--lang-dir", + type=Path, + default="data/lang_bpe_500", + help="The lang dir containing word table and LG graph", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + parser.add_argument( + "--save-masks", + type=str2bool, + default=False, + help="""If true, save masks generated by unmixing module.""", + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + batch: dict, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + feature_lens = batch["input_lens"].to(device) + + # Apply the mask encoder + B, T, F = feature.shape + processed = model.mask_encoder(feature) # B,T,F*num_channels + masks = processed.view(B, T, F, params.num_channels).unbind(dim=-1) + x_masked = [feature * m for m in masks] + + masks_dict = {} + if params.save_masks: + # To save the masks, we split them by batch and trim each mask to the length of + # the corresponding feature. We save them in a dict, where the key is the + # cut ID and the value is the mask. + for i in range(B): + mask = torch.cat( + [x_masked[j][i, : feature_lens[i]] for j in range(params.num_channels)], + dim=-1, + ) + mask = mask.cpu().numpy() + masks_dict[batch["cuts"][i].id] = mask + + # Recognition + # Concatenate the inputs along the batch axis + h = torch.cat(x_masked, dim=0) + h_lens = feature_lens.repeat(params.num_channels) + encoder_out, encoder_out_lens = model.encoder(x=h, x_lens=h_lens) + + if model.joint_encoder_layer is not None: + encoder_out = model.joint_encoder_layer(encoder_out) + + def _group_channels(hyps: List[str]) -> List[List[str]]: + """ + Currently we have a batch of size M*B, where M is the number of + channels and B is the batch size. We need to group the hypotheses + into B groups, each of which contains M hypotheses. + + Example: + hyps = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2'] + _group_channels(hyps) = [['a1', 'a2'], ['b1', 'b2'], ['c1', 'c2']] + """ + assert len(hyps) == B * params.num_channels + out_hyps = [] + for i in range(B): + out_hyps.append(hyps[i::B]) + return out_hyps + + hyps = [] + if params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp) + else: + batch_size = encoder_out.size(0) + + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyps.append(sp.decode(hyp)) + + if params.decoding_method == "greedy_search": + return {"greedy_search": _group_channels(hyps)}, masks_dict + else: + return {f"beam_size_{params.beam_size}": _group_channels(hyps)}, masks_dict + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + masks = {} + for batch_idx, batch in enumerate(dl): + cut_ids = [cut.id for cut in batch["cuts"]] + cuts_batch = batch["cuts"] + + hyps_dict, masks_dict = decode_one_batch( + params=params, + model=model, + sp=sp, + ) + masks.update(masks_dict) + + for name, hyps in hyps_dict.items(): + this_batch = [] + for cut_id, hyp_words in zip(cut_ids, hyps): + # Reference is a list of supervision texts sorted by start time. + ref_words = [ + s.text.strip() + for s in sorted( + cuts_batch[cut_id].supervisions, key=lambda s: s.start + ) + ] + this_batch.append((cut_id, ref_words, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(cut_ids) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results, masks_dict + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_surt_error_stats( + f, + f"{test_set_name}-{key}", + results, + enable_log=True, + num_channels=params.num_channels, + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +def save_masks( + params: AttributeDict, + test_set_name: str, + masks: List[torch.Tensor], +): + masks_path = params.res_dir / f"masks-{test_set_name}.txt" + torch.save(masks, masks_path) + logging.info(f"The masks are stored in {masks_path}") + + +@torch.no_grad() +def main(): + parser = get_parser() + LmScorer.add_arguments(parser) + LibriCssAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + args.lang_dir = Path(args.lang_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "modified_beam_search", + ), f"Decoding method {params.decoding_method} is not supported." + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if "beam_search" in params.decoding_method: + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # and are defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_surt_model(params) + assert model.encoder.decode_chunk_size == params.decode_chunk_len // 2, ( + model.encoder.decode_chunk_size, + params.decode_chunk_len, + ) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + libricss = LibriCssAsrDataModule(args) + + dev_cuts = libricss.libricss_cuts(split="dev", type="ihm-mix").to_eager() + dev_cuts_grouped = [dev_cuts.filter(lambda x: ol in x.id) for ol in OVERLAP_RATIOS] + test_cuts = libricss.libricss_cuts(split="test", type="ihm-mix").to_eager() + test_cuts_grouped = [ + test_cuts.filter(lambda x: ol in x.id) for ol in OVERLAP_RATIOS + ] + + for dev_set, ol in zip(dev_cuts_grouped, OVERLAP_RATIOS): + dev_dl = libricss.test_dataloaders(dev_set) + results_dict, masks = decode_dataset( + dl=dev_dl, + params=params, + model=model, + sp=sp, + ) + + save_results( + params=params, + test_set_name=f"dev_{ol}", + results_dict=results_dict, + ) + + if params.save_masks: + save_masks( + params=params, + test_set_name=f"dev_{ol}", + masks=masks, + ) + + for test_set, ol in zip(test_cuts_grouped, OVERLAP_RATIOS): + test_dl = libricss.test_dataloaders(test_set) + results_dict, masks = decode_dataset( + dl=test_dl, + params=params, + model=model, + sp=sp, + ) + + save_results( + params=params, + test_set_name=f"test_{ol}", + results_dict=results_dict, + ) + + if params.save_masks: + save_masks( + params=params, + test_set_name=f"test_{ol}", + masks=masks, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/libricss/SURT/dprnn_zipformer/decoder.py b/egs/libricss/SURT/dprnn_zipformer/decoder.py new file mode 120000 index 000000000..8283d8c5a --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/decoder.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/decoder.py \ No newline at end of file diff --git a/egs/libricss/SURT/dprnn_zipformer/dprnn.py b/egs/libricss/SURT/dprnn_zipformer/dprnn.py new file mode 100644 index 000000000..440dea885 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/dprnn.py @@ -0,0 +1,305 @@ +import random +from typing import Optional, Tuple + +import torch +import torch.nn as nn +from einops import rearrange +from scaling import ActivationBalancer, BasicNorm, DoubleSwish, ScaledLinear, ScaledLSTM +from torch.autograd import Variable + +EPS = torch.finfo(torch.get_default_dtype()).eps + + +def _pad_segment(input, segment_size): + # Source: https://github.com/espnet/espnet/blob/master/espnet2/enh/layers/dprnn.py#L342 + # input is the features: (B, N, T) + batch_size, dim, seq_len = input.shape + segment_stride = segment_size // 2 + + rest = segment_size - (segment_stride + seq_len % segment_size) % segment_size + if rest > 0: + pad = Variable(torch.zeros(batch_size, dim, rest)).type(input.type()) + input = torch.cat([input, pad], 2) + + pad_aux = Variable(torch.zeros(batch_size, dim, segment_stride)).type(input.type()) + input = torch.cat([pad_aux, input, pad_aux], 2) + + return input, rest + + +def split_feature(input, segment_size): + # Source: https://github.com/espnet/espnet/blob/master/espnet2/enh/layers/dprnn.py#L358 + # split the feature into chunks of segment size + # input is the features: (B, N, T) + + input, rest = _pad_segment(input, segment_size) + batch_size, dim, seq_len = input.shape + segment_stride = segment_size // 2 + + segments1 = ( + input[:, :, :-segment_stride] + .contiguous() + .view(batch_size, dim, -1, segment_size) + ) + segments2 = ( + input[:, :, segment_stride:] + .contiguous() + .view(batch_size, dim, -1, segment_size) + ) + segments = ( + torch.cat([segments1, segments2], 3) + .view(batch_size, dim, -1, segment_size) + .transpose(2, 3) + ) + + return segments.contiguous(), rest + + +def merge_feature(input, rest): + # Source: https://github.com/espnet/espnet/blob/master/espnet2/enh/layers/dprnn.py#L385 + # merge the splitted features into full utterance + # input is the features: (B, N, L, K) + + batch_size, dim, segment_size, _ = input.shape + segment_stride = segment_size // 2 + input = ( + input.transpose(2, 3).contiguous().view(batch_size, dim, -1, segment_size * 2) + ) # B, N, K, L + + input1 = ( + input[:, :, :, :segment_size] + .contiguous() + .view(batch_size, dim, -1)[:, :, segment_stride:] + ) + input2 = ( + input[:, :, :, segment_size:] + .contiguous() + .view(batch_size, dim, -1)[:, :, :-segment_stride] + ) + + output = input1 + input2 + if rest > 0: + output = output[:, :, :-rest] + + return output.contiguous() # B, N, T + + +class RNNEncoderLayer(nn.Module): + """ + RNNEncoderLayer is made up of lstm and feedforward networks. + Args: + input_size: + The number of expected features in the input (required). + hidden_size: + The hidden dimension of rnn layer. + dropout: + The dropout value (default=0.1). + layer_dropout: + The dropout value for model-level warmup (default=0.075). + """ + + def __init__( + self, + input_size: int, + hidden_size: int, + dropout: float = 0.1, + bidirectional: bool = False, + ) -> None: + super(RNNEncoderLayer, self).__init__() + self.input_size = input_size + self.hidden_size = hidden_size + + assert hidden_size >= input_size, (hidden_size, input_size) + self.lstm = ScaledLSTM( + input_size=input_size, + hidden_size=hidden_size // 2 if bidirectional else hidden_size, + proj_size=0, + num_layers=1, + dropout=0.0, + batch_first=True, + bidirectional=bidirectional, + ) + self.norm_final = BasicNorm(input_size) + + # try to ensure the output is close to zero-mean (or at least, zero-median). # noqa + self.balancer = ActivationBalancer( + num_channels=input_size, + channel_dim=-1, + min_positive=0.45, + max_positive=0.55, + max_abs=6.0, + ) + self.dropout = nn.Dropout(dropout) + + def forward( + self, + src: torch.Tensor, + states: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, + warmup: float = 1.0, + ) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: + """ + Pass the input through the encoder layer. + Args: + src: + The sequence to the encoder layer (required). + Its shape is (S, N, E), where S is the sequence length, + N is the batch size, and E is the feature number. + states: + A tuple of 2 tensors (optional). It is for streaming inference. + states[0] is the hidden states of all layers, + with shape of (1, N, input_size); + states[1] is the cell states of all layers, + with shape of (1, N, hidden_size). + """ + src_orig = src + + # alpha = 1.0 means fully use this encoder layer, 0.0 would mean + # completely bypass it. + alpha = warmup if self.training else 1.0 + + # lstm module + src_lstm, new_states = self.lstm(src, states) + src = self.dropout(src_lstm) + src + src = self.norm_final(self.balancer(src)) + + if alpha != 1.0: + src = alpha * src + (1 - alpha) * src_orig + + return src + + +# dual-path RNN +class DPRNN(nn.Module): + """Deep dual-path RNN. + Source: https://github.com/espnet/espnet/blob/master/espnet2/enh/layers/dprnn.py + + args: + input_size: int, dimension of the input feature. The input should have shape + (batch, seq_len, input_size). + hidden_size: int, dimension of the hidden state. + output_size: int, dimension of the output size. + dropout: float, dropout ratio. Default is 0. + num_blocks: int, number of stacked RNN layers. Default is 1. + """ + + def __init__( + self, + feature_dim, + input_size, + hidden_size, + output_size, + dropout=0.1, + num_blocks=1, + segment_size=50, + chunk_width_randomization=False, + ): + super().__init__() + + self.input_size = input_size + self.output_size = output_size + self.hidden_size = hidden_size + + self.segment_size = segment_size + self.chunk_width_randomization = chunk_width_randomization + + self.input_embed = nn.Sequential( + ScaledLinear(feature_dim, input_size), + BasicNorm(input_size), + ActivationBalancer( + num_channels=input_size, + channel_dim=-1, + min_positive=0.45, + max_positive=0.55, + ), + ) + + # dual-path RNN + self.row_rnn = nn.ModuleList([]) + self.col_rnn = nn.ModuleList([]) + for _ in range(num_blocks): + # intra-RNN is non-causal + self.row_rnn.append( + RNNEncoderLayer( + input_size, hidden_size, dropout=dropout, bidirectional=True + ) + ) + self.col_rnn.append( + RNNEncoderLayer( + input_size, hidden_size, dropout=dropout, bidirectional=False + ) + ) + + # output layer + self.out_embed = nn.Sequential( + ScaledLinear(input_size, output_size), + BasicNorm(output_size), + ActivationBalancer( + num_channels=output_size, + channel_dim=-1, + min_positive=0.45, + max_positive=0.55, + ), + ) + + def forward(self, input): + # input shape: B, T, F + input = self.input_embed(input) + B, T, D = input.shape + + if self.chunk_width_randomization and self.training: + segment_size = random.randint(self.segment_size // 2, self.segment_size) + else: + segment_size = self.segment_size + input, rest = split_feature(input.transpose(1, 2), segment_size) + # input shape: batch, N, dim1, dim2 + # apply RNN on dim1 first and then dim2 + # output shape: B, output_size, dim1, dim2 + # input = input.to(device) + batch_size, _, dim1, dim2 = input.shape + output = input + for i in range(len(self.row_rnn)): + row_input = ( + output.permute(0, 3, 2, 1) + .contiguous() + .view(batch_size * dim2, dim1, -1) + ) # B*dim2, dim1, N + output = self.row_rnn[i](row_input) # B*dim2, dim1, H + output = ( + output.view(batch_size, dim2, dim1, -1).permute(0, 3, 2, 1).contiguous() + ) # B, N, dim1, dim2 + + col_input = ( + output.permute(0, 2, 3, 1) + .contiguous() + .view(batch_size * dim1, dim2, -1) + ) # B*dim1, dim2, N + output = self.col_rnn[i](col_input) # B*dim1, dim2, H + output = ( + output.view(batch_size, dim1, dim2, -1).permute(0, 3, 1, 2).contiguous() + ) # B, N, dim1, dim2 + + output = merge_feature(output, rest) + output = output.transpose(1, 2) + output = self.out_embed(output) + + # Apply ReLU to the output + output = torch.relu(output) + + return output + + +if __name__ == "__main__": + + model = DPRNN( + 80, + 256, + 256, + 160, + dropout=0.1, + num_blocks=4, + segment_size=32, + chunk_width_randomization=True, + ) + input = torch.randn(2, 1002, 80) + print(sum(p.numel() for p in model.parameters())) + print(model(input).shape) diff --git a/egs/libricss/SURT/dprnn_zipformer/encoder_interface.py b/egs/libricss/SURT/dprnn_zipformer/encoder_interface.py new file mode 120000 index 000000000..0c2673d46 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/encoder_interface.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/encoder_interface.py \ No newline at end of file diff --git a/egs/libricss/SURT/dprnn_zipformer/export.py b/egs/libricss/SURT/dprnn_zipformer/export.py new file mode 100755 index 000000000..f51f2a7ab --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/export.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# +# Copyright 2021 Xiaomi Corporation (Author: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script converts several saved checkpoints +# to a single one using model averaging. +""" + +Usage: + +(1) Export to torchscript model using torch.jit.script() + +./dprnn_zipformer/export.py \ + --exp-dir ./dprnn_zipformer/exp \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 \ + --jit 1 + +It will generate a file `cpu_jit.pt` in the given `exp_dir`. You can later +load it by `torch.jit.load("cpu_jit.pt")`. + +Note `cpu` in the name `cpu_jit.pt` means the parameters when loaded into Python +are on CPU. You can use `to("cuda")` to move them to a CUDA device. + +Check +https://github.com/k2-fsa/sherpa +for how to use the exported models outside of icefall. + +(2) Export `model.state_dict()` + +./dprnn_zipformer/export.py \ + --exp-dir ./dprnn_zipformer/exp \ + --bpe-model data/lang_bpe_500/bpe.model \ + --epoch 30 \ + --avg 9 + +It will generate a file `pretrained.pt` in the given `exp_dir`. You can later +load it by `icefall.checkpoint.load_checkpoint()`. + +To use the generated file with `dprnn_zipformer/decode.py`, +you can do: + + cd /path/to/exp_dir + ln -s pretrained.pt epoch-9999.pt + + cd /path/to/egs/librispeech/ASR + ./dprnn_zipformer/decode.py \ + --exp-dir ./dprnn_zipformer/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bpe_500/bpe.model +""" + +import argparse +import logging +from pathlib import Path + +import sentencepiece as spm +import torch +import torch.nn as nn +from scaling_converter import convert_scaled_to_non_scaled +from train import add_model_arguments, get_params, get_surt_model + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="dprnn_zipformer/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--jit", + type=str2bool, + default=False, + help="""True to save a model after applying torch.jit.script. + It will generate a file named cpu_jit.pt + + Check ./jit_pretrained.py for how to use it. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_surt_model(params) + + model.to(device) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to("cpu") + model.eval() + + if params.jit is True: + convert_scaled_to_non_scaled(model, inplace=True) + # We won't use the forward() method of the model in C++, so just ignore + # it here. + # Otherwise, one of its arguments is a ragged tensor and is not + # torch scriptabe. + model.__class__.forward = torch.jit.ignore(model.__class__.forward) + logging.info("Using torch.jit.script") + model = torch.jit.script(model) + filename = params.exp_dir / "cpu_jit.pt" + model.save(str(filename)) + logging.info(f"Saved to {filename}") + else: + logging.info("Not using torchscript. Export model.state_dict()") + # Save it using a format so that it can be loaded + # by :func:`load_checkpoint` + filename = params.exp_dir / "pretrained.pt" + torch.save({"model": model.state_dict()}, str(filename)) + logging.info(f"Saved to {filename}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/libricss/SURT/dprnn_zipformer/joiner.py b/egs/libricss/SURT/dprnn_zipformer/joiner.py new file mode 120000 index 000000000..0f0c3c90a --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/joiner.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/joiner.py \ No newline at end of file diff --git a/egs/libricss/SURT/dprnn_zipformer/model.py b/egs/libricss/SURT/dprnn_zipformer/model.py new file mode 100644 index 000000000..688e1e78d --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/model.py @@ -0,0 +1,316 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, Wei Kang) +# Copyright 2023 Johns Hopkins University (author: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 typing import List, Optional, Tuple + +import k2 +import torch +import torch.nn as nn +from encoder_interface import EncoderInterface + +from icefall.utils import add_sos + + +class SURT(nn.Module): + """It implements Streaming Unmixing and Recognition Transducer (SURT). + https://arxiv.org/abs/2011.13148 + """ + + def __init__( + self, + mask_encoder: nn.Module, + encoder: EncoderInterface, + joint_encoder_layer: Optional[nn.Module], + decoder: nn.Module, + joiner: nn.Module, + num_channels: int, + encoder_dim: int, + decoder_dim: int, + joiner_dim: int, + vocab_size: int, + ): + """ + Args: + mask_encoder: + It is the masking network. It generates a mask for each channel of the + encoder. These masks are applied to the input features, and then passed + to the transcription network. + encoder: + It is the transcription network in the paper. Its accepts + two inputs: `x` of (N, T, encoder_dim) and `x_lens` of shape (N,). + It returns two tensors: `logits` of shape (N, T, encoder_dm) and + `logit_lens` of shape (N,). + decoder: + It is the prediction network in the paper. Its input shape + is (N, U) and its output shape is (N, U, decoder_dim). + It should contain one attribute: `blank_id`. + joiner: + It has two inputs with shapes: (N, T, encoder_dim) and (N, U, decoder_dim). + Its output shape is (N, T, U, vocab_size). Note that its output contains + unnormalized probs, i.e., not processed by log-softmax. + num_channels: + It is the number of channels that the input features will be split into. + In general, it should be equal to the maximum number of simultaneously + active speakers. For most real scenarios, using 2 channels is sufficient. + """ + super().__init__() + assert isinstance(encoder, EncoderInterface), type(encoder) + assert hasattr(decoder, "blank_id") + + self.mask_encoder = mask_encoder + self.encoder = encoder + self.joint_encoder_layer = joint_encoder_layer + self.decoder = decoder + self.joiner = joiner + self.num_channels = num_channels + + self.simple_am_proj = nn.Linear( + encoder_dim, + vocab_size, + ) + self.simple_lm_proj = nn.Linear(decoder_dim, vocab_size) + + self.ctc_output = nn.Sequential( + nn.Dropout(p=0.1), + nn.Linear(encoder_dim, vocab_size), + nn.LogSoftmax(dim=-1), + ) + + def forward_helper( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + y: k2.RaggedTensor, + prune_range: int = 5, + am_scale: float = 0.0, + lm_scale: float = 0.0, + reduction: str = "sum", + beam_size: int = 10, + use_double_scores: bool = False, + subsampling_factor: int = 1, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Compute transducer loss for one branch of the SURT model. + """ + encoder_out, x_lens = self.encoder(x, x_lens) + assert torch.all(x_lens > 0) + + if self.joint_encoder_layer is not None: + encoder_out = self.joint_encoder_layer(encoder_out) + + # compute ctc log-probs + ctc_output = self.ctc_output(encoder_out) + + # For the decoder, i.e., the prediction network + row_splits = y.shape.row_splits(1) + y_lens = row_splits[1:] - row_splits[:-1] + + blank_id = self.decoder.blank_id + sos_y = add_sos(y, sos_id=blank_id) + + # sos_y_padded: [B, S + 1], start with SOS. + sos_y_padded = sos_y.pad(mode="constant", padding_value=blank_id) + + # decoder_out: [B, S + 1, decoder_dim] + decoder_out = self.decoder(sos_y_padded) + + # Note: y does not start with SOS + # y_padded : [B, S] + y_padded = y.pad(mode="constant", padding_value=0) + + y_padded = y_padded.to(torch.int64) + boundary = torch.zeros((x.size(0), 4), dtype=torch.int64, device=x.device) + boundary[:, 2] = y_lens + boundary[:, 3] = x_lens + + lm = self.simple_lm_proj(decoder_out) + am = self.simple_am_proj(encoder_out) + + with torch.cuda.amp.autocast(enabled=False): + simple_loss, (px_grad, py_grad) = k2.rnnt_loss_smoothed( + lm=lm.float(), + am=am.float(), + symbols=y_padded, + termination_symbol=blank_id, + lm_only_scale=lm_scale, + am_only_scale=am_scale, + boundary=boundary, + reduction=reduction, + return_grad=True, + ) + + # ranges : [B, T, prune_range] + ranges = k2.get_rnnt_prune_ranges( + px_grad=px_grad, + py_grad=py_grad, + boundary=boundary, + s_range=prune_range, + ) + + # am_pruned : [B, T, prune_range, encoder_dim] + # lm_pruned : [B, T, prune_range, decoder_dim] + am_pruned, lm_pruned = k2.do_rnnt_pruning( + am=self.joiner.encoder_proj(encoder_out), + lm=self.joiner.decoder_proj(decoder_out), + ranges=ranges, + ) + + # logits : [B, T, prune_range, vocab_size] + + # project_input=False since we applied the decoder's input projections + # prior to do_rnnt_pruning (this is an optimization for speed). + logits = self.joiner(am_pruned, lm_pruned, project_input=False) + + with torch.cuda.amp.autocast(enabled=False): + pruned_loss = k2.rnnt_loss_pruned( + logits=logits.float(), + symbols=y_padded, + ranges=ranges, + termination_symbol=blank_id, + boundary=boundary, + reduction=reduction, + ) + + # Compute ctc loss + supervision_segments = torch.stack( + ( + torch.arange(len(x_lens), device="cpu"), + torch.zeros_like(x_lens, device="cpu"), + torch.clone(x_lens).detach().cpu(), + ), + dim=1, + ).to(torch.int32) + # We need to sort supervision_segments in decreasing order of num_frames + indices = torch.argsort(supervision_segments[:, 2], descending=True) + supervision_segments = supervision_segments[indices] + + # Works with a BPE model + decoding_graph = k2.ctc_graph(y, modified=False, device=x.device) + dense_fsa_vec = k2.DenseFsaVec( + ctc_output, + supervision_segments, + allow_truncate=subsampling_factor - 1, + ) + ctc_loss = k2.ctc_loss( + decoding_graph=decoding_graph, + dense_fsa_vec=dense_fsa_vec, + output_beam=beam_size, + reduction="none", + use_double_scores=use_double_scores, + ) + + return (simple_loss, pruned_loss, ctc_loss) + + def forward( + self, + x: torch.Tensor, + x_lens: torch.Tensor, + y: k2.RaggedTensor, + prune_range: int = 5, + am_scale: float = 0.0, + lm_scale: float = 0.0, + reduction: str = "sum", + beam_size: int = 10, + use_double_scores: bool = False, + subsampling_factor: int = 1, + return_masks: bool = False, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """ + Args: + x: + A 3-D tensor of shape (N, T, C). + x_lens: + A 1-D tensor of shape (N,). It contains the number of frames in `x` + before padding. + y: + A ragged tensor of shape (N*num_channels, S). It contains the labels + of the N utterances. The labels are in the range [0, vocab_size). All + the channels are concatenated together one after another. + prune_range: + The prune range for rnnt loss, it means how many symbols(context) + we are considering for each frame to compute the loss. + am_scale: + The scale to smooth the loss with am (output of encoder network) + part + lm_scale: + The scale to smooth the loss with lm (output of predictor network) + part + reduction: + "sum" to sum the losses over all utterances in the batch. + "none" to return the loss in a 1-D tensor for each utterance + in the batch. + beam_size: + The beam size used in CTC decoding. + use_double_scores: + If True, use double precision for CTC decoding. + subsampling_factor: + The subsampling factor of the model. It is used to compute the + supervision segments for CTC loss. + return_masks: + If True, return the masks as well as masked features. + Returns: + Return the transducer loss. + + Note: + Regarding am_scale & lm_scale, it will make the loss-function one of + the form: + lm_scale * lm_probs + am_scale * am_probs + + (1-lm_scale-am_scale) * combined_probs + """ + assert x.ndim == 3, x.shape + assert x_lens.ndim == 1, x_lens.shape + assert y.num_axes == 2, y.num_axes + + assert x.size(0) == x_lens.size(0), (x.size(), x_lens.size()) + + # Apply the mask encoder + B, T, F = x.shape + processed = self.mask_encoder(x) # B,T,F*num_channels + masks = processed.view(B, T, F, self.num_channels).unbind(dim=-1) + x_masked = [x * m for m in masks] + + # Recognition + # Stack the inputs along the batch axis + h = torch.cat(x_masked, dim=0) + h_lens = torch.cat([x_lens for _ in range(self.num_channels)], dim=0) + + simple_loss, pruned_loss, ctc_loss = self.forward_helper( + h, + h_lens, + y, + prune_range, + am_scale, + lm_scale, + reduction=reduction, + beam_size=beam_size, + use_double_scores=use_double_scores, + subsampling_factor=subsampling_factor, + ) + + # Chunks the outputs into 2 parts along batch axis and then stack them along a new axis. + simple_loss = torch.stack( + torch.chunk(simple_loss, self.num_channels, dim=0), dim=0 + ) + pruned_loss = torch.stack( + torch.chunk(pruned_loss, self.num_channels, dim=0), dim=0 + ) + ctc_loss = torch.stack(torch.chunk(ctc_loss, self.num_channels, dim=0), dim=0) + + if return_masks: + return (simple_loss, pruned_loss, ctc_loss, x_masked, masks) + else: + return (simple_loss, pruned_loss, ctc_loss, x_masked) diff --git a/egs/libricss/SURT/dprnn_zipformer/optim.py b/egs/libricss/SURT/dprnn_zipformer/optim.py new file mode 120000 index 000000000..8a05abb5f --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/optim.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/optim.py \ No newline at end of file diff --git a/egs/libricss/SURT/dprnn_zipformer/scaling.py b/egs/libricss/SURT/dprnn_zipformer/scaling.py new file mode 120000 index 000000000..5f9be9fe0 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/scaling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/scaling.py \ No newline at end of file diff --git a/egs/libricss/SURT/dprnn_zipformer/scaling_converter.py b/egs/libricss/SURT/dprnn_zipformer/scaling_converter.py new file mode 120000 index 000000000..f9960e5c6 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/scaling_converter.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/scaling_converter.py \ No newline at end of file diff --git a/egs/libricss/SURT/dprnn_zipformer/train.py b/egs/libricss/SURT/dprnn_zipformer/train.py new file mode 100755 index 000000000..6598f8b5d --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/train.py @@ -0,0 +1,1452 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo,) +# Zengwei Yao) +# 2023 Johns Hopkins University (author: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +cd egs/libricss/SURT +./prepare.sh + +./dprnn_zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --exp-dir dprnn_zipformer/exp \ + --max-duration 300 + +# For mix precision training: + +./dprnn_zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir dprnn_zipformer/exp \ + --max-duration 550 +""" + +import argparse +import copy +import logging +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import LibriCssAsrDataModule +from decoder import Decoder +from dprnn import DPRNN +from einops.layers.torch import Rearrange +from graph_pit.loss.optimized import optimized_graph_pit_mse_loss as gpit_mse +from joiner import Joiner +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import LOG_EPSILON, fix_random_seed +from model import SURT +from optim import Eden, ScaledAdam +from scaling import ScaledLSTM +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.utils import AttributeDict, MetricsTracker, setup_logger, str2bool + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for module in model.modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-mask-encoder-layers", + type=int, + default=4, + help="Number of layers in the DPRNN based mask encoder.", + ) + + parser.add_argument( + "--mask-encoder-dim", + type=int, + default=256, + help="Hidden dimension of the LSTM blocks in DPRNN.", + ) + + parser.add_argument( + "--mask-encoder-segment-size", + type=int, + default=32, + help="Segment size of the SegLSTM in DPRNN. Ideally, this should be equal to the " + "decode-chunk-length of the zipformer encoder.", + ) + + parser.add_argument( + "--chunk-width-randomization", + type=bool, + default=False, + help="Whether to randomize the chunk width in DPRNN.", + ) + + # Zipformer config is based on: + # https://github.com/k2-fsa/icefall/pull/745#issuecomment-1405282740 + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,2,2,2", + help="Number of zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--feedforward-dims", + type=str, + default="768,768,768,768,768", + help="Feedforward dimension of the zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--nhead", + type=str, + default="8,8,8,8,8", + help="Number of attention heads in the zipformer encoder layers.", + ) + + parser.add_argument( + "--encoder-dims", + type=str, + default="256,256,256,256,256", + help="Embedding dimension in the 2 blocks of zipformer encoder layers, comma separated", + ) + + parser.add_argument( + "--attention-dims", + type=str, + default="192,192,192,192,192", + help="""Attention dimension in the 2 blocks of zipformer encoder layers, comma separated; + not the same as embedding dimension.""", + ) + + parser.add_argument( + "--encoder-unmasked-dims", + type=str, + default="192,192,192,192,192", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance " + " worse.", + ) + + parser.add_argument( + "--zipformer-downsampling-factors", + type=str, + default="1,2,4,8,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--cnn-module-kernels", + type=str, + default="31,31,31,31,31", + help="Sizes of kernels in convolution modules", + ) + + parser.add_argument( + "--use-joint-encoder-layer", + type=str, + default="lstm", + choices=["linear", "lstm", "none"], + help="Whether to use a joint layer to combine all branches.", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--short-chunk-size", + type=int, + default=50, + help="""Chunk length of dynamic training, the chunk size would be either + max sequence length of current batch or uniformly sampled from (1, short_chunk_size). + """, + ) + + parser.add_argument( + "--num-left-chunks", + type=int, + default=4, + help="How many left context can be seen in chunks when calculating attention.", + ) + + parser.add_argument( + "--decode-chunk-len", + type=int, + default=32, + help="The chunk size for decoding (in frames before subsampling)", + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="conv_lstm_transducer_stateless_ctc/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--model-init-ckpt", + type=str, + default=None, + help="""The model checkpoint to initialize the model (either full or part). + If not specified, the model is randomly initialized. + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--base-lr", type=float, default=0.004, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=5000, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=6, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network) part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--ctc-loss-scale", + type=float, + default=0.2, + help="Scale for CTC loss.", + ) + + parser.add_argument( + "--heat-loss-scale", + type=float, + default=0.0, + help="Scale for HEAT loss on separated sources.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=2000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=1, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=100, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warm_step for Noam optimizer. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 2000, + # parameters for SURT + "num_channels": 2, + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed + # parameters for Noam + "model_warm_step": 5000, # arg given to model, not for lrate + # parameters for ctc loss + "beam_size": 10, + "use_double_scores": True, + "env_info": get_env_info(), + } + ) + + return params + + +def get_mask_encoder_model(params: AttributeDict) -> nn.Module: + mask_encoder = DPRNN( + feature_dim=params.feature_dim, + input_size=params.mask_encoder_dim, + hidden_size=params.mask_encoder_dim, + output_size=params.feature_dim * params.num_channels, + segment_size=params.mask_encoder_segment_size, + num_blocks=params.num_mask_encoder_layers, + chunk_width_randomization=params.chunk_width_randomization, + ) + return mask_encoder + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + # TODO: We can add an option to switch between Zipformer and Transformer + def to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + encoder = Zipformer( + num_features=params.feature_dim, + output_downsampling_factor=2, + zipformer_downsampling_factors=to_int_tuple( + params.zipformer_downsampling_factors + ), + encoder_dims=to_int_tuple(params.encoder_dims), + attention_dim=to_int_tuple(params.attention_dims), + encoder_unmasked_dims=to_int_tuple(params.encoder_unmasked_dims), + nhead=to_int_tuple(params.nhead), + feedforward_dim=to_int_tuple(params.feedforward_dims), + cnn_module_kernels=to_int_tuple(params.cnn_module_kernels), + num_encoder_layers=to_int_tuple(params.num_encoder_layers), + num_left_chunks=params.num_left_chunks, + short_chunk_size=params.short_chunk_size, + decode_chunk_size=params.decode_chunk_len // 2, + ) + return encoder + + +def get_joint_encoder_layer(params: AttributeDict) -> nn.Module: + class TakeFirst(nn.Module): + def forward(self, x): + return x[0] + + if params.use_joint_encoder_layer == "linear": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + nn.Linear( + params.num_channels * encoder_dim, params.num_channels * encoder_dim + ), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "lstm": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + ScaledLSTM( + input_size=params.num_channels * encoder_dim, + hidden_size=params.num_channels * encoder_dim, + num_layers=1, + bias=True, + batch_first=True, + dropout=0.0, + bidirectional=False, + ), + TakeFirst(), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "none": + joint_layer = None + else: + raise ValueError( + f"Unknown joint encoder layer type: {params.use_joint_encoder_layer}" + ) + return joint_layer + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_surt_model( + params: AttributeDict, +) -> nn.Module: + mask_encoder = get_mask_encoder_model(params) + encoder = get_encoder_model(params) + joint_layer = get_joint_encoder_layer(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = SURT( + mask_encoder=mask_encoder, + encoder=encoder, + joint_encoder_layer=joint_layer, + decoder=decoder, + joiner=joiner, + num_channels=params.num_channels, + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_heat_loss(x_masked, batch, num_channels=2) -> Tensor: + """ + Compute HEAT loss for separated sources using the output of mask encoder. + Args: + x_masked: + The output of mask encoder. It is a tensor of shape (B, T, C). + batch: + A batch of data. See `lhotse.dataset.K2SurtDatasetWithSources()` + for the content in it. + num_channels: + The number of output branches in the SURT model. + """ + B, T, D = x_masked[0].shape + device = x_masked[0].device + + # Create training targets for each channel. + targets = [] + for i in range(num_channels): + target = torch.ones_like(x_masked[i]) * LOG_EPSILON + targets.append(target) + + source_feats = batch["source_feats"] + source_boundaries = batch["source_boundaries"] + input_lens = batch["input_lens"].to(device) + # Assign sources to channels based on the HEAT criteria + for b in range(B): + cut_source_feats = source_feats[b] + cut_source_boundaries = source_boundaries[b] + last_seg_end = [0 for _ in range(num_channels)] + for source_feat, (start, end) in zip(cut_source_feats, cut_source_boundaries): + assigned = False + for i in range(num_channels): + if start >= last_seg_end[i]: + targets[i][b, start:end, :] += source_feat.to(device) + last_seg_end[i] = max(end, last_seg_end[i]) + assigned = True + break + if not assigned: + min_end_channel = last_seg_end.index(min(last_seg_end)) + targets[min_end_channel][b, start:end, :] += source_feat + last_seg_end[min_end_channel] = max(end, last_seg_end[min_end_channel]) + + # Get padding mask based on input lengths + pad_mask = torch.arange(T, device=device).expand(B, T) > input_lens.unsqueeze(1) + pad_mask = pad_mask.unsqueeze(-1) + + # Compute masked loss for each channel + losses = torch.zeros((num_channels, B, T, D), device=device) + for i in range(num_channels): + loss = nn.functional.mse_loss(x_masked[i], targets[i], reduction="none") + # Apply padding mask to loss + loss.masked_fill_(pad_mask, 0) + losses[i] = loss + + # loss: C x B x T x D. pad_mask: B x T x 1 + # We want to compute loss for each item in the batch. Each item has loss given + # by the sum over C, and average over T and D. For T, we need to use the padding. + loss = losses.sum(0).mean(-1).sum(-1) / batch["input_lens"].to(device) + return loss + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute RNN-T loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Conformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"].to(device) + feature_lens = batch["input_lens"].to(device) + + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + + # The dataloader returns text as a list of cuts, each of which is a list of channel + # text. We flatten this to a list where all channels are together, i.e., it looks like + # [utt1_ch1, utt2_ch1, ..., uttN_ch1, utt1_ch2, ...., uttN,ch2]. + text = [val for tup in zip(*batch["text"]) for val in tup] + assert len(text) == len(feature) * params.num_channels + + # Convert all channel texts to token IDs and create a ragged tensor. + y = sp.encode(text, out_type=int) + y = k2.RaggedTensor(y).to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.model_warm_step + + with torch.set_grad_enabled(is_training): + (simple_loss, pruned_loss, ctc_loss, x_masked) = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + reduction="none", + subsampling_factor=params.subsampling_factor, + ) + simple_loss_is_finite = torch.isfinite(simple_loss) + pruned_loss_is_finite = torch.isfinite(pruned_loss) + ctc_loss_is_finite = torch.isfinite(ctc_loss) + + # Compute HEAT loss + if is_training and params.heat_loss_scale > 0.0: + heat_loss = compute_heat_loss( + x_masked, batch, num_channels=params.num_channels + ) + else: + heat_loss = torch.tensor(0.0, device=device) + + heat_loss_is_finite = torch.isfinite(heat_loss) + is_finite = ( + simple_loss_is_finite + & pruned_loss_is_finite + & ctc_loss_is_finite + & heat_loss_is_finite + ) + if not torch.all(is_finite): + logging.info( + "Not all losses are finite!\n" + f"simple_losses: {simple_loss}\n" + f"pruned_losses: {pruned_loss}\n" + f"ctc_losses: {ctc_loss}\n" + f"heat_losses: {heat_loss}\n" + ) + display_and_save_batch(batch, params=params, sp=sp) + simple_loss = simple_loss[simple_loss_is_finite] + pruned_loss = pruned_loss[pruned_loss_is_finite] + ctc_loss = ctc_loss[ctc_loss_is_finite] + heat_loss = heat_loss[heat_loss_is_finite] + + # If either all simple_loss or pruned_loss is inf or nan, + # we stop the training process by raising an exception + if ( + torch.all(~simple_loss_is_finite) + or torch.all(~pruned_loss_is_finite) + or torch.all(~ctc_loss_is_finite) + or torch.all(~heat_loss_is_finite) + ): + raise ValueError( + "There are too many utterances in this batch " + "leading to inf or nan losses." + ) + + simple_loss_sum = simple_loss.sum() + pruned_loss_sum = pruned_loss.sum() + ctc_loss_sum = ctc_loss.sum() + heat_loss_sum = heat_loss.sum() + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss = ( + simple_loss_scale * simple_loss_sum + + pruned_loss_scale * pruned_loss_sum + + params.ctc_loss_scale * ctc_loss_sum + + params.heat_loss_scale * heat_loss_sum + ) + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # info["frames"] is an approximate number for two reasons: + # (1) The acutal subsampling factor is ((lens - 1) // 2 - 1) // 2 + # (2) If some utterances in the batch lead to inf/nan loss, they + # are filtered out. + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # `utt_duration` and `utt_pad_proportion` would be normalized by `utterances` # noqa + info["utterances"] = feature.size(0) + # averaged input duration in frames over utterances + info["utt_duration"] = feature_lens.sum().item() + # averaged padding proportion over utterances + info["utt_pad_proportion"] = ( + ((feature.size(1) - feature_lens) / feature.size(1)).sum().item() + ) + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss_sum.detach().cpu().item() + info["pruned_loss"] = pruned_loss_sum.detach().cpu().item() + if params.ctc_loss_scale > 0.0: + info["ctc_loss"] = ctc_loss_sum.detach().cpu().item() + if params.heat_loss_scale > 0.0: + info["heat_loss"] = heat_loss_sum.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + train_dl_warmup: Optional[torch.utils.data.DataLoader], + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + train_dl_warmup: + Dataloader for the training dataset with 2 speakers. This is used during the + warmup stage. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + torch.cuda.empty_cache() + model.train() + + tot_loss = MetricsTracker() + + iter_train = iter(train_dl) + iter_train_warmup = iter(train_dl_warmup) if train_dl_warmup is not None else None + + batch_idx = 0 + + while True: + # We first sample a batch from the main dataset. This is because we want to + # make sure all epochs have the same number of batches. + try: + batch = next(iter_train) + except StopIteration: + break + + # If we are in warmup stage, get the batch from the warmup dataset. + if ( + params.batch_idx_train <= params.model_warm_step + and iter_train_warmup is not None + ): + try: + batch = next(iter_train_warmup) + except StopIteration: + iter_train_warmup = iter(train_dl_warmup) + batch = next(iter_train_warmup) + + batch_idx += 1 + + params.batch_idx_train += 1 + batch_size = batch["inputs"].shape[0] + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + set_batch_count(model, params.batch_idx_train) + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + if cur_grad_scale < 1.0 or (cur_grad_scale < 8.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_surt_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + + if checkpoints is None and params.model_init_ckpt is not None: + logging.info( + f"Initializing model with checkpoint from {params.model_init_ckpt}" + ) + init_ckpt = torch.load(params.model_init_ckpt, map_location=device) + model.load_state_dict(init_ckpt["model"], strict=False) + + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + diagnostic = diagnostics.attach_diagnostics(model) + + libricss = LibriCssAsrDataModule(args) + + train_cuts = libricss.lsmix_cuts(rvb_affix="comb", type_affix="full", sources=True) + train_cuts_ov40 = libricss.lsmix_cuts( + rvb_affix="comb", type_affix="ov40", sources=True + ) + dev_cuts = libricss.libricss_cuts(split="dev", type="sdm") + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = libricss.train_dataloaders( + train_cuts, + sampler_state_dict=sampler_state_dict, + ) + train_dl_ov40 = libricss.train_dataloaders(train_cuts_ov40) + valid_dl = libricss.valid_dataloaders(dev_cuts) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + train_dl_warmup=train_dl_ov40, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = [sp.encode(text_ch) for text_ch in batch["text"]] + num_tokens = [sum(len(yi) for yi in y_ch) for y_ch in y] + logging.info(f"num tokens: {num_tokens}") + + +def main(): + parser = get_parser() + LibriCssAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") + +if __name__ == "__main__": + main() diff --git a/egs/libricss/SURT/dprnn_zipformer/train_adapt.py b/egs/libricss/SURT/dprnn_zipformer/train_adapt.py new file mode 100755 index 000000000..1c1b0c28c --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/train_adapt.py @@ -0,0 +1,1343 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo,) +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES=0 + +./dprnn_zipformer/train.py \ + --world-size 1 \ + --num-epochs 15 \ + --start-epoch 1 \ + --exp-dir dprnn_zipformer/exp \ + --max-duration 300 + +# For mix precision training: + +./dprnn_zipformer/train.py \ + --world-size 1 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir dprnn_zipformer/exp \ + --max-duration 550 +""" + +import argparse +import copy +import logging +import warnings +from itertools import chain +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import LibriCssAsrDataModule +from decoder import Decoder +from dprnn import DPRNN +from einops.layers.torch import Rearrange +from joiner import Joiner +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import LOG_EPSILON, fix_random_seed +from model import SURT +from optim import Eden, ScaledAdam +from scaling import ScaledLinear, ScaledLSTM +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.utils import AttributeDict, MetricsTracker, setup_logger, str2bool + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for module in model.modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-mask-encoder-layers", + type=int, + default=4, + help="Number of layers in the DPRNN based mask encoder.", + ) + + parser.add_argument( + "--mask-encoder-dim", + type=int, + default=256, + help="Hidden dimension of the LSTM blocks in DPRNN.", + ) + + parser.add_argument( + "--mask-encoder-segment-size", + type=int, + default=32, + help="Segment size of the SegLSTM in DPRNN. Ideally, this should be equal to the " + "decode-chunk-length of the zipformer encoder.", + ) + + parser.add_argument( + "--chunk-width-randomization", + type=bool, + default=False, + help="Whether to randomize the chunk width in DPRNN.", + ) + + # Zipformer config is based on: + # https://github.com/k2-fsa/icefall/pull/745#issuecomment-1405282740 + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,2,2,2", + help="Number of zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--feedforward-dims", + type=str, + default="768,768,768,768,768", + help="Feedforward dimension of the zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--nhead", + type=str, + default="8,8,8,8,8", + help="Number of attention heads in the zipformer encoder layers.", + ) + + parser.add_argument( + "--encoder-dims", + type=str, + default="256,256,256,256,256", + help="Embedding dimension in the 2 blocks of zipformer encoder layers, comma separated", + ) + + parser.add_argument( + "--attention-dims", + type=str, + default="192,192,192,192,192", + help="""Attention dimension in the 2 blocks of zipformer encoder layers, comma separated; + not the same as embedding dimension.""", + ) + + parser.add_argument( + "--encoder-unmasked-dims", + type=str, + default="192,192,192,192,192", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance " + " worse.", + ) + + parser.add_argument( + "--zipformer-downsampling-factors", + type=str, + default="1,2,4,8,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--cnn-module-kernels", + type=str, + default="31,31,31,31,31", + help="Sizes of kernels in convolution modules", + ) + + parser.add_argument( + "--use-joint-encoder-layer", + type=str, + default="lstm", + choices=["linear", "lstm", "none"], + help="Whether to use a joint layer to combine all branches.", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--short-chunk-size", + type=int, + default=50, + help="""Chunk length of dynamic training, the chunk size would be either + max sequence length of current batch or uniformly sampled from (1, short_chunk_size). + """, + ) + + parser.add_argument( + "--num-left-chunks", + type=int, + default=4, + help="How many left context can be seen in chunks when calculating attention.", + ) + + parser.add_argument( + "--decode-chunk-len", + type=int, + default=32, + help="The chunk size for decoding (in frames before subsampling)", + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=15, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="conv_lstm_transducer_stateless_ctc/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--model-init-ckpt", + type=str, + default=None, + help="""The model checkpoint to initialize the model (either full or part). + If not specified, the model is randomly initialized. + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--base-lr", type=float, default=0.0004, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=1000, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=2, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network) part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--ctc-loss-scale", + type=float, + default=0.2, + help="Scale for CTC loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=1000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=5, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=100, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warm_step for Noam optimizer. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 10, + "reset_interval": 200, + "valid_interval": 100, + # parameters for SURT + "num_channels": 2, + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed + # parameters for Noam + "model_warm_step": 5000, # arg given to model, not for lrate + # parameters for ctc loss + "beam_size": 10, + "use_double_scores": True, + "env_info": get_env_info(), + } + ) + + return params + + +def get_mask_encoder_model(params: AttributeDict) -> nn.Module: + mask_encoder = DPRNN( + feature_dim=params.feature_dim, + input_size=params.mask_encoder_dim, + hidden_size=params.mask_encoder_dim, + output_size=params.feature_dim * params.num_channels, + segment_size=params.mask_encoder_segment_size, + num_blocks=params.num_mask_encoder_layers, + chunk_width_randomization=params.chunk_width_randomization, + ) + return mask_encoder + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + # TODO: We can add an option to switch between Zipformer and Transformer + def to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + encoder = Zipformer( + num_features=params.feature_dim, + output_downsampling_factor=2, + zipformer_downsampling_factors=to_int_tuple( + params.zipformer_downsampling_factors + ), + encoder_dims=to_int_tuple(params.encoder_dims), + attention_dim=to_int_tuple(params.attention_dims), + encoder_unmasked_dims=to_int_tuple(params.encoder_unmasked_dims), + nhead=to_int_tuple(params.nhead), + feedforward_dim=to_int_tuple(params.feedforward_dims), + cnn_module_kernels=to_int_tuple(params.cnn_module_kernels), + num_encoder_layers=to_int_tuple(params.num_encoder_layers), + num_left_chunks=params.num_left_chunks, + short_chunk_size=params.short_chunk_size, + decode_chunk_size=params.decode_chunk_len // 2, + ) + return encoder + + +def get_joint_encoder_layer(params: AttributeDict) -> nn.Module: + class TakeFirst(nn.Module): + def forward(self, x): + return x[0] + + if params.use_joint_encoder_layer == "linear": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + nn.Linear( + params.num_channels * encoder_dim, params.num_channels * encoder_dim + ), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "lstm": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + ScaledLSTM( + input_size=params.num_channels * encoder_dim, + hidden_size=params.num_channels * encoder_dim, + num_layers=1, + bias=True, + batch_first=True, + dropout=0.0, + bidirectional=False, + ), + TakeFirst(), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "none": + joint_layer = None + else: + raise ValueError( + f"Unknown joint encoder layer type: {params.use_joint_encoder_layer}" + ) + return joint_layer + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_surt_model( + params: AttributeDict, +) -> nn.Module: + mask_encoder = get_mask_encoder_model(params) + encoder = get_encoder_model(params) + joint_layer = get_joint_encoder_layer(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = SURT( + mask_encoder=mask_encoder, + encoder=encoder, + joint_encoder_layer=joint_layer, + decoder=decoder, + joiner=joiner, + num_channels=params.num_channels, + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute RNN-T loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Conformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"].to(device) + feature_lens = batch["input_lens"].to(device) + + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + + # The dataloader returns text as a list of cuts, each of which is a list of channel + # text. We flatten this to a list where all channels are together, i.e., it looks like + # [utt1_ch1, utt2_ch1, ..., uttN_ch1, utt1_ch2, ...., uttN,ch2]. + text = [val for tup in zip(*batch["text"]) for val in tup] + assert len(text) == len(feature) * params.num_channels + + # Convert all channel texts to token IDs and create a ragged tensor. + y = sp.encode(text, out_type=int) + y = k2.RaggedTensor(y).to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.model_warm_step + + with torch.set_grad_enabled(is_training): + (simple_loss, pruned_loss, ctc_loss, x_masked) = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + reduction="none", + subsampling_factor=params.subsampling_factor, + ) + simple_loss_is_finite = torch.isfinite(simple_loss) + pruned_loss_is_finite = torch.isfinite(pruned_loss) + ctc_loss_is_finite = torch.isfinite(ctc_loss) + + is_finite = simple_loss_is_finite & pruned_loss_is_finite & ctc_loss_is_finite + if not torch.all(is_finite): + logging.info( + "Not all losses are finite!\n" + f"simple_losses: {simple_loss}\n" + f"pruned_losses: {pruned_loss}\n" + f"ctc_losses: {ctc_loss}\n" + ) + display_and_save_batch(batch, params=params, sp=sp) + simple_loss = simple_loss[simple_loss_is_finite] + pruned_loss = pruned_loss[pruned_loss_is_finite] + ctc_loss = ctc_loss[ctc_loss_is_finite] + + # If either all simple_loss or pruned_loss is inf or nan, + # we stop the training process by raising an exception + if ( + torch.all(~simple_loss_is_finite) + or torch.all(~pruned_loss_is_finite) + or torch.all(~ctc_loss_is_finite) + ): + raise ValueError( + "There are too many utterances in this batch " + "leading to inf or nan losses." + ) + + simple_loss_sum = simple_loss.sum() + pruned_loss_sum = pruned_loss.sum() + ctc_loss_sum = ctc_loss.sum() + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss = ( + simple_loss_scale * simple_loss_sum + + pruned_loss_scale * pruned_loss_sum + + params.ctc_loss_scale * ctc_loss_sum + ) + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # info["frames"] is an approximate number for two reasons: + # (1) The acutal subsampling factor is ((lens - 1) // 2 - 1) // 2 + # (2) If some utterances in the batch lead to inf/nan loss, they + # are filtered out. + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # `utt_duration` and `utt_pad_proportion` would be normalized by `utterances` # noqa + info["utterances"] = feature.size(0) + # averaged input duration in frames over utterances + info["utt_duration"] = feature_lens.sum().item() + # averaged padding proportion over utterances + info["utt_pad_proportion"] = ( + ((feature.size(1) - feature_lens) / feature.size(1)).sum().item() + ) + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss_sum.detach().cpu().item() + info["pruned_loss"] = pruned_loss_sum.detach().cpu().item() + if params.ctc_loss_scale > 0.0: + info["ctc_loss"] = ctc_loss_sum.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + train_dl_warmup: + Dataloader for the training dataset with 2 speakers. This is used during the + warmup stage. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + torch.cuda.empty_cache() + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = batch["inputs"].shape[0] + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + set_batch_count(model, params.batch_idx_train) + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + if cur_grad_scale < 1.0 or (cur_grad_scale < 8.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_surt_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + + if checkpoints is None and params.model_init_ckpt is not None: + logging.info( + f"Initializing model with checkpoint from {params.model_init_ckpt}" + ) + init_ckpt = torch.load(params.model_init_ckpt, map_location=device) + model.load_state_dict(init_ckpt["model"], strict=True) + + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + diagnostic = diagnostics.attach_diagnostics(model) + + libricss = LibriCssAsrDataModule(args) + + train_cuts_ihm = libricss.libricss_cuts(split="dev", type="ihm-mix") + train_cuts_sdm = libricss.libricss_cuts(split="dev", type="sdm") + train_cuts = train_cuts_ihm + train_cuts_sdm + + # This will create 2 copies of the sessions with different segmentation + train_cuts = train_cuts.trim_to_supervision_groups( + max_pause=0.1 + ) + train_cuts.trim_to_supervision_groups(max_pause=0.5) + dev_cuts = libricss.libricss_cuts(split="dev", type="sdm") + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = libricss.train_dataloaders( + train_cuts, + sampler_state_dict=sampler_state_dict, + return_sources=False, + strict=False, + ) + valid_dl = libricss.valid_dataloaders(dev_cuts) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = [sp.encode(text_ch) for text_ch in batch["text"]] + num_tokens = [sum(len(yi) for yi in y_ch) for y_ch in y] + logging.info(f"num tokens: {num_tokens}") + + +def main(): + parser = get_parser() + LibriCssAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") + +if __name__ == "__main__": + main() diff --git a/egs/libricss/SURT/dprnn_zipformer/zipformer.py b/egs/libricss/SURT/dprnn_zipformer/zipformer.py new file mode 120000 index 000000000..ec183baa7 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/zipformer.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer.py \ No newline at end of file diff --git a/egs/libricss/SURT/heat.png b/egs/libricss/SURT/heat.png new file mode 100644 index 0000000000000000000000000000000000000000..ac7ecfff4937c566d0d9d396e2f6d246d833f879 GIT binary patch literal 305340 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 5) + .compute_and_store_features_batch( + extractor=extractor, + storage_path=output_dir / "musan_feats", + manifest_path=musan_cuts_path, + batch_duration=500, + num_workers=4, + storage_type=LilcomChunkyWriter, + ) + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + compute_fbank_musan() diff --git a/egs/libricss/SURT/prepare.sh b/egs/libricss/SURT/prepare.sh new file mode 100755 index 000000000..028240e44 --- /dev/null +++ b/egs/libricss/SURT/prepare.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash + +set -eou pipefail + +stage=-1 +stop_stage=100 + +# We assume dl_dir (download dir) contains the following +# directories and files. If not, they will be downloaded +# by this script automatically. +# +# - $dl_dir/librispeech +# You can find audio and transcripts for LibriSpeech in this path. +# +# - $dl_dir/libricss +# You can find audio and transcripts for LibriCSS in this path. +# +# - $dl_dir/musan +# This directory contains the following directories downloaded from +# http://www.openslr.org/17/ +# +# - music +# - noise +# - speech +# +# - $dl_dir/rirs_noises +# This directory contains the RIRS_NOISES corpus downloaded from https://openslr.org/28/. +# +dl_dir=$PWD/download + +. shared/parse_options.sh || exit 1 + +# All files generated by this script are saved in "data". +# You can safely remove "data" and rerun this script to regenerate it. +mkdir -p data +vocab_size=500 + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +log "dl_dir: $dl_dir" + +if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then + log "Stage 0: Download data" + + # If you have pre-downloaded it to /path/to/librispeech, + # you can create a symlink + # + # ln -sfv /path/to/librispeech $dl_dir/librispeech + # + if [ ! -d $dl_dir/librispeech ]; then + lhotse download librispeech $dl_dir/librispeech + fi + + # If you have pre-downloaded it to /path/to/libricss, + # you can create a symlink + # + # ln -sfv /path/to/libricss $dl_dir/libricss + # + if [ ! -d $dl_dir/libricss ]; then + lhotse download libricss $dl_dir/libricss + fi + + # If you have pre-downloaded it to /path/to/musan, + # you can create a symlink + # + # ln -sfv /path/to/musan $dl_dir/ + # + if [ ! -d $dl_dir/musan ]; then + lhotse download musan $dl_dir + fi + + # If you have pre-downloaded it to /path/to/rirs_noises, + # you can create a symlink + # + # ln -sfv /path/to/rirs_noises $dl_dir/ + # + if [ ! -d $dl_dir/rirs_noises ]; then + lhotse download rirs_noises $dl_dir + fi +fi + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + log "Stage 1: Prepare LibriSpeech manifests" + # We assume that you have downloaded the LibriSpeech corpus + # to $dl_dir/librispeech. We perform text normalization for the transcripts. + # NOTE: Alignments are required for this recipe. + mkdir -p data/manifests + lhotse prepare librispeech -p train-clean-100 -p train-clean-360 -p train-other-500 -p dev-clean \ + -j 4 --alignments-dir $dl_dir/libri_alignments/LibriSpeech $dl_dir/librispeech data/manifests/ +fi + +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + log "Stage 2: Prepare LibriCSS manifests" + # We assume that you have downloaded the LibriCSS corpus + # to $dl_dir/libricss. We perform text normalization for the transcripts. + mkdir -p data/manifests + for mic in sdm ihm-mix; do + lhotse prepare libricss --type $mic --segmented $dl_dir/libricss data/manifests/ + done +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + log "Stage 3: Prepare musan manifest and RIRs" + # We assume that you have downloaded the musan corpus + # to $dl_dir/musan + mkdir -p data/manifests + lhotse prepare musan $dl_dir/musan data/manifests + + # We assume that you have downloaded the RIRS_NOISES corpus + # to $dl_dir/rirs_noises + lhotse prepare rir-noise -p real_rir -p iso_noise $dl_dir/rirs_noises data/manifests +fi + +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + log "Stage 4: Extract features for LibriSpeech, trim to alignments, and shuffle the cuts" + python local/compute_fbank_librispeech.py + lhotse combine data/manifests/librispeech_cuts_train* - |\ + lhotse cut trim-to-alignments --type word --max-pause 0.2 - - |\ + shuf | gzip -c > data/manifests/librispeech_cuts_train_trimmed.jsonl.gz +fi + +if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then + log "Stage 5: Create simulated mixtures from LibriSpeech (train and dev). This may take a while." + # We create a high overlap set which will be used during the model warmup phase, and a + # full training set that will be used for the subsequent training. + + gunzip -c data/manifests/libricss-sdm_supervisions_all.jsonl.gz |\ + grep -v "0L" | grep -v "OV10" |\ + gzip -c > data/manifests/libricss-sdm_supervisions_all_v1.jsonl.gz + + gunzip -c data/manifests/libricss-sdm_supervisions_all.jsonl.gz |\ + grep "OV40" |\ + gzip -c > data/manifests/libricss-sdm_supervisions_ov40.jsonl.gz + + # Warmup mixtures (100k) based on high overlap (OV40) + log "Generating 100k anechoic train mixtures for warmup" + lhotse workflows simulate-meetings \ + --method conversational \ + --fit-to-supervisions data/manifests/libricss-sdm_supervisions_ov40.jsonl.gz \ + --num-meetings 100000 \ + --num-speakers-per-meeting 2,3 \ + --max-duration-per-speaker 15.0 \ + --max-utterances-per-speaker 3 \ + --seed 1234 \ + --num-jobs 4 \ + data/manifests/librispeech_cuts_train_trimmed.jsonl.gz \ + data/manifests/lsmix_cuts_train_clean_ov40.jsonl.gz + + # Full training set (2,3 speakers) anechoic + log "Generating anechoic ${part} set (full)" + lhotse workflows simulate-meetings \ + --method conversational \ + --fit-to-supervisions data/manifests/libricss-sdm_supervisions_all_v1.jsonl.gz \ + --num-repeats 1 \ + --num-speakers-per-meeting 2,3 \ + --max-duration-per-speaker 15.0 \ + --max-utterances-per-speaker 3 \ + --seed 1234 \ + --num-jobs 4 \ + data/manifests/librispeech_cuts_train_trimmed.jsonl.gz \ + data/manifests/lsmix_cuts_train_clean_full.jsonl.gz +fi + +if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then + log "Stage 6: Compute fbank features for musan" + mkdir -p data/fbank + python local/compute_fbank_musan.py +fi + +if [ $stage -le 7 ] && [ $stop_stage -ge 7 ]; then + log "Stage 7: Compute fbank features for simulated Libri-mix" + mkdir -p data/fbank + python local/compute_fbank_lsmix.py +fi + +if [ $stage -le 8 ] && [ $stop_stage -ge 8 ]; then + log "Stage 8: Add source feats to mixtures (useful for auxiliary tasks)" + python local/add_source_feats.py + + log "Combining lsmix-clean and lsmix-rvb" + for type in full ov40; do + cat <(gunzip -c data/manifests/cuts_train_clean_${type}_sources.jsonl.gz) \ + <(gunzip -c data/manifests/cuts_train_rvb_${type}_sources.jsonl.gz) |\ + shuf | gzip -c > data/manifests/cuts_train_comb_${type}_sources.jsonl.gz + done +fi + +if [ $stage -le 9 ] && [ $stop_stage -ge 9 ]; then + log "Stage 9: Compute fbank features for LibriCSS" + mkdir -p data/fbank + python local/compute_fbank_libricss.py +fi + +if [ $stage -le 10 ] && [ $stop_stage -ge 10 ]; then + log "Stage 10: Download LibriSpeech BPE model from HuggingFace." + mkdir -p data/lang_bpe_500 + pushd data/lang_bpe_500 + wget https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/resolve/main/data/lang_bpe_500/bpe.model + popd +fi diff --git a/egs/libricss/SURT/shared b/egs/libricss/SURT/shared new file mode 120000 index 000000000..4cbd91a7e --- /dev/null +++ b/egs/libricss/SURT/shared @@ -0,0 +1 @@ +../../../icefall/shared \ No newline at end of file diff --git a/egs/libricss/SURT/surt.png b/egs/libricss/SURT/surt.png new file mode 100644 index 0000000000000000000000000000000000000000..fcc8119d4b4720e2620fd789773a1003d5aa3871 GIT binary patch literal 114318 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 float: + """Write statistics based on predicted results and reference transcripts for SURT + multi-talker ASR systems. The difference between this and the `write_error_stats` + is that this function finds the optimal speaker-agnostic WER using the ``meeteval`` + toolkit. + + Args: + f: File to write the statistics to. + test_set_name: Name of the test set. + results: List of tuples containing the utterance ID and the predicted + transcript. + enable_log: Whether to enable logging. + num_channels: Number of output channels/branches. Defaults to 2. + Returns: + Return None. + """ + from meeteval.wer import wer + + subs: Dict[Tuple[str, str], int] = defaultdict(int) + ins: Dict[str, int] = defaultdict(int) + dels: Dict[str, int] = defaultdict(int) + ref_lens: List[int] = [] + + print( + "Search below for sections starting with PER-UTT DETAILS:, " + "SUBSTITUTIONS:, DELETIONS:, INSERTIONS:, PER-WORD STATS:", + file=f, + ) + + print("", file=f) + print("PER-UTT DETAILS: corr or (ref->hyp) ", file=f) + + # `words` stores counts per word, as follows: + # corr, ref_sub, hyp_sub, ins, dels + words: Dict[str, List[int]] = defaultdict(lambda: [0, 0, 0, 0, 0]) + num_corr = 0 + ERR = "*" + for cut_id, ref, hyp in results: + # First compute the optimal assignment of references to output channels + orc_wer = wer.orc_word_error_rate(ref, hyp) + assignment = orc_wer.assignment + refs = [[] for _ in range(num_channels)] + # Assign references to channels + for i, ref_text in zip(assignment, ref): + refs[i] += ref_text.split() + hyps = [hyp_text.split() for hyp_text in hyp] + # Now compute the WER for each channel + for ref_c, hyp_c in zip(refs, hyps): + ref_lens.append(len(ref_c)) + ali = kaldialign.align(ref_c, hyp_c, ERR) + for ref_word, hyp_word in ali: + if ref_word == ERR: + ins[hyp_word] += 1 + words[hyp_word][3] += 1 + elif hyp_word == ERR: + dels[ref_word] += 1 + words[ref_word][4] += 1 + elif hyp_word != ref_word: + subs[(ref_word, hyp_word)] += 1 + words[ref_word][1] += 1 + words[hyp_word][2] += 1 + else: + words[ref_word][0] += 1 + num_corr += 1 + combine_successive_errors = True + if combine_successive_errors: + ali = [[[x], [y]] for x, y in ali] + for i in range(len(ali) - 1): + if ali[i][0] != ali[i][1] and ali[i + 1][0] != ali[i + 1][1]: + ali[i + 1][0] = ali[i][0] + ali[i + 1][0] + ali[i + 1][1] = ali[i][1] + ali[i + 1][1] + ali[i] = [[], []] + ali = [ + [ + list(filter(lambda a: a != ERR, x)), + list(filter(lambda a: a != ERR, y)), + ] + for x, y in ali + ] + ali = list(filter(lambda x: x != [[], []], ali)) + ali = [ + [ + ERR if x == [] else " ".join(x), + ERR if y == [] else " ".join(y), + ] + for x, y in ali + ] + + print( + f"{cut_id}:\t" + + " ".join( + ( + ref_word + if ref_word == hyp_word + else f"({ref_word}->{hyp_word})" + for ref_word, hyp_word in ali + ) + ), + file=f, + ) + ref_len = sum(ref_lens) + sub_errs = sum(subs.values()) + ins_errs = sum(ins.values()) + del_errs = sum(dels.values()) + tot_errs = sub_errs + ins_errs + del_errs + tot_err_rate = "%.2f" % (100.0 * tot_errs / ref_len) + + if enable_log: + logging.info( + f"[{test_set_name}] %WER {tot_errs / ref_len:.2%} " + f"[{tot_errs} / {ref_len}, {ins_errs} ins, " + f"{del_errs} del, {sub_errs} sub ]" + ) + + print(f"%WER = {tot_err_rate}", file=f) + print( + f"Errors: {ins_errs} insertions, {del_errs} deletions, " + f"{sub_errs} substitutions, over {ref_len} reference " + f"words ({num_corr} correct)", + file=f, + ) + + print("", file=f) + print("SUBSTITUTIONS: count ref -> hyp", file=f) + + for count, (ref, hyp) in sorted([(v, k) for k, v in subs.items()], reverse=True): + print(f"{count} {ref} -> {hyp}", file=f) + + print("", file=f) + print("DELETIONS: count ref", file=f) + for count, ref in sorted([(v, k) for k, v in dels.items()], reverse=True): + print(f"{count} {ref}", file=f) + + print("", file=f) + print("INSERTIONS: count hyp", file=f) + for count, hyp in sorted([(v, k) for k, v in ins.items()], reverse=True): + print(f"{count} {hyp}", file=f) + + print("", file=f) + print("PER-WORD STATS: word corr tot_errs count_in_ref count_in_hyp", file=f) + for _, word, counts in sorted( + [(sum(v[1:]), k, v) for k, v in words.items()], reverse=True + ): + (corr, ref_sub, hyp_sub, ins, dels) = counts + tot_errs = ref_sub + hyp_sub + ins + dels + ref_count = corr + ref_sub + dels + hyp_count = corr + hyp_sub + ins + + print(f"{word} {corr} {tot_errs} {ref_count} {hyp_count}", file=f) + + print(f"%WER = {tot_err_rate}", file=f) + return float(tot_err_rate) class MetricsTracker(collections.defaultdict): From b8a17944e4a1f7a8b04830281affb0b97f26a100 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 5 Jul 2023 10:23:35 +0800 Subject: [PATCH 047/100] Fix zipformer CI test (#1164) --- .../ASR/pruned_transducer_stateless7_streaming/export.py | 4 ++++ .../pruned_transducer_stateless7_streaming/jit_pretrained.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py index 5735ee692..c191b5bcc 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py @@ -856,6 +856,10 @@ def main(): # Otherwise, one of its arguments is a ragged tensor and is not # torch scriptabe. model.__class__.forward = torch.jit.ignore(model.__class__.forward) + model.encoder.__class__.non_streaming_forward = model.encoder.__class__.forward + model.encoder.__class__.non_streaming_forward = torch.jit.export( + model.encoder.__class__.non_streaming_forward + ) model.encoder.__class__.forward = model.encoder.__class__.streaming_forward logging.info("Using torch.jit.script") model = torch.jit.script(model) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_pretrained.py index 4fd5e1820..c8301b2da 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/jit_pretrained.py @@ -252,7 +252,7 @@ def main(): feature_lengths = torch.tensor(feature_lengths, device=device) - encoder_out, encoder_out_lens = model.encoder( + encoder_out, encoder_out_lens = model.encoder.non_streaming_forward( x=features, x_lens=feature_lengths, ) From 130ad0319d93657690687f1e292cc7658ff7e779 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 5 Jul 2023 10:38:29 +0800 Subject: [PATCH 048/100] Fix CI test for zipformer CTC (#1165) --- egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py index 14faeedd1..904d8cd76 100755 --- a/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py +++ b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py @@ -264,7 +264,7 @@ def main(): params.update(vars(args)) token_table = k2.SymbolTable.from_file(params.tokens) - params.vocab_size = num_tokens(token_table) + params.vocab_size = num_tokens(token_table) + 1 logging.info(f"{params}") From 6fd674312c1d87bd9fc888d623cb3e347ac019ff Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 5 Jul 2023 10:52:34 +0800 Subject: [PATCH 049/100] Fix failed CI tests (#1166) --- .github/workflows/run-aishell-2022-06-20.yml | 4 ++-- .github/workflows/run-gigaspeech-2022-05-13.yml | 4 ++-- .github/workflows/run-librispeech-2022-03-12.yml | 4 ++-- .github/workflows/run-librispeech-2022-04-29.yml | 6 +++--- .github/workflows/run-librispeech-2022-05-13.yml | 4 ++-- .github/workflows/run-librispeech-2022-11-11-stateless7.yml | 2 +- .github/workflows/run-librispeech-2022-11-14-stateless8.yml | 2 +- .../workflows/run-librispeech-2022-12-01-stateless7-ctc.yml | 2 +- .../workflows/run-librispeech-2022-12-08-zipformer-mmi.yml | 2 +- .../run-librispeech-2022-12-15-stateless7-ctc-bs.yml | 2 +- .../run-librispeech-2022-12-29-stateless7-streaming.yml | 2 +- .../workflows/run-librispeech-conformer-ctc3-2022-11-28.yml | 2 +- ...un-librispeech-lstm-transducer-stateless2-2022-09-03.yml | 4 ++-- ...-librispeech-pruned-transducer-stateless3-2022-05-13.yml | 4 ++-- ...brispeech-streaming-transducer-stateless2-2022-06-26.yml | 4 ++-- .../run-librispeech-streaming-zipformer-2023-05-18.yml | 2 +- .../run-librispeech-transducer-stateless2-2022-04-19.yml | 4 ++-- .github/workflows/run-librispeech-zipformer-2023-05-18.yml | 2 +- .../workflows/run-librispeech-zipformer-ctc-2023-06-14.yml | 2 +- .github/workflows/run-pretrained-conformer-ctc.yml | 2 +- ...run-pretrained-transducer-stateless-librispeech-100h.yml | 4 ++-- ...ined-transducer-stateless-librispeech-multi-datasets.yml | 4 ++-- ...n-pretrained-transducer-stateless-modified-2-aishell.yml | 2 +- ...run-pretrained-transducer-stateless-modified-aishell.yml | 2 +- .github/workflows/run-pretrained-transducer-stateless.yml | 4 ++-- .github/workflows/run-pretrained-transducer.yml | 2 +- .../run-wenetspeech-pruned-transducer-stateless2.yml | 2 +- .github/workflows/run-yesno-recipe.yml | 2 +- 28 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/run-aishell-2022-06-20.yml b/.github/workflows/run-aishell-2022-06-20.yml index c46cea0f6..d14196f38 100644 --- a/.github/workflows/run-aishell-2022-06-20.yml +++ b/.github/workflows/run-aishell-2022-06-20.yml @@ -44,7 +44,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -119,5 +119,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: aishell-torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless3-2022-06-20 + name: aishell-torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless3-2022-06-20 path: egs/aishell/ASR/pruned_transducer_stateless3/exp/ diff --git a/.github/workflows/run-gigaspeech-2022-05-13.yml b/.github/workflows/run-gigaspeech-2022-05-13.yml index f8ee25cc4..0e47f7538 100644 --- a/.github/workflows/run-gigaspeech-2022-05-13.yml +++ b/.github/workflows/run-gigaspeech-2022-05-13.yml @@ -43,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -122,5 +122,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-gigaspeech-pruned_transducer_stateless2-2022-05-12 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-gigaspeech-pruned_transducer_stateless2-2022-05-12 path: egs/gigaspeech/ASR/pruned_transducer_stateless2/exp/ diff --git a/.github/workflows/run-librispeech-2022-03-12.yml b/.github/workflows/run-librispeech-2022-03-12.yml index d42202b79..3edbe43ec 100644 --- a/.github/workflows/run-librispeech-2022-03-12.yml +++ b/.github/workflows/run-librispeech-2022-03-12.yml @@ -43,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -155,5 +155,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless-2022-03-12 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless-2022-03-12 path: egs/librispeech/ASR/pruned_transducer_stateless/exp/ diff --git a/.github/workflows/run-librispeech-2022-04-29.yml b/.github/workflows/run-librispeech-2022-04-29.yml index f42c8f27a..bb44a073b 100644 --- a/.github/workflows/run-librispeech-2022-04-29.yml +++ b/.github/workflows/run-librispeech-2022-04-29.yml @@ -43,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -174,12 +174,12 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless2-2022-04-29 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless2-2022-04-29 path: egs/librispeech/ASR/pruned_transducer_stateless2/exp/ - name: Upload decoding results for pruned_transducer_stateless3 uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless3-2022-04-29 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless3-2022-04-29 path: egs/librispeech/ASR/pruned_transducer_stateless3/exp/ diff --git a/.github/workflows/run-librispeech-2022-05-13.yml b/.github/workflows/run-librispeech-2022-05-13.yml index 1fbd96157..e7b53b21c 100644 --- a/.github/workflows/run-librispeech-2022-05-13.yml +++ b/.github/workflows/run-librispeech-2022-05-13.yml @@ -43,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -155,5 +155,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless5-2022-05-13 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless5-2022-05-13 path: egs/librispeech/ASR/pruned_transducer_stateless5/exp/ diff --git a/.github/workflows/run-librispeech-2022-11-11-stateless7.yml b/.github/workflows/run-librispeech-2022-11-11-stateless7.yml index 596596bd9..7e378c9a1 100644 --- a/.github/workflows/run-librispeech-2022-11-11-stateless7.yml +++ b/.github/workflows/run-librispeech-2022-11-11-stateless7.yml @@ -155,5 +155,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless7-2022-11-11 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless7-2022-11-11 path: egs/librispeech/ASR/pruned_transducer_stateless7/exp/ diff --git a/.github/workflows/run-librispeech-2022-11-14-stateless8.yml b/.github/workflows/run-librispeech-2022-11-14-stateless8.yml index dca7d6d25..a2c1a0ad6 100644 --- a/.github/workflows/run-librispeech-2022-11-14-stateless8.yml +++ b/.github/workflows/run-librispeech-2022-11-14-stateless8.yml @@ -155,5 +155,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless8-2022-11-14 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless8-2022-11-14 path: egs/librispeech/ASR/pruned_transducer_stateless8/exp/ diff --git a/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml b/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml index cd41e988e..500ab1736 100644 --- a/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml +++ b/.github/workflows/run-librispeech-2022-12-01-stateless7-ctc.yml @@ -159,5 +159,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless7-ctc-2022-12-01 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless7-ctc-2022-12-01 path: egs/librispeech/ASR/pruned_transducer_stateless7_ctc/exp/ diff --git a/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml b/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml index 91242c401..1a7f9f594 100644 --- a/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml +++ b/.github/workflows/run-librispeech-2022-12-08-zipformer-mmi.yml @@ -163,5 +163,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-zipformer_mmi-2022-12-08 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-zipformer_mmi-2022-12-08 path: egs/librispeech/ASR/zipformer_mmi/exp/ diff --git a/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml b/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml index e0130a636..40a742988 100644 --- a/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml +++ b/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml @@ -159,5 +159,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless7-ctc-bs-2022-12-15 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless7-ctc-bs-2022-12-15 path: egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/exp/ diff --git a/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml b/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml index 8490a62fc..68014e20c 100644 --- a/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml +++ b/.github/workflows/run-librispeech-2022-12-29-stateless7-streaming.yml @@ -168,5 +168,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless7-streaming-2022-12-29 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless7-streaming-2022-12-29 path: egs/librispeech/ASR/pruned_transducer_stateless7_streaming/exp/ diff --git a/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml b/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml index 40a37da57..905515dc4 100644 --- a/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml +++ b/.github/workflows/run-librispeech-conformer-ctc3-2022-11-28.yml @@ -151,5 +151,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-conformer_ctc3-2022-11-28 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-conformer_ctc3-2022-11-28 path: egs/librispeech/ASR/conformer_ctc3/exp/ diff --git a/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml b/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml index aba29d066..501fae38c 100644 --- a/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml +++ b/.github/workflows/run-librispeech-lstm-transducer-stateless2-2022-09-03.yml @@ -26,7 +26,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.8] fail-fast: false @@ -159,5 +159,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'shallow-fusion' || github.event.label.name == 'LODR' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-lstm_transducer_stateless2-2022-09-03 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-lstm_transducer_stateless2-2022-09-03 path: egs/librispeech/ASR/lstm_transducer_stateless2/exp/ diff --git a/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml b/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml index fd497601d..bf73d4f18 100644 --- a/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml +++ b/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml @@ -43,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -153,5 +153,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless3-2022-04-29 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless3-2022-04-29 path: egs/librispeech/ASR/pruned_transducer_stateless3/exp/ diff --git a/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml b/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml index 57fe5b999..6ea308468 100644 --- a/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml +++ b/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml @@ -43,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -155,5 +155,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-pruned_transducer_stateless2-2022-06-26 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless2-2022-06-26 path: egs/librispeech/ASR/pruned_transducer_stateless2/exp/ diff --git a/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml b/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml index ed934d56d..5145fb43c 100644 --- a/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml +++ b/.github/workflows/run-librispeech-streaming-zipformer-2023-05-18.yml @@ -170,5 +170,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-zipformer-2022-11-11 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-zipformer-2022-11-11 path: egs/librispeech/ASR/zipformer/exp/ diff --git a/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml b/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml index 515122a66..9fe2f0389 100644 --- a/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml +++ b/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml @@ -43,7 +43,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -155,5 +155,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-transducer_stateless2-2022-04-19 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-transducer_stateless2-2022-04-19 path: egs/librispeech/ASR/transducer_stateless2/exp/ diff --git a/.github/workflows/run-librispeech-zipformer-2023-05-18.yml b/.github/workflows/run-librispeech-zipformer-2023-05-18.yml index 7ecf0d2a0..e9d235ad1 100644 --- a/.github/workflows/run-librispeech-zipformer-2023-05-18.yml +++ b/.github/workflows/run-librispeech-zipformer-2023-05-18.yml @@ -155,5 +155,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-zipformer-2022-11-11 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-zipformer-2022-11-11 path: egs/librispeech/ASR/zipformer/exp/ diff --git a/.github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml b/.github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml index 569ce48fc..48f0b1532 100644 --- a/.github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml +++ b/.github/workflows/run-librispeech-zipformer-ctc-2023-06-14.yml @@ -151,5 +151,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-zipformer-2022-11-11 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-zipformer-2022-11-11 path: egs/librispeech/ASR/zipformer/exp/ diff --git a/.github/workflows/run-pretrained-conformer-ctc.yml b/.github/workflows/run-pretrained-conformer-ctc.yml index 8aaea35f6..bcd326b9d 100644 --- a/.github/workflows/run-pretrained-conformer-ctc.yml +++ b/.github/workflows/run-pretrained-conformer-ctc.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml b/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml index 03a1df48e..1e5b25f5c 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml @@ -42,7 +42,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -154,5 +154,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-transducer_stateless_multi_datasets-100h-2022-02-21 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-transducer_stateless_multi_datasets-100h-2022-02-21 path: egs/librispeech/ASR/transducer_stateless_multi_datasets/exp/ diff --git a/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml b/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml index 8da4ff56a..9063c0ed6 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml @@ -42,7 +42,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -154,5 +154,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-transducer_stateless_multi_datasets-100h-2022-03-01 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-transducer_stateless_multi_datasets-100h-2022-03-01 path: egs/librispeech/ASR/transducer_stateless_multi_datasets/exp/ diff --git a/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml b/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml index 0b3e70d77..2d24528d3 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml b/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml index a6a59d339..761b26131 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless.yml b/.github/workflows/run-pretrained-transducer-stateless.yml index 98d84bf96..e46b9a849 100644 --- a/.github/workflows/run-pretrained-transducer-stateless.yml +++ b/.github/workflows/run-pretrained-transducer-stateless.yml @@ -42,7 +42,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false @@ -154,5 +154,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-18.04-cpu-transducer_stateless-2022-02-07 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-transducer_stateless-2022-02-07 path: egs/librispeech/ASR/transducer_stateless/exp/ diff --git a/.github/workflows/run-pretrained-transducer.yml b/.github/workflows/run-pretrained-transducer.yml index 8c1a652e0..190e446bc 100644 --- a/.github/workflows/run-pretrained-transducer.yml +++ b/.github/workflows/run-pretrained-transducer.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false diff --git a/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml b/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml index 6c70c646b..319a5558a 100644 --- a/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml +++ b/.github/workflows/run-wenetspeech-pruned-transducer-stateless2.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04] + os: [ubuntu-latest] python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-yesno-recipe.yml b/.github/workflows/run-yesno-recipe.yml index f997e634a..8a2c94829 100644 --- a/.github/workflows/run-yesno-recipe.yml +++ b/.github/workflows/run-yesno-recipe.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - # os: [ubuntu-18.04, macos-10.15] + # os: [ubuntu-latest, macos-10.15] # TODO: enable macOS for CPU testing os: [ubuntu-latest] python-version: [3.8] From 11523c5b894f42ded965dcb974fef9a8a8122518 Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Thu, 6 Jul 2023 19:11:01 +0800 Subject: [PATCH 050/100] Shallow fusion & LODR documentation (#1142) * add shallow fusion documentation * add documentation for LODR * upload docs for LM rescoring --- docs/source/conf.py | 1 + .../decoding-with-langugage-models/LODR.rst | 184 +++++++++++++ .../decoding-with-langugage-models/index.rst | 12 + .../rescoring.rst | 252 ++++++++++++++++++ .../shallow-fusion.rst | 176 ++++++++++++ docs/source/index.rst | 5 + .../librispeech/distillation.rst | 8 +- .../pruned_transducer_stateless.rst | 18 +- .../recipes/Streaming-ASR/introduction.rst | 4 +- .../pruned_transducer_stateless.rst | 10 +- .../librispeech/zipformer_transducer.rst | 4 +- 11 files changed, 652 insertions(+), 22 deletions(-) create mode 100644 docs/source/decoding-with-langugage-models/LODR.rst create mode 100644 docs/source/decoding-with-langugage-models/index.rst create mode 100644 docs/source/decoding-with-langugage-models/rescoring.rst create mode 100644 docs/source/decoding-with-langugage-models/shallow-fusion.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 6901dec02..0ff3f801c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -86,6 +86,7 @@ rst_epilog = """ .. _git-lfs: https://git-lfs.com/ .. _ncnn: https://github.com/tencent/ncnn .. _LibriSpeech: https://www.openslr.org/12 +.. _Gigaspeech: https://github.com/SpeechColab/GigaSpeech .. _musan: http://www.openslr.org/17/ .. _ONNX: https://github.com/onnx/onnx .. _onnxruntime: https://github.com/microsoft/onnxruntime diff --git a/docs/source/decoding-with-langugage-models/LODR.rst b/docs/source/decoding-with-langugage-models/LODR.rst new file mode 100644 index 000000000..7ffa0c128 --- /dev/null +++ b/docs/source/decoding-with-langugage-models/LODR.rst @@ -0,0 +1,184 @@ +.. _LODR: + +LODR for RNN Transducer +======================= + + +As a type of E2E model, neural transducers are usually considered as having an internal +language model, which learns the language level information on the training corpus. +In real-life scenario, there is often a mismatch between the training corpus and the target corpus space. +This mismatch can be a problem when decoding for neural transducer models with language models as its internal +language can act "against" the external LM. In this tutorial, we show how to use +`Low-order Density Ratio `_ to alleviate this effect to further improve the performance +of langugae model integration. + +.. note:: + + This tutorial is based on the recipe + `pruned_transducer_stateless7_streaming `_, + which is a streaming transducer model trained on `LibriSpeech`_. + However, you can easily apply LODR to other recipes. + If you encounter any problems, please open an issue here `icefall `__. + + +.. note:: + + For simplicity, the training and testing corpus in this tutorial are the same (`LibriSpeech`_). However, + you can change the testing set to any other domains (e.g `GigaSpeech`_) and prepare the language models + using that corpus. + +First, let's have a look at some background information. As the predecessor of LODR, Density Ratio (DR) is first proposed `here `_ +to address the language information mismatch between the training +corpus (source domain) and the testing corpus (target domain). Assuming that the source domain and the test domain +are acoustically similar, DR derives the following formular for decoding with Bayes' theorem: + +.. math:: + + \text{score}\left(y_u|\mathit{x},y\right) = + \log p\left(y_u|\mathit{x},y_{1:u-1}\right) + + \lambda_1 \log p_{\text{Target LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) - + \lambda_2 \log p_{\text{Source LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) + + +where :math:`\lambda_1` and :math:`\lambda_2` are the weights of LM scores for target domain and source domain respectively. +Here, the source domain LM is trained on the training corpus. The only difference in the above formular compared to +shallow fusion is the subtraction of the source domain LM. + +Some works treat the predictor and the joiner of the neural transducer as its internal LM. However, the LM is +considered to be weak and can only capture low-level language information. Therefore, `LODR `__ proposed to use +a low-order n-gram LM as an approximation of the ILM of the neural transducer. This leads to the following formula +during decoding for transducer model: + +.. math:: + + \text{score}\left(y_u|\mathit{x},y\right) = + \log p_{rnnt}\left(y_u|\mathit{x},y_{1:u-1}\right) + + \lambda_1 \log p_{\text{Target LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) - + \lambda_2 \log p_{\text{bi-gram}}\left(y_u|\mathit{x},y_{1:u-1}\right) + +In LODR, an additional bi-gram LM estimated on the source domain (e.g training corpus) is required. Comared to DR, +the only difference lies in the choice of source domain LM. According to the original `paper `_, +LODR achieves similar performance compared DR in both intra-domain and cross-domain settings. +As a bi-gram is much faster to evaluate, LODR is usually much faster. + +Now, we will show you how to use LODR in ``icefall``. +For illustration purpose, we will use a pre-trained ASR model from this `link `_. +If you want to train your model from scratch, please have a look at :ref:`non_streaming_librispeech_pruned_transducer_stateless`. +The testing scenario here is intra-domain (we decode the model trained on `LibriSpeech`_ on `LibriSpeech`_ testing sets). + +As the initial step, let's download the pre-trained model. + +.. code-block:: bash + + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 + $ pushd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ git lfs pull --include "pretrained.pt" + $ ln -s pretrained.pt epoch-99.pt # create a symbolic link so that the checkpoint can be loaded + +To test the model, let's have a look at the decoding results **without** using LM. This can be done via the following command: + +.. code-block:: bash + + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp/ + $ ./pruned_transducer_stateless7_streaming/decode.py \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model False \ + --exp-dir $exp_dir \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --max-duration 600 \ + --decode-chunk-len 32 \ + --decoding-method modified_beam_search + +The following WERs are achieved on test-clean and test-other: + +.. code-block:: text + + $ For test-clean, WER of different settings are: + $ beam_size_4 3.11 best for test-clean + $ For test-other, WER of different settings are: + $ beam_size_4 7.93 best for test-other + +Then, we download the external language model and bi-gram LM that are necessary for LODR. +Note that the bi-gram is estimated on the LibriSpeech 960 hours' text. + +.. code-block:: bash + + $ # download the external LM + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/ezerhouni/icefall-librispeech-rnn-lm + $ # create a symbolic link so that the checkpoint can be loaded + $ pushd icefall-librispeech-rnn-lm/exp + $ git lfs pull --include "pretrained.pt" + $ ln -s pretrained.pt epoch-99.pt + $ popd + $ + $ # download the bi-gram + $ git lfs install + $ git clone https://huggingface.co/marcoyang/librispeech_bigram + $ pushd data/lang_bpe_500 + $ ln -s ../../librispeech_bigram/2gram.fst.txt . + $ popd + +Then, we perform LODR decoding by setting ``--decoding-method`` to ``modified_beam_search_lm_LODR``: + +.. code-block:: bash + + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ lm_dir=./icefall-librispeech-rnn-lm/exp + $ lm_scale=0.42 + $ LODR_scale=-0.24 + $ ./pruned_transducer_stateless7_streaming/decode.py \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model False \ + --beam-size 4 \ + --exp-dir $exp_dir \ + --max-duration 600 \ + --decode-chunk-len 32 \ + --decoding-method modified_beam_search_lm_LODR \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --use-shallow-fusion 1 \ + --lm-type rnn \ + --lm-exp-dir $lm_dir \ + --lm-epoch 99 \ + --lm-scale $lm_scale \ + --lm-avg 1 \ + --rnn-lm-embedding-dim 2048 \ + --rnn-lm-hidden-dim 2048 \ + --rnn-lm-num-layers 3 \ + --lm-vocab-size 500 \ + --tokens-ngram 2 \ + --ngram-lm-scale $LODR_scale + +There are two extra arguments that need to be given when doing LODR. ``--tokens-ngram`` specifies the order of n-gram. As we +are using a bi-gram, we set it to 2. ``--ngram-lm-scale`` is the scale of the bi-gram, it should be a negative number +as we are subtracting the bi-gram's score during decoding. + +The decoding results obtained with the above command are shown below: + +.. code-block:: text + + $ For test-clean, WER of different settings are: + $ beam_size_4 2.61 best for test-clean + $ For test-other, WER of different settings are: + $ beam_size_4 6.74 best for test-other + +Recall that the lowest WER we obtained in :ref:`shallow_fusion` with beam size of 4 is ``2.77/7.08``, LODR +indeed **further improves** the WER. We can do even better if we increase ``--beam-size``: + +.. list-table:: WER of LODR with different beam sizes + :widths: 25 25 50 + :header-rows: 1 + + * - Beam size + - test-clean + - test-other + * - 4 + - 2.61 + - 6.74 + * - 8 + - 2.45 + - 6.38 + * - 12 + - 2.4 + - 6.23 \ No newline at end of file diff --git a/docs/source/decoding-with-langugage-models/index.rst b/docs/source/decoding-with-langugage-models/index.rst new file mode 100644 index 000000000..577ebbdfb --- /dev/null +++ b/docs/source/decoding-with-langugage-models/index.rst @@ -0,0 +1,12 @@ +Decoding with language models +============================= + +This section describes how to use external langugage models +during decoding to improve the WER of transducer models. + +.. toctree:: + :maxdepth: 2 + + shallow-fusion + LODR + rescoring diff --git a/docs/source/decoding-with-langugage-models/rescoring.rst b/docs/source/decoding-with-langugage-models/rescoring.rst new file mode 100644 index 000000000..d71acc1e5 --- /dev/null +++ b/docs/source/decoding-with-langugage-models/rescoring.rst @@ -0,0 +1,252 @@ +.. _rescoring: + +LM rescoring for Transducer +================================= + +LM rescoring is a commonly used approach to incorporate external LM information. Unlike shallow-fusion-based +methods (see :ref:`shallow-fusion`, :ref:`LODR`), rescoring is usually performed to re-rank the n-best hypotheses after beam search. +Rescoring is usually more efficient than shallow fusion since less computation is performed on the external LM. +In this tutorial, we will show you how to use external LM to rescore the n-best hypotheses decoded from neural transducer models in +`icefall `__. + +.. note:: + + This tutorial is based on the recipe + `pruned_transducer_stateless7_streaming `_, + which is a streaming transducer model trained on `LibriSpeech`_. + However, you can easily apply shallow fusion to other recipes. + If you encounter any problems, please open an issue `here `_. + +.. note:: + + For simplicity, the training and testing corpus in this tutorial is the same (`LibriSpeech`_). However, you can change the testing set + to any other domains (e.g `GigaSpeech`_) and use an external LM trained on that domain. + +.. HINT:: + + We recommend you to use a GPU for decoding. + +For illustration purpose, we will use a pre-trained ASR model from this `link `__. +If you want to train your model from scratch, please have a look at :ref:`non_streaming_librispeech_pruned_transducer_stateless`. + +As the initial step, let's download the pre-trained model. + +.. code-block:: bash + + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 + $ pushd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ git lfs pull --include "pretrained.pt" + $ ln -s pretrained.pt epoch-99.pt # create a symbolic link so that the checkpoint can be loaded + +As usual, we first test the model's performance without external LM. This can be done via the following command: + +.. code-block:: bash + + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp/ + $ ./pruned_transducer_stateless7_streaming/decode.py \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model False \ + --exp-dir $exp_dir \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --max-duration 600 \ + --decode-chunk-len 32 \ + --decoding-method modified_beam_search + +The following WERs are achieved on test-clean and test-other: + +.. code-block:: text + + $ For test-clean, WER of different settings are: + $ beam_size_4 3.11 best for test-clean + $ For test-other, WER of different settings are: + $ beam_size_4 7.93 best for test-other + +Now, we will try to improve the above WER numbers via external LM rescoring. We will download +a pre-trained LM from this `link `__. + +.. note:: + + This is an RNN LM trained on the LibriSpeech text corpus. So it might not be ideal for other corpus. + You may also train a RNN LM from scratch. Please refer to this `script `__ + for training a RNN LM and this `script `__ to train a transformer LM. + +.. code-block:: bash + + $ # download the external LM + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/ezerhouni/icefall-librispeech-rnn-lm + $ # create a symbolic link so that the checkpoint can be loaded + $ pushd icefall-librispeech-rnn-lm/exp + $ git lfs pull --include "pretrained.pt" + $ ln -s pretrained.pt epoch-99.pt + $ popd + + +With the RNNLM available, we can rescore the n-best hypotheses generated from `modified_beam_search`. Here, +`n` should be the number of beams, i.e ``--beam-size``. The command for LM rescoring is +as follows. Note that the ``--decoding-method`` is set to `modified_beam_search_lm_rescore` and ``--use-shallow-fusion`` +is set to `False`. + +.. code-block:: bash + + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ lm_dir=./icefall-librispeech-rnn-lm/exp + $ lm_scale=0.43 + $ ./pruned_transducer_stateless7_streaming/decode.py \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model False \ + --beam-size 4 \ + --exp-dir $exp_dir \ + --max-duration 600 \ + --decode-chunk-len 32 \ + --decoding-method modified_beam_search_lm_rescore \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --use-shallow-fusion 0 \ + --lm-type rnn \ + --lm-exp-dir $lm_dir \ + --lm-epoch 99 \ + --lm-scale $lm_scale \ + --lm-avg 1 \ + --rnn-lm-embedding-dim 2048 \ + --rnn-lm-hidden-dim 2048 \ + --rnn-lm-num-layers 3 \ + --lm-vocab-size 500 + +.. code-block:: text + + $ For test-clean, WER of different settings are: + $ beam_size_4 2.93 best for test-clean + $ For test-other, WER of different settings are: + $ beam_size_4 7.6 best for test-other + +Great! We made some improvements! Increasing the size of the n-best hypotheses will further boost the performance, +see the following table: + +.. list-table:: WERs of LM rescoring with different beam sizes + :widths: 25 25 25 + :header-rows: 1 + + * - Beam size + - test-clean + - test-other + * - 4 + - 2.93 + - 7.6 + * - 8 + - 2.67 + - 7.11 + * - 12 + - 2.59 + - 6.86 + +In fact, we can also apply LODR (see :ref:`LODR`) when doing LM rescoring. To do so, we need to +download the bi-gram required by LODR: + +.. code-block:: bash + + $ # download the bi-gram + $ git lfs install + $ git clone https://huggingface.co/marcoyang/librispeech_bigram + $ pushd data/lang_bpe_500 + $ ln -s ../../librispeech_bigram/2gram.arpa . + $ popd + +Then we can performn LM rescoring + LODR by changing the decoding method to `modified_beam_search_lm_rescore_LODR`. + +.. note:: + + This decoding method requires the dependency of `kenlm `_. You can install it + via this command: `pip install https://github.com/kpu/kenlm/archive/master.zip`. + +.. code-block:: bash + + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ lm_dir=./icefall-librispeech-rnn-lm/exp + $ lm_scale=0.43 + $ ./pruned_transducer_stateless7_streaming/decode.py \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model False \ + --beam-size 4 \ + --exp-dir $exp_dir \ + --max-duration 600 \ + --decode-chunk-len 32 \ + --decoding-method modified_beam_search_lm_rescore_LODR \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --use-shallow-fusion 0 \ + --lm-type rnn \ + --lm-exp-dir $lm_dir \ + --lm-epoch 99 \ + --lm-scale $lm_scale \ + --lm-avg 1 \ + --rnn-lm-embedding-dim 2048 \ + --rnn-lm-hidden-dim 2048 \ + --rnn-lm-num-layers 3 \ + --lm-vocab-size 500 + +You should see the following WERs after executing the commands above: + +.. code-block:: text + + $ For test-clean, WER of different settings are: + $ beam_size_4 2.9 best for test-clean + $ For test-other, WER of different settings are: + $ beam_size_4 7.57 best for test-other + +It's slightly better than LM rescoring. If we further increase the beam size, we will see +further improvements from LM rescoring + LODR: + +.. list-table:: WERs of LM rescoring + LODR with different beam sizes + :widths: 25 25 25 + :header-rows: 1 + + * - Beam size + - test-clean + - test-other + * - 4 + - 2.9 + - 7.57 + * - 8 + - 2.63 + - 7.04 + * - 12 + - 2.52 + - 6.73 + +As mentioned earlier, LM rescoring is usually faster than shallow-fusion based methods. +Here, we benchmark the WERs and decoding speed of them: + +.. list-table:: LM-rescoring-based methods vs shallow-fusion-based methods (The numbers in each field is WER on test-clean, WER on test-other and decoding time on test-clean) + :widths: 25 25 25 25 + :header-rows: 1 + + * - Decoding method + - beam=4 + - beam=8 + - beam=12 + * - `modified_beam_search` + - 3.11/7.93; 132s + - 3.1/7.95; 177s + - 3.1/7.96; 210s + * - `modified_beam_search_lm_shallow_fusion` + - 2.77/7.08; 262s + - 2.62/6.65; 352s + - 2.58/6.65; 488s + * - LODR + - 2.61/6.74; 400s + - 2.45/6.38; 610s + - 2.4/6.23; 870s + * - `modified_beam_search_lm_rescore` + - 2.93/7.6; 156s + - 2.67/7.11; 203s + - 2.59/6.86; 255s + * - `modified_beam_search_lm_rescore_LODR` + - 2.9/7.57; 160s + - 2.63/7.04; 203s + - 2.52/6.73; 263s + +.. note:: + + Decoding is performed with a single 32G V100, we set ``--max-duration`` to 600. + Decoding time here is only for reference and it may vary. \ No newline at end of file diff --git a/docs/source/decoding-with-langugage-models/shallow-fusion.rst b/docs/source/decoding-with-langugage-models/shallow-fusion.rst new file mode 100644 index 000000000..0d2837372 --- /dev/null +++ b/docs/source/decoding-with-langugage-models/shallow-fusion.rst @@ -0,0 +1,176 @@ +.. _shallow_fusion: + +Shallow fusion for Transducer +================================= + +External language models (LM) are commonly used to improve WERs for E2E ASR models. +This tutorial shows you how to perform ``shallow fusion`` with an external LM +to improve the word-error-rate of a transducer model. + +.. note:: + + This tutorial is based on the recipe + `pruned_transducer_stateless7_streaming `_, + which is a streaming transducer model trained on `LibriSpeech`_. + However, you can easily apply shallow fusion to other recipes. + If you encounter any problems, please open an issue here `icefall `_. + +.. note:: + + For simplicity, the training and testing corpus in this tutorial is the same (`LibriSpeech`_). However, you can change the testing set + to any other domains (e.g `GigaSpeech`_) and use an external LM trained on that domain. + +.. HINT:: + + We recommend you to use a GPU for decoding. + +For illustration purpose, we will use a pre-trained ASR model from this `link `__. +If you want to train your model from scratch, please have a look at :ref:`non_streaming_librispeech_pruned_transducer_stateless`. + +As the initial step, let's download the pre-trained model. + +.. code-block:: bash + + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 + $ pushd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ git lfs pull --include "pretrained.pt" + $ ln -s pretrained.pt epoch-99.pt # create a symbolic link so that the checkpoint can be loaded + +To test the model, let's have a look at the decoding results without using LM. This can be done via the following command: + +.. code-block:: bash + + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp/ + $ ./pruned_transducer_stateless7_streaming/decode.py \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model False \ + --exp-dir $exp_dir \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --max-duration 600 \ + --decode-chunk-len 32 \ + --decoding-method modified_beam_search + +The following WERs are achieved on test-clean and test-other: + +.. code-block:: text + + $ For test-clean, WER of different settings are: + $ beam_size_4 3.11 best for test-clean + $ For test-other, WER of different settings are: + $ beam_size_4 7.93 best for test-other + +These are already good numbers! But we can further improve it by using shallow fusion with external LM. +Training a language model usually takes a long time, we can download a pre-trained LM from this `link `__. + +.. code-block:: bash + + $ # download the external LM + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/ezerhouni/icefall-librispeech-rnn-lm + $ # create a symbolic link so that the checkpoint can be loaded + $ pushd icefall-librispeech-rnn-lm/exp + $ git lfs pull --include "pretrained.pt" + $ ln -s pretrained.pt epoch-99.pt + $ popd + +.. note:: + + This is an RNN LM trained on the LibriSpeech text corpus. So it might not be ideal for other corpus. + You may also train a RNN LM from scratch. Please refer to this `script `__ + for training a RNN LM and this `script `__ to train a transformer LM. + +To use shallow fusion for decoding, we can execute the following command: + +.. code-block:: bash + + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ lm_dir=./icefall-librispeech-rnn-lm/exp + $ lm_scale=0.29 + $ ./pruned_transducer_stateless7_streaming/decode.py \ + --epoch 99 \ + --avg 1 \ + --use-averaged-model False \ + --beam-size 4 \ + --exp-dir $exp_dir \ + --max-duration 600 \ + --decode-chunk-len 32 \ + --decoding-method modified_beam_search_lm_shallow_fusion \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --use-shallow-fusion 1 \ + --lm-type rnn \ + --lm-exp-dir $lm_dir \ + --lm-epoch 99 \ + --lm-scale $lm_scale \ + --lm-avg 1 \ + --rnn-lm-embedding-dim 2048 \ + --rnn-lm-hidden-dim 2048 \ + --rnn-lm-num-layers 3 \ + --lm-vocab-size 500 + +Note that we set ``--decoding-method modified_beam_search_lm_shallow_fusion`` and ``--use-shallow-fusion True`` +to use shallow fusion. ``--lm-type`` specifies the type of neural LM we are going to use, you can either choose +between ``rnn`` or ``transformer``. The following three arguments are associated with the rnn: + +- ``--rnn-lm-embedding-dim`` + The embedding dimension of the RNN LM + +- ``--rnn-lm-hidden-dim`` + The hidden dimension of the RNN LM + +- ``--rnn-lm-num-layers`` + The number of RNN layers in the RNN LM. + + +The decoding result obtained with the above command are shown below. + +.. code-block:: text + + $ For test-clean, WER of different settings are: + $ beam_size_4 2.77 best for test-clean + $ For test-other, WER of different settings are: + $ beam_size_4 7.08 best for test-other + +The improvement of shallow fusion is very obvious! The relative WER reduction on test-other is around 10.5%. +A few parameters can be tuned to further boost the performance of shallow fusion: + +- ``--lm-scale`` + + Controls the scale of the LM. If too small, the external language model may not be fully utilized; if too large, + the LM score may dominant during decoding, leading to bad WER. A typical value of this is around 0.3. + +- ``--beam-size`` + + The number of active paths in the search beam. It controls the trade-off between decoding efficiency and accuracy. + +Here, we also show how `--beam-size` effect the WER and decoding time: + +.. list-table:: WERs and decoding time (on test-clean) of shallow fusion with different beam sizes + :widths: 25 25 25 25 + :header-rows: 1 + + * - Beam size + - test-clean + - test-other + - Decoding time on test-clean (s) + * - 4 + - 2.77 + - 7.08 + - 262 + * - 8 + - 2.62 + - 6.65 + - 352 + * - 12 + - 2.58 + - 6.65 + - 488 + +As we see, a larger beam size during shallow fusion improves the WER, but is also slower. + + + + + + + + diff --git a/docs/source/index.rst b/docs/source/index.rst index 8d76eb68b..a7d365a15 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -34,3 +34,8 @@ speech recognition recipes using `k2 `_. contributing/index huggingface/index + +.. toctree:: + :maxdepth: 2 + + decoding-with-langugage-models/index \ No newline at end of file diff --git a/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst b/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst index ea9f350cd..2e8d0893a 100644 --- a/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst +++ b/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst @@ -1,7 +1,7 @@ Distillation with HuBERT ======================== -This tutorial shows you how to perform knowledge distillation in `icefall`_ +This tutorial shows you how to perform knowledge distillation in `icefall `_ with the `LibriSpeech`_ dataset. The distillation method used here is called "Multi Vector Quantization Knowledge Distillation" (MVQ-KD). Please have a look at our paper `Predicting Multi-Codebook Vector Quantization Indexes for Knowledge Distillation `_ @@ -13,7 +13,7 @@ for more details about MVQ-KD. `pruned_transducer_stateless4 `_. Currently, we only implement MVQ-KD in this recipe. However, MVQ-KD is theoretically applicable to all recipes with only minor changes needed. Feel free to try out MVQ-KD in different recipes. If you - encounter any problems, please open an issue here `icefall `_. + encounter any problems, please open an issue here `icefall `__. .. note:: @@ -217,7 +217,7 @@ the following command. --exp-dir $exp_dir \ --enable-distillation True -You should get similar results as `here `_. +You should get similar results as `here `__. That's all! Feel free to experiment with your own setups and report your results. -If you encounter any problems during training, please open up an issue `here `_. +If you encounter any problems during training, please open up an issue `here `__. diff --git a/docs/source/recipes/Non-streaming-ASR/librispeech/pruned_transducer_stateless.rst b/docs/source/recipes/Non-streaming-ASR/librispeech/pruned_transducer_stateless.rst index 42fd3df77..1bc1dd984 100644 --- a/docs/source/recipes/Non-streaming-ASR/librispeech/pruned_transducer_stateless.rst +++ b/docs/source/recipes/Non-streaming-ASR/librispeech/pruned_transducer_stateless.rst @@ -8,10 +8,10 @@ with the `LibriSpeech `_ dataset. .. Note:: - The tutorial is suitable for `pruned_transducer_stateless `_, - `pruned_transducer_stateless2 `_, - `pruned_transducer_stateless4 `_, - `pruned_transducer_stateless5 `_, + The tutorial is suitable for `pruned_transducer_stateless `__, + `pruned_transducer_stateless2 `__, + `pruned_transducer_stateless4 `__, + `pruned_transducer_stateless5 `__, We will take pruned_transducer_stateless4 as an example in this tutorial. .. HINT:: @@ -237,7 +237,7 @@ them, please modify ``./pruned_transducer_stateless4/train.py`` directly. .. NOTE:: - The options for `pruned_transducer_stateless5 `_ are a little different from + The options for `pruned_transducer_stateless5 `__ are a little different from other recipes. It allows you to configure ``--num-encoder-layers``, ``--dim-feedforward``, ``--nhead``, ``--encoder-dim``, ``--decoder-dim``, ``--joiner-dim`` from commandline, so that you can train models with different size with pruned_transducer_stateless5. @@ -529,13 +529,13 @@ Download pretrained models If you don't want to train from scratch, you can download the pretrained models by visiting the following links: - - `pruned_transducer_stateless `_ + - `pruned_transducer_stateless `__ - - `pruned_transducer_stateless2 `_ + - `pruned_transducer_stateless2 `__ - - `pruned_transducer_stateless4 `_ + - `pruned_transducer_stateless4 `__ - - `pruned_transducer_stateless5 `_ + - `pruned_transducer_stateless5 `__ See ``_ for the details of the above pretrained models diff --git a/docs/source/recipes/Streaming-ASR/introduction.rst b/docs/source/recipes/Streaming-ASR/introduction.rst index e1382e77d..ac77a51d1 100644 --- a/docs/source/recipes/Streaming-ASR/introduction.rst +++ b/docs/source/recipes/Streaming-ASR/introduction.rst @@ -45,9 +45,9 @@ the input features. We have three variants of Emformer models in ``icefall``. - - ``pruned_stateless_emformer_rnnt2`` using Emformer from torchaudio, see `LibriSpeech recipe `_. + - ``pruned_stateless_emformer_rnnt2`` using Emformer from torchaudio, see `LibriSpeech recipe `__. - ``conv_emformer_transducer_stateless`` using ConvEmformer implemented by ourself. Different from the Emformer in torchaudio, ConvEmformer has a convolution in each layer and uses the mechanisms in our reworked conformer model. - See `LibriSpeech recipe `_. + See `LibriSpeech recipe `__. - ``conv_emformer_transducer_stateless2`` using ConvEmformer implemented by ourself. The only difference from the above one is that it uses a simplified memory bank. See `LibriSpeech recipe `_. diff --git a/docs/source/recipes/Streaming-ASR/librispeech/pruned_transducer_stateless.rst b/docs/source/recipes/Streaming-ASR/librispeech/pruned_transducer_stateless.rst index de7102ba8..2ca70bcf3 100644 --- a/docs/source/recipes/Streaming-ASR/librispeech/pruned_transducer_stateless.rst +++ b/docs/source/recipes/Streaming-ASR/librispeech/pruned_transducer_stateless.rst @@ -6,10 +6,10 @@ with the `LibriSpeech `_ dataset. .. Note:: - The tutorial is suitable for `pruned_transducer_stateless `_, - `pruned_transducer_stateless2 `_, - `pruned_transducer_stateless4 `_, - `pruned_transducer_stateless5 `_, + The tutorial is suitable for `pruned_transducer_stateless `__, + `pruned_transducer_stateless2 `__, + `pruned_transducer_stateless4 `__, + `pruned_transducer_stateless5 `__, We will take pruned_transducer_stateless4 as an example in this tutorial. .. HINT:: @@ -264,7 +264,7 @@ them, please modify ``./pruned_transducer_stateless4/train.py`` directly. .. NOTE:: - The options for `pruned_transducer_stateless5 `_ are a little different from + The options for `pruned_transducer_stateless5 `__ are a little different from other recipes. It allows you to configure ``--num-encoder-layers``, ``--dim-feedforward``, ``--nhead``, ``--encoder-dim``, ``--decoder-dim``, ``--joiner-dim`` from commandline, so that you can train models with different size with pruned_transducer_stateless5. diff --git a/docs/source/recipes/Streaming-ASR/librispeech/zipformer_transducer.rst b/docs/source/recipes/Streaming-ASR/librispeech/zipformer_transducer.rst index f0e8961d7..8b75473c6 100644 --- a/docs/source/recipes/Streaming-ASR/librispeech/zipformer_transducer.rst +++ b/docs/source/recipes/Streaming-ASR/librispeech/zipformer_transducer.rst @@ -6,7 +6,7 @@ with the `LibriSpeech `_ dataset. .. Note:: - The tutorial is suitable for `pruned_transducer_stateless7_streaming `_, + The tutorial is suitable for `pruned_transducer_stateless7_streaming `__, .. HINT:: @@ -642,7 +642,7 @@ Download pretrained models If you don't want to train from scratch, you can download the pretrained models by visiting the following links: - - `pruned_transducer_stateless7_streaming `_ + - `pruned_transducer_stateless7_streaming `__ See ``_ for the details of the above pretrained models From ffe816e2a8314318a4ef6d5eaba34b62b842ba3f Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Thu, 6 Jul 2023 23:12:41 +0800 Subject: [PATCH 051/100] Fix blank skip ci test (#1167) * Fix for ci * Fix frame_reducer --- ...ned-transducer-stateless7-ctc-bs-2023-01-29.sh} | 2 +- ...n-librispeech-2023-01-29-stateless7-ctc-bs.yml} | 8 ++++---- .../frame_reducer.py | 14 +++++++------- 3 files changed, 12 insertions(+), 12 deletions(-) rename .github/scripts/{run-librispeech-pruned-transducer-stateless7-ctc-bs-2022-12-15.sh => run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh} (100%) rename .github/workflows/{run-librispeech-2022-12-15-stateless7-ctc-bs.yml => run-librispeech-2023-01-29-stateless7-ctc-bs.yml} (97%) diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2022-12-15.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh similarity index 100% rename from .github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2022-12-15.sh rename to .github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh index 761eb72e2..7d2853c17 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2022-12-15.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh @@ -21,9 +21,9 @@ tree $repo/ ls -lh $repo/test_wavs/*.wav pushd $repo/exp -git lfs pull --include "data/lang_bpe_500/HLG.pt" git lfs pull --include "data/lang_bpe_500/L.pt" git lfs pull --include "data/lang_bpe_500/LG.pt" +git lfs pull --include "data/lang_bpe_500/HLG.pt" git lfs pull --include "data/lang_bpe_500/Linv.pt" git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/cpu_jit.pt" diff --git a/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml b/.github/workflows/run-librispeech-2023-01-29-stateless7-ctc-bs.yml similarity index 97% rename from .github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml rename to .github/workflows/run-librispeech-2023-01-29-stateless7-ctc-bs.yml index 40a742988..821abc25d 100644 --- a/.github/workflows/run-librispeech-2022-12-15-stateless7-ctc-bs.yml +++ b/.github/workflows/run-librispeech-2023-01-29-stateless7-ctc-bs.yml @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -name: run-librispeech-2022-12-15-stateless7-ctc-bs +name: run-librispeech-2023-01-29-stateless7-ctc-bs # zipformer on: @@ -34,7 +34,7 @@ on: - cron: "50 15 * * *" jobs: - run_librispeech_2022_12_15_zipformer_ctc_bs: + run_librispeech_2023_01_29_zipformer_ctc_bs: if: github.event.label.name == 'run-decode' || github.event.label.name == 'blank-skip' || github.event_name == 'push' || github.event_name == 'schedule' runs-on: ${{ matrix.os }} strategy: @@ -124,7 +124,7 @@ jobs: export PYTHONPATH=~/tmp/kaldifeat/kaldifeat/python:$PYTHONPATH export PYTHONPATH=~/tmp/kaldifeat/build/lib:$PYTHONPATH - .github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2022-12-15.sh + .github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh - name: Display decoding results for librispeech pruned_transducer_stateless7_ctc_bs if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' @@ -159,5 +159,5 @@ jobs: uses: actions/upload-artifact@v2 if: github.event_name == 'schedule' || github.event.label.name == 'run-decode' with: - name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless7-ctc-bs-2022-12-15 + name: torch-${{ matrix.torch }}-python-${{ matrix.python-version }}-ubuntu-latest-cpu-pruned_transducer_stateless7-ctc-bs-2023-01-29 path: egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/exp/ diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/frame_reducer.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/frame_reducer.py index 0841f7cf1..c44cb1eaf 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/frame_reducer.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/frame_reducer.py @@ -81,20 +81,20 @@ class FrameReducer(nn.Module): fake_limit_indexes = torch.topk( ctc_output[:, :, blank_id], max_limit_len ).indices - T = ( + T_arange = ( torch.arange(max_limit_len) .expand_as( fake_limit_indexes, ) .to(device=x.device) ) - T = torch.remainder(T, limit_lens.unsqueeze(1)) - limit_indexes = torch.gather(fake_limit_indexes, 1, T) + T_arange = torch.remainder(T_arange, limit_lens.unsqueeze(1)) + limit_indexes = torch.gather(fake_limit_indexes, 1, T_arange) limit_mask = torch.full_like( non_blank_mask, - False, + 0, device=x.device, - ).scatter_(1, limit_indexes, True) + ).scatter_(1, limit_indexes, 1) non_blank_mask = non_blank_mask | ~limit_mask @@ -108,9 +108,9 @@ class FrameReducer(nn.Module): ) - out_lens ) - max_pad_len = pad_lens_list.max() + max_pad_len = int(pad_lens_list.max()) - out = F.pad(x, (0, 0, 0, max_pad_len)) + out = F.pad(x, [0, 0, 0, max_pad_len]) valid_pad_mask = ~make_pad_mask(pad_lens_list) total_valid_mask = torch.concat([non_blank_mask, valid_pad_mask], dim=1) From 41b16d783878fe3de304bb70285d97581e629eb5 Mon Sep 17 00:00:00 2001 From: Desh Raj Date: Sat, 8 Jul 2023 17:01:51 +0200 Subject: [PATCH 052/100] SURT recipe for AMI and ICSI (#1133) * merge upstream * add SURT model and training * add libricss decoding * add chunk width randomization * decode SURT with libricss * initial commit for zipformer_ctc * remove unwanted changes * remove changes to other recipe * fix zipformer softlink * fix for JIT export * add missing file * fix symbolic links * update results * clean commit for SURT recipe * training libricss surt model * remove unwanted files * remove unwanted changes * remove changes in librispeech * change some files to symlinks * remove unwanted changes in utils * add export script * add README * minor fix in README * add assets for README * replace some files with symlinks * remove unused decoding methods * initial commit for SURT AMI recipe * fix symlink * add train + decode scripts * add missing symlink * change files to symlink * change file type --- egs/ami/SURT/README.md | 156 ++ .../SURT/dprnn_zipformer/asr_datamodule.py | 399 +++++ egs/ami/SURT/dprnn_zipformer/beam_search.py | 1 + egs/ami/SURT/dprnn_zipformer/decode.py | 622 ++++++++ egs/ami/SURT/dprnn_zipformer/decoder.py | 1 + egs/ami/SURT/dprnn_zipformer/dprnn.py | 1 + .../SURT/dprnn_zipformer/encoder_interface.py | 1 + egs/ami/SURT/dprnn_zipformer/export.py | 1 + egs/ami/SURT/dprnn_zipformer/joiner.py | 1 + egs/ami/SURT/dprnn_zipformer/model.py | 1 + egs/ami/SURT/dprnn_zipformer/optim.py | 1 + egs/ami/SURT/dprnn_zipformer/scaling.py | 1 + .../SURT/dprnn_zipformer/scaling_converter.py | 1 + egs/ami/SURT/dprnn_zipformer/test_model.py | 1 + egs/ami/SURT/dprnn_zipformer/train.py | 1420 +++++++++++++++++ egs/ami/SURT/dprnn_zipformer/train_adapt.py | 1411 ++++++++++++++++ egs/ami/SURT/dprnn_zipformer/zipformer.py | 1 + egs/ami/SURT/local/add_source_feats.py | 78 + egs/ami/SURT/local/compute_fbank_aimix.py | 185 +++ egs/ami/SURT/local/compute_fbank_ami.py | 94 ++ egs/ami/SURT/local/compute_fbank_icsi.py | 95 ++ egs/ami/SURT/local/compute_fbank_ihm.py | 101 ++ egs/ami/SURT/local/prepare_ami_train_cuts.py | 146 ++ egs/ami/SURT/local/prepare_icsi_train_cuts.py | 67 + egs/ami/SURT/local/prepare_lang_bpe.py | 1 + egs/ami/SURT/local/train_bpe_model.py | 1 + egs/ami/SURT/prepare.sh | 195 +++ egs/ami/SURT/shared | 1 + 28 files changed, 4984 insertions(+) create mode 100644 egs/ami/SURT/README.md create mode 100644 egs/ami/SURT/dprnn_zipformer/asr_datamodule.py create mode 120000 egs/ami/SURT/dprnn_zipformer/beam_search.py create mode 100755 egs/ami/SURT/dprnn_zipformer/decode.py create mode 120000 egs/ami/SURT/dprnn_zipformer/decoder.py create mode 120000 egs/ami/SURT/dprnn_zipformer/dprnn.py create mode 120000 egs/ami/SURT/dprnn_zipformer/encoder_interface.py create mode 120000 egs/ami/SURT/dprnn_zipformer/export.py create mode 120000 egs/ami/SURT/dprnn_zipformer/joiner.py create mode 120000 egs/ami/SURT/dprnn_zipformer/model.py create mode 120000 egs/ami/SURT/dprnn_zipformer/optim.py create mode 120000 egs/ami/SURT/dprnn_zipformer/scaling.py create mode 120000 egs/ami/SURT/dprnn_zipformer/scaling_converter.py create mode 120000 egs/ami/SURT/dprnn_zipformer/test_model.py create mode 100755 egs/ami/SURT/dprnn_zipformer/train.py create mode 100755 egs/ami/SURT/dprnn_zipformer/train_adapt.py create mode 120000 egs/ami/SURT/dprnn_zipformer/zipformer.py create mode 100755 egs/ami/SURT/local/add_source_feats.py create mode 100755 egs/ami/SURT/local/compute_fbank_aimix.py create mode 100755 egs/ami/SURT/local/compute_fbank_ami.py create mode 100755 egs/ami/SURT/local/compute_fbank_icsi.py create mode 100755 egs/ami/SURT/local/compute_fbank_ihm.py create mode 100755 egs/ami/SURT/local/prepare_ami_train_cuts.py create mode 100755 egs/ami/SURT/local/prepare_icsi_train_cuts.py create mode 120000 egs/ami/SURT/local/prepare_lang_bpe.py create mode 120000 egs/ami/SURT/local/train_bpe_model.py create mode 100755 egs/ami/SURT/prepare.sh create mode 120000 egs/ami/SURT/shared diff --git a/egs/ami/SURT/README.md b/egs/ami/SURT/README.md new file mode 100644 index 000000000..74a8ba014 --- /dev/null +++ b/egs/ami/SURT/README.md @@ -0,0 +1,156 @@ +# Introduction + +This is a multi-talker ASR recipe for the AMI and ICSI datasets. We train a Streaming +Unmixing and Recognition Transducer (SURT) model for the task. + +Please refer to the `egs/libricss/SURT` recipe README for details about the task and the +model. + +## Description of the recipe + +### Pre-requisites + +The recipes in this directory need the following packages to be installed: + +- [meeteval](https://github.com/fgnt/meeteval) +- [einops](https://github.com/arogozhnikov/einops) + +Additionally, we initialize the model with the pre-trained model from the LibriCSS recipe. +Please download this checkpoint (see below) or train the LibriCSS recipe first. + +### Training + +To train the model, run the following from within `egs/ami/SURT`: + +```bash +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +python dprnn_zipformer/train.py \ + --use-fp16 True \ + --exp-dir dprnn_zipformer/exp/surt_base \ + --world-size 4 \ + --max-duration 500 \ + --max-duration-valid 250 \ + --max-cuts 200 \ + --num-buckets 50 \ + --num-epochs 30 \ + --enable-spec-aug True \ + --enable-musan False \ + --ctc-loss-scale 0.2 \ + --heat-loss-scale 0.2 \ + --base-lr 0.004 \ + --model-init-ckpt exp/libricss_base.pt \ + --chunk-width-randomization True \ + --num-mask-encoder-layers 4 \ + --num-encoder-layers 2,2,2,2,2 +``` + +The above is for SURT-base (~26M). For SURT-large (~38M), use: + +```bash + --model-init-ckpt exp/libricss_large.pt \ + --num-mask-encoder-layers 6 \ + --num-encoder-layers 2,4,3,2,4 \ + --model-init-ckpt exp/zipformer_large.pt \ +``` + +**NOTE:** You may need to decrease the `--max-duration` for SURT-large to avoid OOM. + +### Adaptation + +The training step above only trains on simulated mixtures. For best results, we also +adapt the final model on the AMI+ICSI train set. For this, run the following from within +`egs/ami/SURT`: + +```bash +export CUDA_VISIBLE_DEVICES="0" + +python dprnn_zipformer/train_adapt.py \ + --use-fp16 True \ + --exp-dir dprnn_zipformer/exp/surt_base_adapt \ + --world-size 4 \ + --max-duration 500 \ + --max-duration-valid 250 \ + --max-cuts 200 \ + --num-buckets 50 \ + --num-epochs 8 \ + --lr-epochs 2 \ + --enable-spec-aug True \ + --enable-musan False \ + --ctc-loss-scale 0.2 \ + --base-lr 0.0004 \ + --model-init-ckpt dprnn_zipformer/exp/surt_base/epoch-30.pt \ + --chunk-width-randomization True \ + --num-mask-encoder-layers 4 \ + --num-encoder-layers 2,2,2,2,2 +``` + +For SURT-large, use the following config: + +```bash + --num-mask-encoder-layers 6 \ + --num-encoder-layers 2,4,3,2,4 \ + --model-init-ckpt dprnn_zipformer/exp/surt_large/epoch-30.pt \ + --num-epochs 15 \ + --lr-epochs 4 \ +``` + + +### Decoding + +To decode the model, run the following from within `egs/ami/SURT`: + +#### Greedy search + +```bash +export CUDA_VISIBLE_DEVICES="0" + +python dprnn_zipformer/decode.py \ + --epoch 20 --avg 1 --use-averaged-model False \ + --exp-dir dprnn_zipformer/exp/surt_base_adapt \ + --max-duration 250 \ + --decoding-method greedy_search +``` + +#### Beam search + +```bash +python dprnn_zipformer/decode.py \ + --epoch 20 --avg 1 --use-averaged-model False \ + --exp-dir dprnn_zipformer/exp/surt_base_adapt \ + --max-duration 250 \ + --decoding-method modified_beam_search \ + --beam-size 4 +``` + +## Results (using beam search) + +**AMI** + +| Model | IHM-Mix | SDM | MDM | +|------------|:-------:|:----:|:----:| +| SURT-base | 39.8 | 65.4 | 46.6 | +| + adapt | 37.4 | 46.9 | 43.7 | +| SURT-large | 36.8 | 62.5 | 44.4 | +| + adapt | **35.1** | **44.6** | **41.4** | + +**ICSI** + +| Model | IHM-Mix | SDM | +|------------|:-------:|:----:| +| SURT-base | 28.3 | 60.0 | +| + adapt | 26.3 | 33.9 | +| SURT-large | 27.8 | 59.7 | +| + adapt | **24.4** | **32.3** | + +## Pre-trained models and logs + +* LibriCSS pre-trained model (for initialization): [base](https://huggingface.co/desh2608/icefall-surt-libricss-dprnn-zipformer/tree/main/exp/surt_base) [large](https://huggingface.co/desh2608/icefall-surt-libricss-dprnn-zipformer/tree/main/exp/surt_large) + +* Pre-trained models: + +* Training logs: + - surt_base: + - surt_base_adapt: + - surt_large: + - surt_large_adapt: diff --git a/egs/ami/SURT/dprnn_zipformer/asr_datamodule.py b/egs/ami/SURT/dprnn_zipformer/asr_datamodule.py new file mode 100644 index 000000000..ec8106bc3 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/asr_datamodule.py @@ -0,0 +1,399 @@ +# Copyright 2021 Piotr Żelasko +# Copyright 2022 Xiaomi Corporation (Author: Mingshuang Luo) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 inspect +import logging +from functools import lru_cache +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +import torch +from lhotse import CutSet, Fbank, FbankConfig, load_manifest, load_manifest_lazy +from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures + CutMix, + DynamicBucketingSampler, + K2SurtDataset, + PrecomputedFeatures, + SimpleCutSampler, + SpecAugment, +) +from lhotse.dataset.input_strategies import OnTheFlyFeatures +from lhotse.utils import fix_random_seed +from torch.utils.data import DataLoader + +from icefall.utils import str2bool + + +class _SeedWorkers: + def __init__(self, seed: int): + self.seed = seed + + def __call__(self, worker_id: int): + fix_random_seed(self.seed + worker_id) + + +class AmiAsrDataModule: + """ + DataModule for k2 SURT experiments. + It assumes there is always one train and valid dataloader, + but there can be multiple test dataloaders (e.g. LibriSpeech test-clean + and test-other). + + It contains all the common data pipeline modules used in ASR + experiments, e.g.: + - dynamic batch size, + - bucketing samplers, + - augmentation, + - on-the-fly feature extraction + + This class should be derived for specific corpora used in ASR tasks. + """ + + def __init__(self, args: argparse.Namespace): + self.args = args + + @classmethod + def add_arguments(cls, parser: argparse.ArgumentParser): + group = parser.add_argument_group( + title="ASR data related options", + description="These options are used for the preparation of " + "PyTorch DataLoaders from Lhotse CutSet's -- they control the " + "effective batch sizes, sampling strategies, applied data " + "augmentations, etc.", + ) + group.add_argument( + "--manifest-dir", + type=Path, + default=Path("data/manifests"), + help="Path to directory with train/valid/test cuts.", + ) + group.add_argument( + "--max-duration", + type=int, + default=200.0, + help="Maximum pooled recordings duration (seconds) in a " + "single batch. You can reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--max-duration-valid", + type=int, + default=200.0, + help="Maximum pooled recordings duration (seconds) in a " + "single batch. You can reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--max-cuts", + type=int, + default=100, + help="Maximum number of cuts in a single batch. You can " + "reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--bucketing-sampler", + type=str2bool, + default=True, + help="When enabled, the batches will come from buckets of " + "similar duration (saves padding frames).", + ) + group.add_argument( + "--num-buckets", + type=int, + default=30, + help="The number of buckets for the DynamicBucketingSampler" + "(you might want to increase it for larger datasets).", + ) + group.add_argument( + "--on-the-fly-feats", + type=str2bool, + default=False, + help=( + "When enabled, use on-the-fly cut mixing and feature " + "extraction. Will drop existing precomputed feature manifests " + "if available." + ), + ) + group.add_argument( + "--shuffle", + type=str2bool, + default=True, + help="When enabled (=default), the examples will be " + "shuffled for each epoch.", + ) + group.add_argument( + "--drop-last", + type=str2bool, + default=True, + help="Whether to drop last batch. Used by sampler.", + ) + group.add_argument( + "--return-cuts", + type=str2bool, + default=True, + help="When enabled, each batch will have the " + "field: batch['supervisions']['cut'] with the cuts that " + "were used to construct it.", + ) + + group.add_argument( + "--num-workers", + type=int, + default=2, + help="The number of training dataloader workers that " + "collect the batches.", + ) + + group.add_argument( + "--enable-spec-aug", + type=str2bool, + default=True, + help="When enabled, use SpecAugment for training dataset.", + ) + + group.add_argument( + "--spec-aug-time-warp-factor", + type=int, + default=80, + help="Used only when --enable-spec-aug is True. " + "It specifies the factor for time warping in SpecAugment. " + "Larger values mean more warping. " + "A value less than 1 means to disable time warp.", + ) + + group.add_argument( + "--enable-musan", + type=str2bool, + default=True, + help="When enabled, select noise from MUSAN and mix it" + "with training dataset. ", + ) + + def train_dataloaders( + self, + cuts_train: CutSet, + sampler_state_dict: Optional[Dict[str, Any]] = None, + sources: bool = False, + ) -> DataLoader: + """ + Args: + cuts_train: + CutSet for training. + sampler_state_dict: + The state dict for the training sampler. + """ + transforms = [] + if self.args.enable_musan: + logging.info("Enable MUSAN") + logging.info("About to get Musan cuts") + cuts_musan = load_manifest(self.args.manifest_dir / "musan_cuts.jsonl.gz") + transforms.append( + CutMix(cuts=cuts_musan, prob=0.5, snr=(10, 20), preserve_id=True) + ) + else: + logging.info("Disable MUSAN") + + input_transforms = [] + if self.args.enable_spec_aug: + logging.info("Enable SpecAugment") + logging.info(f"Time warp factor: {self.args.spec_aug_time_warp_factor}") + # Set the value of num_frame_masks according to Lhotse's version. + # In different Lhotse's versions, the default of num_frame_masks is + # different. + num_frame_masks = 10 + num_frame_masks_parameter = inspect.signature( + SpecAugment.__init__ + ).parameters["num_frame_masks"] + if num_frame_masks_parameter.default == 1: + num_frame_masks = 2 + logging.info(f"Num frame mask: {num_frame_masks}") + input_transforms.append( + SpecAugment( + time_warp_factor=self.args.spec_aug_time_warp_factor, + num_frame_masks=num_frame_masks, + features_mask_size=27, + num_feature_masks=2, + frames_mask_size=100, + ) + ) + else: + logging.info("Disable SpecAugment") + + logging.info("About to create train dataset") + train = K2SurtDataset( + input_strategy=OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) + if self.args.on_the_fly_feats + else PrecomputedFeatures(), + cut_transforms=transforms, + input_transforms=input_transforms, + return_cuts=self.args.return_cuts, + return_sources=sources, + strict=False, + ) + + if self.args.bucketing_sampler: + logging.info("Using DynamicBucketingSampler.") + train_sampler = DynamicBucketingSampler( + cuts_train, + max_duration=self.args.max_duration, + quadratic_duration=30.0, + max_cuts=self.args.max_cuts, + shuffle=self.args.shuffle, + num_buckets=self.args.num_buckets, + drop_last=self.args.drop_last, + ) + else: + logging.info("Using SingleCutSampler.") + train_sampler = SimpleCutSampler( + cuts_train, + max_duration=self.args.max_duration, + max_cuts=self.args.max_cuts, + shuffle=self.args.shuffle, + ) + logging.info("About to create train dataloader") + + if sampler_state_dict is not None: + logging.info("Loading sampler state dict") + train_sampler.load_state_dict(sampler_state_dict) + + # 'seed' is derived from the current random state, which will have + # previously been set in the main process. + seed = torch.randint(0, 100000, ()).item() + worker_init_fn = _SeedWorkers(seed) + + train_dl = DataLoader( + train, + sampler=train_sampler, + batch_size=None, + num_workers=self.args.num_workers, + persistent_workers=False, + worker_init_fn=worker_init_fn, + ) + + return train_dl + + def valid_dataloaders(self, cuts_valid: CutSet) -> DataLoader: + transforms = [] + + logging.info("About to create dev dataset") + validate = K2SurtDataset( + input_strategy=OnTheFlyFeatures( + OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) + ) + if self.args.on_the_fly_feats + else PrecomputedFeatures(), + cut_transforms=transforms, + return_cuts=self.args.return_cuts, + return_sources=False, + strict=False, + ) + valid_sampler = DynamicBucketingSampler( + cuts_valid, + max_duration=self.args.max_duration_valid, + quadratic_duration=30.0, + max_cuts=self.args.max_cuts, + shuffle=False, + ) + logging.info("About to create dev dataloader") + + # 'seed' is derived from the current random state, which will have + # previously been set in the main process. + seed = torch.randint(0, 100000, ()).item() + worker_init_fn = _SeedWorkers(seed) + + valid_dl = DataLoader( + validate, + sampler=valid_sampler, + batch_size=None, + num_workers=self.args.num_workers, + persistent_workers=False, + worker_init_fn=worker_init_fn, + ) + + return valid_dl + + def test_dataloaders(self, cuts: CutSet) -> DataLoader: + logging.debug("About to create test dataset") + test = K2SurtDataset( + input_strategy=OnTheFlyFeatures( + OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) + ) + if self.args.on_the_fly_feats + else PrecomputedFeatures(), + return_cuts=self.args.return_cuts, + return_sources=False, + strict=False, + ) + sampler = DynamicBucketingSampler( + cuts, + max_duration=self.args.max_duration_valid, + max_cuts=self.args.max_cuts, + shuffle=False, + ) + + # 'seed' is derived from the current random state, which will have + # previously been set in the main process. + seed = torch.randint(0, 100000, ()).item() + worker_init_fn = _SeedWorkers(seed) + + logging.debug("About to create test dataloader") + test_dl = DataLoader( + test, + batch_size=None, + sampler=sampler, + num_workers=self.args.num_workers, + persistent_workers=False, + worker_init_fn=worker_init_fn, + ) + return test_dl + + @lru_cache() + def aimix_train_cuts( + self, + rvb_affix: str = "clean", + sources: bool = True, + ) -> CutSet: + logging.info("About to get train cuts") + source_affix = "_sources" if sources else "" + cs = load_manifest_lazy( + self.args.manifest_dir / f"cuts_train_{rvb_affix}{source_affix}.jsonl.gz" + ) + cs = cs.filter(lambda c: c.duration >= 1.0 and c.duration <= 30.0) + return cs + + @lru_cache() + def train_cuts( + self, + ) -> CutSet: + logging.info("About to get train cuts") + return load_manifest_lazy( + self.args.manifest_dir / "cuts_train_ami_icsi.jsonl.gz" + ) + + @lru_cache() + def ami_cuts(self, split: str = "dev", type: str = "sdm") -> CutSet: + logging.info(f"About to get AMI {split} {type} cuts") + return load_manifest_lazy( + self.args.manifest_dir / f"cuts_ami-{type}_{split}.jsonl.gz" + ) + + @lru_cache() + def icsi_cuts(self, split: str = "dev", type: str = "sdm") -> CutSet: + logging.info(f"About to get ICSI {split} {type} cuts") + return load_manifest_lazy( + self.args.manifest_dir / f"cuts_icsi-{type}_{split}.jsonl.gz" + ) diff --git a/egs/ami/SURT/dprnn_zipformer/beam_search.py b/egs/ami/SURT/dprnn_zipformer/beam_search.py new file mode 120000 index 000000000..581b29833 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/beam_search.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/beam_search.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/decode.py b/egs/ami/SURT/dprnn_zipformer/decode.py new file mode 100755 index 000000000..d1a1eddc9 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/decode.py @@ -0,0 +1,622 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2022 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./dprnn_zipformer/decode.py \ + --epoch 20 \ + --avg 1 \ + --use-averaged-model false \ + --exp-dir ./dprnn_zipformer/exp_adapt \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) beam search (not recommended) +./dprnn_zipformer/decode.py \ + --epoch 20 \ + --avg 1 \ + --use-averaged-model false \ + --exp-dir ./dprnn_zipformer/exp_adapt \ + --max-duration 600 \ + --decoding-method beam_search \ + --beam-size 4 + +(3) modified beam search +./dprnn_zipformer/decode.py \ + --epoch 20 \ + --avg 1 \ + --use-averaged-model false \ + --exp-dir ./dprnn_zipformer/exp_adapt \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 +""" + + +import argparse +import logging +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import AmiAsrDataModule +from beam_search import ( + beam_search, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from lhotse.utils import EPSILON +from train import add_model_arguments, get_params, get_surt_model + +from icefall import LmScorer, NgramLm +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + setup_logger, + store_transcripts, + str2bool, + write_surt_error_stats, +) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=20, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=1, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="dprnn_zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + batch: dict, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + feature_lens = batch["input_lens"].to(device) + + # Apply the mask encoder + B, T, F = feature.shape + processed = model.mask_encoder(feature) # B,T,F*num_channels + masks = processed.view(B, T, F, params.num_channels).unbind(dim=-1) + x_masked = [feature * m for m in masks] + + # Recognition + # Stack the inputs along the batch axis + h = torch.cat(x_masked, dim=0) + h_lens = torch.cat([feature_lens for _ in range(params.num_channels)], dim=0) + encoder_out, encoder_out_lens = model.encoder(x=h, x_lens=h_lens) + + if model.joint_encoder_layer is not None: + encoder_out = model.joint_encoder_layer(encoder_out) + + def _group_channels(hyps: List[str]) -> List[List[str]]: + """ + Currently we have a batch of size M*B, where M is the number of + channels and B is the batch size. We need to group the hypotheses + into B groups, each of which contains M hypotheses. + + Example: + hyps = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2'] + _group_channels(hyps) = [['a1', 'a2'], ['b1', 'b2'], ['c1', 'c2']] + """ + assert len(hyps) == B * params.num_channels + out_hyps = [] + for i in range(B): + out_hyps.append(hyps[i::B]) + return out_hyps + + hyps = [] + if params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp) + else: + batch_size = encoder_out.size(0) + + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyps.append(sp.decode(hyp)) + + if params.decoding_method == "greedy_search": + return {"greedy_search": _group_channels(hyps)} + elif "fast_beam_search" in params.decoding_method: + key = f"beam_{params.beam}_" + key += f"max_contexts_{params.max_contexts}_" + key += f"max_states_{params.max_states}" + if "nbest" in params.decoding_method: + key += f"_num_paths_{params.num_paths}_" + key += f"nbest_scale_{params.nbest_scale}" + if "LG" in params.decoding_method: + key += f"_ngram_lm_scale_{params.ngram_lm_scale}" + + return {key: _group_channels(hyps)} + else: + return {f"beam_size_{params.beam_size}": _group_channels(hyps)} + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + cut_ids = [cut.id for cut in batch["cuts"]] + cuts_batch = batch["cuts"] + + hyps_dict = decode_one_batch( + params=params, + model=model, + sp=sp, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + for cut_id, hyp_words in zip(cut_ids, hyps): + # Reference is a list of supervision texts sorted by start time. + ref_words = [ + s.text.strip() + for s in sorted( + cuts_batch[cut_id].supervisions, key=lambda s: s.start + ) + ] + this_batch.append((cut_id, ref_words, hyp_words)) + + results[name].extend(this_batch) + + num_cuts += len(cut_ids) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_surt_error_stats( + f, + f"{test_set_name}-{key}", + results, + enable_log=True, + num_channels=params.num_channels, + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + LmScorer.add_arguments(parser) + AmiAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + args.lang_dir = Path(args.lang_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "modified_beam_search", + ), f"Decoding method {params.decoding_method} is not supported." + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if "beam_search" in params.decoding_method: + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # and are defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_surt_model(params) + assert model.encoder.decode_chunk_size == params.decode_chunk_len // 2, ( + model.encoder.decode_chunk_size, + params.decode_chunk_len, + ) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + ami = AmiAsrDataModule(args) + + # NOTE(@desh2608): we filter segments longer than 120s to avoid OOM errors in decoding. + # However, 99.9% of the segments are shorter than 120s, so this should not + # substantially affect the results. In future, we will implement an overlapped + # inference method to avoid OOM errors. + + test_sets = {} + for split in ["dev", "test"]: + for type in ["ihm-mix", "sdm", "mdm8-bf"]: + test_sets[f"ami-{split}_{type}"] = ( + ami.ami_cuts(split=split, type=type) + .trim_to_supervision_groups(max_pause=0.0) + .filter(lambda c: 0.1 < c.duration < 120.0) + .to_eager() + ) + + for split in ["dev", "test"]: + for type in ["ihm-mix", "sdm"]: + test_sets[f"icsi-{split}_{type}"] = ( + ami.icsi_cuts(split=split, type=type) + .trim_to_supervision_groups(max_pause=0.0) + .filter(lambda c: 0.1 < c.duration < 120.0) + .to_eager() + ) + + for test_set, test_cuts in test_sets.items(): + test_dl = ami.test_dataloaders(test_cuts) + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + sp=sp, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/ami/SURT/dprnn_zipformer/decoder.py b/egs/ami/SURT/dprnn_zipformer/decoder.py new file mode 120000 index 000000000..c34865c25 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/decoder.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/decoder.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/dprnn.py b/egs/ami/SURT/dprnn_zipformer/dprnn.py new file mode 120000 index 000000000..8918beb32 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/dprnn.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/dprnn.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/encoder_interface.py b/egs/ami/SURT/dprnn_zipformer/encoder_interface.py new file mode 120000 index 000000000..0ba945d0f --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/encoder_interface.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/encoder_interface.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/export.py b/egs/ami/SURT/dprnn_zipformer/export.py new file mode 120000 index 000000000..3deae4471 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/export.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/export.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/joiner.py b/egs/ami/SURT/dprnn_zipformer/joiner.py new file mode 120000 index 000000000..79fbe8769 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/joiner.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/joiner.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/model.py b/egs/ami/SURT/dprnn_zipformer/model.py new file mode 120000 index 000000000..ae8c65c99 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/model.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/model.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/optim.py b/egs/ami/SURT/dprnn_zipformer/optim.py new file mode 120000 index 000000000..366d0f7a2 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/optim.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/optim.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/scaling.py b/egs/ami/SURT/dprnn_zipformer/scaling.py new file mode 120000 index 000000000..f11d49d77 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/scaling.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/scaling.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/scaling_converter.py b/egs/ami/SURT/dprnn_zipformer/scaling_converter.py new file mode 120000 index 000000000..1533cbe0e --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/scaling_converter.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/scaling_converter.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/test_model.py b/egs/ami/SURT/dprnn_zipformer/test_model.py new file mode 120000 index 000000000..1259849e0 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/test_model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7_streaming/test_model.py \ No newline at end of file diff --git a/egs/ami/SURT/dprnn_zipformer/train.py b/egs/ami/SURT/dprnn_zipformer/train.py new file mode 100755 index 000000000..cd5fafc34 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/train.py @@ -0,0 +1,1420 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo,) +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +cd egs/ami/SURT/ +./prepare.sh + +./dprnn_zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir dprnn_zipformer/exp \ + --max-duration 650 +""" + +import argparse +import copy +import logging +import warnings +from itertools import chain +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import AmiAsrDataModule +from decoder import Decoder +from dprnn import DPRNN +from einops.layers.torch import Rearrange +from joiner import Joiner +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import LOG_EPSILON, fix_random_seed +from model import SURT +from optim import Eden, ScaledAdam +from scaling import ScaledLinear, ScaledLSTM +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.utils import AttributeDict, MetricsTracker, setup_logger, str2bool + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for module in model.modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-mask-encoder-layers", + type=int, + default=4, + help="Number of layers in the DPRNN based mask encoder.", + ) + + parser.add_argument( + "--mask-encoder-dim", + type=int, + default=256, + help="Hidden dimension of the LSTM blocks in DPRNN.", + ) + + parser.add_argument( + "--mask-encoder-segment-size", + type=int, + default=32, + help="Segment size of the SegLSTM in DPRNN. Ideally, this should be equal to the " + "decode-chunk-length of the zipformer encoder.", + ) + + parser.add_argument( + "--chunk-width-randomization", + type=bool, + default=False, + help="Whether to randomize the chunk width in DPRNN.", + ) + + # Zipformer config is based on: + # https://github.com/k2-fsa/icefall/pull/745#issuecomment-1405282740 + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,2,2,2", + help="Number of zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--feedforward-dims", + type=str, + default="768,768,768,768,768", + help="Feedforward dimension of the zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--nhead", + type=str, + default="8,8,8,8,8", + help="Number of attention heads in the zipformer encoder layers.", + ) + + parser.add_argument( + "--encoder-dims", + type=str, + default="256,256,256,256,256", + help="Embedding dimension in the 2 blocks of zipformer encoder layers, comma separated", + ) + + parser.add_argument( + "--attention-dims", + type=str, + default="192,192,192,192,192", + help="""Attention dimension in the 2 blocks of zipformer encoder layers, comma separated; + not the same as embedding dimension.""", + ) + + parser.add_argument( + "--encoder-unmasked-dims", + type=str, + default="192,192,192,192,192", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance " + " worse.", + ) + + parser.add_argument( + "--zipformer-downsampling-factors", + type=str, + default="1,2,4,8,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--cnn-module-kernels", + type=str, + default="31,31,31,31,31", + help="Sizes of kernels in convolution modules", + ) + + parser.add_argument( + "--use-joint-encoder-layer", + type=str, + default="lstm", + choices=["linear", "lstm", "none"], + help="Whether to use a joint layer to combine all branches.", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--short-chunk-size", + type=int, + default=50, + help="""Chunk length of dynamic training, the chunk size would be either + max sequence length of current batch or uniformly sampled from (1, short_chunk_size). + """, + ) + + parser.add_argument( + "--num-left-chunks", + type=int, + default=4, + help="How many left context can be seen in chunks when calculating attention.", + ) + + parser.add_argument( + "--decode-chunk-len", + type=int, + default=32, + help="The chunk size for decoding (in frames before subsampling)", + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="conv_lstm_transducer_stateless_ctc/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--model-init-ckpt", + type=str, + default=None, + help="""The model checkpoint to initialize the model (either full or part). + If not specified, the model is randomly initialized. + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--base-lr", type=float, default=0.004, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=5000, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=5, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network) part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--ctc-loss-scale", + type=float, + default=0.2, + help="Scale for CTC loss.", + ) + + parser.add_argument( + "--heat-loss-scale", + type=float, + default=0.2, + help="Scale for HEAT loss on separated sources.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=2000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=1, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=100, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warm_step for Noam optimizer. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 2000, + # parameters for SURT + "num_channels": 2, + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed + # parameters for Noam + "model_warm_step": 5000, # arg given to model, not for lrate + # parameters for ctc loss + "beam_size": 10, + "use_double_scores": True, + "env_info": get_env_info(), + } + ) + + return params + + +def get_mask_encoder_model(params: AttributeDict) -> nn.Module: + mask_encoder = DPRNN( + feature_dim=params.feature_dim, + input_size=params.mask_encoder_dim, + hidden_size=params.mask_encoder_dim, + output_size=params.feature_dim * params.num_channels, + segment_size=params.mask_encoder_segment_size, + num_blocks=params.num_mask_encoder_layers, + chunk_width_randomization=params.chunk_width_randomization, + ) + return mask_encoder + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + # TODO: We can add an option to switch between Zipformer and Transformer + def to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + encoder = Zipformer( + num_features=params.feature_dim, + output_downsampling_factor=2, + zipformer_downsampling_factors=to_int_tuple( + params.zipformer_downsampling_factors + ), + encoder_dims=to_int_tuple(params.encoder_dims), + attention_dim=to_int_tuple(params.attention_dims), + encoder_unmasked_dims=to_int_tuple(params.encoder_unmasked_dims), + nhead=to_int_tuple(params.nhead), + feedforward_dim=to_int_tuple(params.feedforward_dims), + cnn_module_kernels=to_int_tuple(params.cnn_module_kernels), + num_encoder_layers=to_int_tuple(params.num_encoder_layers), + num_left_chunks=params.num_left_chunks, + short_chunk_size=params.short_chunk_size, + decode_chunk_size=params.decode_chunk_len // 2, + ) + return encoder + + +def get_joint_encoder_layer(params: AttributeDict) -> nn.Module: + class TakeFirst(nn.Module): + def forward(self, x): + return x[0] + + if params.use_joint_encoder_layer == "linear": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + nn.Linear( + params.num_channels * encoder_dim, params.num_channels * encoder_dim + ), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "lstm": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + ScaledLSTM( + input_size=params.num_channels * encoder_dim, + hidden_size=params.num_channels * encoder_dim, + num_layers=1, + bias=True, + batch_first=True, + dropout=0.0, + bidirectional=False, + ), + TakeFirst(), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "none": + joint_layer = None + else: + raise ValueError( + f"Unknown joint encoder layer type: {params.use_joint_encoder_layer}" + ) + return joint_layer + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_surt_model( + params: AttributeDict, +) -> nn.Module: + mask_encoder = get_mask_encoder_model(params) + encoder = get_encoder_model(params) + joint_layer = get_joint_encoder_layer(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = SURT( + mask_encoder=mask_encoder, + encoder=encoder, + joint_encoder_layer=joint_layer, + decoder=decoder, + joiner=joiner, + num_channels=params.num_channels, + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_heat_loss(x_masked, batch, num_channels=2) -> Tensor: + """ + Compute HEAT loss for separated sources using the output of mask encoder. + Args: + x_masked: + The output of mask encoder. It is a tensor of shape (B, T, C). + batch: + A batch of data. See `lhotse.dataset.K2SurtDatasetWithSources()` + for the content in it. + num_channels: + The number of output branches in the SURT model. + """ + B, T, D = x_masked[0].shape + device = x_masked[0].device + + # Create training targets for each channel. + targets = [] + for i in range(num_channels): + target = torch.ones_like(x_masked[i]) * LOG_EPSILON + targets.append(target) + + source_feats = batch["source_feats"] + source_boundaries = batch["source_boundaries"] + input_lens = batch["input_lens"].to(device) + # Assign sources to channels based on the HEAT criteria + for b in range(B): + cut_source_feats = source_feats[b] + cut_source_boundaries = source_boundaries[b] + last_seg_end = [0 for _ in range(num_channels)] + for source_feat, (start, end) in zip(cut_source_feats, cut_source_boundaries): + assigned = False + end = min(end, T) + source_feat = source_feat[: end - start, :] + for i in range(num_channels): + if start >= last_seg_end[i]: + targets[i][b, start:end, :] += source_feat.to(device) + last_seg_end[i] = max(end, last_seg_end[i]) + assigned = True + break + if not assigned: + min_end_channel = last_seg_end.index(min(last_seg_end)) + targets[min_end_channel][b, start:end, :] += source_feat.to(device) + last_seg_end[min_end_channel] = max(end, last_seg_end[min_end_channel]) + + # Get padding mask based on input lengths + pad_mask = torch.arange(T, device=device).expand(B, T) > input_lens.unsqueeze(1) + pad_mask = pad_mask.unsqueeze(-1) + + # Compute masked loss for each channel + losses = torch.zeros((num_channels, B, T, D), device=device) + for i in range(num_channels): + loss = nn.functional.mse_loss(x_masked[i], targets[i], reduction="none") + # Apply padding mask to loss + loss.masked_fill_(pad_mask, 0) + losses[i] = loss + + # loss: C x B x T x D. pad_mask: B x T x 1 + # We want to compute loss for each item in the batch. Each item has loss given + # by the sum over C, and average over T and D. For T, we need to use the padding. + loss = losses.sum(0).mean(-1).sum(-1) / batch["input_lens"].to(device) + return loss + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute RNN-T loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Conformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"].to(device) + feature_lens = batch["input_lens"].to(device) + + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + + # The dataloader returns text as a list of cuts, each of which is a list of channel + # text. We flatten this to a list where all channels are together, i.e., it looks like + # [utt1_ch1, utt2_ch1, ..., uttN_ch1, utt1_ch2, ...., uttN,ch2]. + text = [val for tup in zip(*batch["text"]) for val in tup] + assert len(text) == len(feature) * params.num_channels + + # Convert all channel texts to token IDs and create a ragged tensor. + y = sp.encode(text, out_type=int) + y = k2.RaggedTensor(y).to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.model_warm_step + + with torch.set_grad_enabled(is_training): + (simple_loss, pruned_loss, ctc_loss, x_masked) = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + reduction="none", + subsampling_factor=params.subsampling_factor, + ) + simple_loss_is_finite = torch.isfinite(simple_loss) + pruned_loss_is_finite = torch.isfinite(pruned_loss) + ctc_loss_is_finite = torch.isfinite(ctc_loss) + + # Compute HEAT loss + if is_training and params.heat_loss_scale > 0.0: + heat_loss = compute_heat_loss( + x_masked, batch, num_channels=params.num_channels + ) + else: + heat_loss = torch.tensor(0.0, device=device) + + heat_loss_is_finite = torch.isfinite(heat_loss) + is_finite = ( + simple_loss_is_finite + & pruned_loss_is_finite + & ctc_loss_is_finite + & heat_loss_is_finite + ) + if not torch.all(is_finite): + logging.info( + "Not all losses are finite!\n" + f"simple_losses: {simple_loss}\n" + f"pruned_losses: {pruned_loss}\n" + f"ctc_losses: {ctc_loss}\n" + f"heat_losses: {heat_loss}\n" + ) + display_and_save_batch(batch, params=params, sp=sp) + simple_loss = simple_loss[simple_loss_is_finite] + pruned_loss = pruned_loss[pruned_loss_is_finite] + ctc_loss = ctc_loss[ctc_loss_is_finite] + heat_loss = heat_loss[heat_loss_is_finite] + + # If either all simple_loss or pruned_loss is inf or nan, + # we stop the training process by raising an exception + if ( + torch.all(~simple_loss_is_finite) + or torch.all(~pruned_loss_is_finite) + or torch.all(~ctc_loss_is_finite) + or torch.all(~heat_loss_is_finite) + ): + raise ValueError( + "There are too many utterances in this batch " + "leading to inf or nan losses." + ) + + simple_loss_sum = simple_loss.sum() + pruned_loss_sum = pruned_loss.sum() + ctc_loss_sum = ctc_loss.sum() + heat_loss_sum = heat_loss.sum() + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss = ( + simple_loss_scale * simple_loss_sum + + pruned_loss_scale * pruned_loss_sum + + params.ctc_loss_scale * ctc_loss_sum + + params.heat_loss_scale * heat_loss_sum + ) + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # info["frames"] is an approximate number for two reasons: + # (1) The acutal subsampling factor is ((lens - 1) // 2 - 1) // 2 + # (2) If some utterances in the batch lead to inf/nan loss, they + # are filtered out. + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # `utt_duration` and `utt_pad_proportion` would be normalized by `utterances` # noqa + info["utterances"] = feature.size(0) + # averaged input duration in frames over utterances + info["utt_duration"] = feature_lens.sum().item() + # averaged padding proportion over utterances + info["utt_pad_proportion"] = ( + ((feature.size(1) - feature_lens) / feature.size(1)).sum().item() + ) + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss_sum.detach().cpu().item() + info["pruned_loss"] = pruned_loss_sum.detach().cpu().item() + if params.ctc_loss_scale > 0.0: + info["ctc_loss"] = ctc_loss_sum.detach().cpu().item() + if params.heat_loss_scale > 0.0: + info["heat_loss"] = heat_loss_sum.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + torch.cuda.empty_cache() + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = batch["inputs"].shape[0] + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + set_batch_count(model, params.batch_idx_train) + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + if cur_grad_scale < 1.0 or (cur_grad_scale < 8.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_surt_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + + if checkpoints is None and params.model_init_ckpt is not None: + logging.info( + f"Initializing model with checkpoint from {params.model_init_ckpt}" + ) + init_ckpt = torch.load(params.model_init_ckpt, map_location=device) + model.load_state_dict(init_ckpt["model"], strict=False) + + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + diagnostic = diagnostics.attach_diagnostics(model) + + ami = AmiAsrDataModule(args) + + train_cuts = ami.aimix_train_cuts(rvb_affix="comb", sources=True) + dev_cuts = ami.ami_cuts(split="dev", type="ihm-mix") + dev_cuts = dev_cuts.trim_to_supervision_groups(max_pause=0.0).filter( + lambda c: 0.2 <= c.duration <= 60.0 + ) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = ami.train_dataloaders( + train_cuts, + sampler_state_dict=sampler_state_dict, + sources=True, + ) + valid_dl = ami.valid_dataloaders(dev_cuts) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = [sp.encode(text_ch) for text_ch in batch["text"]] + num_tokens = [sum(len(yi) for yi in y_ch) for y_ch in y] + logging.info(f"num tokens: {num_tokens}") + + +def main(): + parser = get_parser() + AmiAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") + +if __name__ == "__main__": + main() diff --git a/egs/ami/SURT/dprnn_zipformer/train_adapt.py b/egs/ami/SURT/dprnn_zipformer/train_adapt.py new file mode 100755 index 000000000..9f3b4425f --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/train_adapt.py @@ -0,0 +1,1411 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo,) +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +# ./dprnn_zipformer/train.py should be run before this script. + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +./dprnn_zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir dprnn_zipformer/exp_adapt \ + --model-init-ckpt dprnn_zipformer/exp/epoch-30.pt \ + --max-duration 550 +""" + +import argparse +import copy +import logging +import warnings +from itertools import chain +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import AmiAsrDataModule +from decoder import Decoder +from dprnn import DPRNN +from einops.layers.torch import Rearrange +from joiner import Joiner +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import LOG_EPSILON, fix_random_seed +from model import SURT +from optim import Eden, ScaledAdam +from scaling import ScaledLinear, ScaledLSTM +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.utils import AttributeDict, MetricsTracker, setup_logger, str2bool + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for module in model.modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-mask-encoder-layers", + type=int, + default=4, + help="Number of layers in the DPRNN based mask encoder.", + ) + + parser.add_argument( + "--mask-encoder-dim", + type=int, + default=256, + help="Hidden dimension of the LSTM blocks in DPRNN.", + ) + + parser.add_argument( + "--mask-encoder-segment-size", + type=int, + default=32, + help="Segment size of the SegLSTM in DPRNN. Ideally, this should be equal to the " + "decode-chunk-length of the zipformer encoder.", + ) + + parser.add_argument( + "--chunk-width-randomization", + type=bool, + default=False, + help="Whether to randomize the chunk width in DPRNN.", + ) + + # Zipformer config is based on: + # https://github.com/k2-fsa/icefall/pull/745#issuecomment-1405282740 + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,2,2,2", + help="Number of zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--feedforward-dims", + type=str, + default="768,768,768,768,768", + help="Feedforward dimension of the zipformer encoder layers, comma separated.", + ) + + parser.add_argument( + "--nhead", + type=str, + default="8,8,8,8,8", + help="Number of attention heads in the zipformer encoder layers.", + ) + + parser.add_argument( + "--encoder-dims", + type=str, + default="256,256,256,256,256", + help="Embedding dimension in the 2 blocks of zipformer encoder layers, comma separated", + ) + + parser.add_argument( + "--attention-dims", + type=str, + default="192,192,192,192,192", + help="""Attention dimension in the 2 blocks of zipformer encoder layers, comma separated; + not the same as embedding dimension.""", + ) + + parser.add_argument( + "--encoder-unmasked-dims", + type=str, + default="192,192,192,192,192", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "Must be <= each of encoder_dims. Empirically, less than 256 seems to make performance " + " worse.", + ) + + parser.add_argument( + "--zipformer-downsampling-factors", + type=str, + default="1,2,4,8,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--cnn-module-kernels", + type=str, + default="31,31,31,31,31", + help="Sizes of kernels in convolution modules", + ) + + parser.add_argument( + "--use-joint-encoder-layer", + type=str, + default="linear", + choices=["linear", "lstm", "none"], + help="Whether to use a joint layer to combine all branches.", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--short-chunk-size", + type=int, + default=50, + help="""Chunk length of dynamic training, the chunk size would be either + max sequence length of current batch or uniformly sampled from (1, short_chunk_size). + """, + ) + + parser.add_argument( + "--num-left-chunks", + type=int, + default=4, + help="How many left context can be seen in chunks when calculating attention.", + ) + + parser.add_argument( + "--decode-chunk-len", + type=int, + default=32, + help="The chunk size for decoding (in frames before subsampling)", + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=20, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="conv_lstm_transducer_stateless_ctc/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--model-init-ckpt", + type=str, + default=None, + help="""The model checkpoint to initialize the model (either full or part). + If not specified, the model is randomly initialized. + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_500/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--base-lr", type=float, default=0.0001, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=5000, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=2, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network) part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--ctc-loss-scale", + type=float, + default=0.2, + help="Scale for CTC loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=2000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=1, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=100, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warm_step for Noam optimizer. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 2000, + # parameters for SURT + "num_channels": 2, + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed + # parameters for Noam + "model_warm_step": 5000, # arg given to model, not for lrate + # parameters for ctc loss + "beam_size": 10, + "use_double_scores": True, + "env_info": get_env_info(), + } + ) + + return params + + +def get_mask_encoder_model(params: AttributeDict) -> nn.Module: + mask_encoder = DPRNN( + feature_dim=params.feature_dim, + input_size=params.mask_encoder_dim, + hidden_size=params.mask_encoder_dim, + output_size=params.feature_dim * params.num_channels, + segment_size=params.mask_encoder_segment_size, + num_blocks=params.num_mask_encoder_layers, + chunk_width_randomization=params.chunk_width_randomization, + ) + return mask_encoder + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + # TODO: We can add an option to switch between Zipformer and Transformer + def to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + encoder = Zipformer( + num_features=params.feature_dim, + output_downsampling_factor=2, + zipformer_downsampling_factors=to_int_tuple( + params.zipformer_downsampling_factors + ), + encoder_dims=to_int_tuple(params.encoder_dims), + attention_dim=to_int_tuple(params.attention_dims), + encoder_unmasked_dims=to_int_tuple(params.encoder_unmasked_dims), + nhead=to_int_tuple(params.nhead), + feedforward_dim=to_int_tuple(params.feedforward_dims), + cnn_module_kernels=to_int_tuple(params.cnn_module_kernels), + num_encoder_layers=to_int_tuple(params.num_encoder_layers), + num_left_chunks=params.num_left_chunks, + short_chunk_size=params.short_chunk_size, + decode_chunk_size=params.decode_chunk_len // 2, + ) + return encoder + + +def get_joint_encoder_layer(params: AttributeDict) -> nn.Module: + class TakeFirst(nn.Module): + def forward(self, x): + return x[0] + + if params.use_joint_encoder_layer == "linear": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + nn.Linear( + params.num_channels * encoder_dim, params.num_channels * encoder_dim + ), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "lstm": + encoder_dim = int(params.encoder_dims.split(",")[-1]) + joint_layer = nn.Sequential( + Rearrange("(c b) t d -> b t (c d)", c=params.num_channels), + ScaledLSTM( + input_size=params.num_channels * encoder_dim, + hidden_size=params.num_channels * encoder_dim, + num_layers=1, + bias=True, + batch_first=True, + dropout=0.0, + bidirectional=False, + ), + TakeFirst(), + nn.ReLU(), + Rearrange("b t (c d) -> (c b) t d", c=params.num_channels), + ) + elif params.use_joint_encoder_layer == "none": + joint_layer = None + else: + raise ValueError( + f"Unknown joint encoder layer type: {params.use_joint_encoder_layer}" + ) + return joint_layer + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_surt_model( + params: AttributeDict, +) -> nn.Module: + mask_encoder = get_mask_encoder_model(params) + encoder = get_encoder_model(params) + joint_layer = get_joint_encoder_layer(params) + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + + model = SURT( + mask_encoder=mask_encoder, + encoder=encoder, + joint_encoder_layer=joint_layer, + decoder=decoder, + joiner=joiner, + num_channels=params.num_channels, + encoder_dim=int(params.encoder_dims.split(",")[-1]), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_heat_loss(x_masked, batch, num_channels=2) -> Tensor: + """ + Compute HEAT loss for separated sources using the output of mask encoder. + Args: + x_masked: + The output of mask encoder. It is a tensor of shape (B, T, C). + batch: + A batch of data. See `lhotse.dataset.K2SurtDatasetWithSources()` + for the content in it. + num_channels: + The number of output branches in the SURT model. + """ + B, T, D = x_masked[0].shape + device = x_masked[0].device + + # Create training targets for each channel. + targets = [] + for i in range(num_channels): + target = torch.ones_like(x_masked[i]) * LOG_EPSILON + targets.append(target) + + source_feats = batch["source_feats"] + source_boundaries = batch["source_boundaries"] + input_lens = batch["input_lens"].to(device) + # Assign sources to channels based on the HEAT criteria + for b in range(B): + cut_source_feats = source_feats[b] + cut_source_boundaries = source_boundaries[b] + last_seg_end = [0 for _ in range(num_channels)] + for source_feat, (start, end) in zip(cut_source_feats, cut_source_boundaries): + assigned = False + for i in range(num_channels): + if start >= last_seg_end[i]: + targets[i][b, start:end, :] += source_feat.to(device) + last_seg_end[i] = max(end, last_seg_end[i]) + assigned = True + break + if not assigned: + min_end_channel = last_seg_end.index(min(last_seg_end)) + targets[min_end_channel][b, start:end, :] += source_feat + last_seg_end[min_end_channel] = max(end, last_seg_end[min_end_channel]) + + # Get padding mask based on input lengths + pad_mask = torch.arange(T, device=device).expand(B, T) > input_lens.unsqueeze(1) + pad_mask = pad_mask.unsqueeze(-1) + + # Compute masked loss for each channel + losses = torch.zeros((num_channels, B, T, D), device=device) + for i in range(num_channels): + loss = nn.functional.mse_loss(x_masked[i], targets[i], reduction="none") + # Apply padding mask to loss + loss.masked_fill_(pad_mask, 0) + losses[i] = loss + + # loss: C x B x T x D. pad_mask: B x T x 1 + # We want to compute loss for each item in the batch. Each item has loss given + # by the sum over C, and average over T and D. For T, we need to use the padding. + loss = losses.sum(0).mean(-1).sum(-1) / batch["input_lens"].to(device) + return loss + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute RNN-T loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Conformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"].to(device) + feature_lens = batch["input_lens"].to(device) + + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + + # The dataloader returns text as a list of cuts, each of which is a list of channel + # text. We flatten this to a list where all channels are together, i.e., it looks like + # [utt1_ch1, utt2_ch1, ..., uttN_ch1, utt1_ch2, ...., uttN,ch2]. + text = [val for tup in zip(*batch["text"]) for val in tup] + assert len(text) == len(feature) * params.num_channels + + # Convert all channel texts to token IDs and create a ragged tensor. + y = sp.encode(text, out_type=int) + y = k2.RaggedTensor(y).to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.model_warm_step + + with torch.set_grad_enabled(is_training): + (simple_loss, pruned_loss, ctc_loss, x_masked) = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + reduction="none", + subsampling_factor=params.subsampling_factor, + ) + simple_loss_is_finite = torch.isfinite(simple_loss) + pruned_loss_is_finite = torch.isfinite(pruned_loss) + ctc_loss_is_finite = torch.isfinite(ctc_loss) + + # Compute HEAT loss + if is_training and params.heat_loss_scale > 0.0: + heat_loss = compute_heat_loss( + x_masked, batch, num_channels=params.num_channels + ) + else: + heat_loss = torch.tensor(0.0, device=device) + + heat_loss_is_finite = torch.isfinite(heat_loss) + is_finite = ( + simple_loss_is_finite + & pruned_loss_is_finite + & ctc_loss_is_finite + & heat_loss_is_finite + ) + if not torch.all(is_finite): + # logging.info( + # "Not all losses are finite!\n" + # f"simple_losses: {simple_loss}\n" + # f"pruned_losses: {pruned_loss}\n" + # f"ctc_losses: {ctc_loss}\n" + # f"heat_losses: {heat_loss}\n" + # ) + # display_and_save_batch(batch, params=params, sp=sp) + simple_loss = simple_loss[simple_loss_is_finite] + pruned_loss = pruned_loss[pruned_loss_is_finite] + ctc_loss = ctc_loss[ctc_loss_is_finite] + heat_loss = heat_loss[heat_loss_is_finite] + + # If either all simple_loss or pruned_loss is inf or nan, + # we stop the training process by raising an exception + if ( + torch.all(~simple_loss_is_finite) + or torch.all(~pruned_loss_is_finite) + or torch.all(~ctc_loss_is_finite) + or torch.all(~heat_loss_is_finite) + ): + raise ValueError( + "There are too many utterances in this batch " + "leading to inf or nan losses." + ) + + simple_loss_sum = simple_loss.sum() + pruned_loss_sum = pruned_loss.sum() + ctc_loss_sum = ctc_loss.sum() + heat_loss_sum = heat_loss.sum() + + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss = ( + simple_loss_scale * simple_loss_sum + + pruned_loss_scale * pruned_loss_sum + + params.ctc_loss_scale * ctc_loss_sum + + params.heat_loss_scale * heat_loss_sum + ) + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # info["frames"] is an approximate number for two reasons: + # (1) The acutal subsampling factor is ((lens - 1) // 2 - 1) // 2 + # (2) If some utterances in the batch lead to inf/nan loss, they + # are filtered out. + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # `utt_duration` and `utt_pad_proportion` would be normalized by `utterances` # noqa + info["utterances"] = feature.size(0) + # averaged input duration in frames over utterances + info["utt_duration"] = feature_lens.sum().item() + # averaged padding proportion over utterances + info["utt_pad_proportion"] = ( + ((feature.size(1) - feature_lens) / feature.size(1)).sum().item() + ) + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + info["simple_loss"] = simple_loss_sum.detach().cpu().item() + info["pruned_loss"] = pruned_loss_sum.detach().cpu().item() + if params.ctc_loss_scale > 0.0: + info["ctc_loss"] = ctc_loss_sum.detach().cpu().item() + if params.heat_loss_scale > 0.0: + info["heat_loss"] = heat_loss_sum.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + torch.cuda.empty_cache() + model.train() + + tot_loss = MetricsTracker() + + cur_batch_idx = params.get("cur_batch_idx", 0) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx < cur_batch_idx: + continue + cur_batch_idx = batch_idx + + params.batch_idx_train += 1 + batch_size = batch["inputs"].shape[0] + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + set_batch_count(model, params.batch_idx_train) + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + params.cur_batch_idx = batch_idx + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + del params.cur_batch_idx + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + if cur_grad_scale < 1.0 or (cur_grad_scale < 8.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_surt_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + + if checkpoints is None and params.model_init_ckpt is not None: + logging.info( + f"Initializing model with checkpoint from {params.model_init_ckpt}" + ) + init_ckpt = torch.load(params.model_init_ckpt, map_location=device) + model.load_state_dict(init_ckpt["model"], strict=False) + + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + parameters_names = [] + parameters_names.append( + [name_param_pair[0] for name_param_pair in model.named_parameters()] + ) + optimizer = ScaledAdam( + model.parameters(), + lr=params.base_lr, + clipping_scale=2.0, + parameters_names=parameters_names, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + diagnostic = diagnostics.attach_diagnostics(model) + + ami = AmiAsrDataModule(args) + + train_cuts = ami.train_cuts() + train_cuts = train_cuts.filter(lambda c: 0.5 <= c.duration <= 35.0) + dev_cuts = ami.ami_cuts(split="dev", type="ihm-mix") + dev_cuts = dev_cuts.trim_to_supervision_groups(max_pause=0.0).filter( + lambda c: 0.2 <= c.duration <= 60.0 + ) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = ami.train_dataloaders( + train_cuts, + sampler_state_dict=sampler_state_dict, + ) + valid_dl = ami.valid_dataloaders(dev_cuts) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = [sp.encode(text_ch) for text_ch in batch["text"]] + num_tokens = [sum(len(yi) for yi in y_ch) for y_ch in y] + logging.info(f"num tokens: {num_tokens}") + + +def main(): + parser = get_parser() + AmiAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") + +if __name__ == "__main__": + main() diff --git a/egs/ami/SURT/dprnn_zipformer/zipformer.py b/egs/ami/SURT/dprnn_zipformer/zipformer.py new file mode 120000 index 000000000..59b772024 --- /dev/null +++ b/egs/ami/SURT/dprnn_zipformer/zipformer.py @@ -0,0 +1 @@ +../../../libricss/SURT/dprnn_zipformer/zipformer.py \ No newline at end of file diff --git a/egs/ami/SURT/local/add_source_feats.py b/egs/ami/SURT/local/add_source_feats.py new file mode 100755 index 000000000..0917b88a6 --- /dev/null +++ b/egs/ami/SURT/local/add_source_feats.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +# Copyright 2022 Johns Hopkins University (authors: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file adds source features as temporal arrays to the mixture manifests. +It looks for manifests in the directory data/manifests. +""" +import logging +from pathlib import Path + +import numpy as np +from lhotse import CutSet, LilcomChunkyWriter, load_manifest, load_manifest_lazy +from tqdm import tqdm + + +def add_source_feats(): + src_dir = Path("data/manifests") + output_dir = Path("data/fbank") + + logging.info("Reading mixed cuts") + mixed_cuts_clean = load_manifest_lazy(src_dir / "cuts_train_clean.jsonl.gz") + mixed_cuts_reverb = load_manifest_lazy(src_dir / "cuts_train_reverb.jsonl.gz") + + logging.info("Reading source cuts") + source_cuts = load_manifest(src_dir / "ihm_cuts_train_trimmed.jsonl.gz") + + logging.info("Adding source features to the mixed cuts") + pbar = tqdm(total=len(mixed_cuts_clean), desc="Adding source features") + with CutSet.open_writer( + src_dir / "cuts_train_clean_sources.jsonl.gz" + ) as cut_writer_clean, CutSet.open_writer( + src_dir / "cuts_train_reverb_sources.jsonl.gz" + ) as cut_writer_reverb, LilcomChunkyWriter( + output_dir / "feats_train_clean_sources" + ) as source_feat_writer: + for cut_clean, cut_reverb in zip(mixed_cuts_clean, mixed_cuts_reverb): + assert cut_reverb.id == cut_clean.id + "_rvb" + source_feats = [] + source_feat_offsets = [] + cur_offset = 0 + for sup in sorted( + cut_clean.supervisions, key=lambda s: (s.start, s.speaker) + ): + source_cut = source_cuts[sup.id] + source_feats.append(source_cut.load_features()) + source_feat_offsets.append(cur_offset) + cur_offset += source_cut.num_frames + cut_clean.source_feats = source_feat_writer.store_array( + cut_clean.id, np.concatenate(source_feats, axis=0) + ) + cut_clean.source_feat_offsets = source_feat_offsets + cut_writer_clean.write(cut_clean) + # Also write the reverb cut + cut_reverb.source_feats = cut_clean.source_feats + cut_reverb.source_feat_offsets = cut_clean.source_feat_offsets + cut_writer_reverb.write(cut_reverb) + pbar.update(1) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + add_source_feats() diff --git a/egs/ami/SURT/local/compute_fbank_aimix.py b/egs/ami/SURT/local/compute_fbank_aimix.py new file mode 100755 index 000000000..91b3a060b --- /dev/null +++ b/egs/ami/SURT/local/compute_fbank_aimix.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +# Copyright 2022 Johns Hopkins University (authors: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the synthetically mixed AMI and ICSI +train set. +It looks for manifests in the directory data/manifests. + +The generated fbank features are saved in data/fbank. +""" +import logging +import random +import warnings +from pathlib import Path + +import torch +import torch.multiprocessing +import torchaudio +from lhotse import ( + AudioSource, + LilcomChunkyWriter, + Recording, + load_manifest, + load_manifest_lazy, +) +from lhotse.audio import set_ffmpeg_torchaudio_info_enabled +from lhotse.cut import MixedCut, MixTrack, MultiCut +from lhotse.features.kaldifeat import ( + KaldifeatFbank, + KaldifeatFbankConfig, + KaldifeatFrameOptions, + KaldifeatMelOptions, +) +from lhotse.utils import fix_random_seed, uuid4 +from tqdm import tqdm + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") +torchaudio.set_audio_backend("soundfile") +set_ffmpeg_torchaudio_info_enabled(False) + + +def compute_fbank_aimix(): + src_dir = Path("data/manifests") + output_dir = Path("data/fbank") + + sampling_rate = 16000 + num_mel_bins = 80 + + extractor = KaldifeatFbank( + KaldifeatFbankConfig( + frame_opts=KaldifeatFrameOptions(sampling_rate=sampling_rate), + mel_opts=KaldifeatMelOptions(num_bins=num_mel_bins), + device="cuda", + ) + ) + + logging.info("Reading manifests") + train_cuts = load_manifest_lazy(src_dir / "ai-mix_cuts_clean_full.jsonl.gz") + + # only uses RIRs and noises from REVERB challenge + real_rirs = load_manifest(src_dir / "real-rir_recordings_all.jsonl.gz").filter( + lambda r: "RVB2014" in r.id + ) + noises = load_manifest(src_dir / "iso-noise_recordings_all.jsonl.gz").filter( + lambda r: "RVB2014" in r.id + ) + + # Apply perturbation to the training cuts + logging.info("Applying perturbation to the training cuts") + train_cuts_rvb = train_cuts.map( + lambda c: augment( + c, perturb_snr=True, rirs=real_rirs, noises=noises, perturb_loudness=True + ) + ) + + logging.info("Extracting fbank features for training cuts") + _ = train_cuts.compute_and_store_features_batch( + extractor=extractor, + storage_path=output_dir / "ai-mix_feats_clean", + manifest_path=src_dir / "cuts_train_clean.jsonl.gz", + batch_duration=5000, + num_workers=4, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + _ = train_cuts_rvb.compute_and_store_features_batch( + extractor=extractor, + storage_path=output_dir / "ai-mix_feats_reverb", + manifest_path=src_dir / "cuts_train_reverb.jsonl.gz", + batch_duration=5000, + num_workers=4, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + +def augment(cut, perturb_snr=False, rirs=None, noises=None, perturb_loudness=False): + """ + Given a mixed cut, this function optionally applies the following augmentations: + - Perturbing the SNRs of the tracks (in range [-5, 5] dB) + - Reverberation using a randomly selected RIR + - Adding noise + - Perturbing the loudness (in range [-20, -25] dB) + """ + out_cut = cut.drop_features() + + # Perturb the SNRs (optional) + if perturb_snr: + snrs = [random.uniform(-5, 5) for _ in range(len(cut.tracks))] + for i, (track, snr) in enumerate(zip(out_cut.tracks, snrs)): + if i == 0: + # Skip the first track since it is the reference + continue + track.snr = snr + + # Reverberate the cut (optional) + if rirs is not None: + # Select an RIR at random + rir = random.choice(rirs) + # Select a channel at random + rir_channel = random.choice(list(range(rir.num_channels))) + # Reverberate the cut + out_cut = out_cut.reverb_rir(rir_recording=rir, rir_channels=[rir_channel]) + + # Add noise (optional) + if noises is not None: + # Select a noise recording at random + noise = random.choice(noises).to_cut() + if isinstance(noise, MultiCut): + noise = noise.to_mono()[0] + # Select an SNR at random + snr = random.uniform(10, 30) + # Repeat the noise to match the duration of the cut + noise = repeat_cut(noise, out_cut.duration) + out_cut = MixedCut( + id=out_cut.id, + tracks=[ + MixTrack(cut=out_cut, type="MixedCut"), + MixTrack(cut=noise, type="DataCut", snr=snr), + ], + ) + + # Perturb the loudness (optional) + if perturb_loudness: + target_loudness = random.uniform(-20, -25) + out_cut = out_cut.normalize_loudness(target_loudness, mix_first=True) + return out_cut + + +def repeat_cut(cut, duration): + while cut.duration < duration: + cut = cut.mix(cut, offset_other_by=cut.duration) + return cut.truncate(duration=duration) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + fix_random_seed(42) + compute_fbank_aimix() diff --git a/egs/ami/SURT/local/compute_fbank_ami.py b/egs/ami/SURT/local/compute_fbank_ami.py new file mode 100755 index 000000000..351b41765 --- /dev/null +++ b/egs/ami/SURT/local/compute_fbank_ami.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Copyright 2022 Johns Hopkins University (authors: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the AMI dataset. +We compute features for full recordings (i.e., without trimming to supervisions). +This way we can create arbitrary segmentations later. + +The generated fbank features are saved in data/fbank. +""" +import logging +import math +from pathlib import Path + +import torch +import torch.multiprocessing +from lhotse import CutSet, LilcomChunkyWriter +from lhotse.features.kaldifeat import ( + KaldifeatFbank, + KaldifeatFbankConfig, + KaldifeatFrameOptions, + KaldifeatMelOptions, +) +from lhotse.recipes.utils import read_manifests_if_cached + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") + + +def compute_fbank_ami(): + src_dir = Path("data/manifests") + output_dir = Path("data/fbank") + + sampling_rate = 16000 + num_mel_bins = 80 + + extractor = KaldifeatFbank( + KaldifeatFbankConfig( + frame_opts=KaldifeatFrameOptions(sampling_rate=sampling_rate), + mel_opts=KaldifeatMelOptions(num_bins=num_mel_bins), + device="cuda", + ) + ) + + logging.info("Reading manifests") + manifests = {} + for part in ["ihm-mix", "sdm", "mdm8-bf"]: + manifests[part] = read_manifests_if_cached( + dataset_parts=["train", "dev", "test"], + output_dir=src_dir, + prefix=f"ami-{part}", + suffix="jsonl.gz", + ) + + for part in ["ihm-mix", "sdm", "mdm8-bf"]: + for split in ["train", "dev", "test"]: + logging.info(f"Processing {part} {split}") + cuts = CutSet.from_manifests( + **manifests[part][split] + ).compute_and_store_features_batch( + extractor=extractor, + storage_path=output_dir / f"ami-{part}_{split}_feats", + manifest_path=src_dir / f"cuts_ami-{part}_{split}.jsonl.gz", + batch_duration=5000, + num_workers=4, + storage_type=LilcomChunkyWriter, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + compute_fbank_ami() diff --git a/egs/ami/SURT/local/compute_fbank_icsi.py b/egs/ami/SURT/local/compute_fbank_icsi.py new file mode 100755 index 000000000..4e2ff3f3b --- /dev/null +++ b/egs/ami/SURT/local/compute_fbank_icsi.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# Copyright 2022 Johns Hopkins University (authors: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the ICSI dataset. +We compute features for full recordings (i.e., without trimming to supervisions). +This way we can create arbitrary segmentations later. + +The generated fbank features are saved in data/fbank. +""" +import logging +import math +from pathlib import Path + +import torch +import torch.multiprocessing +from lhotse import CutSet, LilcomChunkyWriter +from lhotse.features.kaldifeat import ( + KaldifeatFbank, + KaldifeatFbankConfig, + KaldifeatFrameOptions, + KaldifeatMelOptions, +) +from lhotse.recipes.utils import read_manifests_if_cached + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") + + +def compute_fbank_icsi(): + src_dir = Path("data/manifests") + output_dir = Path("data/fbank") + + sampling_rate = 16000 + num_mel_bins = 80 + + extractor = KaldifeatFbank( + KaldifeatFbankConfig( + frame_opts=KaldifeatFrameOptions(sampling_rate=sampling_rate), + mel_opts=KaldifeatMelOptions(num_bins=num_mel_bins), + device="cuda", + ) + ) + + logging.info("Reading manifests") + manifests = {} + for part in ["ihm-mix", "sdm"]: + manifests[part] = read_manifests_if_cached( + dataset_parts=["train"], + output_dir=src_dir, + prefix=f"icsi-{part}", + suffix="jsonl.gz", + ) + + for part in ["ihm-mix", "sdm"]: + for split in ["train"]: + logging.info(f"Processing {part} {split}") + cuts = CutSet.from_manifests( + **manifests[part][split] + ).compute_and_store_features_batch( + extractor=extractor, + storage_path=output_dir / f"icsi-{part}_{split}_feats", + manifest_path=src_dir / f"cuts_icsi-{part}_{split}.jsonl.gz", + batch_duration=5000, + num_workers=4, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + compute_fbank_icsi() diff --git a/egs/ami/SURT/local/compute_fbank_ihm.py b/egs/ami/SURT/local/compute_fbank_ihm.py new file mode 100755 index 000000000..56f54aa21 --- /dev/null +++ b/egs/ami/SURT/local/compute_fbank_ihm.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# Copyright 2022 Johns Hopkins University (authors: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the trimmed sub-segments which will be +used for simulating the training mixtures. + +The generated fbank features are saved in data/fbank. +""" +import logging +import math +from pathlib import Path + +import torch +import torch.multiprocessing +import torchaudio +from lhotse import CutSet, LilcomChunkyWriter, load_manifest +from lhotse.audio import set_ffmpeg_torchaudio_info_enabled +from lhotse.features.kaldifeat import ( + KaldifeatFbank, + KaldifeatFbankConfig, + KaldifeatFrameOptions, + KaldifeatMelOptions, +) +from lhotse.recipes.utils import read_manifests_if_cached +from tqdm import tqdm + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") +torchaudio.set_audio_backend("soundfile") +set_ffmpeg_torchaudio_info_enabled(False) + + +def compute_fbank_ihm(): + src_dir = Path("data/manifests") + output_dir = Path("data/fbank") + + sampling_rate = 16000 + num_mel_bins = 80 + + extractor = KaldifeatFbank( + KaldifeatFbankConfig( + frame_opts=KaldifeatFrameOptions(sampling_rate=sampling_rate), + mel_opts=KaldifeatMelOptions(num_bins=num_mel_bins), + device="cuda", + ) + ) + + logging.info("Reading manifests") + manifests = {} + for data in ["ami", "icsi"]: + manifests[data] = read_manifests_if_cached( + dataset_parts=["train"], + output_dir=src_dir, + types=["recordings", "supervisions"], + prefix=f"{data}-ihm", + suffix="jsonl.gz", + ) + + logging.info("Computing features") + for data in ["ami", "icsi"]: + cs = CutSet.from_manifests(**manifests[data]["train"]) + cs = cs.trim_to_supervisions(keep_overlapping=False) + cs = cs.normalize_loudness(target=-23.0, affix_id=False) + cs = cs + cs.perturb_speed(0.9) + cs.perturb_speed(1.1) + _ = cs.compute_and_store_features_batch( + extractor=extractor, + storage_path=output_dir / f"{data}-ihm_train_feats", + manifest_path=src_dir / f"{data}-ihm_cuts_train.jsonl.gz", + batch_duration=5000, + num_workers=4, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + compute_fbank_ihm() diff --git a/egs/ami/SURT/local/prepare_ami_train_cuts.py b/egs/ami/SURT/local/prepare_ami_train_cuts.py new file mode 100755 index 000000000..72fced70d --- /dev/null +++ b/egs/ami/SURT/local/prepare_ami_train_cuts.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# Copyright 2022 Johns Hopkins University (authors: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file creates AMI train segments. +""" +import logging +import math +from pathlib import Path + +import torch +import torch.multiprocessing +from lhotse import LilcomChunkyWriter, load_manifest_lazy +from lhotse.cut import Cut, CutSet +from lhotse.utils import EPSILON, add_durations +from tqdm import tqdm + + +def cut_into_windows(cuts: CutSet, duration: float): + """ + This function takes a CutSet and cuts each cut into windows of roughly + `duration` seconds. By roughly, we mean that we try to adjust for the last supervision + that exceeds the duration, or is shorter than the duration. + """ + res = [] + with tqdm() as pbar: + for cut in cuts: + pbar.update(1) + sups = cut.index_supervisions()[cut.id] + sr = cut.sampling_rate + start = 0.0 + end = duration + num_tries = 0 + while start < cut.duration and num_tries < 2: + # Find the supervision that are cut by the window endpoint + hitlist = [iv for iv in sups.at(end) if iv.begin < end] + # If there are no supervisions, we are done + if not hitlist: + res.append( + cut.truncate( + offset=start, + duration=add_durations(end, -start, sampling_rate=sr), + keep_excessive_supervisions=False, + ) + ) + # Update the start and end for the next window + start = end + end = add_durations(end, duration, sampling_rate=sr) + else: + # find ratio of durations cut by the window endpoint + ratios = [ + add_durations(end, -iv.end, sampling_rate=sr) / iv.length() + for iv in hitlist + ] + # we retain the supervisions that have >50% of their duration + # in the window, and discard the others + retained = [] + discarded = [] + for iv, ratio in zip(hitlist, ratios): + if ratio > 0.5: + retained.append(iv) + else: + discarded.append(iv) + cur_end = max(iv.end for iv in retained) if retained else end + res.append( + cut.truncate( + offset=start, + duration=add_durations(cur_end, -start, sampling_rate=sr), + keep_excessive_supervisions=False, + ) + ) + # For the next window, we start at the earliest discarded supervision + next_start = min(iv.begin for iv in discarded) if discarded else end + next_end = add_durations(next_start, duration, sampling_rate=sr) + # It may happen that next_start is the same as start, in which case + # we will advance the window anyway + if next_start == start: + logging.warning( + f"Next start is the same as start: {next_start} == {start} for cut {cut.id}" + ) + start = end + EPSILON + end = add_durations(start, duration, sampling_rate=sr) + num_tries += 1 + else: + start = next_start + end = next_end + return CutSet.from_cuts(res) + + +def prepare_train_cuts(): + src_dir = Path("data/manifests") + + logging.info("Loading the manifests") + train_cuts_ihm = load_manifest_lazy( + src_dir / "cuts_ami-ihm-mix_train.jsonl.gz" + ).map(lambda c: c.with_id(f"{c.id}_ihm-mix")) + train_cuts_sdm = load_manifest_lazy(src_dir / "cuts_ami-sdm_train.jsonl.gz").map( + lambda c: c.with_id(f"{c.id}_sdm") + ) + train_cuts_mdm = load_manifest_lazy( + src_dir / "cuts_ami-mdm8-bf_train.jsonl.gz" + ).map(lambda c: c.with_id(f"{c.id}_mdm8-bf")) + + # Combine all cuts into one CutSet + train_cuts = train_cuts_ihm + train_cuts_sdm + train_cuts_mdm + + train_cuts_1 = train_cuts.trim_to_supervision_groups(max_pause=0.5) + train_cuts_2 = train_cuts.trim_to_supervision_groups(max_pause=0.0) + + # Combine the two segmentations + train_all = train_cuts_1 + train_cuts_2 + + # At this point, some of the cuts may be very long. We will cut them into windows of + # roughly 30 seconds. + logging.info("Cutting the segments into windows of 30 seconds") + train_all_30 = cut_into_windows(train_all, duration=30.0) + logging.info(f"Number of cuts after cutting into windows: {len(train_all_30)}") + + # Show statistics + train_all.describe(full=True) + + # Save the cuts + logging.info("Saving the cuts") + train_all.to_file(src_dir / "cuts_train_ami.jsonl.gz") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + prepare_train_cuts() diff --git a/egs/ami/SURT/local/prepare_icsi_train_cuts.py b/egs/ami/SURT/local/prepare_icsi_train_cuts.py new file mode 100755 index 000000000..818e26bfb --- /dev/null +++ b/egs/ami/SURT/local/prepare_icsi_train_cuts.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# Copyright 2022 Johns Hopkins University (authors: Desh Raj) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file creates ICSI train segments. +""" +import logging +from pathlib import Path + +from lhotse import load_manifest_lazy +from prepare_ami_train_cuts import cut_into_windows + + +def prepare_train_cuts(): + src_dir = Path("data/manifests") + + logging.info("Loading the manifests") + train_cuts_ihm = load_manifest_lazy( + src_dir / "cuts_icsi-ihm-mix_train.jsonl.gz" + ).map(lambda c: c.with_id(f"{c.id}_ihm-mix")) + train_cuts_sdm = load_manifest_lazy(src_dir / "cuts_icsi-sdm_train.jsonl.gz").map( + lambda c: c.with_id(f"{c.id}_sdm") + ) + + # Combine all cuts into one CutSet + train_cuts = train_cuts_ihm + train_cuts_sdm + + train_cuts_1 = train_cuts.trim_to_supervision_groups(max_pause=0.5) + train_cuts_2 = train_cuts.trim_to_supervision_groups(max_pause=0.0) + + # Combine the two segmentations + train_all = train_cuts_1 + train_cuts_2 + + # At this point, some of the cuts may be very long. We will cut them into windows of + # roughly 30 seconds. + logging.info("Cutting the segments into windows of 30 seconds") + train_all_30 = cut_into_windows(train_all, duration=30.0) + logging.info(f"Number of cuts after cutting into windows: {len(train_all_30)}") + + # Show statistics + train_all.describe(full=True) + + # Save the cuts + logging.info("Saving the cuts") + train_all.to_file(src_dir / "cuts_train_icsi.jsonl.gz") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + prepare_train_cuts() diff --git a/egs/ami/SURT/local/prepare_lang_bpe.py b/egs/ami/SURT/local/prepare_lang_bpe.py new file mode 120000 index 000000000..36b40e7fc --- /dev/null +++ b/egs/ami/SURT/local/prepare_lang_bpe.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/prepare_lang_bpe.py \ No newline at end of file diff --git a/egs/ami/SURT/local/train_bpe_model.py b/egs/ami/SURT/local/train_bpe_model.py new file mode 120000 index 000000000..6fad36421 --- /dev/null +++ b/egs/ami/SURT/local/train_bpe_model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/train_bpe_model.py \ No newline at end of file diff --git a/egs/ami/SURT/prepare.sh b/egs/ami/SURT/prepare.sh new file mode 100755 index 000000000..ea4e5baf2 --- /dev/null +++ b/egs/ami/SURT/prepare.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash + +set -eou pipefail + +stage=-1 +stop_stage=100 + +# We assume dl_dir (download dir) contains the following +# directories and files. If not, they will be downloaded +# by this script automatically. +# +# - $dl_dir/ami +# You can find audio and transcripts for AMI in this path. +# +# - $dl_dir/icsi +# You can find audio and transcripts for ICSI in this path. +# +# - $dl_dir/rirs_noises +# This directory contains the RIRS_NOISES corpus downloaded from https://openslr.org/28/. +# +dl_dir=$PWD/download + +. shared/parse_options.sh || exit 1 + +# All files generated by this script are saved in "data". +# You can safely remove "data" and rerun this script to regenerate it. +mkdir -p data +vocab_size=500 + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +log "dl_dir: $dl_dir" + +if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then + log "Stage 0: Download data" + + # If you have pre-downloaded it to /path/to/amicorpus, + # you can create a symlink + # + # ln -sfv /path/to/amicorpus $dl_dir/amicorpus + # + if [ ! -d $dl_dir/amicorpus ]; then + for mic in ihm ihm-mix sdm mdm8-bf; do + lhotse download ami --mic $mic $dl_dir/amicorpus + done + fi + + # If you have pre-downloaded it to /path/to/icsi, + # you can create a symlink + # + # ln -sfv /path/to/icsi $dl_dir/icsi + # + if [ ! -d $dl_dir/icsi ]; then + lhotse download icsi $dl_dir/icsi + fi + + # If you have pre-downloaded it to /path/to/rirs_noises, + # you can create a symlink + # + # ln -sfv /path/to/rirs_noises $dl_dir/ + # + if [ ! -d $dl_dir/rirs_noises ]; then + lhotse download rirs_noises $dl_dir + fi +fi + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + log "Stage 1: Prepare AMI manifests" + # We assume that you have downloaded the AMI corpus + # to $dl_dir/amicorpus. We perform text normalization for the transcripts. + mkdir -p data/manifests + for mic in ihm ihm-mix sdm mdm8-bf; do + log "Preparing AMI manifest for $mic" + lhotse prepare ami --mic $mic --max-words-per-segment 30 --merge-consecutive $dl_dir/amicorpus data/manifests/ + done +fi + +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + log "Stage 2: Prepare ICSI manifests" + # We assume that you have downloaded the ICSI corpus + # to $dl_dir/icsi. We perform text normalization for the transcripts. + mkdir -p data/manifests + log "Preparing ICSI manifest" + for mic in ihm ihm-mix sdm; do + lhotse prepare icsi --mic $mic $dl_dir/icsi data/manifests/ + done +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + log "Stage 3: Prepare RIRs" + # We assume that you have downloaded the RIRS_NOISES corpus + # to $dl_dir/rirs_noises + lhotse prepare rir-noise -p real_rir -p iso_noise $dl_dir/rirs_noises data/manifests +fi + +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + log "Stage 3: Extract features for AMI and ICSI recordings" + python local/compute_fbank_ami.py + python local/compute_fbank_icsi.py +fi + +if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then + log "Stage 5: Create sources for simulating mixtures" + # In the following script, we speed-perturb the IHM recordings and extract features. + python local/compute_fbank_ihm.py + lhotse combine data/manifests/ami-ihm_cuts_train.jsonl.gz \ + data/manifests/icsi-ihm_cuts_train.jsonl.gz - |\ + lhotse cut trim-to-alignments --type word --max-pause 0.5 - - |\ + lhotse filter 'duration<=12.0' - - |\ + shuf | gzip -c > data/manifests/ihm_cuts_train_trimmed.jsonl.gz +fi + +if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then + log "Stage 6: Create training mixtures" + lhotse workflows simulate-meetings \ + --method conversational \ + --same-spk-pause 0.5 \ + --diff-spk-pause 0.5 \ + --diff-spk-overlap 1.0 \ + --prob-diff-spk-overlap 0.8 \ + --num-meetings 200000 \ + --num-speakers-per-meeting 2,3 \ + --max-duration-per-speaker 15.0 \ + --max-utterances-per-speaker 3 \ + --seed 1234 \ + --num-jobs 2 \ + data/manifests/ihm_cuts_train_trimmed.jsonl.gz \ + data/manifests/ai-mix_cuts_clean.jsonl.gz + + python local/compute_fbank_aimix.py + + # Add source features to the manifest (will be used for masking loss) + # This may take ~2 hours. + python local/add_source_feats.py + + # Combine clean and reverb + cat <(gunzip -c data/manifests/cuts_train_clean_sources.jsonl.gz) \ + <(gunzip -c data/manifests/cuts_train_reverb_sources.jsonl.gz) |\ + shuf | gzip -c > data/manifests/cuts_train_comb_sources.jsonl.gz +fi + +if [ $stage -le 7 ] && [ $stop_stage -ge 7 ]; then + log "Stage 7: Create training mixtures from real sessions" + python local/prepare_ami_train_cuts.py + python local/prepare_icsi_train_cuts.py + + # Combine AMI and ICSI + cat <(gunzip -c data/manifests/cuts_train_ami.jsonl.gz) \ + <(gunzip -c data/manifests/cuts_train_icsi.jsonl.gz) |\ + shuf | gzip -c > data/manifests/cuts_train_ami_icsi.jsonl.gz +fi + +if [ $stage -le 8 ] && [ $stop_stage -ge 8 ]; then + log "Stage 8: Dump transcripts for BPE model training (using AMI and ICSI)." + mkdir -p data/lm + cat <(gunzip -c data/manifests/ami-sdm_supervisions_train.jsonl.gz | jq '.text' | sed 's:"::g') \ + <(gunzip -c data/manifests/icsi-sdm_supervisions_train.jsonl.gz | jq '.text' | sed 's:"::g') \ + > data/lm/transcript_words.txt +fi + +if [ $stage -le 9 ] && [ $stop_stage -ge 9 ]; then + log "Stage 9: Prepare BPE based lang (combining AMI and ICSI)" + + lang_dir=data/lang_bpe_${vocab_size} + mkdir -p $lang_dir + + # Add special words to words.txt + echo " 0" > $lang_dir/words.txt + echo "!SIL 1" >> $lang_dir/words.txt + echo " 2" >> $lang_dir/words.txt + + # Add regular words to words.txt + cat data/lm/transcript_words.txt | grep -o -E '\w+' | sort -u | awk '{print $0,NR+2}' >> $lang_dir/words.txt + + # Add remaining special word symbols expected by LM scripts. + num_words=$(cat $lang_dir/words.txt | wc -l) + echo " ${num_words}" >> $lang_dir/words.txt + num_words=$(cat $lang_dir/words.txt | wc -l) + echo " ${num_words}" >> $lang_dir/words.txt + num_words=$(cat $lang_dir/words.txt | wc -l) + echo "#0 ${num_words}" >> $lang_dir/words.txt + + ./local/train_bpe_model.py \ + --lang-dir $lang_dir \ + --vocab-size $vocab_size \ + --transcript data/lm/transcript_words.txt + + if [ ! -f $lang_dir/L_disambig.pt ]; then + ./local/prepare_lang_bpe.py --lang-dir $lang_dir + fi +fi diff --git a/egs/ami/SURT/shared b/egs/ami/SURT/shared new file mode 120000 index 000000000..4cbd91a7e --- /dev/null +++ b/egs/ami/SURT/shared @@ -0,0 +1 @@ +../../../icefall/shared \ No newline at end of file From 5ed6fc0e6d9afeebaf86ec83c16d9ff2c8d6a0ba Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:37:14 +0800 Subject: [PATCH 053/100] add sym link (#1170) --- egs/wenetspeech/ASR/local/prepare_char_lm_training_data.py | 1 + egs/wenetspeech/ASR/local/sort_lm_training_data.py | 1 + 2 files changed, 2 insertions(+) create mode 120000 egs/wenetspeech/ASR/local/prepare_char_lm_training_data.py create mode 120000 egs/wenetspeech/ASR/local/sort_lm_training_data.py diff --git a/egs/wenetspeech/ASR/local/prepare_char_lm_training_data.py b/egs/wenetspeech/ASR/local/prepare_char_lm_training_data.py new file mode 120000 index 000000000..2374cafdd --- /dev/null +++ b/egs/wenetspeech/ASR/local/prepare_char_lm_training_data.py @@ -0,0 +1 @@ +../../../aishell/ASR/local/prepare_char_lm_training_data.py \ No newline at end of file diff --git a/egs/wenetspeech/ASR/local/sort_lm_training_data.py b/egs/wenetspeech/ASR/local/sort_lm_training_data.py new file mode 120000 index 000000000..efef2c445 --- /dev/null +++ b/egs/wenetspeech/ASR/local/sort_lm_training_data.py @@ -0,0 +1 @@ +../../../aishell/ASR/local/sort_lm_training_data.py \ No newline at end of file From 4ab7d610081c0c3b38dd851298cb45381e6ac591 Mon Sep 17 00:00:00 2001 From: zr_jin <60612200+JinZr@users.noreply.github.com> Date: Sat, 15 Jul 2023 12:39:32 +0800 Subject: [PATCH 054/100] removed `batch_name` to fix a KeyError with "uttid" (#1172) --- egs/librispeech/ASR/conformer_ctc2/train.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/egs/librispeech/ASR/conformer_ctc2/train.py b/egs/librispeech/ASR/conformer_ctc2/train.py index 3366af13e..c4a13b101 100755 --- a/egs/librispeech/ASR/conformer_ctc2/train.py +++ b/egs/librispeech/ASR/conformer_ctc2/train.py @@ -675,7 +675,6 @@ def train_one_epoch( for batch_idx, batch in enumerate(train_dl): params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) - batch_name = batch["supervisions"]["uttid"] with torch.cuda.amp.autocast(enabled=params.use_fp16): loss, loss_info = compute_loss( @@ -698,10 +697,7 @@ def train_one_epoch( scaler.scale(loss).backward() except RuntimeError as e: if "CUDA out of memory" in str(e): - logging.error( - f"failing batch size:{batch_size} " - f"failing batch names {batch_name}" - ) + logging.error(f"failing batch size:{batch_size} ") raise scheduler.step_batch(params.batch_idx_train) @@ -756,10 +752,7 @@ def train_one_epoch( if loss_info["ctc_loss"] == float("inf") or loss_info["att_loss"] == float( "inf" ): - logging.error( - "Your loss contains inf, something goes wrong" - f"failing batch names {batch_name}" - ) + logging.error("Your loss contains inf, something goes wrong") if tb_writer is not None: tb_writer.add_scalar( "train/learning_rate", cur_lr, params.batch_idx_train From 1dbbd7759ef707eca36bb899bcea8e32afc52282 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 25 Jul 2023 14:46:18 +0800 Subject: [PATCH 055/100] Add tests for subsample.py and fix typos (#1180) --- .github/workflows/test.yml | 57 ++----- .../pruned_transducer_stateless2/conformer.py | 2 + .../pruned_transducer_stateless3/test_onnx.py | 6 +- .../pruned_transducer_stateless7/test_onnx.py | 3 +- egs/librispeech/ASR/zipformer/.gitignore | 1 + egs/librispeech/ASR/zipformer/model.py | 2 +- egs/librispeech/ASR/zipformer/scaling.py | 14 +- egs/librispeech/ASR/zipformer/subsampling.py | 23 +-- egs/librispeech/ASR/zipformer/test_scaling.py | 82 ++++++++++ .../ASR/zipformer/test_subsampling.py | 152 ++++++++++++++++++ egs/librispeech/ASR/zipformer/zipformer.py | 4 +- 11 files changed, 276 insertions(+), 70 deletions(-) create mode 100644 egs/librispeech/ASR/zipformer/.gitignore create mode 100755 egs/librispeech/ASR/zipformer/test_scaling.py create mode 100755 egs/librispeech/ASR/zipformer/test_subsampling.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e04fb5655..363556bb7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,9 +35,9 @@ jobs: matrix: os: [ubuntu-latest] python-version: ["3.8"] - torch: ["1.10.0"] - torchaudio: ["0.10.0"] - k2-version: ["1.23.2.dev20221201"] + torch: ["1.13.0"] + torchaudio: ["0.13.0"] + k2-version: ["1.24.3.dev20230719"] fail-fast: false @@ -66,14 +66,14 @@ jobs: pip install torch==${{ matrix.torch }}+cpu -f https://download.pytorch.org/whl/cpu/torch_stable.html pip install torchaudio==${{ matrix.torchaudio }}+cpu -f https://download.pytorch.org/whl/cpu/torch_stable.html - pip install k2==${{ matrix.k2-version }}+cpu.torch${{ matrix.torch }} -f https://k2-fsa.org/nightly/ + pip install k2==${{ matrix.k2-version }}+cpu.torch${{ matrix.torch }} -f https://k2-fsa.github.io/k2/cpu.html pip install git+https://github.com/lhotse-speech/lhotse # icefall requirements pip uninstall -y protobuf pip install --no-binary protobuf protobuf==3.20.* pip install kaldifst - pip install onnxruntime + pip install onnxruntime matplotlib pip install -r requirements.txt - name: Install graphviz @@ -83,13 +83,6 @@ jobs: python3 -m pip install -qq graphviz sudo apt-get -qq install graphviz - - name: Install graphviz - if: startsWith(matrix.os, 'macos') - shell: bash - run: | - python3 -m pip install -qq graphviz - brew install -q graphviz - - name: Run tests if: startsWith(matrix.os, 'ubuntu') run: | @@ -129,40 +122,10 @@ jobs: cd ../transducer_lstm pytest -v -s - - name: Run tests - if: startsWith(matrix.os, 'macos') - run: | - ls -lh - export PYTHONPATH=$PWD:$PWD/lhotse:$PYTHONPATH - lib_path=$(python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())") - echo "lib_path: $lib_path" - export DYLD_LIBRARY_PATH=$lib_path:$DYLD_LIBRARY_PATH - pytest -v -s ./test - - # run tests for conformer ctc - cd egs/librispeech/ASR/conformer_ctc + cd ../zipformer pytest -v -s - cd ../pruned_transducer_stateless - pytest -v -s - - cd ../pruned_transducer_stateless2 - pytest -v -s - - cd ../pruned_transducer_stateless3 - pytest -v -s - - cd ../pruned_transducer_stateless4 - pytest -v -s - - cd ../transducer_stateless - pytest -v -s - - # cd ../transducer - # pytest -v -s - - cd ../transducer_stateless2 - pytest -v -s - - cd ../transducer_lstm - pytest -v -s + - uses: actions/upload-artifact@v2 + with: + path: egs/librispeech/ASR/zipformer/swoosh.pdf + name: swoosh.pdf diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/conformer.py b/egs/librispeech/ASR/pruned_transducer_stateless2/conformer.py index 9bac46004..bcd419fb7 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/conformer.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/conformer.py @@ -849,6 +849,8 @@ class RelPositionalEncoding(torch.nn.Module): torch.Tensor: Encoded tensor (batch, 2*time-1, `*`). """ + if isinstance(left_context, torch.Tensor): + left_context = left_context.item() self.extend_pe(x, left_context) x_size_1 = x.size(1) + left_context pos_emb = self.pe[ diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py index 598fcf344..810da8da6 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py @@ -113,7 +113,7 @@ def test_rel_pos(): torch.onnx.export( encoder_pos, - x, + (x, torch.zeros(1, dtype=torch.int64)), filename, verbose=False, opset_version=opset_version, @@ -139,7 +139,9 @@ def test_rel_pos(): assert input_nodes[0].name == "x" assert input_nodes[0].shape == ["N", "T", num_features] - inputs = {input_nodes[0].name: x.numpy()} + inputs = { + input_nodes[0].name: x.numpy(), + } onnx_y, onnx_pos_emb = session.run(["y", "pos_emb"], inputs) onnx_y = torch.from_numpy(onnx_y) onnx_pos_emb = torch.from_numpy(onnx_pos_emb) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py index 2440d267c..1e9b67226 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py @@ -265,7 +265,7 @@ def test_zipformer_encoder(): torch.onnx.export( encoder, - (x), + (x, torch.ones(1, dtype=torch.float32)), filename, verbose=False, opset_version=opset_version, @@ -289,6 +289,7 @@ def test_zipformer_encoder(): input_nodes = session.get_inputs() inputs = { input_nodes[0].name: x.numpy(), + input_nodes[1].name: torch.ones(1, dtype=torch.float32).numpy(), } onnx_y = session.run(["y"], inputs)[0] onnx_y = torch.from_numpy(onnx_y) diff --git a/egs/librispeech/ASR/zipformer/.gitignore b/egs/librispeech/ASR/zipformer/.gitignore new file mode 100644 index 000000000..e47ac1582 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/.gitignore @@ -0,0 +1 @@ +swoosh.pdf diff --git a/egs/librispeech/ASR/zipformer/model.py b/egs/librispeech/ASR/zipformer/model.py index b541ee697..f2f86af47 100644 --- a/egs/librispeech/ASR/zipformer/model.py +++ b/egs/librispeech/ASR/zipformer/model.py @@ -320,7 +320,7 @@ class AsrModel(nn.Module): assert x_lens.ndim == 1, x_lens.shape assert y.num_axes == 2, y.num_axes - assert x.size(0) == x_lens.size(0) == y.dim0 + assert x.size(0) == x_lens.size(0) == y.dim0, (x.shape, x_lens.shape, y.dim0) # Compute encoder outputs encoder_out, encoder_out_lens = self.forward_encoder(x, x_lens) diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py index 4ee7b7826..7c98ef045 100644 --- a/egs/librispeech/ASR/zipformer/scaling.py +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -125,7 +125,7 @@ class PiecewiseLinear(object): p: 'PiecewiseLinear', include_crossings: bool = False): """ - Returns (self_mod, p_mod) which are equivalent piecewise lienar + Returns (self_mod, p_mod) which are equivalent piecewise linear functions to self and p, but with the same x values. p: the other piecewise linear function @@ -166,7 +166,7 @@ class ScheduledFloat(torch.nn.Module): in, float(parent_module.whatever), and use it as something like a dropout prob. It is a floating point value whose value changes depending on the batch count of the - training loop. It is a piecewise linear function where you specifiy the (x,y) pairs + training loop. It is a piecewise linear function where you specify the (x,y) pairs in sorted order on x; x corresponds to the batch index. For batch-index values before the first x or after the last x, we just use the first or last y value. @@ -343,7 +343,7 @@ class MaxEigLimiterFunction(torch.autograd.Function): class BiasNormFunction(torch.autograd.Function): # This computes: # scales = (torch.mean((x - bias) ** 2, keepdim=True)) ** -0.5 * log_scale.exp() - # return (x - bias) * scales + # return x * scales # (after unsqueezing the bias), but it does it in a memory-efficient way so that # it can just store the returned value (chances are, this will also be needed for # some other reason, related to the next operation, so we can save memory). @@ -400,8 +400,8 @@ class BiasNorm(torch.nn.Module): Args: num_channels: the number of channels, e.g. 512. channel_dim: the axis/dimension corresponding to the channel, - interprted as an offset from the input's ndim if negative. - shis is NOT the num_channels; it should typically be one of + interpreted as an offset from the input's ndim if negative. + This is NOT the num_channels; it should typically be one of {-2, -1, 0, 1, 2, 3}. log_scale: the initial log-scale that we multiply the output by; this is learnable. @@ -1286,7 +1286,7 @@ class Dropout3(nn.Module): class SwooshLFunction(torch.autograd.Function): """ - swoosh(x) = log(1 + exp(x-4)) - 0.08*x - 0.035 + swoosh_l(x) = log(1 + exp(x-4)) - 0.08*x - 0.035 """ @staticmethod @@ -1361,7 +1361,7 @@ class SwooshLOnnx(torch.nn.Module): class SwooshRFunction(torch.autograd.Function): """ - swoosh(x) = log(1 + exp(x-1)) - 0.08*x - 0.313261687 + swoosh_r(x) = log(1 + exp(x-1)) - 0.08*x - 0.313261687 derivatives are between -0.08 and 0.92. """ diff --git a/egs/librispeech/ASR/zipformer/subsampling.py b/egs/librispeech/ASR/zipformer/subsampling.py index d6bf57db4..6532ddccb 100644 --- a/egs/librispeech/ASR/zipformer/subsampling.py +++ b/egs/librispeech/ASR/zipformer/subsampling.py @@ -138,9 +138,11 @@ class ConvNeXt(nn.Module): x = bypass + x x = self.out_balancer(x) - x = x.transpose(1, 3) # (N, W, H, C); need channel dim to be last - x = self.out_whiten(x) - x = x.transpose(1, 3) # (N, C, H, W) + + if x.requires_grad: + x = x.transpose(1, 3) # (N, W, H, C); need channel dim to be last + x = self.out_whiten(x) + x = x.transpose(1, 3) # (N, C, H, W) return x @@ -266,6 +268,7 @@ class Conv2dSubsampling(nn.Module): # just one convnext layer self.convnext = ConvNeXt(layer3_channels, kernel_size=(7, 7)) + # (in_channels-3)//4 self.out_width = (((in_channels - 1) // 2) - 1) // 2 self.layer3_channels = layer3_channels @@ -299,7 +302,7 @@ class Conv2dSubsampling(nn.Module): A tensor of shape (batch_size,) containing the number of frames in Returns: - - a tensor of shape (N, ((T-1)//2 - 1)//2, odim) + - a tensor of shape (N, (T-7)//2, odim) - output lengths, of shape (batch_size,) """ # On entry, x is (N, T, idim) @@ -310,14 +313,14 @@ class Conv2dSubsampling(nn.Module): x = self.conv(x) x = self.convnext(x) - # Now x is of shape (N, odim, ((T-3)//2 - 1)//2, ((idim-1)//2 - 1)//2) + # Now x is of shape (N, odim, (T-7)//2, (idim-3)//4) b, c, t, f = x.size() x = x.transpose(1, 2).reshape(b, t, c * f) - # now x: (N, ((T-1)//2 - 1))//2, out_width * layer3_channels)) + # now x: (N, (T-7)//2, out_width * layer3_channels)) x = self.out(x) - # Now x is of shape (N, ((T-1)//2 - 1))//2, odim) + # Now x is of shape (N, (T-7)//2, odim) x = self.out_whiten(x) x = self.out_norm(x) x = self.dropout(x) @@ -328,7 +331,7 @@ class Conv2dSubsampling(nn.Module): with warnings.catch_warnings(): warnings.simplefilter("ignore") x_lens = (x_lens - 7) // 2 - assert x.size(1) == x_lens.max().item() + assert x.size(1) == x_lens.max().item() , (x.size(1), x_lens.max()) return x, x_lens @@ -347,7 +350,7 @@ class Conv2dSubsampling(nn.Module): A tensor of shape (batch_size,) containing the number of frames in Returns: - - a tensor of shape (N, ((T-1)//2 - 1)//2, odim) + - a tensor of shape (N, (T-7)//2, odim) - output lengths, of shape (batch_size,) - updated cache """ @@ -383,7 +386,7 @@ class Conv2dSubsampling(nn.Module): assert self.convnext.padding[0] == 3 x_lens = (x_lens - 7) // 2 - 3 - assert x.size(1) == x_lens.max().item() + assert x.size(1) == x_lens.max().item(), (x.shape, x_lens.max()) return x, x_lens, cached_left_pad diff --git a/egs/librispeech/ASR/zipformer/test_scaling.py b/egs/librispeech/ASR/zipformer/test_scaling.py new file mode 100755 index 000000000..5c04291e7 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/test_scaling.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +import matplotlib.pyplot as plt +import torch +from scaling import PiecewiseLinear, ScheduledFloat, SwooshL, SwooshR + + +def test_piecewise_linear(): + # An identity map in the range [0, 1]. + # 1 - identity map in the range [1, 2] + # x1=0, y1=0 + # x2=1, y2=1 + # x3=2, y3=0 + pl = PiecewiseLinear((0, 0), (1, 1), (2, 0)) + assert pl(0.25) == 0.25, pl(0.25) + assert pl(0.625) == 0.625, pl(0.625) + assert pl(1.25) == 0.75, pl(1.25) + + assert pl(-10) == pl(0), pl(-10) # out of range + assert pl(10) == pl(2), pl(10) # out of range + + # multiplication + pl10 = pl * 10 + assert pl10(1) == 10 * pl(1) + assert pl10(0.5) == 10 * pl(0.5) + + +def test_scheduled_float(): + # Initial value is 0.2 and it decreases linearly towards 0 at 4000 + dropout = ScheduledFloat((0, 0.2), (4000, 0.0), default=0.0) + dropout.batch_count = 0 + assert float(dropout) == 0.2, (float(dropout), dropout.batch_count) + + dropout.batch_count = 1000 + assert abs(float(dropout) - 0.15) < 1e-5, (float(dropout), dropout.batch_count) + + dropout.batch_count = 2000 + assert float(dropout) == 0.1, (float(dropout), dropout.batch_count) + + dropout.batch_count = 3000 + assert abs(float(dropout) - 0.05) < 1e-5, (float(dropout), dropout.batch_count) + + dropout.batch_count = 4000 + assert float(dropout) == 0.0, (float(dropout), dropout.batch_count) + + dropout.batch_count = 5000 # out of range + assert float(dropout) == 0.0, (float(dropout), dropout.batch_count) + + +def test_swoosh(): + x1 = torch.linspace(start=-10, end=0, steps=100, dtype=torch.float32) + x2 = torch.linspace(start=0, end=10, steps=100, dtype=torch.float32) + x = torch.cat([x1, x2[1:]]) + + left = SwooshL()(x) + r = SwooshR()(x) + + relu = torch.nn.functional.relu(x) + print(left[x == 0], r[x == 0]) + plt.plot(x, left, "k") + plt.plot(x, r, "r") + plt.plot(x, relu, "b") + plt.axis([-10, 10, -1, 10]) # [xmin, xmax, ymin, ymax] + plt.legend( + [ + "SwooshL(x) = log(1 + exp(x-4)) - 0.08x - 0.035 ", + "SwooshR(x) = log(1 + exp(x-1)) - 0.08x - 0.313261687", + "ReLU(x) = max(0, x)", + ] + ) + plt.grid() + plt.savefig("swoosh.pdf") + + +def main(): + test_piecewise_linear() + test_scheduled_float() + test_swoosh() + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/test_subsampling.py b/egs/librispeech/ASR/zipformer/test_subsampling.py new file mode 100755 index 000000000..078227fb6 --- /dev/null +++ b/egs/librispeech/ASR/zipformer/test_subsampling.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 + +import torch +from scaling import ScheduledFloat +from subsampling import Conv2dSubsampling + + +def test_conv2d_subsampling(): + layer1_channels = 8 + layer2_channels = 32 + layer3_channels = 128 + + out_channels = 192 + encoder_embed = Conv2dSubsampling( + in_channels=80, + out_channels=out_channels, + layer1_channels=layer1_channels, + layer2_channels=layer2_channels, + layer3_channels=layer3_channels, + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + ) + N = 2 + T = 200 + num_features = 80 + x = torch.rand(N, T, num_features) + x_copy = x.clone() + + x = x.unsqueeze(1) # (N, 1, T, num_features) + + x = encoder_embed.conv[0](x) # conv2d, in 1, out 8, kernel 3, padding (0,1) + assert x.shape == (N, layer1_channels, T - 2, num_features) + # (2, 8, 198, 80) + + x = encoder_embed.conv[1](x) # scale grad + x = encoder_embed.conv[2](x) # balancer + x = encoder_embed.conv[3](x) # swooshR + + x = encoder_embed.conv[4](x) # conv2d, in 8, out 32, kernel 3, stride 2 + assert x.shape == ( + N, + layer2_channels, + ((T - 2) - 3) // 2 + 1, + (num_features - 3) // 2 + 1, + ) + # (2, 32, 98, 39) + + x = encoder_embed.conv[5](x) # balancer + x = encoder_embed.conv[6](x) # swooshR + + # conv2d: + # in 32, out 128, kernel 3, stride (1, 2) + x = encoder_embed.conv[7](x) + assert x.shape == ( + N, + layer3_channels, + (((T - 2) - 3) // 2 + 1) - 2, + (((num_features - 3) // 2 + 1) - 3) // 2 + 1, + ) + # (2, 128, 96, 19) + + x = encoder_embed.conv[8](x) # balancer + x = encoder_embed.conv[9](x) # swooshR + + # (((T - 2) - 3) // 2 + 1) - 2 + # = (T - 2) - 3) // 2 + 1 - 2 + # = ((T - 2) - 3) // 2 - 1 + # = (T - 2 - 3) // 2 - 1 + # = (T - 5) // 2 - 1 + # = (T - 7) // 2 + assert x.shape[2] == (x_copy.shape[1] - 7) // 2 + + # (((num_features - 3) // 2 + 1) - 3) // 2 + 1, + # = ((num_features - 3) // 2 + 1 - 3) // 2 + 1, + # = ((num_features - 3) // 2 - 2) // 2 + 1, + # = (num_features - 3 - 4) // 2 // 2 + 1, + # = (num_features - 7) // 2 // 2 + 1, + # = (num_features - 7) // 4 + 1, + # = (num_features - 3) // 4 + assert x.shape[3] == (x_copy.shape[2] - 3) // 4 + + assert x.shape == (N, layer3_channels, (T - 7) // 2, (num_features - 3) // 4) + + # Input shape to convnext is + # + # (N, layer3_channels, (T-7)//2, (num_features - 3)//4) + + # conv2d: in layer3_channels, out layer3_channels, groups layer3_channels + # kernel_size 7, padding 3 + x = encoder_embed.convnext.depthwise_conv(x) + assert x.shape == (N, layer3_channels, (T - 7) // 2, (num_features - 3) // 4) + + # conv2d: in layer3_channels, out hidden_ratio * layer3_channels, kernel_size 1 + x = encoder_embed.convnext.pointwise_conv1(x) + assert x.shape == (N, layer3_channels * 3, (T - 7) // 2, (num_features - 3) // 4) + + x = encoder_embed.convnext.hidden_balancer(x) # balancer + x = encoder_embed.convnext.activation(x) # swooshL + + # conv2d: in hidden_ratio * layer3_channels, out layer3_channels, kernel 1 + x = encoder_embed.convnext.pointwise_conv2(x) + assert x.shape == (N, layer3_channels, (T - 7) // 2, (num_features - 3) // 4) + + # bypass and layer drop, omitted here. + x = encoder_embed.convnext.out_balancer(x) + + # Note: the input and output shape of ConvNeXt are the same + + x = x.transpose(1, 2).reshape(N, (T - 7) // 2, -1) + assert x.shape == (N, (T - 7) // 2, layer3_channels * ((num_features - 3) // 4)) + + x = encoder_embed.out(x) + assert x.shape == (N, (T - 7) // 2, out_channels) + + x = encoder_embed.out_whiten(x) + x = encoder_embed.out_norm(x) + # final layer is dropout + + # test streaming forward + + subsampling_factor = 2 + cached_left_padding = encoder_embed.get_init_states(batch_size=N) + depthwise_conv_kernel_size = 7 + pad_size = (depthwise_conv_kernel_size - 1) // 2 + + assert cached_left_padding.shape == ( + N, + layer3_channels, + pad_size, + (num_features - 3) // 4, + ) + + chunk_size = 16 + right_padding = pad_size * subsampling_factor + T = chunk_size * subsampling_factor + 7 + right_padding + x = torch.rand(N, T, num_features) + x_lens = torch.tensor([T] * N) + y, y_lens, next_cached_left_padding = encoder_embed.streaming_forward( + x, x_lens, cached_left_padding + ) + + assert y.shape == (N, chunk_size, out_channels), y.shape + assert next_cached_left_padding.shape == cached_left_padding.shape + + assert y.shape[1] == y_lens[0] == y_lens[1] + + +def main(): + test_conv2d_subsampling() + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/ASR/zipformer/zipformer.py b/egs/librispeech/ASR/zipformer/zipformer.py index 7d98dbeb1..b39af02b8 100644 --- a/egs/librispeech/ASR/zipformer/zipformer.py +++ b/egs/librispeech/ASR/zipformer/zipformer.py @@ -219,7 +219,7 @@ class Zipformer2(EncoderInterface): (num_frames0, batch_size, _encoder_dims0) = x.shape - assert self.encoder_dim[0] == _encoder_dims0 + assert self.encoder_dim[0] == _encoder_dims0, (self.encoder_dim[0], _encoder_dims0) feature_mask_dropout_prob = 0.125 @@ -334,7 +334,7 @@ class Zipformer2(EncoderInterface): x = self._get_full_dim_output(outputs) x = self.downsample_output(x) # class Downsample has this rounding behavior.. - assert self.output_downsampling_factor == 2 + assert self.output_downsampling_factor == 2, self.output_downsampling_factor if torch.jit.is_scripting() or torch.jit.is_tracing(): lengths = (x_lens + 1) // 2 else: From 80d922c1583b9b7fb7e9b47008302cdc74ef58b7 Mon Sep 17 00:00:00 2001 From: kobenaxie <572745565@qq.com> Date: Wed, 26 Jul 2023 16:54:42 +0800 Subject: [PATCH 056/100] Update preprocess_commonvoice.py to fix text normalization bug. (#1181) --- egs/commonvoice/ASR/local/preprocess_commonvoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egs/commonvoice/ASR/local/preprocess_commonvoice.py b/egs/commonvoice/ASR/local/preprocess_commonvoice.py index c5ec14502..e60459765 100755 --- a/egs/commonvoice/ASR/local/preprocess_commonvoice.py +++ b/egs/commonvoice/ASR/local/preprocess_commonvoice.py @@ -45,7 +45,7 @@ def get_args(): def normalize_text(utt: str) -> str: utt = re.sub(r"[{0}]+".format("-"), " ", utt) - return re.sub(r"[^a-zA-Z\s]", "", utt).upper() + return re.sub(r"[^a-zA-Z\s']", "", utt).upper() def preprocess_commonvoice( From 625b33e9ad15961239ea77d12472428d8006085d Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:08:20 +0800 Subject: [PATCH 057/100] Update descriptions for different decoding methods with external LMs (#1185) * add some descriptions * minor updates --- .../decoding-with-langugage-models/index.rst | 21 +++++++++++++++++++ .../rescoring.rst | 14 ++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/source/decoding-with-langugage-models/index.rst b/docs/source/decoding-with-langugage-models/index.rst index 577ebbdfb..6e5e3a4d9 100644 --- a/docs/source/decoding-with-langugage-models/index.rst +++ b/docs/source/decoding-with-langugage-models/index.rst @@ -4,6 +4,27 @@ Decoding with language models This section describes how to use external langugage models during decoding to improve the WER of transducer models. +The following decoding methods with external langugage models are available: + + +.. list-table:: LM-rescoring-based methods vs shallow-fusion-based methods (The numbers in each field is WER on test-clean, WER on test-other and decoding time on test-clean) + :widths: 25 50 + :header-rows: 1 + + * - Decoding method + - beam=4 + * - ``modified_beam_search`` + - Beam search (i.e. really n-best decoding, the "beam" is the value of n), similar to the original RNN-T paper. Note, this method does not use language model. + * - ``modified_beam_search_lm_shallow_fusion`` + - As ``modified_beam_search``, but interpolate RNN-T scores with language model scores, also known as shallow fusion + * - ``modified_beam_search_LODR`` + - As ``modified_beam_search_lm_shallow_fusion``, but subtract score of a (BPE-symbol-level) bigram backoff language model used as an approximation to the internal language model of RNN-T. + * - ``modified_beam_search_lm_rescore`` + - As ``modified_beam_search``, but rescore the n-best hypotheses with external language model (e.g. RNNLM) and re-rank them. + * - ``modified_beam_search_lm_rescore_LODR`` + - As ``modified_beam_search_lm_rescore``, but also subtract the score of a (BPE-symbol-level) bigram backoff language model during re-ranking. + + .. toctree:: :maxdepth: 2 diff --git a/docs/source/decoding-with-langugage-models/rescoring.rst b/docs/source/decoding-with-langugage-models/rescoring.rst index d71acc1e5..de7e700d0 100644 --- a/docs/source/decoding-with-langugage-models/rescoring.rst +++ b/docs/source/decoding-with-langugage-models/rescoring.rst @@ -4,7 +4,11 @@ LM rescoring for Transducer ================================= LM rescoring is a commonly used approach to incorporate external LM information. Unlike shallow-fusion-based +<<<<<<< HEAD +methods (see :ref:`shallow_fusion`, :ref:`LODR`), rescoring is usually performed to re-rank the n-best hypotheses after beam search. +======= methods (see :ref:`shallow-fusion`, :ref:`LODR`), rescoring is usually performed to re-rank the n-best hypotheses after beam search. +>>>>>>> 80d922c1583b9b7fb7e9b47008302cdc74ef58b7 Rescoring is usually more efficient than shallow fusion since less computation is performed on the external LM. In this tutorial, we will show you how to use external LM to rescore the n-best hypotheses decoded from neural transducer models in `icefall `__. @@ -225,23 +229,23 @@ Here, we benchmark the WERs and decoding speed of them: - beam=4 - beam=8 - beam=12 - * - `modified_beam_search` + * - ``modified_beam_search`` - 3.11/7.93; 132s - 3.1/7.95; 177s - 3.1/7.96; 210s - * - `modified_beam_search_lm_shallow_fusion` + * - ``modified_beam_search_lm_shallow_fusion`` - 2.77/7.08; 262s - 2.62/6.65; 352s - 2.58/6.65; 488s - * - LODR + * - ``modified_beam_search_LODR`` - 2.61/6.74; 400s - 2.45/6.38; 610s - 2.4/6.23; 870s - * - `modified_beam_search_lm_rescore` + * - ``modified_beam_search_lm_rescore`` - 2.93/7.6; 156s - 2.67/7.11; 203s - 2.59/6.86; 255s - * - `modified_beam_search_lm_rescore_LODR` + * - ``modified_beam_search_lm_rescore_LODR`` - 2.9/7.57; 160s - 2.63/7.04; 203s - 2.52/6.73; 263s From 3fb0a431704a18c9d04230b07a1d75b7ea159970 Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Thu, 27 Jul 2023 12:36:05 +0800 Subject: [PATCH 058/100] Fix conflict (#1187) Resolve conflict --- docs/source/decoding-with-langugage-models/rescoring.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/source/decoding-with-langugage-models/rescoring.rst b/docs/source/decoding-with-langugage-models/rescoring.rst index de7e700d0..ee2e2113c 100644 --- a/docs/source/decoding-with-langugage-models/rescoring.rst +++ b/docs/source/decoding-with-langugage-models/rescoring.rst @@ -4,11 +4,7 @@ LM rescoring for Transducer ================================= LM rescoring is a commonly used approach to incorporate external LM information. Unlike shallow-fusion-based -<<<<<<< HEAD methods (see :ref:`shallow_fusion`, :ref:`LODR`), rescoring is usually performed to re-rank the n-best hypotheses after beam search. -======= -methods (see :ref:`shallow-fusion`, :ref:`LODR`), rescoring is usually performed to re-rank the n-best hypotheses after beam search. ->>>>>>> 80d922c1583b9b7fb7e9b47008302cdc74ef58b7 Rescoring is usually more efficient than shallow fusion since less computation is performed on the external LM. In this tutorial, we will show you how to use external LM to rescore the n-best hypotheses decoded from neural transducer models in `icefall `__. From 19b942c958cba13a78757c9f7a287f8c88460bd0 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 27 Jul 2023 13:36:46 +0800 Subject: [PATCH 059/100] Update installation doc. (#1188) --- docs/source/conf.py | 5 + docs/source/installation/index.rst | 687 +++++++++++++++-------------- 2 files changed, 354 insertions(+), 338 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 0ff3f801c..bf231e3c1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -90,4 +90,9 @@ rst_epilog = """ .. _musan: http://www.openslr.org/17/ .. _ONNX: https://github.com/onnx/onnx .. _onnxruntime: https://github.com/microsoft/onnxruntime +.. _torch: https://github.com/pytorch/pytorch +.. _torchaudio: https://github.com/pytorch/audio +.. _k2: https://github.com/k2-fsa/k2 +.. _lhotse: https://github.com/lhotse-speech/lhotse +.. _yesno: https://www.openslr.org/1/ """ diff --git a/docs/source/installation/index.rst b/docs/source/installation/index.rst index 738b24ab2..534b674f9 100644 --- a/docs/source/installation/index.rst +++ b/docs/source/installation/index.rst @@ -3,40 +3,23 @@ Installation ============ +.. hint:: + We have a colab notebook guiding you step by step to setup the environment. -``icefall`` depends on `k2 `_ and -`lhotse `_. + |yesno colab notebook| + + .. |yesno colab notebook| image:: https://colab.research.google.com/assets/colab-badge.svg + :target: https://colab.research.google.com/drive/1tIjjzaJc3IvGyKiMCDWO-TSnBgkcuN3B?usp=sharing + +`icefall`_ depends on `k2`_ and `lhotse`_. We recommend that you use the following steps to install the dependencies. - (0) Install CUDA toolkit and cuDNN -- (1) Install PyTorch and torchaudio -- (2) Install k2 -- (3) Install lhotse - -.. caution:: - - 99% users who have issues about the installation are using conda. - -.. caution:: - - 99% users who have issues about the installation are using conda. - -.. caution:: - - 99% users who have issues about the installation are using conda. - -.. hint:: - - We suggest that you use ``pip install`` to install PyTorch. - - You can use the following command to create a virutal environment in Python: - - .. code-block:: bash - - python3 -m venv ./my_env - source ./my_env/bin/activate +- (1) Install `torch`_ and `torchaudio`_ +- (2) Install `k2`_ +- (3) Install `lhotse`_ .. caution:: @@ -50,27 +33,20 @@ Please refer to to install CUDA and cuDNN. -(1) Install PyTorch and torchaudio ----------------------------------- +(1) Install torch and torchaudio +-------------------------------- -Please refer ``_ to install PyTorch -and torchaudio. - -.. hint:: - - You can also go to ``_ - to download pre-compiled wheels and install them. +Please refer ``_ to install `torch`_ and `torchaudio`_. .. caution:: Please install torch and torchaudio at the same time. - (2) Install k2 -------------- Please refer to ``_ -to install ``k2``. +to install `k2`_. .. caution:: @@ -78,21 +54,18 @@ to install ``k2``. .. note:: - We suggest that you install k2 from source by following - ``_ - or - ``_. + We suggest that you install k2 from pre-compiled wheels by following + ``_ .. hint:: - Please always install the latest version of k2. + Please always install the latest version of `k2`_. (3) Install lhotse ------------------ Please refer to ``_ -to install ``lhotse``. - +to install `lhotse`_. .. hint:: @@ -100,17 +73,16 @@ to install ``lhotse``. pip install git+https://github.com/lhotse-speech/lhotse - to install the latest version of lhotse. + to install the latest version of `lhotse`_. (4) Download icefall -------------------- -``icefall`` is a collection of Python scripts; what you need is to download it +`icefall`_ is a collection of Python scripts; what you need is to download it and set the environment variable ``PYTHONPATH`` to point to it. -Assume you want to place ``icefall`` in the folder ``/tmp``. The -following commands show you how to setup ``icefall``: - +Assume you want to place `icefall`_ in the folder ``/tmp``. The +following commands show you how to setup `icefall`_: .. code-block:: bash @@ -122,285 +94,334 @@ following commands show you how to setup ``icefall``: .. HINT:: - You can put several versions of ``icefall`` in the same virtual environment. - To switch among different versions of ``icefall``, just set ``PYTHONPATH`` + You can put several versions of `icefall`_ in the same virtual environment. + To switch among different versions of `icefall`_, just set ``PYTHONPATH`` to point to the version you want. - Installation example -------------------- The following shows an example about setting up the environment. - (1) Create a virtual environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: bash - $ virtualenv -p python3.8 test-icefall + kuangfangjun:~$ virtualenv -p python3.8 test-icefall + created virtual environment CPython3.8.0.final.0-64 in 9422ms + creator CPython3Posix(dest=/star-fj/fangjun/test-icefall, clear=False, no_vcs_ignore=False, global=False) + seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/star-fj/fangjun/.local/share/virtualenv) + added seed packages: pip==22.3.1, setuptools==65.6.3, wheel==0.38.4 + activators BashActivator,CShellActivator,FishActivator,NushellActivator,PowerShellActivator,PythonActivator - created virtual environment CPython3.8.6.final.0-64 in 1540ms - creator CPython3Posix(dest=/ceph-fj/fangjun/test-icefall, clear=False, no_vcs_ignore=False, global=False) - seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/root/fangjun/.local/share/v - irtualenv) - added seed packages: pip==21.1.3, setuptools==57.4.0, wheel==0.36.2 - activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator,XonshActivator + kuangfangjun:~$ source test-icefall/bin/activate + (test-icefall) kuangfangjun:~$ -(2) Activate your virtual environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +(2) Install CUDA toolkit and cuDNN +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You need to determine the version of CUDA toolkit to install. .. code-block:: bash - $ source test-icefall/bin/activate + (test-icefall) kuangfangjun:~$ nvidia-smi | head -n 4 -(3) Install k2 + Wed Jul 26 21:57:49 2023 + +-----------------------------------------------------------------------------+ + | NVIDIA-SMI 510.47.03 Driver Version: 510.47.03 CUDA Version: 11.6 | + |-------------------------------+----------------------+----------------------+ + +You can choose any CUDA version that is ``not`` greater than the version printed by ``nvidia-smi``. +In our case, we can choose any version ``<= 11.6``. + +We will use ``CUDA 11.6`` in this example. Please follow +``_ +to install CUDA toolkit and cuDNN if you have not done that before. + +After installing CUDA toolkit, you can use the following command to verify it: + +.. code-block:: bash + + (test-icefall) kuangfangjun:~$ nvcc --version + + nvcc: NVIDIA (R) Cuda compiler driver + Copyright (c) 2005-2019 NVIDIA Corporation + Built on Wed_Oct_23_19:24:38_PDT_2019 + Cuda compilation tools, release 10.2, V10.2.89 + +(3) Install torch and torchaudio +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since we have selected CUDA toolkit ``11.6``, we have to install a version of `torch`_ +that is compiled against CUDA ``11.6``. We select ``torch 1.13.0+cu116`` in this +example. + +After selecting the version of `torch`_ to install, we need to also install +a compatible version of `torchaudio`_, which is ``0.13.0+cu116`` in our case. + +Please refer to ``_ +to select an appropriate version of `torchaudio`_ to install if you use a different +version of `torch`_. + +.. code-block:: bash + + (test-icefall) kuangfangjun:~$ pip install torch==1.13.0+cu116 torchaudio==0.13.0+cu116 -f https://download.pytorch.org/whl/torch_stable.html + + Looking in links: https://download.pytorch.org/whl/torch_stable.html + Collecting torch==1.13.0+cu116 + Downloading https://download.pytorch.org/whl/cu116/torch-1.13.0%2Bcu116-cp38-cp38-linux_x86_64.whl (1983.0 MB) + ________________________________________ 2.0/2.0 GB 764.4 kB/s eta 0:00:00 + Collecting torchaudio==0.13.0+cu116 + Downloading https://download.pytorch.org/whl/cu116/torchaudio-0.13.0%2Bcu116-cp38-cp38-linux_x86_64.whl (4.2 MB) + ________________________________________ 4.2/4.2 MB 1.3 MB/s eta 0:00:00 + Requirement already satisfied: typing-extensions in /star-fj/fangjun/test-icefall/lib/python3.8/site-packages (from torch==1.13.0+cu116) (4.7.1) + Installing collected packages: torch, torchaudio + Successfully installed torch-1.13.0+cu116 torchaudio-0.13.0+cu116 + +Verify that `torch`_ and `torchaudio`_ are successfully installed: + +.. code-block:: bash + + (test-icefall) kuangfangjun:~$ python3 -c "import torch; print(torch.__version__)" + + 1.13.0+cu116 + + (test-icefall) kuangfangjun:~$ python3 -c "import torchaudio; print(torchaudio.__version__)" + + 0.13.0+cu116 + +(4) Install k2 ~~~~~~~~~~~~~~ +We will install `k2`_ from pre-compiled wheels by following +``_ + .. code-block:: bash - $ pip install k2==1.4.dev20210822+cpu.torch1.9.0 -f https://k2-fsa.org/nightly/index.html + (test-icefall) kuangfangjun:~$ pip install k2==1.24.3.dev20230725+cuda11.6.torch1.13.0 -f https://k2-fsa.github.io/k2/cuda.html - Looking in links: https://k2-fsa.org/nightly/index.html - Collecting k2==1.4.dev20210822+cpu.torch1.9.0 - Downloading https://k2-fsa.org/nightly/whl/k2-1.4.dev20210822%2Bcpu.torch1.9.0-cp38-cp38-linux_x86_64.whl (1.6 MB) - |________________________________| 1.6 MB 185 kB/s + Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple + Looking in links: https://k2-fsa.github.io/k2/cuda.html + Collecting k2==1.24.3.dev20230725+cuda11.6.torch1.13.0 + Downloading https://huggingface.co/csukuangfj/k2/resolve/main/ubuntu-cuda/k2-1.24.3.dev20230725%2Bcuda11.6.torch1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (104.3 MB) + ________________________________________ 104.3/104.3 MB 5.1 MB/s eta 0:00:00 + Requirement already satisfied: torch==1.13.0 in /star-fj/fangjun/test-icefall/lib/python3.8/site-packages (from k2==1.24.3.dev20230725+cuda11.6.torch1.13.0) (1.13.0+cu116) Collecting graphviz - Downloading graphviz-0.17-py3-none-any.whl (18 kB) - Collecting torch==1.9.0 - Using cached torch-1.9.0-cp38-cp38-manylinux1_x86_64.whl (831.4 MB) - Collecting typing-extensions - Using cached typing_extensions-3.10.0.0-py3-none-any.whl (26 kB) - Installing collected packages: typing-extensions, torch, graphviz, k2 - Successfully installed graphviz-0.17 k2-1.4.dev20210822+cpu.torch1.9.0 torch-1.9.0 typing-extensions-3.10.0.0 + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/de/5e/fcbb22c68208d39edff467809d06c9d81d7d27426460ebc598e55130c1aa/graphviz-0.20.1-py3-none-any.whl (47 kB) + Requirement already satisfied: typing-extensions in /star-fj/fangjun/test-icefall/lib/python3.8/site-packages (from torch==1.13.0->k2==1.24.3.dev20230725+cuda11.6.torch1.13.0) (4.7.1) + Installing collected packages: graphviz, k2 + Successfully installed graphviz-0.20.1 k2-1.24.3.dev20230725+cuda11.6.torch1.13.0 -.. WARNING:: +.. hint:: - We choose to install a CPU version of k2 for testing. You would probably want to install - a CUDA version of k2. + Please refer to ``_ for the available + pre-compiled wheels about `k2`_. +Verify that `k2`_ has been installed successfully: -(4) Install lhotse +.. code-block:: bash + + (test-icefall) kuangfangjun:~$ python3 -m k2.version + + Collecting environment information... + + k2 version: 1.24.3 + Build type: Release + Git SHA1: 4c05309499a08454997adf500b56dcc629e35ae5 + Git date: Tue Jul 25 16:23:36 2023 + Cuda used to build k2: 11.6 + cuDNN used to build k2: 8.3.2 + Python version used to build k2: 3.8 + OS used to build k2: CentOS Linux release 7.9.2009 (Core) + CMake version: 3.27.0 + GCC version: 9.3.1 + CMAKE_CUDA_FLAGS: -Wno-deprecated-gpu-targets -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_35,code=sm_35 -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_50,code=sm_50 -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_60,code=sm_60 -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_61,code=sm_61 -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_70,code=sm_70 -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_75,code=sm_75 -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_80,code=sm_80 -lineinfo --expt-extended-lambda -use_fast_math -Xptxas=-w --expt-extended-lambda -gencode arch=compute_86,code=sm_86 -DONNX_NAMESPACE=onnx_c2 -gencode arch=compute_35,code=sm_35 -gencode arch=compute_50,code=sm_50 -gencode arch=compute_52,code=sm_52 -gencode arch=compute_60,code=sm_60 -gencode arch=compute_61,code=sm_61 -gencode arch=compute_70,code=sm_70 -gencode arch=compute_75,code=sm_75 -gencode arch=compute_80,code=sm_80 -gencode arch=compute_86,code=sm_86 -gencode arch=compute_86,code=compute_86 -Xcudafe --diag_suppress=cc_clobber_ignored,--diag_suppress=integer_sign_change,--diag_suppress=useless_using_declaration,--diag_suppress=set_but_not_used,--diag_suppress=field_without_dll_interface,--diag_suppress=base_class_has_different_dll_interface,--diag_suppress=dll_interface_conflict_none_assumed,--diag_suppress=dll_interface_conflict_dllexport_assumed,--diag_suppress=implicit_return_from_non_void_function,--diag_suppress=unsigned_compare_with_zero,--diag_suppress=declared_but_not_referenced,--diag_suppress=bad_friend_decl --expt-relaxed-constexpr --expt-extended-lambda -D_GLIBCXX_USE_CXX11_ABI=0 --compiler-options -Wall --compiler-options -Wno-strict-overflow --compiler-options -Wno-unknown-pragmas + CMAKE_CXX_FLAGS: -D_GLIBCXX_USE_CXX11_ABI=0 -Wno-unused-variable -Wno-strict-overflow + PyTorch version used to build k2: 1.13.0+cu116 + PyTorch is using Cuda: 11.6 + NVTX enabled: True + With CUDA: True + Disable debug: True + Sync kernels : False + Disable checks: False + Max cpu memory allocate: 214748364800 bytes (or 200.0 GB) + k2 abort: False + __file__: /star-fj/fangjun/test-icefall/lib/python3.8/site-packages/k2/version/version.py + _k2.__file__: /star-fj/fangjun/test-icefall/lib/python3.8/site-packages/_k2.cpython-38-x86_64-linux-gnu.so + +(5) Install lhotse ~~~~~~~~~~~~~~~~~~ -.. code-block:: +.. code-block:: bash - $ pip install git+https://github.com/lhotse-speech/lhotse + (test-icefall) kuangfangjun:~$ pip install git+https://github.com/lhotse-speech/lhotse Collecting git+https://github.com/lhotse-speech/lhotse - Cloning https://github.com/lhotse-speech/lhotse to /tmp/pip-req-build-7b1b76ge - Running command git clone -q https://github.com/lhotse-speech/lhotse /tmp/pip-req-build-7b1b76ge - Collecting audioread>=2.1.9 - Using cached audioread-2.1.9-py3-none-any.whl - Collecting SoundFile>=0.10 - Using cached SoundFile-0.10.3.post1-py2.py3-none-any.whl (21 kB) - Collecting click>=7.1.1 - Using cached click-8.0.1-py3-none-any.whl (97 kB) + Cloning https://github.com/lhotse-speech/lhotse to /tmp/pip-req-build-vq12fd5i + Running command git clone --filter=blob:none --quiet https://github.com/lhotse-speech/lhotse /tmp/pip-req-build-vq12fd5i + Resolved https://github.com/lhotse-speech/lhotse to commit 7640d663469b22cd0b36f3246ee9b849cd25e3b7 + Installing build dependencies ... done + Getting requirements to build wheel ... done + Preparing metadata (pyproject.toml) ... done Collecting cytoolz>=0.10.1 - Using cached cytoolz-0.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.9 MB) - Collecting dataclasses - Using cached dataclasses-0.6-py3-none-any.whl (14 kB) - Collecting h5py>=2.10.0 - Downloading h5py-3.4.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (4.5 MB) - |________________________________| 4.5 MB 684 kB/s - Collecting intervaltree>=3.1.0 - Using cached intervaltree-3.1.0-py2.py3-none-any.whl - Collecting lilcom>=1.1.0 - Using cached lilcom-1.1.1-cp38-cp38-linux_x86_64.whl - Collecting numpy>=1.18.1 - Using cached numpy-1.21.2-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (15.8 MB) - Collecting packaging - Using cached packaging-21.0-py3-none-any.whl (40 kB) + Downloading https://pypi.tuna.tsinghua.edu.cn/packages/1e/3b/a7828d575aa17fb7acaf1ced49a3655aa36dad7e16eb7e6a2e4df0dda76f/cytoolz-0.12.2-cp38-cp38- + manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.0 MB) + ________________________________________ 2.0/2.0 MB 33.2 MB/s eta 0:00:00 Collecting pyyaml>=5.3.1 - Using cached PyYAML-5.4.1-cp38-cp38-manylinux1_x86_64.whl (662 kB) + Downloading https://pypi.tuna.tsinghua.edu.cn/packages/c8/6b/6600ac24725c7388255b2f5add93f91e58a5d7efaf4af244fdbcc11a541b/PyYAML-6.0.1-cp38-cp38-ma + nylinux_2_17_x86_64.manylinux2014_x86_64.whl (736 kB) + ________________________________________ 736.6/736.6 kB 38.6 MB/s eta 0:00:00 + Collecting dataclasses + Downloading https://pypi.tuna.tsinghua.edu.cn/packages/26/2f/1095cdc2868052dd1e64520f7c0d5c8c550ad297e944e641dbf1ffbb9a5d/dataclasses-0.6-py3-none- + any.whl (14 kB) + Requirement already satisfied: torchaudio in ./test-icefall/lib/python3.8/site-packages (from lhotse==1.16.0.dev0+git.7640d66.clean) (0.13.0+cu116) + Collecting lilcom>=1.1.0 + Downloading https://pypi.tuna.tsinghua.edu.cn/packages/a8/65/df0a69c52bd085ca1ad4e5c4c1a5c680e25f9477d8e49316c4ff1e5084a4/lilcom-1.7-cp38-cp38-many + linux_2_17_x86_64.manylinux2014_x86_64.whl (87 kB) + ________________________________________ 87.1/87.1 kB 8.7 MB/s eta 0:00:00 Collecting tqdm - Downloading tqdm-4.62.1-py2.py3-none-any.whl (76 kB) - |________________________________| 76 kB 2.7 MB/s - Collecting torchaudio==0.9.0 - Downloading torchaudio-0.9.0-cp38-cp38-manylinux1_x86_64.whl (1.9 MB) - |________________________________| 1.9 MB 73.1 MB/s - Requirement already satisfied: torch==1.9.0 in ./test-icefall/lib/python3.8/site-packages (from torchaudio==0.9.0->lhotse===0.8.0.dev - -2a1410b-clean) (1.9.0) - Requirement already satisfied: typing-extensions in ./test-icefall/lib/python3.8/site-packages (from torch==1.9.0->torchaudio==0.9.0- - >lhotse===0.8.0.dev-2a1410b-clean) (3.10.0.0) + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/e6/02/a2cff6306177ae6bc73bc0665065de51dfb3b9db7373e122e2735faf0d97/tqdm-4.65.0-py3-none-any + .whl (77 kB) + Requirement already satisfied: numpy>=1.18.1 in ./test-icefall/lib/python3.8/site-packages (from lhotse==1.16.0.dev0+git.7640d66.clean) (1.24.4) + Collecting audioread>=2.1.9 + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/5d/cb/82a002441902dccbe427406785db07af10182245ee639ea9f4d92907c923/audioread-3.0.0.tar.gz ( + 377 kB) + Preparing metadata (setup.py) ... done + Collecting tabulate>=0.8.1 + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none- + any.whl (35 kB) + Collecting click>=7.1.1 + Downloading https://pypi.tuna.tsinghua.edu.cn/packages/1a/70/e63223f8116931d365993d4a6b7ef653a4d920b41d03de7c59499962821f/click-8.1.6-py3-none-any. + whl (97 kB) + ________________________________________ 97.9/97.9 kB 8.4 MB/s eta 0:00:00 + Collecting packaging + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/ab/c3/57f0601a2d4fe15de7a553c00adbc901425661bf048f2a22dfc500caf121/packaging-23.1-py3-none- + any.whl (48 kB) + Collecting intervaltree>=3.1.0 + Downloading https://pypi.tuna.tsinghua.edu.cn/packages/50/fb/396d568039d21344639db96d940d40eb62befe704ef849b27949ded5c3bb/intervaltree-3.1.0.tar.gz + (32 kB) + Preparing metadata (setup.py) ... done + Requirement already satisfied: torch in ./test-icefall/lib/python3.8/site-packages (from lhotse==1.16.0.dev0+git.7640d66.clean) (1.13.0+cu116) + Collecting SoundFile>=0.10 + Downloading https://pypi.tuna.tsinghua.edu.cn/packages/ad/bd/0602167a213d9184fc688b1086dc6d374b7ae8c33eccf169f9b50ce6568c/soundfile-0.12.1-py2.py3- + none-manylinux_2_17_x86_64.whl (1.3 MB) + ________________________________________ 1.3/1.3 MB 46.5 MB/s eta 0:00:00 Collecting toolz>=0.8.0 - Using cached toolz-0.11.1-py3-none-any.whl (55 kB) + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/7f/5c/922a3508f5bda2892be3df86c74f9cf1e01217c2b1f8a0ac4841d903e3e9/toolz-0.12.0-py3-none-any.whl (55 kB) Collecting sortedcontainers<3.0,>=2.0 - Using cached sortedcontainers-2.4.0-py2.py3-none-any.whl (29 kB) + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl (29 kB) Collecting cffi>=1.0 - Using cached cffi-1.14.6-cp38-cp38-manylinux1_x86_64.whl (411 kB) + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/b7/8b/06f30caa03b5b3ac006de4f93478dbd0239e2a16566d81a106c322dc4f79/cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (442 kB) + Requirement already satisfied: typing-extensions in ./test-icefall/lib/python3.8/site-packages (from torch->lhotse==1.16.0.dev0+git.7640d66.clean) (4.7.1) Collecting pycparser - Using cached pycparser-2.20-py2.py3-none-any.whl (112 kB) - Collecting pyparsing>=2.0.2 - Using cached pyparsing-2.4.7-py2.py3-none-any.whl (67 kB) - Building wheels for collected packages: lhotse - Building wheel for lhotse (setup.py) ... done - Created wheel for lhotse: filename=lhotse-0.8.0.dev_2a1410b_clean-py3-none-any.whl size=342242 sha256=f683444afa4dc0881133206b4646a - 9d0f774224cc84000f55d0a67f6e4a37997 - Stored in directory: /tmp/pip-ephem-wheel-cache-ftu0qysz/wheels/7f/7a/8e/a0bf241336e2e3cb573e1e21e5600952d49f5162454f2e612f - WARNING: Built wheel for lhotse is invalid: Metadata 1.2 mandates PEP 440 version, but '0.8.0.dev-2a1410b-clean' is not - Failed to build lhotse - Installing collected packages: pycparser, toolz, sortedcontainers, pyparsing, numpy, cffi, tqdm, torchaudio, SoundFile, pyyaml, packa - ging, lilcom, intervaltree, h5py, dataclasses, cytoolz, click, audioread, lhotse - Running setup.py install for lhotse ... done - DEPRECATION: lhotse was installed using the legacy 'setup.py install' method, because a wheel could not be built for it. A possible - replacement is to fix the wheel build issue reported above. You can find discussion regarding this at https://github.com/pypa/pip/is - sues/8368. - Successfully installed SoundFile-0.10.3.post1 audioread-2.1.9 cffi-1.14.6 click-8.0.1 cytoolz-0.11.0 dataclasses-0.6 h5py-3.4.0 inter - valtree-3.1.0 lhotse-0.8.0.dev-2a1410b-clean lilcom-1.1.1 numpy-1.21.2 packaging-21.0 pycparser-2.20 pyparsing-2.4.7 pyyaml-5.4.1 sor - tedcontainers-2.4.0 toolz-0.11.1 torchaudio-0.9.0 tqdm-4.62.1 + Using cached https://pypi.tuna.tsinghua.edu.cn/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl (118 kB) + Building wheels for collected packages: lhotse, audioread, intervaltree + Building wheel for lhotse (pyproject.toml) ... done + Created wheel for lhotse: filename=lhotse-1.16.0.dev0+git.7640d66.clean-py3-none-any.whl size=687627 sha256=cbf0a4d2d0b639b33b91637a4175bc251d6a021a069644ecb1a9f2b3a83d072a + Stored in directory: /tmp/pip-ephem-wheel-cache-wwtk90_m/wheels/7f/7a/8e/a0bf241336e2e3cb573e1e21e5600952d49f5162454f2e612f + Building wheel for audioread (setup.py) ... done + Created wheel for audioread: filename=audioread-3.0.0-py3-none-any.whl size=23704 sha256=5e2d3537c96ce9cf0f645a654c671163707bf8cb8d9e358d0e2b0939a85ff4c2 + Stored in directory: /star-fj/fangjun/.cache/pip/wheels/e2/c3/9c/f19ae5a03f8862d9f0776b0c0570f1fdd60a119d90954e3f39 + Building wheel for intervaltree (setup.py) ... done + Created wheel for intervaltree: filename=intervaltree-3.1.0-py2.py3-none-any.whl size=26098 sha256=2604170976cfffe0d2f678cb1a6e5b525f561cd50babe53d631a186734fec9f9 + Stored in directory: /star-fj/fangjun/.cache/pip/wheels/f3/ed/2b/c179ebfad4e15452d6baef59737f27beb9bfb442e0620f7271 + Successfully built lhotse audioread intervaltree + Installing collected packages: sortedcontainers, dataclasses, tqdm, toolz, tabulate, pyyaml, pycparser, packaging, lilcom, intervaltree, click, audioread, cytoolz, cffi, SoundFile, lhotse + Successfully installed SoundFile-0.12.1 audioread-3.0.0 cffi-1.15.1 click-8.1.6 cytoolz-0.12.2 dataclasses-0.6 intervaltree-3.1.0 lhotse-1.16.0.dev0+git.7640d66.clean lilcom-1.7 packaging-23.1 pycparser-2.21 pyyaml-6.0.1 sortedcontainers-2.4.0 tabulate-0.9.0 toolz-0.12.0 tqdm-4.65.0 -(5) Download icefall + +Verify that `lhotse`_ has been installed successfully: + +.. code-block:: bash + + (test-icefall) kuangfangjun:~$ python3 -c "import lhotse; print(lhotse.__version__)" + + 1.16.0.dev+git.7640d66.clean + +(6) Download icefall ~~~~~~~~~~~~~~~~~~~~ -.. code-block:: +.. code-block:: bash - $ cd /tmp - $ git clone https://github.com/k2-fsa/icefall + (test-icefall) kuangfangjun:~$ cd /tmp/ + + (test-icefall) kuangfangjun:tmp$ git clone https://github.com/k2-fsa/icefall Cloning into 'icefall'... - remote: Enumerating objects: 500, done. - remote: Counting objects: 100% (500/500), done. - remote: Compressing objects: 100% (308/308), done. - remote: Total 500 (delta 263), reused 307 (delta 102), pack-reused 0 - Receiving objects: 100% (500/500), 172.49 KiB | 385.00 KiB/s, done. - Resolving deltas: 100% (263/263), done. + remote: Enumerating objects: 12942, done. + remote: Counting objects: 100% (67/67), done. + remote: Compressing objects: 100% (56/56), done. + remote: Total 12942 (delta 17), reused 35 (delta 6), pack-reused 12875 + Receiving objects: 100% (12942/12942), 14.77 MiB | 9.29 MiB/s, done. + Resolving deltas: 100% (8835/8835), done. - $ cd icefall - $ pip install -r requirements.txt - - Collecting kaldilm - Downloading kaldilm-1.8.tar.gz (48 kB) - |________________________________| 48 kB 574 kB/s - Collecting kaldialign - Using cached kaldialign-0.2-cp38-cp38-linux_x86_64.whl - Collecting sentencepiece>=0.1.96 - Using cached sentencepiece-0.1.96-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.2 MB) - Collecting tensorboard - Using cached tensorboard-2.6.0-py3-none-any.whl (5.6 MB) - Requirement already satisfied: setuptools>=41.0.0 in /ceph-fj/fangjun/test-icefall/lib/python3.8/site-packages (from tensorboard->-r - requirements.txt (line 4)) (57.4.0) - Collecting absl-py>=0.4 - Using cached absl_py-0.13.0-py3-none-any.whl (132 kB) - Collecting google-auth-oauthlib<0.5,>=0.4.1 - Using cached google_auth_oauthlib-0.4.5-py2.py3-none-any.whl (18 kB) - Collecting grpcio>=1.24.3 - Using cached grpcio-1.39.0-cp38-cp38-manylinux2014_x86_64.whl (4.3 MB) - Requirement already satisfied: wheel>=0.26 in /ceph-fj/fangjun/test-icefall/lib/python3.8/site-packages (from tensorboard->-r require - ments.txt (line 4)) (0.36.2) - Requirement already satisfied: numpy>=1.12.0 in /ceph-fj/fangjun/test-icefall/lib/python3.8/site-packages (from tensorboard->-r requi - rements.txt (line 4)) (1.21.2) - Collecting protobuf>=3.6.0 - Using cached protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl (1.0 MB) - Collecting werkzeug>=0.11.15 - Using cached Werkzeug-2.0.1-py3-none-any.whl (288 kB) - Collecting tensorboard-data-server<0.7.0,>=0.6.0 - Using cached tensorboard_data_server-0.6.1-py3-none-manylinux2010_x86_64.whl (4.9 MB) - Collecting google-auth<2,>=1.6.3 - Downloading google_auth-1.35.0-py2.py3-none-any.whl (152 kB) - |________________________________| 152 kB 1.4 MB/s - Collecting requests<3,>=2.21.0 - Using cached requests-2.26.0-py2.py3-none-any.whl (62 kB) - Collecting tensorboard-plugin-wit>=1.6.0 - Using cached tensorboard_plugin_wit-1.8.0-py3-none-any.whl (781 kB) - Collecting markdown>=2.6.8 - Using cached Markdown-3.3.4-py3-none-any.whl (97 kB) - Collecting six - Using cached six-1.16.0-py2.py3-none-any.whl (11 kB) - Collecting cachetools<5.0,>=2.0.0 - Using cached cachetools-4.2.2-py3-none-any.whl (11 kB) - Collecting rsa<5,>=3.1.4 - Using cached rsa-4.7.2-py3-none-any.whl (34 kB) - Collecting pyasn1-modules>=0.2.1 - Using cached pyasn1_modules-0.2.8-py2.py3-none-any.whl (155 kB) - Collecting requests-oauthlib>=0.7.0 - Using cached requests_oauthlib-1.3.0-py2.py3-none-any.whl (23 kB) - Collecting pyasn1<0.5.0,>=0.4.6 - Using cached pyasn1-0.4.8-py2.py3-none-any.whl (77 kB) - Collecting urllib3<1.27,>=1.21.1 - Using cached urllib3-1.26.6-py2.py3-none-any.whl (138 kB) - Collecting certifi>=2017.4.17 - Using cached certifi-2021.5.30-py2.py3-none-any.whl (145 kB) - Collecting charset-normalizer~=2.0.0 - Using cached charset_normalizer-2.0.4-py3-none-any.whl (36 kB) - Collecting idna<4,>=2.5 - Using cached idna-3.2-py3-none-any.whl (59 kB) - Collecting oauthlib>=3.0.0 - Using cached oauthlib-3.1.1-py2.py3-none-any.whl (146 kB) - Building wheels for collected packages: kaldilm - Building wheel for kaldilm (setup.py) ... done - Created wheel for kaldilm: filename=kaldilm-1.8-cp38-cp38-linux_x86_64.whl size=897233 sha256=eccb906cafcd45bf9a7e1a1718e4534254bfb - f4c0d0cbc66eee6c88d68a63862 - Stored in directory: /root/fangjun/.cache/pip/wheels/85/7d/63/f2dd586369b8797cb36d213bf3a84a789eeb92db93d2e723c9 - Successfully built kaldilm - Installing collected packages: urllib3, pyasn1, idna, charset-normalizer, certifi, six, rsa, requests, pyasn1-modules, oauthlib, cach - etools, requests-oauthlib, google-auth, werkzeug, tensorboard-plugin-wit, tensorboard-data-server, protobuf, markdown, grpcio, google - -auth-oauthlib, absl-py, tensorboard, sentencepiece, kaldilm, kaldialign - Successfully installed absl-py-0.13.0 cachetools-4.2.2 certifi-2021.5.30 charset-normalizer-2.0.4 google-auth-1.35.0 google-auth-oaut - hlib-0.4.5 grpcio-1.39.0 idna-3.2 kaldialign-0.2 kaldilm-1.8 markdown-3.3.4 oauthlib-3.1.1 protobuf-3.17.3 pyasn1-0.4.8 pyasn1-module - s-0.2.8 requests-2.26.0 requests-oauthlib-1.3.0 rsa-4.7.2 sentencepiece-0.1.96 six-1.16.0 tensorboard-2.6.0 tensorboard-data-server-0 - .6.1 tensorboard-plugin-wit-1.8.0 urllib3-1.26.6 werkzeug-2.0.1 + (test-icefall) kuangfangjun:tmp$ cd icefall/ + (test-icefall) kuangfangjun:icefall$ pip install -r ./requirements.txt Test Your Installation ---------------------- To test that your installation is successful, let us run the `yesno recipe `_ -on CPU. +on ``CPU``. Data preparation ~~~~~~~~~~~~~~~~ .. code-block:: bash - $ export PYTHONPATH=/tmp/icefall:$PYTHONPATH - $ cd /tmp/icefall - $ cd egs/yesno/ASR - $ ./prepare.sh + (test-icefall) kuangfangjun:icefall$ export PYTHONPATH=/tmp/icefall:$PYTHONPATH + + (test-icefall) kuangfangjun:icefall$ cd /tmp/icefall + + (test-icefall) kuangfangjun:icefall$ cd egs/yesno/ASR + + (test-icefall) kuangfangjun:ASR$ ./prepare.sh + The log of running ``./prepare.sh`` is: .. code-block:: - 2023-05-12 17:55:21 (prepare.sh:27:main) dl_dir: /tmp/icefall/egs/yesno/ASR/download - 2023-05-12 17:55:21 (prepare.sh:30:main) Stage 0: Download data - /tmp/icefall/egs/yesno/ASR/download/waves_yesno.tar.gz: 100%|_______________________________________________________________| 4.70M/4.70M [06:54<00:00, 11.4kB/s] - 2023-05-12 18:02:19 (prepare.sh:39:main) Stage 1: Prepare yesno manifest - 2023-05-12 18:02:21 (prepare.sh:45:main) Stage 2: Compute fbank for yesno - 2023-05-12 18:02:23,199 INFO [compute_fbank_yesno.py:65] Processing train - Extracting and storing features: 100%|_______________________________________________________________| 90/90 [00:00<00:00, 212.60it/s] - 2023-05-12 18:02:23,640 INFO [compute_fbank_yesno.py:65] Processing test - Extracting and storing features: 100%|_______________________________________________________________| 30/30 [00:00<00:00, 304.53it/s] - 2023-05-12 18:02:24 (prepare.sh:51:main) Stage 3: Prepare lang - 2023-05-12 18:02:26 (prepare.sh:66:main) Stage 4: Prepare G - /project/kaldilm/csrc/arpa_file_parser.cc:void kaldilm::ArpaFileParser::Read(std::istream&):79 - [I] Reading \data\ section. - /project/kaldilm/csrc/arpa_file_parser.cc:void kaldilm::ArpaFileParser::Read(std::istream&):140 - [I] Reading \1-grams: section. - 2023-05-12 18:02:26 (prepare.sh:92:main) Stage 5: Compile HLG - 2023-05-12 18:02:28,581 INFO [compile_hlg.py:124] Processing data/lang_phone - 2023-05-12 18:02:28,582 INFO [lexicon.py:171] Converting L.pt to Linv.pt - 2023-05-12 18:02:28,609 INFO [compile_hlg.py:48] Building ctc_topo. max_token_id: 3 - 2023-05-12 18:02:28,610 INFO [compile_hlg.py:52] Loading G.fst.txt - 2023-05-12 18:02:28,611 INFO [compile_hlg.py:62] Intersecting L and G - 2023-05-12 18:02:28,613 INFO [compile_hlg.py:64] LG shape: (4, None) - 2023-05-12 18:02:28,613 INFO [compile_hlg.py:66] Connecting LG - 2023-05-12 18:02:28,614 INFO [compile_hlg.py:68] LG shape after k2.connect: (4, None) - 2023-05-12 18:02:28,614 INFO [compile_hlg.py:70] - 2023-05-12 18:02:28,614 INFO [compile_hlg.py:71] Determinizing LG - 2023-05-12 18:02:28,615 INFO [compile_hlg.py:74] - 2023-05-12 18:02:28,615 INFO [compile_hlg.py:76] Connecting LG after k2.determinize - 2023-05-12 18:02:28,615 INFO [compile_hlg.py:79] Removing disambiguation symbols on LG - 2023-05-12 18:02:28,616 INFO [compile_hlg.py:91] LG shape after k2.remove_epsilon: (6, None) - 2023-05-12 18:02:28,617 INFO [compile_hlg.py:96] Arc sorting LG - 2023-05-12 18:02:28,617 INFO [compile_hlg.py:99] Composing H and LG - 2023-05-12 18:02:28,619 INFO [compile_hlg.py:106] Connecting LG - 2023-05-12 18:02:28,619 INFO [compile_hlg.py:109] Arc sorting LG - 2023-05-12 18:02:28,619 INFO [compile_hlg.py:111] HLG.shape: (8, None) - 2023-05-12 18:02:28,619 INFO [compile_hlg.py:127] Saving HLG.pt to data/lang_phone - + 2023-07-27 12:41:39 (prepare.sh:27:main) dl_dir: /tmp/icefall/egs/yesno/ASR/download + 2023-07-27 12:41:39 (prepare.sh:30:main) Stage 0: Download data + /tmp/icefall/egs/yesno/ASR/download/waves_yesno.tar.gz: 100%|___________________________________________________| 4.70M/4.70M [00:00<00:00, 11.1MB/s] + 2023-07-27 12:41:46 (prepare.sh:39:main) Stage 1: Prepare yesno manifest + 2023-07-27 12:41:50 (prepare.sh:45:main) Stage 2: Compute fbank for yesno + 2023-07-27 12:41:55,718 INFO [compute_fbank_yesno.py:65] Processing train + Extracting and storing features: 100%|_______________________________________________________________________________| 90/90 [00:01<00:00, 87.82it/s] + 2023-07-27 12:41:56,778 INFO [compute_fbank_yesno.py:65] Processing test + Extracting and storing features: 100%|______________________________________________________________________________| 30/30 [00:00<00:00, 256.92it/s] + 2023-07-27 12:41:57 (prepare.sh:51:main) Stage 3: Prepare lang + 2023-07-27 12:42:02 (prepare.sh:66:main) Stage 4: Prepare G + /project/kaldilm/csrc/arpa_file_parser.cc:void kaldilm::ArpaFileParser::Read(std::istream&):79 + [I] Reading \data\ section. + /project/kaldilm/csrc/arpa_file_parser.cc:void kaldilm::ArpaFileParser::Read(std::istream&):140 + [I] Reading \1-grams: section. + 2023-07-27 12:42:02 (prepare.sh:92:main) Stage 5: Compile HLG + 2023-07-27 12:42:07,275 INFO [compile_hlg.py:124] Processing data/lang_phone + 2023-07-27 12:42:07,276 INFO [lexicon.py:171] Converting L.pt to Linv.pt + 2023-07-27 12:42:07,309 INFO [compile_hlg.py:48] Building ctc_topo. max_token_id: 3 + 2023-07-27 12:42:07,310 INFO [compile_hlg.py:52] Loading G.fst.txt + 2023-07-27 12:42:07,314 INFO [compile_hlg.py:62] Intersecting L and G + 2023-07-27 12:42:07,323 INFO [compile_hlg.py:64] LG shape: (4, None) + 2023-07-27 12:42:07,323 INFO [compile_hlg.py:66] Connecting LG + 2023-07-27 12:42:07,323 INFO [compile_hlg.py:68] LG shape after k2.connect: (4, None) + 2023-07-27 12:42:07,323 INFO [compile_hlg.py:70] + 2023-07-27 12:42:07,323 INFO [compile_hlg.py:71] Determinizing LG + 2023-07-27 12:42:07,341 INFO [compile_hlg.py:74] + 2023-07-27 12:42:07,341 INFO [compile_hlg.py:76] Connecting LG after k2.determinize + 2023-07-27 12:42:07,341 INFO [compile_hlg.py:79] Removing disambiguation symbols on LG + 2023-07-27 12:42:07,354 INFO [compile_hlg.py:91] LG shape after k2.remove_epsilon: (6, None) + 2023-07-27 12:42:07,445 INFO [compile_hlg.py:96] Arc sorting LG + 2023-07-27 12:42:07,445 INFO [compile_hlg.py:99] Composing H and LG + 2023-07-27 12:42:07,446 INFO [compile_hlg.py:106] Connecting LG + 2023-07-27 12:42:07,446 INFO [compile_hlg.py:109] Arc sorting LG + 2023-07-27 12:42:07,447 INFO [compile_hlg.py:111] HLG.shape: (8, None) + 2023-07-27 12:42:07,447 INFO [compile_hlg.py:127] Saving HLG.pt to data/lang_phone Training ~~~~~~~~ @@ -409,12 +430,13 @@ Now let us run the training part: .. code-block:: - $ export CUDA_VISIBLE_DEVICES="" - $ ./tdnn/train.py + (test-icefall) kuangfangjun:ASR$ export CUDA_VISIBLE_DEVICES="" + + (test-icefall) kuangfangjun:ASR$ ./tdnn/train.py .. CAUTION:: - We use ``export CUDA_VISIBLE_DEVICES=""`` so that ``icefall`` uses CPU + We use ``export CUDA_VISIBLE_DEVICES=""`` so that `icefall`_ uses CPU even if there are GPUs available. .. hint:: @@ -432,53 +454,52 @@ The training log is given below: .. code-block:: - 2023-05-12 18:04:59,759 INFO [train.py:481] Training started - 2023-05-12 18:04:59,759 INFO [train.py:482] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lr': 0.01, 'feature_dim': 23, 'weight_decay': 1e-06, 'start_epoch': 0, - 'best_train_loss': inf, 'best_valid_loss': inf, 'best_train_epoch': -1, 'best_valid_epoch': -1, 'batch_idx_train': 0, 'log_interval': 10, 'reset_interval': 20, 'valid_interval': 10, 'beam_size': 10, - 'reduction': 'sum', 'use_double_scores': True, 'world_size': 1, 'master_port': 12354, 'tensorboard': True, 'num_epochs': 15, 'seed': 42, 'feature_dir': PosixPath('data/fbank'), 'max_duration': 30.0, - 'bucketing_sampler': False, 'num_buckets': 10, 'concatenate_cuts': False, 'duration_factor': 1.0, 'gap': 1.0, 'on_the_fly_feats': False, 'shuffle': False, 'return_cuts': True, 'num_workers': 2, - 'env_info': {'k2-version': '1.24.3', 'k2-build-type': 'Release', 'k2-with-cuda': True, 'k2-git-sha1': '3b7f09fa35e72589914f67089c0da9f196a92ca4', 'k2-git-date': 'Mon May 8 22:58:45 2023', - 'lhotse-version': '1.15.0.dev+git.6fcfced.clean', 'torch-version': '2.0.0+cu118', 'torch-cuda-available': False, 'torch-cuda-version': '11.8', 'python-version': '3.1', 'icefall-git-branch': 'master', - 'icefall-git-sha1': '30bde4b-clean', 'icefall-git-date': 'Thu May 11 17:37:47 2023', 'icefall-path': '/tmp/icefall', - 'k2-path': 'tmp/lib/python3.10/site-packages/k2-1.24.3.dev20230512+cuda11.8.torch2.0.0-py3.10-linux-x86_64.egg/k2/__init__.py', - 'lhotse-path': 'tmp/lib/python3.10/site-packages/lhotse/__init__.py', 'hostname': 'host', 'IP address': '0.0.0.0'}} - 2023-05-12 18:04:59,761 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt - 2023-05-12 18:04:59,764 INFO [train.py:495] device: cpu - 2023-05-12 18:04:59,791 INFO [asr_datamodule.py:146] About to get train cuts - 2023-05-12 18:04:59,791 INFO [asr_datamodule.py:244] About to get train cuts - 2023-05-12 18:04:59,852 INFO [asr_datamodule.py:149] About to create train dataset - 2023-05-12 18:04:59,852 INFO [asr_datamodule.py:199] Using SingleCutSampler. - 2023-05-12 18:04:59,852 INFO [asr_datamodule.py:205] About to create train dataloader - 2023-05-12 18:04:59,853 INFO [asr_datamodule.py:218] About to get test cuts - 2023-05-12 18:04:59,853 INFO [asr_datamodule.py:252] About to get test cuts - 2023-05-12 18:04:59,986 INFO [train.py:422] Epoch 0, batch 0, loss[loss=1.065, over 2436.00 frames. ], tot_loss[loss=1.065, over 2436.00 frames. ], batch size: 4 - 2023-05-12 18:05:00,352 INFO [train.py:422] Epoch 0, batch 10, loss[loss=0.4561, over 2828.00 frames. ], tot_loss[loss=0.7076, over 22192.90 frames. ], batch size: 4 - 2023-05-12 18:05:00,691 INFO [train.py:444] Epoch 0, validation loss=0.9002, over 18067.00 frames. - 2023-05-12 18:05:00,996 INFO [train.py:422] Epoch 0, batch 20, loss[loss=0.2555, over 2695.00 frames. ], tot_loss[loss=0.484, over 34971.47 frames. ], batch size: 5 - 2023-05-12 18:05:01,217 INFO [train.py:444] Epoch 0, validation loss=0.4688, over 18067.00 frames. - 2023-05-12 18:05:01,251 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-0.pt - 2023-05-12 18:05:01,389 INFO [train.py:422] Epoch 1, batch 0, loss[loss=0.2532, over 2436.00 frames. ], tot_loss[loss=0.2532, over 2436.00 frames. ], batch size: 4 - 2023-05-12 18:05:01,637 INFO [train.py:422] Epoch 1, batch 10, loss[loss=0.1139, over 2828.00 frames. ], tot_loss[loss=0.1592, over 22192.90 frames. ], batch size: 4 - 2023-05-12 18:05:01,859 INFO [train.py:444] Epoch 1, validation loss=0.1629, over 18067.00 frames. - 2023-05-12 18:05:02,094 INFO [train.py:422] Epoch 1, batch 20, loss[loss=0.0767, over 2695.00 frames. ], tot_loss[loss=0.118, over 34971.47 frames. ], batch size: 5 - 2023-05-12 18:05:02,350 INFO [train.py:444] Epoch 1, validation loss=0.06778, over 18067.00 frames. - 2023-05-12 18:05:02,395 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-1.pt + 2023-07-27 12:50:51,936 INFO [train.py:481] Training started + 2023-07-27 12:50:51,936 INFO [train.py:482] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lr': 0.01, 'feature_dim': 23, 'weight_decay': 1e-06, 'start_epoch': 0, 'best_train_loss': inf, 'best_valid_loss': inf, 'best_train_epoch': -1, 'best_valid_epoch': -1, 'batch_idx_train': 0, 'log_interval': 10, 'reset_interval': 20, 'valid_interval': 10, 'beam_size': 10, 'reduction': 'sum', 'use_double_scores': True, 'world_size': 1, 'master_port': 12354, 'tensorboard': True, 'num_epochs': 15, 'seed': 42, 'feature_dir': PosixPath('data/fbank'), 'max_duration': 30.0, 'bucketing_sampler': False, 'num_buckets': 10, 'concatenate_cuts': False, 'duration_factor': 1.0, 'gap': 1.0, 'on_the_fly_feats': False, 'shuffle': False, 'return_cuts': True, 'num_workers': 2, 'env_info': {'k2-version': '1.24.3', 'k2-build-type': 'Release', 'k2-with-cuda': True, 'k2-git-sha1': '4c05309499a08454997adf500b56dcc629e35ae5', 'k2-git-date': 'Tue Jul 25 16:23:36 2023', 'lhotse-version': '1.16.0.dev+git.7640d66.clean', 'torch-version': '1.13.0+cu116', 'torch-cuda-available': False, 'torch-cuda-version': '11.6', 'python-version': '3.8', 'icefall-git-branch': 'master', 'icefall-git-sha1': '3fb0a43-clean', 'icefall-git-date': 'Thu Jul 27 12:36:05 2023', 'icefall-path': '/tmp/icefall', 'k2-path': '/star-fj/fangjun/test-icefall/lib/python3.8/site-packages/k2/__init__.py', 'lhotse-path': '/star-fj/fangjun/test-icefall/lib/python3.8/site-packages/lhotse/__init__.py', 'hostname': 'de-74279-k2-train-1-1220091118-57c4d55446-sph26', 'IP address': '10.177.77.20'}} + 2023-07-27 12:50:51,941 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt + 2023-07-27 12:50:51,949 INFO [train.py:495] device: cpu + 2023-07-27 12:50:51,965 INFO [asr_datamodule.py:146] About to get train cuts + 2023-07-27 12:50:51,965 INFO [asr_datamodule.py:244] About to get train cuts + 2023-07-27 12:50:51,967 INFO [asr_datamodule.py:149] About to create train dataset + 2023-07-27 12:50:51,967 INFO [asr_datamodule.py:199] Using SingleCutSampler. + 2023-07-27 12:50:51,967 INFO [asr_datamodule.py:205] About to create train dataloader + 2023-07-27 12:50:51,968 INFO [asr_datamodule.py:218] About to get test cuts + 2023-07-27 12:50:51,968 INFO [asr_datamodule.py:252] About to get test cuts + 2023-07-27 12:50:52,565 INFO [train.py:422] Epoch 0, batch 0, loss[loss=1.065, over 2436.00 frames. ], tot_loss[loss=1.065, over 2436.00 frames. ], batch size: 4 + 2023-07-27 12:50:53,681 INFO [train.py:422] Epoch 0, batch 10, loss[loss=0.4561, over 2828.00 frames. ], tot_loss[loss=0.7076, over 22192.90 frames.], batch size: 4 + 2023-07-27 12:50:54,167 INFO [train.py:444] Epoch 0, validation loss=0.9002, over 18067.00 frames. + 2023-07-27 12:50:55,011 INFO [train.py:422] Epoch 0, batch 20, loss[loss=0.2555, over 2695.00 frames. ], tot_loss[loss=0.484, over 34971.47 frames. ], batch size: 5 + 2023-07-27 12:50:55,331 INFO [train.py:444] Epoch 0, validation loss=0.4688, over 18067.00 frames. + 2023-07-27 12:50:55,368 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-0.pt + 2023-07-27 12:50:55,633 INFO [train.py:422] Epoch 1, batch 0, loss[loss=0.2532, over 2436.00 frames. ], tot_loss[loss=0.2532, over 2436.00 frames. ], + batch size: 4 + 2023-07-27 12:50:56,242 INFO [train.py:422] Epoch 1, batch 10, loss[loss=0.1139, over 2828.00 frames. ], tot_loss[loss=0.1592, over 22192.90 frames.], batch size: 4 + 2023-07-27 12:50:56,522 INFO [train.py:444] Epoch 1, validation loss=0.1627, over 18067.00 frames. + 2023-07-27 12:50:57,209 INFO [train.py:422] Epoch 1, batch 20, loss[loss=0.07055, over 2695.00 frames. ], tot_loss[loss=0.1175, over 34971.47 frames.], batch size: 5 + 2023-07-27 12:50:57,600 INFO [train.py:444] Epoch 1, validation loss=0.07091, over 18067.00 frames. + 2023-07-27 12:50:57,640 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-1.pt + 2023-07-27 12:50:57,847 INFO [train.py:422] Epoch 2, batch 0, loss[loss=0.07731, over 2436.00 frames. ], tot_loss[loss=0.07731, over 2436.00 frames.], batch size: 4 + 2023-07-27 12:50:58,427 INFO [train.py:422] Epoch 2, batch 10, loss[loss=0.04391, over 2828.00 frames. ], tot_loss[loss=0.05341, over 22192.90 frames. ], batch size: 4 + 2023-07-27 12:50:58,884 INFO [train.py:444] Epoch 2, validation loss=0.04384, over 18067.00 frames. + 2023-07-27 12:50:59,387 INFO [train.py:422] Epoch 2, batch 20, loss[loss=0.03458, over 2695.00 frames. ], tot_loss[loss=0.04616, over 34971.47 frames. ], batch size: 5 + 2023-07-27 12:50:59,707 INFO [train.py:444] Epoch 2, validation loss=0.03379, over 18067.00 frames. + 2023-07-27 12:50:59,758 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-2.pt - ... ... + ... ... - 2023-05-12 18:05:14,789 INFO [train.py:422] Epoch 13, batch 0, loss[loss=0.01056, over 2436.00 frames. ], tot_loss[loss=0.01056, over 2436.00 frames. ], batch size: 4 - 2023-05-12 18:05:15,016 INFO [train.py:422] Epoch 13, batch 10, loss[loss=0.009022, over 2828.00 frames. ], tot_loss[loss=0.009985, over 22192.90 frames. ], batch size: 4 - 2023-05-12 18:05:15,271 INFO [train.py:444] Epoch 13, validation loss=0.01088, over 18067.00 frames. - 2023-05-12 18:05:15,497 INFO [train.py:422] Epoch 13, batch 20, loss[loss=0.01174, over 2695.00 frames. ], tot_loss[loss=0.01077, over 34971.47 frames. ], batch size: 5 - 2023-05-12 18:05:15,747 INFO [train.py:444] Epoch 13, validation loss=0.01087, over 18067.00 frames. - 2023-05-12 18:05:15,783 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-13.pt - 2023-05-12 18:05:15,921 INFO [train.py:422] Epoch 14, batch 0, loss[loss=0.01045, over 2436.00 frames. ], tot_loss[loss=0.01045, over 2436.00 frames. ], batch size: 4 - 2023-05-12 18:05:16,146 INFO [train.py:422] Epoch 14, batch 10, loss[loss=0.008957, over 2828.00 frames. ], tot_loss[loss=0.009903, over 22192.90 frames. ], batch size: 4 - 2023-05-12 18:05:16,374 INFO [train.py:444] Epoch 14, validation loss=0.01092, over 18067.00 frames. - 2023-05-12 18:05:16,598 INFO [train.py:422] Epoch 14, batch 20, loss[loss=0.01169, over 2695.00 frames. ], tot_loss[loss=0.01065, over 34971.47 frames. ], batch size: 5 - 2023-05-12 18:05:16,824 INFO [train.py:444] Epoch 14, validation loss=0.01077, over 18067.00 frames. - 2023-05-12 18:05:16,862 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-14.pt - 2023-05-12 18:05:16,865 INFO [train.py:555] Done! + 2023-07-27 12:51:23,433 INFO [train.py:422] Epoch 13, batch 0, loss[loss=0.01054, over 2436.00 frames. ], tot_loss[loss=0.01054, over 2436.00 frames. ], batch size: 4 + 2023-07-27 12:51:23,980 INFO [train.py:422] Epoch 13, batch 10, loss[loss=0.009014, over 2828.00 frames. ], tot_loss[loss=0.009974, over 22192.90 frames. ], batch size: 4 + 2023-07-27 12:51:24,489 INFO [train.py:444] Epoch 13, validation loss=0.01085, over 18067.00 frames. + 2023-07-27 12:51:25,258 INFO [train.py:422] Epoch 13, batch 20, loss[loss=0.01172, over 2695.00 frames. ], tot_loss[loss=0.01055, over 34971.47 frames. ], batch size: 5 + 2023-07-27 12:51:25,621 INFO [train.py:444] Epoch 13, validation loss=0.01074, over 18067.00 frames. + 2023-07-27 12:51:25,699 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-13.pt + 2023-07-27 12:51:25,866 INFO [train.py:422] Epoch 14, batch 0, loss[loss=0.01044, over 2436.00 frames. ], tot_loss[loss=0.01044, over 2436.00 frames. ], batch size: 4 + 2023-07-27 12:51:26,844 INFO [train.py:422] Epoch 14, batch 10, loss[loss=0.008942, over 2828.00 frames. ], tot_loss[loss=0.01, over 22192.90 frames. ], batch size: 4 + 2023-07-27 12:51:27,221 INFO [train.py:444] Epoch 14, validation loss=0.01082, over 18067.00 frames. + 2023-07-27 12:51:27,970 INFO [train.py:422] Epoch 14, batch 20, loss[loss=0.01169, over 2695.00 frames. ], tot_loss[loss=0.01054, over 34971.47 frames. ], batch size: 5 + 2023-07-27 12:51:28,247 INFO [train.py:444] Epoch 14, validation loss=0.01073, over 18067.00 frames. + 2023-07-27 12:51:28,323 INFO [checkpoint.py:75] Saving checkpoint to tdnn/exp/epoch-14.pt + 2023-07-27 12:51:28,326 INFO [train.py:555] Done! Decoding ~~~~~~~~ @@ -487,42 +508,32 @@ Let us use the trained model to decode the test set: .. code-block:: - $ ./tdnn/decode.py + (test-icefall) kuangfangjun:ASR$ ./tdnn/decode.py -The decoding log is: + 2023-07-27 12:55:12,840 INFO [decode.py:263] Decoding started + 2023-07-27 12:55:12,840 INFO [decode.py:264] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lm_dir': PosixPath('data/lm'), 'feature_dim': 23, 'search_beam': 20, 'output_beam': 8, 'min_active_states': 30, 'max_active_states': 10000, 'use_double_scores': True, 'epoch': 14, 'avg': 2, 'export': False, 'feature_dir': PosixPath('data/fbank'), 'max_duration': 30.0, 'bucketing_sampler': False, 'num_buckets': 10, 'concatenate_cuts': False, 'duration_factor': 1.0, 'gap': 1.0, 'on_the_fly_feats': False, 'shuffle': False, 'return_cuts': True, 'num_workers': 2, 'env_info': {'k2-version': '1.24.3', 'k2-build-type': 'Release', 'k2-with-cuda': True, 'k2-git-sha1': '4c05309499a08454997adf500b56dcc629e35ae5', 'k2-git-date': 'Tue Jul 25 16:23:36 2023', 'lhotse-version': '1.16.0.dev+git.7640d66.clean', 'torch-version': '1.13.0+cu116', 'torch-cuda-available': False, 'torch-cuda-version': '11.6', 'python-version': '3.8', 'icefall-git-branch': 'master', 'icefall-git-sha1': '3fb0a43-clean', 'icefall-git-date': 'Thu Jul 27 12:36:05 2023', 'icefall-path': '/tmp/icefall', 'k2-path': '/star-fj/fangjun/test-icefall/lib/python3.8/site-packages/k2/__init__.py', 'lhotse-path': '/star-fj/fangjun/test-icefall/lib/python3.8/site-packages/lhotse/__init__.py', 'hostname': 'de-74279-k2-train-1-1220091118-57c4d55446-sph26', 'IP address': '10.177.77.20'}} + 2023-07-27 12:55:12,841 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt + 2023-07-27 12:55:12,855 INFO [decode.py:273] device: cpu + 2023-07-27 12:55:12,868 INFO [decode.py:291] averaging ['tdnn/exp/epoch-13.pt', 'tdnn/exp/epoch-14.pt'] + 2023-07-27 12:55:12,882 INFO [asr_datamodule.py:218] About to get test cuts + 2023-07-27 12:55:12,883 INFO [asr_datamodule.py:252] About to get test cuts + 2023-07-27 12:55:13,157 INFO [decode.py:204] batch 0/?, cuts processed until now is 4 + 2023-07-27 12:55:13,701 INFO [decode.py:241] The transcripts are stored in tdnn/exp/recogs-test_set.txt + 2023-07-27 12:55:13,702 INFO [utils.py:564] [test_set] %WER 0.42% [1 / 240, 0 ins, 1 del, 0 sub ] + 2023-07-27 12:55:13,704 INFO [decode.py:249] Wrote detailed error stats to tdnn/exp/errs-test_set.txt + 2023-07-27 12:55:13,704 INFO [decode.py:316] Done! -.. code-block:: - 2023-05-12 18:08:30,482 INFO [decode.py:263] Decoding started - 2023-05-12 18:08:30,483 INFO [decode.py:264] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lm_dir': PosixPath('data/lm'), 'feature_dim': 23, - 'search_beam': 20, 'output_beam': 8, 'min_active_states': 30, 'max_active_states': 10000, 'use_double_scores': True, 'epoch': 14, 'avg': 2, 'export': False, 'feature_dir': PosixPath('data/fbank'), - 'max_duration': 30.0, 'bucketing_sampler': False, 'num_buckets': 10, 'concatenate_cuts': False, 'duration_factor': 1.0, 'gap': 1.0, 'on_the_fly_feats': False, 'shuffle': False, 'return_cuts': True, - 'num_workers': 2, 'env_info': {'k2-version': '1.24.3', 'k2-build-type': 'Release', 'k2-with-cuda': True, 'k2-git-sha1': '3b7f09fa35e72589914f67089c0da9f196a92ca4', 'k2-git-date': 'Mon May 8 22:58:45 2023', - 'lhotse-version': '1.15.0.dev+git.6fcfced.clean', 'torch-version': '2.0.0+cu118', 'torch-cuda-available': False, 'torch-cuda-version': '11.8', 'python-version': '3.1', 'icefall-git-branch': 'master', - 'icefall-git-sha1': '30bde4b-clean', 'icefall-git-date': 'Thu May 11 17:37:47 2023', 'icefall-path': '/tmp/icefall', - 'k2-path': '/tmp/lib/python3.10/site-packages/k2-1.24.3.dev20230512+cuda11.8.torch2.0.0-py3.10-linux-x86_64.egg/k2/__init__.py', - 'lhotse-path': '/tmp/lib/python3.10/site-packages/lhotse/__init__.py', 'hostname': 'host', 'IP address': '0.0.0.0'}} - 2023-05-12 18:08:30,483 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt - 2023-05-12 18:08:30,487 INFO [decode.py:273] device: cpu - 2023-05-12 18:08:30,513 INFO [decode.py:291] averaging ['tdnn/exp/epoch-13.pt', 'tdnn/exp/epoch-14.pt'] - 2023-05-12 18:08:30,521 INFO [asr_datamodule.py:218] About to get test cuts - 2023-05-12 18:08:30,521 INFO [asr_datamodule.py:252] About to get test cuts - 2023-05-12 18:08:30,675 INFO [decode.py:204] batch 0/?, cuts processed until now is 4 - 2023-05-12 18:08:30,923 INFO [decode.py:241] The transcripts are stored in tdnn/exp/recogs-test_set.txt - 2023-05-12 18:08:30,924 INFO [utils.py:558] [test_set] %WER 0.42% [1 / 240, 0 ins, 1 del, 0 sub ] - 2023-05-12 18:08:30,925 INFO [decode.py:249] Wrote detailed error stats to tdnn/exp/errs-test_set.txt - 2023-05-12 18:08:30,925 INFO [decode.py:316] Done! - -**Congratulations!** You have successfully setup the environment and have run the first recipe in ``icefall``. +**Congratulations!** You have successfully setup the environment and have run the first recipe in `icefall`_. Have fun with ``icefall``! YouTube Video ------------- -We provide the following YouTube video showing how to install ``icefall``. +We provide the following YouTube video showing how to install `icefall`_. It also shows how to debug various problems that you may encounter while -using ``icefall``. +using `icefall`_. .. note:: From 751bb6ff1a933c69a5ad4aebe8e24972f14dd691 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 28 Jul 2023 10:34:40 +0800 Subject: [PATCH 060/100] Add docker image for icefall (#1189) --- .github/workflows/build-docker-image.yml | 45 ++++++++++++++++ .github/workflows/run-docker-image.yml | 66 ++++++++++++++++++++++++ docker/README.md | 15 ++++++ docker/torch1.12.1-cuda11.3.dockerfile | 62 ++++++++++++++++++++++ docker/torch1.13.0-cuda11.6.dockerfile | 64 +++++++++++++++++++++++ docker/torch1.9.0-cuda10.2.dockerfile | 62 ++++++++++++++++++++++ docker/torch2.0.0-cuda11.7.dockerfile | 62 ++++++++++++++++++++++ 7 files changed, 376 insertions(+) create mode 100644 .github/workflows/build-docker-image.yml create mode 100644 .github/workflows/run-docker-image.yml create mode 100644 docker/torch1.12.1-cuda11.3.dockerfile create mode 100644 docker/torch1.13.0-cuda11.6.dockerfile create mode 100644 docker/torch1.9.0-cuda10.2.dockerfile create mode 100644 docker/torch2.0.0-cuda11.7.dockerfile diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml new file mode 100644 index 000000000..327f0ee45 --- /dev/null +++ b/.github/workflows/build-docker-image.yml @@ -0,0 +1,45 @@ +# see also +# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages +name: Build docker image +on: + workflow_dispatch: + +concurrency: + group: build_docker-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-docker-image: + name: ${{ matrix.image }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + image: ["torch2.0.0-cuda11.7", "torch1.13.0-cuda11.6", "torch1.12.1-cuda11.3", "torch1.9.0-cuda10.2"] + + steps: + # refer to https://github.com/actions/checkout + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Rename + shell: bash + run: | + image=${{ matrix.image }} + mv -v ./docker/$image.dockerfile ./Dockerfile + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: ./Dockerfile + push: true + tags: k2fsa/icefall:${{ matrix.image }} diff --git a/.github/workflows/run-docker-image.yml b/.github/workflows/run-docker-image.yml new file mode 100644 index 000000000..d0ac11071 --- /dev/null +++ b/.github/workflows/run-docker-image.yml @@ -0,0 +1,66 @@ +name: Run docker image +on: + workflow_dispatch: + +concurrency: + group: run_docker_image-${{ github.ref }} + cancel-in-progress: true + +jobs: + run-docker-image: + name: ${{ matrix.image }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + image: ["torch2.0.0-cuda11.7", "torch1.13.0-cuda11.6", "torch1.12.1-cuda11.3", "torch1.9.0-cuda10.2"] + steps: + # refer to https://github.com/actions/checkout + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Run the build process with Docker + uses: addnab/docker-run-action@v3 + with: + image: k2fsa/icefall:${{ matrix.image }} + run: | + uname -a + cat /etc/*release + + nvcc --version + + which nvcc + cuda_dir=$(dirname $(which nvcc)) + echo "cuda_dir: $cuda_dir" + + find $cuda_dir -name libcuda.so* + echo "--------------------" + + find / -name libcuda.so* 2>/dev/null + + pushd /opt/conda/lib/stubs && ln -s libcuda.so libcuda.so.1 && popd + + export LD_LIBRARY_PATH=/opt/conda/lib/stubs:$LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH $LD_LIBRARY_PATH" + + python3 --version + which python3 + + echo "----------torch----------" + python3 -m torch.utils.collect_env + + echo "----------k2----------" + python3 -c "import k2; print(k2.__file__)" + python3 -c "import k2; print(k2.__version__)" + python3 -m k2.version + + echo "----------lhotse----------" + python3 -c "import lhotse; print(lhotse.__file__)" + python3 -c "import lhotse; print(lhotse.__version__)" + + echo "----------kaldifeat----------" + python3 -c "import kaldifeat; print(kaldifeat.__file__)" + python3 -c "import kaldifeat; print(kaldifeat.__version__)" + diff --git a/docker/README.md b/docker/README.md index c14b9bf75..19959bfe6 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,5 +1,20 @@ # icefall dockerfile +## Download from dockerhub + +You can find pre-built docker image for icefall at the following address: + + + +Example usage: + +```bash +docker run --gpus all --rm -it k2fsa/icefall:torch1.13.0-cuda11.6 /bin/bash +``` + + +## Build from dockerfile + 2 sets of configuration are provided - (a) Ubuntu18.04-pytorch1.12.1-cuda11.3-cudnn8, and (b) Ubuntu18.04-pytorch1.7.1-cuda11.0-cudnn8. If your NVIDIA driver supports CUDA Version: 11.3, please go for case (a) Ubuntu18.04-pytorch1.12.1-cuda11.3-cudnn8. diff --git a/docker/torch1.12.1-cuda11.3.dockerfile b/docker/torch1.12.1-cuda11.3.dockerfile new file mode 100644 index 000000000..c5e252abb --- /dev/null +++ b/docker/torch1.12.1-cuda11.3.dockerfile @@ -0,0 +1,62 @@ +FROM pytorch/pytorch:1.12.1-cuda11.3-cudnn8-runtime + +ENV LC_ALL C.UTF-8 + +ARG DEBIAN_FRONTEND=noninteractive + +ARG K2_VERSION="1.24.3.dev20230725+cuda11.3.torch1.12.1" +ARG KALDIFEAT_VERSION="1.25.0.dev20230726+cuda11.3.torch1.12.1" +ARG TORCHAUDIO_VERSION="0.12.1+cu113" + +LABEL authors="Fangjun Kuang " +LABEL k2_version=${K2_VERSION} +LABEL kaldifeat_version=${KALDIFEAT_VERSION} +LABEL github_repo="https://github.com/k2-fsa/icefall" + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + vim \ + libssl-dev \ + autoconf \ + automake \ + bzip2 \ + ca-certificates \ + ffmpeg \ + g++ \ + gfortran \ + git \ + libtool \ + make \ + patch \ + sox \ + subversion \ + unzip \ + valgrind \ + wget \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies +RUN pip install --no-cache-dir \ + torchaudio==${TORCHAUDIO_VERSION} -f https://download.pytorch.org/whl/torch_stable.html \ + k2==${K2_VERSION} -f https://k2-fsa.github.io/k2/cuda.html \ + git+https://github.com/lhotse-speech/lhotse \ + kaldifeat==${KALDIFEAT_VERSION} -f https://csukuangfj.github.io/kaldifeat/cuda.html \ + \ + kaldi_native_io \ + kaldialign \ + kaldifst \ + kaldilm \ + sentencepiece>=0.1.96 \ + tensorboard \ + typeguard \ + dill + +RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ + cd /workspace/icefall && \ + pip install --no-cache-dir -r requirements.txt + +ENV PYTHONPATH /workspace/icefall:$PYTHONPATH + +WORKDIR /workspace/icefall diff --git a/docker/torch1.13.0-cuda11.6.dockerfile b/docker/torch1.13.0-cuda11.6.dockerfile new file mode 100644 index 000000000..bcbf8b599 --- /dev/null +++ b/docker/torch1.13.0-cuda11.6.dockerfile @@ -0,0 +1,64 @@ +FROM pytorch/pytorch:1.13.0-cuda11.6-cudnn8-runtime + +ENV LC_ALL C.UTF-8 + +ARG DEBIAN_FRONTEND=noninteractive + +ARG K2_VERSION="1.24.3.dev20230725+cuda11.6.torch1.13.0" +ARG KALDIFEAT_VERSION="1.25.0.dev20230726+cuda11.6.torch1.13.0" +ARG TORCHAUDIO_VERSION="0.13.0+cu116" + +LABEL authors="Fangjun Kuang " +LABEL k2_version=${K2_VERSION} +LABEL kaldifeat_version=${KALDIFEAT_VERSION} +LABEL github_repo="https://github.com/k2-fsa/icefall" + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + vim \ + libssl-dev \ + autoconf \ + automake \ + bzip2 \ + ca-certificates \ + ffmpeg \ + g++ \ + gfortran \ + git \ + libtool \ + make \ + patch \ + sox \ + subversion \ + unzip \ + valgrind \ + wget \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies +RUN pip install --no-cache-dir \ + torchaudio==${TORCHAUDIO_VERSION} -f https://download.pytorch.org/whl/torch_stable.html \ + k2==${K2_VERSION} -f https://k2-fsa.github.io/k2/cuda.html \ + git+https://github.com/lhotse-speech/lhotse \ + kaldifeat==${KALDIFEAT_VERSION} -f https://csukuangfj.github.io/kaldifeat/cuda.html \ + \ + kaldi_native_io \ + kaldialign \ + kaldifst \ + kaldilm \ + sentencepiece>=0.1.96 \ + tensorboard \ + typeguard \ + dill + +RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ + cd /workspace/icefall && \ + pip install --no-cache-dir -r requirements.txt + +ENV PYTHONPATH /workspace/icefall:$PYTHONPATH + +ENV LD_LIBRARY_PATH /opt/conda/lib/stubs:$LD_LIBRARY_PATH + +WORKDIR /workspace/icefall diff --git a/docker/torch1.9.0-cuda10.2.dockerfile b/docker/torch1.9.0-cuda10.2.dockerfile new file mode 100644 index 000000000..7553fcf86 --- /dev/null +++ b/docker/torch1.9.0-cuda10.2.dockerfile @@ -0,0 +1,62 @@ +FROM pytorch/pytorch:1.9.0-cuda10.2-cudnn7-runtime + +ENV LC_ALL C.UTF-8 + +ARG DEBIAN_FRONTEND=noninteractive + +ARG K2_VERSION="1.24.3.dev20230726+cuda10.2.torch1.9.0" +ARG KALDIFEAT_VERSION="1.25.0.dev20230726+cuda10.2.torch1.9.0" +ARG TORCHAUDIO_VERSION="0.9.0" + +LABEL authors="Fangjun Kuang " +LABEL k2_version=${K2_VERSION} +LABEL kaldifeat_version=${KALDIFEAT_VERSION} +LABEL github_repo="https://github.com/k2-fsa/icefall" + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + vim \ + libssl-dev \ + autoconf \ + automake \ + bzip2 \ + ca-certificates \ + ffmpeg \ + g++ \ + gfortran \ + git \ + libtool \ + make \ + patch \ + sox \ + subversion \ + unzip \ + valgrind \ + wget \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies +RUN pip install --no-cache-dir \ + torchaudio==${TORCHAUDIO_VERSION} -f https://download.pytorch.org/whl/torch_stable.html \ + k2==${K2_VERSION} -f https://k2-fsa.github.io/k2/cuda.html \ + kaldifeat==${KALDIFEAT_VERSION} -f https://csukuangfj.github.io/kaldifeat/cuda.html \ + git+https://github.com/lhotse-speech/lhotse \ + \ + kaldi_native_io \ + kaldialign \ + kaldifst \ + kaldilm \ + sentencepiece>=0.1.96 \ + tensorboard \ + typeguard \ + dill + +RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ + cd /workspace/icefall && \ + pip install --no-cache-dir -r requirements.txt + +ENV PYTHONPATH /workspace/icefall:$PYTHONPATH + +WORKDIR /workspace/icefall diff --git a/docker/torch2.0.0-cuda11.7.dockerfile b/docker/torch2.0.0-cuda11.7.dockerfile new file mode 100644 index 000000000..c11c0bd67 --- /dev/null +++ b/docker/torch2.0.0-cuda11.7.dockerfile @@ -0,0 +1,62 @@ +FROM pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime + +ENV LC_ALL C.UTF-8 + +ARG DEBIAN_FRONTEND=noninteractive + +ARG K2_VERSION="1.24.3.dev20230718+cuda11.7.torch2.0.0" +ARG KALDIFEAT_VERSION="1.25.0.dev20230726+cuda11.7.torch2.0.0" +ARG TORCHAUDIO_VERSION="2.0.0+cu117" + +LABEL authors="Fangjun Kuang " +LABEL k2_version=${K2_VERSION} +LABEL kaldifeat_version=${KALDIFEAT_VERSION} +LABEL github_repo="https://github.com/k2-fsa/icefall" + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + curl \ + vim \ + libssl-dev \ + autoconf \ + automake \ + bzip2 \ + ca-certificates \ + ffmpeg \ + g++ \ + gfortran \ + git \ + libtool \ + make \ + patch \ + sox \ + subversion \ + unzip \ + valgrind \ + wget \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install dependencies +RUN pip install --no-cache-dir \ + torchaudio==${TORCHAUDIO_VERSION} -f https://download.pytorch.org/whl/torch_stable.html \ + k2==${K2_VERSION} -f https://k2-fsa.github.io/k2/cuda.html \ + git+https://github.com/lhotse-speech/lhotse \ + kaldifeat==${KALDIFEAT_VERSION} -f https://csukuangfj.github.io/kaldifeat/cuda.html \ + \ + kaldi_native_io \ + kaldialign \ + kaldifst \ + kaldilm \ + sentencepiece>=0.1.96 \ + tensorboard \ + typeguard \ + dill + +RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ + cd /workspace/icefall && \ + pip install --no-cache-dir -r requirements.txt + +ENV PYTHONPATH /workspace/icefall:$PYTHONPATH + +WORKDIR /workspace/icefall From 375520d419826485a206115d66b1471934295081 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 28 Jul 2023 15:43:08 +0800 Subject: [PATCH 061/100] Run the yesno recipe with docker in GitHub actions (#1191) --- .github/workflows/run-docker-image.yml | 34 +++++++++++++++++++++++--- docker/torch1.12.1-cuda11.3.dockerfile | 12 +++++++-- docker/torch1.13.0-cuda11.6.dockerfile | 10 +++++++- docker/torch1.9.0-cuda10.2.dockerfile | 30 ++++++++++++++++++++--- docker/torch2.0.0-cuda11.7.dockerfile | 12 +++++++-- 5 files changed, 86 insertions(+), 12 deletions(-) diff --git a/.github/workflows/run-docker-image.yml b/.github/workflows/run-docker-image.yml index d0ac11071..12604a132 100644 --- a/.github/workflows/run-docker-image.yml +++ b/.github/workflows/run-docker-image.yml @@ -25,12 +25,23 @@ jobs: uses: addnab/docker-run-action@v3 with: image: k2fsa/icefall:${{ matrix.image }} + shell: bash run: | uname -a cat /etc/*release nvcc --version + # For torch1.9.0-cuda10.2 + export LD_LIBRARY_PATH=/usr/local/cuda-10.2/compat:$LD_LIBRARY_PATH + + # For torch1.12.1-cuda11.3 + export LD_LIBRARY_PATH=/usr/local/cuda-11.3/compat:$LD_LIBRARY_PATH + + # For torch2.0.0-cuda11.7 + export LD_LIBRARY_PATH=/usr/local/cuda-11.7/compat:$LD_LIBRARY_PATH + + which nvcc cuda_dir=$(dirname $(which nvcc)) echo "cuda_dir: $cuda_dir" @@ -40,20 +51,26 @@ jobs: find / -name libcuda.so* 2>/dev/null - pushd /opt/conda/lib/stubs && ln -s libcuda.so libcuda.so.1 && popd + # for torch1.13.0-cuda11.6 + if [ -e /opt/conda/lib/stubs/libcuda.so ]; then + cd /opt/conda/lib/stubs && ln -s libcuda.so libcuda.so.1 && cd - + export LD_LIBRARY_PATH=/opt/conda/lib/stubs:$LD_LIBRARY_PATH + fi - export LD_LIBRARY_PATH=/opt/conda/lib/stubs:$LD_LIBRARY_PATH - echo "LD_LIBRARY_PATH $LD_LIBRARY_PATH" + find / -name libcuda.so* 2>/dev/null + echo "LD_LIBRARY_PATH: $LD_LIBRARY_PATH" python3 --version which python3 + python3 -m pip list + echo "----------torch----------" python3 -m torch.utils.collect_env echo "----------k2----------" python3 -c "import k2; print(k2.__file__)" - python3 -c "import k2; print(k2.__version__)" + python3 -c "import k2; print(k2.__dev_version__)" python3 -m k2.version echo "----------lhotse----------" @@ -64,3 +81,12 @@ jobs: python3 -c "import kaldifeat; print(kaldifeat.__file__)" python3 -c "import kaldifeat; print(kaldifeat.__version__)" + echo "Test yesno recipe" + + cd egs/yesno/ASR + + ./prepare.sh + + ./tdnn/train.py + + ./tdnn/decode.py diff --git a/docker/torch1.12.1-cuda11.3.dockerfile b/docker/torch1.12.1-cuda11.3.dockerfile index c5e252abb..5338bdca7 100644 --- a/docker/torch1.12.1-cuda11.3.dockerfile +++ b/docker/torch1.12.1-cuda11.3.dockerfile @@ -1,4 +1,4 @@ -FROM pytorch/pytorch:1.12.1-cuda11.3-cudnn8-runtime +FROM pytorch/pytorch:1.12.1-cuda11.3-cudnn8-devel ENV LC_ALL C.UTF-8 @@ -51,7 +51,15 @@ RUN pip install --no-cache-dir \ sentencepiece>=0.1.96 \ tensorboard \ typeguard \ - dill + dill \ + onnx \ + onnxruntime \ + onnxmltools \ + multi_quantization \ + typeguard \ + numpy \ + pytest \ + graphviz RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ cd /workspace/icefall && \ diff --git a/docker/torch1.13.0-cuda11.6.dockerfile b/docker/torch1.13.0-cuda11.6.dockerfile index bcbf8b599..4d2f96c8e 100644 --- a/docker/torch1.13.0-cuda11.6.dockerfile +++ b/docker/torch1.13.0-cuda11.6.dockerfile @@ -51,7 +51,15 @@ RUN pip install --no-cache-dir \ sentencepiece>=0.1.96 \ tensorboard \ typeguard \ - dill + dill \ + onnx \ + onnxruntime \ + onnxmltools \ + multi_quantization \ + typeguard \ + numpy \ + pytest \ + graphviz RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ cd /workspace/icefall && \ diff --git a/docker/torch1.9.0-cuda10.2.dockerfile b/docker/torch1.9.0-cuda10.2.dockerfile index 7553fcf86..a7cef6dc8 100644 --- a/docker/torch1.9.0-cuda10.2.dockerfile +++ b/docker/torch1.9.0-cuda10.2.dockerfile @@ -1,4 +1,4 @@ -FROM pytorch/pytorch:1.9.0-cuda10.2-cudnn7-runtime +FROM pytorch/pytorch:1.9.0-cuda10.2-cudnn7-devel ENV LC_ALL C.UTF-8 @@ -13,6 +13,13 @@ LABEL k2_version=${K2_VERSION} LABEL kaldifeat_version=${KALDIFEAT_VERSION} LABEL github_repo="https://github.com/k2-fsa/icefall" +# see https://developer.nvidia.com/blog/updating-the-cuda-linux-gpg-repository-key/ + +RUN rm /etc/apt/sources.list.d/cuda.list && \ + rm /etc/apt/sources.list.d/nvidia-ml.list && \ + apt-key del 7fa2af80 + + RUN apt-get update && \ apt-get install -y --no-install-recommends \ curl \ @@ -37,8 +44,15 @@ RUN apt-get update && \ zlib1g-dev \ && rm -rf /var/lib/apt/lists/* +RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-keyring_1.0-1_all.deb && \ + dpkg -i cuda-keyring_1.0-1_all.deb && \ + rm -v cuda-keyring_1.0-1_all.deb && \ + apt-get update && \ + rm -rf /var/lib/apt/lists/* + # Install dependencies -RUN pip install --no-cache-dir \ +RUN pip uninstall -y tqdm && \ + pip install -U --no-cache-dir \ torchaudio==${TORCHAUDIO_VERSION} -f https://download.pytorch.org/whl/torch_stable.html \ k2==${K2_VERSION} -f https://k2-fsa.github.io/k2/cuda.html \ kaldifeat==${KALDIFEAT_VERSION} -f https://csukuangfj.github.io/kaldifeat/cuda.html \ @@ -51,7 +65,17 @@ RUN pip install --no-cache-dir \ sentencepiece>=0.1.96 \ tensorboard \ typeguard \ - dill + dill \ + onnx \ + onnxruntime \ + onnxmltools \ + multi_quantization \ + typeguard \ + numpy \ + pytest \ + graphviz \ + tqdm>=4.63.0 + RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ cd /workspace/icefall && \ diff --git a/docker/torch2.0.0-cuda11.7.dockerfile b/docker/torch2.0.0-cuda11.7.dockerfile index c11c0bd67..d91fbc24f 100644 --- a/docker/torch2.0.0-cuda11.7.dockerfile +++ b/docker/torch2.0.0-cuda11.7.dockerfile @@ -1,4 +1,4 @@ -FROM pytorch/pytorch:2.0.0-cuda11.7-cudnn8-runtime +FROM pytorch/pytorch:2.0.0-cuda11.7-cudnn8-devel ENV LC_ALL C.UTF-8 @@ -51,7 +51,15 @@ RUN pip install --no-cache-dir \ sentencepiece>=0.1.96 \ tensorboard \ typeguard \ - dill + dill \ + onnx \ + onnxruntime \ + onnxmltools \ + multi_quantization \ + typeguard \ + numpy \ + pytest \ + graphviz RUN git clone https://github.com/k2-fsa/icefall /workspace/icefall && \ cd /workspace/icefall && \ From bcabaf896c0eadef1ed8d86907847c367e4bd14f Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 1 Aug 2023 12:28:34 +0800 Subject: [PATCH 062/100] Add doc describing how to run icefall within a docker container (#1194) --- docs/source/docker/img/docker-hub.png | Bin 0 -> 364778 bytes docs/source/docker/index.rst | 17 +++ docs/source/docker/intro.rst | 171 ++++++++++++++++++++++++++ docs/source/index.rst | 4 +- docs/source/installation/index.rst | 5 + 5 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 docs/source/docker/img/docker-hub.png create mode 100644 docs/source/docker/index.rst create mode 100644 docs/source/docker/intro.rst diff --git a/docs/source/docker/img/docker-hub.png b/docs/source/docker/img/docker-hub.png new file mode 100644 index 0000000000000000000000000000000000000000..a9e7715b0b41d49cf6a2717d2f3f42c193269134 GIT binary patch literal 364778 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 z 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

D=YC5d9=Y(e^_qnMxdqW)Nvs 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 literal 0 HcmV?d00001 diff --git a/docs/source/docker/index.rst b/docs/source/docker/index.rst new file mode 100644 index 000000000..2c92a4cbc --- /dev/null +++ b/docs/source/docker/index.rst @@ -0,0 +1,17 @@ +.. _icefall_docker: + +Docker +====== + +This section describes how to use pre-built docker images to run `icefall`_. + +.. hint:: + + If you only have CPUs available, you can still use the pre-built docker + images. + +.. toctree:: + :maxdepth: 2 + + ./intro.rst + diff --git a/docs/source/docker/intro.rst b/docs/source/docker/intro.rst new file mode 100644 index 000000000..b09247d85 --- /dev/null +++ b/docs/source/docker/intro.rst @@ -0,0 +1,171 @@ +Introduction +============= + +We have pre-built docker images hosted at the following address: + + ``_ + +.. figure:: img/docker-hub.png + :width: 600 + :align: center + +You can find the ``Dockerfile`` at ``_. + +We describe the following items in this section: + + - How to view available tags + - How to download pre-built docker images + - How to run the `yesno`_ recipe within a docker container on ``CPU`` + +View available tags +=================== + +You can use the following command to view available tags: + +.. code-block:: bash + + curl -s 'https://registry.hub.docker.com/v2/repositories/k2fsa/icefall/tags/'|jq '."results"[]["name"]' + +which will give you something like below: + +.. code-block:: bash + + "torch2.0.0-cuda11.7" + "torch1.12.1-cuda11.3" + "torch1.9.0-cuda10.2" + "torch1.13.0-cuda11.6" + +.. hint:: + + Available tags will be updated when there are new releases of `torch`_. + +Please select an appropriate combination of `torch`_ and CUDA. + +Download a docker image +======================= + +Suppose that you select the tag ``torch1.13.0-cuda11.6``, you can use +the following command to download it: + +.. code-block:: bash + + sudo docker image pull k2fsa/icefall:torch1.13.0-cuda11.6 + +Run a docker image with GPU +=========================== + +.. code-block:: bash + + sudo docker run --gpus all --rm -it k2fsa/icefall:torch1.13.0-cuda11.6 /bin/bash + +Run a docker image with CPU +=========================== + +.. code-block:: bash + + sudo docker run --rm -it k2fsa/icefall:torch1.13.0-cuda11.6 /bin/bash + +Run yesno within a docker container +=================================== + +After starting the container, the following interface is presented: + +.. code-block:: bash + + root@60c947eac59c:/workspace/icefall# + +It shows the current user is ``root`` and the current working directory +is ``/workspace/icefall``. + +Update the code +--------------- + +Please first run: + +.. code-block:: bash + + root@60c947eac59c:/workspace/icefall# git pull + +so that your local copy contains the latest code. + +Data preparation +---------------- + +Now we can use + +.. code-block:: bash + + root@60c947eac59c:/workspace/icefall# cd egs/yesno/ASR/ + +to switch to the ``yesno`` recipe and run + +.. code-block:: bash + + root@60c947eac59c:/workspace/icefall/egs/yesno/ASR# ./prepare.sh + +.. hint:: + + If you are running without GPU, it may report the following error: + + .. code-block:: bash + + File "/opt/conda/lib/python3.9/site-packages/k2/__init__.py", line 23, in + from _k2 import DeterminizeWeightPushingType + ImportError: libcuda.so.1: cannot open shared object file: No such file or directory + + We can use the following command to fix it: + + .. code-block:: bash + + root@60c947eac59c:/workspace/icefall/egs/yesno/ASR# ln -s /opt/conda/lib/stubs/libcuda.so /opt/conda/lib/stubs/libcuda.so.1 + +The logs of running ``./prepare.sh`` are listed below: + +.. literalinclude:: ./log/log-preparation.txt + +Training +-------- + +After preparing the data, we can start training with the following command + +.. code-block:: bash + + root@60c947eac59c:/workspace/icefall/egs/yesno/ASR# ./tdnn/train.py + +All of the training logs are given below: + +.. hint:: + + It is running on CPU and it takes only 16 seconds for this run. + +.. literalinclude:: ./log/log-train-2023-08-01-01-55-27 + + +Decoding +-------- + +After training, we can decode the trained model with + +.. code-block:: bash + + root@60c947eac59c:/workspace/icefall/egs/yesno/ASR# ./tdnn/decode.py + +The decoding logs are given below: + +.. code-block:: bash + + 2023-08-01 02:06:22,400 INFO [decode.py:263] Decoding started + 2023-08-01 02:06:22,400 INFO [decode.py:264] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lm_dir': PosixPath('data/lm'), 'feature_dim': 23, 'search_beam': 20, 'output_beam': 8, 'min_active_states': 30, 'max_active_states': 10000, 'use_double_scores': True, 'epoch': 14, 'avg': 2, 'export': False, 'feature_dir': PosixPath('data/fbank'), 'max_duration': 30.0, 'bucketing_sampler': False, 'num_buckets': 10, 'concatenate_cuts': False, 'duration_factor': 1.0, 'gap': 1.0, 'on_the_fly_feats': False, 'shuffle': False, 'return_cuts': True, 'num_workers': 2, 'env_info': {'k2-version': '1.24.3', 'k2-build-type': 'Release', 'k2-with-cuda': True, 'k2-git-sha1': '4c05309499a08454997adf500b56dcc629e35ae5', 'k2-git-date': 'Tue Jul 25 16:23:36 2023', 'lhotse-version': '1.16.0.dev+git.7640d663.clean', 'torch-version': '1.13.0', 'torch-cuda-available': False, 'torch-cuda-version': '11.6', 'python-version': '3.9', 'icefall-git-branch': 'master', 'icefall-git-sha1': '375520d-clean', 'icefall-git-date': 'Fri Jul 28 07:43:08 2023', 'icefall-path': '/workspace/icefall', 'k2-path': '/opt/conda/lib/python3.9/site-packages/k2/__init__.py', 'lhotse-path': '/opt/conda/lib/python3.9/site-packages/lhotse/__init__.py', 'hostname': '60c947eac59c', 'IP address': '172.17.0.2'}} + 2023-08-01 02:06:22,401 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt + 2023-08-01 02:06:22,403 INFO [decode.py:273] device: cpu + 2023-08-01 02:06:22,406 INFO [decode.py:291] averaging ['tdnn/exp/epoch-13.pt', 'tdnn/exp/epoch-14.pt'] + 2023-08-01 02:06:22,424 INFO [asr_datamodule.py:218] About to get test cuts + 2023-08-01 02:06:22,425 INFO [asr_datamodule.py:252] About to get test cuts + 2023-08-01 02:06:22,504 INFO [decode.py:204] batch 0/?, cuts processed until now is 4 + [W NNPACK.cpp:53] Could not initialize NNPACK! Reason: Unsupported hardware. + 2023-08-01 02:06:22,687 INFO [decode.py:241] The transcripts are stored in tdnn/exp/recogs-test_set.txt + 2023-08-01 02:06:22,688 INFO [utils.py:564] [test_set] %WER 0.42% [1 / 240, 0 ins, 1 del, 0 sub ] + 2023-08-01 02:06:22,690 INFO [decode.py:249] Wrote detailed error stats to tdnn/exp/errs-test_set.txt + 2023-08-01 02:06:22,690 INFO [decode.py:316] Done! + +Congratulations! You have finished successfully running `icefall`_ within a docker container. diff --git a/docs/source/index.rst b/docs/source/index.rst index a7d365a15..0fa8fdd1c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,9 +21,11 @@ speech recognition recipes using `k2 `_. :caption: Contents: installation/index + docker/index faqs model-export/index + .. toctree:: :maxdepth: 3 @@ -38,4 +40,4 @@ speech recognition recipes using `k2 `_. .. toctree:: :maxdepth: 2 - decoding-with-langugage-models/index \ No newline at end of file + decoding-with-langugage-models/index diff --git a/docs/source/installation/index.rst b/docs/source/installation/index.rst index 534b674f9..5a034ef5b 100644 --- a/docs/source/installation/index.rst +++ b/docs/source/installation/index.rst @@ -3,6 +3,11 @@ Installation ============ +.. hint:: + + We also provide :ref:`icefall_docker` support, which has already setup + the environment for you. + .. hint:: We have a colab notebook guiding you step by step to setup the environment. From 1ee251c8b385f6dcf06da40b1760b76496b0d812 Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Thu, 3 Aug 2023 15:50:35 +0800 Subject: [PATCH 063/100] Decode zipformer with external LMs (#1193) * update some documentation * support decoding with LMs in zipformer recipe * update RESULTS.md --- .../decoding-with-langugage-models/LODR.rst | 54 ++--- .../rescoring.rst | 6 +- .../shallow-fusion.rst | 4 +- egs/librispeech/ASR/RESULTS.md | 7 + .../decode.py | 7 + egs/librispeech/ASR/zipformer/decode.py | 216 ++++++++++++++++-- 6 files changed, 238 insertions(+), 56 deletions(-) diff --git a/docs/source/decoding-with-langugage-models/LODR.rst b/docs/source/decoding-with-langugage-models/LODR.rst index 7ffa0c128..b6625ee1d 100644 --- a/docs/source/decoding-with-langugage-models/LODR.rst +++ b/docs/source/decoding-with-langugage-models/LODR.rst @@ -4,59 +4,59 @@ LODR for RNN Transducer ======================= -As a type of E2E model, neural transducers are usually considered as having an internal -language model, which learns the language level information on the training corpus. -In real-life scenario, there is often a mismatch between the training corpus and the target corpus space. +As a type of E2E model, neural transducers are usually considered as having an internal +language model, which learns the language level information on the training corpus. +In real-life scenario, there is often a mismatch between the training corpus and the target corpus space. This mismatch can be a problem when decoding for neural transducer models with language models as its internal language can act "against" the external LM. In this tutorial, we show how to use `Low-order Density Ratio `_ to alleviate this effect to further improve the performance -of langugae model integration. +of langugae model integration. .. note:: - This tutorial is based on the recipe + This tutorial is based on the recipe `pruned_transducer_stateless7_streaming `_, - which is a streaming transducer model trained on `LibriSpeech`_. + which is a streaming transducer model trained on `LibriSpeech`_. However, you can easily apply LODR to other recipes. If you encounter any problems, please open an issue here `icefall `__. .. note:: - For simplicity, the training and testing corpus in this tutorial are the same (`LibriSpeech`_). However, - you can change the testing set to any other domains (e.g `GigaSpeech`_) and prepare the language models + For simplicity, the training and testing corpus in this tutorial are the same (`LibriSpeech`_). However, + you can change the testing set to any other domains (e.g `GigaSpeech`_) and prepare the language models using that corpus. -First, let's have a look at some background information. As the predecessor of LODR, Density Ratio (DR) is first proposed `here `_ +First, let's have a look at some background information. As the predecessor of LODR, Density Ratio (DR) is first proposed `here `_ to address the language information mismatch between the training corpus (source domain) and the testing corpus (target domain). Assuming that the source domain and the test domain are acoustically similar, DR derives the following formular for decoding with Bayes' theorem: .. math:: - \text{score}\left(y_u|\mathit{x},y\right) = - \log p\left(y_u|\mathit{x},y_{1:u-1}\right) + - \lambda_1 \log p_{\text{Target LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) - + \text{score}\left(y_u|\mathit{x},y\right) = + \log p\left(y_u|\mathit{x},y_{1:u-1}\right) + + \lambda_1 \log p_{\text{Target LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) - \lambda_2 \log p_{\text{Source LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) -where :math:`\lambda_1` and :math:`\lambda_2` are the weights of LM scores for target domain and source domain respectively. -Here, the source domain LM is trained on the training corpus. The only difference in the above formular compared to +where :math:`\lambda_1` and :math:`\lambda_2` are the weights of LM scores for target domain and source domain respectively. +Here, the source domain LM is trained on the training corpus. The only difference in the above formular compared to shallow fusion is the subtraction of the source domain LM. -Some works treat the predictor and the joiner of the neural transducer as its internal LM. However, the LM is +Some works treat the predictor and the joiner of the neural transducer as its internal LM. However, the LM is considered to be weak and can only capture low-level language information. Therefore, `LODR `__ proposed to use a low-order n-gram LM as an approximation of the ILM of the neural transducer. This leads to the following formula during decoding for transducer model: .. math:: - \text{score}\left(y_u|\mathit{x},y\right) = - \log p_{rnnt}\left(y_u|\mathit{x},y_{1:u-1}\right) + - \lambda_1 \log p_{\text{Target LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) - + \text{score}\left(y_u|\mathit{x},y\right) = + \log p_{rnnt}\left(y_u|\mathit{x},y_{1:u-1}\right) + + \lambda_1 \log p_{\text{Target LM}}\left(y_u|\mathit{x},y_{1:u-1}\right) - \lambda_2 \log p_{\text{bi-gram}}\left(y_u|\mathit{x},y_{1:u-1}\right) -In LODR, an additional bi-gram LM estimated on the source domain (e.g training corpus) is required. Comared to DR, +In LODR, an additional bi-gram LM estimated on the source domain (e.g training corpus) is required. Comared to DR, the only difference lies in the choice of source domain LM. According to the original `paper `_, LODR achieves similar performance compared DR in both intra-domain and cross-domain settings. As a bi-gram is much faster to evaluate, LODR is usually much faster. @@ -85,7 +85,7 @@ To test the model, let's have a look at the decoding results **without** using L --avg 1 \ --use-averaged-model False \ --exp-dir $exp_dir \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model \ --max-duration 600 \ --decode-chunk-len 32 \ --decoding-method modified_beam_search @@ -99,17 +99,17 @@ The following WERs are achieved on test-clean and test-other: $ For test-other, WER of different settings are: $ beam_size_4 7.93 best for test-other -Then, we download the external language model and bi-gram LM that are necessary for LODR. +Then, we download the external language model and bi-gram LM that are necessary for LODR. Note that the bi-gram is estimated on the LibriSpeech 960 hours' text. .. code-block:: bash $ # download the external LM - $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/ezerhouni/icefall-librispeech-rnn-lm + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/ezerhouni/icefall-librispeech-rnn-lm $ # create a symbolic link so that the checkpoint can be loaded $ pushd icefall-librispeech-rnn-lm/exp $ git lfs pull --include "pretrained.pt" - $ ln -s pretrained.pt epoch-99.pt + $ ln -s pretrained.pt epoch-99.pt $ popd $ $ # download the bi-gram @@ -122,7 +122,7 @@ Note that the bi-gram is estimated on the LibriSpeech 960 hours' text. Then, we perform LODR decoding by setting ``--decoding-method`` to ``modified_beam_search_lm_LODR``: .. code-block:: bash - + $ exp_dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp $ lm_dir=./icefall-librispeech-rnn-lm/exp $ lm_scale=0.42 @@ -135,8 +135,8 @@ Then, we perform LODR decoding by setting ``--decoding-method`` to ``modified_be --exp-dir $exp_dir \ --max-duration 600 \ --decode-chunk-len 32 \ - --decoding-method modified_beam_search_lm_LODR \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --decoding-method modified_beam_search_LODR \ + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model \ --use-shallow-fusion 1 \ --lm-type rnn \ --lm-exp-dir $lm_dir \ @@ -181,4 +181,4 @@ indeed **further improves** the WER. We can do even better if we increase ``--be - 6.38 * - 12 - 2.4 - - 6.23 \ No newline at end of file + - 6.23 diff --git a/docs/source/decoding-with-langugage-models/rescoring.rst b/docs/source/decoding-with-langugage-models/rescoring.rst index ee2e2113c..02eba9129 100644 --- a/docs/source/decoding-with-langugage-models/rescoring.rst +++ b/docs/source/decoding-with-langugage-models/rescoring.rst @@ -48,7 +48,7 @@ As usual, we first test the model's performance without external LM. This can be --avg 1 \ --use-averaged-model False \ --exp-dir $exp_dir \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model \ --max-duration 600 \ --decode-chunk-len 32 \ --decoding-method modified_beam_search @@ -101,7 +101,7 @@ is set to `False`. --max-duration 600 \ --decode-chunk-len 32 \ --decoding-method modified_beam_search_lm_rescore \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model \ --use-shallow-fusion 0 \ --lm-type rnn \ --lm-exp-dir $lm_dir \ @@ -173,7 +173,7 @@ Then we can performn LM rescoring + LODR by changing the decoding method to `mod --max-duration 600 \ --decode-chunk-len 32 \ --decoding-method modified_beam_search_lm_rescore_LODR \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model \ --use-shallow-fusion 0 \ --lm-type rnn \ --lm-exp-dir $lm_dir \ diff --git a/docs/source/decoding-with-langugage-models/shallow-fusion.rst b/docs/source/decoding-with-langugage-models/shallow-fusion.rst index 0d2837372..f15e3f1d9 100644 --- a/docs/source/decoding-with-langugage-models/shallow-fusion.rst +++ b/docs/source/decoding-with-langugage-models/shallow-fusion.rst @@ -46,7 +46,7 @@ To test the model, let's have a look at the decoding results without using LM. T --avg 1 \ --use-averaged-model False \ --exp-dir $exp_dir \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model \ --max-duration 600 \ --decode-chunk-len 32 \ --decoding-method modified_beam_search @@ -95,7 +95,7 @@ To use shallow fusion for decoding, we can execute the following command: --max-duration 600 \ --decode-chunk-len 32 \ --decoding-method modified_beam_search_lm_shallow_fusion \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model + --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/data/lang_bpe_500/bpe.model \ --use-shallow-fusion 1 \ --lm-type rnn \ --lm-exp-dir $lm_dir \ diff --git a/egs/librispeech/ASR/RESULTS.md b/egs/librispeech/ASR/RESULTS.md index 1b8e690bd..b945f43fd 100644 --- a/egs/librispeech/ASR/RESULTS.md +++ b/egs/librispeech/ASR/RESULTS.md @@ -90,6 +90,11 @@ You can use to deploy it. | greedy_search | 2.23 | 4.96 | --epoch 40 --avg 16 | | modified_beam_search | 2.21 | 4.91 | --epoch 40 --avg 16 | | fast_beam_search | 2.24 | 4.93 | --epoch 40 --avg 16 | +| modified_beam_search_shallow_fusion | 2.01 | 4.37 | --epoch 40 --avg 16 --beam-size 12 --lm-scale 0.3 | +| modified_beam_search_LODR | 1.94 | 4.17 | --epoch 40 --avg 16 --beam-size 12 --lm-scale 0.52 --LODR-scale -0.26 | +| modified_beam_search_rescore | 2.04 | 4.39 | --epoch 40 --avg 16 --beam-size 12 | +| modified_beam_search_rescore_LODR | 2.01 | 4.33 | --epoch 40 --avg 16 --beam-size 12 | + The training command is: ```bash @@ -119,6 +124,8 @@ for m in greedy_search modified_beam_search fast_beam_search; do done ``` +To decode with external language models, please refer to the documentation [here](https://k2-fsa.github.io/icefall/decoding-with-langugage-models/index.html). + ##### small-scaled model, number of model parameters: 23285615, i.e., 23.3 M The tensorboard log can be found at diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/decode.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/decode.py index 3444f8193..02029c108 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/decode.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/decode.py @@ -396,6 +396,12 @@ def decode_one_batch( The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used only when --decoding_method is fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + LM: + A neural network language model. + ngram_lm: + A ngram language model + ngram_lm_scale: + The scale for the ngram language model. Returns: Return the decoding result. See above description for the format of the returned dict. @@ -907,6 +913,7 @@ def main(): ngram_file_name = str(params.lang_dir / f"{params.tokens_ngram}gram.arpa") logging.info(f"lm filename: {ngram_file_name}") ngram_lm = kenlm.Model(ngram_file_name) + ngram_lm_scale = None # use a list to search elif params.decoding_method == "modified_beam_search_LODR": lm_filename = f"{params.tokens_ngram}gram.fst.txt" diff --git a/egs/librispeech/ASR/zipformer/decode.py b/egs/librispeech/ASR/zipformer/decode.py index 93680602e..2cc157e7a 100755 --- a/egs/librispeech/ASR/zipformer/decode.py +++ b/egs/librispeech/ASR/zipformer/decode.py @@ -115,9 +115,14 @@ from beam_search import ( greedy_search, greedy_search_batch, modified_beam_search, + modified_beam_search_lm_rescore, + modified_beam_search_lm_rescore_LODR, + modified_beam_search_lm_shallow_fusion, + modified_beam_search_LODR, ) -from train import add_model_arguments, get_params, get_model +from train import add_model_arguments, get_model, get_params +from icefall import LmScorer, NgramLm from icefall.checkpoint import ( average_checkpoints, average_checkpoints_with_averaged_model, @@ -273,8 +278,7 @@ def get_parser(): "--context-size", type=int, default=2, - help="The context size in the decoder. 1 means bigram; " - "2 means tri-gram", + help="The context size in the decoder. 1 means bigram; " "2 means tri-gram", ) parser.add_argument( "--max-sym-per-frame", @@ -302,6 +306,47 @@ def get_parser(): fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", ) + parser.add_argument( + "--use-shallow-fusion", + type=str2bool, + default=False, + help="""Use neural network LM for shallow fusion. + If you want to use LODR, you will also need to set this to true + """, + ) + + parser.add_argument( + "--lm-type", + type=str, + default="rnn", + help="Type of NN lm", + choices=["rnn", "transformer"], + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.3, + help="""The scale of the neural network LM + Used only when `--use-shallow-fusion` is set to True. + """, + ) + + parser.add_argument( + "--tokens-ngram", + type=int, + default=2, + help="""The order of the ngram lm. + """, + ) + + parser.add_argument( + "--backoff-id", + type=int, + default=500, + help="ID of the backoff symbol in the ngram LM", + ) + add_model_arguments(parser) return parser @@ -314,6 +359,9 @@ def decode_one_batch( batch: dict, word_table: Optional[k2.SymbolTable] = None, decoding_graph: Optional[k2.Fsa] = None, + LM: Optional[LmScorer] = None, + ngram_lm=None, + ngram_lm_scale: float = 0.0, ) -> Dict[str, List[List[str]]]: """Decode one batch and return the result in a dict. The dict has the following format: @@ -342,6 +390,12 @@ def decode_one_batch( The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used only when --decoding_method is fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + LM: + A neural network language model. + ngram_lm: + A ngram language model + ngram_lm_scale: + The scale for the ngram language model. Returns: Return the decoding result. See above description for the format of the returned dict. @@ -425,10 +479,7 @@ def decode_one_batch( ) for hyp in sp.decode(hyp_tokens): hyps.append(hyp.split()) - elif ( - params.decoding_method == "greedy_search" - and params.max_sym_per_frame == 1 - ): + elif params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, @@ -445,6 +496,50 @@ def decode_one_batch( ) for hyp in sp.decode(hyp_tokens): hyps.append(hyp.split()) + elif params.decoding_method == "modified_beam_search_lm_shallow_fusion": + hyp_tokens = modified_beam_search_lm_shallow_fusion( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + LM=LM, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "modified_beam_search_LODR": + hyp_tokens = modified_beam_search_LODR( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + LODR_lm=ngram_lm, + LODR_lm_scale=ngram_lm_scale, + LM=LM, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "modified_beam_search_lm_rescore": + lm_scale_list = [0.01 * i for i in range(10, 50)] + ans_dict = modified_beam_search_lm_rescore( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + LM=LM, + lm_scale_list=lm_scale_list, + ) + elif params.decoding_method == "modified_beam_search_lm_rescore_LODR": + lm_scale_list = [0.02 * i for i in range(2, 30)] + ans_dict = modified_beam_search_lm_rescore_LODR( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + LM=LM, + LODR_lm=ngram_lm, + sp=sp, + lm_scale_list=lm_scale_list, + ) else: batch_size = encoder_out.size(0) @@ -483,6 +578,16 @@ def decode_one_batch( key += f"_ngram_lm_scale_{params.ngram_lm_scale}" return {key: hyps} + elif params.decoding_method in ( + "modified_beam_search_lm_rescore", + "modified_beam_search_lm_rescore_LODR", + ): + ans = dict() + assert ans_dict is not None + for key, hyps in ans_dict.items(): + hyps = [sp.decode(hyp).split() for hyp in hyps] + ans[f"beam_size_{params.beam_size}_{key}"] = hyps + return ans else: return {f"beam_size_{params.beam_size}": hyps} @@ -494,6 +599,9 @@ def decode_dataset( sp: spm.SentencePieceProcessor, word_table: Optional[k2.SymbolTable] = None, decoding_graph: Optional[k2.Fsa] = None, + LM: Optional[LmScorer] = None, + ngram_lm=None, + ngram_lm_scale: float = 0.0, ) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: """Decode dataset. @@ -543,6 +651,9 @@ def decode_dataset( decoding_graph=decoding_graph, word_table=word_table, batch=batch, + LM=LM, + ngram_lm=ngram_lm, + ngram_lm_scale=ngram_lm_scale, ) for name, hyps in hyps_dict.items(): @@ -559,9 +670,7 @@ def decode_dataset( if batch_idx % log_interval == 0: batch_str = f"{batch_idx}/{num_batches}" - logging.info( - f"batch {batch_str}, cuts processed until now is {num_cuts}" - ) + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") return results @@ -594,8 +703,7 @@ def save_results( test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) errs_info = ( - params.res_dir - / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" ) with open(errs_info, "w") as f: print("settings\tWER", file=f) @@ -614,6 +722,7 @@ def save_results( def main(): parser = get_parser() LibriSpeechAsrDataModule.add_arguments(parser) + LmScorer.add_arguments(parser) args = parser.parse_args() args.exp_dir = Path(args.exp_dir) @@ -628,6 +737,10 @@ def main(): "fast_beam_search_nbest_LG", "fast_beam_search_nbest_oracle", "modified_beam_search", + "modified_beam_search_LODR", + "modified_beam_search_lm_shallow_fusion", + "modified_beam_search_lm_rescore", + "modified_beam_search_lm_rescore_LODR", ) params.res_dir = params.exp_dir / params.decoding_method @@ -656,13 +769,19 @@ def main(): if "LG" in params.decoding_method: params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" elif "beam_search" in params.decoding_method: - params.suffix += ( - f"-{params.decoding_method}-beam-size-{params.beam_size}" - ) + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" else: params.suffix += f"-context-{params.context_size}" params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + if params.use_shallow_fusion: + params.suffix += f"-{params.lm_type}-lm-scale-{params.lm_scale}" + + if "LODR" in params.decoding_method: + params.suffix += ( + f"-LODR-{params.tokens_ngram}gram-scale-{params.ngram_lm_scale}" + ) + if params.use_averaged_model: params.suffix += "-use-averaged-model" @@ -690,9 +809,9 @@ def main(): if not params.use_averaged_model: if params.iter > 0: - filenames = find_checkpoints( - params.exp_dir, iteration=-params.iter - )[: params.avg] + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] if len(filenames) == 0: raise ValueError( f"No checkpoints found for" @@ -719,9 +838,9 @@ def main(): model.load_state_dict(average_checkpoints(filenames, device=device)) else: if params.iter > 0: - filenames = find_checkpoints( - params.exp_dir, iteration=-params.iter - )[: params.avg + 1] + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] if len(filenames) == 0: raise ValueError( f"No checkpoints found for" @@ -768,6 +887,54 @@ def main(): model.to(device) model.eval() + # only load the neural network LM if required + if params.use_shallow_fusion or params.decoding_method in ( + "modified_beam_search_lm_rescore", + "modified_beam_search_lm_rescore_LODR", + "modified_beam_search_lm_shallow_fusion", + "modified_beam_search_LODR", + ): + LM = LmScorer( + lm_type=params.lm_type, + params=params, + device=device, + lm_scale=params.lm_scale, + ) + LM.to(device) + LM.eval() + else: + LM = None + + # only load N-gram LM when needed + if params.decoding_method == "modified_beam_search_lm_rescore_LODR": + try: + import kenlm + except ImportError: + print("Please install kenlm first. You can use") + print(" pip install https://github.com/kpu/kenlm/archive/master.zip") + print("to install it") + import sys + + sys.exit(-1) + ngram_file_name = str(params.lang_dir / f"{params.tokens_ngram}gram.arpa") + logging.info(f"lm filename: {ngram_file_name}") + ngram_lm = kenlm.Model(ngram_file_name) + ngram_lm_scale = None # use a list to search + + elif params.decoding_method == "modified_beam_search_LODR": + lm_filename = f"{params.tokens_ngram}gram.fst.txt" + logging.info(f"Loading token level lm: {lm_filename}") + ngram_lm = NgramLm( + str(params.lang_dir / lm_filename), + backoff_id=params.backoff_id, + is_binary=False, + ) + logging.info(f"num states: {ngram_lm.lm.num_states}") + ngram_lm_scale = params.ngram_lm_scale + else: + ngram_lm = None + ngram_lm_scale = None + if "fast_beam_search" in params.decoding_method: if params.decoding_method == "fast_beam_search_nbest_LG": lexicon = Lexicon(params.lang_dir) @@ -780,9 +947,7 @@ def main(): decoding_graph.scores *= params.ngram_lm_scale else: word_table = None - decoding_graph = k2.trivial_graph( - params.vocab_size - 1, device=device - ) + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) else: decoding_graph = None word_table = None @@ -811,6 +976,9 @@ def main(): sp=sp, word_table=word_table, decoding_graph=decoding_graph, + LM=LM, + ngram_lm=ngram_lm, + ngram_lm_scale=ngram_lm_scale, ) save_results( From 00256a766921dd34a267012b0e2b8ff7d538f0e6 Mon Sep 17 00:00:00 2001 From: Yifan Yang <64255737+yfyeung@users.noreply.github.com> Date: Wed, 9 Aug 2023 09:40:58 +0800 Subject: [PATCH 064/100] Fix decode_stream.py (#1208) * FIx decode_stream.py * Update decode_stream.py --- egs/librispeech/ASR/zipformer/decode_stream.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egs/librispeech/ASR/zipformer/decode_stream.py b/egs/librispeech/ASR/zipformer/decode_stream.py index 946db275c..d6918bf32 100644 --- a/egs/librispeech/ASR/zipformer/decode_stream.py +++ b/egs/librispeech/ASR/zipformer/decode_stream.py @@ -79,12 +79,12 @@ class DecodeStream(object): self.pad_length = 7 + 2 * 3 if params.decoding_method == "greedy_search": - self.hyp = [params.blank_id] * params.context_size + self.hyp = [-1] * (params.context_size - 1) + [params.blank_id] elif params.decoding_method == "modified_beam_search": self.hyps = HypothesisList() self.hyps.add( Hypothesis( - ys=[params.blank_id] * params.context_size, + ys=[-1] * (params.context_size - 1) + [params.blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), ) ) From 74806b744b81620d06645c27f5a2dda307e58322 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Thu, 10 Aug 2023 20:56:02 +0800 Subject: [PATCH 065/100] disable speed perturbation by default (#1176) * disable speed perturbation by default * minor fixes * minor updates * updated bash scripts to incorporate with the `speed-perturb` arg * minor fixes 1. changed the naming scheme from `speed-perturb` to `perturb-speed` to align with the librispeech recipe >> https://github.com/k2-fsa/icefall/blob/00256a766921dd34a267012b0e2b8ff7d538f0e6/egs/librispeech/ASR/local/compute_fbank_librispeech.py#L65 2. changed arg type for `perturb-speed` to str2bool --- .../local/compute_fbank_aidatatang_200zh.py | 18 ++++++++--- egs/aidatatang_200zh/ASR/prepare.sh | 2 +- .../local/compute_fbank_aidatatang_200zh.py | 18 ++++++++--- .../ASR/local/compute_fbank_aishell.py | 18 ++++++++--- egs/aishell/ASR/prepare.sh | 2 +- egs/aishell/ASR/prepare_aidatatang_200zh.sh | 2 +- .../ASR/local/compute_fbank_aishell2.py | 17 +++++++--- egs/aishell2/ASR/prepare.sh | 2 +- .../ASR/local/compute_fbank_aishell4.py | 18 ++++++++--- egs/aishell4/ASR/prepare.sh | 2 +- .../ASR/local/compute_fbank_alimeeting.py | 17 +++++++--- egs/alimeeting/ASR/prepare.sh | 2 +- .../ASR_v2/local/compute_fbank_alimeeting.py | 32 ++++++++++++++++--- egs/alimeeting/ASR_v2/prepare.sh | 2 +- .../ASR/local/preprocess_wenetspeech.py | 20 ++++++++++-- egs/wenetspeech/ASR/prepare.sh | 2 +- 16 files changed, 132 insertions(+), 42 deletions(-) diff --git a/egs/aidatatang_200zh/ASR/local/compute_fbank_aidatatang_200zh.py b/egs/aidatatang_200zh/ASR/local/compute_fbank_aidatatang_200zh.py index 387c14acf..9caacb78b 100755 --- a/egs/aidatatang_200zh/ASR/local/compute_fbank_aidatatang_200zh.py +++ b/egs/aidatatang_200zh/ASR/local/compute_fbank_aidatatang_200zh.py @@ -32,7 +32,7 @@ import torch from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter from lhotse.recipes.utils import read_manifests_if_cached -from icefall.utils import get_executor +from icefall.utils import get_executor, str2bool # Torch's multithreaded behavior needs to be disabled or # it wastes a lot of CPU and slow things down. @@ -42,7 +42,7 @@ torch.set_num_threads(1) torch.set_num_interop_threads(1) -def compute_fbank_aidatatang_200zh(num_mel_bins: int = 80): +def compute_fbank_aidatatang_200zh(num_mel_bins: int = 80, perturb_speed: bool = False): src_dir = Path("data/manifests/aidatatang_200zh") output_dir = Path("data/fbank") num_jobs = min(15, os.cpu_count()) @@ -85,7 +85,8 @@ def compute_fbank_aidatatang_200zh(num_mel_bins: int = 80): recordings=m["recordings"], supervisions=m["supervisions"], ) - if "train" in partition: + if "train" in partition and perturb_speed: + logging.info(f"Doing speed perturb") cut_set = ( cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) ) @@ -109,7 +110,12 @@ def get_args(): default=80, help="""The number of mel bins for Fbank""", ) - + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) return parser.parse_args() @@ -119,4 +125,6 @@ if __name__ == "__main__": logging.basicConfig(format=formatter, level=logging.INFO) args = get_args() - compute_fbank_aidatatang_200zh(num_mel_bins=args.num_mel_bins) + compute_fbank_aidatatang_200zh( + num_mel_bins=args.num_mel_bins, perturb_speed=args.perturb_speed + ) diff --git a/egs/aidatatang_200zh/ASR/prepare.sh b/egs/aidatatang_200zh/ASR/prepare.sh index 46ecd5769..2eb0b3718 100755 --- a/egs/aidatatang_200zh/ASR/prepare.sh +++ b/egs/aidatatang_200zh/ASR/prepare.sh @@ -77,7 +77,7 @@ if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then log "Stage 4: Compute fbank for aidatatang_200zh" if [ ! -f data/fbank/.aidatatang_200zh.done ]; then mkdir -p data/fbank - ./local/compute_fbank_aidatatang_200zh.py + ./local/compute_fbank_aidatatang_200zh.py --perturb-speed True touch data/fbank/.aidatatang_200zh.done fi fi diff --git a/egs/aishell/ASR/local/compute_fbank_aidatatang_200zh.py b/egs/aishell/ASR/local/compute_fbank_aidatatang_200zh.py index 037971927..6a9bb4f42 100755 --- a/egs/aishell/ASR/local/compute_fbank_aidatatang_200zh.py +++ b/egs/aishell/ASR/local/compute_fbank_aidatatang_200zh.py @@ -32,7 +32,7 @@ import torch from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter from lhotse.recipes.utils import read_manifests_if_cached -from icefall.utils import get_executor +from icefall.utils import get_executor, str2bool # Torch's multithreaded behavior needs to be disabled or # it wastes a lot of CPU and slow things down. @@ -42,7 +42,7 @@ torch.set_num_threads(1) torch.set_num_interop_threads(1) -def compute_fbank_aidatatang_200zh(num_mel_bins: int = 80): +def compute_fbank_aidatatang_200zh(num_mel_bins: int = 80, perturb_speed: bool = False): src_dir = Path("data/manifests") output_dir = Path("data/fbank") num_jobs = min(15, os.cpu_count()) @@ -85,7 +85,8 @@ def compute_fbank_aidatatang_200zh(num_mel_bins: int = 80): recordings=m["recordings"], supervisions=m["supervisions"], ) - if "train" in partition: + if "train" in partition and perturb_speed: + logging.info(f"Doing speed perturb") cut_set = ( cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) ) @@ -109,7 +110,12 @@ def get_args(): default=80, help="""The number of mel bins for Fbank""", ) - + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) return parser.parse_args() @@ -119,4 +125,6 @@ if __name__ == "__main__": logging.basicConfig(format=formatter, level=logging.INFO) args = get_args() - compute_fbank_aidatatang_200zh(num_mel_bins=args.num_mel_bins) + compute_fbank_aidatatang_200zh( + num_mel_bins=args.num_mel_bins, perturb_speed=args.perturb_speed + ) diff --git a/egs/aishell/ASR/local/compute_fbank_aishell.py b/egs/aishell/ASR/local/compute_fbank_aishell.py index 115ca1031..c7000da1c 100755 --- a/egs/aishell/ASR/local/compute_fbank_aishell.py +++ b/egs/aishell/ASR/local/compute_fbank_aishell.py @@ -32,7 +32,7 @@ import torch from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter from lhotse.recipes.utils import read_manifests_if_cached -from icefall.utils import get_executor +from icefall.utils import get_executor, str2bool # Torch's multithreaded behavior needs to be disabled or # it wastes a lot of CPU and slow things down. @@ -42,7 +42,7 @@ torch.set_num_threads(1) torch.set_num_interop_threads(1) -def compute_fbank_aishell(num_mel_bins: int = 80): +def compute_fbank_aishell(num_mel_bins: int = 80, perturb_speed: bool = False): src_dir = Path("data/manifests") output_dir = Path("data/fbank") num_jobs = min(15, os.cpu_count()) @@ -81,7 +81,8 @@ def compute_fbank_aishell(num_mel_bins: int = 80): recordings=m["recordings"], supervisions=m["supervisions"], ) - if "train" in partition: + if "train" in partition and perturb_speed: + logging.info(f"Doing speed perturb") cut_set = ( cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) ) @@ -104,7 +105,12 @@ def get_args(): default=80, help="""The number of mel bins for Fbank""", ) - + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) return parser.parse_args() @@ -114,4 +120,6 @@ if __name__ == "__main__": logging.basicConfig(format=formatter, level=logging.INFO) args = get_args() - compute_fbank_aishell(num_mel_bins=args.num_mel_bins) + compute_fbank_aishell( + num_mel_bins=args.num_mel_bins, perturb_speed=args.perturb_speed + ) diff --git a/egs/aishell/ASR/prepare.sh b/egs/aishell/ASR/prepare.sh index b763d72c1..ff8e1301d 100755 --- a/egs/aishell/ASR/prepare.sh +++ b/egs/aishell/ASR/prepare.sh @@ -114,7 +114,7 @@ if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then log "Stage 3: Compute fbank for aishell" if [ ! -f data/fbank/.aishell.done ]; then mkdir -p data/fbank - ./local/compute_fbank_aishell.py + ./local/compute_fbank_aishell.py --perturb-speed True touch data/fbank/.aishell.done fi fi diff --git a/egs/aishell/ASR/prepare_aidatatang_200zh.sh b/egs/aishell/ASR/prepare_aidatatang_200zh.sh index f1d4d18a7..ec89450df 100755 --- a/egs/aishell/ASR/prepare_aidatatang_200zh.sh +++ b/egs/aishell/ASR/prepare_aidatatang_200zh.sh @@ -53,7 +53,7 @@ if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then log "Stage 2: Process aidatatang_200zh" if [ ! -f data/fbank/.aidatatang_200zh_fbank.done ]; then mkdir -p data/fbank - ./local/compute_fbank_aidatatang_200zh.py + ./local/compute_fbank_aidatatang_200zh.py --perturb-speed True touch data/fbank/.aidatatang_200zh_fbank.done fi fi diff --git a/egs/aishell2/ASR/local/compute_fbank_aishell2.py b/egs/aishell2/ASR/local/compute_fbank_aishell2.py index ec0c584ca..1fb1621ff 100755 --- a/egs/aishell2/ASR/local/compute_fbank_aishell2.py +++ b/egs/aishell2/ASR/local/compute_fbank_aishell2.py @@ -32,7 +32,7 @@ import torch from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter from lhotse.recipes.utils import read_manifests_if_cached -from icefall.utils import get_executor +from icefall.utils import get_executor, str2bool # Torch's multithreaded behavior needs to be disabled or # it wastes a lot of CPU and slow things down. @@ -42,7 +42,7 @@ torch.set_num_threads(1) torch.set_num_interop_threads(1) -def compute_fbank_aishell2(num_mel_bins: int = 80): +def compute_fbank_aishell2(num_mel_bins: int = 80, perturb_speed: bool = False): src_dir = Path("data/manifests") output_dir = Path("data/fbank") num_jobs = min(15, os.cpu_count()) @@ -81,7 +81,8 @@ def compute_fbank_aishell2(num_mel_bins: int = 80): recordings=m["recordings"], supervisions=m["supervisions"], ) - if "train" in partition: + if "train" in partition and perturb_speed: + logging.info(f"Doing speed perturb") cut_set = ( cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) ) @@ -104,6 +105,12 @@ def get_args(): default=80, help="""The number of mel bins for Fbank""", ) + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) return parser.parse_args() @@ -114,4 +121,6 @@ if __name__ == "__main__": logging.basicConfig(format=formatter, level=logging.INFO) args = get_args() - compute_fbank_aishell2(num_mel_bins=args.num_mel_bins) + compute_fbank_aishell2( + num_mel_bins=args.num_mel_bins, perturb_speed=args.perturb_speed + ) diff --git a/egs/aishell2/ASR/prepare.sh b/egs/aishell2/ASR/prepare.sh index 3e8e840ab..42631c864 100755 --- a/egs/aishell2/ASR/prepare.sh +++ b/egs/aishell2/ASR/prepare.sh @@ -101,7 +101,7 @@ if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then log "Stage 3: Compute fbank for aishell2" if [ ! -f data/fbank/.aishell2.done ]; then mkdir -p data/fbank - ./local/compute_fbank_aishell2.py + ./local/compute_fbank_aishell2.py --perturb-speed True touch data/fbank/.aishell2.done fi fi diff --git a/egs/aishell4/ASR/local/compute_fbank_aishell4.py b/egs/aishell4/ASR/local/compute_fbank_aishell4.py index 400c406f0..f19163988 100755 --- a/egs/aishell4/ASR/local/compute_fbank_aishell4.py +++ b/egs/aishell4/ASR/local/compute_fbank_aishell4.py @@ -32,7 +32,7 @@ import torch from lhotse import ChunkedLilcomHdf5Writer, CutSet, Fbank, FbankConfig from lhotse.recipes.utils import read_manifests_if_cached -from icefall.utils import get_executor +from icefall.utils import get_executor, str2bool # Torch's multithreaded behavior needs to be disabled or # it wastes a lot of CPU and slow things down. @@ -42,7 +42,7 @@ torch.set_num_threads(1) torch.set_num_interop_threads(1) -def compute_fbank_aishell4(num_mel_bins: int = 80): +def compute_fbank_aishell4(num_mel_bins: int = 80, perturb_speed: bool = False): src_dir = Path("data/manifests/aishell4") output_dir = Path("data/fbank") num_jobs = min(15, os.cpu_count()) @@ -83,10 +83,12 @@ def compute_fbank_aishell4(num_mel_bins: int = 80): recordings=m["recordings"], supervisions=m["supervisions"], ) - if "train" in partition: + if "train" in partition and perturb_speed: + logging.info(f"Doing speed perturb") cut_set = ( cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) ) + cut_set = cut_set.compute_and_store_features( extractor=extractor, storage_path=f"{output_dir}/{prefix}_feats_{partition}", @@ -113,6 +115,12 @@ def get_args(): default=80, help="""The number of mel bins for Fbank""", ) + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) return parser.parse_args() @@ -123,4 +131,6 @@ if __name__ == "__main__": logging.basicConfig(format=formatter, level=logging.INFO) args = get_args() - compute_fbank_aishell4(num_mel_bins=args.num_mel_bins) + compute_fbank_aishell4( + num_mel_bins=args.num_mel_bins, perturb_speed=args.perturb_speed + ) diff --git a/egs/aishell4/ASR/prepare.sh b/egs/aishell4/ASR/prepare.sh index cb2b73a3e..1b1ec0005 100755 --- a/egs/aishell4/ASR/prepare.sh +++ b/egs/aishell4/ASR/prepare.sh @@ -107,7 +107,7 @@ if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then log "Stage 5: Compute fbank for aishell4" if [ ! -f data/fbank/.aishell4.done ]; then mkdir -p data/fbank - ./local/compute_fbank_aishell4.py + ./local/compute_fbank_aishell4.py --perturb-speed True touch data/fbank/.aishell4.done fi fi diff --git a/egs/alimeeting/ASR/local/compute_fbank_alimeeting.py b/egs/alimeeting/ASR/local/compute_fbank_alimeeting.py index 96115a230..f8c10648a 100755 --- a/egs/alimeeting/ASR/local/compute_fbank_alimeeting.py +++ b/egs/alimeeting/ASR/local/compute_fbank_alimeeting.py @@ -32,7 +32,7 @@ import torch from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter from lhotse.recipes.utils import read_manifests_if_cached -from icefall.utils import get_executor +from icefall.utils import get_executor, str2bool # Torch's multithreaded behavior needs to be disabled or # it wastes a lot of CPU and slow things down. @@ -42,7 +42,7 @@ torch.set_num_threads(1) torch.set_num_interop_threads(1) -def compute_fbank_alimeeting(num_mel_bins: int = 80): +def compute_fbank_alimeeting(num_mel_bins: int = 80, perturb_speed: bool = False): src_dir = Path("data/manifests/alimeeting") output_dir = Path("data/fbank") num_jobs = min(15, os.cpu_count()) @@ -82,7 +82,8 @@ def compute_fbank_alimeeting(num_mel_bins: int = 80): recordings=m["recordings"], supervisions=m["supervisions"], ) - if "train" in partition: + if "train" in partition and perturb_speed: + logging.info(f"Doing speed perturb") cut_set = ( cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) ) @@ -114,6 +115,12 @@ def get_args(): default=80, help="""The number of mel bins for Fbank""", ) + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) return parser.parse_args() @@ -124,4 +131,6 @@ if __name__ == "__main__": logging.basicConfig(format=formatter, level=logging.INFO) args = get_args() - compute_fbank_alimeeting(num_mel_bins=args.num_mel_bins) + compute_fbank_alimeeting( + num_mel_bins=args.num_mel_bins, perturb_speed=args.perturb_speed + ) diff --git a/egs/alimeeting/ASR/prepare.sh b/egs/alimeeting/ASR/prepare.sh index 604cc92c6..1709733c7 100755 --- a/egs/alimeeting/ASR/prepare.sh +++ b/egs/alimeeting/ASR/prepare.sh @@ -97,7 +97,7 @@ if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then log "Stage 5: Compute fbank for alimeeting" if [ ! -f data/fbank/.alimeeting.done ]; then mkdir -p data/fbank - ./local/compute_fbank_alimeeting.py + ./local/compute_fbank_alimeeting.py --perturb-speed True touch data/fbank/.alimeeting.done fi fi diff --git a/egs/alimeeting/ASR_v2/local/compute_fbank_alimeeting.py b/egs/alimeeting/ASR_v2/local/compute_fbank_alimeeting.py index c6aa2ab36..833d11c72 100755 --- a/egs/alimeeting/ASR_v2/local/compute_fbank_alimeeting.py +++ b/egs/alimeeting/ASR_v2/local/compute_fbank_alimeeting.py @@ -25,6 +25,7 @@ It looks for manifests in the directory data/manifests. The generated fbank features are saved in data/fbank. """ +import argparse import logging from pathlib import Path @@ -39,6 +40,8 @@ from lhotse.features.kaldifeat import ( ) from lhotse.recipes.utils import read_manifests_if_cached +from icefall.utils import str2bool + # Torch's multithreaded behavior needs to be disabled or # it wastes a lot of CPU and slow things down. # Do this outside of main() in case it needs to take effect @@ -48,7 +51,7 @@ torch.set_num_interop_threads(1) torch.multiprocessing.set_sharing_strategy("file_system") -def compute_fbank_ami(): +def compute_fbank_ami(perturb_speed: bool = False): src_dir = Path("data/manifests") output_dir = Path("data/fbank") @@ -84,8 +87,12 @@ def compute_fbank_ami(): suffix="jsonl.gz", ) - def _extract_feats(cuts: CutSet, storage_path: Path, manifest_path: Path) -> None: - cuts = cuts + cuts.perturb_speed(0.9) + cuts.perturb_speed(1.1) + def _extract_feats( + cuts: CutSet, storage_path: Path, manifest_path: Path, speed_perturb: bool + ) -> None: + if speed_perturb: + logging.info(f"Doing speed perturb") + cuts = cuts + cuts.perturb_speed(0.9) + cuts.perturb_speed(1.1) _ = cuts.compute_and_store_features_batch( extractor=extractor, storage_path=storage_path, @@ -109,6 +116,7 @@ def compute_fbank_ami(): cuts_ihm, output_dir / "feats_train_ihm", src_dir / "cuts_train_ihm.jsonl.gz", + perturb_speed, ) logging.info("Processing train split IHM + reverberated IHM") @@ -117,6 +125,7 @@ def compute_fbank_ami(): cuts_ihm_rvb, output_dir / "feats_train_ihm_rvb", src_dir / "cuts_train_ihm_rvb.jsonl.gz", + perturb_speed, ) logging.info("Processing train split SDM") @@ -129,6 +138,7 @@ def compute_fbank_ami(): cuts_sdm, output_dir / "feats_train_sdm", src_dir / "cuts_train_sdm.jsonl.gz", + perturb_speed, ) logging.info("Processing train split GSS") @@ -141,6 +151,7 @@ def compute_fbank_ami(): cuts_gss, output_dir / "feats_train_gss", src_dir / "cuts_train_gss.jsonl.gz", + perturb_speed, ) logging.info("Preparing test cuts: IHM, SDM, GSS (optional)") @@ -186,8 +197,21 @@ def compute_fbank_ami(): ) +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) + return parser.parse_args() + + if __name__ == "__main__": formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" logging.basicConfig(format=formatter, level=logging.INFO) - compute_fbank_ami() + args = get_args() + + compute_fbank_ami(perturb_speed=args.perturb_speed) diff --git a/egs/alimeeting/ASR_v2/prepare.sh b/egs/alimeeting/ASR_v2/prepare.sh index 76a108771..1098840f8 100755 --- a/egs/alimeeting/ASR_v2/prepare.sh +++ b/egs/alimeeting/ASR_v2/prepare.sh @@ -85,7 +85,7 @@ fi if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then log "Stage 5: Compute fbank for alimeeting" mkdir -p data/fbank - python local/compute_fbank_alimeeting.py + python local/compute_fbank_alimeeting.py --perturb-speed True log "Combine features from train splits" lhotse combine data/manifests/cuts_train_{ihm,ihm_rvb,sdm,gss}.jsonl.gz - | shuf |\ gzip -c > data/manifests/cuts_train_all.jsonl.gz diff --git a/egs/wenetspeech/ASR/local/preprocess_wenetspeech.py b/egs/wenetspeech/ASR/local/preprocess_wenetspeech.py index 93ce750f8..5de3c23a9 100755 --- a/egs/wenetspeech/ASR/local/preprocess_wenetspeech.py +++ b/egs/wenetspeech/ASR/local/preprocess_wenetspeech.py @@ -16,6 +16,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import argparse import logging import re from pathlib import Path @@ -24,6 +25,7 @@ from lhotse import CutSet, SupervisionSegment from lhotse.recipes.utils import read_manifests_if_cached from icefall import setup_logger +from icefall.utils import str2bool # Similar text filtering and normalization procedure as in: # https://github.com/SpeechColab/WenetSpeech/blob/main/toolkits/kaldi/wenetspeech_data_prep.sh @@ -45,7 +47,7 @@ def has_no_oov( return oov_pattern.search(sup.text) is None -def preprocess_wenet_speech(): +def preprocess_wenet_speech(perturb_speed: bool = False): src_dir = Path("data/manifests") output_dir = Path("data/fbank") output_dir.mkdir(exist_ok=True) @@ -110,7 +112,7 @@ def preprocess_wenet_speech(): ) # Run data augmentation that needs to be done in the # time domain. - if partition not in ["DEV", "TEST_NET", "TEST_MEETING"]: + if partition not in ["DEV", "TEST_NET", "TEST_MEETING"] and perturb_speed: logging.info( f"Speed perturb for {partition} with factors 0.9 and 1.1 " "(Perturbing may take 8 minutes and saving may take 20 minutes)" @@ -120,10 +122,22 @@ def preprocess_wenet_speech(): cut_set.to_file(raw_cuts_path) +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) + return parser.parse_args() + + def main(): setup_logger(log_filename="./log-preprocess-wenetspeech") - preprocess_wenet_speech() + args = get_args() + preprocess_wenet_speech(perturb_speed=args.perturb_speed) logging.info("Done") diff --git a/egs/wenetspeech/ASR/prepare.sh b/egs/wenetspeech/ASR/prepare.sh index f7b521794..097a59a5f 100755 --- a/egs/wenetspeech/ASR/prepare.sh +++ b/egs/wenetspeech/ASR/prepare.sh @@ -91,7 +91,7 @@ fi if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then log "Stage 3: Preprocess WenetSpeech manifest" if [ ! -f data/fbank/.preprocess_complete ]; then - python3 ./local/preprocess_wenetspeech.py + python3 ./local/preprocess_wenetspeech.py --perturb-speed True touch data/fbank/.preprocess_complete fi fi From d6b28a11a70871a76b66ccf80667dd1d3ac1ab17 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 11 Aug 2023 23:57:00 +0800 Subject: [PATCH 066/100] Add export script for the yesno recipe. (#1212) --- .github/workflows/run-yesno-recipe.yml | 76 +++++++- egs/yesno/ASR/tdnn/decode.py | 1 - egs/yesno/ASR/tdnn/export.py | 118 ++++++++++++ egs/yesno/ASR/tdnn/export_onnx.py | 158 ++++++++++++++++ egs/yesno/ASR/tdnn/jit_pretrained.py | 199 ++++++++++++++++++++ egs/yesno/ASR/tdnn/onnx_pretrained.py | 241 +++++++++++++++++++++++++ egs/yesno/ASR/tdnn/pretrained.py | 37 +++- 7 files changed, 813 insertions(+), 17 deletions(-) create mode 100755 egs/yesno/ASR/tdnn/export.py create mode 100755 egs/yesno/ASR/tdnn/export_onnx.py create mode 100755 egs/yesno/ASR/tdnn/jit_pretrained.py create mode 100755 egs/yesno/ASR/tdnn/onnx_pretrained.py diff --git a/.github/workflows/run-yesno-recipe.yml b/.github/workflows/run-yesno-recipe.yml index 8a2c94829..57f15fe87 100644 --- a/.github/workflows/run-yesno-recipe.yml +++ b/.github/workflows/run-yesno-recipe.yml @@ -44,11 +44,6 @@ jobs: with: fetch-depth: 0 - - name: Install graphviz - shell: bash - run: | - sudo apt-get -qq install graphviz - - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -70,6 +65,7 @@ jobs: pip install --no-binary protobuf protobuf==3.20.* pip install --no-deps --force-reinstall https://huggingface.co/csukuangfj/k2/resolve/main/cpu/k2-1.24.3.dev20230508+cpu.torch1.13.1-cp38-cp38-linux_x86_64.whl + pip install kaldifeat==1.25.0.dev20230726+cpu.torch1.13.1 -f https://csukuangfj.github.io/kaldifeat/cpu.html - name: Run yesno recipe shell: bash @@ -78,9 +74,75 @@ jobs: export PYTHONPATH=$PWD:$PYTHONPATH echo $PYTHONPATH - cd egs/yesno/ASR ./prepare.sh python3 ./tdnn/train.py python3 ./tdnn/decode.py - # TODO: Check that the WER is less than some value + + - name: Test exporting to pretrained.pt + shell: bash + working-directory: ${{github.workspace}} + run: | + export PYTHONPATH=$PWD:$PYTHONPATH + echo $PYTHONPATH + + cd egs/yesno/ASR + python3 ./tdnn/export.py --epoch 14 --avg 2 + + python3 ./tdnn/pretrained.py \ + --checkpoint ./tdnn/exp/pretrained.pt \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + + - name: Test exporting to torchscript + shell: bash + working-directory: ${{github.workspace}} + run: | + export PYTHONPATH=$PWD:$PYTHONPATH + echo $PYTHONPATH + + cd egs/yesno/ASR + python3 ./tdnn/export.py --epoch 14 --avg 2 --jit 1 + + python3 ./tdnn/jit_pretrained.py \ + --nn-model ./tdnn/exp/cpu_jit.pt \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + + - name: Test exporting to onnx + shell: bash + working-directory: ${{github.workspace}} + run: | + export PYTHONPATH=$PWD:$PYTHONPATH + echo $PYTHONPATH + + cd egs/yesno/ASR + python3 ./tdnn/export_onnx.py --epoch 14 --avg 2 + + echo "Test float32 model" + python3 ./tdnn/onnx_pretrained.py \ + --nn-model ./tdnn/exp/model-epoch-14-avg-2.onnx \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + + + echo "Test int8 model" + python3 ./tdnn/onnx_pretrained.py \ + --nn-model ./tdnn/exp/model-epoch-14-avg-2.int8.onnx \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + + - name: Show generated files + shell: bash + working-directory: ${{github.workspace}} + run: | + cd egs/yesno/ASR + ls -lh tdnn/exp diff --git a/egs/yesno/ASR/tdnn/decode.py b/egs/yesno/ASR/tdnn/decode.py index d5efb41df..f520607af 100755 --- a/egs/yesno/ASR/tdnn/decode.py +++ b/egs/yesno/ASR/tdnn/decode.py @@ -65,7 +65,6 @@ def get_params() -> AttributeDict: { "exp_dir": Path("tdnn/exp/"), "lang_dir": Path("data/lang_phone"), - "lm_dir": Path("data/lm"), "feature_dim": 23, "search_beam": 20, "output_beam": 8, diff --git a/egs/yesno/ASR/tdnn/export.py b/egs/yesno/ASR/tdnn/export.py new file mode 100755 index 000000000..c40cf8cd1 --- /dev/null +++ b/egs/yesno/ASR/tdnn/export.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 + +""" +This file is for exporting trained models to a checkpoint +or to a torchscript model. + +(1) Generate the checkpoint tdnn/exp/pretrained.pt + +./tdnn/export.py \ + --epoch 14 \ + --avg 2 + +See ./tdnn/pretrained.py for how to use the generated file. + +(2) Generate torchscript model tdnn/exp/cpu_jit.pt + +./tdnn/export.py \ + --epoch 14 \ + --avg 2 \ + --jit 1 + +See ./tdnn/jit_pretrained.py for how to use the generated file. +""" + +import argparse +import logging + +import torch +from model import Tdnn +from train import get_params + +from icefall.checkpoint import average_checkpoints, load_checkpoint +from icefall.lexicon import Lexicon +from icefall.utils import str2bool + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=14, + help="It specifies the checkpoint to use for decoding." + "Note: Epoch counts from 0.", + ) + + parser.add_argument( + "--avg", + type=int, + default=2, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch'. ", + ) + + parser.add_argument( + "--jit", + type=str2bool, + default=False, + help="""True to save a model after applying torch.jit.script. + """, + ) + return parser + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + + params = get_params() + params.update(vars(args)) + + logging.info(params) + + lexicon = Lexicon(params.lang_dir) + max_token_id = max(lexicon.tokens) + + model = Tdnn( + num_features=params.feature_dim, + num_classes=max_token_id + 1, # +1 for the blank symbol + ) + if params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if start >= 0: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.load_state_dict(average_checkpoints(filenames)) + + model.to("cpu") + model.eval() + + if params.jit: + logging.info("Using torch.jit.script") + model = torch.jit.script(model) + filename = params.exp_dir / "cpu_jit.pt" + model.save(str(filename)) + logging.info(f"Saved to {filename}") + else: + logging.info("Not using torch.jit.script") + # Save it using a format so that it can be loaded + # by :func:`load_checkpoint` + filename = params.exp_dir / "pretrained.pt" + torch.save({"model": model.state_dict()}, str(filename)) + logging.info(f"Saved to {filename}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/yesno/ASR/tdnn/export_onnx.py b/egs/yesno/ASR/tdnn/export_onnx.py new file mode 100755 index 000000000..9b2a56d59 --- /dev/null +++ b/egs/yesno/ASR/tdnn/export_onnx.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +""" +This file is for exporting trained models to onnx. + +Usage: + + ./tdnn/export_onnx.py \ + --epoch 14 \ + --avg 2 + +The above command generates the following two files: + - ./exp/model-epoch-14-avg-2.onnx + - ./exp/model-epoch-14-avg-2.int8.onnx + +See ./tdnn/onnx_pretrained.py for how to use them. +""" + +import argparse +import logging +from typing import Dict + +import onnx +import torch +from model import Tdnn +from onnxruntime.quantization import QuantType, quantize_dynamic +from train import get_params + +from icefall.checkpoint import average_checkpoints, load_checkpoint +from icefall.lexicon import Lexicon + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=14, + help="It specifies the checkpoint to use for decoding." + "Note: Epoch counts from 0.", + ) + + parser.add_argument( + "--avg", + type=int, + default=2, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch'. ", + ) + + return parser + + +def add_meta_data(filename: str, meta_data: Dict[str, str]): + """Add meta data to an ONNX model. It is changed in-place. + + Args: + filename: + Filename of the ONNX model to be changed. + meta_data: + Key-value pairs. + """ + model = onnx.load(filename) + for key, value in meta_data.items(): + meta = model.metadata_props.add() + meta.key = key + meta.value = str(value) + + onnx.save(model, filename) + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + + params = get_params() + params.update(vars(args)) + + logging.info(params) + + lexicon = Lexicon(params.lang_dir) + max_token_id = max(lexicon.tokens) + + model = Tdnn( + num_features=params.feature_dim, + num_classes=max_token_id + 1, # +1 for the blank symbol + ) + if params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if start >= 0: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.load_state_dict(average_checkpoints(filenames)) + + model.to("cpu") + model.eval() + + N = 1 + T = 100 + C = params.feature_dim + x = torch.rand(N, T, C) + + opset_version = 13 + onnx_filename = f"{params.exp_dir}/model-epoch-{params.epoch}-avg-{params.avg}.onnx" + torch.onnx.export( + model, + x, + onnx_filename, + verbose=False, + opset_version=opset_version, + input_names=["x"], + output_names=["log_prob"], + dynamic_axes={ + "x": {0: "N", 1: "T"}, + "log_prob": {0: "N", 1: "T"}, + }, + ) + + logging.info(f"Saved to {onnx_filename}") + meta_data = { + "model_type": "tdnn_lstm", + "version": "1", + "model_author": "k2-fsa", + "comment": "non-streaming tdnn for the yesno recipe", + "vocab_size": max_token_id + 1, + } + + logging.info(f"meta_data: {meta_data}") + + add_meta_data(filename=onnx_filename, meta_data=meta_data) + + logging.info("Generate int8 quantization models") + onnx_filename_int8 = ( + f"{params.exp_dir}/model-epoch-{params.epoch}-avg-{params.avg}.int8.onnx" + ) + + quantize_dynamic( + model_input=onnx_filename, + model_output=onnx_filename_int8, + op_types_to_quantize=["MatMul"], + weight_type=QuantType.QInt8, + ) + logging.info(f"Saved to {onnx_filename_int8}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/yesno/ASR/tdnn/jit_pretrained.py b/egs/yesno/ASR/tdnn/jit_pretrained.py new file mode 100755 index 000000000..84390fca5 --- /dev/null +++ b/egs/yesno/ASR/tdnn/jit_pretrained.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 + +""" +This file shows how to use a torchscript model for decoding. + +Usage: + + ./tdnn/jit_pretrained.py \ + --nn-model ./tdnn/exp/cpu_jit.pt \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +Note that to generate ./tdnn/exp/cpu_jit.pt, +you can use ./export.py --jit 1 +""" + +import argparse +import logging +from typing import List +import math + + +import k2 +import kaldifeat +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence + +from icefall.decode import get_lattice, one_best_decoding +from icefall.utils import AttributeDict, get_texts + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model", + type=str, + required=True, + help="""Path to the torchscript model. + You can use ./tdnn/export.py --jit 1 + to obtain it + """, + ) + + parser.add_argument( + "--words-file", + type=str, + required=True, + help="Path to words.txt", + ) + + parser.add_argument("--HLG", type=str, required=True, help="Path to HLG.pt.") + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. ", + ) + + return parser + + +def get_params() -> AttributeDict: + params = AttributeDict( + { + "feature_dim": 23, + "num_classes": 4, # [, N, SIL, Y] + "sample_rate": 8000, + "search_beam": 20, + "output_beam": 8, + "min_active_states": 30, + "max_active_states": 10000, + "use_double_scores": True, + } + ) + return params + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + params.update(vars(args)) + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + logging.info("Loading torchscript model") + model = torch.jit.load(args.nn_model) + model.eval() + model.to(device) + + logging.info(f"Loading HLG from {params.HLG}") + HLG = k2.Fsa.from_dict(torch.load(params.HLG, map_location="cpu")) + HLG = HLG.to(device) + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + # Note: We don't use key padding mask for attention during decoding + nnet_output = model(features) + + batch_size = nnet_output.shape[0] + supervision_segments = torch.tensor( + [[i, 0, nnet_output.shape[1]] for i in range(batch_size)], + dtype=torch.int32, + ) + + lattice = get_lattice( + nnet_output=nnet_output, + decoding_graph=HLG, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + ) + + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + + hyps = get_texts(best_path) + word_sym_table = k2.SymbolTable.from_file(params.words_file) + hyps = [[word_sym_table[i] for i in ids] for ids in hyps] + + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/yesno/ASR/tdnn/onnx_pretrained.py b/egs/yesno/ASR/tdnn/onnx_pretrained.py new file mode 100755 index 000000000..626473b6e --- /dev/null +++ b/egs/yesno/ASR/tdnn/onnx_pretrained.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 + +""" +This file shows how to use an ONNX model for decoding with onnxruntime. + +Usage: + +(1) Use a not quantized ONNX model, i.e., a float32 model + ./tdnn/onnx_pretrained.py \ + --nn-model ./tdnn/exp/model-epoch-14-avg-2.onnx \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +(2) Use a quantized ONNX model, i.e., an int8 model + + ./tdnn/onnx_pretrained.py \ + --nn-model ./tdnn/exp/model-epoch-14-avg-2.int8.onnx \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +Note that to generate ./tdnn/exp/model-epoch-14-avg-2.onnx, +and ./tdnn/exp/model-epoch-14-avg-2.onnx, +you can use ./export_onnx.py --epoch 14 --avg 2 +""" + +import argparse +import logging +import math +from typing import List + +import k2 +import kaldifeat +import onnxruntime as ort +import torch +import torchaudio +from torch.nn.utils.rnn import pad_sequence + +from icefall.decode import get_lattice, one_best_decoding +from icefall.utils import AttributeDict, get_texts + + +class OnnxModel: + def __init__(self, nn_model: str): + session_opts = ort.SessionOptions() + session_opts.inter_op_num_threads = 1 + session_opts.intra_op_num_threads = 1 + + self.session_opts = session_opts + self.model = ort.InferenceSession( + nn_model, + sess_options=self.session_opts, + ) + + meta = self.model.get_modelmeta().custom_metadata_map + self.vocab_size = int(meta["vocab_size"]) + + def run( + self, + x: torch.Tensor, + ) -> torch.Tensor: + """ + Args: + x: + A 3-D tensor of shape (N, T, C) + Returns: + Return a 3-D tensor log_prob of shape (N, T, C) + """ + out = self.model.run( + [ + self.model.get_outputs()[0].name, + ], + { + self.model.get_inputs()[0].name: x.numpy(), + }, + ) + return torch.from_numpy(out[0]) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model", + type=str, + required=True, + help="""Path to the torchscript model. + You can use ./tdnn/export.py --jit 1 + to obtain it + """, + ) + + parser.add_argument( + "--words-file", + type=str, + required=True, + help="Path to words.txt", + ) + + parser.add_argument("--HLG", type=str, required=True, help="Path to HLG.pt.") + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. ", + ) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def get_params() -> AttributeDict: + params = AttributeDict( + { + "feature_dim": 23, + "sample_rate": 8000, + "search_beam": 20, + "output_beam": 8, + "min_active_states": 30, + "max_active_states": 10000, + "use_double_scores": True, + } + ) + return params + + +def main(): + parser = get_parser() + args = parser.parse_args() + params = get_params() + params.update(vars(args)) + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + logging.info(f"device: {device}") + + logging.info(f"Loading onnx model {params.nn_model}") + model = OnnxModel(params.nn_model) + + logging.info(f"Loading HLG from {args.HLG}") + HLG = k2.Fsa.from_dict(torch.load(params.HLG, map_location="cpu")) + HLG = HLG.to(device) + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + # Note: We don't use key padding mask for attention during decoding + nnet_output = model.run(features) + + batch_size = nnet_output.shape[0] + supervision_segments = torch.tensor( + [[i, 0, nnet_output.shape[1]] for i in range(batch_size)], + dtype=torch.int32, + ) + + lattice = get_lattice( + nnet_output=nnet_output, + decoding_graph=HLG, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + ) + + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + + hyps = get_texts(best_path) + word_sym_table = k2.SymbolTable.from_file(params.words_file) + hyps = [[word_sym_table[i] for i in ids] for ids in hyps] + + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/yesno/ASR/tdnn/pretrained.py b/egs/yesno/ASR/tdnn/pretrained.py index 65be77db1..987c49de6 100755 --- a/egs/yesno/ASR/tdnn/pretrained.py +++ b/egs/yesno/ASR/tdnn/pretrained.py @@ -15,6 +15,21 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +This file shows how to use a checkpoint for decoding. + +Usage: + + ./tdnn/pretrained.py \ + --checkpoint ./tdnn/exp/pretrained.pt \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +Note that to generate ./tdnn/exp/pretrained.pt, +you can use ./export.py +""" import argparse import logging @@ -43,7 +58,8 @@ def get_parser(): required=True, help="Path to the checkpoint. " "The checkpoint is assumed to be saved by " - "icefall.checkpoint.save_checkpoint().", + "icefall.checkpoint.save_checkpoint(). " + "You can use ./tdnn/export.py to obtain it.", ) parser.add_argument( @@ -61,8 +77,7 @@ def get_parser(): nargs="+", help="The input sound file(s) to transcribe. " "Supported formats are those supported by torchaudio.load(). " - "For example, wav and flac are supported. " - "The sample rate has to be 16kHz.", + "For example, wav and flac are supported. ", ) return parser @@ -99,14 +114,19 @@ def read_sound_files( ans = [] for f in filenames: wave, sample_rate = torchaudio.load(f) - assert ( - sample_rate == expected_sample_rate - ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + # We use only the first channel - ans.append(wave[0]) + ans.append(wave[0].contiguous()) return ans +@torch.no_grad() def main(): parser = get_parser() args = parser.parse_args() @@ -159,8 +179,7 @@ def main(): features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) # Note: We don't use key padding mask for attention during decoding - with torch.no_grad(): - nnet_output = model(features) + nnet_output = model(features) batch_size = nnet_output.shape[0] supervision_segments = torch.tensor( From a81396b482c799b2ace2cefb79859be827b16f00 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Sat, 12 Aug 2023 16:53:59 +0800 Subject: [PATCH 067/100] Use tokens.txt to replace bpe.model (#1162) --- ...n-librispeech-conformer-ctc3-2022-11-28.sh | 10 +- ...h-lstm-transducer-stateless2-2022-09-03.sh | 6 +- ...-pruned-transducer-stateless-2022-03-12.sh | 4 +- ...pruned-transducer-stateless2-2022-04-29.sh | 4 +- ...pruned-transducer-stateless3-2022-04-29.sh | 4 +- ...pruned-transducer-stateless3-2022-05-13.sh | 8 +- ...pruned-transducer-stateless5-2022-05-13.sh | 4 +- ...pruned-transducer-stateless7-2022-11-11.sh | 6 +- ...ed-transducer-stateless7-ctc-2022-12-01.sh | 6 +- ...transducer-stateless7-ctc-bs-2023-01-29.sh | 6 +- ...nsducer-stateless7-streaming-2022-12-29.sh | 6 +- ...pruned-transducer-stateless8-2022-11-14.sh | 6 +- ...pruned-transducer-stateless2-2022-06-26.sh | 4 +- ...speech-transducer-stateless2-2022-04-19.sh | 4 +- ...un-librispeech-zipformer-mmi-2022-12-08.sh | 4 +- .../scripts/run-pre-trained-conformer-ctc.sh | 4 +- ...d-transducer-stateless-librispeech-100h.sh | 4 +- ...d-transducer-stateless-librispeech-960h.sh | 4 +- .../run-pre-trained-transducer-stateless.sh | 4 +- .github/scripts/run-pre-trained-transducer.sh | 2 +- ...enetspeech-pruned-transducer-stateless2.sh | 36 +- .github/scripts/test-ncnn-export.sh | 12 +- .github/scripts/test-onnx-export.sh | 138 ++++++- .../pruned_transducer_stateless7/export.py | 322 +--------------- .../pretrained.py | 349 +----------------- egs/librispeech/ASR/conformer_ctc/export.py | 18 +- .../ASR/conformer_ctc/pretrained.py | 40 +- egs/librispeech/ASR/conformer_ctc2/export.py | 19 +- egs/librispeech/ASR/conformer_ctc3/export.py | 23 +- .../ASR/conformer_ctc3/pretrained.py | 42 ++- .../export.py | 22 +- .../export-for-ncnn.py | 22 +- .../export-onnx.py | 25 +- .../export.py | 22 +- .../onnx_pretrained.py | 2 +- .../ASR/lstm_transducer_stateless/export.py | 25 +- .../lstm_transducer_stateless/pretrained.py | 49 +-- .../export-for-ncnn.py | 23 +- .../export-onnx-zh.py | 2 +- .../lstm_transducer_stateless2/export-onnx.py | 25 +- .../ASR/lstm_transducer_stateless2/export.py | 25 +- .../lstm_transducer_stateless2/pretrained.py | 49 +-- .../ASR/lstm_transducer_stateless3/export.py | 25 +- .../lstm_transducer_stateless3/pretrained.py | 46 ++- .../pruned_stateless_emformer_rnnt2/export.py | 23 +- .../export-onnx.py | 2 +- .../ASR/pruned_transducer_stateless/export.py | 24 +- .../pruned_transducer_stateless/pretrained.py | 49 +-- .../pruned_transducer_stateless2/export.py | 22 +- .../pretrained.py | 49 +-- .../export-onnx.py | 24 +- .../pruned_transducer_stateless3/export.py | 26 +- .../pretrained.py | 51 +-- .../pruned_transducer_stateless4/export.py | 22 +- .../export-onnx-streaming.py | 26 +- .../export-onnx.py | 26 +- .../pruned_transducer_stateless5/export.py | 22 +- .../pretrained.py | 49 +-- .../pruned_transducer_stateless6/export.py | 22 +- .../export-onnx.py | 27 +- .../pruned_transducer_stateless7/export.py | 30 +- .../pretrained.py | 55 +-- .../export.py | 24 +- .../pretrained.py | 51 +-- .../pretrained_ctc.py | 10 +- .../export.py | 24 +- .../export_onnx.py | 26 +- .../pretrained.py | 51 +-- .../pretrained_ctc.py | 10 +- .../export-for-ncnn-zh.py | 21 +- .../export-for-ncnn.py | 22 +- .../export-onnx-zh.py | 25 +- .../export-onnx.py | 24 +- .../export.py | 20 +- .../pretrained.py | 51 +-- .../export-for-ncnn.py | 22 +- .../pruned_transducer_stateless8/export.py | 24 +- .../pretrained.py | 51 +-- egs/librispeech/ASR/transducer/export.py | 22 +- egs/librispeech/ASR/transducer/pretrained.py | 33 +- .../ASR/transducer_stateless/export.py | 22 +- .../ASR/transducer_stateless/pretrained.py | 36 +- .../ASR/transducer_stateless2/export.py | 22 +- .../ASR/transducer_stateless2/pretrained.py | 36 +- .../export.py | 22 +- .../pretrained.py | 36 +- .../ASR/zipformer/export-onnx-streaming.py | 4 +- egs/librispeech/ASR/zipformer/export-onnx.py | 4 +- egs/librispeech/ASR/zipformer/export.py | 25 +- .../ASR/zipformer/jit_pretrained_ctc.py | 18 +- egs/librispeech/ASR/zipformer/onnx_check.py | 1 - .../zipformer/onnx_pretrained-streaming.py | 3 +- .../ASR/zipformer/onnx_pretrained.py | 1 - .../ASR/zipformer/pretrained_ctc.py | 20 +- egs/librispeech/ASR/zipformer_mmi/export.py | 24 +- .../ASR/zipformer_mmi/pretrained.py | 47 +-- .../export-onnx.py | 2 +- .../pretrained.py | 2 +- icefall/utils.py | 20 + 99 files changed, 1243 insertions(+), 1623 deletions(-) mode change 100755 => 120000 egs/aishell/ASR/pruned_transducer_stateless7/export.py mode change 100644 => 120000 egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py diff --git a/.github/scripts/run-librispeech-conformer-ctc3-2022-11-28.sh b/.github/scripts/run-librispeech-conformer-ctc3-2022-11-28.sh index c68ccc954..f6fe8c9b2 100755 --- a/.github/scripts/run-librispeech-conformer-ctc3-2022-11-28.sh +++ b/.github/scripts/run-librispeech-conformer-ctc3-2022-11-28.sh @@ -38,7 +38,7 @@ log "Decode with models exported by torch.jit.trace()" for m in ctc-decoding 1best; do ./conformer_ctc3/jit_pretrained.py \ --model-filename $repo/exp/jit_trace.pt \ - --words-file $repo/data/lang_bpe_500/words.txt \ + --words-file $repo/data/lang_bpe_500/words.txt \ --HLG $repo/data/lang_bpe_500/HLG.pt \ --bpe-model $repo/data/lang_bpe_500/bpe.model \ --G $repo/data/lm/G_4_gram.pt \ @@ -53,7 +53,7 @@ log "Export to torchscript model" ./conformer_ctc3/export.py \ --exp-dir $repo/exp \ - --lang-dir $repo/data/lang_bpe_500 \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --jit-trace 1 \ --epoch 99 \ --avg 1 \ @@ -80,9 +80,9 @@ done for m in ctc-decoding 1best; do ./conformer_ctc3/pretrained.py \ --checkpoint $repo/exp/pretrained.pt \ - --words-file $repo/data/lang_bpe_500/words.txt \ + --words-file $repo/data/lang_bpe_500/words.txt \ --HLG $repo/data/lang_bpe_500/HLG.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --G $repo/data/lm/G_4_gram.pt \ --method $m \ --sample-rate 16000 \ @@ -93,7 +93,7 @@ done echo "GITHUB_EVENT_NAME: ${GITHUB_EVENT_NAME}" echo "GITHUB_EVENT_LABEL_NAME: ${GITHUB_EVENT_LABEL_NAME}" -if [[ x"${GITHUB_EVENT_NAME}" == x"schedule" || x"${GITHUB_EVENT_LABEL_NAME}" == x"run-decode" ]]; then +if [[ x"${GITHUB_EVENT_NAME}" == x"schedule" || x"${GITHUB_EVENT_LABEL_NAME}" == x"run-decode" ]]; then mkdir -p conformer_ctc3/exp ln -s $PWD/$repo/exp/pretrained.pt conformer_ctc3/exp/epoch-999.pt ln -s $PWD/$repo/data/lang_bpe_500 data/ diff --git a/.github/scripts/run-librispeech-lstm-transducer-stateless2-2022-09-03.sh b/.github/scripts/run-librispeech-lstm-transducer-stateless2-2022-09-03.sh index 4cd2c4bec..d547bdd45 100755 --- a/.github/scripts/run-librispeech-lstm-transducer-stateless2-2022-09-03.sh +++ b/.github/scripts/run-librispeech-lstm-transducer-stateless2-2022-09-03.sh @@ -31,7 +31,7 @@ log "Test exporting with torch.jit.trace()" ./lstm_transducer_stateless2/export.py \ --exp-dir $repo/exp \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ @@ -55,7 +55,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -68,7 +68,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless-2022-03-12.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless-2022-03-12.sh index 6792c7088..412e3ad56 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless-2022-03-12.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless-2022-03-12.sh @@ -28,7 +28,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -41,7 +41,7 @@ for method in fast_beam_search modified_beam_search beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless2-2022-04-29.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless2-2022-04-29.sh index dbf678d72..243b669ed 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless2-2022-04-29.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless2-2022-04-29.sh @@ -36,7 +36,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -49,7 +49,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-04-29.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-04-29.sh index b6d477afe..2d0f80304 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-04-29.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-04-29.sh @@ -35,7 +35,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -48,7 +48,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-05-13.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-05-13.sh index efa4b53f0..3d5814c48 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-05-13.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless3-2022-05-13.sh @@ -30,14 +30,14 @@ popd log "Export to torchscript model" ./pruned_transducer_stateless3/export.py \ --exp-dir $repo/exp \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit 1 ./pruned_transducer_stateless3/export.py \ --exp-dir $repo/exp \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit-trace 1 @@ -74,7 +74,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -87,7 +87,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless5-2022-05-13.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless5-2022-05-13.sh index 511fe0c9e..3d2442d54 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless5-2022-05-13.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless5-2022-05-13.sh @@ -32,7 +32,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --num-encoder-layers 18 \ --dim-feedforward 2048 \ --nhead 8 \ @@ -51,7 +51,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav \ diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless7-2022-11-11.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless7-2022-11-11.sh index 2bc179c86..961dde4f4 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless7-2022-11-11.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless7-2022-11-11.sh @@ -33,7 +33,7 @@ log "Export to torchscript model" ./pruned_transducer_stateless7/export.py \ --exp-dir $repo/exp \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit 1 @@ -56,7 +56,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -69,7 +69,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-2022-12-01.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-2022-12-01.sh index 192438353..ba7139efb 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-2022-12-01.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-2022-12-01.sh @@ -37,7 +37,7 @@ log "Export to torchscript model" ./pruned_transducer_stateless7_ctc/export.py \ --exp-dir $repo/exp \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit 1 @@ -74,7 +74,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -87,7 +87,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh index 7d2853c17..1ecbc4798 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless7-ctc-bs-2023-01-29.sh @@ -36,7 +36,7 @@ log "Export to torchscript model" ./pruned_transducer_stateless7_ctc_bs/export.py \ --exp-dir $repo/exp \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit 1 @@ -72,7 +72,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -85,7 +85,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless7-streaming-2022-12-29.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless7-streaming-2022-12-29.sh index e1e4e1f10..37b192a57 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless7-streaming-2022-12-29.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless7-streaming-2022-12-29.sh @@ -37,7 +37,7 @@ log "Export to torchscript model" ./pruned_transducer_stateless7_streaming/export.py \ --exp-dir $repo/exp \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --decode-chunk-len 32 \ --epoch 99 \ --avg 1 \ @@ -81,7 +81,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --decode-chunk-len 32 \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ @@ -95,7 +95,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --decode-chunk-len 32 \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ diff --git a/.github/scripts/run-librispeech-pruned-transducer-stateless8-2022-11-14.sh b/.github/scripts/run-librispeech-pruned-transducer-stateless8-2022-11-14.sh index 5d9485692..4f2bfac24 100755 --- a/.github/scripts/run-librispeech-pruned-transducer-stateless8-2022-11-14.sh +++ b/.github/scripts/run-librispeech-pruned-transducer-stateless8-2022-11-14.sh @@ -41,7 +41,7 @@ log "Decode with models exported by torch.jit.script()" log "Export to torchscript model" ./pruned_transducer_stateless8/export.py \ --exp-dir $repo/exp \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model false \ --epoch 99 \ --avg 1 \ @@ -65,7 +65,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -78,7 +78,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-streaming-pruned-transducer-stateless2-2022-06-26.sh b/.github/scripts/run-librispeech-streaming-pruned-transducer-stateless2-2022-06-26.sh index 77cd59506..5cbdad16d 100755 --- a/.github/scripts/run-librispeech-streaming-pruned-transducer-stateless2-2022-06-26.sh +++ b/.github/scripts/run-librispeech-streaming-pruned-transducer-stateless2-2022-06-26.sh @@ -32,7 +32,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --simulate-streaming 1 \ --causal-convolution 1 \ $repo/test_wavs/1089-134686-0001.wav \ @@ -47,7 +47,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --simulate-streaming 1 \ --causal-convolution 1 \ $repo/test_wavs/1089-134686-0001.wav \ diff --git a/.github/scripts/run-librispeech-transducer-stateless2-2022-04-19.sh b/.github/scripts/run-librispeech-transducer-stateless2-2022-04-19.sh index b4aca1b6b..ff77855a2 100755 --- a/.github/scripts/run-librispeech-transducer-stateless2-2022-04-19.sh +++ b/.github/scripts/run-librispeech-transducer-stateless2-2022-04-19.sh @@ -28,7 +28,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -41,7 +41,7 @@ for method in fast_beam_search modified_beam_search beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-librispeech-zipformer-mmi-2022-12-08.sh b/.github/scripts/run-librispeech-zipformer-mmi-2022-12-08.sh index a58b8ec56..c59921055 100755 --- a/.github/scripts/run-librispeech-zipformer-mmi-2022-12-08.sh +++ b/.github/scripts/run-librispeech-zipformer-mmi-2022-12-08.sh @@ -37,7 +37,7 @@ log "Export to torchscript model" ./zipformer_mmi/export.py \ --exp-dir $repo/exp \ --use-averaged-model false \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --jit 1 @@ -61,7 +61,7 @@ for method in 1best nbest nbest-rescoring-LG nbest-rescoring-3-gram nbest-rescor --method $method \ --checkpoint $repo/exp/pretrained.pt \ --lang-dir $repo/data/lang_bpe_500 \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-pre-trained-conformer-ctc.sh b/.github/scripts/run-pre-trained-conformer-ctc.sh index 125d1f3b1..a4959aa01 100755 --- a/.github/scripts/run-pre-trained-conformer-ctc.sh +++ b/.github/scripts/run-pre-trained-conformer-ctc.sh @@ -27,7 +27,7 @@ log "CTC decoding" --method ctc-decoding \ --num-classes 500 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.flac \ $repo/test_wavs/1221-135766-0001.flac \ $repo/test_wavs/1221-135766-0002.flac @@ -38,7 +38,7 @@ log "HLG decoding" --method 1best \ --num-classes 500 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --words-file $repo/data/lang_bpe_500/words.txt \ --HLG $repo/data/lang_bpe_500/HLG.pt \ $repo/test_wavs/1089-134686-0001.flac \ diff --git a/.github/scripts/run-pre-trained-transducer-stateless-librispeech-100h.sh b/.github/scripts/run-pre-trained-transducer-stateless-librispeech-100h.sh index 89115e88d..7b686328d 100755 --- a/.github/scripts/run-pre-trained-transducer-stateless-librispeech-100h.sh +++ b/.github/scripts/run-pre-trained-transducer-stateless-librispeech-100h.sh @@ -28,7 +28,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -41,7 +41,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-pre-trained-transducer-stateless-librispeech-960h.sh b/.github/scripts/run-pre-trained-transducer-stateless-librispeech-960h.sh index 85e2c89e6..a8eeeb514 100755 --- a/.github/scripts/run-pre-trained-transducer-stateless-librispeech-960h.sh +++ b/.github/scripts/run-pre-trained-transducer-stateless-librispeech-960h.sh @@ -28,7 +28,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -41,7 +41,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-pre-trained-transducer-stateless.sh b/.github/scripts/run-pre-trained-transducer-stateless.sh index 41456f11b..2e2360435 100755 --- a/.github/scripts/run-pre-trained-transducer-stateless.sh +++ b/.github/scripts/run-pre-trained-transducer-stateless.sh @@ -28,7 +28,7 @@ for sym in 1 2 3; do --method greedy_search \ --max-sym-per-frame $sym \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav @@ -41,7 +41,7 @@ for method in fast_beam_search modified_beam_search beam_search; do --method $method \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-pre-trained-transducer.sh b/.github/scripts/run-pre-trained-transducer.sh index 1331c966c..b865f8d13 100755 --- a/.github/scripts/run-pre-trained-transducer.sh +++ b/.github/scripts/run-pre-trained-transducer.sh @@ -27,7 +27,7 @@ log "Beam search decoding" --method beam_search \ --beam-size 4 \ --checkpoint $repo/exp/pretrained.pt \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ $repo/test_wavs/1089-134686-0001.wav \ $repo/test_wavs/1221-135766-0001.wav \ $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/scripts/run-wenetspeech-pruned-transducer-stateless2.sh b/.github/scripts/run-wenetspeech-pruned-transducer-stateless2.sh index 90097c752..a3a2d3080 100755 --- a/.github/scripts/run-wenetspeech-pruned-transducer-stateless2.sh +++ b/.github/scripts/run-wenetspeech-pruned-transducer-stateless2.sh @@ -17,7 +17,6 @@ git lfs install git clone $repo_url repo=$(basename $repo_url) - log "Display test files" tree $repo/ ls -lh $repo/test_wavs/*.wav @@ -29,12 +28,11 @@ popd log "Test exporting to ONNX format" -./pruned_transducer_stateless2/export.py \ +./pruned_transducer_stateless2/export-onnx.py \ --exp-dir $repo/exp \ --lang-dir $repo/data/lang_char \ --epoch 99 \ - --avg 1 \ - --onnx 1 + --avg 1 log "Export to torchscript model" @@ -59,19 +57,17 @@ log "Decode with ONNX models" ./pruned_transducer_stateless2/onnx_check.py \ --jit-filename $repo/exp/cpu_jit.pt \ - --onnx-encoder-filename $repo/exp/encoder.onnx \ - --onnx-decoder-filename $repo/exp/decoder.onnx \ - --onnx-joiner-filename $repo/exp/joiner.onnx \ - --onnx-joiner-encoder-proj-filename $repo/exp/joiner_encoder_proj.onnx \ - --onnx-joiner-decoder-proj-filename $repo/exp/joiner_decoder_proj.onnx + --onnx-encoder-filename $repo/exp/encoder-epoch-10-avg-2.onnx \ + --onnx-decoder-filename $repo/exp/decoder-epoch-10-avg-2.onnx \ + --onnx-joiner-filename $repo/exp/joiner-epoch-10-avg-2.onnx \ + --onnx-joiner-encoder-proj-filename $repo/exp/joiner_encoder_proj-epoch-10-avg-2.onnx \ + --onnx-joiner-decoder-proj-filename $repo/exp/joiner_decoder_proj-epoch-10-avg-2.onnx ./pruned_transducer_stateless2/onnx_pretrained.py \ --tokens $repo/data/lang_char/tokens.txt \ - --encoder-model-filename $repo/exp/encoder.onnx \ - --decoder-model-filename $repo/exp/decoder.onnx \ - --joiner-model-filename $repo/exp/joiner.onnx \ - --joiner-encoder-proj-model-filename $repo/exp/joiner_encoder_proj.onnx \ - --joiner-decoder-proj-model-filename $repo/exp/joiner_decoder_proj.onnx \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ $repo/test_wavs/DEV_T0000000000.wav \ $repo/test_wavs/DEV_T0000000001.wav \ $repo/test_wavs/DEV_T0000000002.wav @@ -104,9 +100,9 @@ for sym in 1 2 3; do --lang-dir $repo/data/lang_char \ --decoding-method greedy_search \ --max-sym-per-frame $sym \ - $repo/test_wavs/DEV_T0000000000.wav \ - $repo/test_wavs/DEV_T0000000001.wav \ - $repo/test_wavs/DEV_T0000000002.wav + $repo/test_wavs/DEV_T0000000000.wav \ + $repo/test_wavs/DEV_T0000000001.wav \ + $repo/test_wavs/DEV_T0000000002.wav done for method in modified_beam_search beam_search fast_beam_search; do @@ -117,7 +113,7 @@ for method in modified_beam_search beam_search fast_beam_search; do --beam-size 4 \ --checkpoint $repo/exp/epoch-99.pt \ --lang-dir $repo/data/lang_char \ - $repo/test_wavs/DEV_T0000000000.wav \ - $repo/test_wavs/DEV_T0000000001.wav \ - $repo/test_wavs/DEV_T0000000002.wav + $repo/test_wavs/DEV_T0000000000.wav \ + $repo/test_wavs/DEV_T0000000001.wav \ + $repo/test_wavs/DEV_T0000000002.wav done diff --git a/.github/scripts/test-ncnn-export.sh b/.github/scripts/test-ncnn-export.sh index ac16131d0..4073c594a 100755 --- a/.github/scripts/test-ncnn-export.sh +++ b/.github/scripts/test-ncnn-export.sh @@ -45,7 +45,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/pretrained-epoch-30-avg-10-averaged.pt" cd exp @@ -56,11 +55,10 @@ log "Export via torch.jit.trace()" ./conv_emformer_transducer_stateless2/export-for-ncnn.py \ --exp-dir $repo/exp \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ - \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --num-encoder-layers 12 \ --chunk-length 32 \ --cnn-module-kernel 31 \ @@ -91,7 +89,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/pretrained-iter-468000-avg-16.pt" cd exp @@ -102,7 +99,7 @@ log "Export via torch.jit.trace()" ./lstm_transducer_stateless2/export-for-ncnn.py \ --exp-dir $repo/exp \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 @@ -140,7 +137,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/pretrained.pt" cd exp @@ -148,7 +144,7 @@ ln -s pretrained.pt epoch-99.pt popd ./pruned_transducer_stateless7_streaming/export-for-ncnn.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --exp-dir $repo/exp \ --use-averaged-model 0 \ --epoch 99 \ @@ -199,7 +195,7 @@ ln -s pretrained.pt epoch-9999.pt popd ./pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py \ - --lang-dir $repo/data/lang_char_bpe \ + --tokens $repo/data/lang_char_bpe/tokens.txt \ --exp-dir $repo/exp \ --use-averaged-model 0 \ --epoch 9999 \ diff --git a/.github/scripts/test-onnx-export.sh b/.github/scripts/test-onnx-export.sh index 39467c44a..fcfc11fa6 100755 --- a/.github/scripts/test-onnx-export.sh +++ b/.github/scripts/test-onnx-export.sh @@ -10,7 +10,123 @@ log() { cd egs/librispeech/ASR +log "==========================================================================" +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 +log "Downloading pre-trained model from $repo_url" +git lfs install +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) +pushd $repo +git lfs pull --include "exp/pretrained.pt" +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +log "Export via torch.jit.script()" +./zipformer/export.py \ + --exp-dir $repo/exp \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --epoch 99 \ + --avg 1 \ + --jit 1 + +log "Test export to ONNX format" +./zipformer/export-onnx.py \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --num-encoder-layers "2,2,3,4,3,2" \ + --downsampling-factor "1,2,4,8,4,2" \ + --feedforward-dim "512,768,1024,1536,1024,768" \ + --num-heads "4,4,4,8,4,4" \ + --encoder-dim "192,256,384,512,384,256" \ + --query-head-dim 32 \ + --value-head-dim 12 \ + --pos-head-dim 4 \ + --pos-dim 48 \ + --encoder-unmasked-dim "192,192,256,256,256,192" \ + --cnn-module-kernel "31,31,15,15,15,31" \ + --decoder-dim 512 \ + --joiner-dim 512 \ + --causal False \ + --chunk-size "16,32,64,-1" \ + --left-context-frames "64,128,256,-1" + +ls -lh $repo/exp + +log "Run onnx_check.py" + +./zipformer/onnx_check.py \ + --jit-filename $repo/exp/jit_script.pt \ + --onnx-encoder-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --onnx-decoder-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --onnx-joiner-filename $repo/exp/joiner-epoch-99-avg-1.onnx + +log "Run onnx_pretrained.py" + +./zipformer/onnx_pretrained.py \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1.onnx \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + $repo/test_wavs/1089-134686-0001.wav + +rm -rf $repo + +repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-streaming-zipformer-2023-05-17 +log "Downloading pre-trained model from $repo_url" +git lfs install +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url +repo=$(basename $repo_url) + +pushd $repo +git lfs pull --include "exp/pretrained.pt" + +cd exp +ln -s pretrained.pt epoch-99.pt +popd + +log "Test export streaming model to ONNX format" +./zipformer/export-onnx-streaming.py \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --use-averaged-model 0 \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --num-encoder-layers "2,2,3,4,3,2" \ + --downsampling-factor "1,2,4,8,4,2" \ + --feedforward-dim "512,768,1024,1536,1024,768" \ + --num-heads "4,4,4,8,4,4" \ + --encoder-dim "192,256,384,512,384,256" \ + --query-head-dim 32 \ + --value-head-dim 12 \ + --pos-head-dim 4 \ + --pos-dim 48 \ + --encoder-unmasked-dim "192,192,256,256,256,192" \ + --cnn-module-kernel "31,31,15,15,15,31" \ + --decoder-dim 512 \ + --joiner-dim 512 \ + --causal True \ + --chunk-size 16 \ + --left-context-frames 64 + +ls -lh $repo/exp + +log "Run onnx_pretrained-streaming.py" + +./zipformer/onnx_pretrained-streaming.py \ + --encoder-model-filename $repo/exp/encoder-epoch-99-avg-1-chunk-16-left-64.onnx \ + --decoder-model-filename $repo/exp/decoder-epoch-99-avg-1-chunk-16-left-64.onnx \ + --joiner-model-filename $repo/exp/joiner-epoch-99-avg-1-chunk-16-left-64.onnx \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + $repo/test_wavs/1089-134686-0001.wav + +rm -rf $repo + +log "--------------------------------------------------------------------------" log "==========================================================================" repo_url=https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 @@ -39,7 +155,7 @@ log "Export via torch.jit.trace()" log "Test exporting to ONNX format" ./pruned_transducer_stateless7_streaming/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -88,7 +204,7 @@ popd log "Export via torch.jit.script()" ./pruned_transducer_stateless3/export.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 9999 \ --avg 1 \ --exp-dir $repo/exp/ \ @@ -97,7 +213,7 @@ log "Export via torch.jit.script()" log "Test exporting to ONNX format" ./pruned_transducer_stateless3/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 9999 \ --avg 1 \ --exp-dir $repo/exp/ @@ -126,7 +242,6 @@ log "Run onnx_pretrained.py" rm -rf $repo log "--------------------------------------------------------------------------" - log "==========================================================================" repo_url=https://huggingface.co/csukuangfj/icefall-asr-librispeech-pruned-transducer-stateless5-2022-05-13 GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url @@ -143,7 +258,7 @@ popd log "Export via torch.jit.script()" ./pruned_transducer_stateless5/export.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ @@ -159,7 +274,7 @@ log "Export via torch.jit.script()" log "Test exporting to ONNX format" ./pruned_transducer_stateless5/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ @@ -205,7 +320,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/pretrained.pt" cd exp @@ -215,7 +329,7 @@ popd log "Export via torch.jit.script()" ./pruned_transducer_stateless7/export.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -226,7 +340,7 @@ log "Export via torch.jit.script()" log "Test exporting to ONNX format" ./pruned_transducer_stateless7/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -270,7 +384,7 @@ popd log "Test exporting to ONNX format" ./conv_emformer_transducer_stateless2/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -310,7 +424,7 @@ popd log "Export via torch.jit.trace()" ./lstm_transducer_stateless2/export.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -320,7 +434,7 @@ log "Export via torch.jit.trace()" log "Test exporting to ONNX format" ./lstm_transducer_stateless2/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/export.py b/egs/aishell/ASR/pruned_transducer_stateless7/export.py deleted file mode 100755 index 1b0e8d3b9..000000000 --- a/egs/aishell/ASR/pruned_transducer_stateless7/export.py +++ /dev/null @@ -1,321 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 2021 Xiaomi Corporation (Author: Fangjun Kuang) -# -# See ../../../../LICENSE for clarification regarding multiple authors -# -# 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 script converts several saved checkpoints -# to a single one using model averaging. -""" - -Usage: - -(1) Export to torchscript model using torch.jit.script() - -./pruned_transducer_stateless7/export.py \ - --exp-dir ./pruned_transducer_stateless7/exp \ - --lang-dir data/lang_char \ - --epoch 30 \ - --avg 9 \ - --jit 1 - -It will generate a file `cpu_jit.pt` in the given `exp_dir`. You can later -load it by `torch.jit.load("cpu_jit.pt")`. - -Note `cpu` in the name `cpu_jit.pt` means the parameters when loaded into Python -are on CPU. You can use `to("cuda")` to move them to a CUDA device. - -Check -https://github.com/k2-fsa/sherpa -for how to use the exported models outside of icefall. - -(2) Export `model.state_dict()` - -./pruned_transducer_stateless7/export.py \ - --exp-dir ./pruned_transducer_stateless7/exp \ - --lang-dir data/lang_char \ - --epoch 20 \ - --avg 10 - -It will generate a file `pretrained.pt` in the given `exp_dir`. You can later -load it by `icefall.checkpoint.load_checkpoint()`. - -To use the generated file with `pruned_transducer_stateless7/decode.py`, -you can do: - - cd /path/to/exp_dir - ln -s pretrained.pt epoch-9999.pt - - cd /path/to/egs/librispeech/ASR - ./pruned_transducer_stateless7/decode.py \ - --exp-dir ./pruned_transducer_stateless7/exp \ - --epoch 9999 \ - --avg 1 \ - --max-duration 600 \ - --decoding-method greedy_search \ - --lang-dir data/lang_char - -Check ./pretrained.py for its usage. - -Note: If you don't want to train a model from scratch, we have -provided one for you. You can get it at - -https://huggingface.co/marcoyang/icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21 - -with the following commands: - - sudo apt-get install git-lfs - git lfs install - git clone https://huggingface.co/marcoyang/icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21 - # You will find the pre-trained model in icefall-asr-aishell-zipformer-pruned-transducer-stateless7-2023-03-21exp -""" - -import argparse -import logging -from pathlib import Path - -import sentencepiece as spm -import torch -import torch.nn as nn -from scaling_converter import convert_scaled_to_non_scaled -from train2 import add_model_arguments, get_params, get_transducer_model - -from icefall.checkpoint import ( - average_checkpoints, - average_checkpoints_with_averaged_model, - find_checkpoints, - load_checkpoint, -) -from icefall.lexicon import Lexicon -from icefall.utils import str2bool - - -def get_parser(): - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) - - parser.add_argument( - "--epoch", - type=int, - default=30, - help="""It specifies the checkpoint to use for decoding. - Note: Epoch counts from 1. - You can specify --avg to use more checkpoints for model averaging.""", - ) - - parser.add_argument( - "--iter", - type=int, - default=0, - help="""If positive, --epoch is ignored and it - will use the checkpoint exp_dir/checkpoint-iter.pt. - You can specify --avg to use more checkpoints for model averaging. - """, - ) - - parser.add_argument( - "--avg", - type=int, - default=9, - help="Number of checkpoints to average. Automatically select " - "consecutive checkpoints before the checkpoint specified by " - "'--epoch' and '--iter'", - ) - - parser.add_argument( - "--use-averaged-model", - type=str2bool, - default=True, - help="Whether to load averaged model. Currently it only supports " - "using --epoch. If True, it would decode with the averaged model " - "over the epoch range from `epoch-avg` (excluded) to `epoch`." - "Actually only the models with epoch number of `epoch-avg` and " - "`epoch` are loaded for averaging. ", - ) - - parser.add_argument( - "--exp-dir", - type=str, - default="pruned_transducer_stateless7/exp", - help="""It specifies the directory where all training related - files, e.g., checkpoints, log, etc, are saved - """, - ) - - parser.add_argument( - "--lang-dir", - type=str, - default="data/lang_char", - help="""The lang dir - It contains language related input files such as - "lexicon.txt" - """, - ) - - parser.add_argument( - "--jit", - type=str2bool, - default=False, - help="""True to save a model after applying torch.jit.script. - It will generate a file named cpu_jit.pt - - Check ./jit_pretrained.py for how to use it. - """, - ) - - parser.add_argument( - "--context-size", - type=int, - default=1, - help="The context size in the decoder. 1 means bigram; 2 means tri-gram", - ) - - add_model_arguments(parser) - - return parser - - -@torch.no_grad() -def main(): - args = get_parser().parse_args() - args.exp_dir = Path(args.exp_dir) - - params = get_params() - params.update(vars(args)) - - device = torch.device("cpu") - if torch.cuda.is_available(): - device = torch.device("cuda", 0) - - logging.info(f"device: {device}") - - lexicon = Lexicon(params.lang_dir) - params.blank_id = 0 - params.vocab_size = max(lexicon.tokens) + 1 - - logging.info(params) - - logging.info("About to create model") - model = get_transducer_model(params) - - model.to(device) - - if not params.use_averaged_model: - if params.iter > 0: - filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ - : params.avg - ] - if len(filenames) == 0: - raise ValueError( - f"No checkpoints found for" - f" --iter {params.iter}, --avg {params.avg}" - ) - elif len(filenames) < params.avg: - raise ValueError( - f"Not enough checkpoints ({len(filenames)}) found for" - f" --iter {params.iter}, --avg {params.avg}" - ) - logging.info(f"averaging {filenames}") - model.to(device) - model.load_state_dict(average_checkpoints(filenames, device=device)) - elif params.avg == 1: - load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) - else: - start = params.epoch - params.avg + 1 - filenames = [] - for i in range(start, params.epoch + 1): - if i >= 1: - filenames.append(f"{params.exp_dir}/epoch-{i}.pt") - logging.info(f"averaging {filenames}") - model.to(device) - model.load_state_dict(average_checkpoints(filenames, device=device)) - else: - if params.iter > 0: - filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ - : params.avg + 1 - ] - if len(filenames) == 0: - raise ValueError( - f"No checkpoints found for" - f" --iter {params.iter}, --avg {params.avg}" - ) - elif len(filenames) < params.avg + 1: - raise ValueError( - f"Not enough checkpoints ({len(filenames)}) found for" - f" --iter {params.iter}, --avg {params.avg}" - ) - filename_start = filenames[-1] - filename_end = filenames[0] - logging.info( - "Calculating the averaged model over iteration checkpoints" - f" from {filename_start} (excluded) to {filename_end}" - ) - model.to(device) - model.load_state_dict( - average_checkpoints_with_averaged_model( - filename_start=filename_start, - filename_end=filename_end, - device=device, - ) - ) - else: - assert params.avg > 0, params.avg - start = params.epoch - params.avg - assert start >= 1, start - filename_start = f"{params.exp_dir}/epoch-{start}.pt" - filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" - logging.info( - f"Calculating the averaged model over epoch range from " - f"{start} (excluded) to {params.epoch}" - ) - model.to(device) - model.load_state_dict( - average_checkpoints_with_averaged_model( - filename_start=filename_start, - filename_end=filename_end, - device=device, - ) - ) - - model.to("cpu") - model.eval() - - if params.jit is True: - convert_scaled_to_non_scaled(model, inplace=True) - # We won't use the forward() method of the model in C++, so just ignore - # it here. - # Otherwise, one of its arguments is a ragged tensor and is not - # torch scriptabe. - model.__class__.forward = torch.jit.ignore(model.__class__.forward) - logging.info("Using torch.jit.script") - model = torch.jit.script(model) - filename = params.exp_dir / "cpu_jit.pt" - model.save(str(filename)) - logging.info(f"Saved to {filename}") - else: - logging.info("Not using torchscript. Export model.state_dict()") - # Save it using a format so that it can be loaded - # by :func:`load_checkpoint` - filename = params.exp_dir / "pretrained.pt" - torch.save({"model": model.state_dict()}, str(filename)) - logging.info(f"Saved to {filename}") - - -if __name__ == "__main__": - formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" - - logging.basicConfig(format=formatter, level=logging.INFO) - main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/export.py b/egs/aishell/ASR/pruned_transducer_stateless7/export.py new file mode 120000 index 000000000..2713792e6 --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/export.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/export.py \ No newline at end of file diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py b/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py deleted file mode 100644 index cc54027d6..000000000 --- a/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) -# -# See ../../../../LICENSE for clarification regarding multiple authors -# -# 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 script loads a checkpoint and uses it to decode waves. -You can generate the checkpoint with the following command: - -./pruned_transducer_stateless7/export.py \ - --exp-dir ./pruned_transducer_stateless7/exp \ - --lang-dir data/lang_char \ - --epoch 20 \ - --avg 10 - -Usage of this script: - -(1) greedy search -./pruned_transducer_stateless7/pretrained.py \ - --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --lang-dir ./data/lang_char \ - --method greedy_search \ - /path/to/foo.wav \ - /path/to/bar.wav - -(2) beam search -./pruned_transducer_stateless7/pretrained.py \ - --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --lang-dir ./data/lang_char \ - --method beam_search \ - --beam-size 4 \ - /path/to/foo.wav \ - /path/to/bar.wav - -(3) modified beam search -./pruned_transducer_stateless7/pretrained.py \ - --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --lang-dir ./data/lang_char \ - --method modified_beam_search \ - --beam-size 4 \ - /path/to/foo.wav \ - /path/to/bar.wav - -(4) fast beam search -./pruned_transducer_stateless7/pretrained.py \ - --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --lang-dir ./data/lang_char \ - --method fast_beam_search \ - --beam-size 4 \ - /path/to/foo.wav \ - /path/to/bar.wav - -You can also use `./pruned_transducer_stateless7/exp/epoch-xx.pt`. - -Note: ./pruned_transducer_stateless7/exp/pretrained.pt is generated by -./pruned_transducer_stateless7/export.py -""" - - -import argparse -import logging -import math -from typing import List - -import k2 -import kaldifeat -import sentencepiece as spm -import torch -import torchaudio -from beam_search import ( - beam_search, - fast_beam_search_one_best, - greedy_search, - greedy_search_batch, - modified_beam_search, -) -from torch.nn.utils.rnn import pad_sequence -from train import add_model_arguments, get_params, get_transducer_model - -from icefall.lexicon import Lexicon -from icefall.utils import str2bool - - -def get_parser(): - parser = argparse.ArgumentParser( - formatter_class=argparse.ArgumentDefaultsHelpFormatter - ) - - parser.add_argument( - "--checkpoint", - type=str, - required=True, - help="Path to the checkpoint. " - "The checkpoint is assumed to be saved by " - "icefall.checkpoint.save_checkpoint().", - ) - - parser.add_argument( - "--lang-dir", - type=str, - help="""The lang dir - It contains language related input files such as - "lexicon.txt" - """, - ) - - parser.add_argument( - "--method", - type=str, - default="greedy_search", - help="""Possible values are: - - greedy_search - - beam_search - - modified_beam_search - - fast_beam_search - """, - ) - - parser.add_argument( - "sound_files", - type=str, - nargs="+", - help="The input sound file(s) to transcribe. " - "Supported formats are those supported by torchaudio.load(). " - "For example, wav and flac are supported. " - "The sample rate has to be 16kHz.", - ) - - parser.add_argument( - "--sample-rate", - type=int, - default=16000, - help="The sample rate of the input sound file", - ) - - parser.add_argument( - "--beam-size", - type=int, - default=4, - help="""An integer indicating how many candidates we will keep for each - frame. Used only when --method is beam_search or - modified_beam_search.""", - ) - - parser.add_argument( - "--beam", - type=float, - default=4, - help="""A floating point value to calculate the cutoff score during beam - search (i.e., `cutoff = max-score - beam`), which is the same as the - `beam` in Kaldi. - Used only when --method is fast_beam_search""", - ) - - parser.add_argument( - "--max-contexts", - type=int, - default=4, - help="""Used only when --method is fast_beam_search""", - ) - - parser.add_argument( - "--max-states", - type=int, - default=8, - help="""Used only when --method is fast_beam_search""", - ) - - parser.add_argument( - "--context-size", - type=int, - default=1, - help="The context size in the decoder. 1 means bigram; 2 means tri-gram", - ) - parser.add_argument( - "--max-sym-per-frame", - type=int, - default=1, - help="""Maximum number of symbols per frame. Used only when - --method is greedy_search. - """, - ) - - add_model_arguments(parser) - - return parser - - -def read_sound_files( - filenames: List[str], expected_sample_rate: float -) -> List[torch.Tensor]: - """Read a list of sound files into a list 1-D float32 torch tensors. - Args: - filenames: - A list of sound filenames. - expected_sample_rate: - The expected sample rate of the sound files. - Returns: - Return a list of 1-D float32 torch tensors. - """ - ans = [] - for f in filenames: - wave, sample_rate = torchaudio.load(f) - assert ( - sample_rate == expected_sample_rate - ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" - # We use only the first channel - ans.append(wave[0]) - return ans - - -@torch.no_grad() -def main(): - parser = get_parser() - args = parser.parse_args() - - params = get_params() - - params.update(vars(args)) - - lexicon = Lexicon(params.lang_dir) - params.blank_id = 0 - params.vocab_size = max(lexicon.tokens) + 1 - token_table = lexicon.token_table - - logging.info(f"{params}") - - device = torch.device("cpu") - if torch.cuda.is_available(): - device = torch.device("cuda", 0) - - logging.info(f"device: {device}") - - logging.info("Creating model") - model = get_transducer_model(params) - - num_param = sum([p.numel() for p in model.parameters()]) - logging.info(f"Number of model parameters: {num_param}") - - checkpoint = torch.load(args.checkpoint, map_location="cpu") - model.load_state_dict(checkpoint["model"], strict=False) - model.to(device) - model.eval() - model.device = device - - logging.info("Constructing Fbank computer") - opts = kaldifeat.FbankOptions() - opts.device = device - opts.frame_opts.dither = 0 - opts.frame_opts.snip_edges = False - opts.frame_opts.samp_freq = params.sample_rate - opts.mel_opts.num_bins = params.feature_dim - - fbank = kaldifeat.Fbank(opts) - - logging.info(f"Reading sound files: {params.sound_files}") - waves = read_sound_files( - filenames=params.sound_files, expected_sample_rate=params.sample_rate - ) - waves = [w.to(device) for w in waves] - - logging.info("Decoding started") - features = fbank(waves) - feature_lengths = [f.size(0) for f in features] - - features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) - - feature_lengths = torch.tensor(feature_lengths, device=device) - - encoder_out, encoder_out_lens = model.encoder(x=features, x_lens=feature_lengths) - - num_waves = encoder_out.size(0) - hyps = [] - msg = f"Using {params.method}" - if params.method == "beam_search": - msg += f" with beam size {params.beam_size}" - logging.info(msg) - - if params.method == "fast_beam_search": - decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) - hyp_tokens = fast_beam_search_one_best( - model=model, - decoding_graph=decoding_graph, - encoder_out=encoder_out, - encoder_out_lens=encoder_out_lens, - beam=params.beam, - max_contexts=params.max_contexts, - max_states=params.max_states, - ) - elif params.method == "modified_beam_search": - hyp_tokens = modified_beam_search( - model=model, - encoder_out=encoder_out, - encoder_out_lens=encoder_out_lens, - beam=params.beam_size, - ) - elif params.method == "greedy_search" and params.max_sym_per_frame == 1: - hyp_tokens = greedy_search_batch( - model=model, - encoder_out=encoder_out, - encoder_out_lens=encoder_out_lens, - ) - else: - for i in range(num_waves): - # fmt: off - encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] - # fmt: on - if params.method == "greedy_search": - hyp_tokens = greedy_search( - model=model, - encoder_out=encoder_out_i, - max_sym_per_frame=params.max_sym_per_frame, - ) - elif params.method == "beam_search": - hyp_tokens = beam_search( - model=model, - encoder_out=encoder_out_i, - beam=params.beam_size, - ) - else: - raise ValueError(f"Unsupported method: {params.method}") - - hyps = [[token_table[t] for t in tokens] for tokens in hyp_tokens] - s = "\n" - for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" - logging.info(s) - - logging.info("Decoding Done") - - -if __name__ == "__main__": - formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" - - logging.basicConfig(format=formatter, level=logging.INFO) - main() diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py b/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py new file mode 120000 index 000000000..068f0f57f --- /dev/null +++ b/egs/aishell/ASR/pruned_transducer_stateless7/pretrained.py @@ -0,0 +1 @@ +../../../librispeech/ASR/pruned_transducer_stateless7/pretrained.py \ No newline at end of file diff --git a/egs/librispeech/ASR/conformer_ctc/export.py b/egs/librispeech/ASR/conformer_ctc/export.py index fbcbd7b29..f0bb97560 100755 --- a/egs/librispeech/ASR/conformer_ctc/export.py +++ b/egs/librispeech/ASR/conformer_ctc/export.py @@ -23,12 +23,13 @@ import argparse import logging from pathlib import Path +import k2 import torch from conformer import Conformer from icefall.checkpoint import average_checkpoints, load_checkpoint from icefall.lexicon import Lexicon -from icefall.utils import AttributeDict, str2bool +from icefall.utils import AttributeDict, num_tokens, str2bool def get_parser(): @@ -63,11 +64,10 @@ def get_parser(): ) parser.add_argument( - "--lang-dir", + "--tokens", type=str, - default="data/lang_bpe_500", - help="""It contains language related input files such as "lexicon.txt" - """, + required=True, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -98,16 +98,16 @@ def get_params() -> AttributeDict: def main(): args = get_parser().parse_args() args.exp_dir = Path(args.exp_dir) - args.lang_dir = Path(args.lang_dir) params = get_params() params.update(vars(args)) logging.info(params) - lexicon = Lexicon(params.lang_dir) - max_token_id = max(lexicon.tokens) - num_classes = max_token_id + 1 # +1 for the blank + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + + num_classes = num_tokens(token_table) + 1 # +1 for the blank device = torch.device("cpu") if torch.cuda.is_available(): diff --git a/egs/librispeech/ASR/conformer_ctc/pretrained.py b/egs/librispeech/ASR/conformer_ctc/pretrained.py index 30def9c40..df3e4d819 100755 --- a/egs/librispeech/ASR/conformer_ctc/pretrained.py +++ b/egs/librispeech/ASR/conformer_ctc/pretrained.py @@ -24,7 +24,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from conformer import Conformer @@ -70,11 +69,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model. - Used only when method is ctc-decoding. - """, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -83,10 +80,9 @@ def get_parser(): default="1best", help="""Decoding method. Possible values are: - (0) ctc-decoding - Use CTC decoding. It uses a sentence - piece model, i.e., lang_dir/bpe.model, to convert - word pieces to words. It needs neither a lexicon - nor an n-gram LM. + (0) ctc-decoding - Use CTC decoding. It uses a tokens.txt file + to convert tokens to actual words or characters. It needs + neither a lexicon nor an n-gram LM. (1) 1best - Use the best path as decoding output. Only the transformer encoder output is used for decoding. We call it HLG decoding. @@ -297,6 +293,7 @@ def main(): waves = [w.to(device) for w in waves] logging.info("Decoding started") + hyps = [] features = fbank(waves) features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) @@ -313,10 +310,17 @@ def main(): if params.method == "ctc-decoding": logging.info("Use CTC decoding") - bpe_model = spm.SentencePieceProcessor() - bpe_model.load(params.bpe_model) max_token_id = params.num_classes - 1 + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + H = k2.ctc_topo( max_token=max_token_id, modified=params.num_classes > 500, @@ -337,9 +341,9 @@ def main(): best_path = one_best_decoding( lattice=lattice, use_double_scores=params.use_double_scores ) - token_ids = get_texts(best_path) - hyps = bpe_model.decode(token_ids) - hyps = [s.split() for s in hyps] + hyp_tokens = get_texts(best_path) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method in [ "1best", "whole-lattice-rescoring", @@ -408,16 +412,16 @@ def main(): ) best_path = next(iter(best_path_dict.values())) - hyps = get_texts(best_path) word_sym_table = k2.SymbolTable.from_file(params.words_file) - hyps = [[word_sym_table[i] for i in ids] for ids in hyps] + hyp_tokens = get_texts(best_path) + for hyp in hyp_tokens: + hyps.append(" ".join([word_sym_table[i] for i in hyp])) else: raise ValueError(f"Unsupported decoding method: {params.method}") s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/conformer_ctc2/export.py b/egs/librispeech/ASR/conformer_ctc2/export.py index 7892b03c6..26a95dbfa 100755 --- a/egs/librispeech/ASR/conformer_ctc2/export.py +++ b/egs/librispeech/ASR/conformer_ctc2/export.py @@ -23,6 +23,7 @@ Usage: ./conformer_ctc2/export.py \ --exp-dir ./conformer_ctc2/exp \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -46,6 +47,7 @@ import argparse import logging from pathlib import Path +import k2 import torch from conformer import Conformer from decode import get_params @@ -56,8 +58,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.lexicon import Lexicon -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -123,10 +124,10 @@ def get_parser(): ) parser.add_argument( - "--lang-dir", + "--tokens", type=str, - default="data/lang_bpe_500", - help="The lang dir", + required=True, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -143,14 +144,14 @@ def get_parser(): def main(): args = get_parser().parse_args() args.exp_dir = Path(args.exp_dir) - args.lang_dir = Path(args.lang_dir) params = get_params() params.update(vars(args)) - lexicon = Lexicon(params.lang_dir) - max_token_id = max(lexicon.tokens) - num_classes = max_token_id + 1 # +1 for the blank + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + + num_classes = num_tokens(token_table) + 1 # +1 for the blank device = torch.device("cpu") if torch.cuda.is_available(): diff --git a/egs/librispeech/ASR/conformer_ctc3/export.py b/egs/librispeech/ASR/conformer_ctc3/export.py index c5b95d981..5cb9b4b6d 100755 --- a/egs/librispeech/ASR/conformer_ctc3/export.py +++ b/egs/librispeech/ASR/conformer_ctc3/export.py @@ -25,7 +25,7 @@ Usage: ./conformer_ctc3/export.py \ --exp-dir ./conformer_ctc3/exp \ - --lang-dir data/lang_bpe_500 \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 \ --jit-trace 1 @@ -36,7 +36,7 @@ It will generates the file: `jit_trace.pt`. ./conformer_ctc3/export.py \ --exp-dir ./conformer_ctc3/exp \ - --lang-dir data/lang_bpe_500 \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -62,6 +62,7 @@ import argparse import logging from pathlib import Path +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_ctc_model, get_params @@ -72,8 +73,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.lexicon import Lexicon -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -130,10 +130,10 @@ def get_parser(): ) parser.add_argument( - "--lang-dir", - type=Path, - default="data/lang_bpe_500", - help="The lang dir containing word table and LG graph", + "--tokens", + type=str, + required=True, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -171,9 +171,10 @@ def main(): logging.info(f"device: {device}") - lexicon = Lexicon(params.lang_dir) - max_token_id = max(lexicon.tokens) - num_classes = max_token_id + 1 # +1 for the blank + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + + num_classes = num_tokens(token_table) + 1 # +1 for the blank params.vocab_size = num_classes if params.streaming_model: diff --git a/egs/librispeech/ASR/conformer_ctc3/pretrained.py b/egs/librispeech/ASR/conformer_ctc3/pretrained.py index 880945ea0..c37b99cce 100755 --- a/egs/librispeech/ASR/conformer_ctc3/pretrained.py +++ b/egs/librispeech/ASR/conformer_ctc3/pretrained.py @@ -24,7 +24,7 @@ Usage (for non-streaming mode): (1) ctc-decoding ./conformer_ctc3/pretrained.py \ --checkpoint conformer_ctc3/exp/pretrained.pt \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method ctc-decoding \ --sample-rate 16000 \ test_wavs/1089-134686-0001.wav @@ -67,7 +67,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from decode import get_decoding_params @@ -114,11 +113,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model. - Used only when method is ctc-decoding. - """, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -127,10 +124,9 @@ def get_parser(): default="1best", help="""Decoding method. Possible values are: - (0) ctc-decoding - Use CTC decoding. It uses a sentence - piece model, i.e., lang_dir/bpe.model, to convert - word pieces to words. It needs neither a lexicon - nor an n-gram LM. + (0) ctc-decoding - Use CTC decoding. It uses a tokens.txt file + to convert tokens to actual words or characters. It needs + neither a lexicon nor an n-gram LM. (1) 1best - Use the best path as decoding output. Only the transformer encoder output is used for decoding. We call it HLG decoding. @@ -316,6 +312,7 @@ def main(): waves = [w.to(device) for w in waves] logging.info("Decoding started") + hyps = [] features = fbank(waves) feature_lengths = [f.size(0) for f in features] @@ -348,10 +345,17 @@ def main(): if params.method == "ctc-decoding": logging.info("Use CTC decoding") - bpe_model = spm.SentencePieceProcessor() - bpe_model.load(params.bpe_model) max_token_id = params.num_classes - 1 + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + H = k2.ctc_topo( max_token=max_token_id, modified=False, @@ -372,9 +376,9 @@ def main(): best_path = one_best_decoding( lattice=lattice, use_double_scores=params.use_double_scores ) - token_ids = get_texts(best_path) - hyps = bpe_model.decode(token_ids) - hyps = [s.split() for s in hyps] + hyp_tokens = get_texts(best_path) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method in [ "1best", "nbest-rescoring", @@ -439,16 +443,16 @@ def main(): ) best_path = next(iter(best_path_dict.values())) - hyps = get_texts(best_path) word_sym_table = k2.SymbolTable.from_file(params.words_file) - hyps = [[word_sym_table[i] for i in ids] for ids in hyps] + hyp_tokens = get_texts(best_path) + for hyp in hyp_tokens: + hyps.append(" ".join([word_sym_table[i] for i in hyp])) else: raise ValueError(f"Unsupported decoding method: {params.method}") s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless/export.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless/export.py index 09a3e96b0..67fcc35a4 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless/export.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless/export.py @@ -22,7 +22,7 @@ Usage: ./conv_emformer_transducer_stateless/export.py \ --exp-dir ./conv_emformer_transducer_stateless/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 10 \ --use-averaged-model=True \ @@ -62,7 +62,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from train import add_model_arguments, get_params, get_transducer_model @@ -72,7 +72,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -118,10 +118,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + required=True, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -166,12 +166,12 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-for-ncnn.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-for-ncnn.py index 8fbb02f14..85dbd4661 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-for-ncnn.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-for-ncnn.py @@ -8,7 +8,7 @@ for more details about how to use this file. Usage: ./conv_emformer_transducer_stateless2/export-for-ncnn.py \ --exp-dir ./conv_emformer_transducer_stateless2/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 10 \ --use-averaged-model=True \ @@ -37,7 +37,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train2 import add_model_arguments, get_params, get_transducer_model @@ -48,7 +48,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -94,10 +94,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + required=True, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -217,12 +217,12 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py index ad0b45bd9..cfd365207 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py @@ -18,7 +18,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/pretrained-epoch-30-avg-10-averaged.pt" cd exp @@ -28,7 +27,7 @@ popd 2. Export the model to ONNX ./conv_emformer_transducer_stateless2/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -55,14 +54,14 @@ import logging from pathlib import Path from typing import Dict, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from decoder import Decoder +from emformer import Emformer from scaling_converter import convert_scaled_to_non_scaled from train2 import add_model_arguments, get_params, get_transducer_model -from emformer import Emformer from icefall.checkpoint import ( average_checkpoints, @@ -70,7 +69,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -127,10 +126,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + required=True, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -484,12 +483,12 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export.py index b53426c75..8e5b14903 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export.py @@ -22,7 +22,7 @@ Usage: ./conv_emformer_transducer_stateless2/export.py \ --exp-dir ./conv_emformer_transducer_stateless2/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 10 \ --use-averaged-model=True \ @@ -62,7 +62,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -73,7 +73,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -119,10 +119,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + required=True, + help="Path to the tokens.txt.", ) parser.add_argument( @@ -167,12 +167,12 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py index db92ac696..5d7e2dfcd 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py @@ -28,7 +28,7 @@ popd 2. Export the model to ONNX ./conv_emformer_transducer_stateless2/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ diff --git a/egs/librispeech/ASR/lstm_transducer_stateless/export.py b/egs/librispeech/ASR/lstm_transducer_stateless/export.py index e338342cc..c007220d5 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless/export.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless/export.py @@ -26,7 +26,7 @@ Usage: ./lstm_transducer_stateless/export.py \ --exp-dir ./lstm_transducer_stateless/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 35 \ --avg 10 \ --jit-trace 1 @@ -38,7 +38,7 @@ It will generate 3 files: `encoder_jit_trace.pt`, ./lstm_transducer_stateless/export.py \ --exp-dir ./lstm_transducer_stateless/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 35 \ --avg 10 @@ -79,7 +79,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from scaling_converter import convert_scaled_to_non_scaled @@ -91,7 +91,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -148,10 +148,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -266,12 +266,13 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size, is + # defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/lstm_transducer_stateless/pretrained.py b/egs/librispeech/ASR/lstm_transducer_stateless/pretrained.py index b3a34a9e3..119fcf1fd 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless/pretrained.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./lstm_transducer_stateless/pretrained.py \ --checkpoint ./lstm_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -28,7 +28,7 @@ Usage: (2) beam search ./lstm_transducer_stateless/pretrained.py \ --checkpoint ./lstm_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -37,7 +37,7 @@ Usage: (3) modified beam search ./lstm_transducer_stateless/pretrained.py \ --checkpoint ./lstm_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage: (4) fast beam search ./lstm_transducer_stateless/pretrained.py \ --checkpoint ./lstm_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -66,7 +66,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -79,6 +78,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -95,9 +96,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -214,13 +215,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -275,6 +277,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -286,8 +294,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -296,16 +304,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -326,12 +334,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/export-for-ncnn.py b/egs/librispeech/ASR/lstm_transducer_stateless2/export-for-ncnn.py index 08bfcb204..2b8c92208 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/export-for-ncnn.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/export-for-ncnn.py @@ -29,7 +29,7 @@ popd ./lstm_transducer_stateless2/export-for-ncnn.py \ --exp-dir $repo/exp \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ @@ -49,7 +49,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -60,7 +60,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -106,10 +106,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -221,12 +221,13 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size, is + # defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py index f068f6a0f..89ced388c 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py @@ -613,7 +613,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py index acaff8540..6b6cb893f 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py @@ -28,7 +28,7 @@ popd 2. Export the model to ONNX ./lstm_transducer_stateless2/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -52,8 +52,8 @@ import logging from pathlib import Path from typing import Dict, Optional, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from decoder import Decoder @@ -68,7 +68,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -125,10 +125,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -437,12 +437,13 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size, is + # defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -607,7 +608,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/export.py b/egs/librispeech/ASR/lstm_transducer_stateless2/export.py index 0adc68112..5712da25e 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/export.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/export.py @@ -27,7 +27,7 @@ Usage: ./lstm_transducer_stateless2/export.py \ --exp-dir ./lstm_transducer_stateless2/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 35 \ --avg 10 \ --jit-trace 1 @@ -39,7 +39,7 @@ It will generate 3 files: `encoder_jit_trace.pt`, ./lstm_transducer_stateless2/export.py \ --exp-dir ./lstm_transducer_stateless2/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 35 \ --avg 10 @@ -80,7 +80,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from scaling_converter import convert_scaled_to_non_scaled @@ -92,7 +92,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -149,10 +149,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -267,12 +267,13 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size, is + # defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/pretrained.py b/egs/librispeech/ASR/lstm_transducer_stateless2/pretrained.py index f3f272b9f..5d6d97320 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/pretrained.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./lstm_transducer_stateless2/pretrained.py \ --checkpoint ./lstm_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -28,7 +28,7 @@ Usage: (2) beam search ./lstm_transducer_stateless2/pretrained.py \ --checkpoint ./lstm_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -37,7 +37,7 @@ Usage: (3) modified beam search ./lstm_transducer_stateless2/pretrained.py \ --checkpoint ./lstm_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage: (4) fast beam search ./lstm_transducer_stateless2/pretrained.py \ --checkpoint ./lstm_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -69,7 +69,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -82,6 +81,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -98,9 +99,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -217,13 +218,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -278,6 +280,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -289,8 +297,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -299,16 +307,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -329,12 +337,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/lstm_transducer_stateless3/export.py b/egs/librispeech/ASR/lstm_transducer_stateless3/export.py index a82cad043..21eaa049b 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless3/export.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless3/export.py @@ -26,7 +26,7 @@ Usage: ./lstm_transducer_stateless3/export.py \ --exp-dir ./lstm_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 40 \ --avg 20 \ --jit-trace 1 @@ -38,7 +38,7 @@ It will generate 3 files: `encoder_jit_trace.pt`, ./lstm_transducer_stateless3/export.py \ --exp-dir ./lstm_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 40 \ --avg 20 @@ -79,7 +79,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from scaling_converter import convert_scaled_to_non_scaled @@ -91,7 +91,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -148,10 +148,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to tokens.txt.", ) parser.add_argument( @@ -266,12 +266,13 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size, is + # defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/lstm_transducer_stateless3/pretrained.py b/egs/librispeech/ASR/lstm_transducer_stateless3/pretrained.py index f49e9c518..29a0d4d1a 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless3/pretrained.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless3/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./lstm_transducer_stateless3/pretrained.py \ --checkpoint ./lstm_transducer_stateless3/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -28,7 +28,7 @@ Usage: (2) beam search ./lstm_transducer_stateless3/pretrained.py \ --checkpoint ./lstm_transducer_stateless3/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -37,7 +37,7 @@ Usage: (3) modified beam search ./lstm_transducer_stateless3/pretrained.py \ --checkpoint ./lstm_transducer_stateless3/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -79,6 +79,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -95,9 +97,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -214,13 +216,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -275,6 +278,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -286,8 +295,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -296,16 +305,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -326,12 +335,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/export.py b/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/export.py index 3612a2bfd..ec2c9d580 100755 --- a/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/export.py +++ b/egs/librispeech/ASR/pruned_stateless_emformer_rnnt2/export.py @@ -22,7 +22,7 @@ Usage: ./prunted_stateless_emformer_rnnt/export.py \ --exp-dir ./prunted_stateless_emformer_rnnt/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -48,7 +48,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from train import add_model_arguments, get_params, get_transducer_model @@ -58,7 +58,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -115,10 +115,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -154,13 +154,12 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # and are defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py index a3ebe9d8c..282238c13 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py @@ -508,7 +508,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless/export.py b/egs/librispeech/ASR/pruned_transducer_stateless/export.py index a19f9ab9a..4b20e3a2b 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless/export.py @@ -22,7 +22,7 @@ Usage: ./pruned_transducer_stateless/export.py \ --exp-dir ./pruned_transducer_stateless/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -47,12 +47,12 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from train import add_model_arguments, get_params, get_transducer_model from icefall.checkpoint import average_checkpoints, load_checkpoint -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -87,10 +87,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -135,13 +135,13 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size, is + # defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.streaming_model: assert params.causal_convolution diff --git a/egs/librispeech/ASR/pruned_transducer_stateless/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless/pretrained.py index 2ed1725b4..02f9f1b03 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./pruned_transducer_stateless/pretrained.py \ --checkpoint ./pruned_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -28,7 +28,7 @@ Usage: (2) beam search ./pruned_transducer_stateless/pretrained.py \ --checkpoint ./pruned_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -37,7 +37,7 @@ Usage: (3) modified beam search ./pruned_transducer_stateless/pretrained.py \ --checkpoint ./pruned_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage: (4) fast beam search ./pruned_transducer_stateless/pretrained.py \ --checkpoint ./pruned_transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -66,7 +66,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -79,7 +78,7 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -97,9 +96,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -237,13 +236,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.simulate_streaming: assert ( @@ -314,6 +314,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -325,8 +331,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -335,16 +341,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -365,12 +371,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/export.py b/egs/librispeech/ASR/pruned_transducer_stateless2/export.py index 984caf5f2..e02afa892 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/export.py @@ -22,7 +22,7 @@ Usage: ./pruned_transducer_stateless2/export.py \ --exp-dir ./pruned_transducer_stateless2/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -47,12 +47,12 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from train import add_model_arguments, get_params, get_transducer_model from icefall.checkpoint import average_checkpoints, find_checkpoints, load_checkpoint -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -98,10 +98,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -145,12 +145,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.streaming_model: assert params.causal_convolution diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless2/pretrained.py index 013964720..029f55ba0 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./pruned_transducer_stateless2/pretrained.py \ --checkpoint ./pruned_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -28,7 +28,7 @@ Usage: (2) beam search ./pruned_transducer_stateless2/pretrained.py \ --checkpoint ./pruned_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -37,7 +37,7 @@ Usage: (3) modified beam search ./pruned_transducer_stateless2/pretrained.py \ --checkpoint ./pruned_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage: (4) fast beam search ./pruned_transducer_stateless2/pretrained.py \ --checkpoint ./pruned_transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -66,7 +66,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -79,7 +78,7 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -97,9 +96,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -238,13 +237,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.simulate_streaming: assert ( @@ -315,6 +315,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -326,8 +332,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -336,16 +342,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -366,12 +372,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py index 9645b7801..26dea7e11 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py @@ -28,7 +28,7 @@ popd 2. Export the model to ONNX ./pruned_transducer_stateless3/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 9999 \ --avg 1 \ --exp-dir $repo/exp/ @@ -48,8 +48,8 @@ import logging from pathlib import Path from typing import Dict, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from conformer import Conformer @@ -59,7 +59,7 @@ from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model from icefall.checkpoint import average_checkpoints, find_checkpoints, load_checkpoint -from icefall.utils import setup_logger +from icefall.utils import num_tokens, setup_logger def get_parser(): @@ -105,10 +105,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -393,12 +393,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -518,7 +520,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/export.py b/egs/librispeech/ASR/pruned_transducer_stateless3/export.py index f30c9df6a..925b15646 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/export.py @@ -26,7 +26,7 @@ Usage: ./pruned_transducer_stateless3/export.py \ --exp-dir ./pruned_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 \ --jit 1 @@ -44,7 +44,7 @@ It will also generate 3 other files: `encoder_jit_script.pt`, ./pruned_transducer_stateless3/export.py \ --exp-dir ./pruned_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 \ --jit-trace 1 @@ -56,7 +56,7 @@ It will generates 3 files: `encoder_jit_trace.pt`, ./pruned_transducer_stateless3/export.py \ --exp-dir ./pruned_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -97,14 +97,14 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model from icefall.checkpoint import average_checkpoints, find_checkpoints, load_checkpoint -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -150,10 +150,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", ) parser.add_argument( @@ -342,12 +342,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.streaming_model: assert params.causal_convolution diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless3/pretrained.py index 7c3dfc660..abda4e2d4 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/pretrained.py @@ -20,7 +20,7 @@ You can generate the checkpoint with the following command: ./pruned_transducer_stateless3/export.py \ --exp-dir ./pruned_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -29,7 +29,7 @@ Usage of this script: (1) greedy search ./pruned_transducer_stateless3/pretrained.py \ --checkpoint ./pruned_transducer_stateless3/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -37,7 +37,7 @@ Usage of this script: (2) beam search ./pruned_transducer_stateless3/pretrained.py \ --checkpoint ./pruned_transducer_stateless3/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage of this script: (3) modified beam search ./pruned_transducer_stateless3/pretrained.py \ --checkpoint ./pruned_transducer_stateless3/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -55,7 +55,7 @@ Usage of this script: (4) fast beam search ./pruned_transducer_stateless3/pretrained.py \ --checkpoint ./pruned_transducer_stateless3/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -75,7 +75,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -88,7 +87,7 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -106,9 +105,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -247,13 +246,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.simulate_streaming: assert ( @@ -324,6 +324,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -335,8 +341,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -345,16 +351,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -375,12 +381,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless4/export.py b/egs/librispeech/ASR/pruned_transducer_stateless4/export.py index 8f33f5b05..08d736f52 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless4/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless4/export.py @@ -22,7 +22,7 @@ Usage: ./pruned_transducer_stateless4/export.py \ --exp-dir ./pruned_transducer_stateless4/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -48,7 +48,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -59,7 +59,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -116,10 +116,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -164,12 +164,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.streaming_model: assert params.causal_convolution diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py index 938ff2f16..549fb13c9 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py @@ -28,7 +28,7 @@ popd 2. Export the model to ONNX ./pruned_transducer_stateless5/export-onnx-streaming.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ @@ -58,13 +58,13 @@ import logging from pathlib import Path from typing import Dict, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from conformer import Conformer -from onnxruntime.quantization import QuantType, quantize_dynamic from decoder import Decoder +from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -74,7 +74,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -131,10 +131,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -489,12 +489,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -662,7 +664,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py index 20fd8dff8..fff0fcdd5 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py @@ -28,7 +28,7 @@ popd 2. Export the model to ONNX ./pruned_transducer_stateless5/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ @@ -55,13 +55,13 @@ import logging from pathlib import Path from typing import Dict, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from conformer import Conformer -from onnxruntime.quantization import QuantType, quantize_dynamic from decoder import Decoder +from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -71,7 +71,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -128,10 +128,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -416,12 +416,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -586,7 +588,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/export.py b/egs/librispeech/ASR/pruned_transducer_stateless5/export.py index 54f656859..e5223be26 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/export.py @@ -22,7 +22,7 @@ Usage: ./pruned_transducer_stateless5/export.py \ --exp-dir ./pruned_transducer_stateless5/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -48,7 +48,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -59,7 +59,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -116,10 +116,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -164,12 +164,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for if params.streaming_model: assert params.causal_convolution diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless5/pretrained.py index 74a2210c3..304fa8693 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./pruned_transducer_stateless5/pretrained.py \ --checkpoint ./pruned_transducer_stateless5/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -28,7 +28,7 @@ Usage: (2) beam search ./pruned_transducer_stateless5/pretrained.py \ --checkpoint ./pruned_transducer_stateless5/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -37,7 +37,7 @@ Usage: (3) modified beam search ./pruned_transducer_stateless5/pretrained.py \ --checkpoint ./pruned_transducer_stateless5/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage: (4) fast beam search ./pruned_transducer_stateless5/pretrained.py \ --checkpoint ./pruned_transducer_stateless5/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -66,7 +66,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -79,6 +78,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -95,9 +96,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -214,13 +215,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -275,6 +277,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -286,8 +294,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -296,16 +304,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -326,12 +334,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless6/export.py b/egs/librispeech/ASR/pruned_transducer_stateless6/export.py index 4d0d8326c..38f48b2ed 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless6/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless6/export.py @@ -22,7 +22,7 @@ Usage: ./pruned_transducer_stateless6/export.py \ --exp-dir ./pruned_transducer_stateless6/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -47,12 +47,12 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from train import get_params, get_transducer_model from icefall.checkpoint import average_checkpoints, find_checkpoints, load_checkpoint -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -98,10 +98,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -135,12 +135,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py index d2db92820..11c885f4d 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang) +# Copyright 2023 Xiaomi Corporation (Author: Fangjun Kuang +# Zengrui Jin) """ This script exports a transducer model from PyTorch to ONNX. @@ -18,7 +19,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/pretrained-epoch-30-avg-9.pt" cd exp @@ -28,7 +28,7 @@ popd 2. Export the model to ONNX ./pruned_transducer_stateless7/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -50,8 +50,8 @@ import logging from pathlib import Path from typing import Dict, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from decoder import Decoder @@ -66,7 +66,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -123,10 +123,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -411,12 +410,12 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -581,7 +580,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/export.py b/egs/librispeech/ASR/pruned_transducer_stateless7/export.py index 3e3160e7e..eb4c4d282 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/export.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -# Copyright 2021 Xiaomi Corporation (Author: Fangjun Kuang) +# Copyright 2021 Xiaomi Corporation (Author: Fangjun Kuang +# Zengrui Jin) # # See ../../../../LICENSE for clarification regarding multiple authors # @@ -26,7 +27,7 @@ Usage: ./pruned_transducer_stateless7/export.py \ --exp-dir ./pruned_transducer_stateless7/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -45,7 +46,7 @@ for how to use the exported models outside of icefall. ./pruned_transducer_stateless7/export.py \ --exp-dir ./pruned_transducer_stateless7/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -65,7 +66,7 @@ you can do: --avg 1 \ --max-duration 600 \ --decoding-method greedy_search \ - --bpe-model data/lang_bpe_500/bpe.model + --tokens data/lang_bpe_500/tokens.txt \ Check ./pretrained.py for its usage. @@ -86,7 +87,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from scaling_converter import convert_scaled_to_non_scaled @@ -98,7 +99,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -155,10 +156,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -198,12 +198,12 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -292,7 +292,7 @@ def main(): model.to("cpu") model.eval() - if params.jit is True: + if params.jit: convert_scaled_to_non_scaled(model, inplace=True) # We won't use the forward() method of the model in C++, so just ignore # it here. diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless7/pretrained.py index d05bafcfb..86c922cda 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/pretrained.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 -# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang +# Zengrui Jin) # # See ../../../../LICENSE for clarification regarding multiple authors # @@ -20,7 +21,7 @@ You can generate the checkpoint with the following command: ./pruned_transducer_stateless7/export.py \ --exp-dir ./pruned_transducer_stateless7/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -29,7 +30,7 @@ Usage of this script: (1) greedy search ./pruned_transducer_stateless7/pretrained.py \ --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -37,7 +38,7 @@ Usage of this script: (2) beam search ./pruned_transducer_stateless7/pretrained.py \ --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +47,7 @@ Usage of this script: (3) modified beam search ./pruned_transducer_stateless7/pretrained.py \ --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -55,7 +56,7 @@ Usage of this script: (4) fast beam search ./pruned_transducer_stateless7/pretrained.py \ --checkpoint ./pruned_transducer_stateless7/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens ./data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -75,7 +76,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -88,7 +88,7 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model -from icefall.utils import str2bool +from icefall.utils import num_tokens def get_parser(): @@ -106,9 +106,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -225,13 +225,13 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) - # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + # Load id of the token and the vocab size + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -286,6 +286,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -297,8 +303,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -307,16 +313,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -337,12 +343,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/export.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/export.py index c1607699f..51e62d6a8 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/export.py @@ -26,7 +26,7 @@ Usage: ./pruned_transducer_stateless7_ctc/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -45,7 +45,7 @@ for how to use the exported models outside of icefall. ./pruned_transducer_stateless7_ctc/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -86,7 +86,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -97,7 +97,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -154,10 +154,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -197,12 +197,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained.py index 2f1b1a49f..78e0fa778 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained.py @@ -20,7 +20,7 @@ You can generate the checkpoint with the following command: ./pruned_transducer_stateless7_ctc/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -29,7 +29,7 @@ Usage of this script: (1) greedy search ./pruned_transducer_stateless7_ctc/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -37,7 +37,7 @@ Usage of this script: (2) beam search ./pruned_transducer_stateless7_ctc/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage of this script: (3) modified beam search ./pruned_transducer_stateless7_ctc/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -55,7 +55,7 @@ Usage of this script: (4) fast beam search ./pruned_transducer_stateless7_ctc/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -75,7 +75,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -88,6 +87,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -104,9 +105,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -223,13 +224,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -284,6 +286,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -295,8 +303,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -305,16 +313,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -335,12 +343,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained_ctc.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained_ctc.py index 5d460edb5..904c1deae 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained_ctc.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/pretrained_ctc.py @@ -22,14 +22,14 @@ You can use the following command to get the exported models: ./pruned_transducer_stateless7_ctc/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 Usage of this script: (1) ctc-decoding -./pruned_transducer_stateless7_ctc/jit_pretrained_ctc.py \ +./pruned_transducer_stateless7_ctc/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ --bpe-model data/lang_bpe_500/bpe.model \ --method ctc-decoding \ @@ -38,7 +38,7 @@ Usage of this script: /path/to/bar.wav (2) 1best -./pruned_transducer_stateless7_ctc/jit_pretrained_ctc.py \ +./pruned_transducer_stateless7_ctc/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ --HLG data/lang_bpe_500/HLG.pt \ --words-file data/lang_bpe_500/words.txt \ @@ -48,7 +48,7 @@ Usage of this script: /path/to/bar.wav (3) nbest-rescoring -./bruned_transducer_stateless7_ctc/jit_pretrained_ctc.py \ +./bruned_transducer_stateless7_ctc/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ --HLG data/lang_bpe_500/HLG.pt \ --words-file data/lang_bpe_500/words.txt \ @@ -60,7 +60,7 @@ Usage of this script: (4) whole-lattice-rescoring -./pruned_transducer_stateless7_ctc/jit_pretrained_ctc.py \ +./pruned_transducer_stateless7_ctc/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc/exp/pretrained.pt \ --HLG data/lang_bpe_500/HLG.pt \ --words-file data/lang_bpe_500/words.txt \ diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export.py index 05df8cfff..9f35cf63e 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export.py @@ -26,7 +26,7 @@ Usage: ./pruned_transducer_stateless7_ctc_bs/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc_bs/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 13 \ --jit 1 @@ -45,7 +45,7 @@ for how to use the exported models outside of icefall. ./pruned_transducer_stateless7_ctc_bs/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc_bs/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 13 @@ -86,7 +86,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -97,7 +97,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -154,10 +154,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -197,12 +197,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export_onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export_onnx.py index 630a7f735..d3033b888 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export_onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/export_onnx.py @@ -28,7 +28,7 @@ Usage: ./pruned_transducer_stateless7_ctc_bs/export_onnx.py \ --exp-dir ./pruned_transducer_stateless7_ctc_bs/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 13 \ --onnx 1 @@ -48,7 +48,7 @@ Check `onnx_check.py` for how to use them. (2) Export to ONNX format which can be used in Triton Server ./pruned_transducer_stateless7_ctc_bs/export_onnx.py \ --exp-dir ./pruned_transducer_stateless7_ctc_bs/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 13 \ --onnx-triton 1 @@ -86,9 +86,10 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn +from onnx_wrapper import TritonOnnxDecoder, TritonOnnxJoiner, TritonOnnxLconv from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_params, get_transducer_model @@ -98,8 +99,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool -from onnx_wrapper import TritonOnnxDecoder, TritonOnnxJoiner, TritonOnnxLconv +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -156,10 +156,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -728,12 +728,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained.py index ea0fe9164..5d240cf30 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained.py @@ -20,7 +20,7 @@ You can generate the checkpoint with the following command: ./pruned_transducer_stateless7_ctc_bs/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc_bs/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 13 @@ -29,7 +29,7 @@ Usage of this script: (1) greedy search ./pruned_transducer_stateless7_ctc_bs/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -37,7 +37,7 @@ Usage of this script: (2) beam search ./pruned_transducer_stateless7_ctc_bs/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage of this script: (3) modified beam search ./pruned_transducer_stateless7_ctc_bs/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -55,7 +55,7 @@ Usage of this script: (4) fast beam search ./pruned_transducer_stateless7_ctc_bs/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -75,7 +75,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -88,6 +87,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -104,9 +105,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -223,13 +224,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -284,6 +286,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -295,8 +303,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -305,16 +313,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -335,12 +343,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained_ctc.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained_ctc.py index 412631ba1..914107526 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained_ctc.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/pretrained_ctc.py @@ -22,14 +22,14 @@ You can use the following command to get the exported models: ./pruned_transducer_stateless7_ctc_bs/export.py \ --exp-dir ./pruned_transducer_stateless7_ctc_bs/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 Usage of this script: (1) ctc-decoding -./pruned_transducer_stateless7_ctc_bs/jit_pretrained_ctc.py \ +./pruned_transducer_stateless7_ctc_bs/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ --bpe-model data/lang_bpe_500/bpe.model \ --method ctc-decoding \ @@ -38,7 +38,7 @@ Usage of this script: /path/to/bar.wav (2) 1best -./pruned_transducer_stateless7_ctc_bs/jit_pretrained_ctc.py \ +./pruned_transducer_stateless7_ctc_bs/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ --HLG data/lang_bpe_500/HLG.pt \ --words-file data/lang_bpe_500/words.txt \ @@ -48,7 +48,7 @@ Usage of this script: /path/to/bar.wav (3) nbest-rescoring -./bruned_transducer_stateless7_ctc/jit_pretrained_ctc.py \ +./bruned_transducer_stateless7_ctc/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ --HLG data/lang_bpe_500/HLG.pt \ --words-file data/lang_bpe_500/words.txt \ @@ -60,7 +60,7 @@ Usage of this script: (4) whole-lattice-rescoring -./pruned_transducer_stateless7_ctc_bs/jit_pretrained_ctc.py \ +./pruned_transducer_stateless7_ctc_bs/pretrained_ctc.py \ --checkpoint ./pruned_transducer_stateless7_ctc_bs/exp/pretrained.pt \ --HLG data/lang_bpe_500/HLG.pt \ --words-file data/lang_bpe_500/words.txt \ diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py index e196f8b7d..07de57a86 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn-zh.py @@ -66,6 +66,7 @@ import argparse import logging from pathlib import Path +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train2 import add_model_arguments, get_params, get_transducer_model @@ -76,8 +77,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.lexicon import Lexicon -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -123,10 +123,10 @@ def get_parser(): ) parser.add_argument( - "--lang-dir", + "--tokens", type=str, - default="data/lang_char", - help="The lang dir", + default="data/lang_char/tokens.txt", + help="The tokens.txt file", ) parser.add_argument( @@ -246,9 +246,14 @@ def main(): logging.info(f"device: {device}") - lexicon = Lexicon(params.lang_dir) - params.blank_id = 0 - params.vocab_size = max(lexicon.tokens) + 1 + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + + # Load id of the token and the vocab size + # is defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py index 4a16a97fb..9a6b31268 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-for-ncnn.py @@ -28,7 +28,7 @@ popd 2. Export to ncnn ./pruned_transducer_stateless7_streaming/export-for-ncnn.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --exp-dir $repo/exp \ --use-averaged-model 0 \ --epoch 99 \ @@ -64,7 +64,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train2 import add_model_arguments, get_params, get_transducer_model @@ -75,7 +75,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -121,10 +121,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -244,12 +244,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py index 04d97808d..8653126de 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py @@ -29,7 +29,7 @@ popd 2. Export the model to ONNX ./pruned_transducer_stateless7_streaming/export-onnx-zh.py \ - --lang-dir $repo/data/lang_char_bpe \ + --tokens $repo/data/lang_char_bpe/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -60,6 +60,7 @@ import logging from pathlib import Path from typing import Dict, List, Tuple +import k2 import onnx import torch import torch.nn as nn @@ -76,8 +77,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.lexicon import Lexicon -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -134,10 +134,10 @@ def get_parser(): ) parser.add_argument( - "--lang-dir", + "--tokens", type=str, - default="data/lang_char", - help="The lang dir", + default="data/lang_char/tokens.txt", + help="The tokens.txt file", ) parser.add_argument( @@ -493,9 +493,14 @@ def main(): logging.info(f"device: {device}") - lexicon = Lexicon(params.lang_dir) - params.blank_id = 0 - params.vocab_size = max(lexicon.tokens) + 1 + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + + # Load id of the token and the vocab size + # is defined in local/train_bpe_model.py + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -661,7 +666,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py index e71bcaf29..6f84d79b4 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py @@ -27,7 +27,7 @@ popd 2. Export the model to ONNX ./pruned_transducer_stateless7_streaming/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ @@ -48,8 +48,8 @@ import logging from pathlib import Path from typing import Dict, List, Tuple +import k2 import onnx -import sentencepiece as spm import torch import torch.nn as nn from decoder import Decoder @@ -65,7 +65,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -122,10 +122,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -481,12 +481,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) @@ -652,7 +654,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py index c191b5bcc..59a7eb589 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export.py @@ -139,8 +139,8 @@ import argparse import logging from pathlib import Path +import k2 import onnxruntime -import sentencepiece as spm import torch import torch.nn as nn from onnx_model_wrapper import OnnxStreamingEncoder, TritonOnnxDecoder, TritonOnnxJoiner @@ -154,7 +154,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -211,10 +211,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", ) parser.add_argument( @@ -675,12 +675,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/pretrained.py index fb77fdd42..bc42e8d05 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/pretrained.py @@ -20,7 +20,7 @@ You can generate the checkpoint with the following command: ./pruned_transducer_stateless7_streaming/export.py \ --exp-dir ./pruned_transducer_stateless7_streaming/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -29,7 +29,7 @@ Usage of this script: (1) greedy search ./pruned_transducer_stateless7_streaming/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_streaming/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -37,7 +37,7 @@ Usage of this script: (2) beam search ./pruned_transducer_stateless7_streaming/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_streaming/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage of this script: (3) modified beam search ./pruned_transducer_stateless7_streaming/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_streaming/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -55,7 +55,7 @@ Usage of this script: (4) fast beam search ./pruned_transducer_stateless7_streaming/pretrained.py \ --checkpoint ./pruned_transducer_stateless7_streaming/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -75,7 +75,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -88,7 +87,7 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -106,9 +105,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -225,13 +224,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -286,6 +286,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -297,8 +303,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -307,16 +313,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -337,12 +343,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py index 4a16a97fb..9a6b31268 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/export-for-ncnn.py @@ -28,7 +28,7 @@ popd 2. Export to ncnn ./pruned_transducer_stateless7_streaming/export-for-ncnn.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --exp-dir $repo/exp \ --use-averaged-model 0 \ --epoch 99 \ @@ -64,7 +64,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train2 import add_model_arguments, get_params, get_transducer_model @@ -75,7 +75,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import setup_logger, str2bool +from icefall.utils import num_tokens, setup_logger, str2bool def get_parser(): @@ -121,10 +121,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -244,12 +244,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless8/export.py b/egs/librispeech/ASR/pruned_transducer_stateless8/export.py index d4a228b47..d9697680b 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless8/export.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless8/export.py @@ -26,7 +26,7 @@ Usage: ./pruned_transducer_stateless8/export.py \ --exp-dir ./pruned_transducer_stateless8/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -45,7 +45,7 @@ for how to use the exported models outside of icefall. ./pruned_transducer_stateless8/export.py \ --exp-dir ./pruned_transducer_stateless8/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -86,7 +86,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from scaling_converter import convert_scaled_to_non_scaled @@ -98,7 +98,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -155,10 +155,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -198,12 +198,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless8/pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless8/pretrained.py index 486d9d74e..64b38c9d5 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless8/pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless8/pretrained.py @@ -20,7 +20,7 @@ You can generate the checkpoint with the following command: ./pruned_transducer_stateless8/export.py \ --exp-dir ./pruned_transducer_stateless8/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -29,7 +29,7 @@ Usage of this script: (1) greedy search ./pruned_transducer_stateless8/pretrained.py \ --checkpoint ./pruned_transducer_stateless8/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav @@ -37,7 +37,7 @@ Usage of this script: (2) beam search ./pruned_transducer_stateless8/pretrained.py \ --checkpoint ./pruned_transducer_stateless8/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -46,7 +46,7 @@ Usage of this script: (3) modified beam search ./pruned_transducer_stateless8/pretrained.py \ --checkpoint ./pruned_transducer_stateless8/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -55,7 +55,7 @@ Usage of this script: (4) fast beam search ./pruned_transducer_stateless8/pretrained.py \ --checkpoint ./pruned_transducer_stateless8/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -75,7 +75,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -88,7 +87,7 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import add_model_arguments, get_params, get_transducer_model -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -106,9 +105,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -225,13 +224,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -286,6 +286,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_tokens = fast_beam_search_one_best( @@ -297,8 +303,8 @@ def main(): max_contexts=params.max_contexts, max_states=params.max_states, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "modified_beam_search": hyp_tokens = modified_beam_search( model=model, @@ -307,16 +313,16 @@ def main(): beam=params.beam_size, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) elif params.method == "greedy_search" and params.max_sym_per_frame == 1: hyp_tokens = greedy_search_batch( model=model, encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, ) - for hyp in sp.decode(hyp_tokens): - hyps.append(hyp.split()) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) else: for i in range(num_waves): # fmt: off @@ -337,12 +343,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/transducer/export.py b/egs/librispeech/ASR/transducer/export.py index 6db0272f0..3b9e4a5dc 100755 --- a/egs/librispeech/ASR/transducer/export.py +++ b/egs/librispeech/ASR/transducer/export.py @@ -22,7 +22,7 @@ Usage: ./transducer/export.py \ --exp-dir ./transducer/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 34 \ --avg 11 @@ -46,7 +46,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from conformer import Conformer from decoder import Decoder @@ -55,7 +55,7 @@ from model import Transducer from icefall.checkpoint import average_checkpoints, load_checkpoint from icefall.env import get_env_info -from icefall.utils import AttributeDict, str2bool +from icefall.utils import AttributeDict, num_tokens, str2bool def get_parser(): @@ -90,10 +90,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -191,12 +191,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/transducer/pretrained.py b/egs/librispeech/ASR/transducer/pretrained.py index 511610245..c2413f5de 100755 --- a/egs/librispeech/ASR/transducer/pretrained.py +++ b/egs/librispeech/ASR/transducer/pretrained.py @@ -19,7 +19,7 @@ Usage: ./transducer/pretrained.py \ --checkpoint ./transducer/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ /path/to/foo.wav \ /path/to/bar.wav \ @@ -36,8 +36,8 @@ import logging import math from typing import List +import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import beam_search, greedy_search @@ -48,7 +48,7 @@ from model import Transducer from torch.nn.utils.rnn import pad_sequence from icefall.env import get_env_info -from icefall.utils import AttributeDict +from icefall.utils import AttributeDict, num_tokens def get_parser(): @@ -66,11 +66,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model. - Used only when method is ctc-decoding. - """, + help="Path to tokens.txt.", ) parser.add_argument( @@ -204,12 +202,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -257,6 +257,12 @@ def main(): x=features, x_lens=feature_lengths ) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + num_waves = encoder_out.size(0) hyps = [] for i in range(num_waves): @@ -272,12 +278,11 @@ def main(): else: raise ValueError(f"Unsupported method: {params.method}") - hyps.append(sp.decode(hyp).split()) + hyps.append(token_ids_to_words(hyp)) s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/transducer_stateless/export.py b/egs/librispeech/ASR/transducer_stateless/export.py index 89359f1a4..c397eb171 100755 --- a/egs/librispeech/ASR/transducer_stateless/export.py +++ b/egs/librispeech/ASR/transducer_stateless/export.py @@ -22,7 +22,7 @@ Usage: ./transducer_stateless/export.py \ --exp-dir ./transducer_stateless/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -46,7 +46,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from conformer import Conformer @@ -56,7 +56,7 @@ from model import Transducer from icefall.checkpoint import average_checkpoints, load_checkpoint from icefall.env import get_env_info -from icefall.utils import AttributeDict, str2bool +from icefall.utils import AttributeDict, num_tokens, str2bool def get_parser(): @@ -91,10 +91,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -191,12 +191,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/transducer_stateless/pretrained.py b/egs/librispeech/ASR/transducer_stateless/pretrained.py index 915a6069d..5898dd0f5 100755 --- a/egs/librispeech/ASR/transducer_stateless/pretrained.py +++ b/egs/librispeech/ASR/transducer_stateless/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./transducer_stateless/pretrained.py \ --checkpoint ./transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ --max-sym-per-frame 1 \ /path/to/foo.wav \ @@ -29,7 +29,7 @@ Usage: (2) beam search ./transducer_stateless/pretrained.py \ --checkpoint ./transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -38,7 +38,7 @@ Usage: (3) modified beam search ./transducer_stateless/pretrained.py \ --checkpoint ./transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -47,7 +47,7 @@ Usage: (4) fast beam search ./transducer_stateless/pretrained.py \ --checkpoint ./transducer_stateless/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -67,7 +67,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -80,6 +79,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -96,9 +97,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -213,12 +214,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -273,6 +276,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_list = fast_beam_search_one_best( @@ -318,12 +327,11 @@ def main(): raise ValueError(f"Unsupported method: {params.method}") hyp_list.append(hyp) - hyps = [sp.decode(hyp).split() for hyp in hyp_list] + hyps = [token_ids_to_words(hyp) for hyp in hyp_list] s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/transducer_stateless2/export.py b/egs/librispeech/ASR/transducer_stateless2/export.py index d33d02642..f4b6f5554 100755 --- a/egs/librispeech/ASR/transducer_stateless2/export.py +++ b/egs/librispeech/ASR/transducer_stateless2/export.py @@ -22,7 +22,7 @@ Usage: ./transducer_stateless2/export.py \ --exp-dir ./transducer_stateless2/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -46,12 +46,12 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from train import get_params, get_transducer_model from icefall.checkpoint import average_checkpoints, load_checkpoint -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -86,10 +86,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", ) parser.add_argument( @@ -123,12 +123,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/transducer_stateless2/pretrained.py b/egs/librispeech/ASR/transducer_stateless2/pretrained.py index 0738f30c0..b69b347ef 100755 --- a/egs/librispeech/ASR/transducer_stateless2/pretrained.py +++ b/egs/librispeech/ASR/transducer_stateless2/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./transducer_stateless2/pretrained.py \ --checkpoint ./transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ --max-sym-per-frame 1 \ /path/to/foo.wav \ @@ -29,7 +29,7 @@ Usage: (2) beam search ./transducer_stateless2/pretrained.py \ --checkpoint ./transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -38,7 +38,7 @@ Usage: (3) modified beam search ./transducer_stateless2/pretrained.py \ --checkpoint ./transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -47,7 +47,7 @@ Usage: (4) fast beam search ./transducer_stateless2/pretrained.py \ --checkpoint ./transducer_stateless2/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -67,7 +67,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -80,6 +79,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -96,9 +97,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -213,12 +214,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -273,6 +276,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_list = fast_beam_search_one_best( @@ -318,12 +327,11 @@ def main(): raise ValueError(f"Unsupported method: {params.method}") hyp_list.append(hyp) - hyps = [sp.decode(hyp).split() for hyp in hyp_list] + hyps = [token_ids_to_words(hyp) for hyp in hyp_list] s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/transducer_stateless_multi_datasets/export.py b/egs/librispeech/ASR/transducer_stateless_multi_datasets/export.py index 3735ef452..6d31dfe34 100755 --- a/egs/librispeech/ASR/transducer_stateless_multi_datasets/export.py +++ b/egs/librispeech/ASR/transducer_stateless_multi_datasets/export.py @@ -22,7 +22,7 @@ Usage: ./transducer_stateless_multi_datasets/export.py \ --exp-dir ./transducer_stateless_multi_datasets/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -47,7 +47,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch import torch.nn as nn from conformer import Conformer @@ -57,7 +57,7 @@ from model import Transducer from icefall.checkpoint import average_checkpoints, load_checkpoint from icefall.env import get_env_info -from icefall.utils import AttributeDict, str2bool +from icefall.utils import AttributeDict, num_tokens, str2bool def get_parser(): @@ -92,10 +92,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -192,12 +192,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/transducer_stateless_multi_datasets/pretrained.py b/egs/librispeech/ASR/transducer_stateless_multi_datasets/pretrained.py index 8c7726367..4f29d6f1f 100755 --- a/egs/librispeech/ASR/transducer_stateless_multi_datasets/pretrained.py +++ b/egs/librispeech/ASR/transducer_stateless_multi_datasets/pretrained.py @@ -20,7 +20,7 @@ Usage: (1) greedy search ./transducer_stateless_multi_datasets/pretrained.py \ --checkpoint ./transducer_stateless_multi_datasets/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method greedy_search \ --max-sym-per-frame 1 \ /path/to/foo.wav \ @@ -29,7 +29,7 @@ Usage: (2) beam search ./transducer_stateless_multi_datasets/pretrained.py \ --checkpoint ./transducer_stateless_multi_datasets/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -38,7 +38,7 @@ Usage: (3) modified beam search ./transducer_stateless_multi_datasets/pretrained.py \ --checkpoint ./transducer_stateless_multi_datasets/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method modified_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -47,7 +47,7 @@ Usage: (4) fast beam search ./transducer_stateless_multi_datasets/pretrained.py \ --checkpoint ./transducer_stateless_multi_datasets/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method fast_beam_search \ --beam-size 4 \ /path/to/foo.wav \ @@ -67,7 +67,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from beam_search import ( @@ -80,6 +79,8 @@ from beam_search import ( from torch.nn.utils.rnn import pad_sequence from train import get_params, get_transducer_model +from icefall.utils import num_tokens + def get_parser(): parser = argparse.ArgumentParser( @@ -96,9 +97,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -213,12 +214,14 @@ def main(): params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -273,6 +276,12 @@ def main(): msg += f" with beam size {params.beam_size}" logging.info(msg) + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + if params.method == "fast_beam_search": decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) hyp_list = fast_beam_search_one_best( @@ -318,12 +327,11 @@ def main(): raise ValueError(f"Unsupported method: {params.method}") hyp_list.append(hyp) - hyps = [sp.decode(hyp).split() for hyp in hyp_list] + hyps = [token_ids_to_words(hyp) for hyp in hyp_list] s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py index 3eb06f68c..a951aeef3 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py +++ b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py @@ -19,7 +19,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/pretrained.pt" cd exp @@ -74,7 +73,6 @@ import onnx import torch import torch.nn as nn from decoder import Decoder -from export import num_tokens from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_model, get_params @@ -86,7 +84,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): diff --git a/egs/librispeech/ASR/zipformer/export-onnx.py b/egs/librispeech/ASR/zipformer/export-onnx.py index 724fdd2a6..e0d664009 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx.py +++ b/egs/librispeech/ASR/zipformer/export-onnx.py @@ -19,7 +19,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/pretrained.pt" cd exp @@ -71,7 +70,6 @@ import onnx import torch import torch.nn as nn from decoder import Decoder -from export import num_tokens from onnxruntime.quantization import QuantType, quantize_dynamic from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_model, get_params @@ -83,7 +81,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import make_pad_mask, str2bool +from icefall.utils import make_pad_mask, num_tokens, str2bool def get_parser(): diff --git a/egs/librispeech/ASR/zipformer/export.py b/egs/librispeech/ASR/zipformer/export.py index 4a48d5bad..2b8d1aaf3 100755 --- a/egs/librispeech/ASR/zipformer/export.py +++ b/egs/librispeech/ASR/zipformer/export.py @@ -160,7 +160,6 @@ with the following commands: import argparse import logging -import re from pathlib import Path from typing import List, Tuple @@ -176,27 +175,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import make_pad_mask, str2bool - - -def num_tokens( - token_table: k2.SymbolTable, disambig_pattern: str = re.compile(r"^#\d+$") -) -> int: - """Return the number of tokens excluding those from - disambiguation symbols. - - Caution: - 0 is not a token ID so it is excluded from the return value. - """ - symbols = token_table.symbols - ans = [] - for s in symbols: - if not disambig_pattern.match(s): - ans.append(token_table[s]) - num_tokens = len(ans) - if 0 in ans: - num_tokens -= 1 - return num_tokens +from icefall.utils import make_pad_mask, num_tokens, str2bool def get_parser(): @@ -487,6 +466,8 @@ def main(): device=device, ) ) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) else: assert params.avg > 0, params.avg start = params.epoch - params.avg diff --git a/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py index 904d8cd76..660a4bfc6 100755 --- a/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py +++ b/egs/librispeech/ASR/zipformer/jit_pretrained_ctc.py @@ -410,10 +410,20 @@ def main(): raise ValueError(f"Unsupported decoding method: {params.method}") s = "\n" - for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - words = words.replace("▁", " ").strip() - s += f"{filename}:\n{words}\n\n" + if params.method == "ctc-decoding": + for filename, hyp in zip(params.sound_files, hyps): + words = "".join(hyp) + words = words.replace("▁", " ").strip() + s += f"{filename}:\n{words}\n\n" + elif params.method in [ + "1best", + "nbest-rescoring", + "whole-lattice-rescoring", + ]: + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + words = words.replace("▁", " ").strip() + s += f"{filename}:\n{words}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/zipformer/onnx_check.py b/egs/librispeech/ASR/zipformer/onnx_check.py index b38b875d0..93bd3a211 100755 --- a/egs/librispeech/ASR/zipformer/onnx_check.py +++ b/egs/librispeech/ASR/zipformer/onnx_check.py @@ -33,7 +33,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/pretrained.pt" cd exp diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py index 2ce4506a8..500b2cd09 100755 --- a/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py @@ -19,7 +19,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/bpe.model" git lfs pull --include "exp/pretrained.pt" cd exp @@ -29,7 +28,7 @@ popd 2. Export the model to ONNX ./zipformer/export-onnx-streaming.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained.py b/egs/librispeech/ASR/zipformer/onnx_pretrained.py index e8a521460..032b07721 100755 --- a/egs/librispeech/ASR/zipformer/onnx_pretrained.py +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained.py @@ -31,7 +31,6 @@ GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) pushd $repo -git lfs pull --include "data/lang_bpe_500/tokens.txt" git lfs pull --include "exp/pretrained.pt" cd exp diff --git a/egs/librispeech/ASR/zipformer/pretrained_ctc.py b/egs/librispeech/ASR/zipformer/pretrained_ctc.py index be239e9c3..9dff2e6fc 100755 --- a/egs/librispeech/ASR/zipformer/pretrained_ctc.py +++ b/egs/librispeech/ASR/zipformer/pretrained_ctc.py @@ -274,7 +274,7 @@ def main(): params.update(vars(args)) token_table = k2.SymbolTable.from_file(params.tokens) - params.vocab_size = num_tokens(token_table) + params.vocab_size = num_tokens(token_table) + 1 # +1 for blank params.blank_id = token_table[""] assert params.blank_id == 0 @@ -429,10 +429,20 @@ def main(): raise ValueError(f"Unsupported decoding method: {params.method}") s = "\n" - for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - words = words.replace("▁", " ").strip() - s += f"{filename}:\n{words}\n\n" + if params.method == "ctc-decoding": + for filename, hyp in zip(params.sound_files, hyps): + words = "".join(hyp) + words = words.replace("▁", " ").strip() + s += f"{filename}:\n{words}\n\n" + elif params.method in [ + "1best", + "nbest-rescoring", + "whole-lattice-rescoring", + ]: + for filename, hyp in zip(params.sound_files, hyps): + words = " ".join(hyp) + words = words.replace("▁", " ").strip() + s += f"{filename}:\n{words}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/librispeech/ASR/zipformer_mmi/export.py b/egs/librispeech/ASR/zipformer_mmi/export.py index 0af7bd367..1aec56420 100755 --- a/egs/librispeech/ASR/zipformer_mmi/export.py +++ b/egs/librispeech/ASR/zipformer_mmi/export.py @@ -26,7 +26,7 @@ Usage: ./zipformer_mmi/export.py \ --exp-dir ./zipformer_mmi/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 \ --jit 1 @@ -45,7 +45,7 @@ for how to use the exported models outside of icefall. ./zipformer_mmi/export.py \ --exp-dir ./zipformer_mmi/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -86,7 +86,7 @@ import argparse import logging from pathlib import Path -import sentencepiece as spm +import k2 import torch from scaling_converter import convert_scaled_to_non_scaled from train import add_model_arguments, get_ctc_model, get_params @@ -97,7 +97,7 @@ from icefall.checkpoint import ( find_checkpoints, load_checkpoint, ) -from icefall.utils import str2bool +from icefall.utils import num_tokens, str2bool def get_parser(): @@ -154,10 +154,10 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - default="data/lang_bpe_500/bpe.model", - help="Path to the BPE model", + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt.", ) parser.add_argument( @@ -190,12 +190,14 @@ def main(): logging.info(f"device: {device}") - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(params) diff --git a/egs/librispeech/ASR/zipformer_mmi/pretrained.py b/egs/librispeech/ASR/zipformer_mmi/pretrained.py index 0e7fd0daf..3ba4da5dd 100755 --- a/egs/librispeech/ASR/zipformer_mmi/pretrained.py +++ b/egs/librispeech/ASR/zipformer_mmi/pretrained.py @@ -21,7 +21,7 @@ You can generate the checkpoint with the following command: ./zipformer_mmi/export.py \ --exp-dir ./zipformer_mmi/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -30,14 +30,14 @@ Usage of this script: (1) 1best ./zipformer_mmi/pretrained.py \ --checkpoint ./zipformer_mmi/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --method 1best \ /path/to/foo.wav \ /path/to/bar.wav (2) nbest ./zipformer_mmi/pretrained.py \ --checkpoint ./zipformer_mmi/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --nbest-scale 1.2 \ --method nbest \ /path/to/foo.wav \ @@ -45,7 +45,7 @@ Usage of this script: (3) nbest-rescoring-LG ./zipformer_mmi/pretrained.py \ --checkpoint ./zipformer_mmi/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --nbest-scale 1.2 \ --method nbest-rescoring-LG \ /path/to/foo.wav \ @@ -53,7 +53,7 @@ Usage of this script: (4) nbest-rescoring-3-gram ./zipformer_mmi/pretrained.py \ --checkpoint ./zipformer_mmi/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --nbest-scale 1.2 \ --method nbest-rescoring-3-gram \ /path/to/foo.wav \ @@ -61,7 +61,7 @@ Usage of this script: (5) nbest-rescoring-4-gram ./zipformer_mmi/pretrained.py \ --checkpoint ./zipformer_mmi/exp/pretrained.pt \ - --bpe-model ./data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --nbest-scale 1.2 \ --method nbest-rescoring-4-gram \ /path/to/foo.wav \ @@ -83,7 +83,6 @@ from typing import List import k2 import kaldifeat -import sentencepiece as spm import torch import torchaudio from decode import get_decoding_params @@ -97,7 +96,7 @@ from icefall.decode import ( one_best_decoding, ) from icefall.mmi_graph_compiler import MmiTrainingGraphCompiler -from icefall.utils import get_texts +from icefall.utils import get_texts, num_tokens def get_parser(): @@ -115,9 +114,9 @@ def get_parser(): ) parser.add_argument( - "--bpe-model", + "--tokens", type=str, - help="""Path to bpe.model.""", + help="""Path to tokens.txt.""", ) parser.add_argument( @@ -247,13 +246,14 @@ def main(): params.update(get_decoding_params()) params.update(vars(args)) - sp = spm.SentencePieceProcessor() - sp.load(params.bpe_model) + # Load tokens.txt here + token_table = k2.SymbolTable.from_file(params.tokens) + # Load id of the token and the vocab size # is defined in local/train_bpe_model.py - params.blank_id = sp.piece_to_id("") - params.unk_id = sp.piece_to_id("") - params.vocab_size = sp.get_piece_size() + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 # +1 for logging.info(f"{params}") @@ -298,8 +298,6 @@ def main(): features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) feature_lengths = torch.tensor(feature_lengths, device=device) - bpe_model = spm.SentencePieceProcessor() - bpe_model.load(str(params.lang_dir / "bpe.model")) mmi_graph_compiler = MmiTrainingGraphCompiler( params.lang_dir, uniq_filename="lexicon.txt", @@ -313,6 +311,12 @@ def main(): if not hasattr(HP, "lm_scores"): HP.lm_scores = HP.scores.clone() + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + method = params.method assert method in ( "1best", @@ -390,14 +394,11 @@ def main(): # # token_ids is a lit-of-list of IDs token_ids = get_texts(best_path) - # hyps is a list of str, e.g., ['xxx yyy zzz', ...] - hyps = bpe_model.decode(token_ids) - # hyps is a list of list of str, e.g., [['xxx', 'yyy', 'zzz'], ... ] - hyps = [s.split() for s in hyps] + hyps = [token_ids_to_words(ids) for ids in token_ids] + s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) - s += f"{filename}:\n{words}\n\n" + s += f"{filename}:\n{hyp}\n\n" logging.info(s) logging.info("Decoding Done") diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py index fad66986b..760fad974 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py @@ -498,7 +498,7 @@ def main(): quantize_dynamic( model_input=decoder_filename, model_output=decoder_filename_int8, - op_types_to_quantize=["MatMul"], + op_types_to_quantize=["MatMul", "Gather"], weight_type=QuantType.QInt8, ) diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/pretrained.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/pretrained.py index bc499f3dd..c3d67ad92 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/pretrained.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/pretrained.py @@ -320,7 +320,7 @@ def main(): s = "\n" for filename, hyp in zip(params.sound_files, hyps): - words = " ".join(hyp) + words = "".join(hyp) s += f"{filename}:\n{words}\n\n" logging.info(s) diff --git a/icefall/utils.py b/icefall/utils.py index 0feff9dc8..b01cd2770 100644 --- a/icefall/utils.py +++ b/icefall/utils.py @@ -2060,3 +2060,23 @@ def symlink_or_copy(exp_dir: Path, src: str, dst: str): except OSError: copyfile(src=exp_dir / src, dst=exp_dir / dst) os.close(dir_fd) + + +def num_tokens( + token_table: k2.SymbolTable, disambig_pattern: str = re.compile(r"^#\d+$") +) -> int: + """Return the number of tokens excluding those from + disambiguation symbols. + + Caution: + 0 is not a token ID so it is excluded from the return value. + """ + symbols = token_table.symbols + ans = [] + for s in symbols: + if not disambig_pattern.match(s): + ans.append(token_table[s]) + num_tokens = len(ans) + if 0 in ans: + num_tokens -= 1 + return num_tokens From dfccadc6b6551696e2dfff787f3ec102e346d4cd Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Sat, 12 Aug 2023 16:59:06 +0800 Subject: [PATCH 068/100] Fix a typo in export_onnx.py for yesno (#1213) --- egs/yesno/ASR/tdnn/export_onnx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egs/yesno/ASR/tdnn/export_onnx.py b/egs/yesno/ASR/tdnn/export_onnx.py index 9b2a56d59..2436ca81b 100755 --- a/egs/yesno/ASR/tdnn/export_onnx.py +++ b/egs/yesno/ASR/tdnn/export_onnx.py @@ -126,7 +126,7 @@ def main(): logging.info(f"Saved to {onnx_filename}") meta_data = { - "model_type": "tdnn_lstm", + "model_type": "tdnn", "version": "1", "model_author": "k2-fsa", "comment": "non-streaming tdnn for the yesno recipe", From b0e8a40c8932d82d356b8a2ad4948331eae9749e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelasko?= Date: Sat, 12 Aug 2023 21:50:59 -0400 Subject: [PATCH 069/100] Speed up yesno training to finish in ~10s on CPU (#1215) --- egs/yesno/ASR/tdnn/asr_datamodule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/egs/yesno/ASR/tdnn/asr_datamodule.py b/egs/yesno/ASR/tdnn/asr_datamodule.py index 3c1682fa1..ada8c1a6c 100644 --- a/egs/yesno/ASR/tdnn/asr_datamodule.py +++ b/egs/yesno/ASR/tdnn/asr_datamodule.py @@ -209,7 +209,7 @@ class YesNoAsrDataModule(DataModule): sampler=train_sampler, batch_size=None, num_workers=self.args.num_workers, - persistent_workers=False, + persistent_workers=True, ) return train_dl @@ -236,6 +236,7 @@ class YesNoAsrDataModule(DataModule): batch_size=None, sampler=sampler, num_workers=self.args.num_workers, + persistent_workers=True, ) return test_dl From 3b5645f5944393121e52739d5b9d5ef43a7e7a0f Mon Sep 17 00:00:00 2001 From: zr_jin Date: Sun, 13 Aug 2023 12:37:08 +0800 Subject: [PATCH 070/100] doc updated (#1214) --- docs/source/model-export/export-model-state-dict.rst | 4 ++-- docs/source/model-export/export-ncnn-conv-emformer.rst | 3 +-- docs/source/model-export/export-ncnn-lstm.rst | 2 +- docs/source/model-export/export-ncnn-zipformer.rst | 3 +-- docs/source/model-export/export-onnx.rst | 2 +- docs/source/model-export/export-with-torch-jit-script.rst | 2 +- docs/source/model-export/export-with-torch-jit-trace.rst | 2 +- 7 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/source/model-export/export-model-state-dict.rst b/docs/source/model-export/export-model-state-dict.rst index c3bbd5708..5596bb7a6 100644 --- a/docs/source/model-export/export-model-state-dict.rst +++ b/docs/source/model-export/export-model-state-dict.rst @@ -41,7 +41,7 @@ as an example. ./pruned_transducer_stateless3/export.py \ --exp-dir ./pruned_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 20 \ --avg 10 @@ -78,7 +78,7 @@ In each recipe, there is also a file ``pretrained.py``, which can use ./pruned_transducer_stateless3/pretrained.py \ --checkpoint ./icefall-asr-librispeech-pruned-transducer-stateless3-2022-05-13/exp/pretrained-iter-1224000-avg-14.pt \ - --bpe-model ./icefall-asr-librispeech-pruned-transducer-stateless3-2022-05-13/data/lang_bpe_500/bpe.model \ + --tokens ./icefall-asr-librispeech-pruned-transducer-stateless3-2022-05-13/data/lang_bpe_500/tokens.txt \ --method greedy_search \ ./icefall-asr-librispeech-pruned-transducer-stateless3-2022-05-13/test_wavs/1089-134686-0001.wav \ ./icefall-asr-librispeech-pruned-transducer-stateless3-2022-05-13/test_wavs/1221-135766-0001.wav \ diff --git a/docs/source/model-export/export-ncnn-conv-emformer.rst b/docs/source/model-export/export-ncnn-conv-emformer.rst index 12b370143..4f5535d83 100644 --- a/docs/source/model-export/export-ncnn-conv-emformer.rst +++ b/docs/source/model-export/export-ncnn-conv-emformer.rst @@ -153,11 +153,10 @@ Next, we use the following code to export our model: ./conv_emformer_transducer_stateless2/export-for-ncnn.py \ --exp-dir $dir/exp \ - --bpe-model $dir/data/lang_bpe_500/bpe.model \ + --tokens $dir/data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 1 \ --use-averaged-model 0 \ - \ --num-encoder-layers 12 \ --chunk-length 32 \ --cnn-module-kernel 31 \ diff --git a/docs/source/model-export/export-ncnn-lstm.rst b/docs/source/model-export/export-ncnn-lstm.rst index 8e6dc7466..310c3d8e4 100644 --- a/docs/source/model-export/export-ncnn-lstm.rst +++ b/docs/source/model-export/export-ncnn-lstm.rst @@ -73,7 +73,7 @@ Next, we use the following code to export our model: ./lstm_transducer_stateless2/export-for-ncnn.py \ --exp-dir $dir/exp \ - --bpe-model $dir/data/lang_bpe_500/bpe.model \ + --tokens $dir/data/lang_bpe_500/tokens.txt \ --epoch 99 \ --avg 1 \ --use-averaged-model 0 \ diff --git a/docs/source/model-export/export-ncnn-zipformer.rst b/docs/source/model-export/export-ncnn-zipformer.rst index 8440d26b7..a5845b0e4 100644 --- a/docs/source/model-export/export-ncnn-zipformer.rst +++ b/docs/source/model-export/export-ncnn-zipformer.rst @@ -72,12 +72,11 @@ Next, we use the following code to export our model: dir=./icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 ./pruned_transducer_stateless7_streaming/export-for-ncnn.py \ - --bpe-model $dir/data/lang_bpe_500/bpe.model \ + --tokens $dir/data/lang_bpe_500/tokens.txt \ --exp-dir $dir/exp \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ - \ --decode-chunk-len 32 \ --num-left-chunks 4 \ --num-encoder-layers "2,4,3,2,4" \ diff --git a/docs/source/model-export/export-onnx.rst b/docs/source/model-export/export-onnx.rst index fb952abb7..d95f2acfe 100644 --- a/docs/source/model-export/export-onnx.rst +++ b/docs/source/model-export/export-onnx.rst @@ -71,7 +71,7 @@ Export the model to ONNX .. code-block:: bash ./pruned_transducer_stateless7_streaming/export-onnx.py \ - --bpe-model $repo/data/lang_bpe_500/bpe.model \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ --use-averaged-model 0 \ --epoch 99 \ --avg 1 \ diff --git a/docs/source/model-export/export-with-torch-jit-script.rst b/docs/source/model-export/export-with-torch-jit-script.rst index efd7dc2e1..31c8f0bf5 100644 --- a/docs/source/model-export/export-with-torch-jit-script.rst +++ b/docs/source/model-export/export-with-torch-jit-script.rst @@ -32,7 +32,7 @@ as an example in the following. ./pruned_transducer_stateless3/export.py \ --exp-dir ./pruned_transducer_stateless3/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch $epoch \ --avg $avg \ --jit 1 diff --git a/docs/source/model-export/export-with-torch-jit-trace.rst b/docs/source/model-export/export-with-torch-jit-trace.rst index 506459909..be7876ab5 100644 --- a/docs/source/model-export/export-with-torch-jit-trace.rst +++ b/docs/source/model-export/export-with-torch-jit-trace.rst @@ -33,7 +33,7 @@ as an example in the following. ./lstm_transducer_stateless2/export.py \ --exp-dir ./lstm_transducer_stateless2/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --iter $iter \ --avg $avg \ --jit-trace 1 From 9a47c08d085f00b63ce2d7c6d0fee16812691ed7 Mon Sep 17 00:00:00 2001 From: Erwan Zerhouni <61225408+ezerhouni@users.noreply.github.com> Date: Mon, 14 Aug 2023 16:10:50 +0200 Subject: [PATCH 071/100] Update padding modified beam search (#1217) --- .../beam_search.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index fd59d4b7f..97e259b40 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -1008,7 +1008,7 @@ def modified_beam_search( for i in range(N): B[i].add( Hypothesis( - ys=[blank_id] * context_size, + ys=[-1] * (context_size - 1) + [blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), context_state=None if context_graph is None else context_graph.root, timestamp=[], @@ -1217,7 +1217,7 @@ def modified_beam_search_lm_rescore( for i in range(N): B[i].add( Hypothesis( - ys=[blank_id] * context_size, + ys=[-1] * (context_size - 1) + [blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), timestamp=[], ) @@ -1417,7 +1417,7 @@ def modified_beam_search_lm_rescore_LODR( for i in range(N): B[i].add( Hypothesis( - ys=[blank_id] * context_size, + ys=[-1] * (context_size - 1) + [blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), timestamp=[], ) @@ -1617,7 +1617,7 @@ def _deprecated_modified_beam_search( B = HypothesisList() B.add( Hypothesis( - ys=[blank_id] * context_size, + ys=[-1] * (context_size - 1) + [blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), timestamp=[], ) @@ -1753,7 +1753,11 @@ def beam_search( t = 0 B = HypothesisList() - B.add(Hypothesis(ys=[blank_id] * context_size, log_prob=0.0, timestamp=[])) + B.add( + Hypothesis( + ys=[-1] * (context_size - 1) + [blank_id], log_prob=0.0, timestamp=[] + ) + ) max_sym_per_utt = 20000 @@ -2265,7 +2269,7 @@ def modified_beam_search_ngram_rescoring( for i in range(N): B[i].add( Hypothesis( - ys=[blank_id] * context_size, + ys=[-1] * (context_size - 1) + [blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), state_cost=NgramLmStateCost(ngram_lm), ) @@ -2446,7 +2450,7 @@ def modified_beam_search_LODR( for i in range(N): B[i].add( Hypothesis( - ys=[blank_id] * context_size, + ys=[-1] * (context_size - 1) + [blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), state=init_states, # state of the NN LM lm_score=init_score.reshape(-1), @@ -2709,7 +2713,7 @@ def modified_beam_search_lm_shallow_fusion( for i in range(N): B[i].add( Hypothesis( - ys=[blank_id] * context_size, + ys=[-1] * (context_size - 1) + [blank_id], log_prob=torch.zeros(1, dtype=torch.float32, device=device), state=init_states, lm_score=init_score.reshape(-1), From fc2df07841b3edbd7bffddfcc2e016515aa75247 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 16 Aug 2023 22:32:41 +0800 Subject: [PATCH 072/100] Add icefall tutorials for dummies. (#1220) --- docs/source/conf.py | 3 + docs/source/for-dummies/data-preparation.rst | 180 ++++++++++ docs/source/for-dummies/decoding.rst | 39 +++ docs/source/for-dummies/environment-setup.rst | 121 +++++++ docs/source/for-dummies/index.rst | 34 ++ docs/source/for-dummies/model-export.rst | 310 ++++++++++++++++++ docs/source/for-dummies/training.rst | 39 +++ docs/source/index.rst | 1 + egs/yesno/ASR/tdnn/onnx_pretrained.py | 1 + 9 files changed, 728 insertions(+) create mode 100644 docs/source/for-dummies/data-preparation.rst create mode 100644 docs/source/for-dummies/decoding.rst create mode 100644 docs/source/for-dummies/environment-setup.rst create mode 100644 docs/source/for-dummies/index.rst create mode 100644 docs/source/for-dummies/model-export.rst create mode 100644 docs/source/for-dummies/training.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index bf231e3c1..5a534e126 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -95,4 +95,7 @@ rst_epilog = """ .. _k2: https://github.com/k2-fsa/k2 .. _lhotse: https://github.com/lhotse-speech/lhotse .. _yesno: https://www.openslr.org/1/ +.. _Next-gen Kaldi: https://github.com/k2-fsa +.. _Kaldi: https://github.com/kaldi-asr/kaldi +.. _lilcom: https://github.com/danpovey/lilcom """ diff --git a/docs/source/for-dummies/data-preparation.rst b/docs/source/for-dummies/data-preparation.rst new file mode 100644 index 000000000..f03d44e79 --- /dev/null +++ b/docs/source/for-dummies/data-preparation.rst @@ -0,0 +1,180 @@ +.. _dummies_tutorial_data_preparation: + +Data Preparation +================ + +After :ref:`dummies_tutorial_environment_setup`, we can start preparing the +data for training and decoding. + +The first step is to prepare the data for training. We have already provided +`prepare.sh `_ +that would prepare everything required for training. + +.. code-block:: + + cd /tmp/icefall + export PYTHONPATH=/tmp/icefall:$PYTHONPATH + cd egs/yesno/ASR + + ./prepare.sh + +Note that in each recipe from `icefall`_, there exists a file ``prepare.sh``, +which you should run before you run anything else. + +That is all you need for data preparation. + +For the more curious +-------------------- + +If you are wondering how to prepare your own dataset, please refer to the following +URLs for more details: + + - ``_ + + It contains recipes for a variety of dataset. If you want to add your own + dataset, please read recipes in this folder first. + + - ``_ + + The `yesno`_ recipe in `lhotse`_. + +If you already have a `Kaldi`_ dataset directory, which contains files like +``wav.scp``, ``feats.scp``, then you can refer to ``_. + +A quick look to the generated files +----------------------------------- + +``./prepare.sh`` puts generated files into two directories: + + - ``download`` + - ``data`` + +download +^^^^^^^^ + +The ``download`` directory contains downloaded dataset files: + +.. code-block:: bas + + tree -L 1 ./download/ + + ./download/ + |-- waves_yesno + `-- waves_yesno.tar.gz + +.. hint:: + + Please refer to ``_ + for how the data is downloaded and extracted. + +data +^^^^ + +.. code-block:: bash + + tree ./data/ + + ./data/ + |-- fbank + | |-- yesno_cuts_test.jsonl.gz + | |-- yesno_cuts_train.jsonl.gz + | |-- yesno_feats_test.lca + | `-- yesno_feats_train.lca + |-- lang_phone + | |-- HLG.pt + | |-- L.pt + | |-- L_disambig.pt + | |-- Linv.pt + | |-- lexicon.txt + | |-- lexicon_disambig.txt + | |-- tokens.txt + | `-- words.txt + |-- lm + | |-- G.arpa + | `-- G.fst.txt + `-- manifests + |-- yesno_recordings_test.jsonl.gz + |-- yesno_recordings_train.jsonl.gz + |-- yesno_supervisions_test.jsonl.gz + `-- yesno_supervisions_train.jsonl.gz + + 4 directories, 18 files + +**data/manifests**: + + This directory contains manifests. They are used to generate files in + ``data/fbank``. + + To give you an idea of what it contains, we examine the first few lines of + the manifests related to the ``train`` dataset. + + .. code-block:: bash + + cd data/manifests + gunzip -c yesno_recordings_train.jsonl.gz | head -n 3 + + The output is given below: + + .. code-block:: bash + + {"id": "0_0_0_0_1_1_1_1", "sources": [{"type": "file", "channels": [0], "source": "/tmp/icefall/egs/yesno/ASR/download/waves_yesno/0_0_0_0_1_1_1_1.wav"}], "sampling_rate": 8000, "num_samples": 50800, "duration": 6.35, "channel_ids": [0]} + {"id": "0_0_0_1_0_1_1_0", "sources": [{"type": "file", "channels": [0], "source": "/tmp/icefall/egs/yesno/ASR/download/waves_yesno/0_0_0_1_0_1_1_0.wav"}], "sampling_rate": 8000, "num_samples": 48880, "duration": 6.11, "channel_ids": [0]} + {"id": "0_0_1_0_0_1_1_0", "sources": [{"type": "file", "channels": [0], "source": "/tmp/icefall/egs/yesno/ASR/download/waves_yesno/0_0_1_0_0_1_1_0.wav"}], "sampling_rate": 8000, "num_samples": 48160, "duration": 6.02, "channel_ids": [0]} + + Please refer to ``_ + for the meaning of each field per line. + + .. code-block:: bash + + gunzip -c yesno_supervisions_train.jsonl.gz | head -n 3 + + The output is given below: + + .. code-block:: bash + + {"id": "0_0_0_0_1_1_1_1", "recording_id": "0_0_0_0_1_1_1_1", "start": 0.0, "duration": 6.35, "channel": 0, "text": "NO NO NO NO YES YES YES YES", "language": "Hebrew"} + {"id": "0_0_0_1_0_1_1_0", "recording_id": "0_0_0_1_0_1_1_0", "start": 0.0, "duration": 6.11, "channel": 0, "text": "NO NO NO YES NO YES YES NO", "language": "Hebrew"} + {"id": "0_0_1_0_0_1_1_0", "recording_id": "0_0_1_0_0_1_1_0", "start": 0.0, "duration": 6.02, "channel": 0, "text": "NO NO YES NO NO YES YES NO", "language": "Hebrew"} + + Please refer to ``_ + for the meaning of each field per line. + +**data/fbank**: + + This directory contains everything from ``data/manifests``. Furthermore, it also contains features + for training. + + ``data/fbank/yesno_feats_train.lca`` contains the features for the train dataset. + Features are compressed using `lilcom`_. + + ``data/fbank/yesno_cuts_train.jsonl.gz`` stores the `CutSet `_, + which stores `RecordingSet `_, + `SupervisionSet `_, + and `FeatureSet `_. + + To give you an idea about what it looks like, we can run the following command: + + .. code-block:: bash + + cd data/fbank + + gunzip -c yesno_cuts_train.jsonl.gz | head -n 3 + + The output is given below: + + .. code-block:: bash + + {"id": "0_0_0_0_1_1_1_1-0", "start": 0, "duration": 6.35, "channel": 0, "supervisions": [{"id": "0_0_0_0_1_1_1_1", "recording_id": "0_0_0_0_1_1_1_1", "start": 0.0, "duration": 6.35, "channel": 0, "text": "NO NO NO NO YES YES YES YES", "language": "Hebrew"}], "features": {"type": "kaldi-fbank", "num_frames": 635, "num_features": 23, "frame_shift": 0.01, "sampling_rate": 8000, "start": 0, "duration": 6.35, "storage_type": "lilcom_chunky", "storage_path": "data/fbank/yesno_feats_train.lca", "storage_key": "0,13000,3570", "channels": 0}, "recording": {"id": "0_0_0_0_1_1_1_1", "sources": [{"type": "file", "channels": [0], "source": "/tmp/icefall/egs/yesno/ASR/download/waves_yesno/0_0_0_0_1_1_1_1.wav"}], "sampling_rate": 8000, "num_samples": 50800, "duration": 6.35, "channel_ids": [0]}, "type": "MonoCut"} + {"id": "0_0_0_1_0_1_1_0-1", "start": 0, "duration": 6.11, "channel": 0, "supervisions": [{"id": "0_0_0_1_0_1_1_0", "recording_id": "0_0_0_1_0_1_1_0", "start": 0.0, "duration": 6.11, "channel": 0, "text": "NO NO NO YES NO YES YES NO", "language": "Hebrew"}], "features": {"type": "kaldi-fbank", "num_frames": 611, "num_features": 23, "frame_shift": 0.01, "sampling_rate": 8000, "start": 0, "duration": 6.11, "storage_type": "lilcom_chunky", "storage_path": "data/fbank/yesno_feats_train.lca", "storage_key": "16570,12964,2929", "channels": 0}, "recording": {"id": "0_0_0_1_0_1_1_0", "sources": [{"type": "file", "channels": [0], "source": "/tmp/icefall/egs/yesno/ASR/download/waves_yesno/0_0_0_1_0_1_1_0.wav"}], "sampling_rate": 8000, "num_samples": 48880, "duration": 6.11, "channel_ids": [0]}, "type": "MonoCut"} + {"id": "0_0_1_0_0_1_1_0-2", "start": 0, "duration": 6.02, "channel": 0, "supervisions": [{"id": "0_0_1_0_0_1_1_0", "recording_id": "0_0_1_0_0_1_1_0", "start": 0.0, "duration": 6.02, "channel": 0, "text": "NO NO YES NO NO YES YES NO", "language": "Hebrew"}], "features": {"type": "kaldi-fbank", "num_frames": 602, "num_features": 23, "frame_shift": 0.01, "sampling_rate": 8000, "start": 0, "duration": 6.02, "storage_type": "lilcom_chunky", "storage_path": "data/fbank/yesno_feats_train.lca", "storage_key": "32463,12936,2696", "channels": 0}, "recording": {"id": "0_0_1_0_0_1_1_0", "sources": [{"type": "file", "channels": [0], "source": "/tmp/icefall/egs/yesno/ASR/download/waves_yesno/0_0_1_0_0_1_1_0.wav"}], "sampling_rate": 8000, "num_samples": 48160, "duration": 6.02, "channel_ids": [0]}, "type": "MonoCut"} + + Note that ``yesno_cuts_train.jsonl.gz`` only stores the information about how to read the features. + The actual features are stored separately in ``data/fbank/yesno_feats_train.lca``. + +**data/lang**: + + This directory contains the lexicon. + +**data/lm**: + + This directory contains language models. diff --git a/docs/source/for-dummies/decoding.rst b/docs/source/for-dummies/decoding.rst new file mode 100644 index 000000000..3e48e8bfd --- /dev/null +++ b/docs/source/for-dummies/decoding.rst @@ -0,0 +1,39 @@ +.. _dummies_tutorial_decoding: + +Decoding +======== + +After :ref:`dummies_tutorial_training`, we can start decoding. + +The command to start the decoding is quite simple: + +.. code-block:: bash + + cd /tmp/icefall + export PYTHONPATH=/tmp/icefall:$PYTHONPATH + cd egs/yesno/ASR + + # We use CPU for decoding by setting the following environment variable + export CUDA_VISIBLE_DEVICES="" + + ./tdnn/decode.py + +The output logs are given below: + +.. literalinclude:: ./code/decoding-yesno.txt + +For the more curious +-------------------- + +.. code-block:: bash + + ./tdnn/decode.py --help + +will print the usage information about ``./tdnn/decode.py``. For instance, you +can specify: + + - ``--epoch`` to use which checkpoint for decoding + - ``--avg`` to select how many checkpoints to use for model averaging + +You usually try different combinations of ``--epoch`` and ``--avg`` and select +one that leads to the lowest WER (`Word Error Rate `_). diff --git a/docs/source/for-dummies/environment-setup.rst b/docs/source/for-dummies/environment-setup.rst new file mode 100644 index 000000000..0cb8ecc1d --- /dev/null +++ b/docs/source/for-dummies/environment-setup.rst @@ -0,0 +1,121 @@ +.. _dummies_tutorial_environment_setup: + +Environment setup +================= + +We will create an environment for `Next-gen Kaldi`_ that runs on ``CPU`` +in this tutorial. + +.. note:: + + Since the `yesno`_ dataset used in this tutorial is very tiny, training on + ``CPU`` works very well for it. + + If your dataset is very large, e.g., hundreds or thousands of hours of + training data, please follow :ref:`install icefall` to install `icefall`_ + that works with ``GPU``. + + +Create a virtual environment +---------------------------- + +.. code-block:: bash + + virtualenv -p python3 /tmp/icefall_env + +The above command creates a virtual environment in the directory ``/tmp/icefall_env``. +You can select any directory you want. + +The output of the above command is given below: + +.. code-block:: bash + + Already using interpreter /usr/bin/python3 + Using base prefix '/usr' + New python executable in /tmp/icefall_env/bin/python3 + Also creating executable in /tmp/icefall_env/bin/python + Installing setuptools, pkg_resources, pip, wheel...done. + +Now we can activate the environment using: + +.. code-block:: bash + + source /tmp/icefall_env/bin/activate + +Install dependencies +-------------------- + +.. warning:: + + Remeber to activate your virtual environment before you continue! + +After activating the virtual environment, we can use the following command +to install dependencies of `icefall`_: + +.. hint:: + + Remeber that we will run this tutorial on ``CPU``, so we install + dependencies required only by running on ``CPU``. + +.. code-block:: bash + + # Caution: Installation order matters! + + # We use torch 2.0.0 and torchaduio 2.0.0 in this tutorial. + # Other versions should also work. + + pip install torch==2.0.0+cpu torchaudio==2.0.0+cpu -f https://download.pytorch.org/whl/torch_stable.html + + # If you are using macOS or Windows, please use the following command to install torch and torchaudio + # pip install torch==2.0.0 torchaudio==2.0.0 -f https://download.pytorch.org/whl/torch_stable.html + + # Now install k2 + # Please refer to https://k2-fsa.github.io/k2/installation/from_wheels.html#linux-cpu-example + + pip install k2==1.24.3.dev20230726+cpu.torch2.0.0 -f https://k2-fsa.github.io/k2/cpu.html + + # Install the latest version of lhotse + + pip install git+https://github.com/lhotse-speech/lhotse + + +Install icefall +--------------- + +We will put the source code of `icefall`_ into the directory ``/tmp`` +You can select any directory you want. + +.. code-block:: bash + + cd /tmp + git clone https://github.com/k2-fsa/icefall + cd icefall + pip install -r ./requirements.txt + +.. code-block:: bash + + # Anytime we want to use icefall, we have to set the following + # environment variable + + export PYTHONPATH=/tmp/icefall:$PYTHONPATH + +.. hint:: + + If you get the following error during this tutorial: + + .. code-block:: bash + + ModuleNotFoundError: No module named 'icefall' + + please set the above environment variable to fix it. + + +Congratulations! You have installed `icefall`_ successfully. + +For the more curious +-------------------- + +`icefall`_ contains a collection of Python scripts and you don't need to +use ``python3 setup.py install`` or ``pip install icefall`` to install it. +All you need to do is to download the code and set the environment variable +``PYTHONPATH``. diff --git a/docs/source/for-dummies/index.rst b/docs/source/for-dummies/index.rst new file mode 100644 index 000000000..7c0a3d8ee --- /dev/null +++ b/docs/source/for-dummies/index.rst @@ -0,0 +1,34 @@ +Icefall for dummies tutorial +============================ + +This tutorial walks you step by step about how to create a simple +ASR (`Automatic Speech Recognition `_) +system with `Next-gen Kaldi`_. + +We use the `yesno`_ dataset for demonstration. We select it out of two reasons: + + - It is quite tiny, containing only about 12 minutes of data + - The training can be finished within 20 seconds on ``CPU``. + +That also means you don't need a ``GPU`` to run this tutorial. + +Let's get started! + +Please follow items below **sequentially**. + +.. note:: + + The :ref:`dummies_tutorial_data_preparation` runs only on Linux and on macOS. + All other parts run on Linux, macOS, and Windows. + + Help from the community is appreciated to port the :ref:`dummies_tutorial_data_preparation` + to Windows. + +.. toctree:: + :maxdepth: 2 + + ./environment-setup.rst + ./data-preparation.rst + ./training.rst + ./decoding.rst + ./model-export.rst diff --git a/docs/source/for-dummies/model-export.rst b/docs/source/for-dummies/model-export.rst new file mode 100644 index 000000000..079ebc712 --- /dev/null +++ b/docs/source/for-dummies/model-export.rst @@ -0,0 +1,310 @@ +Model Export +============ + +There are three ways to export a pre-trained model. + + - Export the model parameters via `model.state_dict() `_ + - Export via `torchscript `_: either `torch.jit.script() `_ or `torch.jit.trace() `_ + - Export to `ONNX`_ via `torch.onnx.export() `_ + +Each method is explained below in detail. + +Export the model parameters via model.state_dict() +--------------------------------------------------- + +The command for this kind of export is + +.. code-block:: bash + + cd /tmp/icefall + export PYTHONPATH=/tmp/icefall:$PYTHONPATH + cd egs/yesno/ASR + + # assume that "--epoch 14 --avg 2" produces the lowest WER. + + ./tdnn/export.py --epoch 14 --avg 2 + +The output logs are given below: + +.. code-block:: bash + + 2023-08-16 20:42:03,912 INFO [export.py:76] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lr': 0.01, 'feature_dim': 23, 'weight_decay': 1e-06, 'start_epoch': 0, 'best_train_loss': inf, 'best_valid_loss': inf, 'best_train_epoch': -1, 'best_valid_epoch': -1, 'batch_idx_train': 0, 'log_interval': 10, 'reset_interval': 20, 'valid_interval': 10, 'beam_size': 10, 'reduction': 'sum', 'use_double_scores': True, 'epoch': 14, 'avg': 2, 'jit': False} + 2023-08-16 20:42:03,913 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt + 2023-08-16 20:42:03,950 INFO [export.py:93] averaging ['tdnn/exp/epoch-13.pt', 'tdnn/exp/epoch-14.pt'] + 2023-08-16 20:42:03,971 INFO [export.py:106] Not using torch.jit.script + 2023-08-16 20:42:03,974 INFO [export.py:111] Saved to tdnn/exp/pretrained.pt + +We can see from the logs that the exported model is saved to the file ``tdnn/exp/pretrained.pt``. + +To give you an idea of what ``tdnn/exp/pretrained.pt`` contains, we can use the following command: + +.. code-block:: python3 + + >>> import torch + >>> m = torch.load("tdnn/exp/pretrained.pt") + >>> list(m.keys()) + ['model'] + >>> list(m["model"].keys()) + ['tdnn.0.weight', 'tdnn.0.bias', 'tdnn.2.running_mean', 'tdnn.2.running_var', 'tdnn.2.num_batches_tracked', 'tdnn.3.weight', 'tdnn.3.bias', 'tdnn.5.running_mean', 'tdnn.5.running_var', 'tdnn.5.num_batches_tracked', 'tdnn.6.weight', 'tdnn.6.bias', 'tdnn.8.running_mean', 'tdnn.8.running_var', 'tdnn.8.num_batches_tracked', 'output_linear.weight', 'output_linear.bias'] + +We can use ``tdnn/exp/pretrained.pt`` in the following way with ``./tdnn/decode.py``: + +.. code-block:: bash + + cd tdnn/exp + ln -s pretrained.pt epoch-99.pt + cd ../.. + + ./tdnn/decode.py --epoch 99 --avg 1 + +The output logs of the above command are given below: + +.. code-block:: bash + + 2023-08-16 20:45:48,089 INFO [decode.py:262] Decoding started + 2023-08-16 20:45:48,090 INFO [decode.py:263] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'feature_dim': 23, 'search_beam': 20, 'output_beam': 8, 'min_active_states': 30, 'max_active_states': 10000, 'use_double_scores': True, 'epoch': 99, 'avg': 1, 'export': False, 'feature_dir': PosixPath('data/fbank'), 'max_duration': 30.0, 'bucketing_sampler': False, 'num_buckets': 10, 'concatenate_cuts': False, 'duration_factor': 1.0, 'gap': 1.0, 'on_the_fly_feats': False, 'shuffle': False, 'return_cuts': True, 'num_workers': 2, 'env_info': {'k2-version': '1.24.3', 'k2-build-type': 'Release', 'k2-with-cuda': False, 'k2-git-sha1': 'ad79f1c699c684de9785ed6ca5edb805a41f78c3', 'k2-git-date': 'Wed Jul 26 09:30:42 2023', 'lhotse-version': '1.16.0.dev+git.aa073f6.clean', 'torch-version': '2.0.0', 'torch-cuda-available': False, 'torch-cuda-version': None, 'python-version': '3.1', 'icefall-git-branch': 'master', 'icefall-git-sha1': '9a47c08-clean', 'icefall-git-date': 'Mon Aug 14 22:10:50 2023', 'icefall-path': '/private/tmp/icefall', 'k2-path': '/private/tmp/icefall_env/lib/python3.11/site-packages/k2/__init__.py', 'lhotse-path': '/private/tmp/icefall_env/lib/python3.11/site-packages/lhotse/__init__.py', 'hostname': 'fangjuns-MacBook-Pro.local', 'IP address': '127.0.0.1'}} + 2023-08-16 20:45:48,092 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt + 2023-08-16 20:45:48,103 INFO [decode.py:272] device: cpu + 2023-08-16 20:45:48,109 INFO [checkpoint.py:112] Loading checkpoint from tdnn/exp/epoch-99.pt + 2023-08-16 20:45:48,115 INFO [asr_datamodule.py:218] About to get test cuts + 2023-08-16 20:45:48,115 INFO [asr_datamodule.py:253] About to get test cuts + 2023-08-16 20:45:50,386 INFO [decode.py:203] batch 0/?, cuts processed until now is 4 + 2023-08-16 20:45:50,556 INFO [decode.py:240] The transcripts are stored in tdnn/exp/recogs-test_set.txt + 2023-08-16 20:45:50,557 INFO [utils.py:564] [test_set] %WER 0.42% [1 / 240, 0 ins, 1 del, 0 sub ] + 2023-08-16 20:45:50,558 INFO [decode.py:248] Wrote detailed error stats to tdnn/exp/errs-test_set.txt + 2023-08-16 20:45:50,559 INFO [decode.py:315] Done! + +We can see that it produces an identical WER as before. + +We can also use it to decode files with the following command: + +.. code-block:: bash + + # ./tdnn/pretrained.py requires kaldifeat + # + # Please refer to https://csukuangfj.github.io/kaldifeat/installation/from_wheels.html + # for how to install kaldifeat + + pip install kaldifeat==1.25.0.dev20230726+cpu.torch2.0.0 -f https://csukuangfj.github.io/kaldifeat/cpu.html + + ./tdnn/pretrained.py \ + --checkpoint ./tdnn/exp/pretrained.pt \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +The output is given below: + +.. code-block:: bash + + 2023-08-16 20:53:19,208 INFO [pretrained.py:136] {'feature_dim': 23, 'num_classes': 4, 'sample_rate': 8000, 'search_beam': 20, 'output_beam': 8, 'min_active_states': 30, 'max_active_states': 10000, 'use_double_scores': True, 'checkpoint': './tdnn/exp/pretrained.pt', 'words_file': './data/lang_phone/words.txt', 'HLG': './data/lang_phone/HLG.pt', 'sound_files': ['download/waves_yesno/0_0_0_1_0_0_0_1.wav', 'download/waves_yesno/0_0_1_0_0_0_1_0.wav']} + 2023-08-16 20:53:19,208 INFO [pretrained.py:142] device: cpu + 2023-08-16 20:53:19,208 INFO [pretrained.py:144] Creating model + 2023-08-16 20:53:19,212 INFO [pretrained.py:156] Loading HLG from ./data/lang_phone/HLG.pt + 2023-08-16 20:53:19,213 INFO [pretrained.py:160] Constructing Fbank computer + 2023-08-16 20:53:19,213 INFO [pretrained.py:170] Reading sound files: ['download/waves_yesno/0_0_0_1_0_0_0_1.wav', 'download/waves_yesno/0_0_1_0_0_0_1_0.wav'] + 2023-08-16 20:53:19,224 INFO [pretrained.py:176] Decoding started + 2023-08-16 20:53:19,304 INFO [pretrained.py:212] + download/waves_yesno/0_0_0_1_0_0_0_1.wav: + NO NO NO YES NO NO NO YES + + download/waves_yesno/0_0_1_0_0_0_1_0.wav: + NO NO YES NO NO NO YES NO + + + 2023-08-16 20:53:19,304 INFO [pretrained.py:214] Decoding Done + + +Export via torch.jit.script() +----------------------------- + +The command for this kind of export is + +.. code-block:: bash + + cd /tmp/icefall + export PYTHONPATH=/tmp/icefall:$PYTHONPATH + cd egs/yesno/ASR + + # assume that "--epoch 14 --avg 2" produces the lowest WER. + + ./tdnn/export.py --epoch 14 --avg 2 --jit true + +The output logs are given below: + +.. code-block:: bash + + 2023-08-16 20:47:44,666 INFO [export.py:76] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lr': 0.01, 'feature_dim': 23, 'weight_decay': 1e-06, 'start_epoch': 0, 'best_train_loss': inf, 'best_valid_loss': inf, 'best_train_epoch': -1, 'best_valid_epoch': -1, 'batch_idx_train': 0, 'log_interval': 10, 'reset_interval': 20, 'valid_interval': 10, 'beam_size': 10, 'reduction': 'sum', 'use_double_scores': True, 'epoch': 14, 'avg': 2, 'jit': True} + 2023-08-16 20:47:44,667 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt + 2023-08-16 20:47:44,670 INFO [export.py:93] averaging ['tdnn/exp/epoch-13.pt', 'tdnn/exp/epoch-14.pt'] + 2023-08-16 20:47:44,677 INFO [export.py:100] Using torch.jit.script + 2023-08-16 20:47:44,843 INFO [export.py:104] Saved to tdnn/exp/cpu_jit.pt + +From the output logs we can see that the generated file is saved to ``tdnn/exp/cpu_jit.pt``. + +Don't be confused by the name ``cpu_jit.pt``. The ``cpu`` part means the model is moved to +CPU before exporting. That means, when you load it with: + +.. code-block:: bash + + torch.jit.load() + +you don't need to specify the argument `map_location `_ +and it resides on CPU by default. + +To use ``tdnn/exp/cpu_jit.pt`` with `icefall`_ to decode files, we can use: + +.. code-block:: bash + + # ./tdnn/jit_pretrained.py requires kaldifeat + # + # Please refer to https://csukuangfj.github.io/kaldifeat/installation/from_wheels.html + # for how to install kaldifeat + + pip install kaldifeat==1.25.0.dev20230726+cpu.torch2.0.0 -f https://csukuangfj.github.io/kaldifeat/cpu.html + + + ./tdnn/jit_pretrained.py \ + --nn-model ./tdnn/exp/cpu_jit.pt \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +The output is given below: + +.. code-block:: bash + + 2023-08-16 20:56:00,603 INFO [jit_pretrained.py:121] {'feature_dim': 23, 'num_classes': 4, 'sample_rate': 8000, 'search_beam': 20, 'output_beam': 8, 'min_active_states': 30, 'max_active_states': 10000, 'use_double_scores': True, 'nn_model': './tdnn/exp/cpu_jit.pt', 'words_file': './data/lang_phone/words.txt', 'HLG': './data/lang_phone/HLG.pt', 'sound_files': ['download/waves_yesno/0_0_0_1_0_0_0_1.wav', 'download/waves_yesno/0_0_1_0_0_0_1_0.wav']} + 2023-08-16 20:56:00,603 INFO [jit_pretrained.py:127] device: cpu + 2023-08-16 20:56:00,603 INFO [jit_pretrained.py:129] Loading torchscript model + 2023-08-16 20:56:00,640 INFO [jit_pretrained.py:134] Loading HLG from ./data/lang_phone/HLG.pt + 2023-08-16 20:56:00,641 INFO [jit_pretrained.py:138] Constructing Fbank computer + 2023-08-16 20:56:00,641 INFO [jit_pretrained.py:148] Reading sound files: ['download/waves_yesno/0_0_0_1_0_0_0_1.wav', 'download/waves_yesno/0_0_1_0_0_0_1_0.wav'] + 2023-08-16 20:56:00,642 INFO [jit_pretrained.py:154] Decoding started + 2023-08-16 20:56:00,727 INFO [jit_pretrained.py:190] + download/waves_yesno/0_0_0_1_0_0_0_1.wav: + NO NO NO YES NO NO NO YES + + download/waves_yesno/0_0_1_0_0_0_1_0.wav: + NO NO YES NO NO NO YES NO + + + 2023-08-16 20:56:00,727 INFO [jit_pretrained.py:192] Decoding Done + +.. hint:: + + We provide only code for ``torch.jit.script()``. You can try ``torch.jit.trace()`` + if you want. + +Export via torch.onnx.export() +------------------------------ + +The command for this kind of export is + +.. code-block:: bash + + cd /tmp/icefall + export PYTHONPATH=/tmp/icefall:$PYTHONPATH + cd egs/yesno/ASR + + # tdnn/export_onnx.py requires onnx and onnxruntime + pip install onnx onnxruntime + + # assume that "--epoch 14 --avg 2" produces the lowest WER. + + ./tdnn/export_onnx.py \ + --epoch 14 \ + --avg 2 + +The output logs are given below: + +.. code-block:: bash + + 2023-08-16 20:59:20,888 INFO [export_onnx.py:83] {'exp_dir': PosixPath('tdnn/exp'), 'lang_dir': PosixPath('data/lang_phone'), 'lr': 0.01, 'feature_dim': 23, 'weight_decay': 1e-06, 'start_epoch': 0, 'best_train_loss': inf, 'best_valid_loss': inf, 'best_train_epoch': -1, 'best_valid_epoch': -1, 'batch_idx_train': 0, 'log_interval': 10, 'reset_interval': 20, 'valid_interval': 10, 'beam_size': 10, 'reduction': 'sum', 'use_double_scores': True, 'epoch': 14, 'avg': 2} + 2023-08-16 20:59:20,888 INFO [lexicon.py:168] Loading pre-compiled data/lang_phone/Linv.pt + 2023-08-16 20:59:20,892 INFO [export_onnx.py:100] averaging ['tdnn/exp/epoch-13.pt', 'tdnn/exp/epoch-14.pt'] + ================ Diagnostic Run torch.onnx.export version 2.0.0 ================ + verbose: False, log level: Level.ERROR + ======================= 0 NONE 0 NOTE 0 WARNING 0 ERROR ======================== + + 2023-08-16 20:59:21,047 INFO [export_onnx.py:127] Saved to tdnn/exp/model-epoch-14-avg-2.onnx + 2023-08-16 20:59:21,047 INFO [export_onnx.py:136] meta_data: {'model_type': 'tdnn', 'version': '1', 'model_author': 'k2-fsa', 'comment': 'non-streaming tdnn for the yesno recipe', 'vocab_size': 4} + 2023-08-16 20:59:21,049 INFO [export_onnx.py:140] Generate int8 quantization models + 2023-08-16 20:59:21,075 INFO [onnx_quantizer.py:538] Quantization parameters for tensor:"/Transpose_1_output_0" not specified + 2023-08-16 20:59:21,081 INFO [export_onnx.py:151] Saved to tdnn/exp/model-epoch-14-avg-2.int8.onnx + +We can see from the logs that it generates two files: + + - ``tdnn/exp/model-epoch-14-avg-2.onnx`` (ONNX model with ``float32`` weights) + - ``tdnn/exp/model-epoch-14-avg-2.int8.onnx`` (ONNX model with ``int8`` weights) + +To use the generated ONNX model files for decoding with `onnxruntime`_, we can use + +.. code-block:: bash + + # ./tdnn/onnx_pretrained.py requires kaldifeat + # + # Please refer to https://csukuangfj.github.io/kaldifeat/installation/from_wheels.html + # for how to install kaldifeat + + pip install kaldifeat==1.25.0.dev20230726+cpu.torch2.0.0 -f https://csukuangfj.github.io/kaldifeat/cpu.html + + ./tdnn/onnx_pretrained.py \ + --nn-model ./tdnn/exp/model-epoch-14-avg-2.onnx \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +The output is given below: + +.. code-block:: bash + + 2023-08-16 21:03:24,260 INFO [onnx_pretrained.py:166] {'feature_dim': 23, 'sample_rate': 8000, 'search_beam': 20, 'output_beam': 8, 'min_active_states': 30, 'max_active_states': 10000, 'use_double_scores': True, 'nn_model': './tdnn/exp/model-epoch-14-avg-2.onnx', 'words_file': './data/lang_phone/words.txt', 'HLG': './data/lang_phone/HLG.pt', 'sound_files': ['download/waves_yesno/0_0_0_1_0_0_0_1.wav', 'download/waves_yesno/0_0_1_0_0_0_1_0.wav']} + 2023-08-16 21:03:24,260 INFO [onnx_pretrained.py:171] device: cpu + 2023-08-16 21:03:24,260 INFO [onnx_pretrained.py:173] Loading onnx model ./tdnn/exp/model-epoch-14-avg-2.onnx + 2023-08-16 21:03:24,267 INFO [onnx_pretrained.py:176] Loading HLG from ./data/lang_phone/HLG.pt + 2023-08-16 21:03:24,270 INFO [onnx_pretrained.py:180] Constructing Fbank computer + 2023-08-16 21:03:24,273 INFO [onnx_pretrained.py:190] Reading sound files: ['download/waves_yesno/0_0_0_1_0_0_0_1.wav', 'download/waves_yesno/0_0_1_0_0_0_1_0.wav'] + 2023-08-16 21:03:24,279 INFO [onnx_pretrained.py:196] Decoding started + 2023-08-16 21:03:24,318 INFO [onnx_pretrained.py:232] + download/waves_yesno/0_0_0_1_0_0_0_1.wav: + NO NO NO YES NO NO NO YES + + download/waves_yesno/0_0_1_0_0_0_1_0.wav: + NO NO YES NO NO NO YES NO + + + 2023-08-16 21:03:24,318 INFO [onnx_pretrained.py:234] Decoding Done + +.. note:: + + To use the ``int8`` ONNX model for decoding, please use: + + .. code-block:: bash + + ./tdnn/onnx_pretrained.py \ + --nn-model ./tdnn/exp/model-epoch-14-avg-2.onnx \ + --HLG ./data/lang_phone/HLG.pt \ + --words-file ./data/lang_phone/words.txt \ + download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + download/waves_yesno/0_0_1_0_0_0_1_0.wav + +For the more curious +-------------------- + +If you are wondering how to deploy the model without ``torch``, please +continue reading. We will show how to use `sherpa-onnx`_ to run the +exported ONNX models, which depends only on `onnxruntime`_ and does not +depend on ``torch``. + +In this tutorial, we will only demonstrate the usage of `sherpa-onnx`_ with the +pre-trained model of the `yesno`_ recipe. There are also other two frameworks +available: + + - `sherpa`_. It works with torchscript models. + - `sherpa-ncnn`_. It works with models exported using :ref:`icefall_export_to_ncnn` with `ncnn`_ + +Please see ``_ for further details. diff --git a/docs/source/for-dummies/training.rst b/docs/source/for-dummies/training.rst new file mode 100644 index 000000000..816ef2d3b --- /dev/null +++ b/docs/source/for-dummies/training.rst @@ -0,0 +1,39 @@ +.. _dummies_tutorial_training: + +Training +======== + +After :ref:`dummies_tutorial_data_preparation`, we can start training. + +The command to start the training is quite simple: + +.. code-block:: bash + + cd /tmp/icefall + export PYTHONPATH=/tmp/icefall:$PYTHONPATH + cd egs/yesno/ASR + + # We use CPU for training by setting the following environment variable + export CUDA_VISIBLE_DEVICES="" + + ./tdnn/train.py + +That's it! + +You can find the training logs below: + +.. literalinclude:: ./code/train-yesno.txt + +For the more curious +-------------------- + +.. code-block:: bash + + ./tdnn/train.py --help + +will print the usage information about ``./tdnn/train.py``. For instance, you +can specify the number of epochs to train and the location to save the training +results. + +The training text logs are saved in ``tdnn/exp/log`` while the tensorboard +logs are in ``tdnn/exp/tensorboard``. diff --git a/docs/source/index.rst b/docs/source/index.rst index 0fa8fdd1c..fb539d3f2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,7 @@ speech recognition recipes using `k2 `_. :maxdepth: 2 :caption: Contents: + for-dummies/index.rst installation/index docker/index faqs diff --git a/egs/yesno/ASR/tdnn/onnx_pretrained.py b/egs/yesno/ASR/tdnn/onnx_pretrained.py index 626473b6e..b23a2a381 100755 --- a/egs/yesno/ASR/tdnn/onnx_pretrained.py +++ b/egs/yesno/ASR/tdnn/onnx_pretrained.py @@ -6,6 +6,7 @@ This file shows how to use an ONNX model for decoding with onnxruntime. Usage: (1) Use a not quantized ONNX model, i.e., a float32 model + ./tdnn/onnx_pretrained.py \ --nn-model ./tdnn/exp/model-epoch-14-avg-2.onnx \ --HLG ./data/lang_phone/HLG.pt \ From 4d7f73ce65e2ce89c6be432ae2f973cb5597474f Mon Sep 17 00:00:00 2001 From: Wei Kang Date: Mon, 28 Aug 2023 19:37:32 +0800 Subject: [PATCH 073/100] Add context biasing for zipformer recipe (#1204) * Add context biasing for zipformer recipe * support context biasing in modified_beam_search_LODR * fix context graph * Minor fixes --- .../beam_search.py | 33 +++++++ egs/librispeech/ASR/zipformer/decode.py | 88 +++++++++++++++---- icefall/context_graph.py | 43 ++++----- 3 files changed, 122 insertions(+), 42 deletions(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index 97e259b40..3298568a3 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -2389,6 +2389,7 @@ def modified_beam_search_LODR( LODR_lm_scale: float, LM: LmScorer, beam: int = 4, + context_graph: Optional[ContextGraph] = None, ) -> List[List[int]]: """This function implements LODR (https://arxiv.org/abs/2203.16776) with `modified_beam_search`. It uses a bi-gram language model as the estimate @@ -2457,6 +2458,7 @@ def modified_beam_search_LODR( state_cost=NgramLmStateCost( LODR_lm ), # state of the source domain ngram + context_state=None if context_graph is None else context_graph.root, ) ) @@ -2602,8 +2604,17 @@ def modified_beam_search_LODR( hyp_log_prob = topk_log_probs[k] # get score of current hyp new_token = topk_token_indexes[k] + + context_score = 0 + new_context_state = None if context_graph is None else hyp.context_state if new_token not in (blank_id, unk_id): + if context_graph is not None: + ( + context_score, + new_context_state, + ) = context_graph.forward_one_step(hyp.context_state, new_token) + ys.append(new_token) state_cost = hyp.state_cost.forward_one_step(new_token) @@ -2619,6 +2630,7 @@ def modified_beam_search_LODR( hyp_log_prob += ( lm_score[new_token] * lm_scale + LODR_lm_scale * current_ngram_score + + context_score ) # add the lm score lm_score = scores[count] @@ -2637,10 +2649,31 @@ def modified_beam_search_LODR( state=state, lm_score=lm_score, state_cost=state_cost, + context_state=new_context_state, ) B[i].add(new_hyp) B = B + finalized_B + + # finalize context_state, if the matched contexts do not reach final state + # we need to add the score on the corresponding backoff arc + if context_graph is not None: + finalized_B = [HypothesisList() for _ in range(len(B))] + for i, hyps in enumerate(B): + for hyp in list(hyps): + context_score, new_context_state = context_graph.finalize( + hyp.context_state + ) + finalized_B[i].add( + Hypothesis( + ys=hyp.ys, + log_prob=hyp.log_prob + context_score, + timestamp=hyp.timestamp, + context_state=new_context_state, + ) + ) + B = finalized_B + best_hyps = [b.get_most_probable(length_norm=True) for b in B] sorted_ans = [h.ys[context_size:] for h in best_hyps] diff --git a/egs/librispeech/ASR/zipformer/decode.py b/egs/librispeech/ASR/zipformer/decode.py index 2cc157e7a..3531d657f 100755 --- a/egs/librispeech/ASR/zipformer/decode.py +++ b/egs/librispeech/ASR/zipformer/decode.py @@ -97,6 +97,7 @@ Usage: import argparse import logging import math +import os from collections import defaultdict from pathlib import Path from typing import Dict, List, Optional, Tuple @@ -122,7 +123,7 @@ from beam_search import ( ) from train import add_model_arguments, get_model, get_params -from icefall import LmScorer, NgramLm +from icefall import ContextGraph, LmScorer, NgramLm from icefall.checkpoint import ( average_checkpoints, average_checkpoints_with_averaged_model, @@ -215,6 +216,7 @@ def get_parser(): - greedy_search - beam_search - modified_beam_search + - modified_beam_search_LODR - fast_beam_search - fast_beam_search_nbest - fast_beam_search_nbest_oracle @@ -251,7 +253,7 @@ def get_parser(): type=float, default=0.01, help=""" - Used only when --decoding_method is fast_beam_search_nbest_LG. + Used only when --decoding-method is fast_beam_search_nbest_LG. It specifies the scale for n-gram LM scores. """, ) @@ -285,7 +287,7 @@ def get_parser(): type=int, default=1, help="""Maximum number of symbols per frame. - Used only when --decoding_method is greedy_search""", + Used only when --decoding-method is greedy_search""", ) parser.add_argument( @@ -347,6 +349,27 @@ def get_parser(): help="ID of the backoff symbol in the ngram LM", ) + parser.add_argument( + "--context-score", + type=float, + default=2, + help=""" + The bonus score of each token for the context biasing words/phrases. + Used only when --decoding-method is modified_beam_search and + modified_beam_search_LODR. + """, + ) + + parser.add_argument( + "--context-file", + type=str, + default="", + help=""" + The path of the context biasing lists, one word/phrase each line + Used only when --decoding-method is modified_beam_search and + modified_beam_search_LODR. + """, + ) add_model_arguments(parser) return parser @@ -359,6 +382,7 @@ def decode_one_batch( batch: dict, word_table: Optional[k2.SymbolTable] = None, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, LM: Optional[LmScorer] = None, ngram_lm=None, ngram_lm_scale: float = 0.0, @@ -388,7 +412,7 @@ def decode_one_batch( The word symbol table. decoding_graph: The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used - only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + only when --decoding-method is fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. LM: A neural network language model. @@ -493,6 +517,7 @@ def decode_one_batch( encoder_out=encoder_out, encoder_out_lens=encoder_out_lens, beam=params.beam_size, + context_graph=context_graph, ) for hyp in sp.decode(hyp_tokens): hyps.append(hyp.split()) @@ -515,6 +540,7 @@ def decode_one_batch( LODR_lm=ngram_lm, LODR_lm_scale=ngram_lm_scale, LM=LM, + context_graph=context_graph, ) for hyp in sp.decode(hyp_tokens): hyps.append(hyp.split()) @@ -578,16 +604,22 @@ def decode_one_batch( key += f"_ngram_lm_scale_{params.ngram_lm_scale}" return {key: hyps} - elif params.decoding_method in ( - "modified_beam_search_lm_rescore", - "modified_beam_search_lm_rescore_LODR", - ): - ans = dict() - assert ans_dict is not None - for key, hyps in ans_dict.items(): - hyps = [sp.decode(hyp).split() for hyp in hyps] - ans[f"beam_size_{params.beam_size}_{key}"] = hyps - return ans + elif "modified_beam_search" in params.decoding_method: + prefix = f"beam_size_{params.beam_size}" + if params.decoding_method in ( + "modified_beam_search_lm_rescore", + "modified_beam_search_lm_rescore_LODR", + ): + ans = dict() + assert ans_dict is not None + for key, hyps in ans_dict.items(): + hyps = [sp.decode(hyp).split() for hyp in hyps] + ans[f"{prefix}_{key}"] = hyps + return ans + else: + if params.has_contexts: + prefix += f"-context-score-{params.context_score}" + return {prefix: hyps} else: return {f"beam_size_{params.beam_size}": hyps} @@ -599,6 +631,7 @@ def decode_dataset( sp: spm.SentencePieceProcessor, word_table: Optional[k2.SymbolTable] = None, decoding_graph: Optional[k2.Fsa] = None, + context_graph: Optional[ContextGraph] = None, LM: Optional[LmScorer] = None, ngram_lm=None, ngram_lm_scale: float = 0.0, @@ -618,7 +651,7 @@ def decode_dataset( The word symbol table. decoding_graph: The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used - only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + only when --decoding-method is fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. Returns: Return a dict, whose key may be "greedy_search" if greedy search @@ -649,6 +682,7 @@ def decode_dataset( model=model, sp=sp, decoding_graph=decoding_graph, + context_graph=context_graph, word_table=word_table, batch=batch, LM=LM, @@ -744,6 +778,11 @@ def main(): ) params.res_dir = params.exp_dir / params.decoding_method + if os.path.exists(params.context_file): + params.has_contexts = True + else: + params.has_contexts = False + if params.iter > 0: params.suffix = f"iter-{params.iter}-avg-{params.avg}" else: @@ -770,6 +809,12 @@ def main(): params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" elif "beam_search" in params.decoding_method: params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + if params.decoding_method in ( + "modified_beam_search", + "modified_beam_search_LODR", + ): + if params.has_contexts: + params.suffix += f"-context-score-{params.context_score}" else: params.suffix += f"-context-{params.context_size}" params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" @@ -952,6 +997,18 @@ def main(): decoding_graph = None word_table = None + if "modified_beam_search" in params.decoding_method: + if os.path.exists(params.context_file): + contexts = [] + for line in open(params.context_file).readlines(): + contexts.append(line.strip()) + context_graph = ContextGraph(params.context_score) + context_graph.build(sp.encode(contexts)) + else: + context_graph = None + else: + context_graph = None + num_param = sum([p.numel() for p in model.parameters()]) logging.info(f"Number of model parameters: {num_param}") @@ -976,6 +1033,7 @@ def main(): sp=sp, word_table=word_table, decoding_graph=decoding_graph, + context_graph=context_graph, LM=LM, ngram_lm=ngram_lm, ngram_lm_scale=ngram_lm_scale, diff --git a/icefall/context_graph.py b/icefall/context_graph.py index c78de30f6..01836df04 100644 --- a/icefall/context_graph.py +++ b/icefall/context_graph.py @@ -29,7 +29,7 @@ class ContextState: token: int, token_score: float, node_score: float, - local_node_score: float, + output_score: float, is_end: bool, ): """Create a ContextState. @@ -40,16 +40,15 @@ class ContextState: The id of the root node is always 0. token: The token id. - score: + token_score: The bonus for each token during decoding, which will hopefully boost the token up to survive beam search. node_score: The accumulated bonus from root of graph to current node, it will be used to calculate the score for fail arc. - local_node_score: - The accumulated bonus from last ``end_node``(node with is_end true) - to current_node, it will be used to calculate the score for fail arc. - Node: The local_node_score of a ``end_node`` is 0. + output_score: + The total scores of matched phrases, sum of the node_score of all + the output node for current node. is_end: True if current token is the end of a context. """ @@ -57,7 +56,7 @@ class ContextState: self.token = token self.token_score = token_score self.node_score = node_score - self.local_node_score = local_node_score + self.output_score = output_score self.is_end = is_end self.next = {} self.fail = None @@ -93,7 +92,7 @@ class ContextGraph: token=-1, token_score=0, node_score=0, - local_node_score=0, + output_score=0, is_end=False, ) self.root.fail = self.root @@ -131,6 +130,7 @@ class ContextGraph: output = None break node.output = output + node.output_score += 0 if output is None else output.output_score queue.append(node) def build(self, token_ids: List[List[int]]): @@ -153,14 +153,13 @@ class ContextGraph: if token not in node.next: self.num_nodes += 1 is_end = i == len(tokens) - 1 + node_score = node.node_score + self.context_score node.next[token] = ContextState( id=self.num_nodes, token=token, token_score=self.context_score, - node_score=node.node_score + self.context_score, - local_node_score=0 - if is_end - else (node.local_node_score + self.context_score), + node_score=node_score, + output_score=node_score if is_end else 0, is_end=is_end, ) node = node.next[token] @@ -186,8 +185,6 @@ class ContextGraph: if token in state.next: node = state.next[token] score = node.token_score - if state.is_end: - score += state.node_score else: # token not matched # We will trace along the fail arc until it matches the token or reaching @@ -202,14 +199,9 @@ class ContextGraph: node = node.next[token] # The score of the fail path - score = node.node_score - state.local_node_score + score = node.node_score - state.node_score assert node is not None - matched_score = 0 - output = node.output - while output is not None: - matched_score += output.node_score - output = output.output - return (score + matched_score, node) + return (score + node.output_score, node) def finalize(self, state: ContextState) -> Tuple[float, ContextState]: """When reaching the end of the decoded sequence, we need to finalize @@ -227,8 +219,6 @@ class ContextGraph: """ # The score of the fail arc score = -state.node_score - if state.is_end: - score = 0 return (score, self.root) def draw( @@ -307,10 +297,8 @@ class ContextGraph: for token, node in current_node.next.items(): if node.id not in seen: node_score = f"{node.node_score:.2f}".rstrip("0").rstrip(".") - local_node_score = f"{node.local_node_score:.2f}".rstrip( - "0" - ).rstrip(".") - label = f"{node.id}/({node_score},{local_node_score})" + output_score = f"{node.output_score:.2f}".rstrip("0").rstrip(".") + label = f"{node.id}/({node_score}, {output_score})" if node.is_end: dot.node(str(node.id), label=label, **final_state_attr) else: @@ -391,6 +379,7 @@ if __name__ == "__main__": "HERSHE": 12, # "HE", "HERS", "S", "SHE", "HE" "HISHE": 9, # "HIS", "S", "SHE", "HE" "SHED": 6, # "S", "SHE", "HE" + "SHELF": 6, # "S", "SHE", "HE" "HELL": 2, # "HE" "HELLO": 7, # "HE", "HELLO" "DHRHISQ": 4, # "HIS", "S" From 3a1ce5963b67413b5d274895a1156e20dc30c3be Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:39:48 +0800 Subject: [PATCH 074/100] Minor fix for documentation (#1229) --- docs/source/decoding-with-langugage-models/LODR.rst | 5 ++++- docs/source/decoding-with-langugage-models/rescoring.rst | 5 ++++- .../source/decoding-with-langugage-models/shallow-fusion.rst | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/source/decoding-with-langugage-models/LODR.rst b/docs/source/decoding-with-langugage-models/LODR.rst index b6625ee1d..8cc1a624c 100644 --- a/docs/source/decoding-with-langugage-models/LODR.rst +++ b/docs/source/decoding-with-langugage-models/LODR.rst @@ -71,9 +71,12 @@ As the initial step, let's download the pre-trained model. .. code-block:: bash $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 - $ pushd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ cd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp $ git lfs pull --include "pretrained.pt" $ ln -s pretrained.pt epoch-99.pt # create a symbolic link so that the checkpoint can be loaded + $ cd ../data/lang_bpe_500 + $ git lfs pull --include bpe.model + $ cd ../../.. To test the model, let's have a look at the decoding results **without** using LM. This can be done via the following command: diff --git a/docs/source/decoding-with-langugage-models/rescoring.rst b/docs/source/decoding-with-langugage-models/rescoring.rst index 02eba9129..4cabaa432 100644 --- a/docs/source/decoding-with-langugage-models/rescoring.rst +++ b/docs/source/decoding-with-langugage-models/rescoring.rst @@ -34,9 +34,12 @@ As the initial step, let's download the pre-trained model. .. code-block:: bash $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 - $ pushd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ cd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp $ git lfs pull --include "pretrained.pt" $ ln -s pretrained.pt epoch-99.pt # create a symbolic link so that the checkpoint can be loaded + $ cd ../data/lang_bpe_500 + $ git lfs pull --include bpe.model + $ cd ../../.. As usual, we first test the model's performance without external LM. This can be done via the following command: diff --git a/docs/source/decoding-with-langugage-models/shallow-fusion.rst b/docs/source/decoding-with-langugage-models/shallow-fusion.rst index f15e3f1d9..684fefeb4 100644 --- a/docs/source/decoding-with-langugage-models/shallow-fusion.rst +++ b/docs/source/decoding-with-langugage-models/shallow-fusion.rst @@ -32,9 +32,12 @@ As the initial step, let's download the pre-trained model. .. code-block:: bash $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29 - $ pushd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp + $ cd icefall-asr-librispeech-pruned-transducer-stateless7-streaming-2022-12-29/exp $ git lfs pull --include "pretrained.pt" $ ln -s pretrained.pt epoch-99.pt # create a symbolic link so that the checkpoint can be loaded + $ cd ../data/lang_bpe_500 + $ git lfs pull --include bpe.model + $ cd ../../.. To test the model, let's have a look at the decoding results without using LM. This can be done via the following command: From 8fcadb68a7cde093069e89830832e1ac728338fe Mon Sep 17 00:00:00 2001 From: Desh Raj Date: Wed, 30 Aug 2023 22:31:05 -0400 Subject: [PATCH 075/100] Missing definitions in scaling.py added (#1232) --- egs/libricss/SURT/dprnn_zipformer/scaling.py | 1577 +++++++++++++++++- 1 file changed, 1576 insertions(+), 1 deletion(-) mode change 120000 => 100644 egs/libricss/SURT/dprnn_zipformer/scaling.py diff --git a/egs/libricss/SURT/dprnn_zipformer/scaling.py b/egs/libricss/SURT/dprnn_zipformer/scaling.py deleted file mode 120000 index 5f9be9fe0..000000000 --- a/egs/libricss/SURT/dprnn_zipformer/scaling.py +++ /dev/null @@ -1 +0,0 @@ -../../../librispeech/ASR/pruned_transducer_stateless7/scaling.py \ No newline at end of file diff --git a/egs/libricss/SURT/dprnn_zipformer/scaling.py b/egs/libricss/SURT/dprnn_zipformer/scaling.py new file mode 100644 index 000000000..4040a7b89 --- /dev/null +++ b/egs/libricss/SURT/dprnn_zipformer/scaling.py @@ -0,0 +1,1576 @@ +# Copyright 2022 Xiaomi Corp. (authors: Daniel Povey) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 random +from typing import Optional, Tuple, Union + +import torch +import torch.backends.cudnn.rnn as rnn +import torch.nn as nn +from torch import _VF, Tensor + +from icefall.utils import is_jit_tracing + + +class ActivationBalancerFunction(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: Tensor, + scale_factor: Tensor, + sign_factor: Optional[Tensor], + channel_dim: int, + ) -> Tensor: + if channel_dim < 0: + channel_dim += x.ndim + ctx.channel_dim = channel_dim + xgt0 = x > 0 + if sign_factor is None: + ctx.save_for_backward(xgt0, scale_factor) + else: + ctx.save_for_backward(xgt0, scale_factor, sign_factor) + return x + + @staticmethod + def backward(ctx, x_grad: Tensor) -> Tuple[Tensor, None, None, None]: + if len(ctx.saved_tensors) == 3: + xgt0, scale_factor, sign_factor = ctx.saved_tensors + for _ in range(ctx.channel_dim, x_grad.ndim - 1): + scale_factor = scale_factor.unsqueeze(-1) + sign_factor = sign_factor.unsqueeze(-1) + factor = sign_factor + scale_factor * (xgt0.to(x_grad.dtype) - 0.5) + else: + xgt0, scale_factor = ctx.saved_tensors + for _ in range(ctx.channel_dim, x_grad.ndim - 1): + scale_factor = scale_factor.unsqueeze(-1) + factor = scale_factor * (xgt0.to(x_grad.dtype) - 0.5) + neg_delta_grad = x_grad.abs() * factor + return ( + x_grad - neg_delta_grad, + None, + None, + None, + ) + + +def _compute_scale_factor( + x: Tensor, + channel_dim: int, + min_abs: float, + max_abs: float, + gain_factor: float, + max_factor: float, +) -> Tensor: + if channel_dim < 0: + channel_dim += x.ndim + sum_dims = [d for d in range(x.ndim) if d != channel_dim] + x_abs_mean = torch.mean(x.abs(), dim=sum_dims).to(torch.float32) + + if min_abs == 0.0: + below_threshold = 0.0 + else: + # below_threshold is 0 if x_abs_mean > min_abs, can be at most max_factor if + # x_abs)_mean , min_abs. + below_threshold = ((min_abs - x_abs_mean) * (gain_factor / min_abs)).clamp( + min=0, max=max_factor + ) + + above_threshold = ((x_abs_mean - max_abs) * (gain_factor / max_abs)).clamp( + min=0, max=max_factor + ) + + return below_threshold - above_threshold + + +def _compute_sign_factor( + x: Tensor, + channel_dim: int, + min_positive: float, + max_positive: float, + gain_factor: float, + max_factor: float, +) -> Tensor: + if channel_dim < 0: + channel_dim += x.ndim + sum_dims = [d for d in range(x.ndim) if d != channel_dim] + proportion_positive = torch.mean((x > 0).to(torch.float32), dim=sum_dims) + if min_positive == 0.0: + factor1 = 0.0 + else: + # 0 if proportion_positive >= min_positive, else can be + # as large as max_factor. + factor1 = ( + (min_positive - proportion_positive) * (gain_factor / min_positive) + ).clamp_(min=0, max=max_factor) + + if max_positive == 1.0: + factor2 = 0.0 + else: + # 0 if self.proportion_positive <= max_positive, else can be + # as large as -max_factor. + factor2 = ( + (proportion_positive - max_positive) * (gain_factor / (1.0 - max_positive)) + ).clamp_(min=0, max=max_factor) + sign_factor = factor1 - factor2 + # require min_positive != 0 or max_positive != 1: + assert not isinstance(sign_factor, float) + return sign_factor + + +class ActivationScaleBalancerFunction(torch.autograd.Function): + """ + This object is used in class ActivationBalancer when the user specified + min_positive=0, max_positive=1, so there are no constraints on the signs + of the activations and only the absolute value has a constraint. + """ + + @staticmethod + def forward( + ctx, + x: Tensor, + sign_factor: Tensor, + scale_factor: Tensor, + channel_dim: int, + ) -> Tensor: + if channel_dim < 0: + channel_dim += x.ndim + ctx.channel_dim = channel_dim + xgt0 = x > 0 + ctx.save_for_backward(xgt0, sign_factor, scale_factor) + return x + + @staticmethod + def backward(ctx, x_grad: Tensor) -> Tuple[Tensor, None, None, None]: + xgt0, sign_factor, scale_factor = ctx.saved_tensors + for _ in range(ctx.channel_dim, x_grad.ndim - 1): + sign_factor = sign_factor.unsqueeze(-1) + scale_factor = scale_factor.unsqueeze(-1) + + factor = sign_factor + scale_factor * (xgt0.to(x_grad.dtype) - 0.5) + neg_delta_grad = x_grad.abs() * factor + return ( + x_grad - neg_delta_grad, + None, + None, + None, + ) + + +class RandomClampFunction(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: Tensor, + min: Optional[float], + max: Optional[float], + prob: float, + reflect: float, + ) -> Tensor: + x_clamped = torch.clamp(x, min=min, max=max) + mask = torch.rand_like(x) < prob + ans = torch.where(mask, x_clamped, x) + if x.requires_grad: + ctx.save_for_backward(ans == x) + ctx.reflect = reflect + if reflect != 0.0: + ans = ans * (1.0 + reflect) - (x * reflect) + return ans + + @staticmethod + def backward(ctx, ans_grad: Tensor) -> Tuple[Tensor, None, None, None, None]: + (is_same,) = ctx.saved_tensors + x_grad = ans_grad * is_same.to(ans_grad.dtype) + reflect = ctx.reflect + if reflect != 0.0: + x_grad = x_grad * (1.0 + reflect) - (ans_grad * reflect) + return x_grad, None, None, None, None + + +def random_clamp( + x: Tensor, + min: Optional[float] = None, + max: Optional[float] = None, + prob: float = 0.5, + reflect: float = 0.0, +): + return RandomClampFunction.apply(x, min, max, prob, reflect) + + +def random_cast_to_half(x: Tensor, min_abs: float = 5.0e-06) -> Tensor: + """ + A randomized way of casting a floating point value to half precision. + """ + if x.dtype == torch.float16: + return x + x_abs = x.abs() + is_too_small = x_abs < min_abs + # for elements where is_too_small is true, random_val will contain +-min_abs with + # probability (x.abs() / min_abs), and 0.0 otherwise. [so this preserves expectations, + # for those elements]. + random_val = min_abs * x.sign() * (torch.rand_like(x) * min_abs < x_abs) + return torch.where(is_too_small, random_val, x).to(torch.float16) + + +class RandomGradFunction(torch.autograd.Function): + """ + Does nothing in forward pass; in backward pass, gets rid of very small grads using + randomized approach that preserves expectations (intended to reduce roundoff). + """ + + @staticmethod + def forward(ctx, x: Tensor, min_abs: float) -> Tensor: + ctx.min_abs = min_abs + return x + + @staticmethod + def backward(ctx, ans_grad: Tensor) -> Tuple[Tensor, None]: + if ans_grad.dtype == torch.float16: + return ( + random_cast_to_half(ans_grad.to(torch.float32), min_abs=ctx.min_abs), + None, + ) + else: + return ans_grad, None + + +class RandomGrad(torch.nn.Module): + """ + Gets rid of very small gradients using an expectation-preserving method, intended to increase + accuracy of training when using amp (automatic mixed precision) + """ + + def __init__(self, min_abs: float = 5.0e-06): + super(RandomGrad, self).__init__() + self.min_abs = min_abs + + def forward(self, x: Tensor): + if torch.jit.is_scripting() or not self.training or torch.jit.is_tracing(): + return x + else: + return RandomGradFunction.apply(x, self.min_abs) + + +class SoftmaxFunction(torch.autograd.Function): + """ + Tries to handle half-precision derivatives in a randomized way that should + be more accurate for training than the default behavior. + """ + + @staticmethod + def forward(ctx, x: Tensor, dim: int): + ans = x.softmax(dim=dim) + # if x dtype is float16, x.softmax() returns a float32 because + # (presumably) that op does not support float16, and autocast + # is enabled. + if torch.is_autocast_enabled(): + ans = ans.to(torch.float16) + ctx.save_for_backward(ans) + ctx.x_dtype = x.dtype + ctx.dim = dim + return ans + + @staticmethod + def backward(ctx, ans_grad: Tensor): + (ans,) = ctx.saved_tensors + with torch.cuda.amp.autocast(enabled=False): + ans_grad = ans_grad.to(torch.float32) + ans = ans.to(torch.float32) + x_grad = ans_grad * ans + x_grad = x_grad - ans * x_grad.sum(dim=ctx.dim, keepdim=True) + return x_grad, None + + +def softmax(x: Tensor, dim: int): + if torch.jit.is_scripting() or torch.jit.is_tracing(): + return x.softmax(dim) + + return SoftmaxFunction.apply(x, dim) + + +class MaxEigLimiterFunction(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: Tensor, + coeffs: Tensor, + direction: Tensor, + channel_dim: int, + grad_scale: float, + ) -> Tensor: + ctx.channel_dim = channel_dim + ctx.grad_scale = grad_scale + ctx.save_for_backward(x.detach(), coeffs.detach(), direction.detach()) + return x + + @staticmethod + def backward(ctx, x_grad, *args): + with torch.enable_grad(): + (x_orig, coeffs, new_direction) = ctx.saved_tensors + x_orig.requires_grad = True + num_channels = x_orig.shape[ctx.channel_dim] + x = x_orig.transpose(ctx.channel_dim, -1).reshape(-1, num_channels) + new_direction.requires_grad = False + x = x - x.mean(dim=0) + x_var = (x**2).mean() + x_residual = x - coeffs * new_direction + x_residual_var = (x_residual**2).mean() + # `variance_proportion` is the proportion of the variance accounted for + # by the top eigen-direction. This is to be minimized. + variance_proportion = (x_var - x_residual_var) / (x_var + 1.0e-20) + variance_proportion.backward() + x_orig_grad = x_orig.grad + x_extra_grad = ( + x_orig.grad + * ctx.grad_scale + * x_grad.norm() + / (x_orig_grad.norm() + 1.0e-20) + ) + return x_grad + x_extra_grad.detach(), None, None, None, None + + +class GradientFilterFunction(torch.autograd.Function): + @staticmethod + def forward( + ctx, + x: Tensor, + batch_dim: int, # e.g., 1 + threshold: float, # e.g., 10.0 + *params: Tensor, # module parameters + ) -> Tuple[Tensor, ...]: + if x.requires_grad: + if batch_dim < 0: + batch_dim += x.ndim + ctx.batch_dim = batch_dim + ctx.threshold = threshold + return (x,) + params + + @staticmethod + def backward( + ctx, + x_grad: Tensor, + *param_grads: Tensor, + ) -> Tuple[Tensor, ...]: + eps = 1.0e-20 + dim = ctx.batch_dim + norm_dims = [d for d in range(x_grad.ndim) if d != dim] + norm_of_batch = (x_grad**2).mean(dim=norm_dims, keepdim=True).sqrt() + median_norm = norm_of_batch.median() + + cutoff = median_norm * ctx.threshold + inv_mask = (cutoff + norm_of_batch) / (cutoff + eps) + mask = 1.0 / (inv_mask + eps) + x_grad = x_grad * mask + + avg_mask = 1.0 / (inv_mask.mean() + eps) + param_grads = [avg_mask * g for g in param_grads] + + return (x_grad, None, None) + tuple(param_grads) + + +class GradientFilter(torch.nn.Module): + """This is used to filter out elements that have extremely large gradients + in batch and the module parameters with soft masks. + + Args: + batch_dim (int): + The batch dimension. + threshold (float): + For each element in batch, its gradient will be + filtered out if the gradient norm is larger than + `grad_norm_threshold * median`, where `median` is the median + value of gradient norms of all elememts in batch. + """ + + def __init__(self, batch_dim: int = 1, threshold: float = 10.0): + super(GradientFilter, self).__init__() + self.batch_dim = batch_dim + self.threshold = threshold + + def forward(self, x: Tensor, *params: Tensor) -> Tuple[Tensor, ...]: + if torch.jit.is_scripting() or is_jit_tracing(): + return (x,) + params + else: + return GradientFilterFunction.apply( + x, + self.batch_dim, + self.threshold, + *params, + ) + + +class BasicNorm(torch.nn.Module): + """ + This is intended to be a simpler, and hopefully cheaper, replacement for + LayerNorm. The observation this is based on, is that Transformer-type + networks, especially with pre-norm, sometimes seem to set one of the + feature dimensions to a large constant value (e.g. 50), which "defeats" + the LayerNorm because the output magnitude is then not strongly dependent + on the other (useful) features. Presumably the weight and bias of the + LayerNorm are required to allow it to do this. + + So the idea is to introduce this large constant value as an explicit + parameter, that takes the role of the "eps" in LayerNorm, so the network + doesn't have to do this trick. We make the "eps" learnable. + + Args: + num_channels: the number of channels, e.g. 512. + channel_dim: the axis/dimension corresponding to the channel, + interprted as an offset from the input's ndim if negative. + shis is NOT the num_channels; it should typically be one of + {-2, -1, 0, 1, 2, 3}. + eps: the initial "epsilon" that we add as ballast in: + scale = ((input_vec**2).mean() + epsilon)**-0.5 + Note: our epsilon is actually large, but we keep the name + to indicate the connection with conventional LayerNorm. + learn_eps: if true, we learn epsilon; if false, we keep it + at the initial value. + eps_min: float + eps_max: float + """ + + def __init__( + self, + num_channels: int, + channel_dim: int = -1, # CAUTION: see documentation. + eps: float = 0.25, + learn_eps: bool = True, + eps_min: float = -3.0, + eps_max: float = 3.0, + ) -> None: + super(BasicNorm, self).__init__() + self.num_channels = num_channels + self.channel_dim = channel_dim + if learn_eps: + self.eps = nn.Parameter(torch.tensor(eps).log().detach()) + else: + self.register_buffer("eps", torch.tensor(eps).log().detach()) + self.eps_min = eps_min + self.eps_max = eps_max + + def forward(self, x: Tensor) -> Tensor: + assert x.shape[self.channel_dim] == self.num_channels + eps = self.eps + if self.training and random.random() < 0.25: + # with probability 0.25, in training mode, clamp eps between the min + # and max; this will encourage it to learn parameters within the + # allowed range by making parameters that are outside the allowed + # range noisy. + + # gradients to allow the parameter to get back into the allowed + # region if it happens to exit it. + eps = eps.clamp(min=self.eps_min, max=self.eps_max) + scales = ( + torch.mean(x**2, dim=self.channel_dim, keepdim=True) + eps.exp() + ) ** -0.5 + return x * scales + + +class ScaledEmbedding(nn.Module): + r"""This is a modified version of nn.Embedding that introduces a learnable scale + on the parameters. Note: due to how we initialize it, it's best used with + schedulers like Noam that have a warmup period. + + It is a simple lookup table that stores embeddings of a fixed dictionary and size. + + This module is often used to store word embeddings and retrieve them using indices. + The input to the module is a list of indices, and the output is the corresponding + word embeddings. + + Args: + num_embeddings (int): size of the dictionary of embeddings + embedding_dim (int): the size of each embedding vector + padding_idx (int, optional): If given, pads the output with the embedding vector at :attr:`padding_idx` + (initialized to zeros) whenever it encounters the index. + scale_grad_by_freq (boolean, optional): If given, this will scale gradients by the inverse of frequency of + the words in the mini-batch. Default ``False``. + sparse (bool, optional): If ``True``, gradient w.r.t. :attr:`weight` matrix will be a sparse tensor. + See Notes for more details regarding sparse gradients. + + initial_speed (float, optional): This affects how fast the parameter will + learn near the start of training; you can set it to a value less than + one if you suspect that a module is contributing to instability near + the start of training. Note: regardless of the use of this option, + it's best to use schedulers like Noam that have a warm-up period. + Alternatively you can set it to more than 1 if you want it to + initially train faster. Must be greater than 0. + + + Attributes: + weight (Tensor): the learnable weights of the module of shape (num_embeddings, embedding_dim) + initialized from :math:`\mathcal{N}(0, 1)` + + Shape: + - Input: :math:`(*)`, LongTensor of arbitrary shape containing the indices to extract + - Output: :math:`(*, H)`, where `*` is the input shape and :math:`H=\text{embedding\_dim}` + + .. note:: + Keep in mind that only a limited number of optimizers support + sparse gradients: currently it's :class:`optim.SGD` (`CUDA` and `CPU`), + :class:`optim.SparseAdam` (`CUDA` and `CPU`) and :class:`optim.Adagrad` (`CPU`) + + .. note:: + With :attr:`padding_idx` set, the embedding vector at + :attr:`padding_idx` is initialized to all zeros. However, note that this + vector can be modified afterwards, e.g., using a customized + initialization method, and thus changing the vector used to pad the + output. The gradient for this vector from :class:`~torch.nn.Embedding` + is always zero. + + Examples:: + + >>> # an Embedding module containing 10 tensors of size 3 + >>> embedding = nn.Embedding(10, 3) + >>> # a batch of 2 samples of 4 indices each + >>> input = torch.LongTensor([[1,2,4,5],[4,3,2,9]]) + >>> embedding(input) + tensor([[[-0.0251, -1.6902, 0.7172], + [-0.6431, 0.0748, 0.6969], + [ 1.4970, 1.3448, -0.9685], + [-0.3677, -2.7265, -0.1685]], + + [[ 1.4970, 1.3448, -0.9685], + [ 0.4362, -0.4004, 0.9400], + [-0.6431, 0.0748, 0.6969], + [ 0.9124, -2.3616, 1.1151]]]) + + + >>> # example with padding_idx + >>> embedding = nn.Embedding(10, 3, padding_idx=0) + >>> input = torch.LongTensor([[0,2,0,5]]) + >>> embedding(input) + tensor([[[ 0.0000, 0.0000, 0.0000], + [ 0.1535, -2.0309, 0.9315], + [ 0.0000, 0.0000, 0.0000], + [-0.1655, 0.9897, 0.0635]]]) + + """ + __constants__ = [ + "num_embeddings", + "embedding_dim", + "padding_idx", + "scale_grad_by_freq", + "sparse", + ] + + num_embeddings: int + embedding_dim: int + padding_idx: int + scale_grad_by_freq: bool + weight: Tensor + sparse: bool + + def __init__( + self, + num_embeddings: int, + embedding_dim: int, + padding_idx: Optional[int] = None, + scale_grad_by_freq: bool = False, + sparse: bool = False, + initial_speed: float = 1.0, + ) -> None: + super(ScaledEmbedding, self).__init__() + self.num_embeddings = num_embeddings + self.embedding_dim = embedding_dim + if padding_idx is not None: + if padding_idx > 0: + assert ( + padding_idx < self.num_embeddings + ), "Padding_idx must be within num_embeddings" + elif padding_idx < 0: + assert ( + padding_idx >= -self.num_embeddings + ), "Padding_idx must be within num_embeddings" + padding_idx = self.num_embeddings + padding_idx + self.padding_idx = padding_idx + self.scale_grad_by_freq = scale_grad_by_freq + + self.scale = nn.Parameter(torch.zeros(())) # see reset_parameters() + self.sparse = sparse + + self.weight = nn.Parameter(torch.Tensor(num_embeddings, embedding_dim)) + self.reset_parameters(initial_speed) + + def reset_parameters(self, initial_speed: float = 1.0) -> None: + std = 0.1 / initial_speed + nn.init.normal_(self.weight, std=std) + nn.init.constant_(self.scale, torch.tensor(1.0 / std).log()) + + if self.padding_idx is not None: + with torch.no_grad(): + self.weight[self.padding_idx].fill_(0) + + def forward(self, input: Tensor) -> Tensor: + F = torch.nn.functional + scale = self.scale.exp() + if input.numel() < self.num_embeddings: + return ( + F.embedding( + input, + self.weight, + self.padding_idx, + None, + 2.0, # None, 2.0 relate to normalization + self.scale_grad_by_freq, + self.sparse, + ) + * scale + ) + else: + return F.embedding( + input, + self.weight * scale, + self.padding_idx, + None, + 2.0, # None, 2.0 relates to normalization + self.scale_grad_by_freq, + self.sparse, + ) + + def extra_repr(self) -> str: + # s = "{num_embeddings}, {embedding_dim}, scale={scale}" + s = "{num_embeddings}, {embedding_dim}" + if self.padding_idx is not None: + s += ", padding_idx={padding_idx}" + if self.scale_grad_by_freq is not False: + s += ", scale_grad_by_freq={scale_grad_by_freq}" + if self.sparse is not False: + s += ", sparse=True" + return s.format(**self.__dict__) + + +def ScaledLinear(*args, initial_scale: float = 1.0, **kwargs) -> nn.Linear: + """ + Behaves like a constructor of a modified version of nn.Linear + that gives an easy way to set the default initial parameter scale. + + Args: + Accepts the standard args and kwargs that nn.Linear accepts + e.g. in_features, out_features, bias=False. + + initial_scale: you can override this if you want to increase + or decrease the initial magnitude of the module's output + (affects the initialization of weight_scale and bias_scale). + Another option, if you want to do something like this, is + to re-initialize the parameters. + """ + ans = nn.Linear(*args, **kwargs) + with torch.no_grad(): + ans.weight[:] *= initial_scale + if ans.bias is not None: + torch.nn.init.uniform_(ans.bias, -0.1 * initial_scale, 0.1 * initial_scale) + return ans + + +def ScaledConv1d(*args, initial_scale: float = 1.0, **kwargs) -> nn.Conv1d: + """ + Behaves like a constructor of a modified version of nn.Conv1d + that gives an easy way to set the default initial parameter scale. + + Args: + Accepts the standard args and kwargs that nn.Linear accepts + e.g. in_features, out_features, bias=False. + + initial_scale: you can override this if you want to increase + or decrease the initial magnitude of the module's output + (affects the initialization of weight_scale and bias_scale). + Another option, if you want to do something like this, is + to re-initialize the parameters. + """ + ans = nn.Conv1d(*args, **kwargs) + with torch.no_grad(): + ans.weight[:] *= initial_scale + if ans.bias is not None: + torch.nn.init.uniform_(ans.bias, -0.1 * initial_scale, 0.1 * initial_scale) + return ans + + +class ScaledLSTM(nn.LSTM): + # See docs for ScaledLinear. + # This class implements LSTM with scaling mechanism, using `torch._VF.lstm` + # Please refer to https://github.com/pytorch/pytorch/blob/master/torch/nn/modules/rnn.py + def __init__( + self, + *args, + initial_scale: float = 1.0, + initial_speed: float = 1.0, + grad_norm_threshold: float = 10.0, + **kwargs, + ): + super(ScaledLSTM, self).__init__(*args, **kwargs) + initial_scale = torch.tensor(initial_scale).log() + self._scales_names = [] + self._scales = [] + self.batch_dim = 0 if self.batch_first else 1 + self.num_directions = 1 + int(self.bidirectional) + for name in self._flat_weights_names: + scale_name = name + "_scale" + self._scales_names.append(scale_name) + param = nn.Parameter(initial_scale.clone().detach()) + setattr(self, scale_name, param) + self._scales.append(param) + + self.grad_filter = GradientFilter( + batch_dim=self.batch_dim, threshold=grad_norm_threshold + ) + + self._reset_parameters( + initial_speed + ) # Overrides the reset_parameters in base class + + def _reset_parameters(self, initial_speed: float): + std = 0.1 / initial_speed + a = (3**0.5) * std + scale = self.hidden_size**-0.5 + v = scale / std + for idx, name in enumerate(self._flat_weights_names): + if "weight" in name: + nn.init.uniform_(self._flat_weights[idx], -a, a) + with torch.no_grad(): + self._scales[idx] += torch.tensor(v).log() + elif "bias" in name: + nn.init.constant_(self._flat_weights[idx], 0.0) + + def _flatten_parameters(self, flat_weights) -> None: + """Resets parameter data pointer so that they can use faster code paths. + + Right now, this works only if the module is on the GPU and cuDNN is enabled. + Otherwise, it's a no-op. + + This function is modified from https://github.com/pytorch/pytorch/blob/master/torch/nn/modules/rnn.py # noqa + """ + # Short-circuits if _flat_weights is only partially instantiated + if len(flat_weights) != len(self._flat_weights_names): + return + + for w in flat_weights: + if not isinstance(w, Tensor): + return + # Short-circuits if any tensor in flat_weights is not acceptable to cuDNN + # or the tensors in flat_weights are of different dtypes + + first_fw = flat_weights[0] + dtype = first_fw.dtype + for fw in flat_weights: + if ( + not isinstance(fw.data, Tensor) + or not (fw.data.dtype == dtype) + or not fw.data.is_cuda + or not torch.backends.cudnn.is_acceptable(fw.data) + ): + return + + # If any parameters alias, we fall back to the slower, copying code path. This is + # a sufficient check, because overlapping parameter buffers that don't completely + # alias would break the assumptions of the uniqueness check in + # Module.named_parameters(). + unique_data_ptrs = set(p.data_ptr() for p in flat_weights) + if len(unique_data_ptrs) != len(flat_weights): + return + + with torch.cuda.device_of(first_fw): + + # Note: no_grad() is necessary since _cudnn_rnn_flatten_weight is + # an inplace operation on self._flat_weights + with torch.no_grad(): + if torch._use_cudnn_rnn_flatten_weight(): + num_weights = 4 if self.bias else 2 + if self.proj_size > 0: + num_weights += 1 + torch._cudnn_rnn_flatten_weight( + flat_weights, + num_weights, + self.input_size, + rnn.get_cudnn_mode(self.mode), + self.hidden_size, + self.proj_size, + self.num_layers, + self.batch_first, + bool(self.bidirectional), + ) + + def _get_flat_weights(self): + """Get scaled weights, and resets their data pointer.""" + flat_weights = [] + for idx in range(len(self._flat_weights_names)): + flat_weights.append(self._flat_weights[idx] * self._scales[idx].exp()) + self._flatten_parameters(flat_weights) + return flat_weights + + def forward(self, input: Tensor, hx: Optional[Tuple[Tensor, Tensor]] = None): + # This function is modified from https://github.com/pytorch/pytorch/blob/master/torch/nn/modules/rnn.py # noqa + # The change for calling `_VF.lstm()` is: + # self._flat_weights -> self._get_flat_weights() + if hx is None: + h_zeros = torch.zeros( + self.num_layers * self.num_directions, + input.size(self.batch_dim), + self.proj_size if self.proj_size > 0 else self.hidden_size, + dtype=input.dtype, + device=input.device, + ) + c_zeros = torch.zeros( + self.num_layers * self.num_directions, + input.size(self.batch_dim), + self.hidden_size, + dtype=input.dtype, + device=input.device, + ) + hx = (h_zeros, c_zeros) + + self.check_forward_args(input, hx, None) + + flat_weights = self._get_flat_weights() + input, *flat_weights = self.grad_filter(input, *flat_weights) + + result = _VF.lstm( + input, + hx, + flat_weights, + self.bias, + self.num_layers, + self.dropout, + self.training, + self.bidirectional, + self.batch_first, + ) + + output = result[0] + hidden = result[1:] + return output, hidden + + +class ActivationBalancer(torch.nn.Module): + """ + Modifies the backpropped derivatives of a function to try to encourage, for + each channel, that it is positive at least a proportion `threshold` of the + time. It does this by multiplying negative derivative values by up to + (1+max_factor), and positive derivative values by up to (1-max_factor), + interpolated from 1 at the threshold to those extremal values when none + of the inputs are positive. + + Args: + num_channels: the number of channels + channel_dim: the dimension/axis corresponding to the channel, e.g. + -1, 0, 1, 2; will be interpreted as an offset from x.ndim if negative. + min_positive: the minimum, per channel, of the proportion of the time + that (x > 0), below which we start to modify the derivatives. + max_positive: the maximum, per channel, of the proportion of the time + that (x > 0), above which we start to modify the derivatives. + max_factor: the maximum factor by which we modify the derivatives for + either the sign constraint or the magnitude constraint; + e.g. with max_factor=0.02, the the derivatives would be multiplied by + values in the range [0.98..1.02]. + sign_gain_factor: determines the 'gain' with which we increase the + change in gradient once the constraints on min_positive and max_positive + are violated. + scale_gain_factor: determines the 'gain' with which we increase the + change in gradient once the constraints on min_abs and max_abs + are violated. + min_abs: the minimum average-absolute-value difference from the mean + value per channel, which we allow, before we start to modify + the derivatives to prevent this. + max_abs: the maximum average-absolute-value difference from the mean + value per channel, which we allow, before we start to modify + the derivatives to prevent this. + min_prob: determines the minimum probability with which we modify the + gradients for the {min,max}_positive and {min,max}_abs constraints, + on each forward(). This is done randomly to prevent all layers + from doing it at the same time. Early in training we may use + higher probabilities than this; it will decay to this value. + """ + + def __init__( + self, + num_channels: int, + channel_dim: int, + min_positive: float = 0.05, + max_positive: float = 0.95, + max_factor: float = 0.04, + sign_gain_factor: float = 0.01, + scale_gain_factor: float = 0.02, + min_abs: float = 0.2, + max_abs: float = 100.0, + min_prob: float = 0.1, + ): + super(ActivationBalancer, self).__init__() + self.num_channels = num_channels + self.channel_dim = channel_dim + self.min_positive = min_positive + self.max_positive = max_positive + self.max_factor = max_factor + self.min_abs = min_abs + self.max_abs = max_abs + self.min_prob = min_prob + self.sign_gain_factor = sign_gain_factor + self.scale_gain_factor = scale_gain_factor + + # count measures how many times the forward() function has been called. + # We occasionally sync this to a tensor called `count`, that exists to + # make sure it is synced to disk when we load and save the model. + self.cpu_count = 0 + self.register_buffer("count", torch.tensor(0, dtype=torch.int64)) + + def forward(self, x: Tensor) -> Tensor: + if torch.jit.is_scripting() or not x.requires_grad or torch.jit.is_tracing(): + return _no_op(x) + + count = self.cpu_count + self.cpu_count += 1 + + if random.random() < 0.01: + # Occasionally sync self.cpu_count with self.count. + # count affects the decay of 'prob'. don't do this on every iter, + # because syncing with the GPU is slow. + self.cpu_count = max(self.cpu_count, self.count.item()) + self.count.fill_(self.cpu_count) + + # the prob of doing some work exponentially decreases from 0.5 till it hits + # a floor at min_prob (==0.1, by default) + prob = max(self.min_prob, 0.5 ** (1 + (count / 4000.0))) + + if random.random() < prob: + sign_gain_factor = 0.5 + if self.min_positive != 0.0 or self.max_positive != 1.0: + sign_factor = _compute_sign_factor( + x, + self.channel_dim, + self.min_positive, + self.max_positive, + gain_factor=self.sign_gain_factor / prob, + max_factor=self.max_factor, + ) + else: + sign_factor = None + + scale_factor = _compute_scale_factor( + x.detach(), + self.channel_dim, + min_abs=self.min_abs, + max_abs=self.max_abs, + gain_factor=self.scale_gain_factor / prob, + max_factor=self.max_factor, + ) + return ActivationBalancerFunction.apply( + x, + scale_factor, + sign_factor, + self.channel_dim, + ) + else: + return _no_op(x) + + +def penalize_abs_values_gt(x: Tensor, limit: float, penalty: float) -> Tensor: + """ + Returns x unmodified, but in backprop will put a penalty for the excess of + the absolute values of elements of x over the limit "limit". E.g. if + limit == 10.0, then if x has any values over 10 it will get a penalty. + + Caution: the value of this penalty will be affected by grad scaling used + in automatic mixed precision training. For this reasons we use this, + it shouldn't really matter, or may even be helpful; we just use this + to disallow really implausible values of scores to be given to softmax. + """ + x_sign = x.sign() + over_limit = (x.abs() - limit) > 0 + # The following is a memory efficient way to penalize the absolute values of + # x that's over the limit. (The memory efficiency comes when you think + # about which items torch needs to cache for the autograd, and which ones it + # can throw away). The numerical value of aux_loss as computed here will + # actually be larger than it should be, by limit * over_limit.sum(), but it + # has the same derivative as the real aux_loss which is penalty * (x.abs() - + # limit).relu(). + aux_loss = penalty * ((x_sign * over_limit).to(torch.int8) * x) + # note: we don't do sum() here on aux)_loss, but it's as if we had done + # sum() due to how with_loss() works. + x = with_loss(x, aux_loss) + # you must use x for something, or this will be ineffective. + return x + + +def _diag(x: Tensor): # like .diag(), but works for tensors with 3 dims. + if x.ndim == 2: + return x.diag() + else: + (batch, dim, dim) = x.shape + x = x.reshape(batch, dim * dim) + x = x[:, :: dim + 1] + assert x.shape == (batch, dim) + return x + + +def _whitening_metric(x: Tensor, num_groups: int): + """ + Computes the "whitening metric", a value which will be 1.0 if all the eigenvalues of + of the centered feature covariance are the same within each group's covariance matrix + and also between groups. + Args: + x: a Tensor of shape (*, num_channels) + num_groups: the number of groups of channels, a number >=1 that divides num_channels + Returns: + Returns a scalar Tensor that will be 1.0 if the data is "perfectly white" and + greater than 1.0 otherwise. + """ + assert x.dtype != torch.float16 + x = x.reshape(-1, x.shape[-1]) + (num_frames, num_channels) = x.shape + assert num_channels % num_groups == 0 + channels_per_group = num_channels // num_groups + x = x.reshape(num_frames, num_groups, channels_per_group).transpose(0, 1) + # x now has shape (num_groups, num_frames, channels_per_group) + # subtract the mean so we use the centered, not uncentered, covariance. + # My experience has been that when we "mess with the gradients" like this, + # it's better not do anything that tries to move the mean around, because + # that can easily cause instability. + x = x - x.mean(dim=1, keepdim=True) + # x_covar: (num_groups, channels_per_group, channels_per_group) + x_covar = torch.matmul(x.transpose(1, 2), x) + x_covar_mean_diag = _diag(x_covar).mean() + # the following expression is what we'd get if we took the matrix product + # of each covariance and measured the mean of its trace, i.e. + # the same as _diag(torch.matmul(x_covar, x_covar)).mean(). + x_covarsq_mean_diag = (x_covar**2).sum() / (num_groups * channels_per_group) + # this metric will be >= 1.0; the larger it is, the less 'white' the data was. + metric = x_covarsq_mean_diag / (x_covar_mean_diag**2 + 1.0e-20) + return metric + + +class WhiteningPenaltyFunction(torch.autograd.Function): + @staticmethod + def forward( + ctx, x: Tensor, num_groups: int, whitening_limit: float, grad_scale: float + ) -> Tensor: + ctx.save_for_backward(x) + ctx.num_groups = num_groups + ctx.whitening_limit = whitening_limit + ctx.grad_scale = grad_scale + return x + + @staticmethod + def backward(ctx, x_grad: Tensor): + (x_orig,) = ctx.saved_tensors + with torch.enable_grad(): + with torch.cuda.amp.autocast(enabled=False): + x_detached = x_orig.to(torch.float32).detach() + x_detached.requires_grad = True + + metric = _whitening_metric(x_detached, ctx.num_groups) + + if random.random() < 0.005 or __name__ == "__main__": + logging.info( + f"Whitening: num_groups={ctx.num_groups}, num_channels={x_orig.shape[-1]}, " + f"metric={metric.item():.2f} vs. limit={ctx.whitening_limit}" + ) + + (metric - ctx.whitening_limit).relu().backward() + penalty_grad = x_detached.grad + scale = ctx.grad_scale * ( + x_grad.to(torch.float32).norm() / (penalty_grad.norm() + 1.0e-20) + ) + penalty_grad = penalty_grad * scale + return x_grad + penalty_grad.to(x_grad.dtype), None, None, None + + +class Whiten(nn.Module): + def __init__( + self, + num_groups: int, + whitening_limit: float, + prob: Union[float, Tuple[float, float]], + grad_scale: float, + ): + """ + Args: + num_groups: the number of groups to divide the channel dim into before + whitening. We will attempt to make the feature covariance + within each group, after mean subtraction, as "white" as possible, + while having the same trace across all groups. + whitening_limit: a value greater than 1.0, that dictates how much + freedom we have to violate the constraints. 1.0 would mean perfectly + white, with exactly the same trace across groups; larger values + give more freedom. E.g. 2.0. + prob: the probability with which we apply the gradient modification + (also affects the grad scale). May be supplied as a float, + or as a pair (min_prob, max_prob) + + grad_scale: determines the scale on the gradient term from this object, + relative to the rest of the gradient on the attention weights. + E.g. 0.02 (you may want to use smaller values than this if prob is large) + """ + super(Whiten, self).__init__() + assert num_groups >= 1 + assert whitening_limit >= 1 + assert grad_scale >= 0 + self.num_groups = num_groups + self.whitening_limit = whitening_limit + if isinstance(prob, float): + assert 0 < prob <= 1 + self.prob = prob + else: + (self.min_prob, self.max_prob) = prob + assert 0 < self.min_prob < self.max_prob <= 1 + self.prob = self.max_prob + + self.grad_scale = grad_scale + + def forward(self, x: Tensor) -> Tensor: + """ + In the forward pass, this function just returns the input unmodified. + In the backward pass, it will modify the gradients to ensure that the + distribution in each group has close to (lambda times I) as the covariance + after mean subtraction, with the same lambda across groups. + For whitening_limit > 1, there will be more freedom to violate this + constraint. + + Args: + x: the input of shape (*, num_channels) + + Returns: + x, unmodified. You should make sure + you use the returned value, or the graph will be freed + and nothing will happen in backprop. + """ + if not x.requires_grad or random.random() > self.prob or self.grad_scale == 0: + return _no_op(x) + else: + if hasattr(self, "min_prob") and random.random() < 0.25: + # occasionally switch between min_prob and max_prob, based on whether + # we are above or below the threshold. + if ( + _whitening_metric(x.to(torch.float32), self.num_groups) + > self.whitening_limit + ): + # there would be a change to the grad. + self.prob = self.max_prob + else: + self.prob = self.min_prob + + return WhiteningPenaltyFunction.apply( + x, self.num_groups, self.whitening_limit, self.grad_scale + ) + + +class WithLoss(torch.autograd.Function): + @staticmethod + def forward(ctx, x: Tensor, y: Tensor): + ctx.y_shape = y.shape + return x + + @staticmethod + def backward(ctx, ans_grad: Tensor): + return ( + ans_grad, + torch.ones(ctx.y_shape, dtype=ans_grad.dtype, device=ans_grad.device), + ) + + +def with_loss(x, y): + if torch.jit.is_scripting() or torch.jit.is_tracing(): + return x + # returns x but adds y.sum() to the loss function. + return WithLoss.apply(x, y) + + +def _no_op(x: Tensor) -> Tensor: + if torch.jit.is_scripting() or torch.jit.is_tracing(): + return x + else: + # a no-op function that will have a node in the autograd graph, + # to avoid certain bugs relating to backward hooks + return x.chunk(1, dim=-1)[0] + + +class Identity(torch.nn.Module): + def __init__(self): + super(Identity, self).__init__() + + def forward(self, x): + return _no_op(x) + + +class MaxEig(torch.nn.Module): + """ + Modifies the backpropped derivatives of a function to try to discourage + that any given direction in activation space accounts for more than + a specified proportion of the covariance (e.g. 0.2). + + + Args: + num_channels: the number of channels + channel_dim: the dimension/axis corresponding to the channel, e.g. + -1, 0, 1, 2; will be interpreted as an offset from x.ndim if negative. + max_var_per_eig: the maximum proportion of the variance of the + features/channels, after mean subtraction, that can come from + any given eigenvalue. + min_prob: the minimum probability with which we apply this during any invocation + of forward(), assuming last time we applied the constraint it was + not active; supplied for speed. + scale: determines the scale with which we modify the gradients, relative + to the existing / unmodified gradients + """ + + def __init__( + self, + num_channels: int, + channel_dim: int, + max_var_per_eig: float = 0.2, + min_prob: float = 0.01, + scale: float = 0.01, + ): + super(MaxEig, self).__init__() + self.num_channels = num_channels + self.channel_dim = channel_dim + self.scale = scale + assert max_var_per_eig == 0.0 or max_var_per_eig > 1.0 / num_channels + self.max_var_per_eig = max_var_per_eig + + # we figure out the dominant direction using the power method: starting with + # a random vector, keep multiplying by the covariance and renormalizing. + with torch.no_grad(): + # arbitrary.. would use randn() but want to leave the rest of the model's + # random parameters unchanged for comparison + direction = torch.arange(num_channels).to(torch.float) + direction = direction / direction.norm() + self.register_buffer("max_eig_direction", direction) + + self.min_prob = min_prob + # cur_prob is the current probability we'll use to apply the ActivationBalancer. + # We'll regress this towards prob, each tiem we try to apply it and it is not + # active. + self.cur_prob = 1.0 + + def forward(self, x: Tensor) -> Tensor: + if ( + torch.jit.is_scripting() + or self.max_var_per_eig <= 0 + or random.random() > self.cur_prob + or torch.jit.is_tracing() + ): + return _no_op(x) + + with torch.cuda.amp.autocast(enabled=False): + eps = 1.0e-20 + orig_x = x + x = x.to(torch.float32) + with torch.no_grad(): + x = x.transpose(self.channel_dim, -1).reshape(-1, self.num_channels) + x = x - x.mean(dim=0) + new_direction, coeffs = self._find_direction_coeffs( + x, self.max_eig_direction + ) + x_var = (x**2).mean() + x_residual = x - coeffs * new_direction + x_residual_var = (x_residual**2).mean() + + # `variance_proportion` is the proportion of the variance accounted for + # by the top eigen-direction. + variance_proportion = (x_var - x_residual_var) / (x_var + 1.0e-20) + + # ensure new direction is nonzero even if x == 0, by including `direction`. + self._set_direction(0.1 * self.max_eig_direction + new_direction) + + if random.random() < 0.01 or __name__ == "__main__": + logging.info( + f"variance_proportion = {variance_proportion.item()}, shape={tuple(orig_x.shape)}, cur_prob={self.cur_prob}" + ) + + if variance_proportion >= self.max_var_per_eig: + # The constraint is active. Note, we should quite rarely + # reach here, only near the beginning of training if we are + # starting to diverge, should this constraint be active. + cur_prob = self.cur_prob + self.cur_prob = 1.0 # next time, do the update with probability 1.0. + return MaxEigLimiterFunction.apply( + orig_x, coeffs, new_direction, self.channel_dim, self.scale + ) + else: + # let self.cur_prob exponentially approach self.min_prob, as + # long as the constraint is inactive. + self.cur_prob = 0.75 * self.cur_prob + 0.25 * self.min_prob + return orig_x + + def _set_direction(self, direction: Tensor): + """ + Sets self.max_eig_direction to a normalized version of `direction` + """ + direction = direction.detach() + direction = direction / direction.norm() + direction_sum = direction.sum().item() + if direction_sum - direction_sum == 0: # no inf/nan + self.max_eig_direction[:] = direction + else: + logging.info( + f"Warning: sum of direction in MaxEig is {direction_sum}, " + "num_channels={self.num_channels}, channel_dim={self.channel_dim}" + ) + + def _find_direction_coeffs( + self, x: Tensor, prev_direction: Tensor + ) -> Tuple[Tensor, Tensor, Tensor]: + """ + Figure out (an approximation to) the proportion of the variance of a set of + feature vectors that can be attributed to the top eigen-direction. + Args: + x: a Tensor of shape (num_frames, num_channels), with num_frames > 1. + prev_direction: a Tensor of shape (num_channels,), that is our previous estimate + of the top eigen-direction, or a random direction if this is the first + iteration. Does not have to be normalized, but should be nonzero. + + Returns: (cur_direction, coeffs), where: + cur_direction: a Tensor of shape (num_channels,) that is the current + estimate of the top eigen-direction. + coeffs: a Tensor of shape (num_frames, 1) that minimizes, or + approximately minimizes, (x - coeffs * cur_direction).norm() + """ + (num_frames, num_channels) = x.shape + assert num_channels > 1 and num_frames > 1 + assert prev_direction.shape == (num_channels,) + # `coeffs` are the coefficients of `prev_direction` in x. + # actually represent the coeffs up to a constant positive factor. + coeffs = (x * prev_direction).sum(dim=1, keepdim=True) + 1.0e-10 + cur_direction = (x * coeffs).sum(dim=0) / ((coeffs**2).sum() + 1.0e-20) + return cur_direction, coeffs + + +class DoubleSwishFunction(torch.autograd.Function): + """ + double_swish(x) = x * torch.sigmoid(x-1) + This is a definition, originally motivated by its close numerical + similarity to swish(swish(x)), where swish(x) = x * sigmoid(x). + + Memory-efficient derivative computation: + double_swish(x) = x * s, where s(x) = torch.sigmoid(x-1) + double_swish'(x) = d/dx double_swish(x) = x * s'(x) + x' * s(x) = x * s'(x) + s(x). + Now, s'(x) = s(x) * (1-s(x)). + double_swish'(x) = x * s'(x) + s(x). + = x * s(x) * (1-s(x)) + s(x). + = double_swish(x) * (1-s(x)) + s(x) + ... so we just need to remember s(x) but not x itself. + """ + + @staticmethod + def forward(ctx, x: Tensor) -> Tensor: + requires_grad = x.requires_grad + x_dtype = x.dtype + if x.dtype == torch.float16: + x = x.to(torch.float32) + + s = torch.sigmoid(x - 1.0) + y = x * s + + if requires_grad: + deriv = y * (1 - s) + s + # notes on derivative of x * sigmoid(x - 1): + # https://www.wolframalpha.com/input?i=d%2Fdx+%28x+*+sigmoid%28x-1%29%29 + # min \simeq -0.043638. Take floor as -0.043637 so it's a lower bund + # max \simeq 1.1990. Take ceil to be 1.2 so it's an upper bound. + # the combination of "+ torch.rand_like(deriv)" and casting to torch.uint8 (which + # floors), should be expectation-preserving. + floor = -0.043637 + ceil = 1.2 + d_scaled = (deriv - floor) * (255.0 / (ceil - floor)) + torch.rand_like( + deriv + ) + if __name__ == "__main__": + # for self-testing only. + assert d_scaled.min() >= 0.0 + assert d_scaled.max() < 256.0 + d_int = d_scaled.to(torch.uint8) + ctx.save_for_backward(d_int) + if x.dtype == torch.float16 or torch.is_autocast_enabled(): + y = y.to(torch.float16) + return y + + @staticmethod + def backward(ctx, y_grad: Tensor) -> Tensor: + (d,) = ctx.saved_tensors + # the same constants as used in forward pass. + floor = -0.043637 + ceil = 1.2 + d = d * ((ceil - floor) / 255.0) + floor + return y_grad * d + + +class DoubleSwish(torch.nn.Module): + def forward(self, x: Tensor) -> Tensor: + """Return double-swish activation function which is an approximation to Swish(Swish(x)), + that we approximate closely with x * sigmoid(x-1). + """ + if torch.jit.is_scripting() or torch.jit.is_tracing(): + return x * torch.sigmoid(x - 1.0) + return DoubleSwishFunction.apply(x) + + +def _test_max_eig(): + for proportion in [0.1, 0.5, 10.0]: + logging.info(f"proportion = {proportion}") + x = torch.randn(100, 128) + direction = torch.randn(128) + coeffs = torch.randn(100, 1) + x += proportion * direction * coeffs + + x.requires_grad = True + + num_channels = 128 + m = MaxEig( + num_channels, 1, 0.5, scale=0.1 # channel_dim # max_var_per_eig + ) # grad_scale + + for _ in range(4): + y = m(x) + + y_grad = torch.randn_like(x) + y.backward(gradient=y_grad) + + if proportion < 0.2: + assert torch.allclose(x.grad, y_grad, atol=1.0e-02) + elif proportion > 1.0: + assert not torch.allclose(x.grad, y_grad) + + +def _test_whiten(): + for proportion in [0.1, 0.5, 10.0]: + logging.info(f"_test_whiten(): proportion = {proportion}") + x = torch.randn(100, 128) + direction = torch.randn(128) + coeffs = torch.randn(100, 1) + x += proportion * direction * coeffs + + x.requires_grad = True + + num_channels = 128 + m = Whiten( + 1, 5.0, prob=1.0, grad_scale=0.1 # num_groups # whitening_limit, + ) # grad_scale + + for _ in range(4): + y = m(x) + + y_grad = torch.randn_like(x) + y.backward(gradient=y_grad) + + if proportion < 0.2: + assert torch.allclose(x.grad, y_grad) + elif proportion > 1.0: + assert not torch.allclose(x.grad, y_grad) + + +def _test_activation_balancer_sign(): + probs = torch.arange(0, 1, 0.01) + N = 1000 + x = 1.0 * ((2.0 * (torch.rand(probs.numel(), N) < probs.unsqueeze(-1))) - 1.0) + x = x.detach() + x.requires_grad = True + m = ActivationBalancer( + probs.numel(), + channel_dim=0, + min_positive=0.05, + max_positive=0.95, + max_factor=0.2, + min_abs=0.0, + ) + + y_grad = torch.sign(torch.randn(probs.numel(), N)) + + y = m(x) + y.backward(gradient=y_grad) + print("_test_activation_balancer_sign: x = ", x) + print("_test_activation_balancer_sign: y grad = ", y_grad) + print("_test_activation_balancer_sign: x grad = ", x.grad) + + +def _test_activation_balancer_magnitude(): + magnitudes = torch.arange(0, 1, 0.01) + N = 1000 + x = torch.sign(torch.randn(magnitudes.numel(), N)) * magnitudes.unsqueeze(-1) + x = x.detach() + x.requires_grad = True + m = ActivationBalancer( + magnitudes.numel(), + channel_dim=0, + min_positive=0.0, + max_positive=1.0, + max_factor=0.2, + min_abs=0.2, + max_abs=0.8, + min_prob=1.0, + ) + + y_grad = torch.sign(torch.randn(magnitudes.numel(), N)) + + y = m(x) + y.backward(gradient=y_grad) + print("_test_activation_balancer_magnitude: x = ", x) + print("_test_activation_balancer_magnitude: y grad = ", y_grad) + print("_test_activation_balancer_magnitude: x grad = ", x.grad) + + +def _test_basic_norm(): + num_channels = 128 + m = BasicNorm(num_channels=num_channels, channel_dim=1) + + x = torch.randn(500, num_channels) + + y = m(x) + + assert y.shape == x.shape + x_rms = (x**2).mean().sqrt() + y_rms = (y**2).mean().sqrt() + print("x rms = ", x_rms) + print("y rms = ", y_rms) + assert y_rms < x_rms + assert y_rms > 0.5 * x_rms + + +def _test_double_swish_deriv(): + x = torch.randn(10, 12, dtype=torch.double) * 3.0 + x.requires_grad = True + m = DoubleSwish() + + tol = (1.2 - (-0.043637)) / 255.0 + torch.autograd.gradcheck(m, x, atol=tol) + + # for self-test. + x = torch.randn(1000, 1000, dtype=torch.double) * 3.0 + x.requires_grad = True + y = m(x) + + +def _test_softmax(): + a = torch.randn(2, 10, dtype=torch.float64) + b = a.clone() + a.requires_grad = True + b.requires_grad = True + a.softmax(dim=1)[:, 0].sum().backward() + print("a grad = ", a.grad) + softmax(b, dim=1)[:, 0].sum().backward() + print("b grad = ", b.grad) + assert torch.allclose(a.grad, b.grad) + + +if __name__ == "__main__": + logging.getLogger().setLevel(logging.INFO) + torch.set_num_threads(1) + torch.set_num_interop_threads(1) + _test_softmax() + _test_whiten() + _test_max_eig() + _test_activation_balancer_sign() + _test_activation_balancer_magnitude() + _test_basic_norm() + _test_double_swish_deriv() From 9ef8145fa3c6e8f45fa8ad8e8e4d348062b84ee4 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Mon, 4 Sep 2023 17:56:05 +0800 Subject: [PATCH 076/100] minor fixes (#1240) --- egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_dev_test.py | 1 + egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_splits.py | 1 + 2 files changed, 2 insertions(+) diff --git a/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_dev_test.py b/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_dev_test.py index 20d7341db..1af08fee2 100755 --- a/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_dev_test.py +++ b/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_dev_test.py @@ -28,6 +28,7 @@ from lhotse import CutSet, KaldifeatFbank, KaldifeatFbankConfig, LilcomChunkyWri # even when we are not invoking the main (e.g. when spawning subprocesses). torch.set_num_threads(1) torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") def compute_fbank_wenetspeech_dev_test(): diff --git a/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_splits.py b/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_splits.py index 1b257fb70..99d39bbdc 100755 --- a/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_splits.py +++ b/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_splits.py @@ -37,6 +37,7 @@ from lhotse import ( # even when we are not invoking the main (e.g. when spawning subprocesses). torch.set_num_threads(1) torch.set_num_interop_threads(1) +torch.multiprocessing.set_sharing_strategy("file_system") def get_parser(): From d50a9ea03055232e742a753dd5e5e4cad914caa6 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Thu, 7 Sep 2023 16:34:53 +0800 Subject: [PATCH 077/100] doc str fixes (#1241) --- .../ASR/pruned_transducer_stateless7/compute_ali.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/compute_ali.py b/egs/librispeech/ASR/pruned_transducer_stateless7/compute_ali.py index 8bcb56d62..27ef0a244 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/compute_ali.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/compute_ali.py @@ -26,7 +26,7 @@ You can generate the checkpoint with the following command: ./pruned_transducer_stateless7/export.py \ --exp-dir ./pruned_transducer_stateless7/exp \ - --bpe-model data/lang_bpe_500/bpe.model \ + --tokens data/lang_bpe_500/tokens.txt \ --epoch 30 \ --avg 9 @@ -52,12 +52,12 @@ import torch import torch.nn as nn from alignment import batch_force_alignment from asr_datamodule import LibriSpeechAsrDataModule -from train import add_model_arguments, get_params, get_transducer_model - -from icefall.utils import AttributeDict, convert_timestamp, parse_timestamp from lhotse import CutSet from lhotse.serialization import SequentialJsonlWriter from lhotse.supervision import AlignmentItem +from train import add_model_arguments, get_params, get_transducer_model + +from icefall.utils import AttributeDict, convert_timestamp, parse_timestamp def get_parser(): From c912bd65d0c301233e8d18fb1e1ea0e9c4c245d5 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Thu, 7 Sep 2023 18:48:27 +0800 Subject: [PATCH 078/100] Update run-gigaspeech-pruned-transducer-stateless2-2022-05-12.sh (#1242) --- .../run-gigaspeech-pruned-transducer-stateless2-2022-05-12.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/scripts/run-gigaspeech-pruned-transducer-stateless2-2022-05-12.sh b/.github/scripts/run-gigaspeech-pruned-transducer-stateless2-2022-05-12.sh index c8d9c6b77..b61a9d7b6 100755 --- a/.github/scripts/run-gigaspeech-pruned-transducer-stateless2-2022-05-12.sh +++ b/.github/scripts/run-gigaspeech-pruned-transducer-stateless2-2022-05-12.sh @@ -29,6 +29,9 @@ if [[ x"${GITHUB_EVENT_NAME}" == x"schedule" || x"${GITHUB_EVENT_LABEL_NAME}" == ls -lh data/fbank ls -lh pruned_transducer_stateless2/exp + ln -s data/fbank/cuts_DEV.jsonl.gz data/fbank/gigaspeech_cuts_DEV.jsonl.gz + ln -s data/fbank/cuts_TEST.jsonl.gz data/fbank/gigaspeech_cuts_TEST.jsonl.gz + log "Decoding dev and test" # use a small value for decoding with CPU From 49a4b672884213809cc04df2caab6c37cee92c22 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Thu, 7 Sep 2023 19:48:46 +0800 Subject: [PATCH 079/100] fixed a CI test issue related to python version (#1243) --- .github/workflows/run-aishell-2022-06-20.yml | 2 +- .github/workflows/run-gigaspeech-2022-05-13.yml | 2 +- .github/workflows/run-librispeech-2022-03-12.yml | 2 +- .github/workflows/run-librispeech-2022-04-29.yml | 2 +- .github/workflows/run-librispeech-2022-05-13.yml | 2 +- .../run-librispeech-pruned-transducer-stateless3-2022-05-13.yml | 2 +- ...n-librispeech-streaming-transducer-stateless2-2022-06-26.yml | 2 +- .../run-librispeech-transducer-stateless2-2022-04-19.yml | 2 +- .github/workflows/run-pretrained-conformer-ctc.yml | 2 +- .../run-pretrained-transducer-stateless-librispeech-100h.yml | 2 +- ...etrained-transducer-stateless-librispeech-multi-datasets.yml | 2 +- .../run-pretrained-transducer-stateless-modified-2-aishell.yml | 2 +- .../run-pretrained-transducer-stateless-modified-aishell.yml | 2 +- .github/workflows/run-pretrained-transducer-stateless.yml | 2 +- .github/workflows/run-pretrained-transducer.yml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/run-aishell-2022-06-20.yml b/.github/workflows/run-aishell-2022-06-20.yml index d14196f38..53fcb2c03 100644 --- a/.github/workflows/run-aishell-2022-06-20.yml +++ b/.github/workflows/run-aishell-2022-06-20.yml @@ -45,7 +45,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-gigaspeech-2022-05-13.yml b/.github/workflows/run-gigaspeech-2022-05-13.yml index 0e47f7538..3121520c1 100644 --- a/.github/workflows/run-gigaspeech-2022-05-13.yml +++ b/.github/workflows/run-gigaspeech-2022-05-13.yml @@ -44,7 +44,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-librispeech-2022-03-12.yml b/.github/workflows/run-librispeech-2022-03-12.yml index 3edbe43ec..f092e3c80 100644 --- a/.github/workflows/run-librispeech-2022-03-12.yml +++ b/.github/workflows/run-librispeech-2022-03-12.yml @@ -44,7 +44,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-librispeech-2022-04-29.yml b/.github/workflows/run-librispeech-2022-04-29.yml index bb44a073b..f8f4d9977 100644 --- a/.github/workflows/run-librispeech-2022-04-29.yml +++ b/.github/workflows/run-librispeech-2022-04-29.yml @@ -44,7 +44,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-librispeech-2022-05-13.yml b/.github/workflows/run-librispeech-2022-05-13.yml index e7b53b21c..dc20185da 100644 --- a/.github/workflows/run-librispeech-2022-05-13.yml +++ b/.github/workflows/run-librispeech-2022-05-13.yml @@ -44,7 +44,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml b/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml index bf73d4f18..3fb0920bc 100644 --- a/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml +++ b/.github/workflows/run-librispeech-pruned-transducer-stateless3-2022-05-13.yml @@ -44,7 +44,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml b/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml index 6ea308468..67a6f6fc4 100644 --- a/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml +++ b/.github/workflows/run-librispeech-streaming-transducer-stateless2-2022-06-26.yml @@ -44,7 +44,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml b/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml index 9fe2f0389..35ca08a31 100644 --- a/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml +++ b/.github/workflows/run-librispeech-transducer-stateless2-2022-04-19.yml @@ -44,7 +44,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-pretrained-conformer-ctc.yml b/.github/workflows/run-pretrained-conformer-ctc.yml index bcd326b9d..6151a5a14 100644 --- a/.github/workflows/run-pretrained-conformer-ctc.yml +++ b/.github/workflows/run-pretrained-conformer-ctc.yml @@ -34,7 +34,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml b/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml index 1e5b25f5c..f8caee8e5 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-librispeech-100h.yml @@ -43,7 +43,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml b/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml index 9063c0ed6..7c3910eb8 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-librispeech-multi-datasets.yml @@ -43,7 +43,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml b/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml index 2d24528d3..ce6d6f92d 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-modified-2-aishell.yml @@ -34,7 +34,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml b/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml index 761b26131..f0cebd94a 100644 --- a/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml +++ b/.github/workflows/run-pretrained-transducer-stateless-modified-aishell.yml @@ -34,7 +34,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer-stateless.yml b/.github/workflows/run-pretrained-transducer-stateless.yml index e46b9a849..1b69b97bf 100644 --- a/.github/workflows/run-pretrained-transducer-stateless.yml +++ b/.github/workflows/run-pretrained-transducer-stateless.yml @@ -43,7 +43,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false diff --git a/.github/workflows/run-pretrained-transducer.yml b/.github/workflows/run-pretrained-transducer.yml index 190e446bc..91d87f1c9 100644 --- a/.github/workflows/run-pretrained-transducer.yml +++ b/.github/workflows/run-pretrained-transducer.yml @@ -34,7 +34,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: [3.7, 3.8, 3.9] + python-version: [3.8] fail-fast: false From 3199058194a48d45aeee740f2aa9bdbef0bec29d Mon Sep 17 00:00:00 2001 From: zr_jin Date: Sat, 9 Sep 2023 21:25:26 +0800 Subject: [PATCH 080/100] enable `sclite_mode` for swbd scoring (#1239) --- icefall/utils.py | 3 ++- requirements-ci.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/icefall/utils.py b/icefall/utils.py index b01cd2770..947d79438 100644 --- a/icefall/utils.py +++ b/icefall/utils.py @@ -493,6 +493,7 @@ def write_error_stats( test_set_name: str, results: List[Tuple[str, str]], enable_log: bool = True, + sclite_mode: bool = False, ) -> float: """Write statistics based on predicted results and reference transcripts. @@ -538,7 +539,7 @@ def write_error_stats( num_corr = 0 ERR = "*" for cut_id, ref, hyp in results: - ali = kaldialign.align(ref, hyp, ERR) + ali = kaldialign.align(ref, hyp, ERR, sclite_mode=sclite_mode) for ref_word, hyp_word in ali: if ref_word == ERR: ins[hyp_word] += 1 diff --git a/requirements-ci.txt b/requirements-ci.txt index 3c2eb5f65..21d33001c 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -15,7 +15,7 @@ graphviz==0.19.1 git+https://github.com/lhotse-speech/lhotse kaldilm==1.11 -kaldialign==0.2 +kaldialign==0.7.1 sentencepiece==0.1.96 tensorboard==2.8.0 typeguard==2.13.3 From 0f1bc6f8af63d585436837b2b14f5075cd680480 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Wed, 13 Sep 2023 11:57:05 +0800 Subject: [PATCH 081/100] Multi_zh-Hans Recipe (#1238) * Init commit for recipes trained on multiple zh datasets. * fbank extraction for thchs30 * added support for aishell1 * added support for aishell-2 * fixes * fixes * fixes * added support for stcmds and primewords * fixes * added support for magicdata script for fbank computation not done yet * added script for magicdata fbank computation * file permission fixed * updated for the wenetspeech recipe * updated * Update preprocess_kespeech.py * updated * updated * updated * updated * file permission fixed * updated paths * fixes * added support for kespeech dev/test set fbank computation * fixes for file permission * refined support for KeSpeech * added scripts for BPE model training * updated * init commit for the multi_zh-cn zipformer recipe * disable speed perturbation by default * updated * updated * added necessary files for the zipformer recipe * removed redundant wenetspeech M and S sets * updates for multi dataset decoding * refined * formatting issues fixed * updated * minor fixes * this commit finalize the recipe (hopefully) * fixed formatting issues * minor fixes * updated * using soft links to reduce redundancy * minor updates * using soft links to reduce redundancy * minor updates * minor updates * using soft links to reduce redundancy * minor updates * Update README.md * minor updates * Update egs/multi_zh-hans/ASR/local/compute_fbank_magicdata.py Co-authored-by: Fangjun Kuang * Update egs/multi_zh-hans/ASR/local/compute_fbank_magicdata.py Co-authored-by: Fangjun Kuang * Update egs/multi_zh-hans/ASR/local/compute_fbank_stcmds.py Co-authored-by: Fangjun Kuang * Update egs/multi_zh-hans/ASR/local/compute_fbank_stcmds.py Co-authored-by: Fangjun Kuang * Update egs/multi_zh-hans/ASR/local/compute_fbank_primewords.py Co-authored-by: Fangjun Kuang * Update egs/multi_zh-hans/ASR/local/compute_fbank_primewords.py Co-authored-by: Fangjun Kuang * minor updates * minor fixes * fixed a formatting issue * Update preprocess_kespeech.py * Update prepare.sh * Update egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_splits.py Co-authored-by: Fangjun Kuang * Update egs/multi_zh-hans/ASR/local/preprocess_kespeech.py Co-authored-by: Fangjun Kuang * removed redundant files * symlinks added * minor updates * added CI tests for `multi_zh-hans` * minor fixes * Update run-multi-zh_hans-zipformer.sh * Update run-multi-zh_hans-zipformer.sh * Update run-multi-zh_hans-zipformer.sh * Update run-multi-zh_hans-zipformer.sh * Update run-multi-zh_hans-zipformer.sh * Update run-multi-zh_hans-zipformer.sh * Update run-multi-zh_hans-zipformer.sh --------- Co-authored-by: Fangjun Kuang --- .../scripts/run-multi-zh_hans-zipformer.sh | 51 + .../workflows/run-multi-zh_hans-zipformer.yml | 84 + egs/librispeech/ASR/zipformer/zipformer.py | 916 ++++++----- egs/multi_zh-hans/ASR/README.md | 39 + egs/multi_zh-hans/ASR/RESULTS.md | 38 + .../ASR/local/bpe_model_to_tokens.py | 37 + egs/multi_zh-hans/ASR/local/compile_lg.py | 1 + .../local/compute_fbank_kespeech_dev_test.py | 93 ++ .../local/compute_fbank_kespeech_splits.py | 180 +++ .../ASR/local/compute_fbank_magicdata.py | 122 ++ .../ASR/local/compute_fbank_primewords.py | 122 ++ .../ASR/local/compute_fbank_stcmds.py | 121 ++ .../ASR/local/compute_fbank_thchs30.py | 127 ++ egs/multi_zh-hans/ASR/local/prepare_char.py | 1 + .../ASR/local/prepare_for_bpe_model.py | 65 + egs/multi_zh-hans/ASR/local/prepare_lang.py | 1 + .../ASR/local/prepare_lang_bpe.py | 1 + .../ASR/local/preprocess_kespeech.py | 151 ++ egs/multi_zh-hans/ASR/local/text2token.py | 1 + .../ASR/local/train_bpe_model.py | 109 ++ .../ASR/local/validate_bpe_lexicon.py | 1 + egs/multi_zh-hans/ASR/prepare.sh | 373 +++++ egs/multi_zh-hans/ASR/shared | 1 + .../ASR/zipformer/asr_datamodule.py | 388 +++++ .../ASR/zipformer/beam_search.py | 1 + egs/multi_zh-hans/ASR/zipformer/decode.py | 828 ++++++++++ egs/multi_zh-hans/ASR/zipformer/decoder.py | 1 + .../ASR/zipformer/encoder_interface.py | 1 + .../ASR/zipformer/export-onnx-streaming.py | 1 + .../ASR/zipformer/export-onnx.py | 1 + egs/multi_zh-hans/ASR/zipformer/export.py | 541 +++++++ .../ASR/zipformer/generate_averaged_model.py | 193 +++ .../ASR/zipformer/jit_pretrained.py | 1 + .../ASR/zipformer/jit_pretrained_ctc.py | 1 + .../ASR/zipformer/jit_pretrained_streaming.py | 1 + egs/multi_zh-hans/ASR/zipformer/joiner.py | 1 + egs/multi_zh-hans/ASR/zipformer/model.py | 1 + .../ASR/zipformer/multi_dataset.py | 316 ++++ egs/multi_zh-hans/ASR/zipformer/onnx_check.py | 1 + .../ASR/zipformer/onnx_decode.py | 1 + .../zipformer/onnx_pretrained-streaming.py | 1 + .../ASR/zipformer/onnx_pretrained.py | 1 + egs/multi_zh-hans/ASR/zipformer/optim.py | 1 + egs/multi_zh-hans/ASR/zipformer/pretrained.py | 381 +++++ egs/multi_zh-hans/ASR/zipformer/scaling.py | 1 + .../ASR/zipformer/scaling_converter.py | 1 + .../ASR/zipformer/streaming_beam_search.py | 1 + .../ASR/zipformer/streaming_decode.py | 1 + .../ASR/zipformer/subsampling.py | 1 + egs/multi_zh-hans/ASR/zipformer/train.py | 1385 +++++++++++++++++ egs/multi_zh-hans/ASR/zipformer/zipformer.py | 1 + 51 files changed, 6319 insertions(+), 369 deletions(-) create mode 100755 .github/scripts/run-multi-zh_hans-zipformer.sh create mode 100644 .github/workflows/run-multi-zh_hans-zipformer.yml create mode 100644 egs/multi_zh-hans/ASR/README.md create mode 100644 egs/multi_zh-hans/ASR/RESULTS.md create mode 100755 egs/multi_zh-hans/ASR/local/bpe_model_to_tokens.py create mode 120000 egs/multi_zh-hans/ASR/local/compile_lg.py create mode 100755 egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_dev_test.py create mode 100755 egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_splits.py create mode 100755 egs/multi_zh-hans/ASR/local/compute_fbank_magicdata.py create mode 100755 egs/multi_zh-hans/ASR/local/compute_fbank_primewords.py create mode 100755 egs/multi_zh-hans/ASR/local/compute_fbank_stcmds.py create mode 100755 egs/multi_zh-hans/ASR/local/compute_fbank_thchs30.py create mode 120000 egs/multi_zh-hans/ASR/local/prepare_char.py create mode 100755 egs/multi_zh-hans/ASR/local/prepare_for_bpe_model.py create mode 120000 egs/multi_zh-hans/ASR/local/prepare_lang.py create mode 120000 egs/multi_zh-hans/ASR/local/prepare_lang_bpe.py create mode 100755 egs/multi_zh-hans/ASR/local/preprocess_kespeech.py create mode 120000 egs/multi_zh-hans/ASR/local/text2token.py create mode 100755 egs/multi_zh-hans/ASR/local/train_bpe_model.py create mode 120000 egs/multi_zh-hans/ASR/local/validate_bpe_lexicon.py create mode 100755 egs/multi_zh-hans/ASR/prepare.sh create mode 120000 egs/multi_zh-hans/ASR/shared create mode 100644 egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/beam_search.py create mode 100755 egs/multi_zh-hans/ASR/zipformer/decode.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/decoder.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/encoder_interface.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/export-onnx-streaming.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/export-onnx.py create mode 100755 egs/multi_zh-hans/ASR/zipformer/export.py create mode 100755 egs/multi_zh-hans/ASR/zipformer/generate_averaged_model.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/jit_pretrained.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/jit_pretrained_ctc.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/jit_pretrained_streaming.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/joiner.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/model.py create mode 100644 egs/multi_zh-hans/ASR/zipformer/multi_dataset.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/onnx_check.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/onnx_decode.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/onnx_pretrained-streaming.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/onnx_pretrained.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/optim.py create mode 100755 egs/multi_zh-hans/ASR/zipformer/pretrained.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/scaling.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/scaling_converter.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/streaming_beam_search.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/streaming_decode.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/subsampling.py create mode 100755 egs/multi_zh-hans/ASR/zipformer/train.py create mode 120000 egs/multi_zh-hans/ASR/zipformer/zipformer.py diff --git a/.github/scripts/run-multi-zh_hans-zipformer.sh b/.github/scripts/run-multi-zh_hans-zipformer.sh new file mode 100755 index 000000000..2bc3137d8 --- /dev/null +++ b/.github/scripts/run-multi-zh_hans-zipformer.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -e + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +cd egs/multi_zh-hans/ASR + +repo_url=https://huggingface.co/zrjin/icefall-asr-multi-zh-hans-zipformer-2023-9-2/ + +log "Downloading pre-trained model from $repo_url" +git lfs install +git clone $repo_url +repo=$(basename $repo_url) + + +log "Display test files" +tree $repo/ +ls -lh $repo/test_wavs/*.wav + +pushd $repo/exp +ln -s epoch-20.pt epoch-99.pt +popd + +ls -lh $repo/exp/*.pt + + +./zipformer/pretrained.py \ + --checkpoint $repo/exp/epoch-99.pt \ + --tokens $repo/data/lang_bpe_2000/tokens.txt \ + --method greedy_search \ +$repo/test_wavs/DEV_T0000000000.wav \ +$repo/test_wavs/DEV_T0000000001.wav \ +$repo/test_wavs/DEV_T0000000002.wav + +for method in modified_beam_search fast_beam_search; do + log "$method" + + ./zipformer/pretrained.py \ + --method $method \ + --beam-size 4 \ + --checkpoint $repo/exp/epoch-99.pt \ + --tokens $repo/data/lang_bpe_2000/tokens.txt \ + $repo/test_wavs/DEV_T0000000000.wav \ + $repo/test_wavs/DEV_T0000000001.wav \ + $repo/test_wavs/DEV_T0000000002.wav +done diff --git a/.github/workflows/run-multi-zh_hans-zipformer.yml b/.github/workflows/run-multi-zh_hans-zipformer.yml new file mode 100644 index 000000000..4ec81585f --- /dev/null +++ b/.github/workflows/run-multi-zh_hans-zipformer.yml @@ -0,0 +1,84 @@ +# Copyright 2023 Xiaomi Corp. (author: Zengrui Jin) + +# See ../../LICENSE for clarification regarding multiple authors +# +# 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-multi-zh_hans-zipformer + +on: + push: + branches: + - master + pull_request: + types: [labeled] + +concurrency: + group: run_multi-zh_hans_zipformer-${{ github.ref }} + cancel-in-progress: true + +jobs: + run_multi-zh_hans_zipformer: + if: github.event.label.name == 'onnx' || github.event.label.name == 'ready' || github.event_name == 'push' || github.event.label.name == 'multi-zh_hans' + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.8] + + fail-fast: false + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: '**/requirements-ci.txt' + + - name: Install Python dependencies + run: | + grep -v '^#' ./requirements-ci.txt | xargs -n 1 -L 1 pip install + pip uninstall -y protobuf + pip install --no-binary protobuf protobuf==3.20.* + + - name: Cache kaldifeat + id: my-cache + uses: actions/cache@v2 + with: + path: | + ~/tmp/kaldifeat + key: cache-tmp-${{ matrix.python-version }}-2023-05-22 + + - name: Install kaldifeat + if: steps.my-cache.outputs.cache-hit != 'true' + shell: bash + run: | + .github/scripts/install-kaldifeat.sh + + - name: Inference with pre-trained model + shell: bash + env: + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_EVENT_LABEL_NAME: ${{ github.event.label.name }} + run: | + sudo apt-get -qq install git-lfs tree + export PYTHONPATH=$PWD:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/kaldifeat/python:$PYTHONPATH + export PYTHONPATH=~/tmp/kaldifeat/build/lib:$PYTHONPATH + + .github/scripts/run-multi-zh_hans-zipformer.sh diff --git a/egs/librispeech/ASR/zipformer/zipformer.py b/egs/librispeech/ASR/zipformer/zipformer.py index b39af02b8..1a174b315 100644 --- a/egs/librispeech/ASR/zipformer/zipformer.py +++ b/egs/librispeech/ASR/zipformer/zipformer.py @@ -91,34 +91,34 @@ class Zipformer2(EncoderInterface): chunks. Must not be less than cnn_module_kernel (after factoring in rounding and downsampling); an error will be thrown if this is violated. """ + def __init__( - self, - output_downsampling_factor: int = 2, - downsampling_factor: Tuple[int] = (2, 4), - encoder_dim: Union[int, Tuple[int]] = 384, - num_encoder_layers: Union[int, Tuple[int]] = 4, - encoder_unmasked_dim: Union[int, Tuple[int]] = 256, - query_head_dim: Union[int, Tuple[int]] = 24, - pos_head_dim: Union[int, Tuple[int]] = 4, - value_head_dim: Union[int, Tuple[int]] = 12, - num_heads: Union[int, Tuple[int]] = 8, - feedforward_dim: Union[int, Tuple[int]] = 1536, - cnn_module_kernel: Union[int, Tuple[int]] = 31, - pos_dim: int = 192, - dropout: FloatLike = None, # see code below for default - warmup_batches: float = 4000.0, - causal: bool = False, - chunk_size: Tuple[int] = [-1], - left_context_frames: Tuple[int] = [-1], + self, + output_downsampling_factor: int = 2, + downsampling_factor: Tuple[int] = (2, 4), + encoder_dim: Union[int, Tuple[int]] = 384, + num_encoder_layers: Union[int, Tuple[int]] = 4, + encoder_unmasked_dim: Union[int, Tuple[int]] = 256, + query_head_dim: Union[int, Tuple[int]] = 24, + pos_head_dim: Union[int, Tuple[int]] = 4, + value_head_dim: Union[int, Tuple[int]] = 12, + num_heads: Union[int, Tuple[int]] = 8, + feedforward_dim: Union[int, Tuple[int]] = 1536, + cnn_module_kernel: Union[int, Tuple[int]] = 31, + pos_dim: int = 192, + dropout: FloatLike = None, # see code below for default + warmup_batches: float = 4000.0, + causal: bool = False, + chunk_size: Tuple[int] = [-1], + left_context_frames: Tuple[int] = [-1], ) -> None: super(Zipformer2, self).__init__() if dropout is None: - dropout = ScheduledFloat((0.0, 0.3), - (20000.0, 0.1)) + dropout = ScheduledFloat((0.0, 0.3), (20000.0, 0.1)) def _to_tuple(x): - """ Converts a single int or a 1-tuple of an int to a tuple with the same length + """Converts a single int or a 1-tuple of an int to a tuple with the same length as downsampling_factor""" if isinstance(x, int): x = (x,) @@ -128,10 +128,12 @@ class Zipformer2(EncoderInterface): assert len(x) == len(downsampling_factor) and isinstance(x[0], int) return x - self.output_downsampling_factor = output_downsampling_factor # int - self.downsampling_factor = downsampling_factor # tuple - self.encoder_dim = encoder_dim = _to_tuple(encoder_dim) # tuple - self.encoder_unmasked_dim = encoder_unmasked_dim = _to_tuple(encoder_unmasked_dim) # tuple + self.output_downsampling_factor = output_downsampling_factor # int + self.downsampling_factor = downsampling_factor # tuple + self.encoder_dim = encoder_dim = _to_tuple(encoder_dim) # tuple + self.encoder_unmasked_dim = encoder_unmasked_dim = _to_tuple( + encoder_unmasked_dim + ) # tuple num_encoder_layers = _to_tuple(num_encoder_layers) self.num_encoder_layers = num_encoder_layers self.query_head_dim = query_head_dim = _to_tuple(query_head_dim) @@ -145,7 +147,7 @@ class Zipformer2(EncoderInterface): self.chunk_size = chunk_size self.left_context_frames = left_context_frames - for u,d in zip(encoder_unmasked_dim, encoder_dim): + for u, d in zip(encoder_unmasked_dim, encoder_dim): assert u <= d # each one will be Zipformer2Encoder or DownsampledZipformer2Encoder @@ -153,7 +155,6 @@ class Zipformer2(EncoderInterface): num_encoders = len(downsampling_factor) for i in range(num_encoders): - encoder_layer = Zipformer2EncoderLayer( embed_dim=encoder_dim[i], pos_dim=pos_dim, @@ -191,13 +192,11 @@ class Zipformer2(EncoderInterface): self.encoders = nn.ModuleList(encoders) - self.downsample_output = SimpleDownsample(max(encoder_dim), - downsample=output_downsampling_factor, - dropout=dropout) + self.downsample_output = SimpleDownsample( + max(encoder_dim), downsample=output_downsampling_factor, dropout=dropout + ) - def get_feature_masks( - self, - x: Tensor) -> Union[List[float], List[Tensor]]: + def get_feature_masks(self, x: Tensor) -> Union[List[float], List[Tensor]]: """ In eval mode, returns [1.0] * num_encoders; in training mode, returns a number of randomized feature masks, one per encoder. @@ -215,24 +214,30 @@ class Zipformer2(EncoderInterface): """ num_encoders = len(self.encoder_dim) if not self.training: - return [ 1.0 ] * num_encoders + return [1.0] * num_encoders (num_frames0, batch_size, _encoder_dims0) = x.shape - assert self.encoder_dim[0] == _encoder_dims0, (self.encoder_dim[0], _encoder_dims0) + assert self.encoder_dim[0] == _encoder_dims0, ( + self.encoder_dim[0], + _encoder_dims0, + ) feature_mask_dropout_prob = 0.125 # mask1 shape: (1, batch_size, 1) - mask1 = (torch.rand(1, batch_size, 1, - device=x.device) > - feature_mask_dropout_prob).to(x.dtype) + mask1 = ( + torch.rand(1, batch_size, 1, device=x.device) > feature_mask_dropout_prob + ).to(x.dtype) # mask2 has additional sequences masked, about twice the number. - mask2 = torch.logical_and(mask1, - (torch.rand(1, batch_size, 1, - device=x.device) > - feature_mask_dropout_prob).to(x.dtype)) + mask2 = torch.logical_and( + mask1, + ( + torch.rand(1, batch_size, 1, device=x.device) + > feature_mask_dropout_prob + ).to(x.dtype), + ) # dim: (1, batch_size, 2) mask = torch.cat((mask1, mask2), dim=-1) @@ -240,8 +245,9 @@ class Zipformer2(EncoderInterface): feature_masks = [] for i in range(num_encoders): channels = self.encoder_dim[i] - feature_mask = torch.ones(1, batch_size, channels, - dtype=x.dtype, device=x.device) + feature_mask = torch.ones( + 1, batch_size, channels, dtype=x.dtype, device=x.device + ) u1 = self.encoder_unmasked_dim[i] u2 = u1 + (channels - u1) // 2 @@ -281,7 +287,8 @@ class Zipformer2(EncoderInterface): return chunk_size, left_context_chunks def forward( - self, x: Tensor, + self, + x: Tensor, x_lens: Tensor, src_key_padding_mask: Optional[Tensor] = None, ) -> Tuple[Tensor, Tensor]: @@ -319,12 +326,17 @@ class Zipformer2(EncoderInterface): ds = self.downsampling_factor[i] x = convert_num_channels(x, self.encoder_dim[i]) - x = module(x, - chunk_size=chunk_size, - feature_mask=feature_masks[i], - src_key_padding_mask=(None if src_key_padding_mask is None - else src_key_padding_mask[...,::ds]), - attn_mask=attn_mask) + x = module( + x, + chunk_size=chunk_size, + feature_mask=feature_masks[i], + src_key_padding_mask=( + None + if src_key_padding_mask is None + else src_key_padding_mask[..., ::ds] + ), + attn_mask=attn_mask, + ) outputs.append(x) # if the last output has the largest dimension, x will be unchanged, @@ -345,9 +357,7 @@ class Zipformer2(EncoderInterface): return x, lengths def _get_attn_mask( - self, x: Tensor, - chunk_size: int, - left_context_chunks: int + self, x: Tensor, chunk_size: int, left_context_chunks: int ) -> Optional[Tensor]: """ Return None if chunk_size == -1, else return attention mask of shape @@ -362,9 +372,11 @@ class Zipformer2(EncoderInterface): assert all(chunk_size % d == 0 for d in self.downsampling_factor) if left_context_chunks >= 0: num_encoders = len(self.encoder_dim) - assert all (chunk_size * left_context_chunks >= - (self.cnn_module_kernel[i] // 2) * self.downsampling_factor[i] - for i in range(num_encoders)) + assert all( + chunk_size * left_context_chunks + >= (self.cnn_module_kernel[i] // 2) * self.downsampling_factor[i] + for i in range(num_encoders) + ) else: left_context_chunks = 1000000 @@ -382,8 +394,7 @@ class Zipformer2(EncoderInterface): src_c = c tgt_c = c.unsqueeze(-1) - attn_mask = torch.logical_or(src_c > tgt_c, - src_c < tgt_c - left_context_chunks) + attn_mask = torch.logical_or(src_c > tgt_c, src_c < tgt_c - left_context_chunks) if __name__ == "__main__": logging.info(f"attn_mask = {attn_mask}") return attn_mask @@ -392,7 +403,7 @@ class Zipformer2(EncoderInterface): num_encoders = len(self.encoder_dim) assert len(outputs) == num_encoders output_dim = max(self.encoder_dim) - output_pieces = [ outputs[-1] ] + output_pieces = [outputs[-1]] cur_dim = self.encoder_dim[-1] for i in range(num_encoders - 2, -1, -1): d = self.encoder_dim[i] @@ -489,21 +500,38 @@ class Zipformer2(EncoderInterface): nonlin_attn_head_dim = 3 * embed_dim // 4 conv_left_pad = self.cnn_module_kernel[i] // 2 for layer in range(num_layers): - cached_key = torch.zeros(downsample_left, batch_size, key_dim).to(device) - cached_nonlin_attn = torch.zeros(1, batch_size, downsample_left, nonlin_attn_head_dim).to(device) - cached_val1 = torch.zeros(downsample_left, batch_size, value_dim).to(device) - cached_val2 = torch.zeros(downsample_left, batch_size, value_dim).to(device) - cached_conv1 = torch.zeros(batch_size, embed_dim, conv_left_pad).to(device) - cached_conv2 = torch.zeros(batch_size, embed_dim, conv_left_pad).to(device) - states += [cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2] + cached_key = torch.zeros(downsample_left, batch_size, key_dim).to( + device + ) + cached_nonlin_attn = torch.zeros( + 1, batch_size, downsample_left, nonlin_attn_head_dim + ).to(device) + cached_val1 = torch.zeros(downsample_left, batch_size, value_dim).to( + device + ) + cached_val2 = torch.zeros(downsample_left, batch_size, value_dim).to( + device + ) + cached_conv1 = torch.zeros(batch_size, embed_dim, conv_left_pad).to( + device + ) + cached_conv2 = torch.zeros(batch_size, embed_dim, conv_left_pad).to( + device + ) + states += [ + cached_key, + cached_nonlin_attn, + cached_val1, + cached_val2, + cached_conv1, + cached_conv2, + ] return states def _whitening_schedule(x: float, ratio: float = 2.0) -> ScheduledFloat: - return ScheduledFloat((0.0, x), - (20000.0, ratio * x), - default=x) + return ScheduledFloat((0.0, x), (20000.0, ratio * x), default=x) def _balancer_schedule(min_prob: float): @@ -525,31 +553,45 @@ class Zipformer2EncoderLayer(nn.Module): >>> pos_emb = torch.rand(32, 19, 512) >>> out = encoder_layer(src, pos_emb) """ + def __init__( - self, - embed_dim: int, - pos_dim: int, - num_heads: int, - query_head_dim: int, - pos_head_dim: int, - value_head_dim: int, - feedforward_dim: int, - dropout: FloatLike = 0.1, - cnn_module_kernel: int = 31, - causal: bool = False, - attention_skip_rate: FloatLike = ScheduledFloat((0.0, 0.2), (4000.0, 0.05), (16000, 0.0), default=0), - conv_skip_rate: FloatLike = ScheduledFloat((0.0, 0.2), (4000.0, 0.05), (16000, 0.0), default=0), - const_attention_rate: FloatLike = ScheduledFloat((0.0, 0.25), (4000.0, 0.025), default=0), - ff2_skip_rate: FloatLike = ScheduledFloat((0.0, 0.1), (4000.0, 0.01), (50000.0, 0.0)), - ff3_skip_rate: FloatLike = ScheduledFloat((0.0, 0.1), (4000.0, 0.01), (50000.0, 0.0)), - bypass_skip_rate: FloatLike = ScheduledFloat((0.0, 0.5), (4000.0, 0.02), default=0), + self, + embed_dim: int, + pos_dim: int, + num_heads: int, + query_head_dim: int, + pos_head_dim: int, + value_head_dim: int, + feedforward_dim: int, + dropout: FloatLike = 0.1, + cnn_module_kernel: int = 31, + causal: bool = False, + attention_skip_rate: FloatLike = ScheduledFloat( + (0.0, 0.2), (4000.0, 0.05), (16000, 0.0), default=0 + ), + conv_skip_rate: FloatLike = ScheduledFloat( + (0.0, 0.2), (4000.0, 0.05), (16000, 0.0), default=0 + ), + const_attention_rate: FloatLike = ScheduledFloat( + (0.0, 0.25), (4000.0, 0.025), default=0 + ), + ff2_skip_rate: FloatLike = ScheduledFloat( + (0.0, 0.1), (4000.0, 0.01), (50000.0, 0.0) + ), + ff3_skip_rate: FloatLike = ScheduledFloat( + (0.0, 0.1), (4000.0, 0.01), (50000.0, 0.0) + ), + bypass_skip_rate: FloatLike = ScheduledFloat( + (0.0, 0.5), (4000.0, 0.02), default=0 + ), ) -> None: super(Zipformer2EncoderLayer, self).__init__() self.embed_dim = embed_dim # self.bypass implements layer skipping as well as bypass; see its default values. - self.bypass = BypassModule(embed_dim, skip_rate=bypass_skip_rate, - straight_through_rate=0) + self.bypass = BypassModule( + embed_dim, skip_rate=bypass_skip_rate, straight_through_rate=0 + ) # bypass_mid is bypass used in the middle of the layer. self.bypass_mid = BypassModule(embed_dim, straight_through_rate=0) @@ -567,39 +609,39 @@ class Zipformer2EncoderLayer(nn.Module): self.const_attention_rate = copy.deepcopy(const_attention_rate) self.self_attn_weights = RelPositionMultiheadAttentionWeights( - embed_dim, pos_dim=pos_dim, num_heads=num_heads, - query_head_dim=query_head_dim, pos_head_dim=pos_head_dim, + embed_dim, + pos_dim=pos_dim, + num_heads=num_heads, + query_head_dim=query_head_dim, + pos_head_dim=pos_head_dim, dropout=0.0, ) - self.self_attn1 = SelfAttention(embed_dim, num_heads, - value_head_dim) + self.self_attn1 = SelfAttention(embed_dim, num_heads, value_head_dim) - self.self_attn2 = SelfAttention(embed_dim, num_heads, - value_head_dim) + self.self_attn2 = SelfAttention(embed_dim, num_heads, value_head_dim) - self.feed_forward1 = FeedforwardModule(embed_dim, - (feedforward_dim * 3) // 4, - dropout) + self.feed_forward1 = FeedforwardModule( + embed_dim, (feedforward_dim * 3) // 4, dropout + ) - self.feed_forward2 = FeedforwardModule(embed_dim, - feedforward_dim, - dropout) + self.feed_forward2 = FeedforwardModule(embed_dim, feedforward_dim, dropout) - self.feed_forward3 = FeedforwardModule(embed_dim, - (feedforward_dim * 5) // 4, - dropout) + self.feed_forward3 = FeedforwardModule( + embed_dim, (feedforward_dim * 5) // 4, dropout + ) - self.nonlin_attention = NonlinAttention(embed_dim, - hidden_channels=3 * embed_dim // 4) + self.nonlin_attention = NonlinAttention( + embed_dim, hidden_channels=3 * embed_dim // 4 + ) - self.conv_module1 = ConvolutionModule(embed_dim, - cnn_module_kernel, - causal=causal) + self.conv_module1 = ConvolutionModule( + embed_dim, cnn_module_kernel, causal=causal + ) - self.conv_module2 = ConvolutionModule(embed_dim, - cnn_module_kernel, - causal=causal) + self.conv_module2 = ConvolutionModule( + embed_dim, cnn_module_kernel, causal=causal + ) # TODO: remove it self.bypass_scale = nn.Parameter(torch.full((embed_dim,), 0.5)) @@ -607,15 +649,20 @@ class Zipformer2EncoderLayer(nn.Module): self.norm = BiasNorm(embed_dim) self.balancer1 = Balancer( - embed_dim, channel_dim=-1, - min_positive=0.45, max_positive=0.55, - min_abs=0.2, max_abs=4.0, + embed_dim, + channel_dim=-1, + min_positive=0.45, + max_positive=0.55, + min_abs=0.2, + max_abs=4.0, ) # balancer for output of NonlinAttentionModule self.balancer_na = Balancer( - embed_dim, channel_dim=-1, - min_positive=0.3, max_positive=0.7, + embed_dim, + channel_dim=-1, + min_positive=0.3, + max_positive=0.7, min_abs=ScheduledFloat((0.0, 0.004), (4000.0, 0.02)), prob=0.05, # out of concern for memory usage ) @@ -624,34 +671,50 @@ class Zipformer2EncoderLayer(nn.Module): # small. give this a very small probability, even at the start of # training, it's to fix a rare problem and it's OK to fix it slowly. self.balancer_ff2 = Balancer( - embed_dim, channel_dim=-1, - min_positive=0.3, max_positive=0.7, + embed_dim, + channel_dim=-1, + min_positive=0.3, + max_positive=0.7, min_abs=ScheduledFloat((0.0, 0.0), (4000.0, 0.1), default=0.0), max_abs=2.0, prob=0.05, ) self.balancer_ff3 = Balancer( - embed_dim, channel_dim=-1, - min_positive=0.3, max_positive=0.7, + embed_dim, + channel_dim=-1, + min_positive=0.3, + max_positive=0.7, min_abs=ScheduledFloat((0.0, 0.0), (4000.0, 0.2), default=0.0), max_abs=4.0, prob=0.05, ) - self.whiten = Whiten(num_groups=1, - whitening_limit=_whitening_schedule(4.0, ratio=3.0), - prob=(0.025, 0.25), - grad_scale=0.01) - - self.balancer2 = Balancer( - embed_dim, channel_dim=-1, - min_positive=0.45, max_positive=0.55, - min_abs=0.1, max_abs=4.0, + self.whiten = Whiten( + num_groups=1, + whitening_limit=_whitening_schedule(4.0, ratio=3.0), + prob=(0.025, 0.25), + grad_scale=0.01, ) - def get_sequence_dropout_mask(self, x: Tensor, dropout_rate: float) -> Optional[Tensor]: - if dropout_rate == 0.0 or not self.training or torch.jit.is_scripting() or torch.jit.is_tracing(): + self.balancer2 = Balancer( + embed_dim, + channel_dim=-1, + min_positive=0.45, + max_positive=0.55, + min_abs=0.1, + max_abs=4.0, + ) + + def get_sequence_dropout_mask( + self, x: Tensor, dropout_rate: float + ) -> Optional[Tensor]: + if ( + dropout_rate == 0.0 + or not self.training + or torch.jit.is_scripting() + or torch.jit.is_tracing() + ): return None batch_size = x.shape[1] mask = (torch.rand(batch_size, 1, device=x.device) > dropout_rate).to(x.dtype) @@ -677,21 +740,21 @@ class Zipformer2EncoderLayer(nn.Module): src_key_padding_mask: Optional[Tensor] = None, ) -> Tensor: """ - Pass the input through the encoder layer. - Args: - src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). - pos_emb: (1, 2*seq_len-1, pos_emb_dim) or (batch_size, 2*seq_len-1, pos_emb_dim) - chunk_size: the number of frames per chunk, of >= 0; if -1, no chunking. - feature_mask: something that broadcasts with src, that we'll multiply `src` - by at every layer: if a Tensor, likely of shape (seq_len, batch_size, embedding_dim) - attn_mask: the attention mask, of shape (batch_size, seq_len, seq_len) or (seq_len, seq_len), - interpreted as (batch_size, tgt_seq_len, src_seq_len) or (tgt_seq_len, src_seq_len). - True means masked position. May be None. - src_key_padding_mask: the mask for padding, of shape (batch_size, seq_len); True means - masked position. May be None. + Pass the input through the encoder layer. + Args: + src: the sequence to the encoder (required): shape (seq_len, batch_size, embedding_dim). + pos_emb: (1, 2*seq_len-1, pos_emb_dim) or (batch_size, 2*seq_len-1, pos_emb_dim) + chunk_size: the number of frames per chunk, of >= 0; if -1, no chunking. + feature_mask: something that broadcasts with src, that we'll multiply `src` + by at every layer: if a Tensor, likely of shape (seq_len, batch_size, embedding_dim) + attn_mask: the attention mask, of shape (batch_size, seq_len, seq_len) or (seq_len, seq_len), + interpreted as (batch_size, tgt_seq_len, src_seq_len) or (tgt_seq_len, src_seq_len). + True means masked position. May be None. + src_key_padding_mask: the mask for padding, of shape (batch_size, seq_len); True means + masked position. May be None. - Returns: - A tensor which has the same shape as src + Returns: + A tensor which has the same shape as src """ src_orig = src @@ -699,7 +762,9 @@ class Zipformer2EncoderLayer(nn.Module): if torch.jit.is_scripting() or torch.jit.is_tracing(): attention_skip_rate = 0.0 else: - attention_skip_rate = float(self.attention_skip_rate) if self.training else 0.0 + attention_skip_rate = ( + float(self.attention_skip_rate) if self.training else 0.0 + ) # attn_weights: (num_heads, batch_size, seq_len, seq_len) attn_weights = self.self_attn_weights( @@ -711,7 +776,9 @@ class Zipformer2EncoderLayer(nn.Module): src = src + self.feed_forward1(src) - self_attn_dropout_mask = self.get_sequence_dropout_mask(src, attention_skip_rate) + self_attn_dropout_mask = self.get_sequence_dropout_mask( + src, attention_skip_rate + ) selected_attn_weights = attn_weights[0:1] if torch.jit.is_scripting() or torch.jit.is_tracing(): @@ -722,53 +789,75 @@ class Zipformer2EncoderLayer(nn.Module): # averaging-over-time operation. # only need the mask, can just use the 1st one and expand later selected_attn_weights = selected_attn_weights[0:1] - selected_attn_weights = (selected_attn_weights > 0.0).to(selected_attn_weights.dtype) - selected_attn_weights = selected_attn_weights * (1.0 / selected_attn_weights.sum(dim=-1, keepdim=True)) + selected_attn_weights = (selected_attn_weights > 0.0).to( + selected_attn_weights.dtype + ) + selected_attn_weights = selected_attn_weights * ( + 1.0 / selected_attn_weights.sum(dim=-1, keepdim=True) + ) na = self.balancer_na(self.nonlin_attention(src, selected_attn_weights)) - src = src + (na if self_attn_dropout_mask is None else na * self_attn_dropout_mask) + src = src + ( + na if self_attn_dropout_mask is None else na * self_attn_dropout_mask + ) self_attn = self.self_attn1(src, attn_weights) - src = src + (self_attn if self_attn_dropout_mask is None else self_attn * self_attn_dropout_mask) + src = src + ( + self_attn + if self_attn_dropout_mask is None + else self_attn * self_attn_dropout_mask + ) if torch.jit.is_scripting() or torch.jit.is_tracing(): conv_skip_rate = 0.0 else: conv_skip_rate = float(self.conv_skip_rate) if self.training else 0.0 - src = src + self.sequence_dropout(self.conv_module1(src, chunk_size=chunk_size, - src_key_padding_mask=src_key_padding_mask), - conv_skip_rate) + src = src + self.sequence_dropout( + self.conv_module1( + src, chunk_size=chunk_size, src_key_padding_mask=src_key_padding_mask + ), + conv_skip_rate, + ) if torch.jit.is_scripting() or torch.jit.is_tracing(): ff2_skip_rate = 0.0 else: ff2_skip_rate = float(self.ff2_skip_rate) if self.training else 0.0 - src = src + self.sequence_dropout(self.balancer_ff2(self.feed_forward2(src)), - ff2_skip_rate) + src = src + self.sequence_dropout( + self.balancer_ff2(self.feed_forward2(src)), ff2_skip_rate + ) # bypass in the middle of the layer. src = self.bypass_mid(src_orig, src) self_attn = self.self_attn2(src, attn_weights) - src = src + (self_attn if self_attn_dropout_mask is None else self_attn * self_attn_dropout_mask) + src = src + ( + self_attn + if self_attn_dropout_mask is None + else self_attn * self_attn_dropout_mask + ) if torch.jit.is_scripting() or torch.jit.is_tracing(): conv_skip_rate = 0.0 else: conv_skip_rate = float(self.conv_skip_rate) if self.training else 0.0 - src = src + self.sequence_dropout(self.conv_module2(src, chunk_size=chunk_size, - src_key_padding_mask=src_key_padding_mask), - conv_skip_rate) + src = src + self.sequence_dropout( + self.conv_module2( + src, chunk_size=chunk_size, src_key_padding_mask=src_key_padding_mask + ), + conv_skip_rate, + ) if torch.jit.is_scripting() or torch.jit.is_tracing(): ff3_skip_rate = 0.0 else: ff3_skip_rate = float(self.ff3_skip_rate) if self.training else 0.0 - src = src + self.sequence_dropout(self.balancer_ff3(self.feed_forward3(src)), - ff3_skip_rate) + src = src + self.sequence_dropout( + self.balancer_ff3(self.feed_forward3(src)), ff3_skip_rate + ) src = self.balancer1(src) src = self.norm(src) @@ -912,20 +1001,22 @@ class Zipformer2Encoder(nn.Module): >>> src = torch.rand(10, 32, 512) >>> out = zipformer_encoder(src) """ + def __init__( - self, - encoder_layer: nn.Module, - num_layers: int, - pos_dim: int, - dropout: float, - warmup_begin: float, - warmup_end: float, - initial_layerdrop_rate: float = 0.5, - final_layerdrop_rate: float = 0.05, + self, + encoder_layer: nn.Module, + num_layers: int, + pos_dim: int, + dropout: float, + warmup_begin: float, + warmup_end: float, + initial_layerdrop_rate: float = 0.5, + final_layerdrop_rate: float = 0.05, ) -> None: super().__init__() - self.encoder_pos = CompactRelPositionalEncoding(pos_dim, dropout_rate=0.15, - length_factor=1.0) + self.encoder_pos = CompactRelPositionalEncoding( + pos_dim, dropout_rate=0.15, length_factor=1.0 + ) self.layers = nn.ModuleList( [copy.deepcopy(encoder_layer) for i in range(num_layers)] @@ -934,13 +1025,15 @@ class Zipformer2Encoder(nn.Module): assert 0 <= warmup_begin <= warmup_end - delta = (1. / num_layers) * (warmup_end - warmup_begin) + delta = (1.0 / num_layers) * (warmup_end - warmup_begin) cur_begin = warmup_begin # interpreted as a training batch index for i in range(num_layers): cur_end = cur_begin + delta - self.layers[i].bypass.skip_rate = ScheduledFloat((cur_begin, initial_layerdrop_rate), - (cur_end, final_layerdrop_rate), - default=0.0) + self.layers[i].bypass.skip_rate = ScheduledFloat( + (cur_begin, initial_layerdrop_rate), + (cur_end, final_layerdrop_rate), + default=0.0, + ) cur_begin = cur_end def forward( @@ -1014,8 +1107,13 @@ class Zipformer2Encoder(nn.Module): new_states = [] for i, mod in enumerate(self.layers): ( - cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2 - ) = states[i * 6: (i + 1) * 6] + cached_key, + cached_nonlin_attn, + cached_val1, + cached_val2, + cached_conv1, + cached_conv2, + ) = states[i * 6 : (i + 1) * 6] ( output, new_cached_key, @@ -1023,7 +1121,7 @@ class Zipformer2Encoder(nn.Module): new_cached_val1, new_cached_val2, new_cached_conv1, - new_cached_conv2 + new_cached_conv2, ) = mod.streaming_forward( output, pos_emb, @@ -1055,13 +1153,15 @@ class BypassModule(nn.Module): "straight-through", i.e. to not do the bypass operation much initially, in order to force all the modules to learn something. """ + def __init__( - self, - embed_dim: int, - skip_rate: FloatLike = 0.0, - straight_through_rate: FloatLike = 0.0, - scale_min: FloatLike = ScheduledFloat((0.0, 0.9), (20000.0, 0.2), default=0), - scale_max: FloatLike = 1.0): + self, + embed_dim: int, + skip_rate: FloatLike = 0.0, + straight_through_rate: FloatLike = 0.0, + scale_min: FloatLike = ScheduledFloat((0.0, 0.9), (20000.0, 0.2), default=0), + scale_max: FloatLike = 1.0, + ): super().__init__() self.bypass_scale = nn.Parameter(torch.full((embed_dim,), 0.5)) self.skip_rate = copy.deepcopy(skip_rate) @@ -1077,9 +1177,9 @@ class BypassModule(nn.Module): if torch.jit.is_scripting() or torch.jit.is_tracing() or not self.training: return self.bypass_scale else: - ans = limit_param_value(self.bypass_scale, - min=float(self.scale_min), - max=float(self.scale_max)) + ans = limit_param_value( + self.bypass_scale, min=float(self.scale_min), max=float(self.scale_max) + ) skip_rate = float(self.skip_rate) if skip_rate != 0.0: mask = torch.rand((batch_size, 1), device=ans.device) > skip_rate @@ -1088,13 +1188,14 @@ class BypassModule(nn.Module): # on which we have randomly chosen to do layer-skipping. straight_through_rate = float(self.straight_through_rate) if straight_through_rate != 0.0: - mask = torch.rand((batch_size, 1), device=ans.device) < straight_through_rate + mask = ( + torch.rand((batch_size, 1), device=ans.device) + < straight_through_rate + ) ans = torch.maximum(ans, mask.to(ans.dtype)) return ans - def forward(self, - src_orig: Tensor, - src: Tensor): + def forward(self, src_orig: Tensor, src: Tensor): """ Args: src_orig and src are both of shape (seq_len, batch_size, num_channels) Returns: something with the same shape as src and src_orig @@ -1109,15 +1210,13 @@ class DownsampledZipformer2Encoder(nn.Module): after convolutional downsampling, and then upsampled again at the output, and combined with the origin input, so that the output has the same shape as the input. """ - def __init__(self, - encoder: nn.Module, - dim: int, - downsample: int, - dropout: FloatLike): + + def __init__( + self, encoder: nn.Module, dim: int, downsample: int, dropout: FloatLike + ): super(DownsampledZipformer2Encoder, self).__init__() self.downsample_factor = downsample - self.downsample = SimpleDownsample(dim, - downsample, dropout) + self.downsample = SimpleDownsample(dim, downsample, dropout) self.num_layers = encoder.num_layers self.encoder = encoder self.upsample = SimpleUpsample(dim, downsample) @@ -1149,7 +1248,7 @@ class DownsampledZipformer2Encoder(nn.Module): src = self.downsample(src) ds = self.downsample_factor if attn_mask is not None: - attn_mask = attn_mask[::ds,::ds] + attn_mask = attn_mask[::ds, ::ds] src = self.encoder( src, @@ -1160,7 +1259,7 @@ class DownsampledZipformer2Encoder(nn.Module): ) src = self.upsample(src) # remove any extra frames that are not a multiple of downsample_factor - src = src[:src_orig.shape[0]] + src = src[: src_orig.shape[0]] return self.out_combiner(src_orig, src) @@ -1196,7 +1295,7 @@ class DownsampledZipformer2Encoder(nn.Module): ) src = self.upsample(src) # remove any extra frames that are not a multiple of downsample_factor - src = src[:src_orig.shape[0]] + src = src[: src_orig.shape[0]] return self.out_combiner(src_orig, src), new_states @@ -1205,10 +1304,8 @@ class SimpleDownsample(torch.nn.Module): """ Does downsampling with attention, by weighted sum, and a projection.. """ - def __init__(self, - channels: int, - downsample: int, - dropout: FloatLike): + + def __init__(self, channels: int, downsample: int, dropout: FloatLike): super(SimpleDownsample, self).__init__() self.bias = nn.Parameter(torch.zeros(downsample)) @@ -1218,8 +1315,7 @@ class SimpleDownsample(torch.nn.Module): self.downsample = downsample - def forward(self, - src: Tensor) -> Tensor: + def forward(self, src: Tensor) -> Tensor: """ x: (seq_len, batch_size, in_channels) Returns a tensor of shape @@ -1232,7 +1328,7 @@ class SimpleDownsample(torch.nn.Module): # Pad to an exact multiple of self.downsample # right-pad src, repeating the last element. pad = d_seq_len * ds - seq_len - src_extra = src[src.shape[0]-1:].expand(pad, src.shape[1], src.shape[2]) + src_extra = src[src.shape[0] - 1 :].expand(pad, src.shape[1], src.shape[2]) src = torch.cat((src, src_extra), dim=0) assert src.shape[0] == d_seq_len * ds @@ -1253,14 +1349,12 @@ class SimpleUpsample(torch.nn.Module): A very simple form of upsampling that mostly just repeats the input, but also adds a position-specific bias. """ - def __init__(self, - num_channels: int, - upsample: int): + + def __init__(self, num_channels: int, upsample: int): super(SimpleUpsample, self).__init__() self.upsample = upsample - def forward(self, - src: Tensor) -> Tensor: + def forward(self, src: Tensor) -> Tensor: """ x: (seq_len, batch_size, num_channels) Returns a tensor of shape @@ -1298,11 +1392,13 @@ class CompactRelPositionalEncoding(torch.nn.Module): length_factor: a heuristic scale (should be >= 1.0) which, if larger, gives less weight to small differences of offset near the origin. """ + def __init__( - self, embed_dim: int, - dropout_rate: FloatLike, - max_len: int = 1000, - length_factor: float = 1.0, + self, + embed_dim: int, + dropout_rate: FloatLike, + max_len: int = 1000, + length_factor: float = 1.0, ) -> None: """Construct a CompactRelPositionalEncoding object.""" super(CompactRelPositionalEncoding, self).__init__() @@ -1326,19 +1422,22 @@ class CompactRelPositionalEncoding(torch.nn.Module): return # if T == 4, x would contain [ -3, -2, 1, 0, 1, 2, 3 ] - x = torch.arange(-(T-1), T, - device=x.device).to(torch.float32).unsqueeze(1) + x = torch.arange(-(T - 1), T, device=x.device).to(torch.float32).unsqueeze(1) freqs = 1 + torch.arange(self.embed_dim // 2, device=x.device) # `compression_length` this is arbitrary/heuristic, if it is larger we have more resolution # for small time offsets but less resolution for large time offsets. - compression_length = (self.embed_dim ** 0.5) + compression_length = self.embed_dim**0.5 # x_compressed, like X, goes from -infinity to infinity as T goes from -infinity to infinity; # but it does so more slowly than T for large absolute values of T. # The formula is chosen so that d(x_compressed )/dx is 1 around x == 0, which # is important. - x_compressed = compression_length * x.sign() * ((x.abs() + compression_length).log() - math.log(compression_length)) + x_compressed = ( + compression_length + * x.sign() + * ((x.abs() + compression_length).log() - math.log(compression_length)) + ) # if self.length_factor == 1.0, then length_scale is chosen so that the # FFT can exactly separate points close to the origin (T == 0). So this @@ -1380,7 +1479,7 @@ class CompactRelPositionalEncoding(torch.nn.Module): - x_size_left + 1 : self.pe.size(0) // 2 # noqa E203 + x.size(0), - : + :, ] pos_emb = pos_emb.unsqueeze(0) return self.dropout(pos_emb) @@ -1407,15 +1506,14 @@ class RelPositionMultiheadAttentionWeights(nn.Module): """ def __init__( - self, - embed_dim: int, - pos_dim: int, - num_heads: int, - query_head_dim: int, - pos_head_dim: int, - dropout: float = 0.0, - pos_emb_skip_rate: FloatLike = ScheduledFloat((0.0, 0.5), - (4000.0, 0.0)) + self, + embed_dim: int, + pos_dim: int, + num_heads: int, + query_head_dim: int, + pos_head_dim: int, + dropout: float = 0.0, + pos_emb_skip_rate: FloatLike = ScheduledFloat((0.0, 0.5), (4000.0, 0.0)), ) -> None: super().__init__() self.embed_dim = embed_dim @@ -1434,13 +1532,16 @@ class RelPositionMultiheadAttentionWeights(nn.Module): # dividing it between the query and key. Note: this module is intended # to be used with the ScaledAdam optimizer; with most other optimizers, # it would be necessary to apply the scaling factor in the forward function. - self.in_proj = ScaledLinear(embed_dim, in_proj_dim, bias=True, - initial_scale=query_head_dim**-0.25) + self.in_proj = ScaledLinear( + embed_dim, in_proj_dim, bias=True, initial_scale=query_head_dim**-0.25 + ) - self.whiten_keys = Whiten(num_groups=num_heads, - whitening_limit=_whitening_schedule(3.0), - prob=(0.025, 0.25), - grad_scale=0.025) + self.whiten_keys = Whiten( + num_groups=num_heads, + whitening_limit=_whitening_schedule(3.0), + prob=(0.025, 0.25), + grad_scale=0.025, + ) # add a balancer for the keys that runs with very small probability, and # tries to enforce that all dimensions have mean around zero. The @@ -1450,19 +1551,20 @@ class RelPositionMultiheadAttentionWeights(nn.Module): # bias because the small numerical roundoff tends to have a non-random # sign. This module is intended to prevent that. Use a very small # probability; that should be suffixient to fix the problem. - self.balance_keys = Balancer(key_head_dim * num_heads, - channel_dim=-1, - min_positive=0.4, - max_positive=0.6, - min_abs=0.0, - max_abs=100.0, - prob=0.025) + self.balance_keys = Balancer( + key_head_dim * num_heads, + channel_dim=-1, + min_positive=0.4, + max_positive=0.6, + min_abs=0.0, + max_abs=100.0, + prob=0.025, + ) # linear transformation for positional encoding. - self.linear_pos = ScaledLinear(pos_dim, - num_heads * pos_head_dim, - bias=False, - initial_scale=0.05) + self.linear_pos = ScaledLinear( + pos_dim, num_heads * pos_head_dim, bias=False, initial_scale=0.05 + ) # the following are for diagnosics only, see --print-diagnostics option self.copy_pos_query = Identity() @@ -1498,10 +1600,10 @@ class RelPositionMultiheadAttentionWeights(nn.Module): query_dim = query_head_dim * num_heads # self-attention - q = x[...,0:query_dim] - k = x[...,query_dim:2*query_dim] + q = x[..., 0:query_dim] + k = x[..., query_dim : 2 * query_dim] # p is the position-encoding query - p = x[...,2*query_dim:] + p = x[..., 2 * query_dim :] assert p.shape[-1] == num_heads * pos_head_dim q = self.copy_query(q) # for diagnostics only, does nothing. @@ -1529,7 +1631,9 @@ class RelPositionMultiheadAttentionWeights(nn.Module): if use_pos_scores: pos_emb = self.linear_pos(pos_emb) seq_len2 = 2 * seq_len - 1 - pos_emb = pos_emb.reshape(-1, seq_len2, num_heads, pos_head_dim).permute(2, 0, 3, 1) + pos_emb = pos_emb.reshape(-1, seq_len2, num_heads, pos_head_dim).permute( + 2, 0, 3, 1 + ) # pos shape now: (head, {1 or batch_size}, pos_dim, seq_len2) # (head, batch, time1, pos_dim) x (head, 1, pos_dim, seq_len2) -> (head, batch, time1, seq_len2) @@ -1548,12 +1652,16 @@ class RelPositionMultiheadAttentionWeights(nn.Module): pos_scores = torch.gather(pos_scores, dim=1, index=indexes) pos_scores = pos_scores.reshape(num_heads, batch_size, time1, seq_len) else: - pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, seq_len), - (pos_scores.stride(0), - pos_scores.stride(1), - pos_scores.stride(2)-pos_scores.stride(3), - pos_scores.stride(3)), - storage_offset=pos_scores.stride(3) * (seq_len - 1)) + pos_scores = pos_scores.as_strided( + (num_heads, batch_size, seq_len, seq_len), + ( + pos_scores.stride(0), + pos_scores.stride(1), + pos_scores.stride(2) - pos_scores.stride(3), + pos_scores.stride(3), + ), + storage_offset=pos_scores.stride(3) * (seq_len - 1), + ) attn_scores = attn_scores + pos_scores @@ -1572,10 +1680,9 @@ class RelPositionMultiheadAttentionWeights(nn.Module): # but we view this as a failsafe to avoid "implausible" parameter # values rather than a regularization method that should be active # under normal circumstances. - attn_scores = penalize_abs_values_gt(attn_scores, - limit=25.0, - penalty=1.0e-04, - name=self.name) + attn_scores = penalize_abs_values_gt( + attn_scores, limit=25.0, penalty=1.0e-04, name=self.name + ) assert attn_scores.shape == (num_heads, batch_size, seq_len, seq_len) @@ -1588,7 +1695,10 @@ class RelPositionMultiheadAttentionWeights(nn.Module): attn_scores = attn_scores.masked_fill(attn_mask, -1000) if key_padding_mask is not None: - assert key_padding_mask.shape == (batch_size, seq_len), key_padding_mask.shape + assert key_padding_mask.shape == ( + batch_size, + seq_len, + ), key_padding_mask.shape attn_scores = attn_scores.masked_fill( key_padding_mask.unsqueeze(1), -1000, @@ -1644,14 +1754,17 @@ class RelPositionMultiheadAttentionWeights(nn.Module): query_dim = query_head_dim * num_heads # self-attention - q = x[...,0:query_dim] - k = x[...,query_dim:2*query_dim] + q = x[..., 0:query_dim] + k = x[..., query_dim : 2 * query_dim] # p is the position-encoding query - p = x[...,2*query_dim:] + p = x[..., 2 * query_dim :] assert p.shape[-1] == num_heads * pos_head_dim # Pad cached left contexts - assert cached_key.shape[0] == left_context_len, (cached_key.shape[0], left_context_len) + assert cached_key.shape[0] == left_context_len, ( + cached_key.shape[0], + left_context_len, + ) k = torch.cat([cached_key, k], dim=0) # Update cached left contexts cached_key = k[-left_context_len:, ...] @@ -1672,13 +1785,15 @@ class RelPositionMultiheadAttentionWeights(nn.Module): pos_emb = self.linear_pos(pos_emb) seq_len2 = 2 * seq_len - 1 + left_context_len - pos_emb = pos_emb.reshape(-1, seq_len2, num_heads, pos_head_dim).permute(2, 0, 3, 1) + pos_emb = pos_emb.reshape(-1, seq_len2, num_heads, pos_head_dim).permute( + 2, 0, 3, 1 + ) # pos shape now: (head, {1 or batch_size}, pos_dim, seq_len2) # (head, batch, time1, pos_dim) x (head, 1, pos_dim, seq_len2) -> (head, batch, time1, seq_len2) # [where seq_len2 represents relative position.] pos_scores = torch.matmul(p, pos_emb) - + if torch.jit.is_tracing(): (num_heads, batch_size, time1, n) = pos_scores.shape rows = torch.arange(start=time1 - 1, end=-1, step=-1) @@ -1692,16 +1807,25 @@ class RelPositionMultiheadAttentionWeights(nn.Module): # to absolute position. I don't know whether I might have got the time-offsets backwards or # not, but let this code define which way round it is supposed to be. else: - pos_scores = pos_scores.as_strided((num_heads, batch_size, seq_len, k_len), - (pos_scores.stride(0), - pos_scores.stride(1), - pos_scores.stride(2)-pos_scores.stride(3), - pos_scores.stride(3)), - storage_offset=pos_scores.stride(3) * (seq_len - 1)) + pos_scores = pos_scores.as_strided( + (num_heads, batch_size, seq_len, k_len), + ( + pos_scores.stride(0), + pos_scores.stride(1), + pos_scores.stride(2) - pos_scores.stride(3), + pos_scores.stride(3), + ), + storage_offset=pos_scores.stride(3) * (seq_len - 1), + ) attn_scores = attn_scores + pos_scores - assert attn_scores.shape == (num_heads, batch_size, seq_len, k_len), attn_scores.shape + assert attn_scores.shape == ( + num_heads, + batch_size, + seq_len, + k_len, + ), attn_scores.shape if key_padding_mask is not None: assert key_padding_mask.shape == (batch_size, k_len), key_padding_mask.shape @@ -1714,18 +1838,21 @@ class RelPositionMultiheadAttentionWeights(nn.Module): return attn_weights, cached_key - def _print_attn_entropy( - self, - attn_weights: Tensor): + def _print_attn_entropy(self, attn_weights: Tensor): # attn_weights: (num_heads, batch_size, seq_len, seq_len) (num_heads, batch_size, seq_len, seq_len) = attn_weights.shape with torch.no_grad(): with torch.cuda.amp.autocast(enabled=False): attn_weights = attn_weights.to(torch.float32) - attn_weights_entropy = -((attn_weights + 1.0e-20).log() * attn_weights).sum( - dim=-1).mean(dim=(1,2)) - logging.info(f"name={self.name}, attn_weights_entropy = {attn_weights_entropy}") + attn_weights_entropy = ( + -((attn_weights + 1.0e-20).log() * attn_weights) + .sum(dim=-1) + .mean(dim=(1, 2)) + ) + logging.info( + f"name={self.name}, attn_weights_entropy = {attn_weights_entropy}" + ) class SelfAttention(nn.Module): @@ -1738,25 +1865,26 @@ class SelfAttention(nn.Module): num_heads: the number of attention heads value_head_dim: the value dimension per head """ + def __init__( - self, - embed_dim: int, - num_heads: int, - value_head_dim: int, + self, + embed_dim: int, + num_heads: int, + value_head_dim: int, ) -> None: super().__init__() - self.in_proj = nn.Linear(embed_dim, - num_heads * value_head_dim, - bias=True) + self.in_proj = nn.Linear(embed_dim, num_heads * value_head_dim, bias=True) - self.out_proj = ScaledLinear(num_heads * value_head_dim, - embed_dim, bias=True, - initial_scale=0.05) + self.out_proj = ScaledLinear( + num_heads * value_head_dim, embed_dim, bias=True, initial_scale=0.05 + ) - self.whiten = Whiten(num_groups=1, - whitening_limit=_whitening_schedule(7.5, ratio=3.0), - prob=(0.025, 0.25), - grad_scale=0.01) + self.whiten = Whiten( + num_groups=1, + whitening_limit=_whitening_schedule(7.5, ratio=3.0), + prob=(0.025, 0.25), + grad_scale=0.01, + ) def forward( self, @@ -1785,8 +1913,11 @@ class SelfAttention(nn.Module): x = torch.matmul(attn_weights, x) # v: (num_heads, batch_size, seq_len, value_head_dim) - x = x.permute(2, 1, 0, 3).contiguous().view( - seq_len, batch_size, num_heads * value_head_dim) + x = ( + x.permute(2, 1, 0, 3) + .contiguous() + .view(seq_len, batch_size, num_heads * value_head_dim) + ) # returned value is of shape (seq_len, batch_size, embed_dim), like the input. x = self.out_proj(x) @@ -1823,7 +1954,10 @@ class SelfAttention(nn.Module): x = self.in_proj(x) # (seq_len, batch_size, num_heads * value_head_dim) # Pad cached left contexts - assert cached_val.shape[0] == left_context_len, (cached_val.shape[0], left_context_len) + assert cached_val.shape[0] == left_context_len, ( + cached_val.shape[0], + left_context_len, + ) x = torch.cat([cached_val, x], dim=0) # Update cached left contexts cached_val = x[-left_context_len:, ...] @@ -1836,8 +1970,11 @@ class SelfAttention(nn.Module): x = torch.matmul(attn_weights, x) # v: (num_heads, batch_size, seq_len, value_head_dim) - x = x.permute(2, 1, 0, 3).contiguous().view( - seq_len, batch_size, num_heads * value_head_dim) + x = ( + x.permute(2, 1, 0, 3) + .contiguous() + .view(seq_len, batch_size, num_heads * value_head_dim) + ) # returned value is of shape (seq_len, batch_size, embed_dim), like the input. x = self.out_proj(x) @@ -1846,33 +1983,38 @@ class SelfAttention(nn.Module): class FeedforwardModule(nn.Module): - """Feedforward module in Zipformer2 model. - """ - def __init__(self, - embed_dim: int, - feedforward_dim: int, - dropout: FloatLike): + """Feedforward module in Zipformer2 model.""" + + def __init__(self, embed_dim: int, feedforward_dim: int, dropout: FloatLike): super(FeedforwardModule, self).__init__() self.in_proj = nn.Linear(embed_dim, feedforward_dim) - self.hidden_balancer = Balancer(feedforward_dim, - channel_dim=-1, - min_positive=0.3, - max_positive=1.0, - min_abs=0.75, - max_abs=5.0) + self.hidden_balancer = Balancer( + feedforward_dim, + channel_dim=-1, + min_positive=0.3, + max_positive=1.0, + min_abs=0.75, + max_abs=5.0, + ) # shared_dim=0 means we share the dropout mask along the time axis - self.out_proj = ActivationDropoutAndLinear(feedforward_dim, embed_dim, - activation='SwooshL', - dropout_p=dropout, - dropout_shared_dim=0, bias=True, - initial_scale=0.1) + self.out_proj = ActivationDropoutAndLinear( + feedforward_dim, + embed_dim, + activation="SwooshL", + dropout_p=dropout, + dropout_shared_dim=0, + bias=True, + initial_scale=0.1, + ) - self.out_whiten = Whiten(num_groups=1, - whitening_limit=_whitening_schedule(7.5), - prob=(0.025, 0.25), - grad_scale=0.01) + self.out_whiten = Whiten( + num_groups=1, + whitening_limit=_whitening_schedule(7.5), + prob=(0.025, 0.25), + grad_scale=0.01, + ) def forward(self, x: Tensor): x = self.in_proj(x) @@ -1893,9 +2035,9 @@ class NonlinAttention(nn.Module): """ def __init__( - self, - channels: int, - hidden_channels: int, + self, + channels: int, + hidden_channels: int, ) -> None: super().__init__() @@ -1908,7 +2050,8 @@ class NonlinAttention(nn.Module): # starting from about 3, and poorly-trained instances of the module have smaller abs values # before the sigmoid. self.balancer = Balancer( - hidden_channels, channel_dim=-1, + hidden_channels, + channel_dim=-1, min_positive=ScheduledFloat((0.0, 0.25), (20000.0, 0.05)), max_positive=ScheduledFloat((0.0, 0.75), (20000.0, 0.95)), min_abs=0.5, @@ -1920,19 +2063,23 @@ class NonlinAttention(nn.Module): self.identity2 = Identity() # for diagnostics. self.identity3 = Identity() # for diagnostics. - self.out_proj = ScaledLinear(hidden_channels, channels, - bias=True, - initial_scale=0.05) + self.out_proj = ScaledLinear( + hidden_channels, channels, bias=True, initial_scale=0.05 + ) - self.whiten1 = Whiten(num_groups=1, - whitening_limit=_whitening_schedule(5.0), - prob=(0.025, 0.25), - grad_scale=0.01) + self.whiten1 = Whiten( + num_groups=1, + whitening_limit=_whitening_schedule(5.0), + prob=(0.025, 0.25), + grad_scale=0.01, + ) - self.whiten2 = Whiten(num_groups=1, - whitening_limit=_whitening_schedule(5.0, ratio=3.0), - prob=(0.025, 0.25), - grad_scale=0.01) + self.whiten2 = Whiten( + num_groups=1, + whitening_limit=_whitening_schedule(5.0, ratio=3.0), + prob=(0.025, 0.25), + grad_scale=0.01, + ) def forward( self, @@ -1940,11 +2087,11 @@ class NonlinAttention(nn.Module): attn_weights: Tensor, ) -> Tensor: """. - Args: - x: a Tensor of shape (seq_len, batch_size, num_channels) -attn_weights: a Tensor of shape (num_heads, batch_size, seq_len, seq_len) - Returns: - a Tensor with the same shape as x + Args: + x: a Tensor of shape (seq_len, batch_size, num_channels) + attn_weights: a Tensor of shape (num_heads, batch_size, seq_len, seq_len) + Returns: + a Tensor with the same shape as x """ x = self.in_proj(x) @@ -2014,13 +2161,21 @@ attn_weights: a Tensor of shape (num_heads, batch_size, seq_len, seq_len) (seq_len, batch_size, embed_dim) = x.shape num_heads = attn_weights.shape[0] - assert attn_weights.shape == (num_heads, batch_size, seq_len, left_context_len + seq_len) + assert attn_weights.shape == ( + num_heads, + batch_size, + seq_len, + left_context_len + seq_len, + ) x = x.reshape(seq_len, batch_size, num_heads, -1).permute(2, 1, 0, 3) # now x: (num_heads, batch_size, seq_len, head_dim) # Pad cached tensor - assert cached_x.shape[2] == left_context_len, (cached_x.shape[2], left_context_len) + assert cached_x.shape[2] == left_context_len, ( + cached_x.shape[2], + left_context_len, + ) x_pad = torch.cat([cached_x, x], dim=2) # Update cached tensor cached_x = x_pad[:, :, -left_context_len:, :] @@ -2045,8 +2200,12 @@ class ConvolutionModule(nn.Module): bias (bool): Whether to use bias in conv layers (default=True). """ + def __init__( - self, channels: int, kernel_size: int, causal: bool, + self, + channels: int, + kernel_size: int, + causal: bool, ) -> None: """Construct a ConvolutionModule object.""" super(ConvolutionModule, self).__init__() @@ -2057,7 +2216,8 @@ class ConvolutionModule(nn.Module): self.causal = causal self.in_proj = nn.Linear( - channels, 2 * bottleneck_dim, + channels, + 2 * bottleneck_dim, ) # the gradients on in_proj are a little noisy, likely to do with the # sigmoid in glu. @@ -2076,7 +2236,8 @@ class ConvolutionModule(nn.Module): # it will be in a better position to start learning something, i.e. to latch onto # the correct range. self.balancer1 = Balancer( - bottleneck_dim, channel_dim=-1, + bottleneck_dim, + channel_dim=-1, min_positive=ScheduledFloat((0.0, 0.05), (8000.0, 0.025)), max_positive=1.0, min_abs=1.5, @@ -2091,31 +2252,40 @@ class ConvolutionModule(nn.Module): assert kernel_size % 2 == 1 - self.depthwise_conv = ChunkCausalDepthwiseConv1d( - channels=bottleneck_dim, - kernel_size=kernel_size) if causal else nn.Conv1d( - in_channels=bottleneck_dim, - out_channels=bottleneck_dim, - groups=bottleneck_dim, - kernel_size=kernel_size, - padding=kernel_size // 2) + self.depthwise_conv = ( + ChunkCausalDepthwiseConv1d(channels=bottleneck_dim, kernel_size=kernel_size) + if causal + else nn.Conv1d( + in_channels=bottleneck_dim, + out_channels=bottleneck_dim, + groups=bottleneck_dim, + kernel_size=kernel_size, + padding=kernel_size // 2, + ) + ) self.balancer2 = Balancer( - bottleneck_dim, channel_dim=1, + bottleneck_dim, + channel_dim=1, min_positive=ScheduledFloat((0.0, 0.1), (8000.0, 0.05)), max_positive=1.0, min_abs=ScheduledFloat((0.0, 0.2), (20000.0, 0.5)), max_abs=10.0, ) - self.whiten = Whiten(num_groups=1, - whitening_limit=_whitening_schedule(7.5), - prob=(0.025, 0.25), - grad_scale=0.01) + self.whiten = Whiten( + num_groups=1, + whitening_limit=_whitening_schedule(7.5), + prob=(0.025, 0.25), + grad_scale=0.01, + ) self.out_proj = ActivationDropoutAndLinear( - bottleneck_dim, channels, activation='SwooshR', - dropout_p=0.0, initial_scale=0.05, + bottleneck_dim, + channels, + activation="SwooshR", + dropout_p=0.0, + initial_scale=0.05, ) def forward( @@ -2153,9 +2323,15 @@ class ConvolutionModule(nn.Module): if src_key_padding_mask is not None: x = x.masked_fill(src_key_padding_mask.unsqueeze(1).expand_as(x), 0.0) - if not torch.jit.is_scripting() and not torch.jit.is_tracing() and chunk_size >= 0: + if ( + not torch.jit.is_scripting() + and not torch.jit.is_tracing() + and chunk_size >= 0 + ): # Not support exporting a model for simulated streaming decoding - assert self.causal, "Must initialize model with causal=True if you use chunk_size" + assert ( + self.causal + ), "Must initialize model with causal=True if you use chunk_size" x = self.depthwise_conv(x, chunk_size=chunk_size) else: x = self.depthwise_conv(x) @@ -2225,10 +2401,12 @@ def _test_zipformer_main(causal: bool = False): # Just make sure the forward pass runs. c = Zipformer2( - encoder_dim=(64, 96), encoder_unmasked_dim=(48, 64), num_heads=(4, 4), + encoder_dim=(64, 96), + encoder_unmasked_dim=(48, 64), + num_heads=(4, 4), causal=causal, chunk_size=(4,) if causal else (-1,), - left_context_frames=(64,) + left_context_frames=(64,), ) batch_size = 5 seq_len = 20 diff --git a/egs/multi_zh-hans/ASR/README.md b/egs/multi_zh-hans/ASR/README.md new file mode 100644 index 000000000..537816a5d --- /dev/null +++ b/egs/multi_zh-hans/ASR/README.md @@ -0,0 +1,39 @@ + +# Introduction + +This recipe includes scripts for training Zipformer model using multiple Chinese datasets. + +# Included Training Sets +1. THCHS-30 +2. AiShell-{1,2,4} +3. ST-CMDS +4. Primewords +5. MagicData +6. Aidatatang_200zh +7. AliMeeting +8. WeNetSpeech +9. KeSpeech-ASR + +|Datset| Number of hours| URL| +|---|---:|---| +|**TOTAL**|14,106|---| +|THCHS-30|35|https://www.openslr.org/18/| +|AiShell-1|170|https://www.openslr.org/33/| +|AiShell-2|1,000|http://www.aishelltech.com/aishell_2| +|AiShell-4|120|https://www.openslr.org/111/| +|ST-CMDS|110|https://www.openslr.org/38/| +|Primewords|99|https://www.openslr.org/47/| +|aidatatang_200zh|200|https://www.openslr.org/62/| +|MagicData|755|https://www.openslr.org/68/| +|AliMeeting|100|https://openslr.org/119/| +|WeNetSpeech|10,000|https://github.com/wenet-e2e/WenetSpeech| +|KeSpeech|1,542|https://github.com/KeSpeech/KeSpeech| + + +# Included Test Sets +1. Aishell-{1,2,4} +2. Aidatatang_200zh +3. AliMeeting +4. MagicData +5. KeSpeech-ASR +6. WeNetSpeech \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/RESULTS.md b/egs/multi_zh-hans/ASR/RESULTS.md new file mode 100644 index 000000000..31fbd9700 --- /dev/null +++ b/egs/multi_zh-hans/ASR/RESULTS.md @@ -0,0 +1,38 @@ +## Results + +### Multi Chinese datasets char-based training results (Non-streaming) on zipformer model + +This is the [pull request #1238](https://github.com/k2-fsa/icefall/pull/1238) in icefall. + +#### Non-streaming + +Best results (num of params : ~69M): + +The training command: + +``` +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 20 \ + --use-fp16 1 \ + --max-duration 600 \ + --num-workers 8 +``` + +The decoding command: + +``` +./zipformer/decode.py \ + --epoch 20 \ + --avg 1 +``` + +Character Error Rates (CERs) listed below are produced by the checkpoint of the 20th epoch using greedy search and BPE model ( # tokens is 2000, byte fallback enabled). + +| Datasets | aidatatang _200zh | aidatatang _200zh | alimeeting | alimeeting | aishell-1 | aishell-1 | aishell-2 | aishell-2 | aishell-4 | magicdata | magicdata | kespeech-asr | kespeech-asr | kespeech-asr | WenetSpeech | WenetSpeech | WenetSpeech | +|--------------------------------|------------------------------|-------------|-------------------|--------------|----------------|-------------|------------------|-------------|------------------|------------------|-------------|-----------------------|-----------------------|-------------|--------------------|-------------------------|---------------------| +| Zipformer CER (%) | dev | test | eval | test | dev | test | dev | test | test | dev | test | dev phase1 | dev phase2 | test | dev | test meeting | test net | +| | 3.2 | 3.67 | 23.15 | 24.78 | 2.91 | 3.04 | 3.59 | 4.03 | 15.68 | 3.68 | 3.12 | 6.69 | 3.19 | 8.01 | 9.32 | 7.05 | 8.78 | + + +The pre-trained model is available here : https://huggingface.co/zrjin/icefall-asr-multi-zh-hans-zipformer-2023-9-2 diff --git a/egs/multi_zh-hans/ASR/local/bpe_model_to_tokens.py b/egs/multi_zh-hans/ASR/local/bpe_model_to_tokens.py new file mode 100755 index 000000000..d078e5b98 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/bpe_model_to_tokens.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 + +""" +This script takes `bpe.model` as input and generates a file `tokens.txt` +from it. + +Usage: +./bpe_model_to_tokens.py /path/to/input/bpe.model > tokens.txt +""" +import argparse + +import sentencepiece as spm + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "bpe_model", + type=str, + help="Path to the input bpe.model", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + + sp = spm.SentencePieceProcessor() + sp.load(args.bpe_model) + + for i in range(sp.vocab_size()): + print(sp.id_to_piece(i), i) + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/local/compile_lg.py b/egs/multi_zh-hans/ASR/local/compile_lg.py new file mode 120000 index 000000000..462d6d3fb --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/compile_lg.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/compile_lg.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_dev_test.py b/egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_dev_test.py new file mode 100755 index 000000000..2581ee42f --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_dev_test.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# Copyright 2021 Johns Hopkins University (Piotr Żelasko) +# Copyright 2021 Xiaomi Corp. (Fangjun Kuang) +# Copyright 2023 Xiaomi Corp. (Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 +from pathlib import Path + +import torch +from lhotse import CutSet, KaldifeatFbank, KaldifeatFbankConfig, LilcomChunkyWriter + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def compute_fbank_kespeech_dev_test(): + in_out_dir = Path("data/fbank/kespeech") + # number of workers in dataloader + num_workers = 42 + + # number of seconds in a batch + batch_duration = 600 + + subsets = ( + "dev_phase1", + "dev_phase2", + "test", + ) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + extractor = KaldifeatFbank(KaldifeatFbankConfig(device=device)) + + logging.info(f"device: {device}") + + for partition in subsets: + cuts_path = in_out_dir / f"kespeech-asr_cuts_{partition}.jsonl.gz" + if cuts_path.is_file(): + logging.info(f"{cuts_path} exists - skipping") + continue + + raw_cuts_path = in_out_dir / f"kespeech-asr_cuts_{partition}_raw.jsonl.gz" + + logging.info(f"Loading {raw_cuts_path}") + cut_set = CutSet.from_file(raw_cuts_path) + + logging.info("Splitting cuts into smaller chunks") + cut_set = cut_set.trim_to_supervisions( + keep_overlapping=False, min_duration=None + ) + + logging.info("Computing features") + cut_set = cut_set.compute_and_store_features_batch( + extractor=extractor, + storage_path=f"{in_out_dir}/feats_{partition}", + num_workers=num_workers, + batch_duration=batch_duration, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + logging.info(f"Saving to {cuts_path}") + cut_set.to_file(cuts_path) + + +def main(): + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + logging.basicConfig(format=formatter, level=logging.INFO) + + compute_fbank_kespeech_dev_test() + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_splits.py b/egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_splits.py new file mode 100755 index 000000000..8bfbc7b50 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/compute_fbank_kespeech_splits.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# Copyright 2021 Johns Hopkins University (Piotr Żelasko) +# Copyright 2021 Xiaomi Corp. (Fangjun Kuang) +# Copyright 2023 Xiaomi Corp. (Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 +from datetime import datetime +from pathlib import Path + +import torch +from lhotse import ( + CutSet, + KaldifeatFbank, + KaldifeatFbankConfig, + LilcomChunkyWriter, + set_audio_duration_mismatch_tolerance, + set_caching_enabled, +) + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--training-subset", + type=str, + default="train_phase1", + choices=["train_phase1", "train_phase2"], + help="The training subset for computing fbank feature.", + ) + + parser.add_argument( + "--num-workers", + type=int, + default=20, + help="Number of dataloading workers used for reading the audio.", + ) + + parser.add_argument( + "--batch-duration", + type=float, + default=600.0, + help="The maximum number of audio seconds in a batch." + "Determines batch size dynamically.", + ) + + parser.add_argument( + "--num-splits", + type=int, + required=True, + help="The number of splits of the given subset", + ) + + parser.add_argument( + "--start", + type=int, + default=0, + help="Process pieces starting from this number (inclusive).", + ) + + parser.add_argument( + "--stop", + type=int, + default=-1, + help="Stop processing pieces until this number (exclusive).", + ) + return parser + + +def compute_fbank_kespeech_splits(args): + subset = args.training_subset + subset = str(subset) + num_splits = args.num_splits + output_dir = f"data/fbank/kespeech/{subset}_split_{num_splits}" + output_dir = Path(output_dir) + assert output_dir.exists(), f"{output_dir} does not exist!" + + num_digits = len(str(num_splits)) + + start = args.start + stop = args.stop + if stop < start: + stop = num_splits + + stop = min(stop, num_splits) + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + extractor = KaldifeatFbank(KaldifeatFbankConfig(device=device)) + logging.info(f"device: {device}") + + set_audio_duration_mismatch_tolerance(0.01) # 10ms tolerance + set_caching_enabled(False) + for i in range(start, stop): + idx = f"{i + 1}".zfill(num_digits) + logging.info(f"Processing {idx}/{num_splits}") + + cuts_path = output_dir / f"kespeech-asr_cuts_{subset}.{idx}.jsonl.gz" + if cuts_path.is_file(): + logging.info(f"{cuts_path} exists - skipping") + continue + + raw_cuts_path = output_dir / f"kespeech-asr_cuts_{subset}_raw.{idx}.jsonl.gz" + + logging.info(f"Loading {raw_cuts_path}") + cut_set = CutSet.from_file(raw_cuts_path) + + logging.info("Splitting cuts into smaller chunks.") + cut_set = cut_set.trim_to_supervisions( + keep_overlapping=False, min_duration=None + ) + + logging.info("Computing features") + cut_set = cut_set.compute_and_store_features_batch( + extractor=extractor, + storage_path=f"{output_dir}/feats_{subset}_{idx}", + num_workers=args.num_workers, + batch_duration=args.batch_duration, + storage_type=LilcomChunkyWriter, + overwrite=True, + ) + + logging.info(f"Saving to {cuts_path}") + cut_set.to_file(cuts_path) + + +def main(): + now = datetime.now() + date_time = now.strftime("%Y-%m-%d-%H-%M-%S") + + log_filename = "log-compute_fbank_kespeech_splits" + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + log_filename = f"{log_filename}-{date_time}" + + logging.basicConfig( + filename=log_filename, + format=formatter, + level=logging.INFO, + filemode="w", + ) + + console = logging.StreamHandler() + console.setLevel(logging.INFO) + console.setFormatter(logging.Formatter(formatter)) + logging.getLogger("").addHandler(console) + + parser = get_parser() + args = parser.parse_args() + logging.info(vars(args)) + + compute_fbank_kespeech_splits(args) + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/local/compute_fbank_magicdata.py b/egs/multi_zh-hans/ASR/local/compute_fbank_magicdata.py new file mode 100755 index 000000000..5649d3815 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/compute_fbank_magicdata.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang +# Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the MagicData dataset. +It looks for manifests in the directory data/manifests/magicdata. + +The generated fbank features are saved in data/fbank. +""" + +import argparse +import logging +import os +from pathlib import Path + +import torch +from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter +from lhotse.recipes.utils import read_manifests_if_cached + +from icefall.utils import get_executor + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def compute_fbank_magicdata(num_mel_bins: int = 80, speed_perturb: bool = False): + src_dir = Path("data/manifests/magicdata") + output_dir = Path("data/fbank") + num_jobs = min(30, os.cpu_count()) + + dataset_parts = ("train", "test", "dev") + prefix = "magicdata" + suffix = "jsonl.gz" + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + prefix=prefix, + suffix=suffix, + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + + extractor = Fbank(FbankConfig(num_mel_bins=num_mel_bins)) + + with get_executor() as ex: # Initialize the executor only once. + for partition, m in manifests.items(): + if (output_dir / f"{prefix}_cuts_{partition}.{suffix}").is_file(): + logging.info(f"{partition} already exists - skipping.") + continue + logging.info(f"Processing {partition}") + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ) + if "train" in partition and speed_perturb: + cut_set = ( + cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) + ) + cut_set = cut_set.compute_and_store_features( + extractor=extractor, + storage_path=f"{output_dir}/{prefix}_feats_{partition}", + # when an executor is specified, make more partitions + num_jobs=num_jobs if ex is None else 80, + executor=ex, + storage_type=LilcomChunkyWriter, + ) + cut_set.to_file(output_dir / f"{prefix}_cuts_{partition}.{suffix}") + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--num-mel-bins", + type=int, + default=80, + help="""The number of mel bins for Fbank""", + ) + parser.add_argument( + "--speed-perturb", + type=bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + args = get_args() + compute_fbank_magicdata( + num_mel_bins=args.num_mel_bins, speed_perturb=args.speed_perturb + ) diff --git a/egs/multi_zh-hans/ASR/local/compute_fbank_primewords.py b/egs/multi_zh-hans/ASR/local/compute_fbank_primewords.py new file mode 100755 index 000000000..303a16580 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/compute_fbank_primewords.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang +# Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the Primewords dataset. +It looks for manifests in the directory data/manifests/primewords. + +The generated fbank features are saved in data/fbank. +""" + +import argparse +import logging +import os +from pathlib import Path + +import torch +from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter +from lhotse.recipes.utils import read_manifests_if_cached + +from icefall.utils import get_executor + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def compute_fbank_primewords(num_mel_bins: int = 80, speed_perturb: bool = False): + src_dir = Path("data/manifests/primewords") + output_dir = Path("data/fbank") + num_jobs = min(15, os.cpu_count()) + + dataset_parts = ("train",) + prefix = "primewords" + suffix = "jsonl.gz" + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + prefix=prefix, + suffix=suffix, + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + + extractor = Fbank(FbankConfig(num_mel_bins=num_mel_bins)) + + with get_executor() as ex: # Initialize the executor only once. + for partition, m in manifests.items(): + if (output_dir / f"{prefix}_cuts_{partition}.{suffix}").is_file(): + logging.info(f"{partition} already exists - skipping.") + continue + logging.info(f"Processing {partition}") + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ) + if "train" in partition and speed_perturb: + cut_set = ( + cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) + ) + cut_set = cut_set.compute_and_store_features( + extractor=extractor, + storage_path=f"{output_dir}/{prefix}_feats_{partition}", + # when an executor is specified, make more partitions + num_jobs=num_jobs if ex is None else 80, + executor=ex, + storage_type=LilcomChunkyWriter, + ) + cut_set.to_file(output_dir / f"{prefix}_cuts_{partition}.{suffix}") + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--num-mel-bins", + type=int, + default=80, + help="""The number of mel bins for Fbank""", + ) + parser.add_argument( + "--speed-perturb", + type=bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) + + return parser.parse_args() + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + args = get_args() + compute_fbank_primewords( + num_mel_bins=args.num_mel_bins, speed_perturb=args.speed_perturb + ) diff --git a/egs/multi_zh-hans/ASR/local/compute_fbank_stcmds.py b/egs/multi_zh-hans/ASR/local/compute_fbank_stcmds.py new file mode 100755 index 000000000..730806954 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/compute_fbank_stcmds.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang +# Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the ST-CMDS dataset. +It looks for manifests in the directory data/manifests/stcmds. + +The generated fbank features are saved in data/fbank. +""" + +import argparse +import logging +import os +from pathlib import Path + +import torch +from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter +from lhotse.recipes.utils import read_manifests_if_cached + +from icefall.utils import get_executor + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def compute_fbank_stcmds(num_mel_bins: int = 80, speed_perturb: bool = False): + src_dir = Path("data/manifests/stcmds") + output_dir = Path("data/fbank") + num_jobs = min(15, os.cpu_count()) + + dataset_parts = ("train",) + prefix = "stcmds" + suffix = "jsonl.gz" + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + prefix=prefix, + suffix=suffix, + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + + extractor = Fbank(FbankConfig(num_mel_bins=num_mel_bins)) + + with get_executor() as ex: # Initialize the executor only once. + for partition, m in manifests.items(): + if (output_dir / f"{prefix}_cuts_{partition}.{suffix}").is_file(): + logging.info(f"{partition} already exists - skipping.") + continue + logging.info(f"Processing {partition}") + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ) + if "train" in partition and speed_perturb: + cut_set = ( + cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) + ) + cut_set = cut_set.compute_and_store_features( + extractor=extractor, + storage_path=f"{output_dir}/{prefix}_feats_{partition}", + # when an executor is specified, make more partitions + num_jobs=num_jobs if ex is None else 80, + executor=ex, + storage_type=LilcomChunkyWriter, + ) + cut_set.to_file(output_dir / f"{prefix}_cuts_{partition}.{suffix}") + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--num-mel-bins", + type=int, + default=80, + help="""The number of mel bins for Fbank""", + ) + parser.add_argument( + "--speed-perturb", + type=bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) + return parser.parse_args() + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + args = get_args() + compute_fbank_stcmds( + num_mel_bins=args.num_mel_bins, speed_perturb=args.speed_perturb + ) diff --git a/egs/multi_zh-hans/ASR/local/compute_fbank_thchs30.py b/egs/multi_zh-hans/ASR/local/compute_fbank_thchs30.py new file mode 100755 index 000000000..58bb8002a --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/compute_fbank_thchs30.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang +# Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the THCHS-30 dataset. +It looks for manifests in the directory data/manifests/thchs30. + +The generated fbank features are saved in data/fbank. +""" + +import argparse +import logging +import os +from pathlib import Path + +import torch +from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter +from lhotse.recipes.utils import read_manifests_if_cached + +from icefall.utils import get_executor + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def compute_fbank_thchs30(num_mel_bins: int = 80, speed_perturb: bool = False): + src_dir = Path("data/manifests/thchs30") + output_dir = Path("data/fbank") + num_jobs = min(15, os.cpu_count()) + + dataset_parts = ( + "train", + "dev", + "test", + ) + prefix = "thchs_30" + suffix = "jsonl.gz" + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + prefix=prefix, + suffix=suffix, + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + + extractor = Fbank(FbankConfig(num_mel_bins=num_mel_bins)) + + with get_executor() as ex: # Initialize the executor only once. + for partition, m in manifests.items(): + if (output_dir / f"{prefix}_cuts_{partition}.{suffix}").is_file(): + logging.info(f"{partition} already exists - skipping.") + continue + logging.info(f"Processing {partition}") + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ) + if "train" in partition: + cut_set = ( + (cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1)) + if speed_perturb + else cut_set + ) + cut_set = cut_set.compute_and_store_features( + extractor=extractor, + storage_path=f"{output_dir}/{prefix}_feats_{partition}", + # when an executor is specified, make more partitions + num_jobs=num_jobs if ex is None else 80, + executor=ex, + storage_type=LilcomChunkyWriter, + ) + cut_set.to_file(output_dir / f"{prefix}_cuts_{partition}.{suffix}") + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--num-mel-bins", + type=int, + default=80, + help="""The number of mel bins for Fbank""", + ) + parser.add_argument( + "--speed-perturb", + type=bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) + return parser.parse_args() + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + args = get_args() + compute_fbank_thchs30( + num_mel_bins=args.num_mel_bins, speed_perturb=args.speed_perturb + ) diff --git a/egs/multi_zh-hans/ASR/local/prepare_char.py b/egs/multi_zh-hans/ASR/local/prepare_char.py new file mode 120000 index 000000000..be7da61af --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/prepare_char.py @@ -0,0 +1 @@ +../../../wenetspeech/ASR/local/prepare_char.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/local/prepare_for_bpe_model.py b/egs/multi_zh-hans/ASR/local/prepare_for_bpe_model.py new file mode 100755 index 000000000..020800c15 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/prepare_for_bpe_model.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script tokenizes the training transcript by CJK characters +# and saves the result to transcript_chars.txt, which is used +# to train the BPE model later. + +import argparse +from pathlib import Path + +from tqdm.auto import tqdm + +from icefall.utils import tokenize_by_CJK_char + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--lang-dir", + type=str, + help="""Output directory. + The generated transcript_chars.txt is saved to this directory. + """, + ) + + parser.add_argument( + "--text", + type=str, + help="WenetSpeech training transcript.", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + lang_dir = Path(args.lang_dir) + text = Path(args.text) + + assert lang_dir.exists() and text.exists(), f"{lang_dir} or {text} does not exist!" + + transcript_path = lang_dir / "transcript_chars.txt" + + with open(text, "r", encoding="utf-8") as fin: + with open(transcript_path, "w+", encoding="utf-8") as fout: + for line in fin: + fout.write(tokenize_by_CJK_char(line) + "\n") + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/local/prepare_lang.py b/egs/multi_zh-hans/ASR/local/prepare_lang.py new file mode 120000 index 000000000..747f2ab39 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/prepare_lang.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/prepare_lang.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/local/prepare_lang_bpe.py b/egs/multi_zh-hans/ASR/local/prepare_lang_bpe.py new file mode 120000 index 000000000..36b40e7fc --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/prepare_lang_bpe.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/prepare_lang_bpe.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/local/preprocess_kespeech.py b/egs/multi_zh-hans/ASR/local/preprocess_kespeech.py new file mode 100755 index 000000000..20274263f --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/preprocess_kespeech.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +# Copyright 2021 Johns Hopkins University (Piotr Żelasko) +# Copyright 2021 Xiaomi Corp. (Fangjun Kuang) +# Copyright 2023 Xiaomi Corp. (Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 re +from pathlib import Path + +from lhotse import CutSet, SupervisionSegment +from lhotse.recipes.utils import read_manifests_if_cached + +from icefall import setup_logger + +# Similar text filtering and normalization procedure as in: +# https://github.com/SpeechColab/WenetSpeech/blob/main/toolkits/kaldi/wenetspeech_data_prep.sh + + +def normalize_text( + utt: str, + punct_pattern=re.compile(r"<(PERIOD|QUESTIONMARK|EXCLAMATIONPOINT)>"), + whitespace_pattern=re.compile(r"\s\s+"), +) -> str: + return whitespace_pattern.sub(" ", punct_pattern.sub("", utt)) + + +def has_no_oov( + sup: SupervisionSegment, + oov_pattern=re.compile(r"<(SIL|MUSIC|NOISE|OTHER|SPOKEN_NOISE)>"), +) -> bool: + return oov_pattern.search(sup.text) is None + + +def preprocess_kespeech(speed_perturb: bool = False): + src_dir = Path("data/manifests/kespeech") + output_dir = Path("data/fbank/kespeech") + output_dir.mkdir(exist_ok=True) + + # Note: By default, we preprocess all sub-parts. + # You can delete those that you don't need. + # For instance, if you don't want to use the test subpart, just remove + # the line below containing "test" + dataset_parts = ( + "dev_phase1", + "dev_phase2", + "test", + "train_phase1", + "train_phase2", + ) + + logging.info("Loading manifest (may take 10 minutes)") + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + suffix="jsonl.gz", + prefix="kespeech-asr", + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + logging_threshold = 50 + logging_count = 0 + + for partition, m in manifests.items(): + logging.info(f"Processing {partition}") + raw_cuts_path = output_dir / f"kespeech-asr_cuts_{partition}_raw.jsonl.gz" + if raw_cuts_path.is_file(): + logging.info(f"{partition} already exists - skipping") + continue + + # Note this step makes the recipe different than LibriSpeech: + # We must filter out some utterances and remove punctuation + # to be consistent with Kaldi. + logging.info("Filtering OOV utterances from supervisions") + m["supervisions"] = m["supervisions"].filter(has_no_oov) + logging.info(f"Normalizing text in {partition}") + for sup in m["supervisions"]: + orig_text = sup.text + sup.text = normalize_text(sup.text) + if logging_count < logging_threshold and len(orig_text) != len(sup.text): + logging_count += 1 + logging.info( + f"\nOriginal text vs normalized text:\n{orig_text}\n{sup.text}" + ) + + # Create long-recording cut manifests. + logging.info(f"Processing {partition}") + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ) + # Run data augmentation that needs to be done in the + # time domain. + if partition not in [ + "dev_phase1", + "dev_phase2", + "test", + ]: + if speed_perturb: + logging.info( + f"Speed perturb for {partition} with factors 0.9 and 1.1 " + "(Perturbing may take 8 minutes and saving may take 20 minutes)" + ) + cut_set = ( + cut_set + cut_set.perturb_speed(0.9) + cut_set.perturb_speed(1.1) + ) + logging.info(f"Saving to {raw_cuts_path}") + cut_set.to_file(raw_cuts_path) + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--speed-perturb", + type=bool, + default=False, + help="Enable 0.9 and 1.1 speed perturbation for data augmentation. Default: False.", + ) + return parser.parse_args() + + +def main(): + setup_logger(log_filename="./log-preprocess-kespeech") + + args = get_args() + preprocess_kespeech(speed_perturb=args.speed_perturb) + logging.info("Done") + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/local/text2token.py b/egs/multi_zh-hans/ASR/local/text2token.py new file mode 120000 index 000000000..ce5cfd537 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/text2token.py @@ -0,0 +1 @@ +../../../wenetspeech/ASR/local/text2token.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/local/train_bpe_model.py b/egs/multi_zh-hans/ASR/local/train_bpe_model.py new file mode 100755 index 000000000..976ea0ba8 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/train_bpe_model.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# Copyright 2023 Xiaomi Corp. (authors: Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + + +# You can install sentencepiece via: +# +# pip install sentencepiece +# +# Due to an issue reported in +# https://github.com/google/sentencepiece/pull/642#issuecomment-857972030 +# +# Please install a version >=0.1.96 + +import argparse +import shutil +from pathlib import Path + +import sentencepiece as spm + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--lang-dir", + type=str, + help="""Input and output directory. + The generated bpe.model is saved to this directory. + """, + ) + + parser.add_argument( + "--transcript", + type=str, + help="Training transcript.", + ) + + parser.add_argument( + "--vocab-size", + type=int, + help="Vocabulary size for BPE training", + ) + + parser.add_argument( + "--byte-fallback", + type=bool, + default=True, + help="Enable byte fallback for BPE model.", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + vocab_size = args.vocab_size + lang_dir = Path(args.lang_dir) + + model_type = "unigram" + + model_prefix = f"{lang_dir}/{model_type}_{vocab_size}" + train_text = args.transcript + character_coverage = 0.98 + input_sentence_size = 100000000 + + user_defined_symbols = ["", ""] + unk_id = len(user_defined_symbols) + # Note: unk_id is fixed to 2. + # If you change it, you should also change other + # places that are using it. + + model_file = Path(model_prefix + ".model") + if not model_file.is_file(): + spm.SentencePieceTrainer.train( + input=train_text, + vocab_size=vocab_size, + model_type=model_type, + model_prefix=model_prefix, + input_sentence_size=input_sentence_size, + character_coverage=character_coverage, + user_defined_symbols=user_defined_symbols, + unk_id=unk_id, + bos_id=-1, + eos_id=-1, + byte_fallback=args.byte_fallback, + ) + else: + print(f"{model_file} exists - skipping") + return + + shutil.copyfile(model_file, f"{lang_dir}/bpe.model") + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/local/validate_bpe_lexicon.py b/egs/multi_zh-hans/ASR/local/validate_bpe_lexicon.py new file mode 120000 index 000000000..721bb48e7 --- /dev/null +++ b/egs/multi_zh-hans/ASR/local/validate_bpe_lexicon.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/validate_bpe_lexicon.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/prepare.sh b/egs/multi_zh-hans/ASR/prepare.sh new file mode 100755 index 000000000..5d0fe66a4 --- /dev/null +++ b/egs/multi_zh-hans/ASR/prepare.sh @@ -0,0 +1,373 @@ +#!/usr/bin/env bash + +# fix segmentation fault reported in https://github.com/k2-fsa/icefall/issues/674 +export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python + +set -eou pipefail + +stage=-1 +stop_stage=100 +num_splits=100 + +dl_dir=$PWD/download + +. shared/parse_options.sh || exit 1 + +vocab_sizes=( + 2000 +) + +# All files generated by this script are saved in "data". +# You can safely remove "data" and rerun this script to regenerate it. +mkdir -p data + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +log "dl_dir: $dl_dir" + +log "Dataset: musan" +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + log "Stage 1: Soft link fbank of musan" + mkdir -p data/fbank + if [ -e ../../librispeech/ASR/data/fbank/.musan.done ]; then + cd data/fbank + ln -svf $(realpath ../../../../librispeech/ASR/data/fbank/musan_feats) . + ln -svf $(realpath ../../../../librispeech/ASR/data/fbank/musan_cuts.jsonl.gz) . + cd ../.. + else + log "Abort! Please run ../../librispeech/ASR/prepare.sh --stage 4 --stop-stage 4" + exit 1 + fi +fi + +log "Dataset: THCHS-30" +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + log "Stage 2: Prepare THCHS-30" + if [ ! -d $dl_dir/thchs30 ]; then + log "Downloading THCHS-30" + lhotse download thchs30 $dl_dir/thchs30 + fi + + if [ ! -f data/manifests/.thchs30.done ]; then + mkdir -p data/manifests + lhotse prepare thchs-30 $dl_dir/thchs30 data/manifests/thchs30 + touch data/manifests/.thchs30.done + fi + + if [ ! -f data/fbank/.thchs30.done ]; then + mkdir -p data/fbank + ./local/compute_fbank_thchs30.py + touch data/fbank/.thchs30.done + fi +fi + +log "Dataset: AISHELL-1" +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + log "Stage 3: Prepare AISHELL-1" + if [ -e ../../aishell/ASR/data/fbank/.aishell.done ]; then + cd data/fbank + ln -svf $(realpath ../../../../aishell/ASR/data/fbank/aishell_feats_train) . + ln -svf $(realpath ../../../../aishell/ASR/data/fbank/aishell_feats_dev) . + ln -svf $(realpath ../../../../aishell/ASR/data/fbank/aishell_feats_test) . + ln -svf $(realpath ../../../../aishell/ASR/data/fbank/aishell_cuts_train.jsonl.gz) . + ln -svf $(realpath ../../../../aishell/ASR/data/fbank/aishell_cuts_dev.jsonl.gz) . + ln -svf $(realpath ../../../../aishell/ASR/data/fbank/aishell_cuts_test.jsonl.gz) . + cd ../.. + else + log "Abort! Please run ../../aishell/ASR/prepare.sh --stage 3 --stop-stage 3" + exit 1 + fi +fi + +log "Dataset: AISHELL-2" +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + log "Stage 4: Prepare AISHELL-2" + if [ -e ../../aishell/ASR/data/fbank/.aishell2.done ]; then + cd data/fbank + ln -svf $(realpath ../../../../aishell2/ASR/data/fbank/aishell2_feats_train) . + ln -svf $(realpath ../../../../aishell2/ASR/data/fbank/aishell2_feats_dev) . + ln -svf $(realpath ../../../../aishell2/ASR/data/fbank/aishell2_feats_test) . + ln -svf $(realpath ../../../../aishell2/ASR/data/fbank/aishell2_cuts_train.jsonl.gz) . + ln -svf $(realpath ../../../../aishell2/ASR/data/fbank/aishell2_cuts_dev.jsonl.gz) . + ln -svf $(realpath ../../../../aishell2/ASR/data/fbank/aishell2_cuts_test.jsonl.gz) . + cd ../.. + else + log "Abort! Please run ../../aishell2/ASR/prepare.sh --stage 3 --stop-stage 3" + exit 1 + fi +fi + +log "Dataset: AISHELL-4" +if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then + log "Stage 5: Prepare AISHELL-4" + if [ -e ../../aishell/ASR/data/fbank/.aishell4.done ]; then + cd data/fbank + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_feats_train) . + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_feats_dev) . + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_feats_test) . + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_cuts_train_L.jsonl.gz) . + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_cuts_train_M.jsonl.gz) . + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_cuts_train_S.jsonl.gz) . + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_cuts_dev.jsonl.gz) . + ln -svf $(realpath ../../../../aishell4/ASR/data/fbank/aishell4_cuts_test.jsonl.gz) . + cd ../.. + else + log "Abort! Please run ../../aishell4/ASR/prepare.sh --stage 3 --stop-stage 3" + exit 1 + fi +fi + +log "Dataset: ST-CMDS" +if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then + log "Stage 6: Prepare ST-CMDS" + if [ ! -f $dl_dir/stcmds/ST-CMDS-20170001_1-OS.tar.gz ]; then + log "Downloading ST-CMDS" + lhotse download stcmds $dl_dir/stcmds + fi + + if [ ! -f data/manifests/.stcmds.done ]; then + mkdir -p data/manifests + lhotse prepare stcmds $dl_dir/stcmds data/manifests/stcmds + touch data/manifests/.stcmds.done + fi + + if [ ! -f data/fbank/.stcmds.done ]; then + mkdir -p data/fbank + ./local/compute_fbank_stcmds.py + touch data/fbank/.stcmds.done + fi +fi + + +log "Dataset: Primewords" +if [ $stage -le 7 ] && [ $stop_stage -ge 7 ]; then + log "Stage 7: Prepare Primewords" + if [ ! -f $dl_dir/primewords/primewords_md_2018_set1.tar.gz ]; then + log "Downloading Primewords" + lhotse download primewords $dl_dir/primewords + fi + + if [ ! -f data/manifests/.stcmds.done ]; then + mkdir -p data/manifests + lhotse prepare stcmds $dl_dir/primewords data/manifests/primewords + touch data/manifests/.primewords.done + fi + + if [ ! -f data/fbank/.primewords.done ]; then + mkdir -p data/fbank + ./local/compute_fbank_primewords.py + touch data/fbank/.primewords.done + fi +fi + +log "Dataset: MagicData" +if [ $stage -le 8 ] && [ $stop_stage -ge 8 ]; then + log "Stage 8: Prepare MagicData" + if [ ! -f $dl_dir/magicdata/train_set.tar.gz ]; then + log "Downloading MagicData" + lhotse download magicdata $dl_dir/magicdata + fi + + if [ ! -f data/manifests/.magicdata.done ]; then + mkdir -p data/manifests + lhotse prepare magicdata $dl_dir/magicdata data/manifests/magicdata + touch data/manifests/.magicdata.done + fi + + if [ ! -f data/fbank/.magicdata.done ]; then + mkdir -p data/fbank + ./local/compute_fbank_magicdata.py + touch data/fbank/.magicdata.done + fi +fi + +log "Dataset: aidatatang_200zh" +if [ $stage -le 9 ] && [ $stop_stage -ge 9 ]; then + log "Stage 9: Prepare aidatatang_200zh" + if [ -e ../../aidatatang_200zh/ASR/data/fbank/.aidatatang_200zh.done ]; then + cd data/fbank + ln -svf $(realpath ../../../../aidatatang_200zh/ASR/data/fbank/aidatatang_feats_train) . + ln -svf $(realpath ../../../../aidatatang_200zh/ASR/data/fbank/aidatatang_feats_dev) . + ln -svf $(realpath ../../../../aidatatang_200zh/ASR/data/fbank/aidatatang_feats_test) . + ln -svf $(realpath ../../../../aidatatang_200zh/ASR/data/fbank/aidatatang_cuts_train.jsonl.gz) . + ln -svf $(realpath ../../../../aidatatang_200zh/ASR/data/fbank/aidatatang_cuts_dev.jsonl.gz) . + ln -svf $(realpath ../../../../aidatatang_200zh/ASR/data/fbank/aidatatang_cuts_test.jsonl.gz) . + cd ../.. + else + log "Abort! Please run ../../aidatatang_200zh/ASR/prepare.sh --stage 4 --stop-stage 4" + exit 1 + fi +fi + +log "Dataset: Ali-Meeting" +if [ $stage -le 10 ] && [ $stop_stage -ge 10 ]; then + log "Stage 10: Prepare Ali-Meeting" + if [ -e ../../alimeeting/ASR/data/fbank/.fbank.done ]; then + cd data/fbank + ln -svf $(realpath ../../../../alimeeting/ASR/data/fbank/alimeeting-far_feats_train) . + ln -svf $(realpath ../../../../alimeeting/ASR/data/fbank/alimeeting-far_feats_eval) . + ln -svf $(realpath ../../../../alimeeting/ASR/data/fbank/alimeeting-far_feats_test) . + ln -svf $(realpath ../../../../alimeeting/ASR/data/fbank/alimeeting-far_cuts_train.jsonl.gz) . + ln -svf $(realpath ../../../../alimeeting/ASR/data/fbank/alimeeting-far_cuts_eval.jsonl.gz) . + ln -svf $(realpath ../../../../alimeeting/ASR/data/fbank/alimeeting-far_cuts_test.jsonl.gz) . + cd ../.. + else + log "Abort! Please run ../../alimeeting/ASR/prepare.sh --stage 5 --stop-stage 5" + exit 1 + fi +fi + +log "Dataset: WenetSpeech" +if [ $stage -le 11 ] && [ $stop_stage -ge 11 ]; then + log "Stage 11: Prepare WenetSpeech" + if [ -e ../../wenetspeech/ASR/data/fbank/.preprocess_complete ]; then + cd data/fbank + ln -svf $(realpath ../../../../wenetspeech/ASR/data/fbank/cuts_DEV.jsonl.gz) . + ln -svf $(realpath ../../../../wenetspeech/ASR/data/fbank/cuts_L.jsonl.gz) . + ln -svf $(realpath ../../../../wenetspeech/ASR/data/fbank/cuts_TEST_MEETING.jsonl.gz) . + ln -svf $(realpath ../../../../wenetspeech/ASR/data/fbank/cuts_TEST_NET.jsonl.gz) . + + ln -svf $(realpath ../../../../wenetspeech/ASR/data/fbank/L_split_1000) . + ln -svf $(realpath ../../../../wenetspeech/ASR/data/fbank/*.lca) . + ln -svf $(realpath ../../../../wenetspeech/ASR/data/fbank/) ./wenetspeech + cd ../.. + else + log "Abort! Please run ../../wenetspeech/ASR/prepare.sh" + exit 1 + fi + + if [ -d ../../wenetspeech/ASR/data/lang_char/ ]; then + cd data + cp -r ../../../../wenetspeech/ASR/data/lang_char . + cd .. + else + log "Abort! Please run ../../wenetspeech/ASR/prepare.sh" + exit 1 + fi +fi + +log "Dataset: KeSpeech" +if [ $stage -le 12 ] && [ $stop_stage -ge 12 ]; then + log "Stage 12: Prepare KeSpeech" + if [ ! -d $dl_dir/KeSpeech ]; then + log "Abort! Please download KeSpeech first." + log "KeSpeech download link: https://github.com/KeSpeech/KeSpeech" + exit 1 + fi + + if [ ! -f data/manifests/.kespeech.done ]; then + mkdir -p data/manifests + lhotse prepare kespeech -j 16 $dl_dir/KeSpeech data/manifests/kespeech + touch data/manifests/.kespeech.done + fi + + if [ ! -f data/fbank/.kespeech.done ]; then + mkdir -p data/fbank + + log "Preprocess KeSpeech manifest" + if [ ! -f data/fbank/.kespeech_preprocess_complete ]; then + python3 ./local/preprocess_kespeech.py + touch data/fbank/.kespeech_preprocess_complete + fi + + if [ -f data/fbank/.kespeech.train_phase1.split.${num_splits}.done ]; then + log "Spliting KeSpeech train_phase1" + lhotse split ${num_splits} \ + data/fbank/kespeech/kespeech-asr_cuts_train_phase1_raw.jsonl.gz \ + data/fbank/kespeech/train_phase1_split_${num_splits} + touch data/fbank/.kespeech.train_phase1.split.${num_splits}.done + fi + + if [ -f data/fbank/.kespeech.train_phase2.split.${num_splits}.done ]; then + log "Spliting KeSpeech train_phase2" + lhotse split ${num_splits} \ + data/fbank/kespeech/kespeech-asr_cuts_train_phase2_raw.jsonl.gz \ + data/fbank/kespeech/train_phase2_split_${num_splits} + touch data/fbank/.kespeech.train_phase2.split.${num_splits}.done + fi + + log "Compute KeSpeech fbank for train_phase1" + ./local/compute_fbank_kespeech_splits.py --num-splits ${num_splits} --training-subset train_phase1 + + log "Compute KeSpeech fbank for train_phase2" + ./local/compute_fbank_kespeech_splits.py --num-splits ${num_splits} --training-subset train_phase2 + + log "Compute KeSpeech fbank for test/dev" + ./local/compute_fbank_kespeech_dev_test.py + + touch data/fbank/.kespeech.done + fi +fi + +if [ $stage -le 13 ] && [ $stop_stage -ge 13 ]; then + log "Stage 13: BPE model training (note that we use transcripts of wenetspeech only for BPE training)" + ./local/prepare_for_bpe_model.py --lang-dir ./data/lang_char --text ./data/lang_char/text + + for vocab_size in ${vocab_sizes[@]}; do + lang_dir=data/lang_bpe_${vocab_size} + + mkdir -p $lang_dir + if [ ! -f $lang_dir/bpe.model ]; then + ./local/train_bpe_model.py \ + --lang-dir $lang_dir \ + --transcript ./data/lang_char/transcript_chars.txt \ + --vocab-size $vocab_size + + ./local/bpe_model_to_tokens.py $lang_dir/bpe.model > $lang_dir/tokens.txt + fi + + if [ ! -f $lang_dir/L_disambig.pt ]; then + cp data/lang_char/words.txt $lang_dir + + ./local/prepare_lang_bpe.py --lang-dir $lang_dir + log "Validating $lang_dir/lexicon.txt" + ./local/validate_bpe_lexicon.py \ + --lexicon $lang_dir/lexicon.txt \ + --bpe-model $lang_dir/bpe.model + fi + + if [ ! -f $lang_dir/L.fst ]; then + log "Converting L.pt to L.fst" + ./shared/convert-k2-to-openfst.py \ + --olabels aux_labels \ + $lang_dir/L.pt \ + $lang_dir/L.fst + fi + + if [ ! -f $lang_dir/L_disambig.fst ]; then + log "Converting L_disambig.pt to L_disambig.fst" + ./shared/convert-k2-to-openfst.py \ + --olabels aux_labels \ + $lang_dir/L_disambig.pt \ + $lang_dir/L_disambig.fst + fi + done +fi + +if [ $stage -le 14 ] && [ $stop_stage -ge 14 ]; then + log "Stage 14: Prepare G (note that we use ngram lm of wenetspeech only for G preparation)" + + if [ -d ../../wenetspeech/ASR/data/lang_char/ ]; then + cd data + ln -s ../../../../wenetspeech/ASR/data/lm . + cd .. + else + log "Abort! Please run ../../wenetspeech/ASR/prepare.sh" + exit 1 + fi +fi + +if [ $stage -le 15 ] && [ $stop_stage -ge 15 ]; then + log "Stage 15: Compile LG" + for vocab_size in ${vocab_sizes[@]}; do + lang_dir=data/lang_bpe_${vocab_size} + + python ./local/compile_lg.py --lang-dir $lang_dir + done +fi + + diff --git a/egs/multi_zh-hans/ASR/shared b/egs/multi_zh-hans/ASR/shared new file mode 120000 index 000000000..4cbd91a7e --- /dev/null +++ b/egs/multi_zh-hans/ASR/shared @@ -0,0 +1 @@ +../../../icefall/shared \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py b/egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py new file mode 100644 index 000000000..b1b7bff93 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py @@ -0,0 +1,388 @@ +# Copyright 2021 Piotr Żelasko +# Copyright 2022 Xiaomi Corporation (Author: Mingshuang Luo) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 inspect +import logging +from functools import lru_cache +from pathlib import Path +from typing import Any, Dict, Optional + +import torch +from lhotse import CutSet, Fbank, FbankConfig, load_manifest, load_manifest_lazy +from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures + CutConcatenate, + CutMix, + DynamicBucketingSampler, + K2SpeechRecognitionDataset, + PrecomputedFeatures, + SingleCutSampler, + SpecAugment, +) +from lhotse.dataset.input_strategies import ( # noqa F401 For AudioSamples + AudioSamples, + OnTheFlyFeatures, +) +from lhotse.utils import fix_random_seed +from torch.utils.data import DataLoader + +from icefall.utils import str2bool + + +class _SeedWorkers: + def __init__(self, seed: int): + self.seed = seed + + def __call__(self, worker_id: int): + fix_random_seed(self.seed + worker_id) + + +class AsrDataModule: + """ + DataModule for k2 ASR experiments. + It assumes there is always one train and valid dataloader, + but there can be multiple test dataloaders (e.g. LibriSpeech test-clean + and test-other). + + It contains all the common data pipeline modules used in ASR + experiments, e.g.: + - dynamic batch size, + - bucketing samplers, + - cut concatenation, + - augmentation, + - on-the-fly feature extraction + + This class should be derived for specific corpora used in ASR tasks. + """ + + def __init__(self, args: argparse.Namespace): + self.args = args + + @classmethod + def add_arguments(cls, parser: argparse.ArgumentParser): + group = parser.add_argument_group( + title="ASR data related options", + description="These options are used for the preparation of " + "PyTorch DataLoaders from Lhotse CutSet's -- they control the " + "effective batch sizes, sampling strategies, applied data " + "augmentations, etc.", + ) + group.add_argument( + "--manifest-dir", + type=Path, + default=Path("data/fbank"), + help="Path to directory with train/valid/test cuts.", + ) + group.add_argument( + "--max-duration", + type=int, + default=300.0, + help="Maximum pooled recordings duration (seconds) in a " + "single batch. You can reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--bucketing-sampler", + type=str2bool, + default=True, + help="When enabled, the batches will come from buckets of " + "similar duration (saves padding frames).", + ) + group.add_argument( + "--num-buckets", + type=int, + default=30, + help="The number of buckets for the DynamicBucketingSampler" + "(you might want to increase it for larger datasets).", + ) + group.add_argument( + "--concatenate-cuts", + type=str2bool, + default=False, + help="When enabled, utterances (cuts) will be concatenated " + "to minimize the amount of padding.", + ) + group.add_argument( + "--duration-factor", + type=float, + default=1.0, + help="Determines the maximum duration of a concatenated cut " + "relative to the duration of the longest cut in a batch.", + ) + group.add_argument( + "--gap", + type=float, + default=1.0, + help="The amount of padding (in seconds) inserted between " + "concatenated cuts. This padding is filled with noise when " + "noise augmentation is used.", + ) + group.add_argument( + "--on-the-fly-feats", + type=str2bool, + default=False, + help="When enabled, use on-the-fly cut mixing and feature " + "extraction. Will drop existing precomputed feature manifests " + "if available.", + ) + group.add_argument( + "--shuffle", + type=str2bool, + default=True, + help="When enabled (=default), the examples will be " + "shuffled for each epoch.", + ) + group.add_argument( + "--drop-last", + type=str2bool, + default=True, + help="Whether to drop last batch. Used by sampler.", + ) + group.add_argument( + "--return-cuts", + type=str2bool, + default=True, + help="When enabled, each batch will have the " + "field: batch['supervisions']['cut'] with the cuts that " + "were used to construct it.", + ) + + group.add_argument( + "--num-workers", + type=int, + default=2, + help="The number of training dataloader workers that " + "collect the batches.", + ) + + group.add_argument( + "--enable-spec-aug", + type=str2bool, + default=True, + help="When enabled, use SpecAugment for training dataset.", + ) + + group.add_argument( + "--spec-aug-time-warp-factor", + type=int, + default=80, + help="Used only when --enable-spec-aug is True. " + "It specifies the factor for time warping in SpecAugment. " + "Larger values mean more warping. " + "A value less than 1 means to disable time warp.", + ) + + group.add_argument( + "--enable-musan", + type=str2bool, + default=True, + help="When enabled, select noise from MUSAN and mix it" + "with training dataset. ", + ) + + group.add_argument( + "--input-strategy", + type=str, + default="PrecomputedFeatures", + help="AudioSamples or PrecomputedFeatures", + ) + + def train_dataloaders( + self, + cuts_train: CutSet, + sampler_state_dict: Optional[Dict[str, Any]] = None, + ) -> DataLoader: + """ + Args: + cuts_train: + CutSet for training. + sampler_state_dict: + The state dict for the training sampler. + """ + transforms = [] + if self.args.enable_musan: + logging.info("Enable MUSAN") + logging.info("About to get Musan cuts") + cuts_musan = load_manifest(self.args.manifest_dir / "musan_cuts.jsonl.gz") + transforms.append( + CutMix(cuts=cuts_musan, prob=0.5, snr=(10, 20), preserve_id=True) + ) + else: + logging.info("Disable MUSAN") + + if self.args.concatenate_cuts: + logging.info( + f"Using cut concatenation with duration factor " + f"{self.args.duration_factor} and gap {self.args.gap}." + ) + # Cut concatenation should be the first transform in the list, + # so that if we e.g. mix noise in, it will fill the gaps between + # different utterances. + transforms = [ + CutConcatenate( + duration_factor=self.args.duration_factor, gap=self.args.gap + ) + ] + transforms + + input_transforms = [] + if self.args.enable_spec_aug: + logging.info("Enable SpecAugment") + logging.info(f"Time warp factor: {self.args.spec_aug_time_warp_factor}") + # Set the value of num_frame_masks according to Lhotse's version. + # In different Lhotse's versions, the default of num_frame_masks is + # different. + num_frame_masks = 10 + num_frame_masks_parameter = inspect.signature( + SpecAugment.__init__ + ).parameters["num_frame_masks"] + if num_frame_masks_parameter.default == 1: + num_frame_masks = 2 + logging.info(f"Num frame mask: {num_frame_masks}") + input_transforms.append( + SpecAugment( + time_warp_factor=self.args.spec_aug_time_warp_factor, + num_frame_masks=num_frame_masks, + features_mask_size=27, + num_feature_masks=2, + frames_mask_size=100, + ) + ) + else: + logging.info("Disable SpecAugment") + + logging.info("About to create train dataset") + train = K2SpeechRecognitionDataset( + input_strategy=eval(self.args.input_strategy)(), + cut_transforms=transforms, + input_transforms=input_transforms, + return_cuts=self.args.return_cuts, + ) + + if self.args.on_the_fly_feats: + # NOTE: the PerturbSpeed transform should be added only if we + # remove it from data prep stage. + # Add on-the-fly speed perturbation; since originally it would + # have increased epoch size by 3, we will apply prob 2/3 and use + # 3x more epochs. + # Speed perturbation probably should come first before + # concatenation, but in principle the transforms order doesn't have + # to be strict (e.g. could be randomized) + # transforms = [PerturbSpeed(factors=[0.9, 1.1], p=2/3)] + transforms # noqa + # Drop feats to be on the safe side. + train = K2SpeechRecognitionDataset( + cut_transforms=transforms, + input_strategy=OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))), + input_transforms=input_transforms, + return_cuts=self.args.return_cuts, + ) + + if self.args.bucketing_sampler: + logging.info("Using DynamicBucketingSampler.") + train_sampler = DynamicBucketingSampler( + cuts_train, + max_duration=self.args.max_duration, + shuffle=self.args.shuffle, + num_buckets=self.args.num_buckets, + drop_last=self.args.drop_last, + ) + else: + logging.info("Using SingleCutSampler.") + train_sampler = SingleCutSampler( + cuts_train, + max_duration=self.args.max_duration, + shuffle=self.args.shuffle, + ) + logging.info("About to create train dataloader") + + if sampler_state_dict is not None: + logging.info("Loading sampler state dict") + train_sampler.load_state_dict(sampler_state_dict) + + # 'seed' is derived from the current random state, which will have + # previously been set in the main process. + seed = torch.randint(0, 100000, ()).item() + worker_init_fn = _SeedWorkers(seed) + + train_dl = DataLoader( + train, + sampler=train_sampler, + batch_size=None, + num_workers=self.args.num_workers, + persistent_workers=True, + worker_init_fn=worker_init_fn, + ) + + return train_dl + + def valid_dataloaders(self, cuts_valid: CutSet) -> DataLoader: + transforms = [] + if self.args.concatenate_cuts: + transforms = [ + CutConcatenate( + duration_factor=self.args.duration_factor, gap=self.args.gap + ) + ] + transforms + + logging.info("About to create dev dataset") + if self.args.on_the_fly_feats: + validate = K2SpeechRecognitionDataset( + cut_transforms=transforms, + input_strategy=OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))), + return_cuts=self.args.return_cuts, + ) + else: + validate = K2SpeechRecognitionDataset( + cut_transforms=transforms, + return_cuts=self.args.return_cuts, + ) + valid_sampler = DynamicBucketingSampler( + cuts_valid, + max_duration=self.args.max_duration, + shuffle=False, + ) + logging.info("About to create dev dataloader") + valid_dl = DataLoader( + validate, + sampler=valid_sampler, + batch_size=None, + num_workers=2, + persistent_workers=False, + ) + + return valid_dl + + def test_dataloaders(self, cuts: CutSet) -> DataLoader: + logging.debug("About to create test dataset") + test = K2SpeechRecognitionDataset( + input_strategy=OnTheFlyFeatures(Fbank(FbankConfig(num_mel_bins=80))) + if self.args.on_the_fly_feats + else eval(self.args.input_strategy)(), + return_cuts=self.args.return_cuts, + ) + sampler = DynamicBucketingSampler( + cuts, + max_duration=self.args.max_duration, + shuffle=False, + ) + logging.debug("About to create test dataloader") + test_dl = DataLoader( + test, + batch_size=None, + sampler=sampler, + num_workers=self.args.num_workers, + ) + return test_dl diff --git a/egs/multi_zh-hans/ASR/zipformer/beam_search.py b/egs/multi_zh-hans/ASR/zipformer/beam_search.py new file mode 120000 index 000000000..8e2c0a65c --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/beam_search.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/beam_search.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/decode.py b/egs/multi_zh-hans/ASR/zipformer/decode.py new file mode 100755 index 000000000..f501c3c30 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/decode.py @@ -0,0 +1,828 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) greedy search +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method greedy_search + +(2) beam search (not recommended) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method beam_search \ + --beam-size 4 + +(3) modified beam search +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method modified_beam_search \ + --beam-size 4 + +(4) fast beam search (one best) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 + +(5) fast beam search (nbest) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(6) fast beam search (nbest oracle WER) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_oracle \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 \ + --num-paths 200 \ + --nbest-scale 0.5 + +(7) fast beam search (with LG) +./zipformer/decode.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp \ + --max-duration 600 \ + --decoding-method fast_beam_search_nbest_LG \ + --beam 20.0 \ + --max-contexts 8 \ + --max-states 64 +""" + + +import argparse +import logging +import math +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import AsrDataModule +from beam_search import ( + beam_search, + fast_beam_search_nbest, + fast_beam_search_nbest_LG, + fast_beam_search_nbest_oracle, + fast_beam_search_one_best, + greedy_search, + greedy_search_batch, + modified_beam_search, +) +from lhotse.cut import Cut +from multi_dataset import MultiDataset +from train import add_model_arguments, get_model, get_params + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.lexicon import Lexicon +from icefall.utils import ( + AttributeDict, + make_pad_mask, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + +LOG_EPS = math.log(1e-10) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=15, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_2000/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--lang-dir", + type=Path, + default="data/lang_bpe_2000", + help="The lang dir containing word table and LG graph", + ) + + parser.add_argument( + "--decoding-method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - beam_search + - modified_beam_search + - fast_beam_search + - fast_beam_search_nbest + - fast_beam_search_nbest_oracle + - fast_beam_search_nbest_LG + If you use fast_beam_search_nbest_LG, you have to specify + `--lang-dir`, which should contain `LG.pt`. + """, + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --decoding-method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=20.0, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --decoding-method is fast_beam_search, + fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle + """, + ) + + parser.add_argument( + "--ngram-lm-scale", + type=float, + default=0.01, + help=""" + Used only when --decoding_method is fast_beam_search_nbest_LG. + It specifies the scale for n-gram LM scores. + """, + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=8, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=64, + help="""Used only when --decoding-method is + fast_beam_search, fast_beam_search_nbest, fast_beam_search_nbest_LG, + and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; " "2 means tri-gram", + ) + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. + Used only when --decoding_method is greedy_search""", + ) + + parser.add_argument( + "--num-paths", + type=int, + default=200, + help="""Number of paths for nbest decoding. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + parser.add_argument( + "--nbest-scale", + type=float, + default=0.5, + help="""Scale applied to lattice scores when computing nbest paths. + Used only when the decoding method is fast_beam_search_nbest, + fast_beam_search_nbest_LG, and fast_beam_search_nbest_oracle""", + ) + + add_model_arguments(parser) + + return parser + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + batch: dict, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if greedy_search is used, it would be "greedy_search" + If beam search with a beam size of 7 is used, it would be + "beam_7" + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return the decoding result. See above description for the format of + the returned dict. + """ + device = next(model.parameters()).device + feature = batch["inputs"] + assert feature.ndim == 3 + + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + if params.causal: + # this seems to cause insertions at the end of the utterance if used with zipformer. + pad_len = 30 + feature_lens += pad_len + feature = torch.nn.functional.pad( + feature, + pad=(0, 0, 0, pad_len), + value=LOG_EPS, + ) + + encoder_out, encoder_out_lens = model.forward_encoder(feature, feature_lens) + + hyps = [] + + if params.decoding_method == "fast_beam_search": + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "fast_beam_search_nbest_LG": + hyp_tokens = fast_beam_search_nbest_LG( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + nbest_scale=params.nbest_scale, + ) + for hyp in hyp_tokens: + hyps.append([word_table[i] for i in hyp]) + elif params.decoding_method == "fast_beam_search_nbest": + hyp_tokens = fast_beam_search_nbest( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + nbest_scale=params.nbest_scale, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "fast_beam_search_nbest_oracle": + hyp_tokens = fast_beam_search_nbest_oracle( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + num_paths=params.num_paths, + ref_texts=sp.encode(supervisions["text"]), + nbest_scale=params.nbest_scale, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + elif params.decoding_method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + for hyp in sp.decode(hyp_tokens): + hyps.append(hyp.split()) + else: + batch_size = encoder_out.size(0) + + for i in range(batch_size): + # fmt: off + encoder_out_i = encoder_out[i:i+1, :encoder_out_lens[i]] + # fmt: on + if params.decoding_method == "greedy_search": + hyp = greedy_search( + model=model, + encoder_out=encoder_out_i, + max_sym_per_frame=params.max_sym_per_frame, + ) + elif params.decoding_method == "beam_search": + hyp = beam_search( + model=model, + encoder_out=encoder_out_i, + beam=params.beam_size, + ) + else: + raise ValueError( + f"Unsupported decoding method: {params.decoding_method}" + ) + hyps.append(sp.decode(hyp).split()) + + if params.decoding_method == "greedy_search": + return {"greedy_search": hyps} + elif "fast_beam_search" in params.decoding_method: + key = f"beam_{params.beam}_" + key += f"max_contexts_{params.max_contexts}_" + key += f"max_states_{params.max_states}" + if "nbest" in params.decoding_method: + key += f"_num_paths_{params.num_paths}_" + key += f"nbest_scale_{params.nbest_scale}" + if "LG" in params.decoding_method: + key += f"_ngram_lm_scale_{params.ngram_lm_scale}" + + return {key: hyps} + else: + return {f"beam_size_{params.beam_size}": hyps} + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + sp: spm.SentencePieceProcessor, + word_table: Optional[k2.SymbolTable] = None, + decoding_graph: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + sp: + The BPE model. + word_table: + The word symbol table. + decoding_graph: + The decoding graph. Can be either a `k2.trivial_graph` or HLG, Used + only when --decoding_method is fast_beam_search, fast_beam_search_nbest, + fast_beam_search_nbest_oracle, and fast_beam_search_nbest_LG. + Returns: + Return a dict, whose key may be "greedy_search" if greedy search + is used, or it may be "beam_7" if beam size of 7 is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + if params.decoding_method == "greedy_search": + log_interval = 50 + else: + log_interval = 20 + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + texts = [list(str(text).replace(" ", "")) for text in texts] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + sp=sp, + decoding_graph=decoding_graph, + word_table=word_table, + batch=batch, + ) + + for name, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + hyp_text = "".join(hyp_words) + this_batch.append((cut_id, ref_text, hyp_text)) + + results[name].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % log_interval == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = ( + params.res_dir / f"recogs-{test_set_name}-{key}-{params.suffix}.txt" + ) + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = ( + params.res_dir / f"errs-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=True + ) + test_set_wers[key] = wer + + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = ( + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + ) + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + AsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + assert params.decoding_method in ( + "greedy_search", + "beam_search", + "fast_beam_search", + "fast_beam_search_nbest", + "fast_beam_search_nbest_LG", + "fast_beam_search_nbest_oracle", + "modified_beam_search", + ) + params.res_dir = params.exp_dir / params.decoding_method + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + if params.causal: + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + params.suffix += f"-chunk-{params.chunk_size}" + params.suffix += f"-left-context-{params.left_context_frames}" + + if "fast_beam_search" in params.decoding_method: + params.suffix += f"-beam-{params.beam}" + params.suffix += f"-max-contexts-{params.max_contexts}" + params.suffix += f"-max-states-{params.max_states}" + if "nbest" in params.decoding_method: + params.suffix += f"-nbest-scale-{params.nbest_scale}" + params.suffix += f"-num-paths-{params.num_paths}" + if "LG" in params.decoding_method: + params.suffix += f"-ngram-lm-scale-{params.ngram_lm_scale}" + elif "beam_search" in params.decoding_method: + params.suffix += f"-{params.decoding_method}-beam-size-{params.beam_size}" + else: + params.suffix += f"-context-{params.context_size}" + params.suffix += f"-max-sym-per-frame-{params.max_sym_per_frame}" + + if params.use_averaged_model: + params.suffix += "-use-averaged-model" + + setup_logger(f"{params.res_dir}/log-decode-{params.suffix}") + logging.info("Decoding started") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # and are defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.unk_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + logging.info(params) + + logging.info("About to create model") + model = get_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + + if "fast_beam_search" in params.decoding_method: + if params.decoding_method == "fast_beam_search_nbest_LG": + lexicon = Lexicon(params.lang_dir) + word_table = lexicon.word_table + lg_filename = params.lang_dir / "LG.pt" + logging.info(f"Loading {lg_filename}") + decoding_graph = k2.Fsa.from_dict( + torch.load(lg_filename, map_location=device) + ) + decoding_graph.scores *= params.ngram_lm_scale + else: + word_table = None + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + else: + decoding_graph = None + word_table = None + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + data_module = AsrDataModule(args) + multi_dataset = MultiDataset(args.manifest_dir) + + def remove_short_utt(c: Cut): + T = ((c.num_frames - 7) // 2 + 1) // 2 + if T <= 0: + logging.warning( + f"Excluding cut with ID: {c.id} from decoding, num_frames: {c.num_frames}" + ) + return T > 0 + + test_sets_cuts = multi_dataset.test_cuts() + + test_sets = test_sets_cuts.keys() + test_dl = [ + data_module.test_dataloaders(test_sets_cuts[cuts_name].filter(remove_short_utt)) + for cuts_name in test_sets + ] + + for test_set, test_dl in zip(test_sets, test_dl): + logging.info(f"Start decoding test set: {test_set}") + + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + sp=sp, + word_table=word_table, + decoding_graph=decoding_graph, + ) + + save_results( + params=params, + test_set_name=test_set, + results_dict=results_dict, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/zipformer/decoder.py b/egs/multi_zh-hans/ASR/zipformer/decoder.py new file mode 120000 index 000000000..5a8018680 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/decoder.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/decoder.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/encoder_interface.py b/egs/multi_zh-hans/ASR/zipformer/encoder_interface.py new file mode 120000 index 000000000..c2eaca671 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/encoder_interface.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/encoder_interface.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/export-onnx-streaming.py b/egs/multi_zh-hans/ASR/zipformer/export-onnx-streaming.py new file mode 120000 index 000000000..2962eb784 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/export-onnx-streaming.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/export-onnx-streaming.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/export-onnx.py b/egs/multi_zh-hans/ASR/zipformer/export-onnx.py new file mode 120000 index 000000000..70a15683c --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/export-onnx.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/export-onnx.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/export.py b/egs/multi_zh-hans/ASR/zipformer/export.py new file mode 100755 index 000000000..723288191 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/export.py @@ -0,0 +1,541 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2023 Xiaomi Corporation (Author: Fangjun Kuang, +# Zengwei Yao, +# Wei Kang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script converts several saved checkpoints +# to a single one using model averaging. +""" + +Usage: + +Note: This is a example for librispeech dataset, if you are using different +dataset, you should change the argument values according to your dataset. + +(1) Export to torchscript model using torch.jit.script() + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --tokens data/lang_bpe_2000/tokens.txt \ + --epoch 20 \ + --avg 1 \ + --jit 1 + +It will generate a file `jit_script.pt` in the given `exp_dir`. You can later +load it by `torch.jit.load("jit_script.pt")`. + +Check ./jit_pretrained.py for its usage. + +Check https://github.com/k2-fsa/sherpa +for how to use the exported models outside of icefall. + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --tokens data/lang_bpe_2000/tokens.txt \ + --epoch 20 \ + --avg 1 \ + --jit 1 + +It will generate a file `jit_script_chunk_16_left_128.pt` in the given `exp_dir`. +You can later load it by `torch.jit.load("jit_script_chunk_16_left_128.pt")`. + +Check ./jit_pretrained_streaming.py for its usage. + +Check https://github.com/k2-fsa/sherpa +for how to use the exported models outside of icefall. + +(2) Export `model.state_dict()` + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --tokens data/lang_bpe_2000/tokens.txt \ + --epoch 20 \ + --avg 1 + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --causal 1 \ + --tokens data/lang_bpe_2000/tokens.txt \ + --epoch 20 \ + --avg 1 + +It will generate a file `pretrained.pt` in the given `exp_dir`. You can later +load it by `icefall.checkpoint.load_checkpoint()`. + +- For non-streaming model: + +To use the generated file with `zipformer/decode.py`, +you can do: + + cd /path/to/exp_dir + ln -s pretrained.pt epoch-9999.pt + + cd /path/to/egs/librispeech/ASR + ./zipformer/decode.py \ + --exp-dir ./zipformer/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bpe_2000/bpe.model + +- For streaming model: + +To use the generated file with `zipformer/decode.py` and `zipformer/streaming_decode.py`, you can do: + + cd /path/to/exp_dir + ln -s pretrained.pt epoch-9999.pt + + cd /path/to/egs/librispeech/ASR + + # simulated streaming decoding + ./zipformer/decode.py \ + --exp-dir ./zipformer/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bpe_2000/bpe.model + + # chunk-wise streaming decoding + ./zipformer/streaming_decode.py \ + --exp-dir ./zipformer/exp \ + --epoch 9999 \ + --avg 1 \ + --max-duration 600 \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --decoding-method greedy_search \ + --bpe-model data/lang_bpe_2000/bpe.model + +Check ./pretrained.py for its usage. + +Note: If you don't want to train a model from scratch, we have +provided one for you. You can get it at + +- non-streaming model: +https://huggingface.co/zrjin/icefall-asr-multi-zh-hans-zipformer-2023-9-2/ + +with the following commands: + + sudo apt-get install git-lfs + git lfs install + git clone https://huggingface.co/zrjin/icefall-asr-multi-zh-hans-zipformer-2023-9-2/ + # You will find the pre-trained models in exp dir +""" + +import argparse +import logging +import re +from pathlib import Path +from typing import List, Tuple + +import k2 +import torch +from scaling_converter import convert_scaled_to_non_scaled +from torch import Tensor, nn +from train import add_model_arguments, get_model, get_params + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.utils import make_pad_mask, str2bool + + +def num_tokens( + token_table: k2.SymbolTable, disambig_pattern: str = re.compile(r"^#\d+$") +) -> int: + """Return the number of tokens excluding those from + disambiguation symbols. + + Caution: + 0 is not a token ID so it is excluded from the return value. + """ + symbols = token_table.symbols + ans = [] + for s in symbols: + if not disambig_pattern.match(s): + ans.append(token_table[s]) + num_tokens = len(ans) + if 0 in ans: + num_tokens -= 1 + return num_tokens + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=20, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=1, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--tokens", + type=str, + default="data/lang_bpe_2000/tokens.txt", + help="Path to the tokens.txt", + ) + + parser.add_argument( + "--jit", + type=str2bool, + default=False, + help="""True to save a model after applying torch.jit.script. + It will generate a file named jit_script.pt. + Check ./jit_pretrained.py for how to use it. + """, + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +class EncoderModel(nn.Module): + """A wrapper for encoder and encoder_embed""" + + def __init__(self, encoder: nn.Module, encoder_embed: nn.Module) -> None: + super().__init__() + self.encoder = encoder + self.encoder_embed = encoder_embed + + def forward( + self, features: Tensor, feature_lengths: Tensor + ) -> Tuple[Tensor, Tensor]: + """ + Args: + features: (N, T, C) + feature_lengths: (N,) + """ + x, x_lens = self.encoder_embed(features, feature_lengths) + + src_key_padding_mask = make_pad_mask(x_lens) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + + encoder_out, encoder_out_lens = self.encoder(x, x_lens, src_key_padding_mask) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + return encoder_out, encoder_out_lens + + +class StreamingEncoderModel(nn.Module): + """A wrapper for encoder and encoder_embed""" + + def __init__(self, encoder: nn.Module, encoder_embed: nn.Module) -> None: + super().__init__() + assert len(encoder.chunk_size) == 1, encoder.chunk_size + assert len(encoder.left_context_frames) == 1, encoder.left_context_frames + self.chunk_size = encoder.chunk_size[0] + self.left_context_len = encoder.left_context_frames[0] + + # The encoder_embed subsample features (T - 7) // 2 + # The ConvNeXt module needs (7 - 1) // 2 = 3 frames of right padding after subsampling + self.pad_length = 7 + 2 * 3 + + self.encoder = encoder + self.encoder_embed = encoder_embed + + def forward( + self, features: Tensor, feature_lengths: Tensor, states: List[Tensor] + ) -> Tuple[Tensor, Tensor, List[Tensor]]: + """Streaming forward for encoder_embed and encoder. + + Args: + features: (N, T, C) + feature_lengths: (N,) + states: a list of Tensors + + Returns encoder outputs, output lengths, and updated states. + """ + chunk_size = self.chunk_size + left_context_len = self.left_context_len + + cached_embed_left_pad = states[-2] + x, x_lens, new_cached_embed_left_pad = self.encoder_embed.streaming_forward( + x=features, + x_lens=feature_lengths, + cached_left_pad=cached_embed_left_pad, + ) + assert x.size(1) == chunk_size, (x.size(1), chunk_size) + + src_key_padding_mask = make_pad_mask(x_lens) + + # processed_mask is used to mask out initial states + processed_mask = torch.arange(left_context_len, device=x.device).expand( + x.size(0), left_context_len + ) + processed_lens = states[-1] # (batch,) + # (batch, left_context_size) + processed_mask = (processed_lens.unsqueeze(1) <= processed_mask).flip(1) + # Update processed lengths + new_processed_lens = processed_lens + x_lens + + # (batch, left_context_size + chunk_size) + src_key_padding_mask = torch.cat([processed_mask, src_key_padding_mask], dim=1) + + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + encoder_states = states[:-2] + + ( + encoder_out, + encoder_out_lens, + new_encoder_states, + ) = self.encoder.streaming_forward( + x=x, + x_lens=x_lens, + states=encoder_states, + src_key_padding_mask=src_key_padding_mask, + ) + encoder_out = encoder_out.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + new_states = new_encoder_states + [ + new_cached_embed_left_pad, + new_processed_lens, + ] + return encoder_out, encoder_out_lens, new_states + + @torch.jit.export + def get_init_states( + self, + batch_size: int = 1, + device: torch.device = torch.device("cpu"), + ) -> List[torch.Tensor]: + """ + Returns a list of cached tensors of all encoder layers. For layer-i, states[i*6:(i+1)*6] + is (cached_key, cached_nonlin_attn, cached_val1, cached_val2, cached_conv1, cached_conv2). + states[-2] is the cached left padding for ConvNeXt module, + of shape (batch_size, num_channels, left_pad, num_freqs) + states[-1] is processed_lens of shape (batch,), which records the number + of processed frames (at 50hz frame rate, after encoder_embed) for each sample in batch. + """ + states = self.encoder.get_init_states(batch_size, device) + + embed_states = self.encoder_embed.get_init_states(batch_size, device) + states.append(embed_states) + + processed_lens = torch.zeros(batch_size, dtype=torch.int32, device=device) + states.append(processed_lens) + + return states + + +@torch.no_grad() +def main(): + args = get_parser().parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + device = torch.device("cpu") + # if torch.cuda.is_available(): + # device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + token_table = k2.SymbolTable.from_file(params.tokens) + params.blank_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 + + logging.info(params) + + logging.info("About to create model") + model = get_model(params) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.eval() + + if params.jit is True: + convert_scaled_to_non_scaled(model, inplace=True) + # We won't use the forward() method of the model in C++, so just ignore + # it here. + # Otherwise, one of its arguments is a ragged tensor and is not + # torch scriptabe. + model.__class__.forward = torch.jit.ignore(model.__class__.forward) + + # Wrap encoder and encoder_embed as a module + if params.causal: + model.encoder = StreamingEncoderModel(model.encoder, model.encoder_embed) + chunk_size = model.encoder.chunk_size + left_context_len = model.encoder.left_context_len + filename = f"jit_script_chunk_{chunk_size}_left_{left_context_len}.pt" + else: + model.encoder = EncoderModel(model.encoder, model.encoder_embed) + filename = "jit_script.pt" + + logging.info("Using torch.jit.script") + model = torch.jit.script(model) + model.save(str(params.exp_dir / filename)) + logging.info(f"Saved to {filename}") + else: + logging.info("Not using torchscript. Export model.state_dict()") + # Save it using a format so that it can be loaded + # by :func:`load_checkpoint` + filename = params.exp_dir / "pretrained.pt" + torch.save({"model": model.state_dict()}, str(filename)) + logging.info(f"Saved to {filename}") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/multi_zh-hans/ASR/zipformer/generate_averaged_model.py b/egs/multi_zh-hans/ASR/zipformer/generate_averaged_model.py new file mode 100755 index 000000000..68111fad7 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/generate_averaged_model.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# +# Copyright 2021-2022 Xiaomi Corporation (Author: Yifan Yang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: +(1) use the checkpoint exp_dir/epoch-xxx.pt +./zipformer/generate_averaged_model.py \ + --epoch 28 \ + --avg 15 \ + --exp-dir ./zipformer/exp + +It will generate a file `epoch-28-avg-15.pt` in the given `exp_dir`. +You can later load it by `torch.load("epoch-28-avg-15.pt")`. + +(2) use the checkpoint exp_dir/checkpoint-iter.pt +./zipformer/generate_averaged_model.py \ + --iter 22000 \ + --avg 5 \ + --exp-dir ./zipformer/exp + +It will generate a file `iter-22000-avg-5.pt` in the given `exp_dir`. +You can later load it by `torch.load("iter-22000-avg-5.pt")`. +""" + + +import argparse +from pathlib import Path + +import k2 +import torch +from train import add_model_arguments, get_model, get_params + +from icefall.checkpoint import average_checkpoints_with_averaged_model, find_checkpoints + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--epoch", + type=int, + default=30, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=9, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--tokens", + type=str, + default="data/lang_bpe_500/tokens.txt", + help="Path to the tokens.txt", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + add_model_arguments(parser) + + return parser + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + params = get_params() + params.update(vars(args)) + + if params.iter > 0: + params.suffix = f"iter-{params.iter}-avg-{params.avg}" + else: + params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" + + print("Script started") + + device = torch.device("cpu") + print(f"Device: {device}") + + symbol_table = k2.SymbolTable.from_file(params.tokens) + params.blank_id = symbol_table[""] + params.unk_id = symbol_table[""] + params.vocab_size = len(symbol_table) + + print("About to create model") + model = get_model(params) + + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + print( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + filename = params.exp_dir / f"iter-{params.iter}-avg-{params.avg}.pt" + torch.save({"model": model.state_dict()}, filename) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + print( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + filename = params.exp_dir / f"epoch-{params.epoch}-avg-{params.avg}.pt" + torch.save({"model": model.state_dict()}, filename) + + num_param = sum([p.numel() for p in model.parameters()]) + print(f"Number of model parameters: {num_param}") + + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/zipformer/jit_pretrained.py b/egs/multi_zh-hans/ASR/zipformer/jit_pretrained.py new file mode 120000 index 000000000..25108391f --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/jit_pretrained.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/jit_pretrained.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/jit_pretrained_ctc.py b/egs/multi_zh-hans/ASR/zipformer/jit_pretrained_ctc.py new file mode 120000 index 000000000..9a8da5844 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/jit_pretrained_ctc.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/jit_pretrained_ctc.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/jit_pretrained_streaming.py b/egs/multi_zh-hans/ASR/zipformer/jit_pretrained_streaming.py new file mode 120000 index 000000000..1962351e9 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/jit_pretrained_streaming.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/jit_pretrained_streaming.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/joiner.py b/egs/multi_zh-hans/ASR/zipformer/joiner.py new file mode 120000 index 000000000..5b8a36332 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/joiner.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/joiner.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/model.py b/egs/multi_zh-hans/ASR/zipformer/model.py new file mode 120000 index 000000000..cd7e07d72 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/model.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/model.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/multi_dataset.py b/egs/multi_zh-hans/ASR/zipformer/multi_dataset.py new file mode 100644 index 000000000..b1920e62e --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/multi_dataset.py @@ -0,0 +1,316 @@ +# Copyright 2023 Xiaomi Corp. (authors: Zengrui Jin) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 glob +import logging +import re +from pathlib import Path +from typing import Dict, List + +import lhotse +from lhotse import CutSet, load_manifest_lazy + + +class MultiDataset: + def __init__(self, fbank_dir: str): + """ + Args: + manifest_dir: + It is expected to contain the following files: + - aidatatang_cuts_train.jsonl.gz + - aishell_cuts_train.jsonl.gz + - aishell2_cuts_train.jsonl.gz + - aishell4_cuts_train_L.jsonl.gz + - aishell4_cuts_train_M.jsonl.gz + - aishell4_cuts_train_S.jsonl.gz + - alimeeting-far_cuts_train.jsonl.gz + - magicdata_cuts_train.jsonl.gz + - primewords_cuts_train.jsonl.gz + - stcmds_cuts_train.jsonl.gz + - thchs_30_cuts_train.jsonl.gz + - kespeech/kespeech-asr_cuts_train_phase1.jsonl.gz + - kespeech/kespeech-asr_cuts_train_phase2.jsonl.gz + - wenetspeech/cuts_L.jsonl.gz + """ + self.fbank_dir = Path(fbank_dir) + + def train_cuts(self) -> CutSet: + logging.info("About to get multidataset train cuts") + + # THCHS-30 + logging.info("Loading THCHS-30 in lazy mode") + thchs_30_cuts = load_manifest_lazy( + self.fbank_dir / "thchs_30_cuts_train.jsonl.gz" + ) + + # AISHELL-1 + logging.info("Loading Aishell-1 in lazy mode") + aishell_cuts = load_manifest_lazy( + self.fbank_dir / "aishell_cuts_train.jsonl.gz" + ) + + # AISHELL-2 + logging.info("Loading Aishell-2 in lazy mode") + aishell_2_cuts = load_manifest_lazy( + self.fbank_dir / "aishell2_cuts_train.jsonl.gz" + ) + + # AISHELL-4 + logging.info("Loading Aishell-4 in lazy mode") + aishell_4_L_cuts = load_manifest_lazy( + self.fbank_dir / "aishell4_cuts_train_L.jsonl.gz" + ) + aishell_4_M_cuts = load_manifest_lazy( + self.fbank_dir / "aishell4_cuts_train_M.jsonl.gz" + ) + aishell_4_S_cuts = load_manifest_lazy( + self.fbank_dir / "aishell4_cuts_train_S.jsonl.gz" + ) + + # ST-CMDS + logging.info("Loading ST-CMDS in lazy mode") + stcmds_cuts = load_manifest_lazy(self.fbank_dir / "stcmds_cuts_train.jsonl.gz") + + # Primewords + logging.info("Loading Primewords in lazy mode") + primewords_cuts = load_manifest_lazy( + self.fbank_dir / "primewords_cuts_train.jsonl.gz" + ) + + # MagicData + logging.info("Loading MagicData in lazy mode") + magicdata_cuts = load_manifest_lazy( + self.fbank_dir / "magicdata_cuts_train.jsonl.gz" + ) + + # Aidatatang_200zh + logging.info("Loading Aidatatang_200zh in lazy mode") + aidatatang_200zh_cuts = load_manifest_lazy( + self.fbank_dir / "aidatatang_cuts_train.jsonl.gz" + ) + + # Ali-Meeting + logging.info("Loading Ali-Meeting in lazy mode") + alimeeting_cuts = load_manifest_lazy( + self.fbank_dir / "alimeeting-far_cuts_train.jsonl.gz" + ) + + # WeNetSpeech + logging.info("Loading WeNetSpeech in lazy mode") + wenetspeech_L_cuts = load_manifest_lazy( + self.fbank_dir / "wenetspeech" / "cuts_L.jsonl.gz" + ) + + # KeSpeech + logging.info("Loading KeSpeech in lazy mode") + kespeech_1_cuts = load_manifest_lazy( + self.fbank_dir / "kespeech" / "kespeech-asr_cuts_train_phase1.jsonl.gz" + ) + kespeech_2_cuts = load_manifest_lazy( + self.fbank_dir / "kespeech" / "kespeech-asr_cuts_train_phase2.jsonl.gz" + ) + + return CutSet.mux( + thchs_30_cuts, + aishell_cuts, + aishell_2_cuts, + aishell_4_L_cuts, + aishell_4_M_cuts, + aishell_4_S_cuts, + stcmds_cuts, + primewords_cuts, + magicdata_cuts, + aidatatang_200zh_cuts, + alimeeting_cuts, + wenetspeech_L_cuts, + kespeech_1_cuts, + kespeech_2_cuts, + weights=[ + len(thchs_30_cuts), + len(aishell_cuts), + len(aishell_2_cuts), + len(aishell_4_L_cuts), + len(aishell_4_M_cuts), + len(aishell_4_S_cuts), + len(stcmds_cuts), + len(primewords_cuts), + len(magicdata_cuts), + len(aidatatang_200zh_cuts), + len(alimeeting_cuts), + len(wenetspeech_L_cuts), + len(kespeech_1_cuts), + len(kespeech_2_cuts), + ], + ) + + def dev_cuts(self) -> CutSet: + logging.info("About to get multidataset dev cuts") + + # Aidatatang_200zh + logging.info("Loading Aidatatang_200zh DEV set in lazy mode") + aidatatang_dev_cuts = load_manifest_lazy( + self.fbank_dir / "aidatatang_cuts_dev.jsonl.gz" + ) + + # AISHELL + logging.info("Loading Aishell DEV set in lazy mode") + aishell_dev_cuts = load_manifest_lazy( + self.fbank_dir / "aishell_cuts_dev.jsonl.gz" + ) + + # AISHELL-2 + logging.info("Loading Aishell-2 DEV set in lazy mode") + aishell2_dev_cuts = load_manifest_lazy( + self.fbank_dir / "aishell2_cuts_dev.jsonl.gz" + ) + + # Ali-Meeting + logging.info("Loading Ali-Meeting DEV set in lazy mode") + alimeeting_dev_cuts = load_manifest_lazy( + self.fbank_dir / "alimeeting-far_cuts_eval.jsonl.gz" + ) + + # MagicData + logging.info("Loading MagicData DEV set in lazy mode") + magicdata_dev_cuts = load_manifest_lazy( + self.fbank_dir / "magicdata_cuts_dev.jsonl.gz" + ) + + # KeSpeech + logging.info("Loading KeSpeech DEV set in lazy mode") + kespeech_dev_phase1_cuts = load_manifest_lazy( + self.fbank_dir / "kespeech" / "kespeech-asr_cuts_dev_phase1.jsonl.gz" + ) + kespeech_dev_phase2_cuts = load_manifest_lazy( + self.fbank_dir / "kespeech" / "kespeech-asr_cuts_dev_phase2.jsonl.gz" + ) + + # WeNetSpeech + logging.info("Loading WeNetSpeech DEV set in lazy mode") + wenetspeech_dev_cuts = load_manifest_lazy( + self.fbank_dir / "wenetspeech" / "cuts_DEV.jsonl.gz" + ) + + return wenetspeech_dev_cuts + # return [ + # aidatatang_dev_cuts, + # aishell_dev_cuts, + # aishell2_dev_cuts, + # alimeeting_dev_cuts, + # magicdata_dev_cuts, + # kespeech_dev_phase1_cuts, + # kespeech_dev_phase2_cuts, + # wenetspeech_dev_cuts, + # ] + + def test_cuts(self) -> Dict[str, CutSet]: + logging.info("About to get multidataset test cuts") + + # Aidatatang_200zh + logging.info("Loading Aidatatang_200zh set in lazy mode") + aidatatang_test_cuts = load_manifest_lazy( + self.fbank_dir / "aidatatang_cuts_test.jsonl.gz" + ) + aidatatang_dev_cuts = load_manifest_lazy( + self.fbank_dir / "aidatatang_cuts_dev.jsonl.gz" + ) + + # AISHELL + logging.info("Loading Aishell set in lazy mode") + aishell_test_cuts = load_manifest_lazy( + self.fbank_dir / "aishell_cuts_test.jsonl.gz" + ) + aishell_dev_cuts = load_manifest_lazy( + self.fbank_dir / "aishell_cuts_dev.jsonl.gz" + ) + + # AISHELL-2 + logging.info("Loading Aishell-2 set in lazy mode") + aishell2_test_cuts = load_manifest_lazy( + self.fbank_dir / "aishell2_cuts_test.jsonl.gz" + ) + aishell2_dev_cuts = load_manifest_lazy( + self.fbank_dir / "aishell2_cuts_dev.jsonl.gz" + ) + + # AISHELL-4 + logging.info("Loading Aishell-4 TEST set in lazy mode") + aishell4_test_cuts = load_manifest_lazy( + self.fbank_dir / "aishell4_cuts_test.jsonl.gz" + ) + + # Ali-Meeting + logging.info("Loading Ali-Meeting set in lazy mode") + alimeeting_test_cuts = load_manifest_lazy( + self.fbank_dir / "alimeeting-far_cuts_test.jsonl.gz" + ) + alimeeting_eval_cuts = load_manifest_lazy( + self.fbank_dir / "alimeeting-far_cuts_eval.jsonl.gz" + ) + + # MagicData + logging.info("Loading MagicData set in lazy mode") + magicdata_test_cuts = load_manifest_lazy( + self.fbank_dir / "magicdata_cuts_test.jsonl.gz" + ) + magicdata_dev_cuts = load_manifest_lazy( + self.fbank_dir / "magicdata_cuts_dev.jsonl.gz" + ) + + # KeSpeech + logging.info("Loading KeSpeech set in lazy mode") + kespeech_test_cuts = load_manifest_lazy( + self.fbank_dir / "kespeech" / "kespeech-asr_cuts_test.jsonl.gz" + ) + kespeech_dev_phase1_cuts = load_manifest_lazy( + self.fbank_dir / "kespeech" / "kespeech-asr_cuts_dev_phase1.jsonl.gz" + ) + kespeech_dev_phase2_cuts = load_manifest_lazy( + self.fbank_dir / "kespeech" / "kespeech-asr_cuts_dev_phase2.jsonl.gz" + ) + + # WeNetSpeech + logging.info("Loading WeNetSpeech set in lazy mode") + wenetspeech_test_meeting_cuts = load_manifest_lazy( + self.fbank_dir / "wenetspeech" / "cuts_TEST_MEETING.jsonl.gz" + ) + wenetspeech_test_net_cuts = load_manifest_lazy( + self.fbank_dir / "wenetspeech" / "cuts_TEST_NET.jsonl.gz" + ) + wenetspeech_dev_cuts = load_manifest_lazy( + self.fbank_dir / "wenetspeech" / "cuts_DEV.jsonl.gz" + ) + + return { + "aidatatang_test": aidatatang_test_cuts, + "aidatatang_dev": aidatatang_dev_cuts, + "alimeeting_test": alimeeting_test_cuts, + "alimeeting_eval": alimeeting_eval_cuts, + "aishell_test": aishell_test_cuts, + "aishell_dev": aishell_dev_cuts, + "aishell-2_test": aishell2_test_cuts, + "aishell-2_dev": aishell2_dev_cuts, + "aishell-4": aishell4_test_cuts, + "magicdata_test": magicdata_test_cuts, + "magicdata_dev": magicdata_dev_cuts, + "kespeech-asr_test": kespeech_test_cuts, + "kespeech-asr_dev_phase1": kespeech_dev_phase1_cuts, + "kespeech-asr_dev_phase2": kespeech_dev_phase2_cuts, + "wenetspeech-meeting_test": wenetspeech_test_meeting_cuts, + "wenetspeech-net_test": wenetspeech_test_net_cuts, + "wenetspeech_dev": wenetspeech_dev_cuts, + } diff --git a/egs/multi_zh-hans/ASR/zipformer/onnx_check.py b/egs/multi_zh-hans/ASR/zipformer/onnx_check.py new file mode 120000 index 000000000..f3dd42004 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/onnx_check.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/onnx_check.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/onnx_decode.py b/egs/multi_zh-hans/ASR/zipformer/onnx_decode.py new file mode 120000 index 000000000..0573b88c5 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/onnx_decode.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/onnx_decode.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/onnx_pretrained-streaming.py b/egs/multi_zh-hans/ASR/zipformer/onnx_pretrained-streaming.py new file mode 120000 index 000000000..cfea104c2 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/onnx_pretrained-streaming.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/onnx_pretrained-streaming.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/onnx_pretrained.py b/egs/multi_zh-hans/ASR/zipformer/onnx_pretrained.py new file mode 120000 index 000000000..8f32f4ee7 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/onnx_pretrained.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/onnx_pretrained.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/optim.py b/egs/multi_zh-hans/ASR/zipformer/optim.py new file mode 120000 index 000000000..5eaa3cffd --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/optim.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/optim.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/pretrained.py b/egs/multi_zh-hans/ASR/zipformer/pretrained.py new file mode 100755 index 000000000..69ff382da --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/pretrained.py @@ -0,0 +1,381 @@ +#!/usr/bin/env python3 +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, Zengwei Yao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script loads a checkpoint and uses it to decode waves. +You can generate the checkpoint with the following command: + +Note: This is a example for librispeech dataset, if you are using different +dataset, you should change the argument values according to your dataset. + +- For non-streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --tokens data/lang_bpe_2000/tokens.txt \ + --epoch 23 \ + --avg 1 + +- For streaming model: + +./zipformer/export.py \ + --exp-dir ./zipformer/exp \ + --causal 1 \ + --tokens data/lang_bpe_2000/tokens.txt \ + --epoch 23 \ + --avg 1 + +Usage of this script: + +- For non-streaming model: + +(1) greedy search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --tokens data/lang_bpe_2000/tokens.txt \ + --method greedy_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) modified beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --tokens ./data/lang_bpe_2000/tokens.txt \ + --method modified_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) fast beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --tokens ./data/lang_bpe_2000/tokens.txt \ + --method fast_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +- For streaming model: + +(1) greedy search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --tokens ./data/lang_bpe_2000/tokens.txt \ + --method greedy_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(2) modified beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --tokens ./data/lang_bpe_2000/tokens.txt \ + --method modified_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + +(3) fast beam search +./zipformer/pretrained.py \ + --checkpoint ./zipformer/exp/pretrained.pt \ + --causal 1 \ + --chunk-size 16 \ + --left-context-frames 128 \ + --tokens ./data/lang_bpe_2000/tokens.txt \ + --method fast_beam_search \ + /path/to/foo.wav \ + /path/to/bar.wav + + +You can also use `./zipformer/exp/epoch-xx.pt`. + +Note: ./zipformer/exp/pretrained.pt is generated by ./zipformer/export.py +""" + + +import argparse +import logging +import math +from typing import List + +import k2 +import kaldifeat +import torch +import torchaudio +from beam_search import ( + fast_beam_search_one_best, + greedy_search_batch, + modified_beam_search, +) +from export import num_tokens +from torch.nn.utils.rnn import pad_sequence +from train import add_model_arguments, get_model, get_params + +from icefall.utils import make_pad_mask + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--checkpoint", + type=str, + required=True, + help="Path to the checkpoint. " + "The checkpoint is assumed to be saved by " + "icefall.checkpoint.save_checkpoint().", + ) + + parser.add_argument( + "--tokens", + type=str, + help="""Path to tokens.txt.""", + ) + + parser.add_argument( + "--method", + type=str, + default="greedy_search", + help="""Possible values are: + - greedy_search + - modified_beam_search + - fast_beam_search + """, + ) + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. " + "The sample rate has to be 16kHz.", + ) + + parser.add_argument( + "--sample-rate", + type=int, + default=16000, + help="The sample rate of the input sound file", + ) + + parser.add_argument( + "--beam-size", + type=int, + default=4, + help="""An integer indicating how many candidates we will keep for each + frame. Used only when --method is beam_search or + modified_beam_search.""", + ) + + parser.add_argument( + "--beam", + type=float, + default=4, + help="""A floating point value to calculate the cutoff score during beam + search (i.e., `cutoff = max-score - beam`), which is the same as the + `beam` in Kaldi. + Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-contexts", + type=int, + default=4, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--max-states", + type=int, + default=8, + help="""Used only when --method is fast_beam_search""", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; 2 means tri-gram", + ) + + parser.add_argument( + "--max-sym-per-frame", + type=int, + default=1, + help="""Maximum number of symbols per frame. Used only when + --method is greedy_search. + """, + ) + + add_model_arguments(parser) + + return parser + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + assert ( + sample_rate == expected_sample_rate + ), f"expected sample rate: {expected_sample_rate}. Given: {sample_rate}" + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + params = get_params() + + params.update(vars(args)) + + token_table = k2.SymbolTable.from_file(params.tokens) + + params.blank_id = token_table[""] + params.unk_id = token_table[""] + params.vocab_size = num_tokens(token_table) + 1 + + logging.info(f"{params}") + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + if params.causal: + assert ( + "," not in params.chunk_size + ), "chunk_size should be one value in decoding." + assert ( + "," not in params.left_context_frames + ), "left_context_frames should be one value in decoding." + + logging.info("Creating model") + model = get_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + checkpoint = torch.load(args.checkpoint, map_location="cpu") + model.load_state_dict(checkpoint["model"], strict=False) + model.to(device) + model.eval() + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = params.sample_rate + opts.mel_opts.num_bins = params.feature_dim + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {params.sound_files}") + waves = read_sound_files( + filenames=params.sound_files, expected_sample_rate=params.sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.size(0) for f in features] + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + feature_lengths = torch.tensor(feature_lengths, device=device) + + # model forward + encoder_out, encoder_out_lens = model.forward_encoder(features, feature_lengths) + + hyps = [] + msg = f"Using {params.method}" + logging.info(msg) + + def token_ids_to_words(token_ids: List[int]) -> str: + text = "" + for i in token_ids: + text += token_table[i] + return text.replace("▁", " ").strip() + + if params.method == "fast_beam_search": + decoding_graph = k2.trivial_graph(params.vocab_size - 1, device=device) + hyp_tokens = fast_beam_search_one_best( + model=model, + decoding_graph=decoding_graph, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam, + max_contexts=params.max_contexts, + max_states=params.max_states, + ) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) + elif params.method == "modified_beam_search": + hyp_tokens = modified_beam_search( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + beam=params.beam_size, + ) + + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) + elif params.method == "greedy_search" and params.max_sym_per_frame == 1: + hyp_tokens = greedy_search_batch( + model=model, + encoder_out=encoder_out, + encoder_out_lens=encoder_out_lens, + ) + for hyp in hyp_tokens: + hyps.append(token_ids_to_words(hyp)) + else: + raise ValueError(f"Unsupported method: {params.method}") + + s = "\n" + for filename, hyp in zip(params.sound_files, hyps): + s += f"{filename}:\n{hyp}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/multi_zh-hans/ASR/zipformer/scaling.py b/egs/multi_zh-hans/ASR/zipformer/scaling.py new file mode 120000 index 000000000..6f398f431 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/scaling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/scaling.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/scaling_converter.py b/egs/multi_zh-hans/ASR/zipformer/scaling_converter.py new file mode 120000 index 000000000..b0ecee05e --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/scaling_converter.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/scaling_converter.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/streaming_beam_search.py b/egs/multi_zh-hans/ASR/zipformer/streaming_beam_search.py new file mode 120000 index 000000000..b1ed54557 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/streaming_beam_search.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/streaming_beam_search.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/streaming_decode.py b/egs/multi_zh-hans/ASR/zipformer/streaming_decode.py new file mode 120000 index 000000000..13fd02a78 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/streaming_decode.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/streaming_decode.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/subsampling.py b/egs/multi_zh-hans/ASR/zipformer/subsampling.py new file mode 120000 index 000000000..01ae9002c --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/subsampling.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/subsampling.py \ No newline at end of file diff --git a/egs/multi_zh-hans/ASR/zipformer/train.py b/egs/multi_zh-hans/ASR/zipformer/train.py new file mode 100755 index 000000000..4f2d728be --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/train.py @@ -0,0 +1,1385 @@ +#!/usr/bin/env python3 +# Copyright 2021-2023 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo, +# Zengwei Yao, +# Daniel Povey) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +# For non-streaming model training: +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --max-duration 1000 + +# For streaming model training: +./zipformer/train.py \ + --world-size 4 \ + --num-epochs 30 \ + --start-epoch 1 \ + --use-fp16 1 \ + --exp-dir zipformer/exp \ + --causal 1 \ + --max-duration 1000 + +It supports training with: + - transducer loss (default), with `--use-transducer True --use-ctc False` + - ctc loss (not recommended), with `--use-transducer False --use-ctc True` + - transducer loss & ctc loss, with `--use-transducer True --use-ctc True` +""" + + +import argparse +import copy +import logging +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import sentencepiece as spm +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import AsrDataModule +from decoder import Decoder +from joiner import Joiner +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from model import AsrModel +from multi_dataset import MultiDataset +from optim import Eden, ScaledAdam +from scaling import ScheduledFloat +from subsampling import Conv2dSubsampling +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter +from zipformer import Zipformer2 + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.hooks import register_inf_check_hooks +from icefall.utils import ( + AttributeDict, + MetricsTracker, + get_parameter_groups_with_lrs, + setup_logger, + str2bool, +) + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def get_adjusted_batch_count(params: AttributeDict) -> float: + # returns the number of batches we would have used so far if we had used the reference + # duration. This is for purposes of set_batch_count(). + return ( + params.batch_idx_train + * (params.max_duration * params.world_size) + / params.ref_duration + ) + + +def set_batch_count(model: Union[nn.Module, DDP], batch_count: float) -> None: + if isinstance(model, DDP): + # get underlying nn.Module + model = model.module + for name, module in model.named_modules(): + if hasattr(module, "batch_count"): + module.batch_count = batch_count + if hasattr(module, "name"): + module.name = name + + +def add_model_arguments(parser: argparse.ArgumentParser): + parser.add_argument( + "--num-encoder-layers", + type=str, + default="2,2,3,4,3,2", + help="Number of zipformer encoder layers per stack, comma separated.", + ) + + parser.add_argument( + "--downsampling-factor", + type=str, + default="1,2,4,8,4,2", + help="Downsampling factor for each stack of encoder layers.", + ) + + parser.add_argument( + "--feedforward-dim", + type=str, + default="512,768,1024,1536,1024,768", + help="Feedforward dimension of the zipformer encoder layers, per stack, comma separated.", + ) + + parser.add_argument( + "--num-heads", + type=str, + default="4,4,4,8,4,4", + help="Number of attention heads in the zipformer encoder layers: a single int or comma-separated list.", + ) + + parser.add_argument( + "--encoder-dim", + type=str, + default="192,256,384,512,384,256", + help="Embedding dimension in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--query-head-dim", + type=str, + default="32", + help="Query/key dimension per head in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--value-head-dim", + type=str, + default="12", + help="Value dimension per head in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--pos-head-dim", + type=str, + default="4", + help="Positional-encoding dimension per head in encoder stacks: a single int or comma-separated list.", + ) + + parser.add_argument( + "--pos-dim", + type=int, + default="48", + help="Positional-encoding embedding dimension", + ) + + parser.add_argument( + "--encoder-unmasked-dim", + type=str, + default="192,192,256,256,256,192", + help="Unmasked dimensions in the encoders, relates to augmentation during training. " + "A single int or comma-separated list. Must be <= each corresponding encoder_dim.", + ) + + parser.add_argument( + "--cnn-module-kernel", + type=str, + default="31,31,15,15,15,31", + help="Sizes of convolutional kernels in convolution modules in each encoder stack: " + "a single int or comma-separated list.", + ) + + parser.add_argument( + "--decoder-dim", + type=int, + default=512, + help="Embedding dimension in the decoder model.", + ) + + parser.add_argument( + "--joiner-dim", + type=int, + default=512, + help="""Dimension used in the joiner model. + Outputs from the encoder and decoder model are projected + to this dimension before adding. + """, + ) + + parser.add_argument( + "--causal", + type=str2bool, + default=False, + help="If True, use causal version of model.", + ) + + parser.add_argument( + "--chunk-size", + type=str, + default="16,32,64,-1", + help="Chunk sizes (at 50Hz frame rate) will be chosen randomly from this list during training. " + " Must be just -1 if --causal=False", + ) + + parser.add_argument( + "--left-context-frames", + type=str, + default="64,128,256,-1", + help="Maximum left-contexts for causal training, measured in frames which will " + "be converted to a number of chunks. If splitting into chunks, " + "chunk left-context frames will be chosen randomly from this list; else not relevant.", + ) + + parser.add_argument( + "--use-transducer", + type=str2bool, + default=True, + help="If True, use Transducer head.", + ) + + parser.add_argument( + "--use-ctc", + type=str2bool, + default=False, + help="If True, use CTC head.", + ) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=30, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="zipformer/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--bpe-model", + type=str, + default="data/lang_bpe_2000/bpe.model", + help="Path to the BPE model", + ) + + parser.add_argument( + "--base-lr", type=float, default=0.045, help="The base learning rate." + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=7500, + help="""Number of steps that affects how rapidly the learning rate + decreases. We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=3.5, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--ref-duration", + type=float, + default=600, + help="Reference batch duration for purposes of adjusting batch counts for setting various " + "schedules inside the model", + ) + + parser.add_argument( + "--context-size", + type=int, + default=2, + help="The context size in the decoder. 1 means bigram; " "2 means tri-gram", + ) + + parser.add_argument( + "--prune-range", + type=int, + default=5, + help="The prune range for rnnt loss, it means how many symbols(context)" + "we are using to compute the loss", + ) + + parser.add_argument( + "--lm-scale", + type=float, + default=0.25, + help="The scale to smooth the loss with lm " + "(output of prediction network) part.", + ) + + parser.add_argument( + "--am-scale", + type=float, + default=0.0, + help="The scale to smooth the loss with am (output of encoder network)" "part.", + ) + + parser.add_argument( + "--simple-loss-scale", + type=float, + default=0.5, + help="To get pruning ranges, we will calculate a simple version" + "loss(joiner is just addition), this simple loss also uses for" + "training (as a regularization item). We will scale the simple loss" + "with this parameter before adding to the final loss.", + ) + + parser.add_argument( + "--ctc-loss-scale", + type=float, + default=0.2, + help="Scale for CTC loss.", + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--inf-check", + type=str2bool, + default=False, + help="Add hooks to check for infinite module outputs and gradients.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=4000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 1. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=30, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=200, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + add_model_arguments(parser) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - warm_step: The warmup period that dictates the decay of the + scale on "simple" (un-pruned) loss. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 50, + "reset_interval": 200, + "valid_interval": 3000, # For the 100h subset, use 800 + # parameters for zipformer + "feature_dim": 80, + "subsampling_factor": 4, # not passed in, this is fixed. + "warm_step": 2000, + "env_info": get_env_info(), + } + ) + + return params + + +def _to_int_tuple(s: str): + return tuple(map(int, s.split(","))) + + +def get_encoder_embed(params: AttributeDict) -> nn.Module: + # encoder_embed converts the input of shape (N, T, num_features) + # to the shape (N, (T - 7) // 2, encoder_dims). + # That is, it does two things simultaneously: + # (1) subsampling: T -> (T - 7) // 2 + # (2) embedding: num_features -> encoder_dims + # In the normal configuration, we will downsample once more at the end + # by a factor of 2, and most of the encoder stacks will run at a lower + # sampling rate. + encoder_embed = Conv2dSubsampling( + in_channels=params.feature_dim, + out_channels=_to_int_tuple(params.encoder_dim)[0], + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + ) + return encoder_embed + + +def get_encoder_model(params: AttributeDict) -> nn.Module: + encoder = Zipformer2( + output_downsampling_factor=2, + downsampling_factor=_to_int_tuple(params.downsampling_factor), + num_encoder_layers=_to_int_tuple(params.num_encoder_layers), + encoder_dim=_to_int_tuple(params.encoder_dim), + encoder_unmasked_dim=_to_int_tuple(params.encoder_unmasked_dim), + query_head_dim=_to_int_tuple(params.query_head_dim), + pos_head_dim=_to_int_tuple(params.pos_head_dim), + value_head_dim=_to_int_tuple(params.value_head_dim), + pos_dim=params.pos_dim, + num_heads=_to_int_tuple(params.num_heads), + feedforward_dim=_to_int_tuple(params.feedforward_dim), + cnn_module_kernel=_to_int_tuple(params.cnn_module_kernel), + dropout=ScheduledFloat((0.0, 0.3), (20000.0, 0.1)), + warmup_batches=4000.0, + causal=params.causal, + chunk_size=_to_int_tuple(params.chunk_size), + left_context_frames=_to_int_tuple(params.left_context_frames), + ) + return encoder + + +def get_decoder_model(params: AttributeDict) -> nn.Module: + decoder = Decoder( + vocab_size=params.vocab_size, + decoder_dim=params.decoder_dim, + blank_id=params.blank_id, + context_size=params.context_size, + ) + return decoder + + +def get_joiner_model(params: AttributeDict) -> nn.Module: + joiner = Joiner( + encoder_dim=max(_to_int_tuple(params.encoder_dim)), + decoder_dim=params.decoder_dim, + joiner_dim=params.joiner_dim, + vocab_size=params.vocab_size, + ) + return joiner + + +def get_model(params: AttributeDict) -> nn.Module: + assert params.use_transducer or params.use_ctc, ( + f"At least one of them should be True, " + f"but got params.use_transducer={params.use_transducer}, " + f"params.use_ctc={params.use_ctc}" + ) + + encoder_embed = get_encoder_embed(params) + encoder = get_encoder_model(params) + + if params.use_transducer: + decoder = get_decoder_model(params) + joiner = get_joiner_model(params) + else: + decoder = None + joiner = None + + model = AsrModel( + encoder_embed=encoder_embed, + encoder=encoder, + decoder=decoder, + joiner=joiner, + encoder_dim=max(_to_int_tuple(params.encoder_dim)), + decoder_dim=params.decoder_dim, + vocab_size=params.vocab_size, + use_transducer=params.use_transducer, + use_ctc=params.use_ctc, + ) + return model + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + batch: dict, + is_training: bool, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Zipformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + batch_idx_train = params.batch_idx_train + warm_step = params.warm_step + + texts = batch["supervisions"]["text"] + y = sp.encode(texts, out_type=int) + y = k2.RaggedTensor(y) + + with torch.set_grad_enabled(is_training): + simple_loss, pruned_loss, ctc_loss = model( + x=feature, + x_lens=feature_lens, + y=y, + prune_range=params.prune_range, + am_scale=params.am_scale, + lm_scale=params.lm_scale, + ) + + loss = 0.0 + + if params.use_transducer: + s = params.simple_loss_scale + # take down the scale on the simple loss from 1.0 at the start + # to params.simple_loss scale by warm_step. + simple_loss_scale = ( + s + if batch_idx_train >= warm_step + else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) + ) + pruned_loss_scale = ( + 1.0 + if batch_idx_train >= warm_step + else 0.1 + 0.9 * (batch_idx_train / warm_step) + ) + loss += simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss + + if params.use_ctc: + loss += params.ctc_loss_scale * ctc_loss + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + if params.use_transducer: + info["simple_loss"] = simple_loss.detach().cpu().item() + info["pruned_loss"] = pruned_loss.detach().cpu().item() + if params.use_ctc: + info["ctc_loss"] = ctc_loss.detach().cpu().item() + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + sp: spm.SentencePieceProcessor, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + scheduler: LRSchedulerType, + sp: spm.SentencePieceProcessor, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + saved_bad_model = False + + def save_bad_model(suffix: str = ""): + save_checkpoint_impl( + filename=params.exp_dir / f"bad-model{suffix}-{rank}.pt", + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=0, + ) + + for batch_idx, batch in enumerate(train_dl): + if batch_idx % 10 == 0: + set_batch_count(model, get_adjusted_batch_count(params)) + + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + scaler.scale(loss).backward() + scheduler.step_batch(params.batch_idx_train) + + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + except: # noqa + save_bad_model() + display_and_save_batch(batch, params=params, sp=sp) + raise + + if params.print_diagnostics and batch_idx == 5: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % 100 == 0 and params.use_fp16: + # If the grad scale was less than 1, try increasing it. The _growth_interval + # of the grad scaler is configurable, but we can't configure it to have different + # behavior depending on the current grad scale. + cur_grad_scale = scaler._scale.item() + + if cur_grad_scale < 8.0 or (cur_grad_scale < 32.0 and batch_idx % 400 == 0): + scaler.update(cur_grad_scale * 2.0) + if cur_grad_scale < 0.01: + if not saved_bad_model: + save_bad_model(suffix="-first-warning") + saved_bad_model = True + logging.warning(f"Grad scale is small: {cur_grad_scale}") + if cur_grad_scale < 1.0e-05: + save_bad_model() + raise RuntimeError( + f"grad_scale is too small, exiting: {cur_grad_scale}" + ) + + if batch_idx % params.log_interval == 0: + cur_lr = max(scheduler.get_last_lr()) + cur_grad_scale = scaler._scale.item() if params.use_fp16 else 1.0 + + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}, " + + (f"grad_scale: {scaler._scale.item()}" if params.use_fp16 else "") + ) + + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + if params.use_fp16: + tb_writer.add_scalar( + "train/grad_scale", cur_grad_scale, params.batch_idx_train + ) + + if batch_idx % params.valid_interval == 0 and not params.print_diagnostics: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + sp=sp, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + logging.info(f"Device: {device}") + + sp = spm.SentencePieceProcessor() + sp.load(params.bpe_model) + + # is defined in local/train_bpe_model.py + params.blank_id = sp.piece_to_id("") + params.vocab_size = sp.get_piece_size() + + if not params.use_transducer: + params.ctc_loss_scale = 1.0 + + logging.info(params) + + logging.info("About to create model") + model = get_model(params) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model).to(torch.float64) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank], find_unused_parameters=True) + + optimizer = ScaledAdam( + get_parameter_groups_with_lrs(model, lr=params.base_lr, include_names=True), + lr=params.base_lr, # should have no effect + clipping_scale=2.0, + ) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + opts = diagnostics.TensorDiagnosticOptions( + 2**22 + ) # allow 4 megabytes per sub-module + diagnostic = diagnostics.attach_diagnostics(model, opts) + + if params.inf_check: + register_inf_check_hooks(model) + + data_module = AsrDataModule(args) + multi_dataset = MultiDataset(args.manifest_dir) + + train_cuts = multi_dataset.train_cuts() + + def remove_short_and_long_utt(c: Cut): + # Keep only utterances with duration between 1 second and 20 seconds + # + # Caution: There is a reason to select 20.0 here. Please see + # ../local/display_manifest_statistics.py + # + # You should use ../local/display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + if c.duration < 1.0 or c.duration > 20.0: + # logging.warning( + # f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + # ) + return False + + # In pruned RNN-T, we require that T >= S + # where T is the number of feature frames after subsampling + # and S is the number of tokens in the utterance + + # In ./zipformer.py, the conv module uses the following expression + # for subsampling + T = ((c.num_frames - 7) // 2 + 1) // 2 + tokens = sp.encode(c.supervisions[0].text, out_type=str) + + if T < len(tokens): + logging.warning( + f"Exclude cut with ID {c.id} from training. " + f"Number of frames (before subsampling): {c.num_frames}. " + f"Number of frames (after subsampling): {T}. " + f"Text: {c.supervisions[0].text}. " + f"Tokens: {tokens}. " + f"Number of tokens: {len(tokens)}" + ) + return False + + return True + + train_cuts = train_cuts.filter(remove_short_and_long_utt) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = data_module.train_dataloaders( + train_cuts, sampler_state_dict=sampler_state_dict + ) + + valid_cuts = multi_dataset.dev_cuts() + valid_dl = data_module.valid_dataloaders(valid_cuts) + + if not params.print_diagnostics: + scan_pessimistic_batches_for_oom( + model=model, + train_dl=train_dl, + optimizer=optimizer, + sp=sp, + params=params, + ) + + scaler = GradScaler(enabled=params.use_fp16, init_scale=1.0) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sp=sp, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def display_and_save_batch( + batch: dict, + params: AttributeDict, + sp: spm.SentencePieceProcessor, +) -> None: + """Display the batch statistics and save the batch into disk. + + Args: + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + params: + Parameters for training. See :func:`get_params`. + sp: + The BPE model. + """ + from lhotse.utils import uuid4 + + filename = f"{params.exp_dir}/batch-{uuid4()}.pt" + logging.info(f"Saving batch to {filename}") + torch.save(batch, filename) + + supervisions = batch["supervisions"] + features = batch["inputs"] + + logging.info(f"features shape: {features.shape}") + + y = sp.encode(supervisions["text"], out_type=int) + num_tokens = sum(len(i) for i in y) + logging.info(f"num tokens: {num_tokens}") + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + sp: spm.SentencePieceProcessor, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + sp=sp, + batch=batch, + is_training=True, + ) + loss.backward() + optimizer.zero_grad() + except Exception as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + display_and_save_batch(batch, params=params, sp=sp) + raise + logging.info( + f"Maximum memory allocated so far is {torch.cuda.max_memory_allocated()//1000000}MB" + ) + + +def main(): + parser = get_parser() + AsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/multi_zh-hans/ASR/zipformer/zipformer.py b/egs/multi_zh-hans/ASR/zipformer/zipformer.py new file mode 120000 index 000000000..23011dda7 --- /dev/null +++ b/egs/multi_zh-hans/ASR/zipformer/zipformer.py @@ -0,0 +1 @@ +../../../librispeech/ASR/zipformer/zipformer.py \ No newline at end of file From 7cc2dae9409c76e54ef32b31fe647c5b30409cea Mon Sep 17 00:00:00 2001 From: zr_jin Date: Wed, 13 Sep 2023 12:39:49 +0800 Subject: [PATCH 082/100] Fixes to incorporate with the latest Lhotse release (#1249) --- .../ASR/pruned_transducer_stateless2/asr_datamodule.py | 6 +++--- egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py | 6 +++--- .../ASR/pruned_transducer_stateless5/asr_datamodule.py | 6 +++--- .../ASR/pruned_transducer_stateless5/asr_datamodule.py | 6 +++--- .../ASR/pruned_transducer_stateless2/asr_datamodule.py | 6 +++--- egs/ami/SURT/dprnn_zipformer/asr_datamodule.py | 2 +- .../ASR/pruned_transducer_stateless7/asr_datamodule.py | 6 +++--- egs/csj/ASR/local/utils/asr_datamodule.py | 6 +++--- egs/gigaspeech/ASR/conformer_ctc/asr_datamodule.py | 6 +++--- .../ASR/pruned_transducer_stateless2/asr_datamodule.py | 6 +++--- egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py | 2 +- .../ASR/pruned2_knowledge/asr_datamodule.py | 6 +++--- .../ASR/pruned_transducer_stateless7/gigaspeech.py | 6 +++--- egs/librispeech/ASR/tdnn_lstm_ctc/asr_datamodule.py | 6 +++--- egs/mgb2/ASR/conformer_ctc/asr_datamodule.py | 6 +++--- egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py | 6 +++--- .../ASR/pruned_transducer_stateless5/asr_datamodule.py | 6 +++--- .../ASR/transducer_stateless/asr_datamodule.py | 8 +++----- egs/timit/ASR/tdnn_lstm_ctc/asr_datamodule.py | 10 +++++----- .../ASR/pruned_transducer_stateless2/asr_datamodule.py | 6 +++--- .../ASR/pruned_transducer_stateless5/asr_datamodule.py | 6 +++--- egs/yesno/ASR/tdnn/asr_datamodule.py | 6 +++--- requirements-ci.txt | 1 + test/test_ali.py | 4 ++-- 24 files changed, 67 insertions(+), 68 deletions(-) diff --git a/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/asr_datamodule.py b/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/asr_datamodule.py index 167d5e15e..49a697bfd 100644 --- a/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/asr_datamodule.py +++ b/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/asr_datamodule.py @@ -37,7 +37,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -291,8 +291,8 @@ class Aidatatang_200zhAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py b/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py index efb32336a..180930747 100644 --- a/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py +++ b/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py @@ -30,7 +30,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -278,8 +278,8 @@ class AishellAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/aishell2/ASR/pruned_transducer_stateless5/asr_datamodule.py b/egs/aishell2/ASR/pruned_transducer_stateless5/asr_datamodule.py index 0f383a244..af37cc175 100644 --- a/egs/aishell2/ASR/pruned_transducer_stateless5/asr_datamodule.py +++ b/egs/aishell2/ASR/pruned_transducer_stateless5/asr_datamodule.py @@ -31,7 +31,7 @@ from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import ( # noqa F401 For AudioSamples @@ -299,8 +299,8 @@ class AiShell2AsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/aishell4/ASR/pruned_transducer_stateless5/asr_datamodule.py b/egs/aishell4/ASR/pruned_transducer_stateless5/asr_datamodule.py index d980a857f..da9da371e 100644 --- a/egs/aishell4/ASR/pruned_transducer_stateless5/asr_datamodule.py +++ b/egs/aishell4/ASR/pruned_transducer_stateless5/asr_datamodule.py @@ -30,7 +30,7 @@ from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import ( # noqa F401 for AudioSamples @@ -310,8 +310,8 @@ class Aishell4AsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/alimeeting/ASR/pruned_transducer_stateless2/asr_datamodule.py b/egs/alimeeting/ASR/pruned_transducer_stateless2/asr_datamodule.py index a9a4675a9..4799da19d 100644 --- a/egs/alimeeting/ASR/pruned_transducer_stateless2/asr_datamodule.py +++ b/egs/alimeeting/ASR/pruned_transducer_stateless2/asr_datamodule.py @@ -37,7 +37,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -292,8 +292,8 @@ class AlimeetingAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/ami/SURT/dprnn_zipformer/asr_datamodule.py b/egs/ami/SURT/dprnn_zipformer/asr_datamodule.py index ec8106bc3..3dd786d33 100644 --- a/egs/ami/SURT/dprnn_zipformer/asr_datamodule.py +++ b/egs/ami/SURT/dprnn_zipformer/asr_datamodule.py @@ -257,7 +257,7 @@ class AmiAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") + logging.info("Using SimpleCutSampler.") train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, diff --git a/egs/commonvoice/ASR/pruned_transducer_stateless7/asr_datamodule.py b/egs/commonvoice/ASR/pruned_transducer_stateless7/asr_datamodule.py index 2c37244a4..73f2f1dce 100644 --- a/egs/commonvoice/ASR/pruned_transducer_stateless7/asr_datamodule.py +++ b/egs/commonvoice/ASR/pruned_transducer_stateless7/asr_datamodule.py @@ -30,7 +30,7 @@ from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import ( # noqa F401 For AudioSamples @@ -311,8 +311,8 @@ class CommonVoiceAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/csj/ASR/local/utils/asr_datamodule.py b/egs/csj/ASR/local/utils/asr_datamodule.py index 619820a75..272486227 100644 --- a/egs/csj/ASR/local/utils/asr_datamodule.py +++ b/egs/csj/ASR/local/utils/asr_datamodule.py @@ -31,7 +31,7 @@ from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import ( # noqa F401 For AudioSamples @@ -339,8 +339,8 @@ class CSJAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/gigaspeech/ASR/conformer_ctc/asr_datamodule.py b/egs/gigaspeech/ASR/conformer_ctc/asr_datamodule.py index 9437c935c..9d6e3c42a 100644 --- a/egs/gigaspeech/ASR/conformer_ctc/asr_datamodule.py +++ b/egs/gigaspeech/ASR/conformer_ctc/asr_datamodule.py @@ -27,7 +27,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -264,8 +264,8 @@ class GigaSpeechAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/gigaspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py b/egs/gigaspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py index 4d5d2b8f9..29e72b408 100644 --- a/egs/gigaspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py +++ b/egs/gigaspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py @@ -30,7 +30,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -297,8 +297,8 @@ class GigaSpeechAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py b/egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py index 51df91598..a72df89e0 100644 --- a/egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py +++ b/egs/libricss/SURT/dprnn_zipformer/asr_datamodule.py @@ -259,7 +259,7 @@ class LibriCssAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") + logging.info("Using SimpleCutSampler.") train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, diff --git a/egs/librispeech/ASR/pruned2_knowledge/asr_datamodule.py b/egs/librispeech/ASR/pruned2_knowledge/asr_datamodule.py index b839a4a4c..f8f558ce1 100644 --- a/egs/librispeech/ASR/pruned2_knowledge/asr_datamodule.py +++ b/egs/librispeech/ASR/pruned2_knowledge/asr_datamodule.py @@ -31,7 +31,7 @@ from lhotse.dataset import ( CutMix, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -290,8 +290,8 @@ class LibriSpeechAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/gigaspeech.py b/egs/librispeech/ASR/pruned_transducer_stateless7/gigaspeech.py index 5c01d7190..75e153cb0 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/gigaspeech.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/gigaspeech.py @@ -30,7 +30,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -297,8 +297,8 @@ class GigaSpeechAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/librispeech/ASR/tdnn_lstm_ctc/asr_datamodule.py b/egs/librispeech/ASR/tdnn_lstm_ctc/asr_datamodule.py index c47964b07..20df469da 100644 --- a/egs/librispeech/ASR/tdnn_lstm_ctc/asr_datamodule.py +++ b/egs/librispeech/ASR/tdnn_lstm_ctc/asr_datamodule.py @@ -31,7 +31,7 @@ from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import ( # noqa F401 For AudioSamples @@ -314,8 +314,8 @@ class LibriSpeechAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/mgb2/ASR/conformer_ctc/asr_datamodule.py b/egs/mgb2/ASR/conformer_ctc/asr_datamodule.py index 8242e986d..442ff85c2 100644 --- a/egs/mgb2/ASR/conformer_ctc/asr_datamodule.py +++ b/egs/mgb2/ASR/conformer_ctc/asr_datamodule.py @@ -17,7 +17,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -270,8 +270,8 @@ class MGB2AsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py b/egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py index b1b7bff93..3d58ebf3a 100644 --- a/egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py +++ b/egs/multi_zh-hans/ASR/zipformer/asr_datamodule.py @@ -31,7 +31,7 @@ from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import ( # noqa F401 For AudioSamples @@ -300,8 +300,8 @@ class AsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless5/asr_datamodule.py b/egs/tal_csasr/ASR/pruned_transducer_stateless5/asr_datamodule.py index 2240c1c1d..39beffdcf 100644 --- a/egs/tal_csasr/ASR/pruned_transducer_stateless5/asr_datamodule.py +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless5/asr_datamodule.py @@ -30,7 +30,7 @@ from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import ( # noqa F401 for AudioSamples @@ -311,8 +311,8 @@ class TAL_CSASRAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/tedlium3/ASR/transducer_stateless/asr_datamodule.py b/egs/tedlium3/ASR/transducer_stateless/asr_datamodule.py index c647392f0..28d0d3826 100644 --- a/egs/tedlium3/ASR/transducer_stateless/asr_datamodule.py +++ b/egs/tedlium3/ASR/transducer_stateless/asr_datamodule.py @@ -28,7 +28,7 @@ from lhotse.dataset import ( CutMix, DynamicBucketingSampler, K2SpeechRecognitionDataset, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -259,8 +259,8 @@ class TedLiumAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, @@ -282,7 +282,6 @@ class TedLiumAsrDataModule: return train_dl def valid_dataloaders(self, cuts_valid: CutSet) -> DataLoader: - transforms = [] if self.args.concatenate_cuts: transforms = [ @@ -322,7 +321,6 @@ class TedLiumAsrDataModule: return valid_dl def test_dataloaders(self, cuts_test: CutSet) -> DataLoader: - logging.debug("About to create test dataset") if self.args.on_the_fly_feats: test = K2SpeechRecognitionDataset( diff --git a/egs/timit/ASR/tdnn_lstm_ctc/asr_datamodule.py b/egs/timit/ASR/tdnn_lstm_ctc/asr_datamodule.py index 51ca4cc6e..7c299d601 100644 --- a/egs/timit/ASR/tdnn_lstm_ctc/asr_datamodule.py +++ b/egs/timit/ASR/tdnn_lstm_ctc/asr_datamodule.py @@ -30,7 +30,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -225,8 +225,8 @@ class TimitAsrDataModule(DataModule): drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, @@ -267,7 +267,7 @@ class TimitAsrDataModule(DataModule): cut_transforms=transforms, return_cuts=self.args.return_cuts, ) - valid_sampler = SingleCutSampler( + valid_sampler = SimpleCutSampler( cuts_valid, max_duration=self.args.max_duration, shuffle=False, @@ -298,7 +298,7 @@ class TimitAsrDataModule(DataModule): else PrecomputedFeatures(), return_cuts=self.args.return_cuts, ) - sampler = SingleCutSampler(cuts_test, max_duration=self.args.max_duration) + sampler = SimpleCutSampler(cuts_test, max_duration=self.args.max_duration) logging.debug("About to create test dataloader") test_dl = DataLoader(test, batch_size=None, sampler=sampler, num_workers=1) test_loaders.append(test_dl) diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py index 746b212ff..c5967f10a 100644 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py @@ -37,7 +37,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures @@ -296,8 +296,8 @@ class WenetSpeechAsrDataModule: drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/asr_datamodule.py b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/asr_datamodule.py index 55d5f4636..6362ab7cd 100644 --- a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/asr_datamodule.py +++ b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/asr_datamodule.py @@ -32,7 +32,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, SpecAugment, ) from lhotse.dataset.input_strategies import AudioSamples # noqa F401 For AudioSamples @@ -299,8 +299,8 @@ class Xbmu_AmdoAsrDataModule: drop_last=self.args.drop_last, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/egs/yesno/ASR/tdnn/asr_datamodule.py b/egs/yesno/ASR/tdnn/asr_datamodule.py index ada8c1a6c..dc66b217d 100644 --- a/egs/yesno/ASR/tdnn/asr_datamodule.py +++ b/egs/yesno/ASR/tdnn/asr_datamodule.py @@ -26,7 +26,7 @@ from lhotse.dataset import ( DynamicBucketingSampler, K2SpeechRecognitionDataset, PrecomputedFeatures, - SingleCutSampler, + SimpleCutSampler, ) from lhotse.dataset.input_strategies import OnTheFlyFeatures from torch.utils.data import DataLoader @@ -196,8 +196,8 @@ class YesNoAsrDataModule(DataModule): drop_last=True, ) else: - logging.info("Using SingleCutSampler.") - train_sampler = SingleCutSampler( + logging.info("Using SimpleCutSampler.") + train_sampler = SimpleCutSampler( cuts_train, max_duration=self.args.max_duration, shuffle=self.args.shuffle, diff --git a/requirements-ci.txt b/requirements-ci.txt index 21d33001c..2433e190b 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -10,6 +10,7 @@ graphviz==0.19.1 -f https://download.pytorch.org/whl/cpu/torch_stable.html torch==1.13.1+cpu -f https://download.pytorch.org/whl/cpu/torch_stable.html torchaudio==0.13.1+cpu +six -f https://k2-fsa.org/nightly/ k2==1.23.4.dev20230319+cpu.torch1.13.1 diff --git a/test/test_ali.py b/test/test_ali.py index b107a6d80..d607e40aa 100755 --- a/test/test_ali.py +++ b/test/test_ali.py @@ -26,7 +26,7 @@ from pathlib import Path from lhotse import CutSet, load_manifest -from lhotse.dataset import K2SpeechRecognitionDataset, SingleCutSampler +from lhotse.dataset import K2SpeechRecognitionDataset, SimpleCutSampler from lhotse.dataset.collation import collate_custom_field from torch.utils.data import DataLoader @@ -44,7 +44,7 @@ def get_dataloader(): cuts = load_manifest(cuts_json) print(cuts[0]) cuts = cuts.with_features_path_prefix(egs_dir) - sampler = SingleCutSampler( + sampler = SimpleCutSampler( cuts, max_duration=10, shuffle=False, From fba17106228badbc77c5aa75c1a1263877067906 Mon Sep 17 00:00:00 2001 From: docterstrange <44291127+docterstrange@users.noreply.github.com> Date: Thu, 14 Sep 2023 09:58:28 +0800 Subject: [PATCH 083/100] modify tal_csasr recipe (#1252) Co-authored-by: zss11 --- egs/tal_csasr/ASR/pruned_transducer_stateless5/decode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless5/decode.py b/egs/tal_csasr/ASR/pruned_transducer_stateless5/decode.py index 3bfb832fb..3485d4005 100755 --- a/egs/tal_csasr/ASR/pruned_transducer_stateless5/decode.py +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless5/decode.py @@ -724,12 +724,12 @@ def main(): ) save_results( params=params, - test_set_name=test_set, + test_set_name=test_set + "-zh", results_dict=zh_results_dict, ) save_results( params=params, - test_set_name=test_set, + test_set_name=test_set + "-en", results_dict=en_results_dict, ) From 565d2c2f5b920a4ea16be3c6ea04802c2350691a Mon Sep 17 00:00:00 2001 From: zr_jin Date: Fri, 15 Sep 2023 02:37:53 +0800 Subject: [PATCH 084/100] Minor fixes to the libricss recipe (#1256) --- egs/libricss/SURT/prepare.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/egs/libricss/SURT/prepare.sh b/egs/libricss/SURT/prepare.sh index 028240e44..3d2581d96 100755 --- a/egs/libricss/SURT/prepare.sh +++ b/egs/libricss/SURT/prepare.sh @@ -79,7 +79,7 @@ if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then # ln -sfv /path/to/rirs_noises $dl_dir/ # if [ ! -d $dl_dir/rirs_noises ]; then - lhotse download rirs_noises $dl_dir + lhotse download rir-noise $dl_dir/rirs_noises fi fi @@ -89,6 +89,7 @@ if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then # to $dl_dir/librispeech. We perform text normalization for the transcripts. # NOTE: Alignments are required for this recipe. mkdir -p data/manifests + lhotse prepare librispeech -p train-clean-100 -p train-clean-360 -p train-other-500 -p dev-clean \ -j 4 --alignments-dir $dl_dir/libri_alignments/LibriSpeech $dl_dir/librispeech data/manifests/ fi @@ -112,7 +113,7 @@ if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then # We assume that you have downloaded the RIRS_NOISES corpus # to $dl_dir/rirs_noises - lhotse prepare rir-noise -p real_rir -p iso_noise $dl_dir/rirs_noises data/manifests + lhotse prepare rir-noise -p real_rir -p iso_noise $dl_dir/rirs_noises/RIRS_NOISES data/manifests fi if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then From 0c564c6c812bee08ebe7fa402f1668883b7847f3 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sun, 17 Sep 2023 13:25:37 +0900 Subject: [PATCH 085/100] Fix typo in README.md (#1257) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a876fb24e..523203aa4 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ We provide one model for this recipe: [Pruned stateless RNN-T: Conformer encoder #### Pruned stateless RNN-T: Conformer encoder + Embedding decoder + k2 pruned RNN-T loss -The best results for Chinese CER(%) and English WER(%) respectivly (zh: Chinese, en: English): +The best results for Chinese CER(%) and English WER(%) respectively (zh: Chinese, en: English): |decoding-method | dev | dev_zh | dev_en | test | test_zh | test_en | |--|--|--|--|--|--|--| |greedy_search| 7.30 | 6.48 | 19.19 |7.39| 6.66 | 19.13| From 7e1288af50e699a1c09ad3c6acdd58b9765c5745 Mon Sep 17 00:00:00 2001 From: Tiance Wang Date: Tue, 19 Sep 2023 16:46:36 +0800 Subject: [PATCH 086/100] fix thchs-30 download command (#1260) --- egs/multi_zh-hans/ASR/prepare.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egs/multi_zh-hans/ASR/prepare.sh b/egs/multi_zh-hans/ASR/prepare.sh index 5d0fe66a4..c09b9c1de 100755 --- a/egs/multi_zh-hans/ASR/prepare.sh +++ b/egs/multi_zh-hans/ASR/prepare.sh @@ -49,7 +49,7 @@ if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then log "Stage 2: Prepare THCHS-30" if [ ! -d $dl_dir/thchs30 ]; then log "Downloading THCHS-30" - lhotse download thchs30 $dl_dir/thchs30 + lhotse download thchs-30 $dl_dir/thchs30 fi if [ ! -f data/manifests/.thchs30.done ]; then From bbb03f7962eda9519e0947b92bb573140fdb2a04 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Wed, 20 Sep 2023 08:15:54 +0800 Subject: [PATCH 087/100] Update decoder.py (#1262) --- egs/librispeech/ASR/pruned_transducer_stateless7/decoder.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/decoder.py b/egs/librispeech/ASR/pruned_transducer_stateless7/decoder.py index b085a1817..bfd019ff5 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/decoder.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/decoder.py @@ -71,6 +71,10 @@ class Decoder(nn.Module): groups=decoder_dim // 4, # group size == 4 bias=False, ) + else: + # To avoid `RuntimeError: Module 'Decoder' has no attribute 'conv'` + # when inference with torch.jit.script and context_size == 1 + self.conv = nn.Identity() def forward(self, y: torch.Tensor, need_pad: bool = True) -> torch.Tensor: """ From 45d60ef262fe65fa1a63cbdd7b89658b359f7724 Mon Sep 17 00:00:00 2001 From: l2009312042 Date: Thu, 21 Sep 2023 19:41:10 +0800 Subject: [PATCH 088/100] Update conformer.py (#1200) * Update conformer.py * Update zipformer.py fix bug in get_dynamic_dropout_rate --- .../ASR/pruned_transducer_stateless7_streaming/zipformer.py | 2 +- egs/librispeech/ASR/streaming_conformer_ctc/conformer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer.py index a5c422959..c7e45564f 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/zipformer.py @@ -865,7 +865,7 @@ class ZipformerEncoderLayer(nn.Module): return final_dropout_rate else: return initial_dropout_rate - ( - initial_dropout_rate * final_dropout_rate + initial_dropout_rate - final_dropout_rate ) * (self.batch_count / warmup_period) def forward( diff --git a/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py b/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py index 5fe92172e..be6fabf35 100644 --- a/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py +++ b/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py @@ -230,7 +230,7 @@ class Conformer(Transformer): x, pos_emb, mask=mask, src_key_padding_mask=src_key_padding_mask ) # (T, B, F) else: - x = self.encoder(x, pos_emb, src_key_padding_mask=mask) # (T, B, F) + x = self.encoder(x, pos_emb, src_key_padding_mask=src_key_padding_mask) # (T, B, F) if self.normalize_before: x = self.after_norm(x) From f5dc957d44350ea0ec9adb81578c32af5e6bb809 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Thu, 21 Sep 2023 21:16:14 +0800 Subject: [PATCH 089/100] Fix CI tests (#1266) --- .../ASR/pruned_transducer_stateless7/onnx_pretrained.py | 3 +++ .../ASR/pruned_transducer_stateless7/onnx_pretrained.py | 3 +++ .../onnx_pretrained.py | 3 +++ .../ASR/lstm_transducer_stateless2/onnx_pretrained.py | 3 +++ .../lstm_transducer_stateless2/streaming-onnx-decode.py | 5 +++++ .../ASR/pruned_transducer_stateless3/onnx_pretrained.py | 3 +++ .../ASR/pruned_transducer_stateless3/test_onnx.py | 5 +++++ .../onnx_pretrained-streaming.py | 3 +++ .../ASR/pruned_transducer_stateless7/test_onnx.py | 5 +++++ .../onnx_pretrained.py | 8 ++++++++ .../onnx_pretrained.py | 3 +++ .../ASR/zipformer/onnx_pretrained-streaming.py | 3 +++ egs/librispeech/ASR/zipformer/onnx_pretrained.py | 3 +++ .../ASR/pruned_transducer_stateless2/onnx_check.py | 5 +++++ .../onnx_pretrained-streaming.py | 3 +++ .../ASR/pruned_transducer_stateless5/onnx_pretrained.py | 3 +++ egs/yesno/ASR/tdnn/onnx_pretrained.py | 1 + 17 files changed, 62 insertions(+) diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py b/egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py index 5adb6c16a..a92182e8d 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/onnx_pretrained.py @@ -151,12 +151,14 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_decoder(self, decoder_model_filename: str): self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -170,6 +172,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/commonvoice/ASR/pruned_transducer_stateless7/onnx_pretrained.py b/egs/commonvoice/ASR/pruned_transducer_stateless7/onnx_pretrained.py index eee19191e..cf6ddfa36 100755 --- a/egs/commonvoice/ASR/pruned_transducer_stateless7/onnx_pretrained.py +++ b/egs/commonvoice/ASR/pruned_transducer_stateless7/onnx_pretrained.py @@ -152,12 +152,14 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_decoder(self, decoder_model_filename: str): self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -171,6 +173,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py index 5d7e2dfcd..a6c69d54f 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/onnx_pretrained.py @@ -136,6 +136,7 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) self.init_encoder_states() @@ -184,6 +185,7 @@ class OnnxModel: self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -197,6 +199,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/onnx_pretrained.py b/egs/librispeech/ASR/lstm_transducer_stateless2/onnx_pretrained.py index fb9e121e5..06159e56a 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/onnx_pretrained.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/onnx_pretrained.py @@ -129,6 +129,7 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) self.init_encoder_states() @@ -166,6 +167,7 @@ class OnnxModel: self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -179,6 +181,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/streaming-onnx-decode.py b/egs/librispeech/ASR/lstm_transducer_stateless2/streaming-onnx-decode.py index 34d2e5630..487fc2114 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/streaming-onnx-decode.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/streaming-onnx-decode.py @@ -172,30 +172,35 @@ class Model: self.encoder = ort.InferenceSession( args.encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_decoder(self, args): self.decoder = ort.InferenceSession( args.decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_joiner(self, args): self.joiner = ort.InferenceSession( args.joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_joiner_encoder_proj(self, args): self.joiner_encoder_proj = ort.InferenceSession( args.joiner_encoder_proj_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_joiner_decoder_proj(self, args): self.joiner_decoder_proj = ort.InferenceSession( args.joiner_decoder_proj_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def run_encoder(self, x, h0, c0) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/onnx_pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless3/onnx_pretrained.py index e10915086..de3e03da6 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/onnx_pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/onnx_pretrained.py @@ -150,12 +150,14 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_decoder(self, decoder_model_filename: str): self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -169,6 +171,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py index 810da8da6..b98248128 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/test_onnx.py @@ -78,6 +78,7 @@ def test_conv2d_subsampling(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -133,6 +134,7 @@ def test_rel_pos(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -220,6 +222,7 @@ def test_conformer_encoder_layer(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -304,6 +307,7 @@ def test_conformer_encoder(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -359,6 +363,7 @@ def test_conformer(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py b/egs/librispeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py index 29be4c655..6e290e799 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py @@ -138,6 +138,7 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) self.init_encoder_states() @@ -185,6 +186,7 @@ class OnnxModel: self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -198,6 +200,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py index 1e9b67226..f3f7b1ea9 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/test_onnx.py @@ -74,6 +74,7 @@ def test_conv2d_subsampling(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -128,6 +129,7 @@ def test_rel_pos(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -204,6 +206,7 @@ def test_zipformer_encoder_layer(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -284,6 +287,7 @@ def test_zipformer_encoder(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() @@ -338,6 +342,7 @@ def test_zipformer(): session = ort.InferenceSession( filename, sess_options=options, + providers=["CPUExecutionProvider"], ) input_nodes = session.get_inputs() diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/onnx_pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/onnx_pretrained.py index 8ff02fbcb..494a34d97 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/onnx_pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/onnx_pretrained.py @@ -326,41 +326,49 @@ def main(): encoder = ort.InferenceSession( args.encoder_model_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) decoder = ort.InferenceSession( args.decoder_model_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) joiner = ort.InferenceSession( args.joiner_model_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) joiner_encoder_proj = ort.InferenceSession( args.joiner_encoder_proj_model_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) joiner_decoder_proj = ort.InferenceSession( args.joiner_decoder_proj_model_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) lconv = ort.InferenceSession( args.lconv_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) frame_reducer = ort.InferenceSession( args.frame_reducer_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) ctc_output = ort.InferenceSession( args.ctc_output_filename, sess_options=session_opts, + providers=["CPUExecutionProvider"], ) sp = spm.SentencePieceProcessor() diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/onnx_pretrained.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/onnx_pretrained.py index 8192e01fd..04861ea37 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/onnx_pretrained.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/onnx_pretrained.py @@ -130,6 +130,7 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) self.init_encoder_states() @@ -229,6 +230,7 @@ class OnnxModel: self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -242,6 +244,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py index 500b2cd09..e62491444 100755 --- a/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained-streaming.py @@ -146,6 +146,7 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) self.init_encoder_states() @@ -236,6 +237,7 @@ class OnnxModel: self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -249,6 +251,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/librispeech/ASR/zipformer/onnx_pretrained.py b/egs/librispeech/ASR/zipformer/onnx_pretrained.py index 032b07721..334376093 100755 --- a/egs/librispeech/ASR/zipformer/onnx_pretrained.py +++ b/egs/librispeech/ASR/zipformer/onnx_pretrained.py @@ -151,12 +151,14 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_decoder(self, decoder_model_filename: str): self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -170,6 +172,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_check.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_check.py index a46ff5a07..2d46eede1 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_check.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/onnx_check.py @@ -258,6 +258,7 @@ def main(): encoder_session = ort.InferenceSession( args.onnx_encoder_filename, sess_options=options, + providers=["CPUExecutionProvider"], ) test_encoder(model, encoder_session) @@ -265,6 +266,7 @@ def main(): decoder_session = ort.InferenceSession( args.onnx_decoder_filename, sess_options=options, + providers=["CPUExecutionProvider"], ) test_decoder(model, decoder_session) @@ -272,14 +274,17 @@ def main(): joiner_session = ort.InferenceSession( args.onnx_joiner_filename, sess_options=options, + providers=["CPUExecutionProvider"], ) joiner_encoder_proj_session = ort.InferenceSession( args.onnx_joiner_encoder_proj_filename, sess_options=options, + providers=["CPUExecutionProvider"], ) joiner_decoder_proj_session = ort.InferenceSession( args.onnx_joiner_decoder_proj_filename, sess_options=options, + providers=["CPUExecutionProvider"], ) test_joiner( model, diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py index facfc2258..c31db6859 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained-streaming.py @@ -139,6 +139,7 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) self.init_encoder_states() @@ -186,6 +187,7 @@ class OnnxModel: self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -199,6 +201,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py index e7c8b4556..c784853ee 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/onnx_pretrained.py @@ -158,12 +158,14 @@ class OnnxModel: self.encoder = ort.InferenceSession( encoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) def init_decoder(self, decoder_model_filename: str): self.decoder = ort.InferenceSession( decoder_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) decoder_meta = self.decoder.get_modelmeta().custom_metadata_map @@ -177,6 +179,7 @@ class OnnxModel: self.joiner = ort.InferenceSession( joiner_model_filename, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) joiner_meta = self.joiner.get_modelmeta().custom_metadata_map diff --git a/egs/yesno/ASR/tdnn/onnx_pretrained.py b/egs/yesno/ASR/tdnn/onnx_pretrained.py index b23a2a381..72a1d69c8 100755 --- a/egs/yesno/ASR/tdnn/onnx_pretrained.py +++ b/egs/yesno/ASR/tdnn/onnx_pretrained.py @@ -54,6 +54,7 @@ class OnnxModel: self.model = ort.InferenceSession( nn_model, sess_options=self.session_opts, + providers=["CPUExecutionProvider"], ) meta = self.model.get_modelmeta().custom_metadata_map From 34e40a86b33102576b3442329421178a487e3ea3 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Fri, 22 Sep 2023 09:57:15 +0800 Subject: [PATCH 090/100] Fix exporting decoder model to onnx (#1264) * Use torch.jit.script() to export the decoder model See also https://github.com/k2-fsa/sherpa-onnx/issues/327 --- egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py | 1 + egs/commonvoice/ASR/pruned_transducer_stateless7/export-onnx.py | 1 + .../ASR/conv_emformer_transducer_stateless2/export-onnx.py | 1 + egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py | 1 + egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py | 1 + egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py | 1 + egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py | 1 + .../ASR/pruned_transducer_stateless5/export-onnx-streaming.py | 1 + egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py | 1 + egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py | 1 + .../ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py | 1 + .../ASR/pruned_transducer_stateless7_streaming/export-onnx.py | 1 + egs/librispeech/ASR/zipformer/export-onnx-streaming.py | 1 + egs/librispeech/ASR/zipformer/export-onnx.py | 1 + egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py | 1 + .../ASR/pruned_transducer_stateless5/export-onnx-streaming.py | 1 + egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py | 1 + 17 files changed, 17 insertions(+) diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py b/egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py index e8211500a..2a9fc57d5 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/export-onnx.py @@ -322,6 +322,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/commonvoice/ASR/pruned_transducer_stateless7/export-onnx.py b/egs/commonvoice/ASR/pruned_transducer_stateless7/export-onnx.py index 0c98885ac..2b9f2293a 100755 --- a/egs/commonvoice/ASR/pruned_transducer_stateless7/export-onnx.py +++ b/egs/commonvoice/ASR/pruned_transducer_stateless7/export-onnx.py @@ -330,6 +330,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py index cfd365207..ab046557f 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/export-onnx.py @@ -401,6 +401,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py index 89ced388c..2a52e2eec 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx-zh.py @@ -359,6 +359,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py index 6b6cb893f..c543628ff 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless2/export-onnx.py @@ -356,6 +356,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py index 282238c13..0a2132e56 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless/export-onnx.py @@ -307,6 +307,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py index 26dea7e11..2685ea95a 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless3/export-onnx.py @@ -312,6 +312,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py index 549fb13c9..b90d81dcf 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py @@ -404,6 +404,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py index fff0fcdd5..02aa24f2c 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/export-onnx.py @@ -335,6 +335,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py index 11c885f4d..b75548f8b 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/export-onnx.py @@ -329,6 +329,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py index 8653126de..2de56837e 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx-zh.py @@ -413,6 +413,7 @@ def export_decoder_model_onnx( context_size = decoder_model.decoder.context_size vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py index 6f84d79b4..d71080760 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/export-onnx.py @@ -401,6 +401,7 @@ def export_decoder_model_onnx( context_size = decoder_model.decoder.context_size vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py index a951aeef3..e2c7d7d95 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx-streaming.py +++ b/egs/librispeech/ASR/zipformer/export-onnx-streaming.py @@ -506,6 +506,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/librispeech/ASR/zipformer/export-onnx.py b/egs/librispeech/ASR/zipformer/export-onnx.py index e0d664009..3682f0b62 100755 --- a/egs/librispeech/ASR/zipformer/export-onnx.py +++ b/egs/librispeech/ASR/zipformer/export-onnx.py @@ -353,6 +353,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py index 760fad974..140b1d37f 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/export-onnx.py @@ -315,6 +315,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py index 9a926d7e5..921766ad4 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx-streaming.py @@ -404,6 +404,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py index 68c7cc352..037c7adf1 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/export-onnx.py @@ -335,6 +335,7 @@ def export_decoder_model_onnx( vocab_size = decoder_model.decoder.vocab_size y = torch.zeros(10, context_size, dtype=torch.int64) + decoder_model = torch.jit.script(decoder_model) torch.onnx.export( decoder_model, y, From ef658d691e75041398abb76567c810af1c22c7fc Mon Sep 17 00:00:00 2001 From: zr_jin Date: Sun, 24 Sep 2023 17:06:47 +0800 Subject: [PATCH 091/100] fixes for init value of `diagnostics.TensorDiagnosticOptions` (#1269) * fixes for `diagnostics` Replace `2 ** 22` with `512` as the default value of `diagnostics.TensorDiagnosticOptions` also black formatted some scripts * fixed formatting issues --- .../ASR/pruned_transducer_stateless2/train.py | 3 +- .../ASR/pruned_transducer_stateless2/train.py | 2 +- .../ASR/pruned_transducer_stateless3/train.py | 2 +- .../ASR/pruned_transducer_stateless7/train.py | 2 +- .../pruned_transducer_stateless7/train2.py | 2 +- .../train.py | 2 +- .../ASR/pruned_transducer_stateless5/train.py | 3 +- .../ASR/pruned_transducer_stateless5/train.py | 2 +- .../ASR/pruned_transducer_stateless2/train.py | 3 +- .../pruned_transducer_stateless7/train.py | 2 +- .../ASR/pruned_transducer_stateless7/train.py | 2 +- .../ASR/pruned_transducer_stateless7/train.py | 2 +- .../train.py | 2 +- .../train2.py | 2 +- .../train.py | 2 +- .../train.py | 2 +- .../train2.py | 2 +- .../ASR/pruned2_knowledge/train.py | 2 +- .../ASR/pruned_transducer_stateless5/train.py | 2 +- .../pruned_transducer_stateless7/finetune.py | 2 +- .../ASR/pruned_transducer_stateless7/optim.py | 12 +- .../ASR/pruned_transducer_stateless7/train.py | 2 +- .../pruned_transducer_stateless7_ctc/train.py | 2 +- .../train.py | 2 +- .../train.py | 2 +- .../train2.py | 2 +- .../train.py | 2 +- .../ASR/pruned_transducer_stateless8/train.py | 2 +- .../ASR/streaming_conformer_ctc/conformer.py | 4 +- egs/librispeech/ASR/zipformer/decoder.py | 30 +- egs/librispeech/ASR/zipformer/joiner.py | 9 +- egs/librispeech/ASR/zipformer/onnx_decode.py | 4 +- egs/librispeech/ASR/zipformer/optim.py | 22 +- egs/librispeech/ASR/zipformer/profile.py | 12 +- egs/librispeech/ASR/zipformer/scaling.py | 715 ++++++++++-------- .../ASR/zipformer/streaming_decode.py | 57 +- egs/librispeech/ASR/zipformer/subsampling.py | 16 +- egs/librispeech/ASR/zipformer/train.py | 21 +- egs/librispeech/ASR/zipformer_mmi/train.py | 2 +- .../ASR/pruned_transducer_stateless5/train.py | 5 +- egs/multi_zh-hans/ASR/zipformer/train.py | 2 +- .../ASR/pruned_transducer_stateless5/train.py | 2 +- .../train.py | 4 +- egs/tedlium3/ASR/conformer_ctc2/train.py | 2 +- egs/tedlium3/ASR/zipformer/train.py | 2 +- .../pruned_transducer_stateless2/finetune.py | 2 +- .../ASR/pruned_transducer_stateless2/train.py | 2 +- .../ASR/pruned_transducer_stateless5/train.py | 2 +- egs/wenetspeech/ASR/zipformer/train.py | 2 +- .../ASR/pruned_transducer_stateless5/train.py | 2 +- .../ASR/pruned_transducer_stateless7/train.py | 2 +- 51 files changed, 511 insertions(+), 479 deletions(-) diff --git a/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/train.py b/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/train.py index c9d9c4aa8..fa809b768 100644 --- a/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/train.py +++ b/egs/aidatatang_200zh/ASR/pruned_transducer_stateless2/train.py @@ -635,7 +635,6 @@ def train_one_epoch( tot_loss = MetricsTracker() for batch_idx, batch in enumerate(train_dl): - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -800,7 +799,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/aishell/ASR/pruned_transducer_stateless2/train.py b/egs/aishell/ASR/pruned_transducer_stateless2/train.py index d08908238..60f014c48 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless2/train.py +++ b/egs/aishell/ASR/pruned_transducer_stateless2/train.py @@ -872,7 +872,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/aishell/ASR/pruned_transducer_stateless3/train.py b/egs/aishell/ASR/pruned_transducer_stateless3/train.py index 62e67530d..7c23041ca 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless3/train.py +++ b/egs/aishell/ASR/pruned_transducer_stateless3/train.py @@ -1045,7 +1045,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train.py b/egs/aishell/ASR/pruned_transducer_stateless7/train.py index cbb7db086..11671db92 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/train.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train.py @@ -1028,7 +1028,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/aishell/ASR/pruned_transducer_stateless7/train2.py b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py index c30f6f960..057af297f 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7/train2.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7/train2.py @@ -1031,7 +1031,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py b/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py index 4e52f9573..3858bafd7 100755 --- a/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py +++ b/egs/aishell/ASR/pruned_transducer_stateless7_bbpe/train.py @@ -1019,7 +1019,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/aishell2/ASR/pruned_transducer_stateless5/train.py b/egs/aishell2/ASR/pruned_transducer_stateless5/train.py index 74bf68ccb..8c7448d4c 100755 --- a/egs/aishell2/ASR/pruned_transducer_stateless5/train.py +++ b/egs/aishell2/ASR/pruned_transducer_stateless5/train.py @@ -730,7 +730,6 @@ def train_one_epoch( tot_loss = MetricsTracker() for batch_idx, batch in enumerate(train_dl): - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -919,7 +918,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/aishell4/ASR/pruned_transducer_stateless5/train.py b/egs/aishell4/ASR/pruned_transducer_stateless5/train.py index 47015cbe7..a354f761e 100755 --- a/egs/aishell4/ASR/pruned_transducer_stateless5/train.py +++ b/egs/aishell4/ASR/pruned_transducer_stateless5/train.py @@ -908,7 +908,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/alimeeting/ASR/pruned_transducer_stateless2/train.py b/egs/alimeeting/ASR/pruned_transducer_stateless2/train.py index e57b5c859..30154291d 100644 --- a/egs/alimeeting/ASR/pruned_transducer_stateless2/train.py +++ b/egs/alimeeting/ASR/pruned_transducer_stateless2/train.py @@ -635,7 +635,6 @@ def train_one_epoch( tot_loss = MetricsTracker() for batch_idx, batch in enumerate(train_dl): - params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -800,7 +799,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py b/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py index 45d777922..8f09f1aa5 100755 --- a/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py +++ b/egs/alimeeting/ASR_v2/pruned_transducer_stateless7/train.py @@ -999,7 +999,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/ami/ASR/pruned_transducer_stateless7/train.py b/egs/ami/ASR/pruned_transducer_stateless7/train.py index 8c8d9593b..9b67141c0 100755 --- a/egs/ami/ASR/pruned_transducer_stateless7/train.py +++ b/egs/ami/ASR/pruned_transducer_stateless7/train.py @@ -988,7 +988,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py b/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py index 4bd5b83a2..4aedeffe4 100755 --- a/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py +++ b/egs/commonvoice/ASR/pruned_transducer_stateless7/train.py @@ -1019,7 +1019,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py index 18cb75c37..73fcd67aa 100755 --- a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py +++ b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train.py @@ -1074,7 +1074,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py index bc4bcf253..4c866ddd8 100755 --- a/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py +++ b/egs/csj/ASR/pruned_transducer_stateless7_streaming/train2.py @@ -1075,7 +1075,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py index c5a05d349..ca21bd6bf 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless/train.py @@ -953,7 +953,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py index 6bb37b017..23ddb6bec 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train.py @@ -953,7 +953,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py index 36067510c..420dc1065 100755 --- a/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py +++ b/egs/librispeech/ASR/conv_emformer_transducer_stateless2/train2.py @@ -955,7 +955,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned2_knowledge/train.py b/egs/librispeech/ASR/pruned2_knowledge/train.py index 77e06d3b7..a4899f7bd 100755 --- a/egs/librispeech/ASR/pruned2_knowledge/train.py +++ b/egs/librispeech/ASR/pruned2_knowledge/train.py @@ -811,7 +811,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless5/train.py b/egs/librispeech/ASR/pruned_transducer_stateless5/train.py index 3b5a635e4..66dc5f991 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless5/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless5/train.py @@ -1003,7 +1003,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py index 3ee2b7d65..4e261dbc1 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/finetune.py @@ -1132,7 +1132,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/optim.py b/egs/librispeech/ASR/pruned_transducer_stateless7/optim.py index aa3cef338..8ab3589da 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/optim.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/optim.py @@ -117,7 +117,7 @@ class BatchedOptimizer(Optimizer): yield tuples # <-- calling code will do the actual optimization here! - for ((stacked_params, _state, _names), batch) in zip(tuples, batches): + for (stacked_params, _state, _names), batch in zip(tuples, batches): for i, p in enumerate(batch): # batch is list of Parameter p.copy_(stacked_params[i]) @@ -181,7 +181,6 @@ class ScaledAdam(BatchedOptimizer): parameters_names=None, show_dominant_parameters=True, ): - assert parameters_names is not None, ( "Please prepare parameters_names," "which is a List[List[str]]. Each List[str] is for a group" @@ -224,9 +223,7 @@ class ScaledAdam(BatchedOptimizer): batch = True for group, group_params_names in zip(self.param_groups, self.parameters_names): - with self.batched_params(group["params"], group_params_names) as batches: - # batches is list of pairs (stacked_param, state). stacked_param is like # a regular parameter, and will have a .grad, but the 1st dim corresponds to # a stacking dim, it is not a real dim. @@ -325,7 +322,7 @@ class ScaledAdam(BatchedOptimizer): clipping_update_period = group["clipping_update_period"] tot_sumsq = torch.tensor(0.0, device=first_p.device) - for (p, state, param_names) in tuples: + for p, state, param_names in tuples: grad = p.grad if grad.is_sparse: raise RuntimeError( @@ -410,7 +407,7 @@ class ScaledAdam(BatchedOptimizer): from tuples, we still pass it to save some time. """ all_sumsq_orig = {} - for (p, state, batch_param_names) in tuples: + for p, state, batch_param_names in tuples: # p is a stacked batch parameters. batch_grad = p.grad if p.numel() == p.shape[0]: # a batch of scalars @@ -426,7 +423,6 @@ class ScaledAdam(BatchedOptimizer): for name, sumsq_orig, rms, grad in zip( batch_param_names, batch_sumsq_orig, batch_rms_orig, batch_grad ): - proportion_orig = sumsq_orig / tot_sumsq all_sumsq_orig[name] = (proportion_orig, sumsq_orig, rms, grad) @@ -1039,7 +1035,7 @@ def _test_scaled_adam(hidden_dim: int): # if epoch == 130: # opts = diagnostics.TensorDiagnosticOptions( - # 2 ** 22 + # 512 # ) # allow 4 megabytes per sub-module # diagnostic = diagnostics.attach_diagnostics(m, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py index 2b4d51089..fac3706d2 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/train.py @@ -1028,7 +1028,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py index b387968a9..d8fa08372 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc/train.py @@ -1052,7 +1052,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py index 23fb6f497..25a1aa674 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_ctc_bs/train.py @@ -1042,7 +1042,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py index 99090b2c1..2d915ff87 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train.py @@ -1029,7 +1029,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py index 9be629149..aa6c0668a 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming/train2.py @@ -1030,7 +1030,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py index b494253d6..565dc7a16 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7_streaming_multi/train.py @@ -1141,7 +1141,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless8/train.py b/egs/librispeech/ASR/pruned_transducer_stateless8/train.py index bee414292..3f271c5b4 100755 --- a/egs/librispeech/ASR/pruned_transducer_stateless8/train.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless8/train.py @@ -1154,7 +1154,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py b/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py index be6fabf35..0b982f4bf 100644 --- a/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py +++ b/egs/librispeech/ASR/streaming_conformer_ctc/conformer.py @@ -230,7 +230,9 @@ class Conformer(Transformer): x, pos_emb, mask=mask, src_key_padding_mask=src_key_padding_mask ) # (T, B, F) else: - x = self.encoder(x, pos_emb, src_key_padding_mask=src_key_padding_mask) # (T, B, F) + x = self.encoder( + x, pos_emb, src_key_padding_mask=src_key_padding_mask + ) # (T, B, F) if self.normalize_before: x = self.after_norm(x) diff --git a/egs/librispeech/ASR/zipformer/decoder.py b/egs/librispeech/ASR/zipformer/decoder.py index e8db988f6..e77e54118 100644 --- a/egs/librispeech/ASR/zipformer/decoder.py +++ b/egs/librispeech/ASR/zipformer/decoder.py @@ -61,10 +61,15 @@ class Decoder(nn.Module): ) # the balancers are to avoid any drift in the magnitude of the # embeddings, which would interact badly with parameter averaging. - self.balancer = Balancer(decoder_dim, channel_dim=-1, - min_positive=0.0, max_positive=1.0, - min_abs=0.5, max_abs=1.0, - prob=0.05) + self.balancer = Balancer( + decoder_dim, + channel_dim=-1, + min_positive=0.0, + max_positive=1.0, + min_abs=0.5, + max_abs=1.0, + prob=0.05, + ) self.blank_id = blank_id @@ -81,10 +86,15 @@ class Decoder(nn.Module): groups=decoder_dim // 4, # group size == 4 bias=False, ) - self.balancer2 = Balancer(decoder_dim, channel_dim=-1, - min_positive=0.0, max_positive=1.0, - min_abs=0.5, max_abs=1.0, - prob=0.05) + self.balancer2 = Balancer( + decoder_dim, + channel_dim=-1, + min_positive=0.0, + max_positive=1.0, + min_abs=0.5, + max_abs=1.0, + prob=0.05, + ) def forward(self, y: torch.Tensor, need_pad: bool = True) -> torch.Tensor: """ @@ -107,9 +117,7 @@ class Decoder(nn.Module): if self.context_size > 1: embedding_out = embedding_out.permute(0, 2, 1) if need_pad is True: - embedding_out = F.pad( - embedding_out, pad=(self.context_size - 1, 0) - ) + embedding_out = F.pad(embedding_out, pad=(self.context_size - 1, 0)) else: # During inference time, there is no need to do extra padding # as we only need one output diff --git a/egs/librispeech/ASR/zipformer/joiner.py b/egs/librispeech/ASR/zipformer/joiner.py index f03cc930e..dfb0a0057 100644 --- a/egs/librispeech/ASR/zipformer/joiner.py +++ b/egs/librispeech/ASR/zipformer/joiner.py @@ -52,12 +52,13 @@ class Joiner(nn.Module): Returns: Return a tensor of shape (N, T, s_range, C). """ - assert encoder_out.ndim == decoder_out.ndim, (encoder_out.shape, decoder_out.shape) + assert encoder_out.ndim == decoder_out.ndim, ( + encoder_out.shape, + decoder_out.shape, + ) if project_input: - logit = self.encoder_proj(encoder_out) + self.decoder_proj( - decoder_out - ) + logit = self.encoder_proj(encoder_out) + self.decoder_proj(decoder_out) else: logit = encoder_out + decoder_out diff --git a/egs/librispeech/ASR/zipformer/onnx_decode.py b/egs/librispeech/ASR/zipformer/onnx_decode.py index 2aca36ca9..356c2a830 100755 --- a/egs/librispeech/ASR/zipformer/onnx_decode.py +++ b/egs/librispeech/ASR/zipformer/onnx_decode.py @@ -303,7 +303,9 @@ def main(): for test_set, test_dl in zip(test_sets, test_dl): start_time = time.time() - results, total_duration = decode_dataset(dl=test_dl, model=model, token_table=token_table) + results, total_duration = decode_dataset( + dl=test_dl, model=model, token_table=token_table + ) end_time = time.time() elapsed_seconds = end_time - start_time rtf = elapsed_seconds / total_duration diff --git a/egs/librispeech/ASR/zipformer/optim.py b/egs/librispeech/ASR/zipformer/optim.py index abfb2092c..c9b76526c 100644 --- a/egs/librispeech/ASR/zipformer/optim.py +++ b/egs/librispeech/ASR/zipformer/optim.py @@ -116,7 +116,7 @@ class BatchedOptimizer(Optimizer): yield tuples # <-- calling code will do the actual optimization here! - for ((stacked_params, _state, _names), batch) in zip(tuples, batches): + for (stacked_params, _state, _names), batch in zip(tuples, batches): for i, p in enumerate(batch): # batch is list of Parameter p.copy_(stacked_params[i]) @@ -181,7 +181,6 @@ class ScaledAdam(BatchedOptimizer): size_update_period=4, clipping_update_period=100, ): - defaults = dict( lr=lr, clipping_scale=clipping_scale, @@ -299,8 +298,8 @@ class ScaledAdam(BatchedOptimizer): # the input is groups of parameter or named parameter. for cur_group in iterable_or_groups: assert "named_params" in cur_group - name_list = [ x[0] for x in cur_group["named_params"] ] - p_list = [ x[1] for x in cur_group["named_params"] ] + name_list = [x[0] for x in cur_group["named_params"]] + p_list = [x[1] for x in cur_group["named_params"]] del cur_group["named_params"] cur_group["params"] = p_list param_groups.append(cur_group) @@ -327,9 +326,7 @@ class ScaledAdam(BatchedOptimizer): batch = True for group, group_params_names in zip(self.param_groups, self.parameters_names): - with self.batched_params(group["params"], group_params_names) as batches: - # batches is list of pairs (stacked_param, state). stacked_param is like # a regular parameter, and will have a .grad, but the 1st dim corresponds to # a stacking dim, it is not a real dim. @@ -428,7 +425,7 @@ class ScaledAdam(BatchedOptimizer): clipping_update_period = group["clipping_update_period"] tot_sumsq = torch.tensor(0.0, device=first_p.device) - for (p, state, param_names) in tuples: + for p, state, param_names in tuples: grad = p.grad if grad.is_sparse: raise RuntimeError( @@ -513,7 +510,7 @@ class ScaledAdam(BatchedOptimizer): from tuples, we still pass it to save some time. """ all_sumsq_orig = {} - for (p, state, batch_param_names) in tuples: + for p, state, batch_param_names in tuples: # p is a stacked batch parameters. batch_grad = p.grad if p.numel() == p.shape[0]: # a batch of scalars @@ -529,7 +526,6 @@ class ScaledAdam(BatchedOptimizer): for name, sumsq_orig, rms, grad in zip( batch_param_names, batch_sumsq_orig, batch_rms_orig, batch_grad ): - proportion_orig = sumsq_orig / tot_sumsq all_sumsq_orig[name] = (proportion_orig, sumsq_orig, rms, grad) @@ -667,8 +663,7 @@ class ScaledAdam(BatchedOptimizer): # We have to look at the trained model for parameters at or around the # param_max_rms, because sometimes they can indicate a problem with the # topology or settings. - scale_step = torch.minimum(scale_step, - (param_max_rms - param_rms) / param_rms) + scale_step = torch.minimum(scale_step, (param_max_rms - param_rms) / param_rms) delta = state["delta"] # the factor of (1-beta1) relates to momentum. @@ -879,7 +874,8 @@ class Eden(LRScheduler): warmup_factor = ( 1.0 if self.batch >= self.warmup_batches - else self.warmup_start + (1.0 - self.warmup_start) * (self.batch / self.warmup_batches) + else self.warmup_start + + (1.0 - self.warmup_start) * (self.batch / self.warmup_batches) # else 0.5 + 0.5 * (self.batch / self.warmup_batches) ) @@ -1111,7 +1107,7 @@ def _test_scaled_adam(hidden_dim: int): # if epoch == 130: # opts = diagnostics.TensorDiagnosticOptions( - # 2 ** 22 + # 512 # ) # allow 4 megabytes per sub-module # diagnostic = diagnostics.attach_diagnostics(m, opts) diff --git a/egs/librispeech/ASR/zipformer/profile.py b/egs/librispeech/ASR/zipformer/profile.py index b460b5338..57f44a90a 100755 --- a/egs/librispeech/ASR/zipformer/profile.py +++ b/egs/librispeech/ASR/zipformer/profile.py @@ -100,17 +100,13 @@ class Model(nn.Module): self.encoder_embed = encoder_embed self.encoder_proj = encoder_proj - def forward( - self, feature: Tensor, feature_lens: Tensor - ) -> Tuple[Tensor, Tensor]: + def forward(self, feature: Tensor, feature_lens: Tensor) -> Tuple[Tensor, Tensor]: x, x_lens = self.encoder_embed(feature, feature_lens) src_key_padding_mask = make_pad_mask(x_lens) x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) - encoder_out, encoder_out_lens = self.encoder( - x, x_lens, src_key_padding_mask - ) + encoder_out, encoder_out_lens = self.encoder(x, x_lens, src_key_padding_mask) encoder_out = encoder_out.permute(1, 0, 2) # (N, T, C) -> (T, N, C) logits = self.encoder_proj(encoder_out) @@ -168,9 +164,7 @@ def main(): if __name__ == "__main__": - formatter = ( - "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" - ) + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" logging.basicConfig(format=formatter, level=logging.INFO) main() diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py index 7c98ef045..23fd279b3 100644 --- a/egs/librispeech/ASR/zipformer/scaling.py +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -25,6 +25,7 @@ import math import torch.nn as nn from torch import Tensor + def logaddexp_onnx(x: Tensor, y: Tensor) -> Tensor: max_value = torch.max(x, y) diff = torch.abs(x - y) @@ -55,28 +56,34 @@ def logaddexp(x: Tensor, y: Tensor) -> Tensor: # for torch.jit.trace() return torch.logaddexp(x, y) + class PiecewiseLinear(object): """ Piecewise linear function, from float to float, specified as nonempty list of (x,y) pairs with the x values in order. x values <[initial x] or >[final x] are map to [initial y], [final y] respectively. """ + def __init__(self, *args): assert len(args) >= 1, len(args) if len(args) == 1 and isinstance(args[0], PiecewiseLinear): self.pairs = list(args[0].pairs) else: - self.pairs = [ (float(x), float(y)) for x,y in args ] - for (x,y) in self.pairs: + self.pairs = [(float(x), float(y)) for x, y in args] + for (x, y) in self.pairs: assert isinstance(x, (float, int)), type(x) assert isinstance(y, (float, int)), type(y) for i in range(len(self.pairs) - 1): - assert self.pairs[i + 1][0] > self.pairs[i][0], (i, self.pairs[i], self.pairs[i + 1]) + assert self.pairs[i + 1][0] > self.pairs[i][0], ( + i, + self.pairs[i], + self.pairs[i + 1], + ) def __str__(self): # e.g. 'PiecewiseLinear((0., 10.), (100., 0.))' - return f'PiecewiseLinear({str(self.pairs)[1:-1]})' + return f"PiecewiseLinear({str(self.pairs)[1:-1]})" def __call__(self, x): if x <= self.pairs[0][0]: @@ -93,37 +100,36 @@ class PiecewiseLinear(object): assert False def __mul__(self, alpha): - return PiecewiseLinear( - * [(x, y * alpha) for x, y in self.pairs]) + return PiecewiseLinear(*[(x, y * alpha) for x, y in self.pairs]) def __add__(self, x): if isinstance(x, (float, int)): - return PiecewiseLinear( - * [(p[0], p[1] + x) for p in self.pairs]) + return PiecewiseLinear(*[(p[0], p[1] + x) for p in self.pairs]) s, x = self.get_common_basis(x) return PiecewiseLinear( - * [(sp[0], sp[1] + xp[1]) for sp, xp in zip(s.pairs, x.pairs)]) + *[(sp[0], sp[1] + xp[1]) for sp, xp in zip(s.pairs, x.pairs)] + ) def max(self, x): if isinstance(x, (float, int)): - x = PiecewiseLinear( (0, x) ) + x = PiecewiseLinear((0, x)) s, x = self.get_common_basis(x, include_crossings=True) return PiecewiseLinear( - * [(sp[0], max(sp[1], xp[1])) for sp, xp in zip(s.pairs, x.pairs)]) + *[(sp[0], max(sp[1], xp[1])) for sp, xp in zip(s.pairs, x.pairs)] + ) def min(self, x): if isinstance(x, float) or isinstance(x, int): - x = PiecewiseLinear( (0, x) ) + x = PiecewiseLinear((0, x)) s, x = self.get_common_basis(x, include_crossings=True) return PiecewiseLinear( - * [ (sp[0], min(sp[1], xp[1])) for sp, xp in zip(s.pairs, x.pairs)]) + *[(sp[0], min(sp[1], xp[1])) for sp, xp in zip(s.pairs, x.pairs)] + ) def __eq__(self, other): return self.pairs == other.pairs - def get_common_basis(self, - p: 'PiecewiseLinear', - include_crossings: bool = False): + def get_common_basis(self, p: "PiecewiseLinear", include_crossings: bool = False): """ Returns (self_mod, p_mod) which are equivalent piecewise linear functions to self and p, but with the same x values. @@ -135,28 +141,30 @@ class PiecewiseLinear(object): assert isinstance(p, PiecewiseLinear), type(p) # get sorted x-values without repetition. - x_vals = sorted(set([ x for x, _ in self.pairs ] + [ x for x, _ in p.pairs ])) - y_vals1 = [ self(x) for x in x_vals ] - y_vals2 = [ p(x) for x in x_vals ] + x_vals = sorted(set([x for x, _ in self.pairs] + [x for x, _ in p.pairs])) + y_vals1 = [self(x) for x in x_vals] + y_vals2 = [p(x) for x in x_vals] if include_crossings: extra_x_vals = [] for i in range(len(x_vals) - 1): - if (y_vals1[i] > y_vals2[i]) != (y_vals1[i+1] > y_vals2[i+1]): + if (y_vals1[i] > y_vals2[i]) != (y_vals1[i + 1] > y_vals2[i + 1]): # if the two lines in this subsegment potentially cross each other.. diff_cur = abs(y_vals1[i] - y_vals2[i]) - diff_next = abs(y_vals1[i+1] - y_vals2[i+1]) + diff_next = abs(y_vals1[i + 1] - y_vals2[i + 1]) # `pos`, between 0 and 1, gives the relative x position, # with 0 being x_vals[i] and 1 being x_vals[i+1]. pos = diff_cur / (diff_cur + diff_next) - extra_x_val = x_vals[i] + pos * (x_vals[i+1] - x_vals[i]) + extra_x_val = x_vals[i] + pos * (x_vals[i + 1] - x_vals[i]) extra_x_vals.append(extra_x_val) if len(extra_x_vals) > 0: x_vals = sorted(set(x_vals + extra_x_vals)) - y_vals1 = [ self(x) for x in x_vals ] - y_vals2 = [ p(x) for x in x_vals ] - return ( PiecewiseLinear(* zip(x_vals, y_vals1)), - PiecewiseLinear(* zip(x_vals, y_vals2)) ) + y_vals1 = [self(x) for x in x_vals] + y_vals2 = [p(x) for x in x_vals] + return ( + PiecewiseLinear(*zip(x_vals, y_vals1)), + PiecewiseLinear(*zip(x_vals, y_vals2)), + ) class ScheduledFloat(torch.nn.Module): @@ -176,9 +184,8 @@ class ScheduledFloat(torch.nn.Module): `default` is used when self.batch_count is not set or not in training mode or in torch.jit scripting mode. """ - def __init__(self, - *args, - default: float = 0.0): + + def __init__(self, *args, default: float = 0.0): super().__init__() # self.batch_count and self.name will be written to in the training loop. self.batch_count = None @@ -187,47 +194,55 @@ class ScheduledFloat(torch.nn.Module): self.schedule = PiecewiseLinear(*args) def extra_repr(self) -> str: - return f'batch_count={self.batch_count}, schedule={str(self.schedule.pairs[1:-1])}' + return ( + f"batch_count={self.batch_count}, schedule={str(self.schedule.pairs[1:-1])}" + ) def __float__(self): batch_count = self.batch_count - if batch_count is None or not self.training or torch.jit.is_scripting() or torch.jit.is_tracing(): + if ( + batch_count is None + or not self.training + or torch.jit.is_scripting() + or torch.jit.is_tracing() + ): return float(self.default) else: ans = self.schedule(self.batch_count) if random.random() < 0.0002: - logging.info(f"ScheduledFloat: name={self.name}, batch_count={self.batch_count}, ans={ans}") + logging.info( + f"ScheduledFloat: name={self.name}, batch_count={self.batch_count}, ans={ans}" + ) return ans def __add__(self, x): if isinstance(x, float) or isinstance(x, int): - return ScheduledFloat(self.schedule + x, - default=self.default) + return ScheduledFloat(self.schedule + x, default=self.default) else: - return ScheduledFloat(self.schedule + x.schedule, - default=self.default+x.default) + return ScheduledFloat( + self.schedule + x.schedule, default=self.default + x.default + ) def max(self, x): if isinstance(x, float) or isinstance(x, int): - return ScheduledFloat(self.schedule.max(x), - default=self.default) + return ScheduledFloat(self.schedule.max(x), default=self.default) else: - return ScheduledFloat(self.schedule.max(x.schedule), - default=max(self.default, x.default)) + return ScheduledFloat( + self.schedule.max(x.schedule), default=max(self.default, x.default) + ) FloatLike = Union[float, ScheduledFloat] -def random_cast_to_half(x: Tensor, - min_abs: float = 5.0e-06) -> Tensor: +def random_cast_to_half(x: Tensor, min_abs: float = 5.0e-06) -> Tensor: """ A randomized way of casting a floating point value to half precision. """ if x.dtype == torch.float16: return x x_abs = x.abs() - is_too_small = (x_abs < min_abs) + is_too_small = x_abs < min_abs # for elements where is_too_small is true, random_val will contain +-min_abs with # probability (x.abs() / min_abs), and 0.0 otherwise. [so this preserves expectations, # for those elements]. @@ -242,6 +257,7 @@ class CutoffEstimator: p is the proportion of items that should be above the cutoff. """ + def __init__(self, p: float): self.p = p # total count of items @@ -255,7 +271,7 @@ class CutoffEstimator: """ Returns true if x is above the cutoff. """ - ans = (x > self.cutoff) + ans = x > self.cutoff self.count += 1 if ans: self.count_above += 1 @@ -263,7 +279,7 @@ class CutoffEstimator: delta_p = cur_p - self.p if (delta_p > 0) == ans: q = abs(delta_p) - self.cutoff = x * q + self.cutoff * (1-q) + self.cutoff = x * q + self.cutoff * (1 - q) return ans @@ -272,6 +288,7 @@ class SoftmaxFunction(torch.autograd.Function): Tries to handle half-precision derivatives in a randomized way that should be more accurate for training than the default behavior. """ + @staticmethod def forward(ctx, x: Tensor, dim: int): ans = x.softmax(dim=dim) @@ -287,7 +304,7 @@ class SoftmaxFunction(torch.autograd.Function): @staticmethod def backward(ctx, ans_grad: Tensor): - ans, = ctx.saved_tensors + (ans,) = ctx.saved_tensors with torch.cuda.amp.autocast(enabled=False): ans_grad = ans_grad.to(torch.float32) ans = ans.to(torch.float32) @@ -306,17 +323,16 @@ def softmax(x: Tensor, dim: int): class MaxEigLimiterFunction(torch.autograd.Function): @staticmethod def forward( - ctx, - x: Tensor, - coeffs: Tensor, - direction: Tensor, - channel_dim: int, - grad_scale: float) -> Tensor: + ctx, + x: Tensor, + coeffs: Tensor, + direction: Tensor, + channel_dim: int, + grad_scale: float, + ) -> Tensor: ctx.channel_dim = channel_dim ctx.grad_scale = grad_scale - ctx.save_for_backward(x.detach(), - coeffs.detach(), - direction.detach()) + ctx.save_for_backward(x.detach(), coeffs.detach(), direction.detach()) return x @staticmethod @@ -328,15 +344,20 @@ class MaxEigLimiterFunction(torch.autograd.Function): x = x_orig.transpose(ctx.channel_dim, -1).reshape(-1, num_channels) new_direction.requires_grad = False x = x - x.mean(dim=0) - x_var = (x ** 2).mean() + x_var = (x**2).mean() x_residual = x - coeffs * new_direction - x_residual_var = (x_residual ** 2).mean() + x_residual_var = (x_residual**2).mean() # `variance_proportion` is the proportion of the variance accounted for # by the top eigen-direction. This is to be minimized. variance_proportion = (x_var - x_residual_var) / (x_var + 1.0e-20) variance_proportion.backward() x_orig_grad = x_orig.grad - x_extra_grad = x_orig.grad * ctx.grad_scale * x_grad.norm() / (x_orig_grad.norm() + 1.0e-20) + x_extra_grad = ( + x_orig.grad + * ctx.grad_scale + * x_grad.norm() + / (x_orig_grad.norm() + 1.0e-20) + ) return x_grad + x_extra_grad.detach(), None, None, None, None @@ -348,8 +369,14 @@ class BiasNormFunction(torch.autograd.Function): # it can just store the returned value (chances are, this will also be needed for # some other reason, related to the next operation, so we can save memory). @staticmethod - def forward(ctx, x: Tensor, bias: Tensor, log_scale: Tensor, channel_dim: int, - store_output_for_backprop: bool) -> Tensor: + def forward( + ctx, + x: Tensor, + bias: Tensor, + log_scale: Tensor, + channel_dim: int, + store_output_for_backprop: bool, + ) -> Tensor: assert bias.ndim == 1 if channel_dim < 0: channel_dim = channel_dim + x.ndim @@ -357,10 +384,16 @@ class BiasNormFunction(torch.autograd.Function): ctx.channel_dim = channel_dim for _ in range(channel_dim + 1, x.ndim): bias = bias.unsqueeze(-1) - scales = (torch.mean((x - bias) ** 2, dim=channel_dim, keepdim=True) ** -0.5) * log_scale.exp() + scales = ( + torch.mean((x - bias) ** 2, dim=channel_dim, keepdim=True) ** -0.5 + ) * log_scale.exp() ans = x * scales - ctx.save_for_backward(ans.detach() if store_output_for_backprop else x, - scales.detach(), bias.detach(), log_scale.detach()) + ctx.save_for_backward( + ans.detach() if store_output_for_backprop else x, + scales.detach(), + bias.detach(), + log_scale.detach(), + ) return ans @staticmethod @@ -376,7 +409,9 @@ class BiasNormFunction(torch.autograd.Function): log_scale.requires_grad = True with torch.enable_grad(): # recompute scales from x, bias and log_scale. - scales = (torch.mean((x - bias) ** 2, dim=ctx.channel_dim, keepdim=True) ** -0.5) * log_scale.exp() + scales = ( + torch.mean((x - bias) ** 2, dim=ctx.channel_dim, keepdim=True) ** -0.5 + ) * log_scale.exp() ans = x * scales ans.backward(gradient=ans_grad) return x.grad, bias.grad.flatten(), log_scale.grad, None, None @@ -412,14 +447,15 @@ class BiasNorm(torch.nn.Module): than the input of this module to be required to be stored for the backprop. """ + def __init__( - self, - num_channels: int, - channel_dim: int = -1, # CAUTION: see documentation. - log_scale: float = 1.0, - log_scale_min: float = -1.5, - log_scale_max: float = 1.5, - store_output_for_backprop: bool = False + self, + num_channels: int, + channel_dim: int = -1, # CAUTION: see documentation. + log_scale: float = 1.0, + log_scale_min: float = -1.5, + log_scale_max: float = 1.5, + store_output_for_backprop: bool = False, ) -> None: super(BiasNorm, self).__init__() self.num_channels = num_channels @@ -442,23 +478,24 @@ class BiasNorm(torch.nn.Module): bias = self.bias for _ in range(channel_dim + 1, x.ndim): bias = bias.unsqueeze(-1) - scales = ((torch.mean((x - bias) ** 2, dim=channel_dim, keepdim=True) ** -0.5) * - self.log_scale.exp()) + scales = ( + torch.mean((x - bias) ** 2, dim=channel_dim, keepdim=True) ** -0.5 + ) * self.log_scale.exp() return x * scales - log_scale = limit_param_value(self.log_scale, - min=float(self.log_scale_min), - max=float(self.log_scale_max), - training=self.training) + log_scale = limit_param_value( + self.log_scale, + min=float(self.log_scale_min), + max=float(self.log_scale_max), + training=self.training, + ) - return BiasNormFunction.apply(x, self.bias, log_scale, - self.channel_dim, - self.store_output_for_backprop) + return BiasNormFunction.apply( + x, self.bias, log_scale, self.channel_dim, self.store_output_for_backprop + ) -def ScaledLinear(*args, - initial_scale: float = 1.0, - **kwargs) -> nn.Linear: +def ScaledLinear(*args, initial_scale: float = 1.0, **kwargs) -> nn.Linear: """ Behaves like a constructor of a modified version of nn.Linear that gives an easy way to set the default initial parameter scale. @@ -477,15 +514,11 @@ def ScaledLinear(*args, with torch.no_grad(): ans.weight[:] *= initial_scale if ans.bias is not None: - torch.nn.init.uniform_(ans.bias, - -0.1 * initial_scale, - 0.1 * initial_scale) + torch.nn.init.uniform_(ans.bias, -0.1 * initial_scale, 0.1 * initial_scale) return ans -def ScaledConv1d(*args, - initial_scale: float = 1.0, - **kwargs) -> nn.Conv1d: +def ScaledConv1d(*args, initial_scale: float = 1.0, **kwargs) -> nn.Conv1d: """ Behaves like a constructor of a modified version of nn.Conv1d that gives an easy way to set the default initial parameter scale. @@ -504,15 +537,11 @@ def ScaledConv1d(*args, with torch.no_grad(): ans.weight[:] *= initial_scale if ans.bias is not None: - torch.nn.init.uniform_(ans.bias, - -0.1 * initial_scale, - 0.1 * initial_scale) + torch.nn.init.uniform_(ans.bias, -0.1 * initial_scale, 0.1 * initial_scale) return ans -def ScaledConv2d(*args, - initial_scale: float = 1.0, - **kwargs) -> nn.Conv2d: +def ScaledConv2d(*args, initial_scale: float = 1.0, **kwargs) -> nn.Conv2d: """ Behaves like a constructor of a modified version of nn.Conv2d that gives an easy way to set the default initial parameter scale. @@ -532,9 +561,7 @@ def ScaledConv2d(*args, with torch.no_grad(): ans.weight[:] *= initial_scale if ans.bias is not None: - torch.nn.init.uniform_(ans.bias, - -0.1 * initial_scale, - 0.1 * initial_scale) + torch.nn.init.uniform_(ans.bias, -0.1 * initial_scale, 0.1 * initial_scale) return ans @@ -562,29 +589,36 @@ class ChunkCausalDepthwiseConv1d(torch.nn.Module): Another option, if you want to do something like this, is to re-initialize the parameters. """ - def __init__(self, - channels: int, - kernel_size: int, - initial_scale: float = 1.0, - bias: bool = True): + + def __init__( + self, + channels: int, + kernel_size: int, + initial_scale: float = 1.0, + bias: bool = True, + ): super().__init__() assert kernel_size % 2 == 1 half_kernel_size = (kernel_size + 1) // 2 # will pad manually, on one side. - self.causal_conv = nn.Conv1d(in_channels=channels, - out_channels=channels, - groups=channels, - kernel_size=half_kernel_size, - padding=0, - bias=True) + self.causal_conv = nn.Conv1d( + in_channels=channels, + out_channels=channels, + groups=channels, + kernel_size=half_kernel_size, + padding=0, + bias=True, + ) - self.chunkwise_conv = nn.Conv1d(in_channels=channels, - out_channels=channels, - groups=channels, - kernel_size=kernel_size, - padding=kernel_size // 2, - bias=bias) + self.chunkwise_conv = nn.Conv1d( + in_channels=channels, + out_channels=channels, + groups=channels, + kernel_size=kernel_size, + padding=kernel_size // 2, + bias=bias, + ) # first row is correction factors added to the scale near the left edge of the chunk, # second row is correction factors added to the scale near the right edge of the chunk, @@ -596,17 +630,15 @@ class ChunkCausalDepthwiseConv1d(torch.nn.Module): self.causal_conv.weight[:] *= initial_scale self.chunkwise_conv.weight[:] *= initial_scale if bias: - torch.nn.init.uniform_(self.causal_conv.bias, - -0.1 * initial_scale, - 0.1 * initial_scale) + torch.nn.init.uniform_( + self.causal_conv.bias, -0.1 * initial_scale, 0.1 * initial_scale + ) - def forward(self, - x: Tensor, - chunk_size: int = -1) -> Tensor: + def forward(self, x: Tensor, chunk_size: int = -1) -> Tensor: """ - Forward function. Args: - x: a Tensor of shape (batch_size, channels, seq_len) - chunk_size: the chunk size, in frames; does not have to divide seq_len exactly. + Forward function. Args: + x: a Tensor of shape (batch_size, channels, seq_len) + chunk_size: the chunk size, in frames; does not have to divide seq_len exactly. """ (batch_size, num_channels, seq_len) = x.shape @@ -622,28 +654,32 @@ class ChunkCausalDepthwiseConv1d(torch.nn.Module): x = torch.nn.functional.pad(x, (left_pad, right_pad)) - x_causal = self.causal_conv(x[..., :left_pad + seq_len]) + x_causal = self.causal_conv(x[..., : left_pad + seq_len]) assert x_causal.shape == (batch_size, num_channels, seq_len) x_chunk = x[..., left_pad:] num_chunks = x_chunk.shape[2] // chunk_size x_chunk = x_chunk.reshape(batch_size, num_channels, num_chunks, chunk_size) - x_chunk = x_chunk.permute(0, 2, 1, 3).reshape(batch_size * num_chunks, - num_channels, chunk_size) + x_chunk = x_chunk.permute(0, 2, 1, 3).reshape( + batch_size * num_chunks, num_channels, chunk_size + ) x_chunk = self.chunkwise_conv(x_chunk) # does not change shape chunk_scale = self._get_chunk_scale(chunk_size) x_chunk = x_chunk * chunk_scale - x_chunk = x_chunk.reshape(batch_size, num_chunks, - num_channels, chunk_size).permute(0, 2, 1, 3) - x_chunk = x_chunk.reshape(batch_size, num_channels, num_chunks * chunk_size)[..., :seq_len] + x_chunk = x_chunk.reshape( + batch_size, num_chunks, num_channels, chunk_size + ).permute(0, 2, 1, 3) + x_chunk = x_chunk.reshape(batch_size, num_channels, num_chunks * chunk_size)[ + ..., :seq_len + ] return x_chunk + x_causal def _get_chunk_scale(self, chunk_size: int): """Returns tensor of shape (num_channels, chunk_size) that will be used to - scale the output of self.chunkwise_conv.""" + scale the output of self.chunkwise_conv.""" left_edge = self.chunkwise_conv_scale[0] right_edge = self.chunkwise_conv_scale[1] if chunk_size < self.kernel_size: @@ -652,9 +688,9 @@ class ChunkCausalDepthwiseConv1d(torch.nn.Module): else: t = chunk_size - self.kernel_size channels = left_edge.shape[0] - pad = torch.zeros(channels, t, - device=left_edge.device, - dtype=left_edge.dtype) + pad = torch.zeros( + channels, t, device=left_edge.device, dtype=left_edge.dtype + ) left_edge = torch.cat((left_edge, pad), dim=-1) right_edge = torch.cat((pad, right_edge), dim=-1) return 1.0 + (left_edge + right_edge) @@ -698,14 +734,14 @@ class ChunkCausalDepthwiseConv1d(torch.nn.Module): class BalancerFunction(torch.autograd.Function): @staticmethod def forward( - ctx, - x: Tensor, - min_mean: float, - max_mean: float, - min_rms: float, - max_rms: float, - grad_scale: float, - channel_dim: int, + ctx, + x: Tensor, + min_mean: float, + max_mean: float, + min_rms: float, + max_rms: float, + grad_scale: float, + channel_dim: int, ) -> Tensor: if channel_dim < 0: channel_dim += x.ndim @@ -715,10 +751,8 @@ class BalancerFunction(torch.autograd.Function): return x @staticmethod - def backward( - ctx, x_grad: Tensor - ) -> Tuple[Tensor, None, None, None, None, None]: - x, = ctx.saved_tensors + def backward(ctx, x_grad: Tensor) -> Tuple[Tensor, None, None, None, None, None]: + (x,) = ctx.saved_tensors (min_mean, max_mean, min_rms, max_rms, grad_scale, channel_dim) = ctx.config try: @@ -727,8 +761,8 @@ class BalancerFunction(torch.autograd.Function): x = x.to(torch.float32) x = x.detach() x.requires_grad = True - mean_dims = [ i for i in range(x.ndim) if i != channel_dim ] - uncentered_var = (x ** 2).mean(dim=mean_dims, keepdim=True) + mean_dims = [i for i in range(x.ndim) if i != channel_dim] + uncentered_var = (x**2).mean(dim=mean_dims, keepdim=True) mean = x.mean(dim=mean_dims, keepdim=True) stddev = (uncentered_var - (mean * mean)).clamp(min=1.0e-20).sqrt() rms = uncentered_var.clamp(min=1.0e-20).sqrt() @@ -742,11 +776,16 @@ class BalancerFunction(torch.autograd.Function): rms_clamped = rms.clamp(min=min_rms, max=max_rms) r_loss = (rms_clamped / rms).log().abs() - loss = (m_loss + r_loss) + loss = m_loss + r_loss loss.backward(gradient=torch.ones_like(loss)) loss_grad = x.grad - loss_grad_rms = (loss_grad ** 2).mean(dim=mean_dims, keepdim=True).sqrt().clamp(min=1.0e-20) + loss_grad_rms = ( + (loss_grad**2) + .mean(dim=mean_dims, keepdim=True) + .sqrt() + .clamp(min=1.0e-20) + ) loss_grad = loss_grad * (grad_scale / loss_grad_rms) @@ -757,7 +796,9 @@ class BalancerFunction(torch.autograd.Function): x_grad_mod = x_grad_float + (x_grad_float.abs() * loss_grad) x_grad = x_grad_mod.to(x_grad.dtype) except Exception as e: - logging.info(f"Caught exception in Balancer backward: {e}, size={list(x_grad.shape)}, will continue.") + logging.info( + f"Caught exception in Balancer backward: {e}, size={list(x_grad.shape)}, will continue." + ) return x_grad, None, None, None, None, None, None @@ -793,16 +834,17 @@ class Balancer(torch.nn.Module): on each forward(). This is done randomly to prevent all layers from doing it at the same time. """ + def __init__( - self, - num_channels: int, - channel_dim: int, - min_positive: FloatLike = 0.05, - max_positive: FloatLike = 0.95, - min_abs: FloatLike = 0.2, - max_abs: FloatLike = 100.0, - grad_scale: FloatLike = 0.04, - prob: Optional[FloatLike] = None, + self, + num_channels: int, + channel_dim: int, + min_positive: FloatLike = 0.05, + max_positive: FloatLike = 0.95, + min_abs: FloatLike = 0.2, + max_abs: FloatLike = 100.0, + grad_scale: FloatLike = 0.04, + prob: Optional[FloatLike] = None, ): super().__init__() @@ -823,8 +865,11 @@ class Balancer(torch.nn.Module): self.grad_scale = grad_scale def forward(self, x: Tensor) -> Tensor: - if (torch.jit.is_scripting() or not x.requires_grad or - (x.is_cuda and self.mem_cutoff(torch.cuda.memory_allocated()))): + if ( + torch.jit.is_scripting() + or not x.requires_grad + or (x.is_cuda and self.mem_cutoff(torch.cuda.memory_allocated())) + ): return _no_op(x) prob = float(self.prob) @@ -842,7 +887,7 @@ class Balancer(torch.nn.Module): eps = 1.0e-10 # eps is to prevent crashes if x is exactly 0 or 1. # we'll just end up returning a fairly large value. - return (math.log (1+x+eps) - math.log (1-x+eps)) / 2. + return (math.log(1 + x + eps) - math.log(1 - x + eps)) / 2.0 def _approx_inverse_erf(x): # 1 / (sqrt(pi) * ln(2)), @@ -853,6 +898,7 @@ class Balancer(torch.nn.Module): # and math.erf(0.0407316414078772) = 0.045935330944660666, # which is pretty close to 0.05. return 0.8139535143 * _atanh(x) + # first convert x from the range 0..1 to the range -1..1 which the error # function returns x = -1 + (2 * x) @@ -873,8 +919,9 @@ class Balancer(torch.nn.Module): return _no_op(x) -def penalize_abs_values_gt(x: Tensor, limit: float, penalty: float, - name: str = None) -> Tensor: +def penalize_abs_values_gt( + x: Tensor, limit: float, penalty: float, name: str = None +) -> Tensor: """ Returns x unmodified, but in backprop will put a penalty for the excess of the absolute values of elements of x over the limit "limit". E.g. if @@ -910,13 +957,12 @@ def _diag(x: Tensor): # like .diag(), but works for tensors with 3 dims. else: (batch, dim, dim) = x.shape x = x.reshape(batch, dim * dim) - x = x[:, ::dim+1] + x = x[:, :: dim + 1] assert x.shape == (batch, dim) return x -def _whitening_metric(x: Tensor, - num_groups: int): +def _whitening_metric(x: Tensor, num_groups: int): """ Computes the "whitening metric", a value which will be 1.0 if all the eigenvalues of of the centered feature covariance are the same within each group's covariance matrix @@ -946,25 +992,22 @@ def _whitening_metric(x: Tensor, # the following expression is what we'd get if we took the matrix product # of each covariance and measured the mean of its trace, i.e. # the same as _diag(torch.matmul(x_covar, x_covar)).mean(). - x_covarsq_mean_diag = (x_covar ** 2).sum() / (num_groups * channels_per_group) + x_covarsq_mean_diag = (x_covar**2).sum() / (num_groups * channels_per_group) # this metric will be >= 1.0; the larger it is, the less 'white' the data was. - metric = x_covarsq_mean_diag / (x_covar_mean_diag ** 2 + 1.0e-20) + metric = x_covarsq_mean_diag / (x_covar_mean_diag**2 + 1.0e-20) return metric class WhiteningPenaltyFunction(torch.autograd.Function): @staticmethod - def forward(ctx, - x: Tensor, - module: nn.Module) -> Tensor: + def forward(ctx, x: Tensor, module: nn.Module) -> Tensor: ctx.save_for_backward(x) ctx.module = module return x @staticmethod - def backward(ctx, - x_grad: Tensor): - x_orig, = ctx.saved_tensors + def backward(ctx, x_grad: Tensor): + (x_orig,) = ctx.saved_tensors w = ctx.module try: @@ -976,8 +1019,10 @@ class WhiteningPenaltyFunction(torch.autograd.Function): metric = _whitening_metric(x_detached, w.num_groups) if random.random() < 0.005 or __name__ == "__main__": - logging.info(f"Whitening: name={w.name}, num_groups={w.num_groups}, num_channels={x_orig.shape[-1]}, " - f"metric={metric.item():.2f} vs. limit={float(w.whitening_limit)}") + logging.info( + f"Whitening: name={w.name}, num_groups={w.num_groups}, num_channels={x_orig.shape[-1]}, " + f"metric={metric.item():.2f} vs. limit={float(w.whitening_limit)}" + ) if metric < float(w.whitening_limit): w.prob = w.min_prob @@ -986,22 +1031,27 @@ class WhiteningPenaltyFunction(torch.autograd.Function): w.prob = w.max_prob metric.backward() penalty_grad = x_detached.grad - scale = w.grad_scale * (x_grad.to(torch.float32).norm() / - (penalty_grad.norm() + 1.0e-20)) + scale = w.grad_scale * ( + x_grad.to(torch.float32).norm() + / (penalty_grad.norm() + 1.0e-20) + ) penalty_grad = penalty_grad * scale return x_grad + penalty_grad.to(x_grad.dtype), None except Exception as e: - logging.info(f"Caught exception in Whiten backward: {e}, size={list(x_grad.shape)}, will continue.") + logging.info( + f"Caught exception in Whiten backward: {e}, size={list(x_grad.shape)}, will continue." + ) return x_grad, None class Whiten(nn.Module): def __init__( - self, - num_groups: int, - whitening_limit: FloatLike, - prob: Union[float, Tuple[float,float]], - grad_scale: FloatLike): + self, + num_groups: int, + whitening_limit: FloatLike, + prob: Union[float, Tuple[float, float]], + grad_scale: FloatLike, + ): """ Args: num_groups: the number of groups to divide the channel dim into before @@ -1033,10 +1083,9 @@ class Whiten(nn.Module): (self.min_prob, self.max_prob) = prob assert 0 < self.min_prob <= self.max_prob <= 1 self.prob = self.max_prob - self.name = None # will be set in training loop + self.name = None # will be set in training loop - def forward(self, - x: Tensor) -> Tensor: + def forward(self, x: Tensor) -> Tensor: """ In the forward pass, this function just returns the input unmodified. In the backward pass, it will modify the gradients to ensure that the @@ -1071,9 +1120,11 @@ class WithLoss(torch.autograd.Function): @staticmethod def backward(ctx, ans_grad: Tensor): - return ans_grad, torch.ones(ctx.y_shape, - dtype=ans_grad.dtype, - device=ans_grad.device), None + return ( + ans_grad, + torch.ones(ctx.y_shape, dtype=ans_grad.dtype, device=ans_grad.device), + None, + ) def with_loss(x, y, name): @@ -1118,20 +1169,21 @@ class LimitParamValue(torch.autograd.Function): @staticmethod def backward(ctx, x_grad: Tensor): - x, = ctx.saved_tensors + (x,) = ctx.saved_tensors # where x < ctx.min, ensure all grads are negative (this will tend to make # x more positive). - x_grad = x_grad * torch.where(torch.logical_and(x_grad > 0, x < ctx.min), -1.0, 1.0) + x_grad = x_grad * torch.where( + torch.logical_and(x_grad > 0, x < ctx.min), -1.0, 1.0 + ) # where x > ctx.max, ensure all grads are positive (this will tend to make # x more negative). x_grad *= torch.where(torch.logical_and(x_grad < 0, x > ctx.max), -1.0, 1.0) return x_grad, None, None -def limit_param_value(x: Tensor, - min: float, max: float, - prob: float = 0.6, - training: bool = True): +def limit_param_value( + x: Tensor, min: float, max: float, prob: float = 0.6, training: bool = True +): # You apply this to (typically) an nn.Parameter during training to ensure that its # (elements mostly) stays within a supplied range. This is done by modifying the # gradients in backprop. @@ -1187,7 +1239,7 @@ class DoubleSwishFunction(torch.autograd.Function): y = x * s if requires_grad: - deriv = (y * (1 - s) + s) + deriv = y * (1 - s) + s # notes on derivative of x * sigmoid(x - 1): # https://www.wolframalpha.com/input?i=d%2Fdx+%28x+*+sigmoid%28x-1%29%29 @@ -1197,7 +1249,9 @@ class DoubleSwishFunction(torch.autograd.Function): # floors), should be expectation-preserving. floor = -0.044 ceil = 1.2 - d_scaled = ((deriv - floor) * (255.0 / (ceil - floor)) + torch.rand_like(deriv)) + d_scaled = (deriv - floor) * (255.0 / (ceil - floor)) + torch.rand_like( + deriv + ) if __name__ == "__main__": # for self-testing only. assert d_scaled.min() >= 0.0 @@ -1210,12 +1264,12 @@ class DoubleSwishFunction(torch.autograd.Function): @staticmethod def backward(ctx, y_grad: Tensor) -> Tensor: - d, = ctx.saved_tensors + (d,) = ctx.saved_tensors # the same constants as used in forward pass. floor = -0.043637 ceil = 1.2 - d = (d * ((ceil - floor) / 255.0) + floor) + d = d * ((ceil - floor) / 255.0) + floor return y_grad * d @@ -1239,9 +1293,7 @@ class Dropout2(nn.Module): self.p = p def forward(self, x: Tensor) -> Tensor: - return torch.nn.functional.dropout(x, - p=float(self.p), - training=self.training) + return torch.nn.functional.dropout(x, p=float(self.p), training=self.training) class MulForDropout3(torch.autograd.Function): @@ -1259,7 +1311,7 @@ class MulForDropout3(torch.autograd.Function): @staticmethod @custom_bwd def backward(ctx, ans_grad): - ans, = ctx.saved_tensors + (ans,) = ctx.saved_tensors x_grad = ctx.alpha * ans_grad * (ans != 0) return x_grad, None, None @@ -1286,7 +1338,7 @@ class Dropout3(nn.Module): class SwooshLFunction(torch.autograd.Function): """ - swoosh_l(x) = log(1 + exp(x-4)) - 0.08*x - 0.035 + swoosh_l(x) = log(1 + exp(x-4)) - 0.08*x - 0.035 """ @staticmethod @@ -1308,13 +1360,15 @@ class SwooshLFunction(torch.autograd.Function): if not requires_grad: return y - y.backward(gradient = torch.ones_like(y)) + y.backward(gradient=torch.ones_like(y)) grad = x.grad floor = coeff ceil = 1.0 + coeff + 0.005 - d_scaled = ((grad - floor) * (255.0 / (ceil - floor)) + torch.rand_like(grad)) + d_scaled = (grad - floor) * (255.0 / (ceil - floor)) + torch.rand_like( + grad + ) if __name__ == "__main__": # for self-testing only. assert d_scaled.min() >= 0.0 @@ -1328,20 +1382,19 @@ class SwooshLFunction(torch.autograd.Function): @staticmethod def backward(ctx, y_grad: Tensor) -> Tensor: - d, = ctx.saved_tensors + (d,) = ctx.saved_tensors # the same constants as used in forward pass. coeff = -0.08 floor = coeff ceil = 1.0 + coeff + 0.005 - d = (d * ((ceil - floor) / 255.0) + floor) - return (y_grad * d) + d = d * ((ceil - floor) / 255.0) + floor + return y_grad * d class SwooshL(torch.nn.Module): def forward(self, x: Tensor) -> Tensor: - """Return Swoosh-L activation. - """ + """Return Swoosh-L activation.""" if torch.jit.is_scripting() or torch.jit.is_tracing(): zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) return logaddexp(zero, x - 4.0) - 0.08 * x - 0.035 @@ -1351,19 +1404,19 @@ class SwooshL(torch.nn.Module): return k2.swoosh_l(x) # return SwooshLFunction.apply(x) + class SwooshLOnnx(torch.nn.Module): def forward(self, x: Tensor) -> Tensor: - """Return Swoosh-L activation. - """ + """Return Swoosh-L activation.""" zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) return logaddexp_onnx(zero, x - 4.0) - 0.08 * x - 0.035 class SwooshRFunction(torch.autograd.Function): """ - swoosh_r(x) = log(1 + exp(x-1)) - 0.08*x - 0.313261687 + swoosh_r(x) = log(1 + exp(x-1)) - 0.08*x - 0.313261687 - derivatives are between -0.08 and 0.92. + derivatives are between -0.08 and 0.92. """ @staticmethod @@ -1379,17 +1432,19 @@ class SwooshRFunction(torch.autograd.Function): with torch.enable_grad(): x = x.detach() x.requires_grad = True - y = torch.logaddexp(zero, x - 1.) - 0.08 * x - 0.313261687 + y = torch.logaddexp(zero, x - 1.0) - 0.08 * x - 0.313261687 if not requires_grad: return y - y.backward(gradient = torch.ones_like(y)) + y.backward(gradient=torch.ones_like(y)) grad = x.grad floor = -0.08 ceil = 0.925 - d_scaled = ((grad - floor) * (255.0 / (ceil - floor)) + torch.rand_like(grad)) + d_scaled = (grad - floor) * (255.0 / (ceil - floor)) + torch.rand_like( + grad + ) if __name__ == "__main__": # for self-testing only. assert d_scaled.min() >= 0.0 @@ -1403,33 +1458,32 @@ class SwooshRFunction(torch.autograd.Function): @staticmethod def backward(ctx, y_grad: Tensor) -> Tensor: - d, = ctx.saved_tensors + (d,) = ctx.saved_tensors # the same constants as used in forward pass. floor = -0.08 ceil = 0.925 - d = (d * ((ceil - floor) / 255.0) + floor) - return (y_grad * d) + d = d * ((ceil - floor) / 255.0) + floor + return y_grad * d class SwooshR(torch.nn.Module): def forward(self, x: Tensor) -> Tensor: - """Return Swoosh-R activation. - """ + """Return Swoosh-R activation.""" if torch.jit.is_scripting() or torch.jit.is_tracing(): zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) - return logaddexp(zero, x - 1.) - 0.08 * x - 0.313261687 + return logaddexp(zero, x - 1.0) - 0.08 * x - 0.313261687 if not x.requires_grad: return k2.swoosh_r_forward(x) else: return k2.swoosh_r(x) # return SwooshRFunction.apply(x) + class SwooshROnnx(torch.nn.Module): def forward(self, x: Tensor) -> Tensor: - """Return Swoosh-R activation. - """ + """Return Swoosh-R activation.""" zero = torch.tensor(0.0, dtype=x.dtype, device=x.device) - return logaddexp_onnx(zero, x - 1.) - 0.08 * x - 0.313261687 + return logaddexp_onnx(zero, x - 1.0) - 0.08 * x - 0.313261687 # simple version of SwooshL that does not redefine the backprop, used in @@ -1437,7 +1491,7 @@ class SwooshROnnx(torch.nn.Module): def SwooshLForward(x: Tensor): x_offset = x - 4.0 log_sum = (1.0 + x_offset.exp()).log().to(x.dtype) - log_sum = torch.where(log_sum == float('inf'), x_offset, log_sum) + log_sum = torch.where(log_sum == float("inf"), x_offset, log_sum) return log_sum - 0.08 * x - 0.035 @@ -1446,28 +1500,30 @@ def SwooshLForward(x: Tensor): def SwooshRForward(x: Tensor): x_offset = x - 1.0 log_sum = (1.0 + x_offset.exp()).log().to(x.dtype) - log_sum = torch.where(log_sum == float('inf'), x_offset, log_sum) + log_sum = torch.where(log_sum == float("inf"), x_offset, log_sum) return log_sum - 0.08 * x - 0.313261687 class ActivationDropoutAndLinearFunction(torch.autograd.Function): @staticmethod @custom_fwd - def forward(ctx, - x: Tensor, - weight: Tensor, - bias: Optional[Tensor], - activation: str, - dropout_p: float, - dropout_shared_dim: Optional[int]): + def forward( + ctx, + x: Tensor, + weight: Tensor, + bias: Optional[Tensor], + activation: str, + dropout_p: float, + dropout_shared_dim: Optional[int], + ): if dropout_p != 0.0: dropout_shape = list(x.shape) if dropout_shared_dim is not None: dropout_shape[dropout_shared_dim] = 1 # else it won't be very memory efficient. - dropout_mask = ((1.0 / (1.0 - dropout_p)) * - (torch.rand(*dropout_shape, - device=x.device, dtype=x.dtype) > dropout_p)) + dropout_mask = (1.0 / (1.0 - dropout_p)) * ( + torch.rand(*dropout_shape, device=x.device, dtype=x.dtype) > dropout_p + ) else: dropout_mask = None @@ -1476,8 +1532,8 @@ class ActivationDropoutAndLinearFunction(torch.autograd.Function): ctx.activation = activation forward_activation_dict = { - 'SwooshL': k2.swoosh_l_forward, - 'SwooshR': k2.swoosh_r_forward + "SwooshL": k2.swoosh_l_forward, + "SwooshR": k2.swoosh_r_forward, } # it will raise a KeyError if this fails. This will be an error. We let it # propagate to the user. @@ -1495,8 +1551,8 @@ class ActivationDropoutAndLinearFunction(torch.autograd.Function): (x, weight, bias, dropout_mask) = saved forward_and_deriv_activation_dict = { - 'SwooshL': k2.swoosh_l_forward_and_deriv, - 'SwooshR': k2.swoosh_r_forward_and_deriv + "SwooshL": k2.swoosh_l_forward_and_deriv, + "SwooshR": k2.swoosh_r_forward_and_deriv, } # the following lines a KeyError if the activation is unrecognized. # This will be an error. We let it propagate to the user. @@ -1511,8 +1567,7 @@ class ActivationDropoutAndLinearFunction(torch.autograd.Function): in_channels = y.shape[-1] g = ans_grad.reshape(-1, out_channels) - weight_deriv = torch.matmul(g.t(), - y.reshape(-1, in_channels)) + weight_deriv = torch.matmul(g.t(), y.reshape(-1, in_channels)) y_deriv = torch.matmul(ans_grad, weight) bias_deriv = None if bias is None else g.sum(dim=0) x_deriv = y_deriv * func_deriv @@ -1525,71 +1580,76 @@ class ActivationDropoutAndLinearFunction(torch.autograd.Function): class ActivationDropoutAndLinear(torch.nn.Module): """ - This merges an activation function followed by dropout and then a nn.Linear module; - it does so in a memory efficient way so that it only stores the input to the whole - module. If activation == SwooshL and dropout_shared_dim != None, this will be - equivalent to: - nn.Sequential(SwooshL(), - Dropout3(dropout_p, shared_dim=dropout_shared_dim), - ScaledLinear(in_channels, out_channels, bias=bias, - initial_scale=initial_scale)) - If dropout_shared_dim is None, the dropout would be equivalent to - Dropout2(dropout_p). Note: Dropout3 will be more memory efficient as the dropout - mask is smaller. + This merges an activation function followed by dropout and then a nn.Linear module; + it does so in a memory efficient way so that it only stores the input to the whole + module. If activation == SwooshL and dropout_shared_dim != None, this will be + equivalent to: + nn.Sequential(SwooshL(), + Dropout3(dropout_p, shared_dim=dropout_shared_dim), + ScaledLinear(in_channels, out_channels, bias=bias, + initial_scale=initial_scale)) + If dropout_shared_dim is None, the dropout would be equivalent to + Dropout2(dropout_p). Note: Dropout3 will be more memory efficient as the dropout + mask is smaller. - Args: - in_channels: number of input channels, e.g. 256 - out_channels: number of output channels, e.g. 256 - bias: if true, have a bias - activation: the activation function, for now just support SwooshL. - dropout_p: the dropout probability or schedule (happens after nonlinearity). - dropout_shared_dim: the dimension, if any, across which the dropout mask is - shared (e.g. the time dimension). If None, this may be less memory - efficient if there are modules before this one that cache the input - for their backprop (e.g. Balancer or Whiten). + Args: + in_channels: number of input channels, e.g. 256 + out_channels: number of output channels, e.g. 256 + bias: if true, have a bias + activation: the activation function, for now just support SwooshL. + dropout_p: the dropout probability or schedule (happens after nonlinearity). + dropout_shared_dim: the dimension, if any, across which the dropout mask is + shared (e.g. the time dimension). If None, this may be less memory + efficient if there are modules before this one that cache the input + for their backprop (e.g. Balancer or Whiten). """ - def __init__(self, - in_channels: int, - out_channels: int, - bias: bool = True, - activation: str = 'SwooshL', - dropout_p: FloatLike = 0.0, - dropout_shared_dim: Optional[int] = -1, - initial_scale: float = 1.0): + + def __init__( + self, + in_channels: int, + out_channels: int, + bias: bool = True, + activation: str = "SwooshL", + dropout_p: FloatLike = 0.0, + dropout_shared_dim: Optional[int] = -1, + initial_scale: float = 1.0, + ): super().__init__() # create a temporary module of nn.Linear that we'll steal the # weights and bias from - l = ScaledLinear(in_channels, out_channels, - bias=bias, - initial_scale=initial_scale) + l = ScaledLinear( + in_channels, out_channels, bias=bias, initial_scale=initial_scale + ) self.weight = l.weight # register_parameter properly handles making it a parameter when l.bias # is None. I think there is some reason for doing it this way rather # than just setting it to None but I don't know what it is, maybe # something to do with exporting the module.. - self.register_parameter('bias', l.bias) + self.register_parameter("bias", l.bias) self.activation = activation self.dropout_p = dropout_p self.dropout_shared_dim = dropout_shared_dim - def forward(self, - x: Tensor): + def forward(self, x: Tensor): if torch.jit.is_scripting() or torch.jit.is_tracing(): - if self.activation == 'SwooshL': + if self.activation == "SwooshL": x = SwooshLForward(x) elif self.activation == "SwooshR": x = SwooshRForward(x) else: assert False, self.activation - return torch.nn.functional.linear(x, - self.weight, - self.bias) + return torch.nn.functional.linear(x, self.weight, self.bias) return ActivationDropoutAndLinearFunction.apply( - x, self.weight, self.bias, self.activation, - float(self.dropout_p), self.dropout_shared_dim) + x, + self.weight, + self.bias, + self.activation, + float(self.dropout_p), + self.dropout_shared_dim, + ) def convert_num_channels(x: Tensor, num_channels: int) -> Tensor: @@ -1612,10 +1672,9 @@ def _test_whiten(): x.requires_grad = True - m = Whiten(1, # num_groups - 5.0, # whitening_limit, - prob=1.0, - grad_scale=0.1) # grad_scale + m = Whiten( + 1, 5.0, prob=1.0, grad_scale=0.1 # num_groups # whitening_limit, + ) # grad_scale for _ in range(4): y = m(x) @@ -1656,9 +1715,7 @@ def _test_balancer_sign(): def _test_balancer_magnitude(): magnitudes = torch.arange(0, 1, 0.01) N = 1000 - x = torch.sign(torch.randn(magnitudes.numel(), N)) * magnitudes.unsqueeze( - -1 - ) + x = torch.sign(torch.randn(magnitudes.numel(), N)) * magnitudes.unsqueeze(-1) x = x.detach() x.requires_grad = True m = Balancer( @@ -1685,7 +1742,7 @@ def _test_double_swish_deriv(): x.requires_grad = True m = DoubleSwish() - tol = ((1.2-(-0.043637))/255.0) + tol = (1.2 - (-0.043637)) / 255.0 torch.autograd.gradcheck(m, x, atol=tol) # for self-test. @@ -1699,7 +1756,7 @@ def _test_swooshl_deriv(): x.requires_grad = True m = SwooshL() - tol = (1.0 / 255.0) + tol = 1.0 / 255.0 torch.autograd.gradcheck(m, x, atol=tol, eps=0.01) # for self-test. @@ -1713,7 +1770,7 @@ def _test_swooshr_deriv(): x.requires_grad = True m = SwooshR() - tol = (1.0 / 255.0) + tol = 1.0 / 255.0 torch.autograd.gradcheck(m, x, atol=tol, eps=0.01) # for self-test. @@ -1727,24 +1784,24 @@ def _test_softmax(): b = a.clone() a.requires_grad = True b.requires_grad = True - a.softmax(dim=1)[:,0].sum().backward() + a.softmax(dim=1)[:, 0].sum().backward() print("a grad = ", a.grad) - softmax(b, dim=1)[:,0].sum().backward() + softmax(b, dim=1)[:, 0].sum().backward() print("b grad = ", b.grad) assert torch.allclose(a.grad, b.grad) def _test_piecewise_linear(): - p = PiecewiseLinear( (0, 10.0) ) + p = PiecewiseLinear((0, 10.0)) for x in [-100, 0, 100]: assert p(x) == 10.0 - p = PiecewiseLinear( (0, 10.0), (1, 0.0) ) - for x, y in [ (-100, 10.0), (0, 10.0), (0.5, 5.0), (1, 0.0), (2, 0.0) ]: + p = PiecewiseLinear((0, 10.0), (1, 0.0)) + for x, y in [(-100, 10.0), (0, 10.0), (0.5, 5.0), (1, 0.0), (2, 0.0)]: print("x, y = ", x, y) assert p(x) == y, (x, p(x), y) q = PiecewiseLinear((0.5, 15.0), (0.6, 1.0)) - x_vals = [ -1.0, 0.0, 0.1, 0.2, 0.5, 0.6, 0.7, 0.9, 1.0, 2.0 ] + x_vals = [-1.0, 0.0, 0.1, 0.2, 0.5, 0.6, 0.7, 0.9, 1.0, 2.0] pq = p.max(q) for x in x_vals: y1 = max(p(x), q(x)) @@ -1757,7 +1814,7 @@ def _test_piecewise_linear(): assert abs(y1 - y2) < 0.001 pq = p + q for x in x_vals: - y1 = p(x) + q(x) + y1 = p(x) + q(x) y2 = pq(x) assert abs(y1 - y2) < 0.001 @@ -1772,15 +1829,22 @@ def _test_activation_dropout_and_linear(): # swoosh_l an swoosh_r inside SwooshL() and SwooshR(), and they call randn() # internally, messing up the random state. for dropout_p in [0.0]: - for activation in ['SwooshL', 'SwooshR']: - m1 = nn.Sequential(SwooshL() if activation == 'SwooshL' else SwooshR(), - Dropout3(p=dropout_p, shared_dim=-1), - ScaledLinear(in_channels, out_channels, bias=bias, - initial_scale=0.5)) - m2 = ActivationDropoutAndLinear(in_channels, out_channels, - bias=bias, initial_scale=0.5, - activation=activation, - dropout_p=dropout_p) + for activation in ["SwooshL", "SwooshR"]: + m1 = nn.Sequential( + SwooshL() if activation == "SwooshL" else SwooshR(), + Dropout3(p=dropout_p, shared_dim=-1), + ScaledLinear( + in_channels, out_channels, bias=bias, initial_scale=0.5 + ), + ) + m2 = ActivationDropoutAndLinear( + in_channels, + out_channels, + bias=bias, + initial_scale=0.5, + activation=activation, + dropout_p=dropout_p, + ) with torch.no_grad(): m2.weight[:] = m1[2].weight if bias: @@ -1790,9 +1854,9 @@ def _test_activation_dropout_and_linear(): x1.requires_grad = True # TEMP. - assert torch.allclose(SwooshRFunction.apply(x1), - SwooshRForward(x1), - atol=1.0e-03) + assert torch.allclose( + SwooshRFunction.apply(x1), SwooshRForward(x1), atol=1.0e-03 + ) x2 = x1.clone().detach() x2.requires_grad = True @@ -1805,21 +1869,24 @@ def _test_activation_dropout_and_linear(): y2 = m2(x2) y2.backward(gradient=y_grad) - print(f"bias = {bias}, dropout_p = {dropout_p}, activation = {activation}") + print( + f"bias = {bias}, dropout_p = {dropout_p}, activation = {activation}" + ) print("y1 = ", y1) print("y2 = ", y2) assert torch.allclose(y1, y2, atol=0.02) - assert torch.allclose(m1[2].weight.grad, m2.weight.grad, - atol=1.0e-05) + assert torch.allclose(m1[2].weight.grad, m2.weight.grad, atol=1.0e-05) if bias: - assert torch.allclose(m1[2].bias.grad, m2.bias.grad, - atol=1.0e-05) + assert torch.allclose(m1[2].bias.grad, m2.bias.grad, atol=1.0e-05) print("x1.grad = ", x1.grad) print("x2.grad = ", x2.grad) def isclose(a, b): # return true if cosine similarity is > 0.9. - return (a * b).sum() > 0.9 * ((a**2).sum() * (b**2).sum()).sqrt() + return (a * b).sum() > 0.9 * ( + (a**2).sum() * (b**2).sum() + ).sqrt() + # the SwooshL() implementation has a noisy gradient due to 1-byte # storage of it. assert isclose(x1.grad, x2.grad) diff --git a/egs/librispeech/ASR/zipformer/streaming_decode.py b/egs/librispeech/ASR/zipformer/streaming_decode.py index 44ff392a3..904caf8af 100755 --- a/egs/librispeech/ASR/zipformer/streaming_decode.py +++ b/egs/librispeech/ASR/zipformer/streaming_decode.py @@ -282,9 +282,7 @@ def stack_states(state_list: List[List[torch.Tensor]]) -> List[torch.Tensor]: ) batch_states.append(cached_embed_left_pad) - processed_lens = torch.cat( - [state_list[i][-1] for i in range(batch_size)], dim=0 - ) + processed_lens = torch.cat([state_list[i][-1] for i in range(batch_size)], dim=0) batch_states.append(processed_lens) return batch_states @@ -322,9 +320,7 @@ def unstack_states(batch_states: List[Tensor]) -> List[List[Tensor]]: for layer in range(tot_num_layers): layer_offset = layer * 6 # cached_key: (left_context_len, batch_size, key_dim) - cached_key_list = batch_states[layer_offset].chunk( - chunks=batch_size, dim=1 - ) + cached_key_list = batch_states[layer_offset].chunk(chunks=batch_size, dim=1) # cached_nonlin_attn: (num_heads, batch_size, left_context_len, head_dim) cached_nonlin_attn_list = batch_states[layer_offset + 1].chunk( chunks=batch_size, dim=1 @@ -355,9 +351,7 @@ def unstack_states(batch_states: List[Tensor]) -> List[List[Tensor]]: cached_conv2_list[i], ] - cached_embed_left_pad_list = batch_states[-2].chunk( - chunks=batch_size, dim=0 - ) + cached_embed_left_pad_list = batch_states[-2].chunk(chunks=batch_size, dim=0) for i in range(batch_size): state_list[i].append(cached_embed_left_pad_list[i]) @@ -380,11 +374,7 @@ def streaming_forward( Returns encoder outputs, output lengths, and updated states. """ cached_embed_left_pad = states[-2] - ( - x, - x_lens, - new_cached_embed_left_pad, - ) = model.encoder_embed.streaming_forward( + (x, x_lens, new_cached_embed_left_pad,) = model.encoder_embed.streaming_forward( x=features, x_lens=feature_lens, cached_left_pad=cached_embed_left_pad, @@ -404,9 +394,7 @@ def streaming_forward( new_processed_lens = processed_lens + x_lens # (batch, left_context_size + chunk_size) - src_key_padding_mask = torch.cat( - [processed_mask, src_key_padding_mask], dim=1 - ) + src_key_padding_mask = torch.cat([processed_mask, src_key_padding_mask], dim=1) x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) encoder_states = states[:-2] @@ -494,9 +482,7 @@ def decode_one_chunk( encoder_out = model.joiner.encoder_proj(encoder_out) if params.decoding_method == "greedy_search": - greedy_search( - model=model, encoder_out=encoder_out, streams=decode_streams - ) + greedy_search(model=model, encoder_out=encoder_out, streams=decode_streams) elif params.decoding_method == "fast_beam_search": processed_lens = torch.tensor(processed_lens, device=device) processed_lens = processed_lens + encoder_out_lens @@ -517,9 +503,7 @@ def decode_one_chunk( num_active_paths=params.num_active_paths, ) else: - raise ValueError( - f"Unsupported decoding method: {params.decoding_method}" - ) + raise ValueError(f"Unsupported decoding method: {params.decoding_method}") states = unstack_states(new_states) @@ -577,9 +561,7 @@ def decode_dataset( decode_streams = [] for num, cut in enumerate(cuts): # each utterance has a DecodeStream. - initial_states = get_init_states( - model=model, batch_size=1, device=device - ) + initial_states = get_init_states(model=model, batch_size=1, device=device) decode_stream = DecodeStream( params=params, cut_id=cut.id, @@ -649,9 +631,7 @@ def decode_dataset( elif params.decoding_method == "modified_beam_search": key = f"num_active_paths_{params.num_active_paths}" else: - raise ValueError( - f"Unsupported decoding method: {params.decoding_method}" - ) + raise ValueError(f"Unsupported decoding method: {params.decoding_method}") return {key: decode_results} @@ -684,8 +664,7 @@ def save_results( test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) errs_info = ( - params.res_dir - / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" + params.res_dir / f"wer-summary-{test_set_name}-{key}-{params.suffix}.txt" ) with open(errs_info, "w") as f: print("settings\tWER", file=f) @@ -718,9 +697,7 @@ def main(): params.suffix = f"epoch-{params.epoch}-avg-{params.avg}" assert params.causal, params.causal - assert ( - "," not in params.chunk_size - ), "chunk_size should be one value in decoding." + assert "," not in params.chunk_size, "chunk_size should be one value in decoding." assert ( "," not in params.left_context_frames ), "left_context_frames should be one value in decoding." @@ -760,9 +737,9 @@ def main(): if not params.use_averaged_model: if params.iter > 0: - filenames = find_checkpoints( - params.exp_dir, iteration=-params.iter - )[: params.avg] + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] if len(filenames) == 0: raise ValueError( f"No checkpoints found for" @@ -789,9 +766,9 @@ def main(): model.load_state_dict(average_checkpoints(filenames, device=device)) else: if params.iter > 0: - filenames = find_checkpoints( - params.exp_dir, iteration=-params.iter - )[: params.avg + 1] + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] if len(filenames) == 0: raise ValueError( f"No checkpoints found for" diff --git a/egs/librispeech/ASR/zipformer/subsampling.py b/egs/librispeech/ASR/zipformer/subsampling.py index 6532ddccb..d16d87bac 100644 --- a/egs/librispeech/ASR/zipformer/subsampling.py +++ b/egs/librispeech/ASR/zipformer/subsampling.py @@ -107,9 +107,7 @@ class ConvNeXt(nn.Module): if layerdrop_rate != 0.0: batch_size = x.shape[0] mask = ( - torch.rand( - (batch_size, 1, 1, 1), dtype=x.dtype, device=x.device - ) + torch.rand((batch_size, 1, 1, 1), dtype=x.dtype, device=x.device) > layerdrop_rate ) else: @@ -278,9 +276,7 @@ class Conv2dSubsampling(nn.Module): # many copies of this extra gradient term. self.out_whiten = Whiten( num_groups=1, - whitening_limit=ScheduledFloat( - (0.0, 4.0), (20000.0, 8.0), default=4.0 - ), + whitening_limit=ScheduledFloat((0.0, 4.0), (20000.0, 8.0), default=4.0), prob=(0.025, 0.25), grad_scale=0.02, ) @@ -331,7 +327,7 @@ class Conv2dSubsampling(nn.Module): with warnings.catch_warnings(): warnings.simplefilter("ignore") x_lens = (x_lens - 7) // 2 - assert x.size(1) == x_lens.max().item() , (x.size(1), x_lens.max()) + assert x.size(1) == x_lens.max().item(), (x.size(1), x_lens.max()) return x, x_lens @@ -403,8 +399,8 @@ class Conv2dSubsampling(nn.Module): left_pad = self.convnext.padding[0] freq = self.out_width channels = self.layer3_channels - cached_embed_left_pad = torch.zeros( - batch_size, channels, left_pad, freq - ).to(device) + cached_embed_left_pad = torch.zeros(batch_size, channels, left_pad, freq).to( + device + ) return cached_embed_left_pad diff --git a/egs/librispeech/ASR/zipformer/train.py b/egs/librispeech/ASR/zipformer/train.py index bc3e9c1ba..7009f3346 100755 --- a/egs/librispeech/ASR/zipformer/train.py +++ b/egs/librispeech/ASR/zipformer/train.py @@ -604,11 +604,11 @@ def get_joiner_model(params: AttributeDict) -> nn.Module: def get_model(params: AttributeDict) -> nn.Module: - assert ( - params.use_transducer or params.use_ctc - ), (f"At least one of them should be True, " + assert params.use_transducer or params.use_ctc, ( + f"At least one of them should be True, " f"but got params.use_transducer={params.use_transducer}, " - f"params.use_ctc={params.use_ctc}") + f"params.use_ctc={params.use_ctc}" + ) encoder_embed = get_encoder_embed(params) encoder = get_encoder_model(params) @@ -808,17 +808,16 @@ def compute_loss( # take down the scale on the simple loss from 1.0 at the start # to params.simple_loss scale by warm_step. simple_loss_scale = ( - s if batch_idx_train >= warm_step + s + if batch_idx_train >= warm_step else 1.0 - (batch_idx_train / warm_step) * (1.0 - s) ) pruned_loss_scale = ( - 1.0 if batch_idx_train >= warm_step + 1.0 + if batch_idx_train >= warm_step else 0.1 + 0.9 * (batch_idx_train / warm_step) ) - loss += ( - simple_loss_scale * simple_loss - + pruned_loss_scale * pruned_loss - ) + loss += simple_loss_scale * simple_loss + pruned_loss_scale * pruned_loss if params.use_ctc: loss += params.ctc_loss_scale * ctc_loss @@ -1166,7 +1165,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/librispeech/ASR/zipformer_mmi/train.py b/egs/librispeech/ASR/zipformer_mmi/train.py index c1b3ea3e0..4b50acdde 100755 --- a/egs/librispeech/ASR/zipformer_mmi/train.py +++ b/egs/librispeech/ASR/zipformer_mmi/train.py @@ -981,7 +981,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/mgb2/ASR/pruned_transducer_stateless5/train.py b/egs/mgb2/ASR/pruned_transducer_stateless5/train.py index a68702776..48468cfbd 100755 --- a/egs/mgb2/ASR/pruned_transducer_stateless5/train.py +++ b/egs/mgb2/ASR/pruned_transducer_stateless5/train.py @@ -746,7 +746,6 @@ def train_one_epoch( tot_loss = MetricsTracker() for batch_idx, batch in enumerate(train_dl): - if batch["inputs"].shape[0] == len(batch["supervisions"]["text"]): params.batch_idx_train += 1 batch_size = len(batch["supervisions"]["text"]) @@ -966,7 +965,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) @@ -1019,7 +1018,6 @@ def run(rank, world_size, args): scaler.load_state_dict(checkpoints["grad_scaler"]) for epoch in range(params.start_epoch, params.num_epochs + 1): - scheduler.step_epoch(epoch - 1) fix_random_seed(params.seed + epoch - 1) train_dl.sampler.set_epoch(epoch - 1) @@ -1118,7 +1116,6 @@ def scan_pessimistic_batches_for_oom( # (i.e. are not remembered by the decaying-average in adam), because # we want to avoid these params being subject to shrinkage in adam. with torch.cuda.amp.autocast(enabled=params.use_fp16): - loss, _, _ = compute_loss( params=params, model=model, diff --git a/egs/multi_zh-hans/ASR/zipformer/train.py b/egs/multi_zh-hans/ASR/zipformer/train.py index 4f2d728be..c1bbd2ee8 100755 --- a/egs/multi_zh-hans/ASR/zipformer/train.py +++ b/egs/multi_zh-hans/ASR/zipformer/train.py @@ -1164,7 +1164,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py b/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py index 417515968..d03970265 100755 --- a/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless5/train.py @@ -915,7 +915,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py index d80e0147c..aee3972cd 100755 --- a/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py +++ b/egs/tal_csasr/ASR/pruned_transducer_stateless7_bbpe/train.py @@ -69,7 +69,7 @@ from torch.nn.parallel import DistributedDataParallel as DDP from torch.utils.tensorboard import SummaryWriter from zipformer import Zipformer -from icefall import diagnostics, byte_encode, tokenize_by_CJK_char +from icefall import byte_encode, diagnostics, tokenize_by_CJK_char from icefall.checkpoint import load_checkpoint, remove_checkpoints from icefall.checkpoint import save_checkpoint as save_checkpoint_impl from icefall.checkpoint import ( @@ -1018,7 +1018,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/tedlium3/ASR/conformer_ctc2/train.py b/egs/tedlium3/ASR/conformer_ctc2/train.py index 42e4c010a..fc3e3b2d9 100755 --- a/egs/tedlium3/ASR/conformer_ctc2/train.py +++ b/egs/tedlium3/ASR/conformer_ctc2/train.py @@ -905,7 +905,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/tedlium3/ASR/zipformer/train.py b/egs/tedlium3/ASR/zipformer/train.py index 9271c8438..33d03908c 100755 --- a/egs/tedlium3/ASR/zipformer/train.py +++ b/egs/tedlium3/ASR/zipformer/train.py @@ -1126,7 +1126,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/finetune.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/finetune.py index e703100a9..82bc882bd 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/finetune.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/finetune.py @@ -886,7 +886,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py b/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py index 48b347b64..49977e01b 100644 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py @@ -851,7 +851,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py b/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py index 8e1b12dba..931e699d9 100755 --- a/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py +++ b/egs/wenetspeech/ASR/pruned_transducer_stateless5/train.py @@ -985,7 +985,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/wenetspeech/ASR/zipformer/train.py b/egs/wenetspeech/ASR/zipformer/train.py index 83dbfa22f..b1557dedb 100755 --- a/egs/wenetspeech/ASR/zipformer/train.py +++ b/egs/wenetspeech/ASR/zipformer/train.py @@ -1128,7 +1128,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/train.py b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/train.py index 5b5ac17be..a6fa46b17 100755 --- a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/train.py +++ b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless5/train.py @@ -1001,7 +1001,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) diff --git a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py index f8dd7b287..8c53972fd 100755 --- a/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py +++ b/egs/xbmu_amdo31/ASR/pruned_transducer_stateless7/train.py @@ -993,7 +993,7 @@ def run(rank, world_size, args): if params.print_diagnostics: opts = diagnostics.TensorDiagnosticOptions( - 2**22 + 512 ) # allow 4 megabytes per sub-module diagnostic = diagnostics.attach_diagnostics(model, opts) From ef5da4824d033153f118556bd8407ace848061d2 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Sun, 24 Sep 2023 17:31:01 +0800 Subject: [PATCH 092/100] formatted the entire LibriSpeech recipe (#1270) * formatted the entire librispeech recipe * minor updates --- egs/librispeech/ASR/conformer_ctc/train.py | 1 - egs/librispeech/ASR/local/download_lm.py | 1 + .../ASR/long_file_recog/beam_search.py | 4 +- .../ASR/long_file_recog/merge_chunks.py | 1 - .../ASR/long_file_recog/recognize.py | 1 + .../ASR/pruned2_knowledge/optim.py | 1 - .../beam_search.py | 12 +- .../ASR/pruned_transducer_stateless2/optim.py | 1 - .../pruned_transducer_stateless2/scaling.py | 1 - .../pruned_transducer_stateless6/vq_utils.py | 1 - .../pruned_transducer_stateless7/alignment.py | 2 +- .../ASR/streaming_conformer_ctc/train.py | 1 - egs/librispeech/ASR/tdnn_lstm_ctc/train.py | 1 - egs/librispeech/ASR/transducer/train.py | 1 - egs/librispeech/ASR/transducer_lstm/train.py | 1 - egs/librispeech/ASR/zipformer/scaling.py | 2 +- icefall/__init__.py | 8 +- icefall/context_graph.py | 1 - icefall/diagnostics.py | 206 ++++++++++-------- icefall/profiler.py | 61 ++---- icefall/rnn_lm/check-onnx-streaming.py | 1 - icefall/rnn_lm/train.py | 1 - icefall/shared/make_kn_lm.py | 2 - icefall/transformer_lm/model.py | 1 - requirements-ci.txt | 1 + requirements.txt | 1 + 26 files changed, 144 insertions(+), 171 deletions(-) diff --git a/egs/librispeech/ASR/conformer_ctc/train.py b/egs/librispeech/ASR/conformer_ctc/train.py index 99fe64793..828106f41 100755 --- a/egs/librispeech/ASR/conformer_ctc/train.py +++ b/egs/librispeech/ASR/conformer_ctc/train.py @@ -557,7 +557,6 @@ def train_one_epoch( ) if batch_idx % params.log_interval == 0: - if tb_writer is not None: loss_info.write_summary( tb_writer, "train/current_", params.batch_idx_train diff --git a/egs/librispeech/ASR/local/download_lm.py b/egs/librispeech/ASR/local/download_lm.py index da1648d06..5a36ff2a9 100755 --- a/egs/librispeech/ASR/local/download_lm.py +++ b/egs/librispeech/ASR/local/download_lm.py @@ -43,6 +43,7 @@ from pathlib import Path from tqdm.auto import tqdm + # This function is copied from lhotse def tqdm_urlretrieve_hook(t): """Wraps tqdm instance. diff --git a/egs/librispeech/ASR/long_file_recog/beam_search.py b/egs/librispeech/ASR/long_file_recog/beam_search.py index f8c31861c..b65e9d40a 100644 --- a/egs/librispeech/ASR/long_file_recog/beam_search.py +++ b/egs/librispeech/ASR/long_file_recog/beam_search.py @@ -236,7 +236,7 @@ def greedy_search_batch( encoder_out = model.joiner.encoder_proj(packed_encoder_out.data) offset = 0 - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] @@ -507,7 +507,7 @@ def modified_beam_search( offset = 0 finalized_B = [] - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] diff --git a/egs/librispeech/ASR/long_file_recog/merge_chunks.py b/egs/librispeech/ASR/long_file_recog/merge_chunks.py index d38d9c86a..9e31e00d5 100755 --- a/egs/librispeech/ASR/long_file_recog/merge_chunks.py +++ b/egs/librispeech/ASR/long_file_recog/merge_chunks.py @@ -162,7 +162,6 @@ def merge_chunks( futures = [] with ThreadPoolExecutor(max_workers=1) as executor: - for cut in cuts_chunk: cur_rec_id = cut.recording.id if len(cut_list) == 0: diff --git a/egs/librispeech/ASR/long_file_recog/recognize.py b/egs/librispeech/ASR/long_file_recog/recognize.py index 96c83f859..466253446 100755 --- a/egs/librispeech/ASR/long_file_recog/recognize.py +++ b/egs/librispeech/ASR/long_file_recog/recognize.py @@ -264,6 +264,7 @@ def decode_dataset( - timestamps of reference transcript - timestamps of predicted result """ + # Background worker to add alignemnt and save cuts to disk. def _save_worker( cuts: List[Cut], diff --git a/egs/librispeech/ASR/pruned2_knowledge/optim.py b/egs/librispeech/ASR/pruned2_knowledge/optim.py index 76cd4e11e..9f287ce70 100644 --- a/egs/librispeech/ASR/pruned2_knowledge/optim.py +++ b/egs/librispeech/ASR/pruned2_knowledge/optim.py @@ -66,7 +66,6 @@ class Eve(Optimizer): weight_decay=1e-3, target_rms=0.1, ): - if not 0.0 <= lr: raise ValueError("Invalid learning rate: {}".format(lr)) if not 0.0 <= eps: diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py index 3298568a3..7fcd242fc 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/beam_search.py @@ -719,7 +719,7 @@ def greedy_search_batch( encoder_out = model.joiner.encoder_proj(packed_encoder_out.data) offset = 0 - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] @@ -1019,7 +1019,7 @@ def modified_beam_search( offset = 0 finalized_B = [] - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] @@ -1227,7 +1227,7 @@ def modified_beam_search_lm_rescore( offset = 0 finalized_B = [] - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] @@ -1427,7 +1427,7 @@ def modified_beam_search_lm_rescore_LODR( offset = 0 finalized_B = [] - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] @@ -2608,7 +2608,6 @@ def modified_beam_search_LODR( context_score = 0 new_context_state = None if context_graph is None else hyp.context_state if new_token not in (blank_id, unk_id): - if context_graph is not None: ( context_score, @@ -2758,7 +2757,7 @@ def modified_beam_search_lm_shallow_fusion( offset = 0 finalized_B = [] - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] # get batch @@ -2900,7 +2899,6 @@ def modified_beam_search_lm_shallow_fusion( new_token = topk_token_indexes[k] new_timestamp = hyp.timestamp[:] if new_token not in (blank_id, unk_id): - ys.append(new_token) new_timestamp.append(t) diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/optim.py b/egs/librispeech/ASR/pruned_transducer_stateless2/optim.py index 2d7f557ad..f54bc2709 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/optim.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/optim.py @@ -66,7 +66,6 @@ class Eve(Optimizer): weight_decay=1e-3, target_rms=0.1, ): - if not 0.0 <= lr: raise ValueError("Invalid learning rate: {}".format(lr)) if not 0.0 <= eps: diff --git a/egs/librispeech/ASR/pruned_transducer_stateless2/scaling.py b/egs/librispeech/ASR/pruned_transducer_stateless2/scaling.py index 963ebdc2d..91d64c1df 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless2/scaling.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless2/scaling.py @@ -528,7 +528,6 @@ class ScaledLSTM(nn.LSTM): return with torch.cuda.device_of(first_fw): - # Note: no_grad() is necessary since _cudnn_rnn_flatten_weight is # an inplace operation on self._flat_weights with torch.no_grad(): diff --git a/egs/librispeech/ASR/pruned_transducer_stateless6/vq_utils.py b/egs/librispeech/ASR/pruned_transducer_stateless6/vq_utils.py index 14ff86f23..3bca7db2c 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless6/vq_utils.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless6/vq_utils.py @@ -56,7 +56,6 @@ class CodebookIndexExtractor: """ def __init__(self, params: AttributeDict): - self.params = params params.subsets = ["clean-100"] if self.params.full_libri: diff --git a/egs/librispeech/ASR/pruned_transducer_stateless7/alignment.py b/egs/librispeech/ASR/pruned_transducer_stateless7/alignment.py index 76cd56bbb..bfb5fe609 100644 --- a/egs/librispeech/ASR/pruned_transducer_stateless7/alignment.py +++ b/egs/librispeech/ASR/pruned_transducer_stateless7/alignment.py @@ -111,7 +111,7 @@ def batch_force_alignment( offset = 0 finalized_B = [] - for (t, batch_size) in enumerate(batch_size_list): + for t, batch_size in enumerate(batch_size_list): start = offset end = offset + batch_size current_encoder_out = encoder_out.data[start:end] diff --git a/egs/librispeech/ASR/streaming_conformer_ctc/train.py b/egs/librispeech/ASR/streaming_conformer_ctc/train.py index bb55ed6bb..14d7274c2 100755 --- a/egs/librispeech/ASR/streaming_conformer_ctc/train.py +++ b/egs/librispeech/ASR/streaming_conformer_ctc/train.py @@ -543,7 +543,6 @@ def train_one_epoch( ) if batch_idx % params.log_interval == 0: - if tb_writer is not None: loss_info.write_summary( tb_writer, "train/current_", params.batch_idx_train diff --git a/egs/librispeech/ASR/tdnn_lstm_ctc/train.py b/egs/librispeech/ASR/tdnn_lstm_ctc/train.py index 0aa1587ba..90245ed46 100755 --- a/egs/librispeech/ASR/tdnn_lstm_ctc/train.py +++ b/egs/librispeech/ASR/tdnn_lstm_ctc/train.py @@ -463,7 +463,6 @@ def train_one_epoch( f"tot_loss[{tot_loss}], batch size: {batch_size}" ) if batch_idx % params.log_interval == 0: - if tb_writer is not None: loss_info.write_summary( tb_writer, "train/current_", params.batch_idx_train diff --git a/egs/librispeech/ASR/transducer/train.py b/egs/librispeech/ASR/transducer/train.py index f2a09346c..9ac6b7d03 100755 --- a/egs/librispeech/ASR/transducer/train.py +++ b/egs/librispeech/ASR/transducer/train.py @@ -513,7 +513,6 @@ def train_one_epoch( ) if batch_idx % params.log_interval == 0: - if tb_writer is not None: loss_info.write_summary( tb_writer, "train/current_", params.batch_idx_train diff --git a/egs/librispeech/ASR/transducer_lstm/train.py b/egs/librispeech/ASR/transducer_lstm/train.py index a6f2bd08c..92134116c 100755 --- a/egs/librispeech/ASR/transducer_lstm/train.py +++ b/egs/librispeech/ASR/transducer_lstm/train.py @@ -517,7 +517,6 @@ def train_one_epoch( ) if batch_idx % params.log_interval == 0: - if tb_writer is not None: loss_info.write_summary( tb_writer, "train/current_", params.batch_idx_train diff --git a/egs/librispeech/ASR/zipformer/scaling.py b/egs/librispeech/ASR/zipformer/scaling.py index 23fd279b3..c0f1e3087 100644 --- a/egs/librispeech/ASR/zipformer/scaling.py +++ b/egs/librispeech/ASR/zipformer/scaling.py @@ -70,7 +70,7 @@ class PiecewiseLinear(object): self.pairs = list(args[0].pairs) else: self.pairs = [(float(x), float(y)) for x, y in args] - for (x, y) in self.pairs: + for x, y in self.pairs: assert isinstance(x, (float, int)), type(x) assert isinstance(y, (float, int)), type(y) diff --git a/icefall/__init__.py b/icefall/__init__.py index 05e2b408c..b1e4313e9 100644 --- a/icefall/__init__.py +++ b/icefall/__init__.py @@ -1,12 +1,6 @@ # isort:skip_file -from . import ( - checkpoint, - decode, - dist, - env, - utils -) +from . import checkpoint, decode, dist, env, utils from .byte_utils import ( byte_decode, diff --git a/icefall/context_graph.py b/icefall/context_graph.py index 01836df04..0b7c42c0b 100644 --- a/icefall/context_graph.py +++ b/icefall/context_graph.py @@ -227,7 +227,6 @@ class ContextGraph: filename: Optional[str] = "", symbol_table: Optional[Dict[int, str]] = None, ) -> "Digraph": # noqa - """Visualize a ContextGraph via graphviz. Render ContextGraph as an image via graphviz, and return the Digraph object; diff --git a/icefall/diagnostics.py b/icefall/diagnostics.py index 98870684e..700dc1500 100644 --- a/icefall/diagnostics.py +++ b/icefall/diagnostics.py @@ -23,6 +23,7 @@ from typing import Optional, Tuple, List import torch from torch import Tensor, nn + class TensorDiagnosticOptions(object): """Options object for tensor diagnostics: @@ -77,11 +78,11 @@ def get_tensor_stats( elif stats_type == "abs": x = x.abs() elif stats_type == "rms": - x = x ** 2 + x = x**2 elif stats_type == "positive": x = (x > 0).to(dtype=torch.float) else: - assert stats_type in [ "value", "max", "min" ] + assert stats_type in ["value", "max", "min"] sum_dims = [d for d in range(x.ndim) if d != dim] if len(sum_dims) > 0: @@ -121,10 +122,10 @@ class TensorDiagnostic(object): self.class_name = None # will assign in accumulate() self.stats = None # we'll later assign a list to self.stats. - # It's a list of dicts, indexed by dim (i.e. by the - # axis of the tensor). The dicts, in turn, are - # indexed by `stats-type` which are strings in - # ["abs", "max", "min", "positive", "value", "rms"]. + # It's a list of dicts, indexed by dim (i.e. by the + # axis of the tensor). The dicts, in turn, are + # indexed by `stats-type` which are strings in + # ["abs", "max", "min", "positive", "value", "rms"]. # scalar_stats contains some analysis of the activations and gradients, self.scalar_stats = None @@ -139,7 +140,6 @@ class TensorDiagnostic(object): # only adding a new element to the list if there was a different dim. # if the string in the key is "eigs", if we detect a length mismatch we put None as the value. - def accumulate(self, x, class_name: Optional[str] = None): """ Accumulate tensors. @@ -193,17 +193,12 @@ class TensorDiagnostic(object): done = True break if not done: - if ( - this_dim_stats[stats_type] != [] - and stats_type == "eigs" - ): + if this_dim_stats[stats_type] != [] and stats_type == "eigs": # >1 size encountered on this dim, e.g. it's a batch or time dimension, # don't accumulat "eigs" stats type, it uses too much memory this_dim_stats[stats_type] = None else: - this_dim_stats[stats_type].append( - TensorAndCount(stats, count) - ) + this_dim_stats[stats_type].append(TensorAndCount(stats, count)) def print_diagnostics(self): """Print diagnostics for each dimension of the tensor.""" @@ -220,8 +215,11 @@ class TensorDiagnostic(object): for r, v in zip(rms_stats_list, value_stats_list): stddev_stats_list.append( # r.count and v.count should be the same, but we don't check this. - TensorAndCount(r.tensor - v.tensor * v.tensor / (v.count + 1.0e-20), - r.count)) + TensorAndCount( + r.tensor - v.tensor * v.tensor / (v.count + 1.0e-20), + r.count, + ) + ) this_dim_stats["stddev"] = stddev_stats_list for stats_type, stats_list in this_dim_stats.items(): @@ -232,7 +230,6 @@ class TensorDiagnostic(object): assert stats_type == "eigs" continue - def get_count(count): return 1 if stats_type in ["max", "min"] else count @@ -250,22 +247,20 @@ class TensorDiagnostic(object): eigs, _ = torch.symeig(stats) stats = eigs.abs().sqrt() except: # noqa - print( - "Error getting eigenvalues, trying another method." - ) + print("Error getting eigenvalues, trying another method.") eigs, _ = torch.eig(stats) stats = eigs.norm(dim=1).sqrt() # sqrt so it reflects data magnitude, like stddev- not variance - if stats_type in [ "rms", "stddev" ]: + if stats_type in ["rms", "stddev"]: # we stored the square; after aggregation we need to take sqrt. stats = stats.sqrt() # if `summarize` we print percentiles of the stats; else, # we print out individual elements. - summarize = ( - len(stats_list) > 1 - ) or self.opts.dim_is_summarized(stats.numel()) + summarize = (len(stats_list) > 1) or self.opts.dim_is_summarized( + stats.numel() + ) if summarize: # usually `summarize` will be true # print out percentiles. stats = stats.sort()[0] @@ -282,15 +277,15 @@ class TensorDiagnostic(object): ans = stats.tolist() ans = ["%.2g" % x for x in ans] ans = "[" + " ".join(ans) + "]" - if stats_type in [ "value", "rms", "stddev", "eigs" ]: + if stats_type in ["value", "rms", "stddev", "eigs"]: # This norm is useful because it is strictly less than the largest # sqrt(eigenvalue) of the variance, which we print out, and shows, # speaking in an approximate way, how much of that largest eigenvalue # can be attributed to the mean of the distribution. - norm = (stats ** 2).sum().sqrt().item() + norm = (stats**2).sum().sqrt().item() ans += f", norm={norm:.2g}" mean = stats.mean().item() - rms = (stats ** 2).mean().sqrt().item() + rms = (stats**2).mean().sqrt().item() ans += f", mean={mean:.3g}, rms={rms:.3g}" # OK, "ans" contains the actual stats, e.g. @@ -298,11 +293,11 @@ class TensorDiagnostic(object): sizes = [x.tensor.shape[0] for x in stats_list] size_str = ( - f"{sizes[0]}" - if len(sizes) == 1 - else f"{min(sizes)}..{max(sizes)}" + f"{sizes[0]}" if len(sizes) == 1 else f"{min(sizes)}..{max(sizes)}" + ) + maybe_class_name = ( + f" type={self.class_name}," if self.class_name is not None else "" ) - maybe_class_name = f" type={self.class_name}," if self.class_name is not None else "" print( f"module={self.name},{maybe_class_name} dim={dim}, size={size_str}, {stats_type} {ans}" ) @@ -330,7 +325,6 @@ class ScalarDiagnostic(object): self.sum_gradsq = None self.sum_abs_grad = None - def accumulate_input(self, x: Tensor, class_name: Optional[str] = None): """ Called in forward pass. @@ -347,8 +341,10 @@ class ScalarDiagnostic(object): limit = 10 if len(self.saved_inputs) > limit: - print(f"ERROR: forward pass called for this module over {limit} times with no backward pass. " - f" Will not accumulate scalar stats.") + print( + f"ERROR: forward pass called for this module over {limit} times with no backward pass. " + f" Will not accumulate scalar stats." + ) self.is_ok = False return self.saved_inputs.append(x) @@ -359,11 +355,15 @@ class ScalarDiagnostic(object): if self.is_forward_pass: self.is_forward_pass = False - last_shape = 'n/a' if len(self.saved_inputs) == 0 else self.saved_inputs[-1].shape + last_shape = ( + "n/a" if len(self.saved_inputs) == 0 else self.saved_inputs[-1].shape + ) if len(self.saved_inputs) == 0 or grad.shape != last_shape: - print(f"ERROR: shape mismatch or no forward activation present when backward " - f"pass called: grad shape ={tuple(grad.shape)}, num-saved-inputs={len(self.saved_inputs)}" - f", shape-of-last-saved-input={last_shape}") + print( + f"ERROR: shape mismatch or no forward activation present when backward " + f"pass called: grad shape ={tuple(grad.shape)}, num-saved-inputs={len(self.saved_inputs)}" + f", shape-of-last-saved-input={last_shape}" + ) self.is_ok = False return @@ -384,11 +384,19 @@ class ScalarDiagnostic(object): self.tick_scale = float(x_abs_sorted[index] / num_ticks_per_side) # integerize from tick * (-num ticks_per_side .. num_ticks_per_side - 1] - self.counts = torch.zeros(2 * num_ticks_per_side, dtype=torch.long, device=x.device) - self.sum_grad = torch.zeros(2 * num_ticks_per_side, dtype=torch.double, device=x.device) + self.counts = torch.zeros( + 2 * num_ticks_per_side, dtype=torch.long, device=x.device + ) + self.sum_grad = torch.zeros( + 2 * num_ticks_per_side, dtype=torch.double, device=x.device + ) # sum_gradsq is for getting error bars. - self.sum_gradsq = torch.zeros(2 * num_ticks_per_side, dtype=torch.double, device=x.device) - self.sum_abs_grad = torch.zeros(2 * num_ticks_per_side, dtype=torch.double, device=x.device) + self.sum_gradsq = torch.zeros( + 2 * num_ticks_per_side, dtype=torch.double, device=x.device + ) + self.sum_abs_grad = torch.zeros( + 2 * num_ticks_per_side, dtype=torch.double, device=x.device + ) # this will round down. x = (x / self.tick_scale).to(torch.long) @@ -397,20 +405,21 @@ class ScalarDiagnostic(object): self.counts.index_add_(dim=0, index=x, source=torch.ones_like(x)) self.sum_grad.index_add_(dim=0, index=x, source=grad.to(torch.double)) - self.sum_gradsq.index_add_(dim=0, index=x, source=(grad*grad).to(torch.double)) + self.sum_gradsq.index_add_( + dim=0, index=x, source=(grad * grad).to(torch.double) + ) self.sum_abs_grad.index_add_(dim=0, index=x, source=grad.abs().to(torch.double)) - def print_diagnostics(self): """Print diagnostics.""" if self.is_ok is False or self.counts is None: print(f"Warning: no stats accumulated for {self.name}, is_ok={self.is_ok}") return - counts = self.counts.to('cpu') - sum_grad = self.sum_grad.to(device='cpu', dtype=torch.float32) - sum_gradsq = self.sum_gradsq.to(device='cpu', dtype=torch.float32) - sum_abs_grad = self.sum_abs_grad.to(device='cpu', dtype=torch.float32) + counts = self.counts.to("cpu") + sum_grad = self.sum_grad.to(device="cpu", dtype=torch.float32) + sum_gradsq = self.sum_gradsq.to(device="cpu", dtype=torch.float32) + sum_abs_grad = self.sum_abs_grad.to(device="cpu", dtype=torch.float32) counts_cumsum = counts.cumsum(dim=0) counts_tot = counts_cumsum[-1] @@ -433,19 +442,22 @@ class ScalarDiagnostic(object): bin_abs_grad = torch.zeros(num_bins) bin_abs_grad.index_add_(dim=0, index=bin_indexes, source=sum_abs_grad) - avg_grad = (bin_grad / bin_counts) + avg_grad = bin_grad / bin_counts avg_grad_stddev = (bin_gradsq / bin_counts).sqrt() - bin_boundary_counts = torch.arange(num_bins + 1, dtype=torch.long) * counts_per_bin + bin_boundary_counts = ( + torch.arange(num_bins + 1, dtype=torch.long) * counts_per_bin + ) bin_tick_indexes = torch.searchsorted(counts_cumsum, bin_boundary_counts) # boundaries are the "x" values between the bins, e.g. corresponding to the # locations of percentiles of the distribution. num_ticks_per_side = counts.numel() // 2 bin_boundaries = (bin_tick_indexes - num_ticks_per_side) * self.tick_scale - bin_grad = bin_grad / (bin_counts + 1) - bin_conf_interval = bin_gradsq.sqrt() / (bin_counts + 1) # consider this a standard deviation. + bin_conf_interval = bin_gradsq.sqrt() / ( + bin_counts + 1 + ) # consider this a standard deviation. # bin_grad / bin_abs_grad will give us a sense for how important in a practical sense, # the gradients are. bin_abs_grad = bin_abs_grad / (bin_counts + 1) @@ -458,8 +470,9 @@ class ScalarDiagnostic(object): x = "[" + " ".join(x) + "]" return x - - maybe_class_name = f" type={self.class_name}," if self.class_name is not None else "" + maybe_class_name = ( + f" type={self.class_name}," if self.class_name is not None else "" + ) print( f"module={self.name},{maybe_class_name} bin-boundaries={tensor_to_str(bin_boundaries)}, " @@ -467,7 +480,6 @@ class ScalarDiagnostic(object): ) - class ModelDiagnostic(object): """This class stores diagnostics for all tensors in the torch.nn.Module. @@ -485,9 +497,8 @@ class ModelDiagnostic(object): self.opts = opts self.diagnostics = dict() - def __getitem__(self, name: str): - T = ScalarDiagnostic if name[-7:] == '.scalar' else TensorDiagnostic + T = ScalarDiagnostic if name[-7:] == ".scalar" else TensorDiagnostic if name not in self.diagnostics: self.diagnostics[name] = T(self.opts, name) return self.diagnostics[name] @@ -502,18 +513,19 @@ def get_class_name(module: nn.Module): ans = type(module).__name__ # we put the below in try blocks in case anyone is using a different version of these modules that # might have different member names. - if ans == 'Balancer' or ans == 'ActivationBalancer': + if ans == "Balancer" or ans == "ActivationBalancer": try: - ans += f'[{float(module.min_positive)},{float(module.max_positive)},{float(module.min_abs)},{float(module.max_abs)}]' + ans += f"[{float(module.min_positive)},{float(module.max_positive)},{float(module.min_abs)},{float(module.max_abs)}]" except: pass - elif ans == 'AbsValuePenalizer': + elif ans == "AbsValuePenalizer": try: - ans += f'[{module.limit}]' + ans += f"[{module.limit}]" except: pass return ans + def attach_diagnostics( model: nn.Module, opts: Optional[TensorDiagnosticOptions] = None ) -> ModelDiagnostic: @@ -538,73 +550,85 @@ def attach_diagnostics( if name == "": name = "" - - # Setting model_diagnostic=ans and n=name below, instead of trying to # capture the variables, ensures that we use the current values. # (this matters for `name`, since the variable gets overwritten). # These closures don't really capture by value, only by # "the final value the variable got in the function" :-( - def forward_hook( - _module, _input, _output, _model_diagnostic=ans, _name=name - ): + def forward_hook(_module, _input, _output, _model_diagnostic=ans, _name=name): if isinstance(_output, tuple) and len(_output) == 1: _output = _output[0] - if isinstance(_output, Tensor) and _output.dtype in ( torch.float32, torch.float16, torch.float64 ): - _model_diagnostic[f"{_name}.output"].accumulate(_output, - class_name=get_class_name(_module)) + if isinstance(_output, Tensor) and _output.dtype in ( + torch.float32, + torch.float16, + torch.float64, + ): + _model_diagnostic[f"{_name}.output"].accumulate( + _output, class_name=get_class_name(_module) + ) elif isinstance(_output, tuple): for i, o in enumerate(_output): - if o.dtype in ( torch.float32, torch.float16, torch.float64 ): - _model_diagnostic[f"{_name}.output[{i}]"].accumulate(o, - class_name=get_class_name(_module)) + if o.dtype in (torch.float32, torch.float16, torch.float64): + _model_diagnostic[f"{_name}.output[{i}]"].accumulate( + o, class_name=get_class_name(_module) + ) - def backward_hook( - _module, _input, _output, _model_diagnostic=ans, _name=name - ): + def backward_hook(_module, _input, _output, _model_diagnostic=ans, _name=name): if isinstance(_output, tuple) and len(_output) == 1: _output = _output[0] - if isinstance(_output, Tensor) and _output.dtype in ( torch.float32, torch.float16, torch.float64 ): - _model_diagnostic[f"{_name}.grad"].accumulate(_output, - class_name=get_class_name(_module)) + if isinstance(_output, Tensor) and _output.dtype in ( + torch.float32, + torch.float16, + torch.float64, + ): + _model_diagnostic[f"{_name}.grad"].accumulate( + _output, class_name=get_class_name(_module) + ) elif isinstance(_output, tuple): for i, o in enumerate(_output): - if o.dtype in ( torch.float32, torch.float16, torch.float64 ): - _model_diagnostic[f"{_name}.grad[{i}]"].accumulate(o, - class_name=get_class_name(_module)) - + if o.dtype in (torch.float32, torch.float16, torch.float64): + _model_diagnostic[f"{_name}.grad[{i}]"].accumulate( + o, class_name=get_class_name(_module) + ) module.register_forward_hook(forward_hook) module.register_backward_hook(backward_hook) - if type(module).__name__ in ["Sigmoid", "Tanh", "ReLU", "TanSwish", "Swish", "DoubleSwish", "Swoosh"]: + if type(module).__name__ in [ + "Sigmoid", + "Tanh", + "ReLU", + "TanSwish", + "Swish", + "DoubleSwish", + "Swoosh", + ]: # For these specific module types, accumulate some additional diagnostics # that can help us improve the activation function. These require a lot of memory, # to save the forward activations, so limit this to some select classes. # Note: this will not work correctly for all model types. def scalar_forward_hook( - _module, _input, _output, _model_diagnostic=ans, _name=name + _module, _input, _output, _model_diagnostic=ans, _name=name ): if isinstance(_input, tuple): - _input, = _input + (_input,) = _input assert isinstance(_input, Tensor) - _model_diagnostic[f"{_name}.scalar"].accumulate_input(_input, - class_name=get_class_name(_module)) + _model_diagnostic[f"{_name}.scalar"].accumulate_input( + _input, class_name=get_class_name(_module) + ) def scalar_backward_hook( - _module, _input, _output, _model_diagnostic=ans, _name=name + _module, _input, _output, _model_diagnostic=ans, _name=name ): if isinstance(_output, tuple): - _output, = _output + (_output,) = _output assert isinstance(_output, Tensor) _model_diagnostic[f"{_name}.scalar"].accumulate_output_grad(_output) module.register_forward_hook(scalar_forward_hook) module.register_backward_hook(scalar_backward_hook) - - for name, parameter in model.named_parameters(): def param_backward_hook( diff --git a/icefall/profiler.py b/icefall/profiler.py index dc76ebebc..49e138579 100644 --- a/icefall/profiler.py +++ b/icefall/profiler.py @@ -70,25 +70,17 @@ class FlopsProfiler(object): module_flop_count.append([]) if not hasattr(module, "__pre_hook_handle__"): - module.__pre_hook_handle__ = module.register_forward_pre_hook( - pre_hook - ) + module.__pre_hook_handle__ = module.register_forward_pre_hook(pre_hook) def post_hook(module, input, output): if module_flop_count: - module.__flops__ += sum( - [elem[1] for elem in module_flop_count[-1]] - ) + module.__flops__ += sum([elem[1] for elem in module_flop_count[-1]]) module_flop_count.pop() if not hasattr(module, "__post_hook_handle__"): - module.__post_hook_handle__ = module.register_forward_hook( - post_hook - ) + module.__post_hook_handle__ = module.register_forward_hook(post_hook) - self.model.apply( - partial(register_module_hooks, ignore_list=ignore_list) - ) + self.model.apply(partial(register_module_hooks, ignore_list=ignore_list)) self.started = True self.func_patched = True @@ -194,9 +186,7 @@ def _prelu_flops_compute(input: Tensor, weight: Tensor): return input.numel() -def _elu_flops_compute( - input: Tensor, alpha: float = 1.0, inplace: bool = False -): +def _elu_flops_compute(input: Tensor, alpha: float = 1.0, inplace: bool = False): return input.numel() @@ -259,9 +249,7 @@ def _conv_flops_compute( output_dims.append(output_dim) filters_per_channel = out_channels // groups - conv_per_position_macs = ( - int(_prod(kernel_dims)) * in_channels * filters_per_channel - ) + conv_per_position_macs = int(_prod(kernel_dims)) * in_channels * filters_per_channel active_elements_count = batch_size * int(_prod(output_dims)) overall_conv_macs = conv_per_position_macs * active_elements_count overall_conv_flops = 2 * overall_conv_macs @@ -297,7 +285,6 @@ def _conv_trans_flops_compute( output_dims = [] for idx, input_dim in enumerate(input_dims): - output_dim = ( input_dim + 2 * paddings[idx] @@ -310,9 +297,7 @@ def _conv_trans_flops_compute( dilations = dilation if type(dilation) is tuple else (dilation, dilation) filters_per_channel = out_channels // groups - conv_per_position_macs = ( - int(_prod(kernel_dims)) * in_channels * filters_per_channel - ) + conv_per_position_macs = int(_prod(kernel_dims)) * in_channels * filters_per_channel active_elements_count = batch_size * int(_prod(input_dims)) overall_conv_macs = conv_per_position_macs * active_elements_count overall_conv_flops = 2 * overall_conv_macs @@ -389,9 +374,7 @@ def _upsample_flops_compute(input, **kwargs): else: return int(size), 0 scale_factor = kwargs.get("scale_factor", None) - assert ( - scale_factor is not None - ), "either size or scale_factor should be defined" + assert scale_factor is not None, "either size or scale_factor should be defined" flops = input.numel() if isinstance(scale_factor, tuple) and len(scale_factor) == len(input): flops * int(_prod(scale_factor)) @@ -593,12 +576,8 @@ def _patch_functionals(): F.embedding = wrapFunc(F.embedding, _embedding_flops_compute) # swoosh functions in k2 - k2.swoosh_l_forward = wrapFunc( - k2.swoosh_l_forward, _k2_swoosh_flops_compute - ) - k2.swoosh_r_forward = wrapFunc( - k2.swoosh_r_forward, _k2_swoosh_flops_compute - ) + k2.swoosh_l_forward = wrapFunc(k2.swoosh_l_forward, _k2_swoosh_flops_compute) + k2.swoosh_r_forward = wrapFunc(k2.swoosh_r_forward, _k2_swoosh_flops_compute) k2.swoosh_l = wrapFunc(k2.swoosh_l, _k2_swoosh_flops_compute) k2.swoosh_r = wrapFunc(k2.swoosh_r, _k2_swoosh_flops_compute) @@ -612,9 +591,7 @@ def _patch_tensor_methods(): torch.Tensor.bmm = wrapFunc(torch.Tensor.bmm, _matmul_flops_compute) torch.addmm = wrapFunc(torch.addmm, _addmm_flops_compute) - torch.Tensor.addmm = wrapFunc( - torch.Tensor.addmm, _tensor_addmm_flops_compute - ) + torch.Tensor.addmm = wrapFunc(torch.Tensor.addmm, _tensor_addmm_flops_compute) torch.mul = wrapFunc(torch.mul, _mul_flops_compute) torch.Tensor.mul = wrapFunc(torch.Tensor.mul, _mul_flops_compute) @@ -631,14 +608,10 @@ def _patch_tensor_methods(): torch.tanh = wrapFunc(torch.tanh, _tanh_flops_compute) - torch.Tensor.softmax = wrapFunc( - torch.Tensor.softmax, _softmax_flops_compute - ) + torch.Tensor.softmax = wrapFunc(torch.Tensor.softmax, _softmax_flops_compute) torch.sigmoid = wrapFunc(torch.sigmoid, _sigmoid_flops_compute) - torch.Tensor.sigmoid = wrapFunc( - torch.Tensor.sigmoid, _sigmoid_flops_compute - ) + torch.Tensor.sigmoid = wrapFunc(torch.Tensor.sigmoid, _sigmoid_flops_compute) def _reload_functionals(): @@ -732,15 +705,11 @@ def _rnn_flops(flops, rnn_module, w_ih, w_hh, input_size): flops += rnn_module.hidden_size * 4 # two hadamard _product and add for C state flops += ( - rnn_module.hidden_size - + rnn_module.hidden_size - + rnn_module.hidden_size + rnn_module.hidden_size + rnn_module.hidden_size + rnn_module.hidden_size ) # final hadamard flops += ( - rnn_module.hidden_size - + rnn_module.hidden_size - + rnn_module.hidden_size + rnn_module.hidden_size + rnn_module.hidden_size + rnn_module.hidden_size ) return flops diff --git a/icefall/rnn_lm/check-onnx-streaming.py b/icefall/rnn_lm/check-onnx-streaming.py index d51a4b76b..28b908f82 100755 --- a/icefall/rnn_lm/check-onnx-streaming.py +++ b/icefall/rnn_lm/check-onnx-streaming.py @@ -112,7 +112,6 @@ def main(): for torch_v, onnx_v in zip( (torch_log_prob, torch_h0, torch_c0), (onnx_log_prob, onnx_h0, onnx_c0) ): - assert torch.allclose(torch_v, onnx_v, atol=1e-5), ( torch_v.shape, onnx_v.shape, diff --git a/icefall/rnn_lm/train.py b/icefall/rnn_lm/train.py index 3d206d139..0178b80bf 100755 --- a/icefall/rnn_lm/train.py +++ b/icefall/rnn_lm/train.py @@ -463,7 +463,6 @@ def train_one_epoch( cur_batch_idx = params.get("cur_batch_idx", 0) for batch_idx, batch in enumerate(train_dl): - if batch_idx < cur_batch_idx: continue cur_batch_idx = batch_idx diff --git a/icefall/shared/make_kn_lm.py b/icefall/shared/make_kn_lm.py index 7150297d6..231aca7f1 100755 --- a/icefall/shared/make_kn_lm.py +++ b/icefall/shared/make_kn_lm.py @@ -225,7 +225,6 @@ class NgramCounts: for n in range(0, self.ngram_order - 1): this_order_counts = self.counts[n] for hist, counts_for_hist in this_order_counts.items(): - n_star_star = 0 for w in counts_for_hist.word_to_count.keys(): n_star_star += len(counts_for_hist.word_to_context[w]) @@ -424,7 +423,6 @@ class NgramCounts: if __name__ == "__main__": - ngram_counts = NgramCounts(args.ngram_order) if args.text is None: diff --git a/icefall/transformer_lm/model.py b/icefall/transformer_lm/model.py index 79dda3168..c78cf1821 100644 --- a/icefall/transformer_lm/model.py +++ b/icefall/transformer_lm/model.py @@ -103,7 +103,6 @@ class TransformerLM(torch.nn.Module): return nll_loss def score_token(self, x: torch.Tensor, x_lens: torch.Tensor, state=None): - bs = x.size(0) state = None diff --git a/requirements-ci.txt b/requirements-ci.txt index 2433e190b..652e2ab47 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -20,6 +20,7 @@ kaldialign==0.7.1 sentencepiece==0.1.96 tensorboard==2.8.0 typeguard==2.13.3 +black==22.3.0 multi_quantization onnx diff --git a/requirements.txt b/requirements.txt index a07f6b7c7..f0098c236 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ sentencepiece>=0.1.96 tensorboard typeguard dill +black==22.3.0 From 97f9b9c33b9e3d4a7152c45f28dec397202aabb6 Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Mon, 25 Sep 2023 10:48:50 +0800 Subject: [PATCH 093/100] Add documentation for RNNLM training (#1267) * add documentation for training an RNNLM --- .../decoding-with-langugage-models/index.rst | 5 +- docs/source/recipes/RNN-LM/index.rst | 7 ++ .../RNN-LM/librispeech/lm-training.rst | 104 ++++++++++++++++++ docs/source/recipes/index.rst | 1 + 4 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 docs/source/recipes/RNN-LM/index.rst create mode 100644 docs/source/recipes/RNN-LM/librispeech/lm-training.rst diff --git a/docs/source/decoding-with-langugage-models/index.rst b/docs/source/decoding-with-langugage-models/index.rst index 6e5e3a4d9..c49da9a4e 100644 --- a/docs/source/decoding-with-langugage-models/index.rst +++ b/docs/source/decoding-with-langugage-models/index.rst @@ -2,12 +2,13 @@ Decoding with language models ============================= This section describes how to use external langugage models -during decoding to improve the WER of transducer models. +during decoding to improve the WER of transducer models. To train an external language model, +please refer to this tutorial: :ref:`train_nnlm`. The following decoding methods with external langugage models are available: -.. list-table:: LM-rescoring-based methods vs shallow-fusion-based methods (The numbers in each field is WER on test-clean, WER on test-other and decoding time on test-clean) +.. list-table:: :widths: 25 50 :header-rows: 1 diff --git a/docs/source/recipes/RNN-LM/index.rst b/docs/source/recipes/RNN-LM/index.rst new file mode 100644 index 000000000..4b74e64c7 --- /dev/null +++ b/docs/source/recipes/RNN-LM/index.rst @@ -0,0 +1,7 @@ +RNN-LM +====== + +.. toctree:: + :maxdepth: 2 + + librispeech/lm-training \ No newline at end of file diff --git a/docs/source/recipes/RNN-LM/librispeech/lm-training.rst b/docs/source/recipes/RNN-LM/librispeech/lm-training.rst new file mode 100644 index 000000000..736120275 --- /dev/null +++ b/docs/source/recipes/RNN-LM/librispeech/lm-training.rst @@ -0,0 +1,104 @@ +.. _train_nnlm: + +Train an RNN langugage model +====================================== + +If you have enough text data, you can train a neural network language model (NNLM) to improve +the WER of your E2E ASR system. This tutorial shows you how to train an RNNLM from +scratch. + +.. HINT:: + + For how to use an NNLM during decoding, please refer to the following tutorials: + :ref:`shallow_fusion`, :ref:`LODR`, :ref:`rescoring` + +.. note:: + + This tutorial is based on the LibriSpeech recipe. Please check it out for the necessary + python scripts for this tutorial. We use the LibriSpeech LM-corpus as the LM training set + for illustration purpose. You can also collect your own data. The data format is quite simple: + each line should contain a complete sentence, and words should be separated by space. + +First, let's download the training data for the RNNLM. This can be done via the +following command: + +.. code-block:: bash + + $ wget https://www.openslr.org/resources/11/librispeech-lm-norm.txt.gz + $ gzip -d librispeech-lm-norm.txt.gz + +As we are training a BPE-level RNNLM, we need to tokenize the training text, which requires a +BPE tokenizer. This can be achieved by executing the following command: + +.. code-block:: bash + + $ # if you don't have the BPE + $ GIT_LFS_SKIP_SMUDGE=1 git clone https://huggingface.co/Zengwei/icefall-asr-librispeech-zipformer-2023-05-15 + $ cd icefall-asr-librispeech-zipformer-2023-05-15/data/lang_bpe_500 + $ git lfs pull --include bpe.model + $ cd ../../.. + + $ ./local/prepare_lm_training_data.py \ + --bpe-model icefall-asr-librispeech-zipformer-2023-05-15/data/lang_bpe_500/bpe.model \ + --lm-data librispeech-lm-norm.txt \ + --lm-archive data/lang_bpe_500/lm_data.pt + +Now, you should have a file name ``lm_data.pt`` file store under the directory ``data/lang_bpe_500``. +This is the packed training data for the RNNLM. We then sort the training data according to its +sentence length. + +.. code-block:: bash + + $ # This could take a while (~ 20 minutes), feel free to grab a cup of coffee :) + $ ./local/sort_lm_training_data.py \ + --in-lm-data data/lang_bpe_500/lm_data.pt \ + --out-lm-data data/lang_bpe_500/sorted_lm_data.pt \ + --out-statistics data/lang_bpe_500/lm_data_stats.txt + + +The aforementioned steps can be repeated to create a a validation set for you RNNLM. Let's say +you have a validation set in ``valid.txt``, you can just set ``--lm-data valid.txt`` +and ``--lm-archive data/lang_bpe_500/lm-data-valid.pt`` when calling ``./local/prepare_lm_training_data.py``. + +After completing the previous steps, the training and testing sets for training RNNLM are ready. +The next step is to train the RNNLM model. The training command is as follows: + +.. code-block:: bash + + $ # assume you are in the icefall root directory + $ cd rnn_lm + $ ln -s ../../egs/librispeech/ASR/data . + $ cd .. + $ ./rnn_lm/train.py \ + --world-size 4 \ + --exp-dir ./rnn_lm/exp \ + --start-epoch 0 \ + --num-epochs 10 \ + --use-fp16 0 \ + --tie-weights 1 \ + --embedding-dim 2048 \ + --hidden_dim 2048 \ + --num-layers 3 \ + --batch-size 300 \ + --lm-data rnn_lm/data/lang_bpe_500/sorted_lm_data.pt \ + --lm-data-valid rnn_lm/data/lang_bpe_500/sorted_lm_data.pt + + +.. note:: + + You can adjust the RNNLM hyper parameters to control the size of the RNNLM, + such as embedding dimension and hidden state dimension. For more details, please + run ``./rnn_lm/train.py --help``. + +.. note:: + + The training of RNNLM can take a long time (usually a couple of days). + + + + + + + + + diff --git a/docs/source/recipes/index.rst b/docs/source/recipes/index.rst index 63793275c..7265e1cf6 100644 --- a/docs/source/recipes/index.rst +++ b/docs/source/recipes/index.rst @@ -15,3 +15,4 @@ We may add recipes for other tasks as well in the future. Non-streaming-ASR/index Streaming-ASR/index + RNN-LM/index From e17f884ace2dba7561d4d4eaaac6726234cad20f Mon Sep 17 00:00:00 2001 From: marcoyang1998 <45973641+marcoyang1998@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:36:40 +0800 Subject: [PATCH 094/100] Fix docs for MVQ (#1272) * typo fix --- .../librispeech/distillation.rst | 16 ++++++++-------- egs/librispeech/ASR/distillation_with_hubert.sh | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst b/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst index 2e8d0893a..37edf7de9 100644 --- a/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst +++ b/docs/source/recipes/Non-streaming-ASR/librispeech/distillation.rst @@ -47,7 +47,7 @@ The data preparation contains several stages, you can use the following two options: - ``--stage`` - - ``--stop-stage`` + - ``--stop_stage`` to control which stage(s) should be run. By default, all stages are executed. @@ -56,8 +56,8 @@ For example, .. code-block:: bash $ cd egs/librispeech/ASR - $ ./prepare.sh --stage 0 --stop-stage 0 # run only stage 0 - $ ./prepare.sh --stage 2 --stop-stage 5 # run from stage 2 to stage 5 + $ ./prepare.sh --stage 0 --stop_stage 0 # run only stage 0 + $ ./prepare.sh --stage 2 --stop_stage 5 # run from stage 2 to stage 5 .. HINT:: @@ -108,15 +108,15 @@ As usual, you can control the stages you want to run by specifying the following two options: - ``--stage`` - - ``--stop-stage`` + - ``--stop_stage`` For example, .. code-block:: bash $ cd egs/librispeech/ASR - $ ./distillation_with_hubert.sh --stage 0 --stop-stage 0 # run only stage 0 - $ ./distillation_with_hubert.sh --stage 2 --stop-stage 4 # run from stage 2 to stage 5 + $ ./distillation_with_hubert.sh --stage 0 --stop_stage 0 # run only stage 0 + $ ./distillation_with_hubert.sh --stage 2 --stop_stage 4 # run from stage 2 to stage 5 Here are a few options in `./distillation_with_hubert.sh `_ you need to know before you proceed. @@ -134,7 +134,7 @@ and prepares MVQ-augmented training manifests. .. code-block:: bash - $ ./distillation_with_hubert.sh --stage 2 --stop-stage 2 # run only stage 2 + $ ./distillation_with_hubert.sh --stage 2 --stop_stage 2 # run only stage 2 Please see the following screenshot for the output of an example execution. @@ -172,7 +172,7 @@ To perform training, please run stage 3 by executing the following command. .. code-block:: bash - $ ./prepare.sh --stage 3 --stop-stage 3 # run MVQ training + $ ./prepare.sh --stage 3 --stop_stage 3 # run MVQ training Here is the code snippet for training: diff --git a/egs/librispeech/ASR/distillation_with_hubert.sh b/egs/librispeech/ASR/distillation_with_hubert.sh index 6aaa0333b..a5b0b85af 100755 --- a/egs/librispeech/ASR/distillation_with_hubert.sh +++ b/egs/librispeech/ASR/distillation_with_hubert.sh @@ -56,6 +56,8 @@ use_extracted_codebook=True # "hubert_xtralarge_ll60k" -> pretrained model without fintuing teacher_model_id=hubert_xtralarge_ll60k_finetune_ls960 +. shared/parse_options.sh || exit 1 + log() { # This function is from espnet local fname=${BASH_SOURCE[1]##*/} From 1b565dd25198f700bcfe88e86a0f6a435e11a429 Mon Sep 17 00:00:00 2001 From: zr_jin Date: Tue, 26 Sep 2023 15:41:39 +0800 Subject: [PATCH 095/100] added softlinks to local dir (#1273) --- egs/tedlium3/ASR/conformer_ctc2/local | 1 + egs/tedlium3/ASR/pruned_transducer_stateless/local | 1 + egs/tedlium3/ASR/transducer_stateless/local | 1 + egs/tedlium3/ASR/zipformer/local | 1 + 4 files changed, 4 insertions(+) create mode 120000 egs/tedlium3/ASR/conformer_ctc2/local create mode 120000 egs/tedlium3/ASR/pruned_transducer_stateless/local create mode 120000 egs/tedlium3/ASR/transducer_stateless/local create mode 120000 egs/tedlium3/ASR/zipformer/local diff --git a/egs/tedlium3/ASR/conformer_ctc2/local b/egs/tedlium3/ASR/conformer_ctc2/local new file mode 120000 index 000000000..c820590c5 --- /dev/null +++ b/egs/tedlium3/ASR/conformer_ctc2/local @@ -0,0 +1 @@ +../local \ No newline at end of file diff --git a/egs/tedlium3/ASR/pruned_transducer_stateless/local b/egs/tedlium3/ASR/pruned_transducer_stateless/local new file mode 120000 index 000000000..c820590c5 --- /dev/null +++ b/egs/tedlium3/ASR/pruned_transducer_stateless/local @@ -0,0 +1 @@ +../local \ No newline at end of file diff --git a/egs/tedlium3/ASR/transducer_stateless/local b/egs/tedlium3/ASR/transducer_stateless/local new file mode 120000 index 000000000..c820590c5 --- /dev/null +++ b/egs/tedlium3/ASR/transducer_stateless/local @@ -0,0 +1 @@ +../local \ No newline at end of file diff --git a/egs/tedlium3/ASR/zipformer/local b/egs/tedlium3/ASR/zipformer/local new file mode 120000 index 000000000..c820590c5 --- /dev/null +++ b/egs/tedlium3/ASR/zipformer/local @@ -0,0 +1 @@ +../local \ No newline at end of file From 2318c3fbd011b14ceffe8b3a8663057708afeea0 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Tue, 26 Sep 2023 16:36:19 +0800 Subject: [PATCH 096/100] Support CTC decoding on CPU using OpenFst and kaldi decoders. (#1244) --- .flake8 | 1 + .../scripts/run-pre-trained-conformer-ctc.sh | 43 +++ .../run-pretrained-conformer-ctc.yml | 2 +- .github/workflows/run-yesno-recipe.yml | 37 ++ .gitignore | 2 + docs/source/model-export/export-ncnn.rst | 2 + .../jit_pretrained_decode_with_H.py | 235 ++++++++++++ .../jit_pretrained_decode_with_HL.py | 232 ++++++++++++ egs/librispeech/ASR/local/prepare_lang_fst.py | 127 +++++++ .../lstm_transducer_stateless/test_model.py | 3 +- egs/librispeech/ASR/prepare.sh | 4 + egs/yesno/ASR/local/prepare_lang_fst.py | 1 + egs/yesno/ASR/prepare.sh | 1 + egs/yesno/ASR/tdnn/jit_pretrained.py | 1 - .../ASR/tdnn/jit_pretrained_decode_with_H.py | 208 +++++++++++ .../ASR/tdnn/jit_pretrained_decode_with_HL.py | 207 +++++++++++ icefall/ctc/.gitignore | 2 + icefall/ctc/README.md | 17 + icefall/ctc/__init__.py | 6 + icefall/ctc/prepare_lang.py | 334 ++++++++++++++++++ icefall/ctc/test_ctc_topo.py | 140 ++++++++ icefall/ctc/test_prepare_lang.py | 43 +++ icefall/ctc/topo.py | 137 +++++++ requirements-ci.txt | 1 + requirements.txt | 1 + 25 files changed, 1783 insertions(+), 4 deletions(-) create mode 100755 egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_H.py create mode 100755 egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py create mode 100755 egs/librispeech/ASR/local/prepare_lang_fst.py create mode 120000 egs/yesno/ASR/local/prepare_lang_fst.py create mode 100755 egs/yesno/ASR/tdnn/jit_pretrained_decode_with_H.py create mode 100755 egs/yesno/ASR/tdnn/jit_pretrained_decode_with_HL.py create mode 100644 icefall/ctc/.gitignore create mode 100644 icefall/ctc/README.md create mode 100644 icefall/ctc/__init__.py create mode 100644 icefall/ctc/prepare_lang.py create mode 100755 icefall/ctc/test_ctc_topo.py create mode 100755 icefall/ctc/test_prepare_lang.py create mode 100644 icefall/ctc/topo.py diff --git a/.flake8 b/.flake8 index 1c0c2cdbb..410cb5482 100644 --- a/.flake8 +++ b/.flake8 @@ -24,6 +24,7 @@ exclude = **/data/**, icefall/shared/make_kn_lm.py, icefall/__init__.py + icefall/ctc/__init__.py ignore = # E203 white space before ":" diff --git a/.github/scripts/run-pre-trained-conformer-ctc.sh b/.github/scripts/run-pre-trained-conformer-ctc.sh index a4959aa01..19cbd96fc 100755 --- a/.github/scripts/run-pre-trained-conformer-ctc.sh +++ b/.github/scripts/run-pre-trained-conformer-ctc.sh @@ -44,3 +44,46 @@ log "HLG decoding" $repo/test_wavs/1089-134686-0001.flac \ $repo/test_wavs/1221-135766-0001.flac \ $repo/test_wavs/1221-135766-0002.flac + +log "CTC decoding on CPU with kaldi decoders using OpenFst" + +log "Exporting model with torchscript" + +pushd $repo/exp +ln -s pretrained.pt epoch-99.pt +popd + +./conformer_ctc/export.py \ + --epoch 99 \ + --avg 1 \ + --exp-dir $repo/exp \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + --jit 1 + +ls -lh $repo/exp + + +log "Generating H.fst, HL.fst" + +./local/prepare_lang_fst.py --lang-dir $repo/data/lang_bpe_500 +ls -lh $repo/data/lang_bpe_500 + +log "Decoding with H on CPU with OpenFst" + +./conformer_ctc/jit_pretrained_decode_with_H.py \ + --nn-model $repo/exp/cpu_jit.pt \ + --H $repo/data/lang_bpe_500/H.fst \ + --tokens $repo/data/lang_bpe_500/tokens.txt \ + $repo/test_wavs/1089-134686-0001.flac \ + $repo/test_wavs/1221-135766-0001.flac \ + $repo/test_wavs/1221-135766-0002.flac + +log "Decoding with HL on CPU with OpenFst" + +./conformer_ctc/jit_pretrained_decode_with_HL.py \ + --nn-model $repo/exp/cpu_jit.pt \ + --HL $repo/data/lang_bpe_500/HL.fst \ + --words $repo/data/lang_bpe_500/words.txt \ + $repo/test_wavs/1089-134686-0001.flac \ + $repo/test_wavs/1221-135766-0001.flac \ + $repo/test_wavs/1221-135766-0002.flac diff --git a/.github/workflows/run-pretrained-conformer-ctc.yml b/.github/workflows/run-pretrained-conformer-ctc.yml index 6151a5a14..e268d840d 100644 --- a/.github/workflows/run-pretrained-conformer-ctc.yml +++ b/.github/workflows/run-pretrained-conformer-ctc.yml @@ -29,7 +29,7 @@ concurrency: jobs: run_pre_trained_conformer_ctc: - if: github.event.label.name == 'ready' || github.event_name == 'push' + if: github.event.label.name == 'ready' || github.event_name == 'push' || github.event.label.name == 'ctc' runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/.github/workflows/run-yesno-recipe.yml b/.github/workflows/run-yesno-recipe.yml index 57f15fe87..400595749 100644 --- a/.github/workflows/run-yesno-recipe.yml +++ b/.github/workflows/run-yesno-recipe.yml @@ -140,9 +140,46 @@ jobs: download/waves_yesno/0_0_0_1_0_0_0_1.wav \ download/waves_yesno/0_0_1_0_0_0_1_0.wav + - name: Test decoding with H + shell: bash + working-directory: ${{github.workspace}} + run: | + export PYTHONPATH=$PWD:$PYTHONPATH + echo $PYTHONPATH + + cd egs/yesno/ASR + python3 ./tdnn/export.py --epoch 14 --avg 2 --jit 1 + + python3 ./tdnn/jit_pretrained_decode_with_H.py \ + --nn-model ./tdnn/exp/cpu_jit.pt \ + --H ./data/lang_phone/H.fst \ + --tokens ./data/lang_phone/tokens.txt \ + ./download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + ./download/waves_yesno/0_0_1_0_0_0_1_0.wav \ + ./download/waves_yesno/0_0_1_0_0_1_1_1.wav + + - name: Test decoding with HL + shell: bash + working-directory: ${{github.workspace}} + run: | + export PYTHONPATH=$PWD:$PYTHONPATH + echo $PYTHONPATH + + cd egs/yesno/ASR + python3 ./tdnn/export.py --epoch 14 --avg 2 --jit 1 + + python3 ./tdnn/jit_pretrained_decode_with_HL.py \ + --nn-model ./tdnn/exp/cpu_jit.pt \ + --HL ./data/lang_phone/HL.fst \ + --words ./data/lang_phone/words.txt \ + ./download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + ./download/waves_yesno/0_0_1_0_0_0_1_0.wav \ + ./download/waves_yesno/0_0_1_0_0_1_1_1.wav + - name: Show generated files shell: bash working-directory: ${{github.workspace}} run: | cd egs/yesno/ASR ls -lh tdnn/exp + ls -lh data/lang_phone diff --git a/.gitignore b/.gitignore index 8af05d884..fa18ca83c 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ node_modules *.param *.bin .DS_Store +*.fst +*.arpa diff --git a/docs/source/model-export/export-ncnn.rst b/docs/source/model-export/export-ncnn.rst index 9eb5f85d2..634fb1e59 100644 --- a/docs/source/model-export/export-ncnn.rst +++ b/docs/source/model-export/export-ncnn.rst @@ -1,3 +1,5 @@ +.. _icefall_export_to_ncnn: + Export to ncnn ============== diff --git a/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_H.py b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_H.py new file mode 100755 index 000000000..b52c7cfed --- /dev/null +++ b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_H.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +""" +This file shows how to use a torchscript model for decoding with H +on CPU using OpenFST and decoders from kaldi. + +Usage: + + ./conformer_ctc/jit_pretrained_decode_with_H.py \ + --nn-model ./conformer_ctc/exp/cpu_jit.pt \ + --H ./data/lang_bpe_500/H.fst \ + --tokens ./data/lang_bpe_500/tokens.txt \ + ./download/LibriSpeech/test-clean/1089/134686/1089-134686-0002.flac \ + ./download/LibriSpeech/test-clean/1221/135766/1221-135766-0001.flac + +Note that to generate ./conformer_ctc/exp/cpu_jit.pt, +you can use ./export.py --jit 1 +""" + +import argparse +import logging +import math +from typing import Dict, List + +import kaldi_hmm_gmm +import kaldifeat +import kaldifst +import torch +import torchaudio +from kaldi_hmm_gmm import DecodableCtc, FasterDecoder, FasterDecoderOptions +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model", + type=str, + required=True, + help="""Path to the torchscript model. + You can use ./conformer_ctc/export.py --jit 1 + to obtain it + """, + ) + + parser.add_argument( + "--tokens", + type=str, + required=True, + help="Path to tokens.txt", + ) + + parser.add_argument("--H", type=str, required=True, help="Path to H.fst") + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. ", + ) + + return parser + + +def read_tokens(tokens_txt: str) -> Dict[int, str]: + id2token = dict() + with open(tokens_txt, encoding="utf-8") as f: + for line in f: + token, idx = line.strip().split() + id2token[int(idx)] = token + + return id2token + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def decode( + filename: str, + nnet_output: torch.Tensor, + H: kaldifst, + id2token: Dict[int, str], +) -> List[str]: + """ + Args: + filename: + Path to the filename for decoding. Used for debugging. + nnet_output: + A 2-D float32 tensor of shape (num_frames, vocab_size). It + contains output from log_softmax. + H: + The H graph. + id2token: + A map mapping token ID to token string. + Returns: + Return a list of decoded tokens. + """ + logging.info(f"{filename}, {nnet_output.shape}") + decodable = DecodableCtc(nnet_output.cpu()) + + decoder_opts = FasterDecoderOptions(max_active=3000) + decoder = FasterDecoder(H, decoder_opts) + decoder.decode(decodable) + + if not decoder.reached_final(): + print(f"failed to decode {filename}") + return [""] + + ok, best_path = decoder.get_best_path() + + ( + ok, + isymbols_out, + osymbols_out, + total_weight, + ) = kaldifst.get_linear_symbol_sequence(best_path) + if not ok: + print(f"failed to get linear symbol sequence for {filename}") + return [""] + + # tokens are incremented during graph construction + # so they need to be decremented + hyps = [id2token[i - 1] for i in osymbols_out] + # hyps = "".join(hyps).split("▁") + hyps = "".join(hyps).split("\u2581") # unicode codepoint of ▁ + + return hyps + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + device = torch.device("cpu") + + logging.info(f"device: {device}") + + logging.info("Loading torchscript model") + model = torch.jit.load(args.nn_model) + model.eval() + model.to(device) + + logging.info(f"Loading H from {args.H}") + H = kaldifst.StdVectorFst.read(args.H) + + sample_rate = 16000 + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = sample_rate + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, expected_sample_rate=sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.shape[0] for f in features] + feature_lengths = torch.tensor(feature_lengths) + + supervisions = dict() + supervisions["sequence_idx"] = torch.arange(len(features)) + supervisions["start_frame"] = torch.zeros(len(features)) + supervisions["num_frames"] = feature_lengths + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + nnet_output, _, _ = model(features, supervisions) + feature_lengths = ((feature_lengths - 1) // 2 - 1) // 2 + + id2token = read_tokens(args.tokens) + + hyps = [] + for i in range(nnet_output.shape[0]): + hyp = decode( + filename=args.sound_files[i], + nnet_output=nnet_output[i, : feature_lengths[i]], + H=H, + id2token=id2token, + ) + hyps.append(hyp) + + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py new file mode 100755 index 000000000..f0326ccdf --- /dev/null +++ b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +""" +This file shows how to use a torchscript model for decoding with H +on CPU using OpenFST and decoders from kaldi. + +Usage: + + ./conformer_ctc/jit_pretrained_decode_with_H.py \ + --nn-model ./conformer_ctc/exp/cpu_jit.pt \ + --HL ./data/lang_bpe_500/HL.fst \ + --words ./data/lang_bpe_500/words.txt \ + ./download/LibriSpeech/test-clean/1089/134686/1089-134686-0002.flac \ + ./download/LibriSpeech/test-clean/1221/135766/1221-135766-0001.flac + +Note that to generate ./conformer_ctc/exp/cpu_jit.pt, +you can use ./export.py --jit 1 +""" + +import argparse +import logging +import math +from typing import Dict, List + +import kaldi_hmm_gmm +import kaldifeat +import kaldifst +import torch +import torchaudio +from kaldi_hmm_gmm import DecodableCtc, FasterDecoder, FasterDecoderOptions +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model", + type=str, + required=True, + help="""Path to the torchscript model. + You can use ./conformer_ctc/export.py --jit 1 + to obtain it + """, + ) + + parser.add_argument( + "--words", + type=str, + required=True, + help="Path to words.txt", + ) + + parser.add_argument("--HL", type=str, required=True, help="Path to HL.fst") + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. ", + ) + + return parser + + +def read_words(words_txt: str) -> Dict[int, str]: + id2word = dict() + with open(words_txt, encoding="utf-8") as f: + for line in f: + word, idx = line.strip().split() + id2word[int(idx)] = word + + return id2word + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def decode( + filename: str, + nnet_output: torch.Tensor, + HL: kaldifst, + id2word: Dict[int, str], +) -> List[str]: + """ + Args: + filename: + Path to the filename for decoding. Used for debugging. + nnet_output: + A 2-D float32 tensor of shape (num_frames, vocab_size). It + contains output from log_softmax. + HL: + The HL graph. + word2token: + A map mapping token ID to word string. + Returns: + Return a list of decoded words. + """ + logging.info(f"{filename}, {nnet_output.shape}") + decodable = DecodableCtc(nnet_output.cpu()) + + decoder_opts = FasterDecoderOptions(max_active=3000) + decoder = FasterDecoder(HL, decoder_opts) + decoder.decode(decodable) + + if not decoder.reached_final(): + print(f"failed to decode {filename}") + return [""] + + ok, best_path = decoder.get_best_path() + + ( + ok, + isymbols_out, + osymbols_out, + total_weight, + ) = kaldifst.get_linear_symbol_sequence(best_path) + if not ok: + print(f"failed to get linear symbol sequence for {filename}") + return [""] + + # are shifted by 1 during graph construction + hyps = [id2word[i] for i in osymbols_out] + + return hyps + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + device = torch.device("cpu") + + logging.info(f"device: {device}") + + logging.info("Loading torchscript model") + model = torch.jit.load(args.nn_model) + model.eval() + model.to(device) + + logging.info(f"Loading HL from {args.HL}") + HL = kaldifst.StdVectorFst.read(args.HL) + + sample_rate = 16000 + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = sample_rate + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, expected_sample_rate=sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.shape[0] for f in features] + feature_lengths = torch.tensor(feature_lengths) + + supervisions = dict() + supervisions["sequence_idx"] = torch.arange(len(features)) + supervisions["start_frame"] = torch.zeros(len(features)) + supervisions["num_frames"] = feature_lengths + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + nnet_output, _, _ = model(features, supervisions) + feature_lengths = ((feature_lengths - 1) // 2 - 1) // 2 + + id2word = read_words(args.words) + + hyps = [] + for i in range(nnet_output.shape[0]): + hyp = decode( + filename=args.sound_files[i], + nnet_output=nnet_output[i, : feature_lengths[i]], + HL=HL, + id2word=id2word, + ) + hyps.append(hyp) + + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/local/prepare_lang_fst.py b/egs/librispeech/ASR/local/prepare_lang_fst.py new file mode 100755 index 000000000..e8401123f --- /dev/null +++ b/egs/librispeech/ASR/local/prepare_lang_fst.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2023 Xiaomi Corporation (authors: Fangjun Kuang) + +""" +This script takes as input lang_dir containing lexicon_disambig.txt, +tokens.txt, and words.txt and generates the following files: + + - H.fst + - HL.fst + +Note that saved files are in OpenFst binary format. + +Usage: + +./local/prepare_lang_fst.py \ + --lang-dir ./data/lang_phone \ + --has-silence 1 + +Or + +./local/prepare_lang_fst.py \ + --lang-dir ./data/lang_bpe_500 +""" + +import argparse +import logging +from pathlib import Path + +import kaldifst + +from icefall.ctc import ( + Lexicon, + add_disambig_self_loops, + add_one, + build_standard_ctc_topo, + make_lexicon_fst_no_silence, + make_lexicon_fst_with_silence, +) +from icefall.utils import str2bool + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--lang-dir", + type=str, + help="""Input and output directory. + """, + ) + + parser.add_argument( + "--has-silence", + type=str2bool, + default=False, + help="True if the lexicon has silence.", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + lang_dir = args.lang_dir + + lexicon = Lexicon(lang_dir) + + logging.info("Building standard CTC topology") + max_token_id = max(lexicon.tokens) + H = build_standard_ctc_topo(max_token_id=max_token_id) + + # We need to add one to all tokens since we want to use ID 0 + # for epsilon + add_one(H, treat_ilabel_zero_specially=False, update_olabel=True) + H.write(f"{lang_dir}/H.fst") + + logging.info("Building L") + # Now for HL + + if args.has_silence: + L = make_lexicon_fst_with_silence(lexicon, attach_symbol_table=False) + else: + L = make_lexicon_fst_no_silence(lexicon, attach_symbol_table=False) + + if args.has_silence: + # We also need to change the input labels of L + add_one(L, treat_ilabel_zero_specially=True, update_olabel=False) + else: + add_one(L, treat_ilabel_zero_specially=False, update_olabel=False) + + # Invoke add_disambig_self_loops() so that it eats the disambig symbols + # from L after composition + add_disambig_self_loops( + H, + start=lexicon.token2id["#0"] + 1, + end=lexicon.max_disambig_id + 1, + ) + with open("H_1.fst.txt", "w") as f: + print(H, file=f) + + kaldifst.arcsort(H, sort_type="olabel") + kaldifst.arcsort(L, sort_type="ilabel") + + logging.info("Building HL") + HL = kaldifst.compose(H, L) + kaldifst.determinize_star(HL) + + disambig0 = lexicon.token2id["#0"] + 1 + max_disambig = lexicon.max_disambig_id + 1 + for state in kaldifst.StateIterator(HL): + for arc in kaldifst.ArcIterator(HL, state): + # If treat_ilabel_zero_specially is False, we always change it + # Otherwise, we only change non-zero input labels + if disambig0 <= arc.ilabel <= max_disambig: + arc.ilabel = 0 + + # Note: We are not composing L with G, so there is no need to add + # self-loops to L to handle #0 + + HL.write(f"{lang_dir}/HL.fst") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/lstm_transducer_stateless/test_model.py b/egs/librispeech/ASR/lstm_transducer_stateless/test_model.py index 03dfe1997..91ef53e24 100755 --- a/egs/librispeech/ASR/lstm_transducer_stateless/test_model.py +++ b/egs/librispeech/ASR/lstm_transducer_stateless/test_model.py @@ -57,8 +57,7 @@ def test_model(): convert_scaled_to_non_scaled(model, inplace=True) - if not os.path.exists(params.exp_dir): - os.path.mkdir(params.exp_dir) + params.exp_dir.mkdir(exist_ok=True) encoder_filename = params.exp_dir / "encoder_jit_trace.pt" export_encoder_model_jit_trace(model.encoder, encoder_filename) diff --git a/egs/librispeech/ASR/prepare.sh b/egs/librispeech/ASR/prepare.sh index 8ce1eb478..fca2c6cc4 100755 --- a/egs/librispeech/ASR/prepare.sh +++ b/egs/librispeech/ASR/prepare.sh @@ -242,6 +242,10 @@ if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then $lang_dir/L_disambig.pt \ $lang_dir/L_disambig.fst fi + + if [ ! -f $lang_dir/HL.fst ]; then + ./local/prepare_lang_fst.py --lang-dir $lang_dir + fi done fi diff --git a/egs/yesno/ASR/local/prepare_lang_fst.py b/egs/yesno/ASR/local/prepare_lang_fst.py new file mode 120000 index 000000000..c5787c534 --- /dev/null +++ b/egs/yesno/ASR/local/prepare_lang_fst.py @@ -0,0 +1 @@ +../../../librispeech/ASR/local/prepare_lang_fst.py \ No newline at end of file diff --git a/egs/yesno/ASR/prepare.sh b/egs/yesno/ASR/prepare.sh index d4ef8d601..41db0cf7c 100755 --- a/egs/yesno/ASR/prepare.sh +++ b/egs/yesno/ASR/prepare.sh @@ -60,6 +60,7 @@ if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then ) > $lang_dir/lexicon.txt ./local/prepare_lang.py + ./local/prepare_lang_fst.py --lang-dir ./data/lang_phone --has-silence 1 fi if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then diff --git a/egs/yesno/ASR/tdnn/jit_pretrained.py b/egs/yesno/ASR/tdnn/jit_pretrained.py index 84390fca5..7581ecb83 100755 --- a/egs/yesno/ASR/tdnn/jit_pretrained.py +++ b/egs/yesno/ASR/tdnn/jit_pretrained.py @@ -156,7 +156,6 @@ def main(): features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) - # Note: We don't use key padding mask for attention during decoding nnet_output = model(features) batch_size = nnet_output.shape[0] diff --git a/egs/yesno/ASR/tdnn/jit_pretrained_decode_with_H.py b/egs/yesno/ASR/tdnn/jit_pretrained_decode_with_H.py new file mode 100755 index 000000000..209ab477a --- /dev/null +++ b/egs/yesno/ASR/tdnn/jit_pretrained_decode_with_H.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +""" +This file shows how to use a torchscript model for decoding with H +on CPU using OpenFST and decoders from kaldi. + +Usage: + + ./tdnn/jit_pretrained_decode_with_H.py \ + --nn-model ./tdnn/exp/cpu_jit.pt \ + --H ./data/lang_phone/H.fst \ + --tokens ./data/lang_phone/tokens.txt \ + ./download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + ./download/waves_yesno/0_0_1_0_0_0_1_0.wav \ + ./download/waves_yesno/0_0_1_0_0_1_1_1.wav + +Note that to generate ./tdnn/exp/cpu_jit.pt, +you can use ./export.py --jit 1 +""" + +import argparse +import logging +import math +from typing import Dict, List + +import kaldifeat +import kaldifst +import torch +import torchaudio +from kaldi_hmm_gmm import DecodableCtc, FasterDecoder, FasterDecoderOptions +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model", + type=str, + required=True, + help="""Path to the torchscript model. + You can use ./tdnn/export.py --jit 1 + to obtain it + """, + ) + + parser.add_argument( + "--tokens", + type=str, + required=True, + help="Path to tokens.txt", + ) + + parser.add_argument("--H", type=str, required=True, help="Path to H.fst") + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. ", + ) + + return parser + + +def read_tokens(tokens_txt: str) -> Dict[int, str]: + id2token = dict() + with open(tokens_txt, encoding="utf-8") as f: + for line in f: + token, idx = line.strip().split() + id2token[int(idx)] = token + + return id2token + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def decode( + filename: str, + nnet_output: torch.Tensor, + H: kaldifst, + id2token: Dict[int, str], +) -> List[str]: + decodable = DecodableCtc(nnet_output) + decoder_opts = FasterDecoderOptions(max_active=3000) + decoder = FasterDecoder(H, decoder_opts) + decoder.decode(decodable) + + if not decoder.reached_final(): + print(f"failed to decode {filename}") + return [""] + + ok, best_path = decoder.get_best_path() + + ( + ok, + isymbols_out, + osymbols_out, + total_weight, + ) = kaldifst.get_linear_symbol_sequence(best_path) + if not ok: + print(f"failed to get linear symbol sequence for {filename}") + return [""] + + # are shifted by 1 during graph construction + hyps = [id2token[i - 1] for i in osymbols_out if id2token[i - 1] != "SIL"] + + return hyps + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + device = torch.device("cpu") + + logging.info(f"device: {device}") + + logging.info("Loading torchscript model") + model = torch.jit.load(args.nn_model) + model.eval() + model.to(device) + + logging.info(f"Loading H from {args.H}") + H = kaldifst.StdVectorFst.read(args.H) + + sample_rate = 8000 + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = sample_rate + opts.mel_opts.num_bins = 23 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, expected_sample_rate=sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + nnet_output = model(features) + + id2token = read_tokens(args.tokens) + + hyps = [] + for i in range(nnet_output.shape[0]): + hyp = decode( + filename=args.sound_files[0], + nnet_output=nnet_output[i], + H=H, + id2token=id2token, + ) + hyps.append(hyp) + + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/yesno/ASR/tdnn/jit_pretrained_decode_with_HL.py b/egs/yesno/ASR/tdnn/jit_pretrained_decode_with_HL.py new file mode 100755 index 000000000..74864e17d --- /dev/null +++ b/egs/yesno/ASR/tdnn/jit_pretrained_decode_with_HL.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +""" +This file shows how to use a torchscript model for decoding with HL +on CPU using OpenFST and decoders from kaldi. + +Usage: + + ./tdnn/jit_pretrained_decode_with_HL.py \ + --nn-model ./tdnn/exp/cpu_jit.pt \ + --HL ./data/lang_phone/HL.fst \ + --words ./data/lang_phone/words.txt \ + ./download/waves_yesno/0_0_0_1_0_0_0_1.wav \ + ./download/waves_yesno/0_0_1_0_0_0_1_0.wav \ + ./download/waves_yesno/0_0_1_0_0_1_1_1.wav + +Note that to generate ./tdnn/exp/cpu_jit.pt, +you can use ./export.py --jit 1 +""" + +import argparse +import logging +import math +from typing import Dict, List + +import kaldifeat +import kaldifst +import torch +import torchaudio +from kaldi_hmm_gmm import DecodableCtc, FasterDecoder, FasterDecoderOptions +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model", + type=str, + required=True, + help="""Path to the torchscript model. + You can use ./tdnn/export.py --jit 1 + to obtain it + """, + ) + + parser.add_argument( + "--words", + type=str, + required=True, + help="Path to words.txt", + ) + + parser.add_argument("--HL", type=str, required=True, help="Path to HL.fst") + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. ", + ) + + return parser + + +def read_words(words_txt: str) -> Dict[int, str]: + id2word = dict() + with open(words_txt, encoding="utf-8") as f: + for line in f: + word, idx = line.strip().split() + id2word[int(idx)] = word + + return id2word + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def decode( + filename: str, + nnet_output: torch.Tensor, + HL: kaldifst, + id2word: Dict[int, str], +) -> List[str]: + decodable = DecodableCtc(nnet_output) + decoder_opts = FasterDecoderOptions(max_active=3000) + decoder = FasterDecoder(HL, decoder_opts) + decoder.decode(decodable) + + if not decoder.reached_final(): + print(f"failed to decode {filename}") + return [""] + + ok, best_path = decoder.get_best_path() + + ( + ok, + isymbols_out, + osymbols_out, + total_weight, + ) = kaldifst.get_linear_symbol_sequence(best_path) + if not ok: + print(f"failed to get linear symbol sequence for {filename}") + return [""] + + hyps = [id2word[i] for i in osymbols_out if id2word[i] != ""] + + return hyps + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + device = torch.device("cpu") + + logging.info(f"device: {device}") + + logging.info("Loading torchscript model") + model = torch.jit.load(args.nn_model) + model.eval() + model.to(device) + + logging.info(f"Loading HL from {args.HL}") + HL = kaldifst.StdVectorFst.read(args.HL) + + sample_rate = 8000 + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = sample_rate + opts.mel_opts.num_bins = 23 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, expected_sample_rate=sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + nnet_output = model(features) + + id2word = read_words(args.words) + + hyps = [] + for i in range(nnet_output.shape[0]): + hyp = decode( + filename=args.sound_files[0], + nnet_output=nnet_output[i], + HL=HL, + id2word=id2word, + ) + hyps.append(hyp) + + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/icefall/ctc/.gitignore b/icefall/ctc/.gitignore new file mode 100644 index 000000000..8154cb57f --- /dev/null +++ b/icefall/ctc/.gitignore @@ -0,0 +1,2 @@ +*.pdf +*.gv diff --git a/icefall/ctc/README.md b/icefall/ctc/README.md new file mode 100644 index 000000000..07b0ff8cd --- /dev/null +++ b/icefall/ctc/README.md @@ -0,0 +1,17 @@ +# Introduction + +This folder uses [kaldifst][kaldifst] for graph construction +and decoders from [kaldi-hmm-gmm][kaldi-hmm-gmm] for CTC decoding. + +It supports only `CPU`. + +You can use + +```bash +pip install kaldifst kaldi-hmm-gmm +``` +to install the dependencies. + +[kaldi-hmm-gmm]: https://github.com/csukuangfj/kaldi-hmm-gmm +[kaldifst]: https://github.com/k2-fsa/kaldifst +[k2]: https://github.com/k2-fsa/k2 diff --git a/icefall/ctc/__init__.py b/icefall/ctc/__init__.py new file mode 100644 index 000000000..b546b31af --- /dev/null +++ b/icefall/ctc/__init__.py @@ -0,0 +1,6 @@ +from .prepare_lang import ( + Lexicon, + make_lexicon_fst_no_silence, + make_lexicon_fst_with_silence, +) +from .topo import add_disambig_self_loops, add_one, build_standard_ctc_topo diff --git a/icefall/ctc/prepare_lang.py b/icefall/ctc/prepare_lang.py new file mode 100644 index 000000000..4801b1beb --- /dev/null +++ b/icefall/ctc/prepare_lang.py @@ -0,0 +1,334 @@ +# Copyright 2023 Xiaomi Corp. (author: Fangjun Kuang) + +""" +The lang_dir should contain the following files: + - "lexicon_disambig.txt" + - "tokens.txt" + - "words.txt" +""" + +import math +from collections import defaultdict +from pathlib import Path +from typing import List, Tuple + +import kaldifst +import re + + +class Lexicon: + """Once constructed it is immutable""" + + def __init__( + self, + lang_dir: str, + disambig_pattern: str = re.compile(r"^#\d+$"), + ): + """ + Args: + lang_dir: + The path to the lang directory. We expect that it contains the + following files: + - lexicon_disambig.txt + - tokens.txt + - words.txt + + The format of the above files is described below. + + (1) lexicon_disambig.txt + + Each line in the lexicon_disambig.txt has the following format: + + word token1 token2 ... tokenN + + That is, the first field is the word, the remaining fields are + pronunciations of this word. Fields are separated by space(s). + + (2) tokens.txt + + Each line in tokens.txt has two fields separated by space(s): + + token ID + + The first field is the token symbol and the second filed is the + integer ID of the token. + + (3) words.txt + + Each line in words.txt has two fields separated by space(s): + + word ID + + The first field is the word symbol and the second filed is the + integer ID of the word. + disambig_pattern: + It contains the pattern for disambiguation symbols. + """ + lang_dir = Path(lang_dir) + + lexicon_txt = lang_dir / "lexicon_disambig.txt" + tokens_txt = lang_dir / "tokens.txt" + words_txt = lang_dir / "words.txt" + + assert lexicon_txt.is_file(), lexicon_txt + assert tokens_txt.is_file(), tokens_txt + assert words_txt.is_file(), words_txt + + self._read_lexicon(lexicon_txt) + self._read_tokens(tokens_txt) + self._read_words(words_txt) + + self.disambig_pattern = disambig_pattern + + max_disambig_id = -1 + for s, i in self.token2id.items(): + if self.disambig_pattern.match(s) and i > max_disambig_id: + max_disambig_id = i + + self.max_disambig_id = max_disambig_id + + def _read_lexicon(self, lexicon_txt: str): + word2phones = defaultdict(list) + with open(lexicon_txt, encoding="utf-8") as f: + for line in f: + word_phones = line.strip().split() + assert len(word_phones) >= 2, (word_phones, line) + word = word_phones[0] + phones: str = " ".join(word_phones[1:]) + word2phones[word].append(phones) + # We use a list here since a word may have multiple + # pronunciations + + self.word2phones = word2phones + + def _read_tokens(self, tokens_txt): + token2id = dict() + id2token = dict() + with open(tokens_txt, encoding="utf-8") as f: + for line in f: + token_id = line.strip().split() + assert len(token_id) == 2, token_id + + token = token_id[0] + idx = int(token_id[1]) + + assert token not in token2id, f"Duplicate token {line}" + assert idx not in id2token, f"Duplicate ID {line}" + + token2id[token] = idx + id2token[idx] = token + self.token2id = token2id + self.id2token = id2token + + def _read_words(self, words_txt): + word2id = dict() + id2word = dict() + with open(words_txt, encoding="utf-8") as f: + for line in f: + word_id = line.strip().split() + assert len(word_id) == 2, word_id + + word = word_id[0] + idx = int(word_id[1]) + + assert word not in word2id, f"Duplicate token {line}" + assert idx not in id2word, f"Duplicate ID {line}" + + word2id[word] = idx + id2word[idx] = word + + self.word2id = word2id + self.id2word = id2word + + def __iter__(self) -> Tuple[str, List[str]]: + for word, phones_list in self.word2phones.items(): + for phones in phones_list: + yield word, phones + + def __str__(self): + return str(self.word2phones) + + @property + def tokens(self) -> List[int]: + """Return a list of token IDs excluding those from + disambiguation symbols. + + Caution: + 0 is not a token ID so it is excluded from the return value. + """ + ans = [] + for s in self.token2id: + if not self.disambig_pattern.match(s): + ans.append(self.token2id[s]) + if 0 in ans: + ans.remove(0) + ans.sort() + return ans + + +# See also +# http://vpanayotov.blogspot.com/2012/06/kaldi-decoding-graph-construction.html +def make_lexicon_fst_with_silence( + lexicon: Lexicon, + sil_prob: float = 0.5, + sil_phone: str = "SIL", + attach_symbol_table: bool = True, +) -> kaldifst.StdVectorFst: + phone2id = lexicon.token2id + word2id = lexicon.word2id + + assert sil_phone in phone2id + + assert sil_phone in phone2id, sil_phone + + sil_cost = -1 * math.log(sil_prob) + no_sil_cost = -1 * math.log(1.0 - sil_prob) + + fst = kaldifst.StdVectorFst() + + start_state = fst.add_state() + loop_state = fst.add_state() + sil_state = fst.add_state() + + fst.start = start_state + fst.set_final(state=loop_state, weight=0) + + fst.add_arc( + state=start_state, + arc=kaldifst.StdArc( + ilabel=0, + olabel=0, + weight=no_sil_cost, + nextstate=loop_state, + ), + ) + + fst.add_arc( + state=start_state, + arc=kaldifst.StdArc( + ilabel=0, + olabel=0, + weight=sil_cost, + nextstate=sil_state, + ), + ) + + fst.add_arc( + state=sil_state, + arc=kaldifst.StdArc( + ilabel=phone2id[sil_phone], + olabel=0, + weight=0, + nextstate=loop_state, + ), + ) + + for word, phones in lexicon: + phoneseq = phones.split() + pron_cost = 0 + cur_state = loop_state + + for i in range(len(phoneseq) - 1): + next_state = fst.add_state() + fst.add_arc( + state=cur_state, + arc=kaldifst.StdArc( + ilabel=phone2id[phoneseq[i]], + olabel=word2id[word] if i == 0 else 0, + weight=pron_cost if i == 0 else 0, + nextstate=next_state, + ), + ) + cur_state = next_state + + i = len(phoneseq) - 1 # note: i == -1 if phoneseq is empty. + + fst.add_arc( + state=cur_state, + arc=kaldifst.StdArc( + ilabel=phone2id[phoneseq[i]] if i >= 0 else 0, + olabel=word2id[word] if i <= 0 else 0, + weight=no_sil_cost + (pron_cost if i <= 0 else 0), + nextstate=loop_state, + ), + ) + + fst.add_arc( + state=cur_state, + arc=kaldifst.StdArc( + ilabel=phone2id[phoneseq[i]] if i >= 0 else 0, + olabel=word2id[word] if i <= 0 else 0, + weight=sil_cost + (pron_cost if i <= 0 else 0), + nextstate=sil_state, + ), + ) + + if attach_symbol_table: + isym = kaldifst.SymbolTable() + for p, i in phone2id.items(): + isym.add_symbol(symbol=p, key=i) + fst.input_symbols = isym + + osym = kaldifst.SymbolTable() + for w, i in word2id.items(): + osym.add_symbol(symbol=w, key=i) + fst.output_symbols = osym + + return fst + + +def make_lexicon_fst_no_silence( + lexicon: Lexicon, + attach_symbol_table: bool = True, +) -> kaldifst.StdVectorFst: + phone2id = lexicon.token2id + word2id = lexicon.word2id + + fst = kaldifst.StdVectorFst() + + start_state = fst.add_state() + fst.start = start_state + fst.set_final(state=start_state, weight=0) + + for word, phones in lexicon: + phoneseq = phones.split() + pron_cost = 0 + cur_state = start_state + + for i in range(len(phoneseq) - 1): + next_state = fst.add_state() + fst.add_arc( + state=cur_state, + arc=kaldifst.StdArc( + ilabel=phone2id[phoneseq[i]], + olabel=word2id[word] if i == 0 else 0, + weight=pron_cost if i == 0 else 0, + nextstate=next_state, + ), + ) + cur_state = next_state + + i = len(phoneseq) - 1 # note: i == -1 if phoneseq is empty. + + fst.add_arc( + state=cur_state, + arc=kaldifst.StdArc( + ilabel=phone2id[phoneseq[i]] if i >= 0 else 0, + olabel=word2id[word] if i <= 0 else 0, + weight=pron_cost if i <= 0 else 0, + nextstate=start_state, + ), + ) + + if attach_symbol_table: + isym = kaldifst.SymbolTable() + for p, i in phone2id.items(): + isym.add_symbol(symbol=p, key=i) + fst.input_symbols = isym + + osym = kaldifst.SymbolTable() + for w, i in word2id.items(): + osym.add_symbol(symbol=w, key=i) + fst.output_symbols = osym + + return fst diff --git a/icefall/ctc/test_ctc_topo.py b/icefall/ctc/test_ctc_topo.py new file mode 100755 index 000000000..4d4667209 --- /dev/null +++ b/icefall/ctc/test_ctc_topo.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +from pathlib import Path + +import graphviz +import kaldifst +import sentencepiece as spm +from prepare_lang import ( + Lexicon, + make_lexicon_fst_no_silence, + make_lexicon_fst_with_silence, +) +from topo import add_disambig_self_loops, add_one, build_standard_ctc_topo + + +def test_yesno(): + lang_dir = "/Users/fangjun/open-source/icefall/egs/yesno/ASR/data/lang_phone" + if not Path(lang_dir).is_dir(): + print(f"{lang_dir} does not exist! Skip testing") + return + lexicon = Lexicon(lang_dir) + max_token_id = max(lexicon.tokens) + + H = build_standard_ctc_topo(max_token_id=max_token_id) + + isym = kaldifst.SymbolTable() + isym.add_symbol(symbol="", key=0) + for i in range(1, max_token_id + 1): + isym.add_symbol(symbol=lexicon.id2token[i], key=i) + + osym = kaldifst.SymbolTable() + osym.add_symbol(symbol="", key=0) + for i in range(1, max_token_id + 1): + osym.add_symbol(symbol=lexicon.id2token[i], key=i) + + H.input_symbols = isym + H.output_symbols = osym + + fst_dot = kaldifst.draw(H, acceptor=False, portrait=True) + source = graphviz.Source(fst_dot) + source.render(outfile="standard_ctc_topo_yesno.pdf") + # See the link below to visualize the above PDF + # https://t.ly/7uXZ9 + + # Now test HL + + # We need to add one to all tokens since we want to use ID 0 + # for epsilon + add_one(H, treat_ilabel_zero_specially=False, update_olabel=True) + + add_disambig_self_loops( + H, + start=lexicon.token2id["#0"] + 1, + end=lexicon.max_disambig_id, + ) + + fst_dot = kaldifst.draw(H, acceptor=False, portrait=True) + source = graphviz.Source(fst_dot) + source.render(outfile="standard_ctc_topo_disambig_yesno.pdf") + + L = make_lexicon_fst_with_silence(lexicon) + + # We also need to change the input labels of L + add_one(L, treat_ilabel_zero_specially=True, update_olabel=False) + + H.output_symbols = None + + kaldifst.arcsort(H, sort_type="olabel") + kaldifst.arcsort(L, sort_type="ilabel") + HL = kaldifst.compose(H, L) + + lexicon.id2token[0] = "" + lexicon.token2id[""] = 0 + + isym = kaldifst.SymbolTable() + isym.add_symbol(symbol="", key=0) + for i in range(0, lexicon.max_disambig_id + 1): + isym.add_symbol(symbol=lexicon.id2token[i], key=i + 1) + + osym = kaldifst.SymbolTable() + for i, word in lexicon.id2word.items(): + osym.add_symbol(symbol=word, key=i) + + HL.input_symbols = isym + HL.output_symbols = osym + + fst_dot = kaldifst.draw(HL, acceptor=False, portrait=True) + source = graphviz.Source(fst_dot) + source.render(outfile="HL_yesno.pdf") + + +def test_librispeech(): + lang_dir = ( + "/star-fj/fangjun/open-source/icefall-2/egs/librispeech/ASR/data/lang_bpe_500" + ) + + if not Path(lang_dir).is_dir(): + print(f"{lang_dir} does not exist! Skip testing") + return + + lexicon = Lexicon(lang_dir) + HL = kaldifst.StdVectorFst.read(lang_dir + "/HL.fst") + + sp = spm.SentencePieceProcessor() + sp.load(lang_dir + "/bpe.model") + + i = lexicon.word2id["HELLOA"] + k = lexicon.word2id["WORLD"] + print(i, k) + s = f""" + 0 1 {i} {i} + 1 2 {k} {k} + 2 + """ + fst = kaldifst.compile( + s=s, + acceptor=False, + ) + + L = make_lexicon_fst_no_silence(lexicon, attach_symbol_table=False) + kaldifst.arcsort(L, sort_type="olabel") + with open("L.fst.txt", "w") as f: + print(L, file=f) + + fst = kaldifst.compose(L, fst) + print(fst) + fst_dot = kaldifst.draw(fst, acceptor=False, portrait=True) + source = graphviz.Source(fst_dot) + source.render(outfile="a.pdf") + print(sp.encode(["HELLOA", "WORLD"])) + + +def main(): + test_yesno() + test_librispeech() + + +if __name__ == "__main__": + main() diff --git a/icefall/ctc/test_prepare_lang.py b/icefall/ctc/test_prepare_lang.py new file mode 100755 index 000000000..6c4b9e510 --- /dev/null +++ b/icefall/ctc/test_prepare_lang.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +from pathlib import Path + +import graphviz +import kaldifst +from prepare_lang import Lexicon, make_lexicon_fst_with_silence + + +def test_yesno(): + lang_dir = "/Users/fangjun/open-source/icefall/egs/yesno/ASR/data/lang_phone" + if not Path(lang_dir).is_dir(): + print(f"{lang_dir} does not exist! Skip testing") + return + + lexicon = Lexicon(lang_dir) + + L = make_lexicon_fst_with_silence(lexicon) + + isym = kaldifst.SymbolTable() + for i, token in lexicon.id2token.items(): + isym.add_symbol(symbol=token, key=i) + + osym = kaldifst.SymbolTable() + for i, word in lexicon.id2word.items(): + osym.add_symbol(symbol=word, key=i) + + L.input_symbols = isym + L.output_symbols = osym + fst_dot = kaldifst.draw(L, acceptor=False, portrait=True) + source = graphviz.Source(fst_dot) + source.render(outfile="L_yesno.pdf") + # See the link below to visualize the above PDF + # https://t.ly/jMfXW + + +def main(): + test_yesno() + + +if __name__ == "__main__": + main() diff --git a/icefall/ctc/topo.py b/icefall/ctc/topo.py new file mode 100644 index 000000000..6a96dd038 --- /dev/null +++ b/icefall/ctc/topo.py @@ -0,0 +1,137 @@ +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +import kaldifst + + +# Note the name contains `standard`; it means there will be non-standard +# topologies. +def build_standard_ctc_topo(max_token_id: int) -> kaldifst.StdVectorFst: + """Build a standard CTC topology. + + Args: + Maximum valid token ID. We assume token IDs are contiguous + and starts from 0. In other words, the vocabulary size is + ``max_token_id + 1``. We assume the ID of the blank symbol is 0. + """ + # Token ID starts from 0 and there are as many states as the + # number of tokens. + # + # Note that epsilon is not a token and the token with ID 0 in tokens.txt + # is not an epsilon. It means input label 0 of the resulting FST does + # not represent an epsilon. + # + # You can use the function `add_one()` to modify the input/output labels + # of the resulting FST + + num_states = max_token_id + 1 + + # Step 1: Create as many states as the number of tokens. + # Each state is a final state + fst = kaldifst.StdVectorFst() + for i in range(num_states): + s = fst.add_state() + fst.set_final(state=s, weight=0) + + # Step 2: Set state 0 as the start state. + # We assume the ID of the blank symbol is 0. + fst.start = 0 + + # Step 3: Build a fully connected graph. + for i in range(num_states): + for k in range(num_states): + fst.add_arc( + state=i, + arc=kaldifst.StdArc( + ilabel=k, + olabel=k if i != k else 0, # if i==k, it is a self loop + weight=0, + nextstate=k, + ), + ) + # Please see ./test_ctc_topo.py if you want to know what the resulting + # FST looks like + + return fst + + +def add_one( + fst: kaldifst.StdVectorFst, + treat_ilabel_zero_specially: bool, + update_olabel: bool, +) -> None: + """Modify the input and output labels of the given FST in-place. + + Args: + fst: + The FST to be modified. It is changed in-place. + treat_ilabel_zero_specially: + If True, then every non-zero input label is increased by one and the + zero input label is not changed. + If False, then every input label is increased by one. + update_olabel: + If False, the output label is not changed. + If True, then every non-zero output label is increased by one. + In either case, output label with 0 is not changed. + """ + for state in kaldifst.StateIterator(fst): + for arc in kaldifst.ArcIterator(fst, state): + # If treat_ilabel_zero_specially is False, we always change it + # Otherwise, we only change non-zero input labels + if treat_ilabel_zero_specially is False or arc.ilabel != 0: + arc.ilabel += 1 + + if update_olabel and arc.olabel != 0: + arc.olabel += 1 + + if fst.input_symbols is not None: + input_symbols = kaldifst.SymbolTable() + input_symbols.add_symbol(symbol="", key=0) + + for i in range(0, fst.input_symbols.num_symbols()): + s = fst.input_symbols.find(i) + input_symbols.add_symbol(symbol=s, key=i + 1) + + fst.input_symbols = input_symbols + + if update_olabel and fst.output_symbols is not None: + output_symbols = kaldifst.SymbolTable() + output_symbols.add_symbol(symbol="", key=0) + + for i in range(0, fst.output_symbols.num_symbols()): + s = fst.output_symbols.find(i) + output_symbols.add_symbol(symbol=s, key=i + 1) + + fst.output_symbols = output_symbols + + +def add_disambig_self_loops(fst: kaldifst.StdVectorFst, start: int, end: int): + """Add self-loops to each state. + + For each disambig symbol, we add a self-loop with input label disambig_id + and output label diambig_id of that disambig symbol. + + Args: + fst: + It is changed in-place. + start: + The ID of #0 + end: + The ID of the last disambig symbol. For instance if there are 3 + disambig symbols ``#0``, ``#1``, and ``#2``, then ``end`` is the ID + of ``#2``. + """ + for state in kaldifst.StateIterator(fst): + for i in range(start, end + 1): + fst.add_arc( + state=state, + arc=kaldifst.StdArc( + ilabel=i, + olabel=i, + weight=0, + nextstate=state, + ), + ) + + if fst.output_symbols: + for i in range(start, end + 1): + fst.output_symbols.add_symbol(symbol=f"#{i-start}", key=i) diff --git a/requirements-ci.txt b/requirements-ci.txt index 652e2ab47..6f8739ce0 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -27,3 +27,4 @@ onnx onnxmltools onnxruntime kaldifst +kaldi-hmm-gmm diff --git a/requirements.txt b/requirements.txt index f0098c236..c031d683c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ kaldifst kaldilm kaldialign +kaldi-hmm-gmm sentencepiece>=0.1.96 tensorboard typeguard From 772ee3955bcfcfcaf76c06aeb40f69765609f7b4 Mon Sep 17 00:00:00 2001 From: Fangjun Kuang Date: Wed, 27 Sep 2023 14:49:27 +0800 Subject: [PATCH 097/100] Support HLG decoding using OpenFst with kaldi decoders (#1275) --- .../scripts/run-pre-trained-conformer-ctc.sh | 61 +++-- .../run-pretrained-conformer-ctc.yml | 9 +- .../jit_pretrained_decode_with_HL.py | 4 +- .../jit_pretrained_decode_with_HLG.py | 232 ++++++++++++++++++ egs/librispeech/ASR/local/prepare_lang_fst.py | 160 +++++++++--- egs/librispeech/ASR/prepare.sh | 2 +- 6 files changed, 412 insertions(+), 56 deletions(-) create mode 100755 egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HLG.py diff --git a/.github/scripts/run-pre-trained-conformer-ctc.sh b/.github/scripts/run-pre-trained-conformer-ctc.sh index 19cbd96fc..a82d85fb2 100755 --- a/.github/scripts/run-pre-trained-conformer-ctc.sh +++ b/.github/scripts/run-pre-trained-conformer-ctc.sh @@ -10,16 +10,30 @@ log() { cd egs/librispeech/ASR -repo_url=https://github.com/csukuangfj/icefall-asr-conformer-ctc-bpe-500 -git lfs install - +# repo_url=https://github.com/csukuangfj/icefall-asr-conformer-ctc-bpe-500 +repo_url=https://huggingface.co/csukuangfj/icefall-asr-librispeech-conformer-ctc-jit-bpe-500-2021-11-09 log "Downloading pre-trained model from $repo_url" -git clone $repo_url +GIT_LFS_SKIP_SMUDGE=1 git clone $repo_url repo=$(basename $repo_url) +pushd $repo + +git lfs pull --include "exp/pretrained.pt" +git lfs pull --include "data/lang_bpe_500/HLG.pt" +git lfs pull --include "data/lang_bpe_500/L.pt" +git lfs pull --include "data/lang_bpe_500/L_disambig.pt" +git lfs pull --include "data/lang_bpe_500/Linv.pt" +git lfs pull --include "data/lang_bpe_500/bpe.model" +git lfs pull --include "data/lang_bpe_500/lexicon.txt" +git lfs pull --include "data/lang_bpe_500/lexicon_disambig.txt" +git lfs pull --include "data/lang_bpe_500/tokens.txt" +git lfs pull --include "data/lang_bpe_500/words.txt" +git lfs pull --include "data/lm/G_3_gram.fst.txt" + +popd log "Display test files" tree $repo/ -ls -lh $repo/test_wavs/*.flac +ls -lh $repo/test_wavs/*.wav log "CTC decoding" @@ -28,9 +42,9 @@ log "CTC decoding" --num-classes 500 \ --checkpoint $repo/exp/pretrained.pt \ --tokens $repo/data/lang_bpe_500/tokens.txt \ - $repo/test_wavs/1089-134686-0001.flac \ - $repo/test_wavs/1221-135766-0001.flac \ - $repo/test_wavs/1221-135766-0002.flac + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav log "HLG decoding" @@ -41,9 +55,9 @@ log "HLG decoding" --tokens $repo/data/lang_bpe_500/tokens.txt \ --words-file $repo/data/lang_bpe_500/words.txt \ --HLG $repo/data/lang_bpe_500/HLG.pt \ - $repo/test_wavs/1089-134686-0001.flac \ - $repo/test_wavs/1221-135766-0001.flac \ - $repo/test_wavs/1221-135766-0002.flac + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav log "CTC decoding on CPU with kaldi decoders using OpenFst" @@ -65,7 +79,8 @@ ls -lh $repo/exp log "Generating H.fst, HL.fst" -./local/prepare_lang_fst.py --lang-dir $repo/data/lang_bpe_500 +./local/prepare_lang_fst.py --lang-dir $repo/data/lang_bpe_500 --ngram-G $repo/data/lm/G_3_gram.fst.txt + ls -lh $repo/data/lang_bpe_500 log "Decoding with H on CPU with OpenFst" @@ -74,9 +89,9 @@ log "Decoding with H on CPU with OpenFst" --nn-model $repo/exp/cpu_jit.pt \ --H $repo/data/lang_bpe_500/H.fst \ --tokens $repo/data/lang_bpe_500/tokens.txt \ - $repo/test_wavs/1089-134686-0001.flac \ - $repo/test_wavs/1221-135766-0001.flac \ - $repo/test_wavs/1221-135766-0002.flac + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav log "Decoding with HL on CPU with OpenFst" @@ -84,6 +99,16 @@ log "Decoding with HL on CPU with OpenFst" --nn-model $repo/exp/cpu_jit.pt \ --HL $repo/data/lang_bpe_500/HL.fst \ --words $repo/data/lang_bpe_500/words.txt \ - $repo/test_wavs/1089-134686-0001.flac \ - $repo/test_wavs/1221-135766-0001.flac \ - $repo/test_wavs/1221-135766-0002.flac + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav + +log "Decoding with HLG on CPU with OpenFst" + +./conformer_ctc/jit_pretrained_decode_with_HLG.py \ + --nn-model $repo/exp/cpu_jit.pt \ + --HLG $repo/data/lang_bpe_500/HLG.fst \ + --words $repo/data/lang_bpe_500/words.txt \ + $repo/test_wavs/1089-134686-0001.wav \ + $repo/test_wavs/1221-135766-0001.wav \ + $repo/test_wavs/1221-135766-0002.wav diff --git a/.github/workflows/run-pretrained-conformer-ctc.yml b/.github/workflows/run-pretrained-conformer-ctc.yml index e268d840d..54845159d 100644 --- a/.github/workflows/run-pretrained-conformer-ctc.yml +++ b/.github/workflows/run-pretrained-conformer-ctc.yml @@ -23,13 +23,20 @@ on: pull_request: types: [labeled] + workflow_dispatch: + inputs: + test-run: + description: 'Test (y/n)?' + required: true + default: 'y' + concurrency: group: run_pre_trained_conformer_ctc-${{ github.ref }} cancel-in-progress: true jobs: run_pre_trained_conformer_ctc: - if: github.event.label.name == 'ready' || github.event_name == 'push' || github.event.label.name == 'ctc' + if: github.event.label.name == 'ready' || github.event_name == 'push' || github.event.inputs.test-run == 'y' runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py index f0326ccdf..3420c4da3 100755 --- a/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py +++ b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HL.py @@ -2,12 +2,12 @@ # Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) """ -This file shows how to use a torchscript model for decoding with H +This file shows how to use a torchscript model for decoding with HL on CPU using OpenFST and decoders from kaldi. Usage: - ./conformer_ctc/jit_pretrained_decode_with_H.py \ + ./conformer_ctc/jit_pretrained_decode_with_HL.py \ --nn-model ./conformer_ctc/exp/cpu_jit.pt \ --HL ./data/lang_bpe_500/HL.fst \ --words ./data/lang_bpe_500/words.txt \ diff --git a/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HLG.py b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HLG.py new file mode 100755 index 000000000..42129f073 --- /dev/null +++ b/egs/librispeech/ASR/conformer_ctc/jit_pretrained_decode_with_HLG.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 +# Copyright 2023 Xiaomi Corp. (authors: Fangjun Kuang) + +""" +This file shows how to use a torchscript model for decoding with HLG +on CPU using OpenFST and decoders from kaldi. + +Usage: + + ./conformer_ctc/jit_pretrained_decode_with_HLG.py \ + --nn-model ./conformer_ctc/exp/cpu_jit.pt \ + --HLG ./data/lang_bpe_500/HLG.fst \ + --words ./data/lang_bpe_500/words.txt \ + ./download/LibriSpeech/test-clean/1089/134686/1089-134686-0002.flac \ + ./download/LibriSpeech/test-clean/1221/135766/1221-135766-0001.flac + +Note that to generate ./conformer_ctc/exp/cpu_jit.pt, +you can use ./export.py --jit 1 +""" + +import argparse +import logging +import math +from typing import Dict, List + +import kaldi_hmm_gmm +import kaldifeat +import kaldifst +import torch +import torchaudio +from kaldi_hmm_gmm import DecodableCtc, FasterDecoder, FasterDecoderOptions +from torch.nn.utils.rnn import pad_sequence + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--nn-model", + type=str, + required=True, + help="""Path to the torchscript model. + You can use ./conformer_ctc/export.py --jit 1 + to obtain it + """, + ) + + parser.add_argument( + "--words", + type=str, + required=True, + help="Path to words.txt", + ) + + parser.add_argument("--HLG", type=str, required=True, help="Path to HLG.fst") + + parser.add_argument( + "sound_files", + type=str, + nargs="+", + help="The input sound file(s) to transcribe. " + "Supported formats are those supported by torchaudio.load(). " + "For example, wav and flac are supported. ", + ) + + return parser + + +def read_words(words_txt: str) -> Dict[int, str]: + id2word = dict() + with open(words_txt, encoding="utf-8") as f: + for line in f: + word, idx = line.strip().split() + id2word[int(idx)] = word + + return id2word + + +def read_sound_files( + filenames: List[str], expected_sample_rate: float +) -> List[torch.Tensor]: + """Read a list of sound files into a list 1-D float32 torch tensors. + Args: + filenames: + A list of sound filenames. + expected_sample_rate: + The expected sample rate of the sound files. + Returns: + Return a list of 1-D float32 torch tensors. + """ + ans = [] + for f in filenames: + wave, sample_rate = torchaudio.load(f) + if sample_rate != expected_sample_rate: + wave = torchaudio.functional.resample( + wave, + orig_freq=sample_rate, + new_freq=expected_sample_rate, + ) + + # We use only the first channel + ans.append(wave[0].contiguous()) + return ans + + +def decode( + filename: str, + nnet_output: torch.Tensor, + HLG: kaldifst, + id2word: Dict[int, str], +) -> List[str]: + """ + Args: + filename: + Path to the filename for decoding. Used for debugging. + nnet_output: + A 2-D float32 tensor of shape (num_frames, vocab_size). It + contains output from log_softmax. + HLG: + The HLG graph. + word2token: + A map mapping token ID to word string. + Returns: + Return a list of decoded words. + """ + logging.info(f"{filename}, {nnet_output.shape}") + decodable = DecodableCtc(nnet_output.cpu()) + + decoder_opts = FasterDecoderOptions(max_active=3000) + decoder = FasterDecoder(HLG, decoder_opts) + decoder.decode(decodable) + + if not decoder.reached_final(): + print(f"failed to decode {filename}") + return [""] + + ok, best_path = decoder.get_best_path() + + ( + ok, + isymbols_out, + osymbols_out, + total_weight, + ) = kaldifst.get_linear_symbol_sequence(best_path) + if not ok: + print(f"failed to get linear symbol sequence for {filename}") + return [""] + + # are shifted by 1 during graph construction + hyps = [id2word[i] for i in osymbols_out] + + return hyps + + +@torch.no_grad() +def main(): + parser = get_parser() + args = parser.parse_args() + + device = torch.device("cpu") + + logging.info(f"device: {device}") + + logging.info("Loading torchscript model") + model = torch.jit.load(args.nn_model) + model.eval() + model.to(device) + + logging.info(f"Loading HLG from {args.HLG}") + HLG = kaldifst.StdVectorFst.read(args.HLG) + + sample_rate = 16000 + + logging.info("Constructing Fbank computer") + opts = kaldifeat.FbankOptions() + opts.device = device + opts.frame_opts.dither = 0 + opts.frame_opts.snip_edges = False + opts.frame_opts.samp_freq = sample_rate + opts.mel_opts.num_bins = 80 + + fbank = kaldifeat.Fbank(opts) + + logging.info(f"Reading sound files: {args.sound_files}") + waves = read_sound_files( + filenames=args.sound_files, expected_sample_rate=sample_rate + ) + waves = [w.to(device) for w in waves] + + logging.info("Decoding started") + features = fbank(waves) + feature_lengths = [f.shape[0] for f in features] + feature_lengths = torch.tensor(feature_lengths) + + supervisions = dict() + supervisions["sequence_idx"] = torch.arange(len(features)) + supervisions["start_frame"] = torch.zeros(len(features)) + supervisions["num_frames"] = feature_lengths + + features = pad_sequence(features, batch_first=True, padding_value=math.log(1e-10)) + + nnet_output, _, _ = model(features, supervisions) + feature_lengths = ((feature_lengths - 1) // 2 - 1) // 2 + + id2word = read_words(args.words) + + hyps = [] + for i in range(nnet_output.shape[0]): + hyp = decode( + filename=args.sound_files[i], + nnet_output=nnet_output[i, : feature_lengths[i]], + HLG=HLG, + id2word=id2word, + ) + hyps.append(hyp) + + s = "\n" + for filename, hyp in zip(args.sound_files, hyps): + words = " ".join(hyp) + s += f"{filename}:\n{words}\n\n" + logging.info(s) + + logging.info("Decoding Done") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + main() diff --git a/egs/librispeech/ASR/local/prepare_lang_fst.py b/egs/librispeech/ASR/local/prepare_lang_fst.py index e8401123f..fb1e7f9c0 100755 --- a/egs/librispeech/ASR/local/prepare_lang_fst.py +++ b/egs/librispeech/ASR/local/prepare_lang_fst.py @@ -8,6 +8,7 @@ tokens.txt, and words.txt and generates the following files: - H.fst - HL.fst + - HLG.fst Note that saved files are in OpenFst binary format. @@ -56,9 +57,114 @@ def get_args(): help="True if the lexicon has silence.", ) + parser.add_argument( + "--ngram-G", + type=str, + help="""If not empty, it is the filename of G used to build HLG. + For instance, --ngram-G=./data/lm/G_3_fst.txt + """, + ) + return parser.parse_args() +def build_HL( + H: kaldifst.StdVectorFst, + L: kaldifst.StdVectorFst, + has_silence: bool, + lexicon: Lexicon, +) -> kaldifst.StdVectorFst: + if has_silence: + # We also need to change the input labels of L + add_one(L, treat_ilabel_zero_specially=True, update_olabel=False) + else: + add_one(L, treat_ilabel_zero_specially=False, update_olabel=False) + + # Invoke add_disambig_self_loops() so that it eats the disambig symbols + # from L after composition + add_disambig_self_loops( + H, + start=lexicon.token2id["#0"] + 1, + end=lexicon.max_disambig_id + 1, + ) + + kaldifst.arcsort(H, sort_type="olabel") + kaldifst.arcsort(L, sort_type="ilabel") + + HL = kaldifst.compose(H, L) + kaldifst.determinize_star(HL) + + disambig0 = lexicon.token2id["#0"] + 1 + max_disambig = lexicon.max_disambig_id + 1 + for state in kaldifst.StateIterator(HL): + for arc in kaldifst.ArcIterator(HL, state): + # If treat_ilabel_zero_specially is False, we always change it + # Otherwise, we only change non-zero input labels + if disambig0 <= arc.ilabel <= max_disambig: + arc.ilabel = 0 + + # Note: We are not composing L with G, so there is no need to add + # self-loops to L to handle #0 + + return HL + + +def build_HLG( + H: kaldifst.StdVectorFst, + L: kaldifst.StdVectorFst, + G: kaldifst.StdVectorFst, + has_silence: bool, + lexicon: Lexicon, +) -> kaldifst.StdVectorFst: + if has_silence: + # We also need to change the input labels of L + add_one(L, treat_ilabel_zero_specially=True, update_olabel=False) + else: + add_one(L, treat_ilabel_zero_specially=False, update_olabel=False) + + # add-self-loops + token_disambig0 = lexicon.token2id["#0"] + 1 + word_disambig0 = lexicon.word2id["#0"] + + kaldifst.add_self_loops(L, isyms=[token_disambig0], osyms=[word_disambig0]) + + kaldifst.arcsort(L, sort_type="olabel") + kaldifst.arcsort(G, sort_type="ilabel") + LG = kaldifst.compose(L, G) + kaldifst.determinize_star(LG) + kaldifst.minimize_encoded(LG) + + kaldifst.arcsort(LG, sort_type="ilabel") + + # Invoke add_disambig_self_loops() so that it eats the disambig symbols + # from L after composition + add_disambig_self_loops( + H, + start=lexicon.token2id["#0"] + 1, + end=lexicon.max_disambig_id + 1, + ) + + kaldifst.arcsort(H, sort_type="olabel") + + HLG = kaldifst.compose(H, LG) + kaldifst.determinize_star(HLG) + + disambig0 = lexicon.token2id["#0"] + 1 + max_disambig = lexicon.max_disambig_id + 1 + for state in kaldifst.StateIterator(HLG): + for arc in kaldifst.ArcIterator(HLG, state): + # If treat_ilabel_zero_specially is False, we always change it + # Otherwise, we only change non-zero input labels + if disambig0 <= arc.ilabel <= max_disambig: + arc.ilabel = 0 + return HLG + + +def copy_fst(fst): + # Please don't use fst.copy() + return kaldifst.StdVectorFst(fst) + + def main(): args = get_args() lang_dir = args.lang_dir @@ -82,43 +188,29 @@ def main(): else: L = make_lexicon_fst_no_silence(lexicon, attach_symbol_table=False) - if args.has_silence: - # We also need to change the input labels of L - add_one(L, treat_ilabel_zero_specially=True, update_olabel=False) - else: - add_one(L, treat_ilabel_zero_specially=False, update_olabel=False) - - # Invoke add_disambig_self_loops() so that it eats the disambig symbols - # from L after composition - add_disambig_self_loops( - H, - start=lexicon.token2id["#0"] + 1, - end=lexicon.max_disambig_id + 1, - ) - with open("H_1.fst.txt", "w") as f: - print(H, file=f) - - kaldifst.arcsort(H, sort_type="olabel") - kaldifst.arcsort(L, sort_type="ilabel") - logging.info("Building HL") - HL = kaldifst.compose(H, L) - kaldifst.determinize_star(HL) - - disambig0 = lexicon.token2id["#0"] + 1 - max_disambig = lexicon.max_disambig_id + 1 - for state in kaldifst.StateIterator(HL): - for arc in kaldifst.ArcIterator(HL, state): - # If treat_ilabel_zero_specially is False, we always change it - # Otherwise, we only change non-zero input labels - if disambig0 <= arc.ilabel <= max_disambig: - arc.ilabel = 0 - - # Note: We are not composing L with G, so there is no need to add - # self-loops to L to handle #0 - + HL = build_HL( + H=copy_fst(H), + L=copy_fst(L), + has_silence=args.has_silence, + lexicon=lexicon, + ) HL.write(f"{lang_dir}/HL.fst") + if not args.ngram_G: + logging.info("Skip building HLG") + return + + logging.info("Building HLG") + with open(args.ngram_G) as f: + G = kaldifst.compile( + s=f.read(), + acceptor=False, + ) + + HLG = build_HLG(H=H, L=L, G=G, has_silence=args.has_silence, lexicon=lexicon) + HLG.write(f"{lang_dir}/HLG.fst") + if __name__ == "__main__": formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" diff --git a/egs/librispeech/ASR/prepare.sh b/egs/librispeech/ASR/prepare.sh index fca2c6cc4..93d010ea8 100755 --- a/egs/librispeech/ASR/prepare.sh +++ b/egs/librispeech/ASR/prepare.sh @@ -244,7 +244,7 @@ if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then fi if [ ! -f $lang_dir/HL.fst ]; then - ./local/prepare_lang_fst.py --lang-dir $lang_dir + ./local/prepare_lang_fst.py --lang-dir $lang_dir --ngram-G ./data/lm/G_3_gram.fst.txt fi done fi From a5ba1133c4cc6755530217876e8ff3bfb64e4d36 Mon Sep 17 00:00:00 2001 From: yaguang Date: Wed, 27 Sep 2023 17:33:38 +0800 Subject: [PATCH 098/100] Compatible with new lhotse versions. (#1278) --- egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py b/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py index 180930747..6abe6c084 100644 --- a/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py +++ b/egs/aishell/ASR/tdnn_lstm_ctc/asr_datamodule.py @@ -198,7 +198,7 @@ class AishellAsrDataModule: if self.args.enable_musan: logging.info("Enable MUSAN") transforms.append( - CutMix(cuts=cuts_musan, prob=0.5, snr=(10, 20), preserve_id=True) + CutMix(cuts=cuts_musan, p=0.5, snr=(10, 20), preserve_id=True) ) else: logging.info("Disable MUSAN") From 8181d19860cbec21593e32af99deb4959f762540 Mon Sep 17 00:00:00 2001 From: yaguang Date: Wed, 27 Sep 2023 17:35:26 +0800 Subject: [PATCH 099/100] check bbpe model exists in advance. (#1277) --- egs/aishell/ASR/local/train_bbpe_model.py | 37 +++++++++++------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/egs/aishell/ASR/local/train_bbpe_model.py b/egs/aishell/ASR/local/train_bbpe_model.py index d231d5d77..48160897d 100755 --- a/egs/aishell/ASR/local/train_bbpe_model.py +++ b/egs/aishell/ASR/local/train_bbpe_model.py @@ -15,7 +15,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - # You can install sentencepiece via: # # pip install sentencepiece @@ -26,12 +25,12 @@ # Please install a version >=0.1.96 import argparse -import re import shutil import tempfile from pathlib import Path import sentencepiece as spm + from icefall import byte_encode, tokenize_by_CJK_char @@ -74,6 +73,11 @@ def main(): model_type = "unigram" model_prefix = f"{lang_dir}/{model_type}_{vocab_size}" + model_file = Path(model_prefix + ".model") + if model_file.is_file(): + print(f"{model_file} exists - skipping") + return + character_coverage = 1.0 input_sentence_size = 100000000 @@ -88,23 +92,18 @@ def main(): _convert_to_bchar(args.transcript, train_text) - model_file = Path(model_prefix + ".model") - if not model_file.is_file(): - spm.SentencePieceTrainer.train( - input=train_text, - vocab_size=vocab_size, - model_type=model_type, - model_prefix=model_prefix, - input_sentence_size=input_sentence_size, - character_coverage=character_coverage, - user_defined_symbols=user_defined_symbols, - unk_id=unk_id, - bos_id=-1, - eos_id=-1, - ) - else: - print(f"{model_file} exists - skipping") - return + spm.SentencePieceTrainer.train( + input=train_text, + vocab_size=vocab_size, + model_type=model_type, + model_prefix=model_prefix, + input_sentence_size=input_sentence_size, + character_coverage=character_coverage, + user_defined_symbols=user_defined_symbols, + unk_id=unk_id, + bos_id=-1, + eos_id=-1, + ) shutil.copyfile(model_file, f"{lang_dir}/bbpe.model") From 3abc290c1119d3adbb64112d854a9973ec486b3d Mon Sep 17 00:00:00 2001 From: Dongji Gao Date: Thu, 28 Sep 2023 19:52:46 -0400 Subject: [PATCH 100/100] Add scripts and recipe for BTC/OTC (#1255) --- egs/librispeech/WSASR/README.md | 224 ++++ .../WSASR/conformer_ctc2/__init__.py | 1 + .../WSASR/conformer_ctc2/asr_datamodule.py | 369 ++++++ .../WSASR/conformer_ctc2/attention.py | 1 + .../WSASR/conformer_ctc2/conformer.py | 949 ++++++++++++++ .../WSASR/conformer_ctc2/decode.py | 718 +++++++++++ .../WSASR/conformer_ctc2/export.py | 1 + .../WSASR/conformer_ctc2/label_smoothing.py | 1 + egs/librispeech/WSASR/conformer_ctc2/optim.py | 1 + .../WSASR/conformer_ctc2/scaling.py | 1 + .../WSASR/conformer_ctc2/subsampling.py | 184 +++ egs/librispeech/WSASR/conformer_ctc2/train.py | 1115 +++++++++++++++++ .../WSASR/conformer_ctc2/transformer.py | 1055 ++++++++++++++++ egs/librispeech/WSASR/figures/del.png | Bin 0 -> 14544 bytes egs/librispeech/WSASR/figures/ins.png | Bin 0 -> 16947 bytes .../WSASR/figures/otc_emission.drawio.png | Bin 0 -> 39476 bytes egs/librispeech/WSASR/figures/otc_g.png | Bin 0 -> 33339 bytes .../figures/otc_training_graph.drawio.png | Bin 0 -> 154014 bytes egs/librispeech/WSASR/figures/sub.png | Bin 0 -> 15900 bytes egs/librispeech/WSASR/local/compile_hlg.py | 173 +++ .../WSASR/local/compute_fbank_librispeech.py | 162 +++ .../WSASR/local/compute_ssl_librispeech.py | 100 ++ egs/librispeech/WSASR/local/filter_cuts.py | 160 +++ .../WSASR/local/get_words_from_lexicon.py | 48 + .../WSASR/local/make_error_cutset.py | 175 +++ egs/librispeech/WSASR/local/prepare_lang.py | 413 ++++++ .../WSASR/local/prepare_otc_lang_bpe.py | 295 +++++ .../WSASR/local/train_bpe_model.py | 100 ++ .../WSASR/local/validate_bpe_lexicon.py | 85 ++ .../WSASR/local/validate_manifest.py | 92 ++ egs/librispeech/WSASR/prepare.sh | 233 ++++ icefall/otc_graph_compiler.py | 246 ++++ icefall/utils.py | 64 + 33 files changed, 6966 insertions(+) create mode 100644 egs/librispeech/WSASR/README.md create mode 120000 egs/librispeech/WSASR/conformer_ctc2/__init__.py create mode 100644 egs/librispeech/WSASR/conformer_ctc2/asr_datamodule.py create mode 120000 egs/librispeech/WSASR/conformer_ctc2/attention.py create mode 100644 egs/librispeech/WSASR/conformer_ctc2/conformer.py create mode 100755 egs/librispeech/WSASR/conformer_ctc2/decode.py create mode 120000 egs/librispeech/WSASR/conformer_ctc2/export.py create mode 120000 egs/librispeech/WSASR/conformer_ctc2/label_smoothing.py create mode 120000 egs/librispeech/WSASR/conformer_ctc2/optim.py create mode 120000 egs/librispeech/WSASR/conformer_ctc2/scaling.py create mode 100644 egs/librispeech/WSASR/conformer_ctc2/subsampling.py create mode 100755 egs/librispeech/WSASR/conformer_ctc2/train.py create mode 100644 egs/librispeech/WSASR/conformer_ctc2/transformer.py create mode 100644 egs/librispeech/WSASR/figures/del.png create mode 100644 egs/librispeech/WSASR/figures/ins.png create mode 100644 egs/librispeech/WSASR/figures/otc_emission.drawio.png create mode 100644 egs/librispeech/WSASR/figures/otc_g.png create mode 100644 egs/librispeech/WSASR/figures/otc_training_graph.drawio.png create mode 100644 egs/librispeech/WSASR/figures/sub.png create mode 100755 egs/librispeech/WSASR/local/compile_hlg.py create mode 100755 egs/librispeech/WSASR/local/compute_fbank_librispeech.py create mode 100755 egs/librispeech/WSASR/local/compute_ssl_librispeech.py create mode 100644 egs/librispeech/WSASR/local/filter_cuts.py create mode 100755 egs/librispeech/WSASR/local/get_words_from_lexicon.py create mode 100755 egs/librispeech/WSASR/local/make_error_cutset.py create mode 100755 egs/librispeech/WSASR/local/prepare_lang.py create mode 100755 egs/librispeech/WSASR/local/prepare_otc_lang_bpe.py create mode 100755 egs/librispeech/WSASR/local/train_bpe_model.py create mode 100755 egs/librispeech/WSASR/local/validate_bpe_lexicon.py create mode 100755 egs/librispeech/WSASR/local/validate_manifest.py create mode 100755 egs/librispeech/WSASR/prepare.sh create mode 100644 icefall/otc_graph_compiler.py diff --git a/egs/librispeech/WSASR/README.md b/egs/librispeech/WSASR/README.md new file mode 100644 index 000000000..3b8822fd2 --- /dev/null +++ b/egs/librispeech/WSASR/README.md @@ -0,0 +1,224 @@ +# Introduction + +This is a weakly supervised ASR recipe for the LibriSpeech (clean 100 hours) dataset. We train a +conformer model using [Bypass Temporal Classification](https://arxiv.org/pdf/2306.01031.pdf) (BTC)/[Omni-temporal Classification](https://arxiv.org/pdf/2309.15796.pdf) (OTC) with transcripts with synthetic errors. In this README, we will describe +the task and the BTC/OTC training process. + +Note that OTC is an extension of BTC and supports all BTC functions. Therefore, in the following, we only describe OTC. +## Task +We propose BTC/OTC to directly train an ASR system leveraging weak supervision, i.e., speech with non-verbatim transcripts. This is achieved by using a special token $\star$ to model uncertainties (i.e., substitution errors, insertion errors, and deletion errors) +within the WFST framework during training. + + +

+
+ Image 1 + +
+
+ Image 2 + +
+
+ Image 3 + +
+
+
Examples of errors (substitution, insertion, and deletion) in the transcript. The grey box is the verbatim transcript and the red box is the inaccurate transcript. Inaccurate words are marked in bold.


+ + +We modify $G(\mathbf{y})$ by adding self-loop arcs into each state and bypass arcs into each arc. +

+ Image Alt Text + +

+ +We incorporate the penalty strategy and apply different configurations for the self-loop arc and bypass arc. The penalties are set as + +$$\lambda_{1_{i}} = \beta_{1} * \tau_{1}^{i},\quad \lambda_{2_{i}} = \beta_{2} * \tau_{2}^{i}$$ + +for the $i$-th training epoch. $\beta$ is the initial penalty that encourages the model to rely more on the given transcript at the start of training. +It decays exponentially by a factor of $\tau \in (0, 1)$, gradually encouraging the model to align speech with $\star$ when getting confused. + +After composing the modified WFST $G_{\text{otc}}(\mathbf{y})$ with $L$ and $T$, the OTC training graph is shown in this figure: +
+ Image Alt Text +
OTC training graph. The self-loop arcs and bypass arcs are highlighted in green and blue, respectively.
+
+ +The $\star$ is represented as the average probability of all non-blank tokens. +

+ +

+ +The weight of $\star$ is the log average probability of "a" and "b": $\log \frac{e^{-1.2} + e^{-2.3}}{2} = -1.6$ and $\log \frac{e^{-1.9} + e^{-0.5}}{2} = -1.0$ for 2 frames. + +## Description of the recipe +### Preparation +``` +# feature_type can be ssl or fbank +feature_type=ssl +feature_dir="data/${feature_type}" +manifest_dir="${feature_dir}" +lang_dir="data/lang" +lm_dir="data/lm" +exp_dir="conformer_ctc2/exp" +otc_token="" + +./prepare.sh \ + --feature-type "${feature_type}" \ + --feature-dir "${feature_dir}" \ + --lang-dir "${lang_dir}" \ + --lm-dir "${lm_dir}" \ + --otc-token "${otc_token}" +``` +This script adds the 'otc_token' ('\') and its corresponding sentence-piece ('▁\') to 'words.txt' and 'tokens.txt,' respectively. Additionally, it computes SSL features using the 'wav2vec2-base' model. (You can use GPU to accelerate feature extraction). + +### Making synthetic errors to the transcript (train-clean-100) [optional] +``` +sub_er=0.17 +ins_er=0.17 +del_er=0.17 +synthetic_train_manifest="librispeech_cuts_train-clean-100_${sub_er}_${ins_er}_${del_er}.jsonl.gz" + +./local/make_error_cutset.py \ + --input-cutset "${manifest_dir}/librispeech_cuts_train-clean-100.jsonl.gz" \ + --words-file "${lang_dir}/words.txt" \ + --sub-error-rate "${sub_er}" \ + --ins-error-rate "${ins_er}" \ + --del-error-rate "${del_er}" \ + --output-cutset "${manifest_dir}/${synthetic_train_manifest}" +``` +This script generates synthetic substitution, insertion, and deletion errors in the transcript with ratios 'sub_er', 'ins_er', and 'del_er', respectively. The original transcript is saved as 'verbatim transcript' in the cutset, along with information on how the transcript is corrupted: + + - '[hello]' indicates the original word 'hello' is substituted by another word + - '[]' indicates an extra word is inserted into the transcript + - '-hello-' indicates the word 'hello' is deleted from the transcript + +So if the original transcript is "have a nice day" and the synthetic one is "a very good day", the 'verbatim transcript' would be: +``` +original: have a nice day +synthetic: a very good day +verbatim: -have- a [] [nice] day +``` + +### Training +The training uses synthetic data based on the train-clean-100 subset. +``` +otc_lang_dir=data/lang_bpe_200 + +allow_bypass_arc=true +allow_self_loop_arc=true +initial_bypass_weight=-19 +initial_self_loop_weight=3.75 +bypass_weight_decay=0.975 +self_loop_weight_decay=0.999 + +show_alignment=true + +export CUDA_VISIBLE_DEVICES="0,1,2,3" +./conformer_ctc2/train.py \ + --world-size 4 \ + --manifest-dir "${manifest_dir}" \ + --train-manifest "${synthetic_train_manifest}" \ + --exp-dir "${exp_dir}" \ + --lang-dir "${otc_lang_dir}" \ + --otc-token "${otc_token}" \ + --allow-bypass-arc "${allow_bypass_arc}" \ + --allow-self-loop-arc "${allow_self_loop_arc}" \ + --initial-bypass-weight "${initial_bypass_weight}" \ + --initial-self-loop-weight "${initial_self_loop_weight}" \ + --bypass-weight-decay "${bypass_weight_decay}" \ + --self-loop-weight-decay "${self_loop_weight_decay}" \ + --show-alignment "${show_alignment}" +``` +The bypass arc deals with substitution and insertion errors, while the self-loop arc deals with deletion errors. Using "--show-alignment" would print the best alignment during training, which is very helpful for tuning hyperparameters and debugging. + +### Decoding +``` +export CUDA_VISIBLE_DEVICES="0" +./conformer_ctc2/decode.py \ + --manifest-dir "${manifest_dir}" \ + --exp-dir "${exp_dir}" \ + --lang-dir "${otc_lang_dir}" \ + --lm-dir "${lm_dir}" \ + --otc-token "${otc_token}" +``` + +### Results (ctc-greedy-search) + + + + + + + + + + + + + + + + + + + + + + + + + + +
Training Criterionsslfbank
test-cleantest-othertest-cleantest-other
CTC100.0100.099.8999.98
OTC11.8925.4620.1444.24
+ +### Results (1best, blank_bias=-4) + + + + + + + + + + + + + + + + + + + + + + + + + + +
Training Criterionsslfbank
test-cleantest-othertest-cleantest-other
CTC98.4098.6899.7999.86
OTC6.5915.9811.7832.38
+ +## Pre-trained Model +Pre-trained model: + +## Citations +``` +@inproceedings{gao2023bypass, + title={Bypass Temporal Classification: Weakly Supervised Automatic Speech Recognition with Imperfect Transcripts}, + author={Gao, Dongji and Wiesner, Matthew and Xu, Hainan and Garcia, Leibny Paola and Povey, Daniel and Khudanpur, Sanjeev}, + booktitle={INTERSPEECH}, + year={2023} +} + +@inproceedings{gao2023learning, + title={Learning from Flawed Data: Weakly Supervised Automatic Speech Recognition}, + author={Gao, Dongji and Xu, Hainan and Raj, Desh and Garcia, Leibny Paola and Povey, Daniel and Khudanpur, Sanjeev}, + booktitle={IEEE ASRU}, + year={2023} +} +``` diff --git a/egs/librispeech/WSASR/conformer_ctc2/__init__.py b/egs/librispeech/WSASR/conformer_ctc2/__init__.py new file mode 120000 index 000000000..43a85af20 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/__init__.py @@ -0,0 +1 @@ +../../ASR/pruned_transducer_stateless2/__init__.py \ No newline at end of file diff --git a/egs/librispeech/WSASR/conformer_ctc2/asr_datamodule.py b/egs/librispeech/WSASR/conformer_ctc2/asr_datamodule.py new file mode 100644 index 000000000..1b6991bcd --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/asr_datamodule.py @@ -0,0 +1,369 @@ +# Copyright 2021 Piotr Żelasko +# Copyright 2022 Xiaomi Corporation (Author: Mingshuang Luo) +# 2023 John Hopkins University (author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 inspect +import logging +from functools import lru_cache +from pathlib import Path +from typing import Any, Dict, Optional + +import torch +from lhotse import CutSet, load_manifest, load_manifest_lazy +from lhotse.dataset import ( # noqa F401 for PrecomputedFeatures + CutConcatenate, + CutMix, + DynamicBucketingSampler, + K2SpeechRecognitionDataset, + PrecomputedFeatures, + SingleCutSampler, + SpecAugment, +) +from lhotse.dataset.input_strategies import AudioSamples # noqa F401 For AudioSamples +from lhotse.utils import fix_random_seed +from torch.utils.data import DataLoader + +from icefall.utils import str2bool + + +class _SeedWorkers: + def __init__(self, seed: int): + self.seed = seed + + def __call__(self, worker_id: int): + fix_random_seed(self.seed + worker_id) + + +class LibriSpeechAsrDataModule: + """ + DataModule for k2 ASR experiments. + It assumes there is always one train and valid dataloader, + but there can be multiple test dataloaders (e.g. LibriSpeech test-clean + and test-other). + + It contains all the common data pipeline modules used in ASR + experiments, e.g.: + - dynamic batch size, + - bucketing samplers, + - cut concatenation, + - augmentation, + - on-the-fly feature extraction + + This class should be derived for specific corpora used in ASR tasks. + """ + + def __init__(self, args: argparse.Namespace): + self.args = args + + @classmethod + def add_arguments(cls, parser: argparse.ArgumentParser): + group = parser.add_argument_group( + title="ASR data related options", + description="These options are used for the preparation of " + "PyTorch DataLoaders from Lhotse CutSet's -- they control the " + "effective batch sizes, sampling strategies, applied data " + "augmentations, etc.", + ) + group.add_argument( + "--full-libri", + type=str2bool, + default=False, + help="""Used only when --mini-libri is False.When enabled, + use 960h LibriSpeech. Otherwise, use 100h subset.""", + ) + group.add_argument( + "--mini-libri", + type=str2bool, + default=False, + help="True for mini librispeech", + ) + group.add_argument( + "--manifest-dir", + type=Path, + default=Path("data/ssl"), + help="Path to directory with train/valid/test cuts.", + ) + group.add_argument( + "--max-duration", + type=int, + default=200.0, + help="Maximum pooled recordings duration (seconds) in a " + "single batch. You can reduce it if it causes CUDA OOM.", + ) + group.add_argument( + "--bucketing-sampler", + type=str2bool, + default=True, + help="When enabled, the batches will come from buckets of " + "similar duration (saves padding frames).", + ) + group.add_argument( + "--num-buckets", + type=int, + default=30, + help="The number of buckets for the DynamicBucketingSampler" + "(you might want to increase it for larger datasets).", + ) + group.add_argument( + "--concatenate-cuts", + type=str2bool, + default=False, + help="When enabled, utterances (cuts) will be concatenated " + "to minimize the amount of padding.", + ) + group.add_argument( + "--duration-factor", + type=float, + default=1.0, + help="Determines the maximum duration of a concatenated cut " + "relative to the duration of the longest cut in a batch.", + ) + group.add_argument( + "--gap", + type=float, + default=1.0, + help="The amount of padding (in seconds) inserted between " + "concatenated cuts. This padding is filled with noise when " + "noise augmentation is used.", + ) + group.add_argument( + "--shuffle", + type=str2bool, + default=True, + help="When enabled (=default), the examples will be " + "shuffled for each epoch.", + ) + group.add_argument( + "--drop-last", + type=str2bool, + default=True, + help="Whether to drop last batch. Used by sampler.", + ) + group.add_argument( + "--return-cuts", + type=str2bool, + default=True, + help="When enabled, each batch will have the " + "field: batch['supervisions']['cut'] with the cuts that " + "were used to construct it.", + ) + + group.add_argument( + "--num-workers", + type=int, + default=2, + help="The number of training dataloader workers that " + "collect the batches.", + ) + + group.add_argument( + "--input-strategy", + type=str, + default="PrecomputedFeatures", + help="AudioSamples or PrecomputedFeatures", + ) + + group.add_argument( + "--train-manifest", + type=str, + default="librispeech_cuts_train-clean-100.jsonl.gz", + help="Train manifest file.", + ) + + def train_dataloaders( + self, + cuts_train: CutSet, + sampler_state_dict: Optional[Dict[str, Any]] = None, + ) -> DataLoader: + """ + Args: + cuts_train: + CutSet for training. + sampler_state_dict: + The state dict for the training sampler. + """ + transforms = [] + if self.args.concatenate_cuts: + logging.info( + f"Using cut concatenation with duration factor " + f"{self.args.duration_factor} and gap {self.args.gap}." + ) + # Cut concatenation should be the first transform in the list, + # so that if we e.g. mix noise in, it will fill the gaps between + # different utterances. + transforms = [ + CutConcatenate( + duration_factor=self.args.duration_factor, gap=self.args.gap + ) + ] + transforms + + logging.info("About to create train dataset") + train = K2SpeechRecognitionDataset( + input_strategy=eval(self.args.input_strategy)(), + cut_transforms=transforms, + return_cuts=self.args.return_cuts, + ) + + if self.args.bucketing_sampler: + logging.info("Using DynamicBucketingSampler.") + train_sampler = DynamicBucketingSampler( + cuts_train, + max_duration=self.args.max_duration, + shuffle=self.args.shuffle, + num_buckets=self.args.num_buckets, + drop_last=self.args.drop_last, + ) + else: + logging.info("Using SingleCutSampler.") + train_sampler = SingleCutSampler( + cuts_train, + max_duration=self.args.max_duration, + shuffle=self.args.shuffle, + ) + logging.info("About to create train dataloader") + + if sampler_state_dict is not None: + logging.info("Loading sampler state dict") + train_sampler.load_state_dict(sampler_state_dict) + + # 'seed' is derived from the current random state, which will have + # previously been set in the main process. + seed = torch.randint(0, 100000, ()).item() + worker_init_fn = _SeedWorkers(seed) + + train_dl = DataLoader( + train, + sampler=train_sampler, + batch_size=None, + num_workers=self.args.num_workers, + persistent_workers=False, + worker_init_fn=worker_init_fn, + ) + + return train_dl + + def valid_dataloaders(self, cuts_valid: CutSet) -> DataLoader: + transforms = [] + if self.args.concatenate_cuts: + transforms = [ + CutConcatenate( + duration_factor=self.args.duration_factor, gap=self.args.gap + ) + ] + transforms + + logging.info("About to create dev dataset") + + validate = K2SpeechRecognitionDataset( + cut_transforms=transforms, + return_cuts=self.args.return_cuts, + ) + + valid_sampler = DynamicBucketingSampler( + cuts_valid, + max_duration=self.args.max_duration, + shuffle=False, + ) + + logging.info("About to create dev dataloader") + valid_dl = DataLoader( + validate, + sampler=valid_sampler, + batch_size=None, + num_workers=2, + persistent_workers=False, + ) + + return valid_dl + + def test_dataloaders(self, cuts: CutSet) -> DataLoader: + logging.debug("About to create test dataset") + test = K2SpeechRecognitionDataset( + input_strategy=eval(self.args.input_strategy)(), + return_cuts=self.args.return_cuts, + ) + sampler = DynamicBucketingSampler( + cuts, + max_duration=self.args.max_duration, + shuffle=False, + ) + logging.debug("About to create test dataloader") + test_dl = DataLoader( + test, + batch_size=None, + sampler=sampler, + num_workers=self.args.num_workers, + ) + return test_dl + + @lru_cache() + def train_clean_5_cuts(self) -> CutSet: + logging.info("mini_librispeech: About to get train-clean-5 cuts") + return load_manifest_lazy( + self.args.manifest_dir / "librispeech_cuts_train-clean-5.jsonl.gz" + ) + + @lru_cache() + def train_clean_100_cuts(self) -> CutSet: + logging.info("About to get train-clean-100 cuts") + return load_manifest_lazy(self.args.manifest_dir / self.args.train_manifest) + + @lru_cache() + def train_all_shuf_cuts(self) -> CutSet: + logging.info( + "About to get the shuffled train-clean-100, \ + train-clean-360 and train-other-500 cuts" + ) + return load_manifest_lazy( + self.args.manifest_dir / "librispeech_cuts_train-all-shuf.jsonl.gz" + ) + + @lru_cache() + def dev_clean_2_cuts(self) -> CutSet: + logging.info("mini_librispeech: About to get dev-clean-2 cuts") + return load_manifest_lazy( + self.args.manifest_dir / "librispeech_cuts_dev-clean-2.jsonl.gz" + ) + + @lru_cache() + def dev_clean_cuts(self) -> CutSet: + logging.info("About to get dev-clean cuts") + return load_manifest_lazy( + self.args.manifest_dir / "librispeech_cuts_dev-clean.jsonl.gz" + ) + + @lru_cache() + def dev_other_cuts(self) -> CutSet: + logging.info("About to get dev-other cuts") + return load_manifest_lazy( + self.args.manifest_dir / "librispeech_cuts_dev-other.jsonl.gz" + ) + + @lru_cache() + def test_clean_cuts(self) -> CutSet: + logging.info("About to get test-clean cuts") + return load_manifest_lazy( + self.args.manifest_dir / "librispeech_cuts_test-clean.jsonl.gz" + ) + + @lru_cache() + def test_other_cuts(self) -> CutSet: + logging.info("About to get test-other cuts") + return load_manifest_lazy( + self.args.manifest_dir / "librispeech_cuts_test-other.jsonl.gz" + ) diff --git a/egs/librispeech/WSASR/conformer_ctc2/attention.py b/egs/librispeech/WSASR/conformer_ctc2/attention.py new file mode 120000 index 000000000..e808a6f20 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/attention.py @@ -0,0 +1 @@ +../../ASR/conformer_ctc2/attention.py \ No newline at end of file diff --git a/egs/librispeech/WSASR/conformer_ctc2/conformer.py b/egs/librispeech/WSASR/conformer_ctc2/conformer.py new file mode 100644 index 000000000..db4821d37 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/conformer.py @@ -0,0 +1,949 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 University of Chinese Academy of Sciences (author: Han Zhu) +# 2022 Xiaomi Corp. (author: Quandong Wang) +# 2023 Johns Hopkins University (author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 math +import warnings +from typing import Optional, Tuple + +import torch +from scaling import ( + ActivationBalancer, + BasicNorm, + DoubleSwish, + ScaledConv1d, + ScaledLinear, +) +from subsampling import Conv2dSubsampling, Conv2dSubsampling2 +from torch import Tensor, nn +from transformer import Supervisions, Transformer, encoder_padding_mask + + +class Conformer(Transformer): + """ + Args: + num_features (int): Number of input features + num_classes (int): Number of output classes + subsampling_factor (int): subsampling factor of encoder (the convolution layers before transformers) + d_model (int): attention dimension, also the output dimension + nhead (int): number of head + dim_feedforward (int): feedforward dimention + num_encoder_layers (int): number of encoder layers + num_decoder_layers (int): number of decoder layers + dropout (float): dropout rate + layer_dropout (float): layer-dropout rate. + cnn_module_kernel (int): Kernel size of convolution module + vgg_frontend (bool): whether to use vgg frontend. + """ + + def __init__( + self, + num_features: int, + num_classes: int, + subsampling_factor: int = 2, + d_model: int = 256, + nhead: int = 4, + dim_feedforward: int = 2048, + num_encoder_layers: int = 12, + num_decoder_layers: int = 6, + dropout: float = 0.2, + layer_dropout: float = 0.075, + cnn_module_kernel: int = 31, + ) -> None: + super(Conformer, self).__init__( + num_features=num_features, + num_classes=num_classes, + subsampling_factor=subsampling_factor, + d_model=d_model, + nhead=nhead, + dim_feedforward=dim_feedforward, + num_encoder_layers=num_encoder_layers, + num_decoder_layers=num_decoder_layers, + dropout=dropout, + layer_dropout=layer_dropout, + ) + + self.num_features = num_features + self.subsampling_factor = subsampling_factor + if subsampling_factor != 4 and subsampling_factor != 2: + raise NotImplementedError("Support only 'subsampling_factor=4 or 2'.") + + # self.encoder_embed converts the input of shape (N, T, num_features) + # to the shape (N, T//subsampling_factor, d_model). + # That is, it does two things simultaneously: + # (1) subsampling: T -> T//subsampling_factor + # (2) embedding: num_features -> d_model + if self.subsampling_factor == 4: + self.encoder_embed = Conv2dSubsampling(num_features, d_model) + elif self.subsampling_factor == 2: + self.encoder_embed = Conv2dSubsampling2(num_features, d_model) + + self.encoder_pos = RelPositionalEncoding(d_model, dropout) + + encoder_layer = ConformerEncoderLayer( + d_model, + nhead, + dim_feedforward, + dropout, + layer_dropout, + cnn_module_kernel, + ) + self.encoder = ConformerEncoder(encoder_layer, num_encoder_layers) + + def run_encoder( + self, + x: torch.Tensor, + supervisions: Optional[Supervisions] = None, + warmup: float = 1.0, + ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: + """ + Args: + x: + The input tensor. Its shape is (batch_size, seq_len, feature_dim). + supervisions: + Supervision in lhotse format. + See https://github.com/lhotse-speech/lhotse/blob/master/lhotse/dataset/speech_recognition.py#L32 # noqa + CAUTION: It contains length information, i.e., start and number of + frames, before subsampling + It is read directly from the batch, without any sorting. It is used + to compute encoder padding mask, which is used as memory key padding + mask for the decoder. + warmup: + A floating point value that gradually increases from 0 throughout + training; when it is >= 1.0 we are "fully warmed up". It is used + to turn modules on sequentially. + Returns: + Tensor: Predictor tensor of dimension (input_length, batch_size, d_model). + Tensor: Mask tensor of dimension (batch_size, input_length) + """ + x = self.encoder_embed(x) + x, pos_emb = self.encoder_pos(x) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + mask = encoder_padding_mask(x.size(0), self.subsampling_factor, supervisions) + if mask is not None: + mask = mask.to(x.device) + + # Caution: We assume the subsampling factor is 4! + + x = self.encoder( + x, pos_emb, src_key_padding_mask=mask, warmup=warmup + ) # (T, N, C) + + # x = x.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + + # return x, lengths + return x, mask + + +class ConformerEncoderLayer(nn.Module): + """ + ConformerEncoderLayer is made up of self-attn, feedforward and convolution networks. + See: "Conformer: Convolution-augmented Transformer for Speech Recognition" + + Args: + d_model: the number of expected features in the input (required). + nhead: the number of heads in the multiheadattention models (required). + dim_feedforward: the dimension of the feedforward network model (default=2048). + dropout: the dropout value (default=0.1). + cnn_module_kernel (int): Kernel size of convolution module. + + Examples:: + >>> encoder_layer = ConformerEncoderLayer(d_model=512, nhead=8) + >>> src = torch.rand(10, 32, 512) + >>> pos_emb = torch.rand(32, 19, 512) + >>> out = encoder_layer(src, pos_emb) + """ + + def __init__( + self, + d_model: int, + nhead: int, + dim_feedforward: int = 2048, + dropout: float = 0.1, + layer_dropout: float = 0.075, + cnn_module_kernel: int = 31, + ) -> None: + super(ConformerEncoderLayer, self).__init__() + + self.layer_dropout = layer_dropout + + self.d_model = d_model + + self.self_attn = RelPositionMultiheadAttention(d_model, nhead, dropout=0.0) + + self.feed_forward = nn.Sequential( + ScaledLinear(d_model, dim_feedforward), + ActivationBalancer(channel_dim=-1), + DoubleSwish(), + nn.Dropout(dropout), + ScaledLinear(dim_feedforward, d_model, initial_scale=0.25), + ) + + self.feed_forward_macaron = nn.Sequential( + ScaledLinear(d_model, dim_feedforward), + ActivationBalancer(channel_dim=-1), + DoubleSwish(), + nn.Dropout(dropout), + ScaledLinear(dim_feedforward, d_model, initial_scale=0.25), + ) + + self.conv_module = ConvolutionModule(d_model, cnn_module_kernel) + + self.norm_final = BasicNorm(d_model) + + # try to ensure the output is close to zero-mean (or at least, zero-median). + self.balancer = ActivationBalancer( + channel_dim=-1, min_positive=0.45, max_positive=0.55, max_abs=6.0 + ) + + self.dropout = nn.Dropout(dropout) + + def forward( + self, + src: Tensor, + pos_emb: Tensor, + src_mask: Optional[Tensor] = None, + src_key_padding_mask: Optional[Tensor] = None, + warmup: float = 1.0, + ) -> Tensor: + """ + Pass the input through the encoder layer. + + Args: + src: the sequence to the encoder layer (required). + pos_emb: Positional embedding tensor (required). + src_mask: the mask for the src sequence (optional). + src_key_padding_mask: the mask for the src keys per batch (optional). + warmup: controls selective bypass of of layers; if < 1.0, we will + bypass layers more frequently. + + Shape: + src: (S, N, E). + pos_emb: (N, 2*S-1, E) + src_mask: (S, S). + src_key_padding_mask: (N, S). + S is the source sequence length, N is the batch size, E is the feature number + """ + src_orig = src + + warmup_scale = min(0.1 + warmup, 1.0) + # alpha = 1.0 means fully use this encoder layer, 0.0 would mean + # completely bypass it. + if self.training: + alpha = ( + warmup_scale + if torch.rand(()).item() <= (1.0 - self.layer_dropout) + else 0.1 + ) + else: + alpha = 1.0 + + # macaron style feed forward module + src = src + self.dropout(self.feed_forward_macaron(src)) + + # multi-headed self-attention module + src_att = self.self_attn( + src, + src, + src, + pos_emb=pos_emb, + attn_mask=src_mask, + key_padding_mask=src_key_padding_mask, + )[0] + src = src + self.dropout(src_att) + + # convolution module + src = src + self.dropout( + self.conv_module(src, src_key_padding_mask=src_key_padding_mask) + ) + + # feed forward module + src = src + self.dropout(self.feed_forward(src)) + + src = self.norm_final(self.balancer(src)) + + if alpha != 1.0: + src = alpha * src + (1 - alpha) * src_orig + + return src + + +class ConformerEncoder(nn.Module): + r"""ConformerEncoder is a stack of N encoder layers + + Args: + encoder_layer: an instance of the ConformerEncoderLayer() class (required). + num_layers: the number of sub-encoder-layers in the encoder (required). + + Examples:: + >>> encoder_layer = ConformerEncoderLayer(d_model=512, nhead=8) + >>> conformer_encoder = ConformerEncoder(encoder_layer, num_layers=6) + >>> src = torch.rand(10, 32, 512) + >>> pos_emb = torch.rand(32, 19, 512) + >>> out = conformer_encoder(src, pos_emb) + """ + + def __init__(self, encoder_layer: nn.Module, num_layers: int) -> None: + super().__init__() + self.layers = nn.ModuleList( + [copy.deepcopy(encoder_layer) for i in range(num_layers)] + ) + self.num_layers = num_layers + + def forward( + self, + src: Tensor, + pos_emb: Tensor, + mask: Optional[Tensor] = None, + src_key_padding_mask: Optional[Tensor] = None, + warmup: float = 1.0, + ) -> Tensor: + r"""Pass the input through the encoder layers in turn. + + Args: + src: the sequence to the encoder (required). + pos_emb: Positional embedding tensor (required). + mask: the mask for the src sequence (optional). + src_key_padding_mask: the mask for the src keys per batch (optional). + + Shape: + src: (S, N, E). + pos_emb: (N, 2*S-1, E) + mask: (S, S). + src_key_padding_mask: (N, S). + S is the source sequence length, T is the target sequence length, N is the batch size, E is the feature number + + """ + output = src + + for i, mod in enumerate(self.layers): + output = mod( + output, + pos_emb, + src_mask=mask, + src_key_padding_mask=src_key_padding_mask, + warmup=warmup, + ) + + return output + + +class RelPositionalEncoding(torch.nn.Module): + """Relative positional encoding module. + + See : Appendix B in "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context" + Modified from https://github.com/espnet/espnet/blob/master/espnet/nets/pytorch_backend/transformer/embedding.py + + Args: + d_model: Embedding dimension. + dropout_rate: Dropout rate. + max_len: Maximum input length. + + """ + + def __init__(self, d_model: int, dropout_rate: float, max_len: int = 5000) -> None: + """Construct an PositionalEncoding object.""" + super(RelPositionalEncoding, self).__init__() + self.d_model = d_model + self.dropout = torch.nn.Dropout(p=dropout_rate) + self.pe = None + self.extend_pe(torch.tensor(0.0).expand(1, max_len)) + + def extend_pe(self, x: Tensor) -> None: + """Reset the positional encodings.""" + if self.pe is not None: + # self.pe contains both positive and negative parts + # the length of self.pe is 2 * input_len - 1 + if self.pe.size(1) >= x.size(1) * 2 - 1: + # Note: TorchScript doesn't implement operator== for torch.Device + if self.pe.dtype != x.dtype or str(self.pe.device) != str(x.device): + self.pe = self.pe.to(dtype=x.dtype, device=x.device) + return + # Suppose `i` means to the position of query vecotr and `j` means the + # position of key vector. We use position relative positions when keys + # are to the left (i>j) and negative relative positions otherwise (i Tuple[Tensor, Tensor]: + """Add positional encoding. + + Args: + x (torch.Tensor): Input tensor (batch, time, `*`). + + Returns: + torch.Tensor: Encoded tensor (batch, time, `*`). + torch.Tensor: Encoded tensor (batch, 2*time-1, `*`). + + """ + self.extend_pe(x) + pos_emb = self.pe[ + :, + self.pe.size(1) // 2 + - x.size(1) + + 1 : self.pe.size(1) // 2 # noqa E203 + + x.size(1), + ] + return self.dropout(x), self.dropout(pos_emb) + + +class RelPositionMultiheadAttention(nn.Module): + r"""Multi-Head Attention layer with relative position encoding + + See reference: "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context" + + Args: + embed_dim: total dimension of the model. + num_heads: parallel attention heads. + dropout: a Dropout layer on attn_output_weights. Default: 0.0. + + Examples:: + + >>> rel_pos_multihead_attn = RelPositionMultiheadAttention(embed_dim, num_heads) + >>> attn_output, attn_output_weights = multihead_attn(query, key, value, pos_emb) + """ + + def __init__( + self, + embed_dim: int, + num_heads: int, + dropout: float = 0.0, + ) -> None: + super(RelPositionMultiheadAttention, self).__init__() + self.embed_dim = embed_dim + self.num_heads = num_heads + self.dropout = dropout + self.head_dim = embed_dim // num_heads + assert ( + self.head_dim * num_heads == self.embed_dim + ), "embed_dim must be divisible by num_heads" + + self.in_proj = ScaledLinear(embed_dim, 3 * embed_dim, bias=True) + self.out_proj = ScaledLinear( + embed_dim, embed_dim, bias=True, initial_scale=0.25 + ) + + # linear transformation for positional encoding. + self.linear_pos = ScaledLinear(embed_dim, embed_dim, bias=False) + # these two learnable bias are used in matrix c and matrix d + # as described in "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context" Section 3.3 + self.pos_bias_u = nn.Parameter(torch.Tensor(num_heads, self.head_dim)) + self.pos_bias_v = nn.Parameter(torch.Tensor(num_heads, self.head_dim)) + self.pos_bias_u_scale = nn.Parameter(torch.zeros(()).detach()) + self.pos_bias_v_scale = nn.Parameter(torch.zeros(()).detach()) + self._reset_parameters() + + def _pos_bias_u(self): + return self.pos_bias_u * self.pos_bias_u_scale.exp() + + def _pos_bias_v(self): + return self.pos_bias_v * self.pos_bias_v_scale.exp() + + def _reset_parameters(self) -> None: + nn.init.normal_(self.pos_bias_u, std=0.01) + nn.init.normal_(self.pos_bias_v, std=0.01) + + def forward( + self, + query: Tensor, + key: Tensor, + value: Tensor, + pos_emb: Tensor, + key_padding_mask: Optional[Tensor] = None, + need_weights: bool = True, + attn_mask: Optional[Tensor] = None, + ) -> Tuple[Tensor, Optional[Tensor]]: + r""" + Args: + query, key, value: map a query and a set of key-value pairs to an output. + pos_emb: Positional embedding tensor + key_padding_mask: if provided, specified padding elements in the key will + be ignored by the attention. When given a binary mask and a value is True, + the corresponding value on the attention layer will be ignored. When given + a byte mask and a value is non-zero, the corresponding value on the attention + layer will be ignored + need_weights: output attn_output_weights. + attn_mask: 2D or 3D mask that prevents attention to certain positions. A 2D mask will be broadcasted for all + the batches while a 3D mask allows to specify a different mask for the entries of each batch. + + Shape: + - Inputs: + - query: :math:`(L, N, E)` where L is the target sequence length, N is the batch size, E is + the embedding dimension. + - key: :math:`(S, N, E)`, where S is the source sequence length, N is the batch size, E is + the embedding dimension. + - value: :math:`(S, N, E)` where S is the source sequence length, N is the batch size, E is + the embedding dimension. + - pos_emb: :math:`(N, 2*L-1, E)` where L is the target sequence length, N is the batch size, E is + the embedding dimension. + - key_padding_mask: :math:`(N, S)` where N is the batch size, S is the source sequence length. + If a ByteTensor is provided, the non-zero positions will be ignored while the position + with the zero positions will be unchanged. If a BoolTensor is provided, the positions with the + value of ``True`` will be ignored while the position with the value of ``False`` will be unchanged. + - attn_mask: 2D mask :math:`(L, S)` where L is the target sequence length, S is the source sequence length. + 3D mask :math:`(N*num_heads, L, S)` where N is the batch size, L is the target sequence length, + S is the source sequence length. attn_mask ensure that position i is allowed to attend the unmasked + positions. If a ByteTensor is provided, the non-zero positions are not allowed to attend + while the zero positions will be unchanged. If a BoolTensor is provided, positions with ``True`` + is not allowed to attend while ``False`` values will be unchanged. If a FloatTensor + is provided, it will be added to the attention weight. + + - Outputs: + - attn_output: :math:`(L, N, E)` where L is the target sequence length, N is the batch size, + E is the embedding dimension. + - attn_output_weights: :math:`(N, L, S)` where N is the batch size, + L is the target sequence length, S is the source sequence length. + """ + return self.multi_head_attention_forward( + query, + key, + value, + pos_emb, + self.embed_dim, + self.num_heads, + self.in_proj.get_weight(), + self.in_proj.get_bias(), + self.dropout, + self.out_proj.get_weight(), + self.out_proj.get_bias(), + training=self.training, + key_padding_mask=key_padding_mask, + need_weights=need_weights, + attn_mask=attn_mask, + ) + + def rel_shift(self, x: Tensor) -> Tensor: + """Compute relative positional encoding. + + Args: + x: Input tensor (batch, head, time1, 2*time1-1). + time1 means the length of query vector. + + Returns: + Tensor: tensor of shape (batch, head, time1, time2) + (note: time2 has the same value as time1, but it is for + the key, while time1 is for the query). + """ + (batch_size, num_heads, time1, n) = x.shape + assert n == 2 * time1 - 1 + # Note: TorchScript requires explicit arg for stride() + batch_stride = x.stride(0) + head_stride = x.stride(1) + time1_stride = x.stride(2) + n_stride = x.stride(3) + return x.as_strided( + (batch_size, num_heads, time1, time1), + (batch_stride, head_stride, time1_stride - n_stride, n_stride), + storage_offset=n_stride * (time1 - 1), + ) + + def multi_head_attention_forward( + self, + query: Tensor, + key: Tensor, + value: Tensor, + pos_emb: Tensor, + embed_dim_to_check: int, + num_heads: int, + in_proj_weight: Tensor, + in_proj_bias: Tensor, + dropout_p: float, + out_proj_weight: Tensor, + out_proj_bias: Tensor, + training: bool = True, + key_padding_mask: Optional[Tensor] = None, + need_weights: bool = True, + attn_mask: Optional[Tensor] = None, + ) -> Tuple[Tensor, Optional[Tensor]]: + r""" + Args: + query, key, value: map a query and a set of key-value pairs to an output. + pos_emb: Positional embedding tensor + embed_dim_to_check: total dimension of the model. + num_heads: parallel attention heads. + in_proj_weight, in_proj_bias: input projection weight and bias. + dropout_p: probability of an element to be zeroed. + out_proj_weight, out_proj_bias: the output projection weight and bias. + training: apply dropout if is ``True``. + key_padding_mask: if provided, specified padding elements in the key will + be ignored by the attention. This is an binary mask. When the value is True, + the corresponding value on the attention layer will be filled with -inf. + need_weights: output attn_output_weights. + attn_mask: 2D or 3D mask that prevents attention to certain positions. A 2D mask will be broadcasted for all + the batches while a 3D mask allows to specify a different mask for the entries of each batch. + + Shape: + Inputs: + - query: :math:`(L, N, E)` where L is the target sequence length, N is the batch size, E is + the embedding dimension. + - key: :math:`(S, N, E)`, where S is the source sequence length, N is the batch size, E is + the embedding dimension. + - value: :math:`(S, N, E)` where S is the source sequence length, N is the batch size, E is + the embedding dimension. + - pos_emb: :math:`(N, 2*L-1, E)` or :math:`(1, 2*L-1, E)` where L is the target sequence + length, N is the batch size, E is the embedding dimension. + - key_padding_mask: :math:`(N, S)` where N is the batch size, S is the source sequence length. + If a ByteTensor is provided, the non-zero positions will be ignored while the zero positions + will be unchanged. If a BoolTensor is provided, the positions with the + value of ``True`` will be ignored while the position with the value of ``False`` will be unchanged. + - attn_mask: 2D mask :math:`(L, S)` where L is the target sequence length, S is the source sequence length. + 3D mask :math:`(N*num_heads, L, S)` where N is the batch size, L is the target sequence length, + S is the source sequence length. attn_mask ensures that position i is allowed to attend the unmasked + positions. If a ByteTensor is provided, the non-zero positions are not allowed to attend + while the zero positions will be unchanged. If a BoolTensor is provided, positions with ``True`` + are not allowed to attend while ``False`` values will be unchanged. If a FloatTensor + is provided, it will be added to the attention weight. + + Outputs: + - attn_output: :math:`(L, N, E)` where L is the target sequence length, N is the batch size, + E is the embedding dimension. + - attn_output_weights: :math:`(N, L, S)` where N is the batch size, + L is the target sequence length, S is the source sequence length. + """ + + tgt_len, bsz, embed_dim = query.size() + assert embed_dim == embed_dim_to_check + assert key.size(0) == value.size(0) and key.size(1) == value.size(1) + + head_dim = embed_dim // num_heads + assert ( + head_dim * num_heads == embed_dim + ), "embed_dim must be divisible by num_heads" + + scaling = float(head_dim) ** -0.5 + + if torch.equal(query, key) and torch.equal(key, value): + # self-attention + q, k, v = nn.functional.linear(query, in_proj_weight, in_proj_bias).chunk( + 3, dim=-1 + ) + + elif torch.equal(key, value): + # encoder-decoder attention + # This is inline in_proj function with in_proj_weight and in_proj_bias + _b = in_proj_bias + _start = 0 + _end = embed_dim + _w = in_proj_weight[_start:_end, :] + if _b is not None: + _b = _b[_start:_end] + q = nn.functional.linear(query, _w, _b) + + # This is inline in_proj function with in_proj_weight and in_proj_bias + _b = in_proj_bias + _start = embed_dim + _end = None + _w = in_proj_weight[_start:, :] + if _b is not None: + _b = _b[_start:] + k, v = nn.functional.linear(key, _w, _b).chunk(2, dim=-1) + + else: + # This is inline in_proj function with in_proj_weight and in_proj_bias + _b = in_proj_bias + _start = 0 + _end = embed_dim + _w = in_proj_weight[_start:_end, :] + if _b is not None: + _b = _b[_start:_end] + q = nn.functional.linear(query, _w, _b) + + # This is inline in_proj function with in_proj_weight and in_proj_bias + _b = in_proj_bias + _start = embed_dim + _end = embed_dim * 2 + _w = in_proj_weight[_start:_end, :] + if _b is not None: + _b = _b[_start:_end] + k = nn.functional.linear(key, _w, _b) + + # This is inline in_proj function with in_proj_weight and in_proj_bias + _b = in_proj_bias + _start = embed_dim * 2 + _end = None + _w = in_proj_weight[_start:, :] + if _b is not None: + _b = _b[_start:] + v = nn.functional.linear(value, _w, _b) + + if attn_mask is not None: + assert ( + attn_mask.dtype == torch.float32 + or attn_mask.dtype == torch.float64 + or attn_mask.dtype == torch.float16 + or attn_mask.dtype == torch.uint8 + or attn_mask.dtype == torch.bool + ), "Only float, byte, and bool types are supported for attn_mask, not {}".format( + attn_mask.dtype + ) + if attn_mask.dtype == torch.uint8: + warnings.warn( + "Byte tensor for attn_mask is deprecated. Use bool tensor instead." + ) + attn_mask = attn_mask.to(torch.bool) + + if attn_mask.dim() == 2: + attn_mask = attn_mask.unsqueeze(0) + if list(attn_mask.size()) != [1, query.size(0), key.size(0)]: + raise RuntimeError("The size of the 2D attn_mask is not correct.") + elif attn_mask.dim() == 3: + if list(attn_mask.size()) != [ + bsz * num_heads, + query.size(0), + key.size(0), + ]: + raise RuntimeError("The size of the 3D attn_mask is not correct.") + else: + raise RuntimeError( + "attn_mask's dimension {} is not supported".format(attn_mask.dim()) + ) + # attn_mask's dim is 3 now. + + # convert ByteTensor key_padding_mask to bool + if key_padding_mask is not None and key_padding_mask.dtype == torch.uint8: + warnings.warn( + "Byte tensor for key_padding_mask is deprecated. Use bool tensor instead." + ) + key_padding_mask = key_padding_mask.to(torch.bool) + + q = (q * scaling).contiguous().view(tgt_len, bsz, num_heads, head_dim) + k = k.contiguous().view(-1, bsz, num_heads, head_dim) + v = v.contiguous().view(-1, bsz * num_heads, head_dim).transpose(0, 1) + + src_len = k.size(0) + + if key_padding_mask is not None: + assert key_padding_mask.size(0) == bsz, "{} == {}".format( + key_padding_mask.size(0), bsz + ) + assert key_padding_mask.size(1) == src_len, "{} == {}".format( + key_padding_mask.size(1), src_len + ) + + q = q.transpose(0, 1) # (batch, time1, head, d_k) + + pos_emb_bsz = pos_emb.size(0) + assert pos_emb_bsz in (1, bsz) # actually it is 1 + p = self.linear_pos(pos_emb).view(pos_emb_bsz, -1, num_heads, head_dim) + p = p.transpose(1, 2) # (batch, head, 2*time1-1, d_k) + + q_with_bias_u = (q + self._pos_bias_u()).transpose( + 1, 2 + ) # (batch, head, time1, d_k) + + q_with_bias_v = (q + self._pos_bias_v()).transpose( + 1, 2 + ) # (batch, head, time1, d_k) + + # compute attention score + # first compute matrix a and matrix c + # as described in "Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context" Section 3.3 + k = k.permute(1, 2, 3, 0) # (batch, head, d_k, time2) + matrix_ac = torch.matmul(q_with_bias_u, k) # (batch, head, time1, time2) + + # compute matrix b and matrix d + matrix_bd = torch.matmul( + q_with_bias_v, p.transpose(-2, -1) + ) # (batch, head, time1, 2*time1-1) + matrix_bd = self.rel_shift(matrix_bd) + + attn_output_weights = matrix_ac + matrix_bd # (batch, head, time1, time2) + + attn_output_weights = attn_output_weights.view(bsz * num_heads, tgt_len, -1) + + assert list(attn_output_weights.size()) == [ + bsz * num_heads, + tgt_len, + src_len, + ] + + if attn_mask is not None: + if attn_mask.dtype == torch.bool: + attn_output_weights.masked_fill_(attn_mask, float("-inf")) + else: + attn_output_weights += attn_mask + + if key_padding_mask is not None: + attn_output_weights = attn_output_weights.view( + bsz, num_heads, tgt_len, src_len + ) + attn_output_weights = attn_output_weights.masked_fill( + key_padding_mask.unsqueeze(1).unsqueeze(2), + float("-inf"), + ) + attn_output_weights = attn_output_weights.view( + bsz * num_heads, tgt_len, src_len + ) + + attn_output_weights = nn.functional.softmax(attn_output_weights, dim=-1) + attn_output_weights = nn.functional.dropout( + attn_output_weights, p=dropout_p, training=training + ) + + attn_output = torch.bmm(attn_output_weights, v) + assert list(attn_output.size()) == [bsz * num_heads, tgt_len, head_dim] + attn_output = ( + attn_output.transpose(0, 1).contiguous().view(tgt_len, bsz, embed_dim) + ) + attn_output = nn.functional.linear(attn_output, out_proj_weight, out_proj_bias) + + if need_weights: + # average attention weights over heads + attn_output_weights = attn_output_weights.view( + bsz, num_heads, tgt_len, src_len + ) + return attn_output, attn_output_weights.sum(dim=1) / num_heads + else: + return attn_output, None + + +class ConvolutionModule(nn.Module): + """ConvolutionModule in Conformer model. + Modified from https://github.com/espnet/espnet/blob/master/espnet/nets/pytorch_backend/conformer/convolution.py + + Args: + channels (int): The number of channels of conv layers. + kernel_size (int): Kernerl size of conv layers. + bias (bool): Whether to use bias in conv layers (default=True). + + """ + + def __init__(self, channels: int, kernel_size: int, bias: bool = True) -> None: + """Construct an ConvolutionModule object.""" + super(ConvolutionModule, self).__init__() + # kernerl_size should be a odd number for 'SAME' padding + assert (kernel_size - 1) % 2 == 0 + + self.pointwise_conv1 = ScaledConv1d( + channels, + 2 * channels, + kernel_size=1, + stride=1, + padding=0, + bias=bias, + ) + + # after pointwise_conv1 we put x through a gated linear unit (nn.functional.glu). + # For most layers the normal rms value of channels of x seems to be in the range 1 to 4, + # but sometimes, for some reason, for layer 0 the rms ends up being very large, + # between 50 and 100 for different channels. This will cause very peaky and + # sparse derivatives for the sigmoid gating function, which will tend to make + # the loss function not learn effectively. (for most layers the average absolute values + # are in the range 0.5..9.0, and the average p(x>0), i.e. positive proportion, + # at the output of pointwise_conv1.output is around 0.35 to 0.45 for different + # layers, which likely breaks down as 0.5 for the "linear" half and + # 0.2 to 0.3 for the part that goes into the sigmoid. The idea is that if we + # constrain the rms values to a reasonable range via a constraint of max_abs=10.0, + # it will be in a better position to start learning something, i.e. to latch onto + # the correct range. + self.deriv_balancer1 = ActivationBalancer( + channel_dim=1, max_abs=10.0, min_positive=0.05, max_positive=1.0 + ) + + self.depthwise_conv = ScaledConv1d( + channels, + channels, + kernel_size, + stride=1, + padding=(kernel_size - 1) // 2, + groups=channels, + bias=bias, + ) + + self.deriv_balancer2 = ActivationBalancer( + channel_dim=1, min_positive=0.05, max_positive=1.0 + ) + + self.activation = DoubleSwish() + + self.pointwise_conv2 = ScaledConv1d( + channels, + channels, + kernel_size=1, + stride=1, + padding=0, + bias=bias, + initial_scale=0.25, + ) + + def forward( + self, + x: Tensor, + src_key_padding_mask: Optional[Tensor] = None, + ) -> Tensor: + """Compute convolution module. + + Args: + x: Input tensor (#time, batch, channels). + src_key_padding_mask: the mask for the src keys per batch (optional). + + Returns: + Tensor: Output tensor (#time, batch, channels). + + """ + # exchange the temporal dimension and the feature dimension + x = x.permute(1, 2, 0) # (#batch, channels, time). + + # GLU mechanism + x = self.pointwise_conv1(x) # (batch, 2*channels, time) + + x = self.deriv_balancer1(x) + x = nn.functional.glu(x, dim=1) # (batch, channels, time) + + # 1D Depthwise Conv + if src_key_padding_mask is not None: + x.masked_fill_(src_key_padding_mask.unsqueeze(1).expand_as(x), 0.0) + x = self.depthwise_conv(x) + + x = self.deriv_balancer2(x) + x = self.activation(x) + + x = self.pointwise_conv2(x) # (batch, channel, time) + + return x.permute(2, 0, 1) + + +if __name__ == "__main__": + feature_dim = 50 + c = Conformer(num_features=feature_dim, d_model=128, nhead=4) + batch_size = 5 + seq_len = 20 + # Just make sure the forward pass runs. + f = c( + torch.randn(batch_size, seq_len, feature_dim), + torch.full((batch_size,), seq_len, dtype=torch.int64), + warmup=0.5, + ) diff --git a/egs/librispeech/WSASR/conformer_ctc2/decode.py b/egs/librispeech/WSASR/conformer_ctc2/decode.py new file mode 100755 index 000000000..3fa045533 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/decode.py @@ -0,0 +1,718 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corporation (Author: Liyong Guo, +# Fangjun Kuang, +# Quandong Wang) +# 2023 Johns Hopkins University (Author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 +from collections import defaultdict +from pathlib import Path +from typing import Dict, List, Optional, Tuple + +import k2 +import sentencepiece as spm +import torch +import torch.nn as nn +from asr_datamodule import LibriSpeechAsrDataModule +from conformer import Conformer + +from icefall.checkpoint import ( + average_checkpoints, + average_checkpoints_with_averaged_model, + find_checkpoints, + load_checkpoint, +) +from icefall.decode import get_lattice, one_best_decoding +from icefall.env import get_env_info +from icefall.lexicon import Lexicon +from icefall.otc_graph_compiler import OtcTrainingGraphCompiler +from icefall.utils import ( + AttributeDict, + get_texts, + load_averaged_model, + setup_logger, + store_transcripts, + str2bool, + write_error_stats, +) + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--otc-token", + type=str, + default="", + help="OTC token", + ) + + parser.add_argument( + "--blank-bias", + type=float, + default=0, + help="bias (log-prob) added to blank token during decoding", + ) + + parser.add_argument( + "--epoch", + type=int, + default=20, + help="""It specifies the checkpoint to use for decoding. + Note: Epoch counts from 1. + You can specify --avg to use more checkpoints for model averaging.""", + ) + + parser.add_argument( + "--iter", + type=int, + default=0, + help="""If positive, --epoch is ignored and it + will use the checkpoint exp_dir/checkpoint-iter.pt. + You can specify --avg to use more checkpoints for model averaging. + """, + ) + + parser.add_argument( + "--avg", + type=int, + default=1, + help="Number of checkpoints to average. Automatically select " + "consecutive checkpoints before the checkpoint specified by " + "'--epoch' and '--iter'", + ) + + parser.add_argument( + "--method", + type=str, + default="ctc-greedy-search", + help="""Decoding method. + Supported values are: + - (0) ctc-decoding. Use CTC decoding. It uses a sentence piece + model, i.e., lang_dir/bpe.model, to convert word pieces to words. + It needs neither a lexicon nor an n-gram LM. + - (1) ctc-greedy-search. It only use CTC output and a sentence piece + model for decoding. It produces the same results with ctc-decoding. + - (2) 1best. Extract the best path from the decoding lattice as the + decoding result. + """, + ) + + parser.add_argument( + "--use-averaged-model", + type=str2bool, + default=True, + help="Whether to load averaged model. Currently it only supports " + "using --epoch. If True, it would decode with the averaged model " + "over the epoch range from `epoch-avg` (excluded) to `epoch`." + "Actually only the models with epoch number of `epoch-avg` and " + "`epoch` are loaded for averaging. ", + ) + + parser.add_argument( + "--num-decoder-layers", + type=int, + default=0, + help="""Number of decoder layer of transformer decoder. + Setting this to 0 will not create the decoder at all (pure CTC model) + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="conformer_ctc2/exp", + help="The experiment dir", + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_bpe_200", + help="The lang dir", + ) + + parser.add_argument( + "--lm-dir", + type=str, + default="data/lm", + help="""The n-gram LM dir. + It should contain either G_4_gram.pt or G_4_gram.fst.txt + """, + ) + + return parser + + +def get_params() -> AttributeDict: + params = AttributeDict( + { + # parameters for conformer + "subsampling_factor": 2, + "feature_dim": 768, + "nhead": 8, + "dim_feedforward": 2048, + "encoder_dim": 512, + "num_encoder_layers": 12, + # parameters for decoding + "search_beam": 20, + "output_beam": 8, + "min_active_states": 30, + "max_active_states": 10000, + "use_double_scores": True, + "env_info": get_env_info(), + } + ) + return params + + +def ctc_greedy_search( + nnet_output: torch.Tensor, + memory: torch.Tensor, + memory_key_padding_mask: torch.Tensor, +) -> List[List[int]]: + """Apply CTC greedy search + + Args: + speech (torch.Tensor): (batch, max_len, feat_dim) + speech_length (torch.Tensor): (batch, ) + Returns: + List[List[int]]: best path result + """ + batch_size = memory.shape[1] + # Let's assume B = batch_size + encoder_out = memory + encoder_mask = memory_key_padding_mask + maxlen = encoder_out.size(0) + + ctc_probs = nnet_output # (B, maxlen, vocab_size) + topk_prob, topk_index = ctc_probs.topk(1, dim=2) # (B, maxlen, 1) + topk_index = topk_index.view(batch_size, maxlen) # (B, maxlen) + topk_index = topk_index.masked_fill_(encoder_mask, 0) # (B, maxlen) + hyps = [hyp.tolist() for hyp in topk_index] + scores = topk_prob.max(1) + hyps = [remove_duplicates_and_blank(hyp) for hyp in hyps] + return hyps, scores + + +def remove_duplicates_and_blank(hyp: List[int]) -> List[int]: + # from https://github.com/wenet-e2e/wenet/blob/main/wenet/utils/common.py + new_hyp: List[int] = [] + cur = 0 + while cur < len(hyp): + if hyp[cur] != 0: + new_hyp.append(hyp[cur]) + prev = cur + while cur < len(hyp) and hyp[cur] == hyp[prev]: + cur += 1 + return new_hyp + + +def decode_one_batch( + params: AttributeDict, + model: nn.Module, + HLG: Optional[k2.Fsa], + H: Optional[k2.Fsa], + bpe_model: Optional[spm.SentencePieceProcessor], + batch: dict, + word_table: k2.SymbolTable, + sos_id: int, + eos_id: int, + G: Optional[k2.Fsa] = None, +) -> Dict[str, List[List[str]]]: + """Decode one batch and return the result in a dict. The dict has the + following format: + + - key: It indicates the setting used for decoding. For example, + if no rescoring is used, the key is the string `no_rescore`. + If LM rescoring is used, the key is the string `lm_scale_xxx`, + where `xxx` is the value of `lm_scale`. An example key is + `lm_scale_0.7` + - value: It contains the decoding result. `len(value)` equals to + batch size. `value[i]` is the decoding result for the i-th + utterance in the given batch. + Args: + params: + It's the return value of :func:`get_params`. + + - params.method is "1best", it uses 1best decoding without LM rescoring. + + model: + The neural model. + HLG: + The decoding graph. Used only when params.method is NOT ctc-decoding. + H: + The ctc topo. Used only when params.method is ctc-decoding. + bpe_model: + The BPE model. Used only when params.method is ctc-decoding. + batch: + It is the return value from iterating + `lhotse.dataset.K2SpeechRecognitionDataset`. See its documentation + for the format of the `batch`. + word_table: + The word symbol table. + sos_id: + The token ID of the SOS. + eos_id: + The token ID of the EOS. + G: + An LM. It is not None when params.method is "nbest-rescoring" + or "whole-lattice-rescoring". In general, the G in HLG + is a 3-gram LM, while this G is a 4-gram LM. + Returns: + Return the decoding result. See above description for the format of + the returned dict. Note: If it decodes to nothing, then return None. + """ + if HLG is not None: + device = HLG.device + else: + device = H.device + feature = batch["inputs"] + assert feature.ndim == 3 + feature = feature.to(device) + # at entry, feature is (N, T, C) + + supervisions = batch["supervisions"] + + nnet_output, memory, memory_key_padding_mask = model(feature, supervisions) + # nnet_output is (N, T, C) + nnet_output[:, :, 0] += params.blank_bias + + supervision_segments = torch.stack( + ( + supervisions["sequence_idx"], + torch.div( + supervisions["start_frame"], + params.subsampling_factor, + rounding_mode="trunc", + ), + torch.div( + supervisions["num_frames"], + params.subsampling_factor, + rounding_mode="trunc", + ), + ), + 1, + ).to(torch.int32) + + if H is None: + assert HLG is not None + decoding_graph = HLG + else: + assert HLG is None + assert bpe_model is not None + decoding_graph = H + + lattice = get_lattice( + nnet_output=nnet_output, + decoding_graph=decoding_graph, + supervision_segments=supervision_segments, + search_beam=params.search_beam, + output_beam=params.output_beam, + min_active_states=params.min_active_states, + max_active_states=params.max_active_states, + subsampling_factor=params.subsampling_factor + 2, + ) + + if params.method == "ctc-decoding": + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + # Note: `best_path.aux_labels` contains token IDs, not word IDs + # since we are using H, not HLG here. + # + # token_ids is a lit-of-list of IDs + token_ids = get_texts(best_path) + + # hyps is a list of str, e.g., ['xxx yyy zzz', ...] + hyps = bpe_model.decode(token_ids) + + # hyps is a list of list of str, e.g., [['xxx', 'yyy', 'zzz'], ... ] + hyps = [s.split() for s in hyps] + key = "ctc-decoding" + return {key: hyps} + + if params.method == "ctc-greedy-search": + hyps, _ = ctc_greedy_search( + nnet_output, + memory, + memory_key_padding_mask, + ) + + # hyps is a list of str, e.g., ['xxx yyy zzz', ...] + hyps = bpe_model.decode(hyps) + + # hyps is a list of list of str, e.g., [['xxx', 'yyy', 'zzz'], ... ] + hyps = [s.split() for s in hyps] + key = "ctc-greedy-search" + return {key: hyps} + + if params.method in ["1best"]: + best_path = one_best_decoding( + lattice=lattice, use_double_scores=params.use_double_scores + ) + key = "no_rescore" + + hyps = get_texts(best_path) + hyps = [[word_table[i] for i in ids] for ids in hyps] + + return {key: hyps} + else: + assert False, f"Unsupported decoding method: {params.method}" + + +def decode_dataset( + dl: torch.utils.data.DataLoader, + params: AttributeDict, + model: nn.Module, + HLG: Optional[k2.Fsa], + H: Optional[k2.Fsa], + bpe_model: Optional[spm.SentencePieceProcessor], + word_table: k2.SymbolTable, + sos_id: int, + eos_id: int, + G: Optional[k2.Fsa] = None, +) -> Dict[str, List[Tuple[str, List[str], List[str]]]]: + """Decode dataset. + + Args: + dl: + PyTorch's dataloader containing the dataset to decode. + params: + It is returned by :func:`get_params`. + model: + The neural model. + HLG: + The decoding graph. Used only when params.method is NOT ctc-decoding. + H: + The ctc topo. Used only when params.method is ctc-decoding. + bpe_model: + The BPE model. Used only when params.method is ctc-decoding. + word_table: + It is the word symbol table. + sos_id: + The token ID for SOS. + eos_id: + The token ID for EOS. + G: + An LM. It is not None when params.method is "nbest-rescoring" + or "whole-lattice-rescoring". In general, the G in HLG + is a 3-gram LM, while this G is a 4-gram LM. + Returns: + Return a dict, whose key may be "no-rescore" if no LM rescoring + is used, or it may be "lm_scale_0.7" if LM rescoring is used. + Its value is a list of tuples. Each tuple contains two elements: + The first is the reference transcript, and the second is the + predicted result. + """ + num_cuts = 0 + + try: + num_batches = len(dl) + except TypeError: + num_batches = "?" + + results = defaultdict(list) + for batch_idx, batch in enumerate(dl): + texts = batch["supervisions"]["text"] + cut_ids = [cut.id for cut in batch["supervisions"]["cut"]] + + hyps_dict = decode_one_batch( + params=params, + model=model, + HLG=HLG, + H=H, + bpe_model=bpe_model, + batch=batch, + word_table=word_table, + G=G, + sos_id=sos_id, + eos_id=eos_id, + ) + + if hyps_dict is not None: + for lm_scale, hyps in hyps_dict.items(): + this_batch = [] + assert len(hyps) == len(texts) + for cut_id, hyp_words, ref_text in zip(cut_ids, hyps, texts): + ref_words = ref_text.split() + this_batch.append((cut_id, ref_words, hyp_words)) + + results[lm_scale].extend(this_batch) + else: + assert len(results) > 0, "It should not decode to empty in the first batch!" + this_batch = [] + hyp_words = [] + for ref_text in texts: + ref_words = ref_text.split() + this_batch.append((ref_words, hyp_words)) + + for lm_scale in results.keys(): + results[lm_scale].extend(this_batch) + + num_cuts += len(texts) + + if batch_idx % 100 == 0: + batch_str = f"{batch_idx}/{num_batches}" + + logging.info(f"batch {batch_str}, cuts processed until now is {num_cuts}") + return results + + +def save_results( + params: AttributeDict, + test_set_name: str, + results_dict: Dict[str, List[Tuple[str, List[str], List[str]]]], +): + if params.method in ("attention-decoder", "rnn-lm"): + # Set it to False since there are too many logs. + enable_log = False + else: + enable_log = True + test_set_wers = dict() + for key, results in results_dict.items(): + recog_path = params.exp_dir / f"recogs-{test_set_name}-{key}.txt" + results = sorted(results) + store_transcripts(filename=recog_path, texts=results) + if enable_log: + logging.info(f"The transcripts are stored in {recog_path}") + + # The following prints out WERs, per-word error statistics and aligned + # ref/hyp pairs. + errs_filename = params.exp_dir / f"errs-{test_set_name}-{key}.txt" + with open(errs_filename, "w") as f: + wer = write_error_stats( + f, f"{test_set_name}-{key}", results, enable_log=enable_log + ) + test_set_wers[key] = wer + + if enable_log: + logging.info("Wrote detailed error stats to {}".format(errs_filename)) + + test_set_wers = sorted(test_set_wers.items(), key=lambda x: x[1]) + errs_info = params.exp_dir / f"wer-summary-{test_set_name}.txt" + with open(errs_info, "w") as f: + print("settings\tWER", file=f) + for key, val in test_set_wers: + print("{}\t{}".format(key, val), file=f) + + s = "\nFor {}, WER of different settings are:\n".format(test_set_name) + note = "\tbest for {}".format(test_set_name) + for key, val in test_set_wers: + s += "{}\t{}{}\n".format(key, val, note) + note = "" + logging.info(s) + + +@torch.no_grad() +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + args.lang_dir = Path(args.lang_dir) + args.lm_dir = Path(args.lm_dir) + assert "▁" not in args.otc_token + args.otc_token = f"▁{args.otc_token}" + + params = get_params() + params.update(vars(args)) + + setup_logger(f"{params.exp_dir}/log-{params.method}/log-decode") + logging.info("Decoding started") + logging.info(params) + + lexicon = Lexicon(params.lang_dir) + # remove otc_token from decoding units + max_token_id = max(lexicon.tokens) - 1 + num_classes = max_token_id + 1 # +1 for the blank + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", 0) + + logging.info(f"device: {device}") + + graph_compiler = OtcTrainingGraphCompiler( + params.lang_dir, + params.otc_token, + device=device, + sos_token="", + eos_token="", + ) + sos_id = graph_compiler.sos_id + eos_id = graph_compiler.eos_id + + params.num_classes = num_classes + params.sos_id = sos_id + params.eos_id = eos_id + + if params.method == "ctc-decoding" or params.method == "ctc-greedy-search": + HLG = None + H = k2.ctc_topo( + max_token=max_token_id, + modified=False, + device=device, + ) + bpe_model = spm.SentencePieceProcessor() + bpe_model.load(str(params.lang_dir / "bpe.model")) + else: + H = None + bpe_model = None + HLG = k2.Fsa.from_dict( + torch.load(f"{params.lang_dir}/HLG.pt", map_location=device) + ) + assert HLG.requires_grad is False + + if not hasattr(HLG, "lm_scores"): + HLG.lm_scores = HLG.scores.clone() + + G = None + + model = Conformer( + num_features=params.feature_dim, + nhead=params.nhead, + d_model=params.encoder_dim, + num_classes=num_classes, + subsampling_factor=params.subsampling_factor, + num_encoder_layers=params.num_encoder_layers, + num_decoder_layers=params.num_decoder_layers, + ) + + if not params.use_averaged_model: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + elif params.avg == 1: + load_checkpoint(f"{params.exp_dir}/epoch-{params.epoch}.pt", model) + else: + start = params.epoch - params.avg + 1 + filenames = [] + for i in range(start, params.epoch + 1): + if i >= 1: + filenames.append(f"{params.exp_dir}/epoch-{i}.pt") + logging.info(f"averaging {filenames}") + model.to(device) + model.load_state_dict(average_checkpoints(filenames, device=device)) + else: + if params.iter > 0: + filenames = find_checkpoints(params.exp_dir, iteration=-params.iter)[ + : params.avg + 1 + ] + if len(filenames) == 0: + raise ValueError( + f"No checkpoints found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + elif len(filenames) < params.avg + 1: + raise ValueError( + f"Not enough checkpoints ({len(filenames)}) found for" + f" --iter {params.iter}, --avg {params.avg}" + ) + filename_start = filenames[-1] + filename_end = filenames[0] + logging.info( + "Calculating the averaged model over iteration checkpoints" + f" from {filename_start} (excluded) to {filename_end}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + else: + assert params.avg > 0, params.avg + start = params.epoch - params.avg + assert start >= 1, start + filename_start = f"{params.exp_dir}/epoch-{start}.pt" + filename_end = f"{params.exp_dir}/epoch-{params.epoch}.pt" + logging.info( + f"Calculating the averaged model over epoch range from " + f"{start} (excluded) to {params.epoch}" + ) + model.to(device) + model.load_state_dict( + average_checkpoints_with_averaged_model( + filename_start=filename_start, + filename_end=filename_end, + device=device, + ) + ) + + model.to(device) + model.eval() + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + # we need cut ids to display recognition results. + args.return_cuts = True + librispeech = LibriSpeechAsrDataModule(args) + + test_clean_cuts = librispeech.test_clean_cuts() + test_other_cuts = librispeech.test_other_cuts() + + test_clean_dl = librispeech.test_dataloaders(test_clean_cuts) + test_other_dl = librispeech.test_dataloaders(test_other_cuts) + + test_sets = ["test-clean", "test-other"] + test_dl = [test_clean_dl, test_other_dl] + + for test_set, test_dl in zip(test_sets, test_dl): + results_dict = decode_dataset( + dl=test_dl, + params=params, + model=model, + HLG=HLG, + H=H, + bpe_model=bpe_model, + word_table=lexicon.word_table, + G=G, + sos_id=sos_id, + eos_id=eos_id, + ) + + save_results(params=params, test_set_name=test_set, results_dict=results_dict) + + logging.info("Done!") + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/conformer_ctc2/export.py b/egs/librispeech/WSASR/conformer_ctc2/export.py new file mode 120000 index 000000000..5f484e391 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/export.py @@ -0,0 +1 @@ +../../ASR/conformer_ctc2/export.py \ No newline at end of file diff --git a/egs/librispeech/WSASR/conformer_ctc2/label_smoothing.py b/egs/librispeech/WSASR/conformer_ctc2/label_smoothing.py new file mode 120000 index 000000000..c050ea637 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/label_smoothing.py @@ -0,0 +1 @@ +../../ASR/conformer_ctc/label_smoothing.py \ No newline at end of file diff --git a/egs/librispeech/WSASR/conformer_ctc2/optim.py b/egs/librispeech/WSASR/conformer_ctc2/optim.py new file mode 120000 index 000000000..db836b5e0 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/optim.py @@ -0,0 +1 @@ +../../ASR/pruned_transducer_stateless2/optim.py \ No newline at end of file diff --git a/egs/librispeech/WSASR/conformer_ctc2/scaling.py b/egs/librispeech/WSASR/conformer_ctc2/scaling.py new file mode 120000 index 000000000..bd0abfeee --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/scaling.py @@ -0,0 +1 @@ +../../ASR/pruned_transducer_stateless2/scaling.py \ No newline at end of file diff --git a/egs/librispeech/WSASR/conformer_ctc2/subsampling.py b/egs/librispeech/WSASR/conformer_ctc2/subsampling.py new file mode 100644 index 000000000..2ba802866 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/subsampling.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 University of Chinese Academy of Sciences (author: Han Zhu) +# 2022 Xiaomi Corporation (author: Quandong Wang) +# 2023 Johns Hopkins University (author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 scaling import ( + ActivationBalancer, + BasicNorm, + DoubleSwish, + ScaledConv2d, + ScaledLinear, +) + + +class Conv2dSubsampling(torch.nn.Module): + """Convolutional 2D subsampling (to 1/4 length). + + Convert an input of shape (N, T, idim) to an output + with shape (N, T', odim), where + T' = ((T-1)//2 - 1)//2, which approximates T' == T//4 + + It is based on + https://github.com/espnet/espnet/blob/master/espnet/nets/pytorch_backend/transformer/subsampling.py # noqa + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + layer1_channels: int = 8, + layer2_channels: int = 32, + layer3_channels: int = 128, + ) -> None: + """ + Args: + in_channels: + Number of channels in. The input shape is (N, T, in_channels). + Caution: It requires: T >=7, in_channels >=7 + out_channels + Output dim. The output shape is (N, ((T-1)//2 - 1)//2, out_channels) + layer1_channels: + Number of channels in layer1 + layer1_channels: + Number of channels in layer2 + """ + assert in_channels >= 7 + super().__init__() + + self.conv = torch.nn.Sequential( + ScaledConv2d( + in_channels=1, + out_channels=layer1_channels, + kernel_size=3, + padding=1, + ), + ActivationBalancer(channel_dim=1), + DoubleSwish(), + ScaledConv2d( + in_channels=layer1_channels, + out_channels=layer2_channels, + kernel_size=3, + stride=2, + ), + ActivationBalancer(channel_dim=1), + DoubleSwish(), + ScaledConv2d( + in_channels=layer2_channels, + out_channels=layer3_channels, + kernel_size=3, + stride=2, + ), + ActivationBalancer(channel_dim=1), + DoubleSwish(), + ) + self.out = ScaledLinear( + layer3_channels * (((in_channels - 1) // 2 - 1) // 2), out_channels + ) + # set learn_eps=False because out_norm is preceded by `out`, and `out` + # itself has learned scale, so the extra degree of freedom is not + # needed. + self.out_norm = BasicNorm(out_channels, learn_eps=False) + # constrain median of output to be close to zero. + self.out_balancer = ActivationBalancer( + channel_dim=-1, min_positive=0.45, max_positive=0.55 + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """Subsample x. + + Args: + x: + Its shape is (N, T, idim). + + Returns: + Return a tensor of shape (N, ((T-1)//2 - 1)//2, odim) + """ + # On entry, x is (N, T, idim) + x = x.unsqueeze(1) # (N, T, idim) -> (N, 1, T, idim) i.e., (N, C, H, W) + x = self.conv(x) + # Now x is of shape (N, odim, ((T-1)//2 - 1)//2, ((idim-1)//2 - 1)//2) + b, c, t, f = x.size() + x = self.out(x.transpose(1, 2).contiguous().view(b, t, c * f)) + # Now x is of shape (N, ((T-1)//2 - 1))//2, odim) + x = self.out_norm(x) + x = self.out_balancer(x) + return x + + +class Conv2dSubsampling2(torch.nn.Module): + """Convolutional 2D subsampling (to 1/2 length). + + Convert an input of shape (N, T, idim) to an output + with shape (N, T', odim) where + T' = (T - 1) // 2 - 2, which approximates T' == T // 2 + """ + + def __init__( + self, + in_channels: int, + out_channels: int, + layer1_channels: int = 8, + layer2_channels: int = 32, + layer3_channels: int = 128, + ) -> None: + assert in_channels >= 7 + super().__init__() + + self.conv = torch.nn.Sequential( + ScaledConv2d( + in_channels=1, + out_channels=layer1_channels, + kernel_size=3, + padding=1, + ), + ActivationBalancer(channel_dim=1), + DoubleSwish(), + ScaledConv2d( + in_channels=layer1_channels, + out_channels=layer2_channels, + kernel_size=3, + stride=2, + ), + ActivationBalancer(channel_dim=1), + DoubleSwish(), + ScaledConv2d( + in_channels=layer2_channels, + out_channels=layer3_channels, + kernel_size=3, + stride=1, + ), + ActivationBalancer(channel_dim=1), + DoubleSwish(), + ) + self.out = ScaledLinear( + layer3_channels * ((in_channels - 1) // 2 - 2), out_channels + ) + self.out_norm = BasicNorm(out_channels, learn_eps=False) + self.out_balancer = ActivationBalancer( + channel_dim=-1, min_positive=0.45, max_positive=0.55 + ) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x = x.unsqueeze(1) + x = self.conv(x) + b, c, t, f = x.size() + x = self.out(x.transpose(1, 2).contiguous().view(b, t, c * f)) + x = self.out_norm(x) + x = self.out_balancer(x) + return x diff --git a/egs/librispeech/WSASR/conformer_ctc2/train.py b/egs/librispeech/WSASR/conformer_ctc2/train.py new file mode 100755 index 000000000..fe6c5af91 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/train.py @@ -0,0 +1,1115 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang, +# Wei Kang, +# Mingshuang Luo, +# Zengwei Yao, +# Quandong Wang) +# 2023 Johns Hopkins University (author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. +""" +Usage: + +export CUDA_VISIBLE_DEVICES="0,1,2,3" + +./conformer_ctc2/train.py \ + --world-size 4 \ + --manifest-dir data/ssl \ + --train-manifest librispeech_cuts_train-clean-100_0.17_0.17_0.17.jsonl.gz \ + --exp-dir conformer_ctc2/exp \ + --lang-dir data/lang_bpe_200 \ + --otc-token "" \ + --allow-bypass-arc true \ + --allow-self-loop-arc true \ + --initial-bypass-weight -19 \ + --initial-self-loop-weight 3.75 \ + --bypass-weight-decay 0.975 \ + --self-loop-weight-decay 0.999 \ + --show-alignment true +""" + + +import argparse +import copy +import logging +import warnings +from pathlib import Path +from shutil import copyfile +from typing import Any, Dict, Optional, Tuple, Union + +import k2 +import optim +import torch +import torch.multiprocessing as mp +import torch.nn as nn +from asr_datamodule import LibriSpeechAsrDataModule +from conformer import Conformer +from lhotse.cut import Cut +from lhotse.dataset.sampling.base import CutSampler +from lhotse.utils import fix_random_seed +from optim import Eden, Eve +from torch import Tensor +from torch.cuda.amp import GradScaler +from torch.nn.parallel import DistributedDataParallel as DDP +from torch.utils.tensorboard import SummaryWriter + +from icefall import diagnostics +from icefall.checkpoint import load_checkpoint, remove_checkpoints +from icefall.checkpoint import save_checkpoint as save_checkpoint_impl +from icefall.checkpoint import ( + save_checkpoint_with_global_batch_idx, + update_averaged_model, +) +from icefall.decode import one_best_decoding +from icefall.dist import cleanup_dist, setup_dist +from icefall.env import get_env_info +from icefall.otc_graph_compiler import OtcTrainingGraphCompiler +from icefall.utils import ( + AttributeDict, + MetricsTracker, + encode_supervisions_otc, + get_texts, + setup_logger, + str2bool, +) + +LRSchedulerType = Union[torch.optim.lr_scheduler._LRScheduler, optim.LRScheduler] + + +def get_parser(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + parser.add_argument( + "--world-size", + type=int, + default=1, + help="Number of GPUs for DDP training.", + ) + + parser.add_argument( + "--master-port", + type=int, + default=12354, + help="Master port to use for DDP training.", + ) + + parser.add_argument( + "--tensorboard", + type=str2bool, + default=True, + help="Should various information be logged in tensorboard.", + ) + + parser.add_argument( + "--num-epochs", + type=int, + default=20, + help="Number of epochs to train.", + ) + + parser.add_argument( + "--start-epoch", + type=int, + default=1, + help="""Resume training from this epoch. It should be positive. + If larger than 1, it will load checkpoint from + exp-dir/epoch-{start_epoch-1}.pt + """, + ) + + parser.add_argument( + "--start-batch", + type=int, + default=0, + help="""If positive, --start-epoch is ignored and + it loads the checkpoint from exp-dir/checkpoint-{start_batch}.pt + """, + ) + + parser.add_argument( + "--exp-dir", + type=str, + default="conformer_ctc2/exp", + help="""The experiment dir. + It specifies the directory where all training related + files, e.g., checkpoints, log, etc, are saved + """, + ) + + parser.add_argument( + "--lang-dir", + type=str, + default="data/lang_bpe_200", + help="""The lang dir + It contains language related input files such as + "lexicon.txt" + """, + ) + + parser.add_argument( + "--initial-lr", + type=float, + default=0.003, + help="""The initial learning rate. This value should not need to be + changed.""", + ) + + parser.add_argument( + "--lr-batches", + type=float, + default=5000, + help="""Number of steps that affects how rapidly the learning rate decreases. + We suggest not to change this.""", + ) + + parser.add_argument( + "--lr-epochs", + type=float, + default=6, + help="""Number of epochs that affects how rapidly the learning rate decreases. + """, + ) + + parser.add_argument( + "--att-rate", + type=float, + default=0.0, + help="""The attention rate. + The total loss is (1 - att_rate) * ctc_loss + att_rate * att_loss + """, + ) + + parser.add_argument( + "--num-decoder-layers", + type=int, + default=0, + help="""Number of decoder layer of transformer decoder. + Setting this to 0 will not create the decoder at all (pure CTC model) + """, + ) + + parser.add_argument( + "--seed", + type=int, + default=42, + help="The seed for random generators intended for reproducibility", + ) + + parser.add_argument( + "--print-diagnostics", + type=str2bool, + default=False, + help="Accumulate stats on activations, print them and exit.", + ) + + parser.add_argument( + "--save-every-n", + type=int, + default=8000, + help="""Save checkpoint after processing this number of batches" + periodically. We save checkpoint to exp-dir/ whenever + params.batch_idx_train % save_every_n == 0. The checkpoint filename + has the form: f'exp-dir/checkpoint-{params.batch_idx_train}.pt' + Note: It also saves checkpoint to `exp-dir/epoch-xxx.pt` at the + end of each epoch where `xxx` is the epoch number counting from 0. + """, + ) + + parser.add_argument( + "--keep-last-k", + type=int, + default=10, + help="""Only keep this number of checkpoints on disk. + For instance, if it is 3, there are only 3 checkpoints + in the exp-dir with filenames `checkpoint-xxx.pt`. + It does not affect checkpoints with name `epoch-xxx.pt`. + """, + ) + + parser.add_argument( + "--average-period", + type=int, + default=100, + help="""Update the averaged model, namely `model_avg`, after processing + this number of batches. `model_avg` is a separate version of model, + in which each floating-point parameter is the average of all the + parameters from the start of training. Each time we take the average, + we do: `model_avg = model * (average_period / batch_idx_train) + + model_avg * ((batch_idx_train - average_period) / batch_idx_train)`. + """, + ) + + parser.add_argument( + "--use-fp16", + type=str2bool, + default=False, + help="Whether to use half precision training.", + ) + + parser.add_argument( + "--otc-token", + type=str, + default="_", + help="OTC token", + ) + + parser.add_argument( + "--allow-bypass-arc", + type=str2bool, + default=True, + help="""Whether to add bypass arc to training graph for substitution + and insertion errors (wrong or extra words in the transcript).""", + ) + + parser.add_argument( + "--allow-self-loop-arc", + type=str2bool, + default=True, + help="""Whether to self-loop bypass arc to training graph for deletion errors + (missing words in the transcript).""", + ) + + parser.add_argument( + "--initial-bypass-weight", + type=float, + default=0.0, + help="Initial weight associated with bypass arc", + ) + + parser.add_argument( + "--initial-self-loop-weight", + type=float, + default=0.0, + help="Initial weight associated with self-loop arc", + ) + + parser.add_argument( + "--bypass-weight-decay", + type=float, + default=1.0, + help="""Weight decay factor of bypass arc weight: + bypass_arc_weight = intial_bypass_weight * bypass_weight_decay ^ ith-epoch""", + ) + + parser.add_argument( + "--self-loop-weight-decay", + type=float, + default=1.0, + help="""Weight decay factor of self-loop arc weight: + self_loop_arc_weight = intial_self_loop_weight * self_loop_weight_decay ^ ith-epoch""", + ) + + parser.add_argument( + "--show-alignment", + type=str2bool, + default=True, + help="Whether to print OTC alignment during training", + ) + + return parser + + +def get_params() -> AttributeDict: + """Return a dict containing training parameters. + + All training related parameters that are not passed from the commandline + are saved in the variable `params`. + + Commandline options are merged into `params` after they are parsed, so + you can also access them via `params`. + + Explanation of options saved in `params`: + + - best_train_loss: Best training loss so far. It is used to select + the model that has the lowest training loss. It is + updated during the training. + + - best_valid_loss: Best validation loss so far. It is used to select + the model that has the lowest validation loss. It is + updated during the training. + + - best_train_epoch: It is the epoch that has the best training loss. + + - best_valid_epoch: It is the epoch that has the best validation loss. + + - batch_idx_train: Used to writing statistics to tensorboard. It + contains number of batches trained so far across + epochs. + + - log_interval: Print training loss if batch_idx % log_interval` is 0 + + - reset_interval: Reset statistics if batch_idx % reset_interval is 0 + + - valid_interval: Run validation if batch_idx % valid_interval is 0 + + - feature_dim: The model input dim. It has to match the one used + in computing features. + + - subsampling_factor: The subsampling factor for the model. + + - encoder_dim: Hidden dim for multi-head attention model. + + - num_decoder_layers: Number of decoder layer of transformer decoder. + + - beam_size: It is used in k2.ctc_loss + + - reduction: It is used in k2.ctc_loss + + - use_double_scores: It is used in k2.ctc_loss + + - warm_step: The warm_step for Noam optimizer. + """ + params = AttributeDict( + { + "best_train_loss": float("inf"), + "best_valid_loss": float("inf"), + "best_train_epoch": -1, + "best_valid_epoch": -1, + "batch_idx_train": 0, + "log_interval": 1, + "reset_interval": 200, + "valid_interval": 800, # For the 100h subset, use 800 + "alignment_interval": 25, + # parameters for conformer + "feature_dim": 768, + "subsampling_factor": 2, + "encoder_dim": 512, + "nhead": 8, + "dim_feedforward": 2048, + "num_encoder_layers": 12, + # parameters for ctc loss + "beam_size": 10, + "reduction": "sum", + "use_double_scores": True, + # parameters for Noam + "model_warm_step": 3000, # arg given to model, not for lrate + "env_info": get_env_info(), + } + ) + + return params + + +def load_checkpoint_if_available( + params: AttributeDict, + model: nn.Module, + model_avg: nn.Module = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, +) -> Optional[Dict[str, Any]]: + """Load checkpoint from file. + + If params.start_batch is positive, it will load the checkpoint from + `params.exp_dir/checkpoint-{params.start_batch}.pt`. Otherwise, if + params.start_epoch is larger than 1, it will load the checkpoint from + `params.start_epoch - 1`. + + Apart from loading state dict for `model` and `optimizer` it also updates + `best_train_epoch`, `best_train_loss`, `best_valid_epoch`, + and `best_valid_loss` in `params`. + + Args: + params: + The return value of :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer that we are using. + scheduler: + The scheduler that we are using. + Returns: + Return a dict containing previously saved training info. + """ + if params.start_batch > 0: + filename = params.exp_dir / f"checkpoint-{params.start_batch}.pt" + elif params.start_epoch > 1: + filename = params.exp_dir / f"epoch-{params.start_epoch-1}.pt" + else: + return None + + assert filename.is_file(), f"{filename} does not exist!" + + saved_params = load_checkpoint( + filename, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + ) + + keys = [ + "best_train_epoch", + "best_valid_epoch", + "batch_idx_train", + "best_train_loss", + "best_valid_loss", + ] + for k in keys: + params[k] = saved_params[k] + + if params.start_batch > 0: + if "cur_epoch" in saved_params: + params["start_epoch"] = saved_params["cur_epoch"] + + return saved_params + + +def save_checkpoint( + params: AttributeDict, + model: Union[nn.Module, DDP], + model_avg: Optional[nn.Module] = None, + optimizer: Optional[torch.optim.Optimizer] = None, + scheduler: Optional[LRSchedulerType] = None, + sampler: Optional[CutSampler] = None, + scaler: Optional[GradScaler] = None, + rank: int = 0, +) -> None: + """Save model, optimizer, scheduler and training stats to file. + + Args: + params: + It is returned by :func:`get_params`. + model: + The training model. + model_avg: + The stored model averaged from the start of training. + optimizer: + The optimizer used in the training. + sampler: + The sampler for the training dataset. + scaler: + The scaler used for mix precision training. + """ + if rank != 0: + return + filename = params.exp_dir / f"epoch-{params.cur_epoch}.pt" + save_checkpoint_impl( + filename=filename, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=sampler, + scaler=scaler, + rank=rank, + ) + + if params.best_train_epoch == params.cur_epoch: + best_train_filename = params.exp_dir / "best-train-loss.pt" + copyfile(src=filename, dst=best_train_filename) + + if params.best_valid_epoch == params.cur_epoch: + best_valid_filename = params.exp_dir / "best-valid-loss.pt" + copyfile(src=filename, dst=best_valid_filename) + + +def compute_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + batch: dict, + graph_compiler: OtcTrainingGraphCompiler, + is_training: bool, + warmup: float = 2.0, +) -> Tuple[Tensor, MetricsTracker]: + """ + Compute OTC loss given the model and its inputs. + + Args: + params: + Parameters for training. See :func:`get_params`. + model: + The model for training. It is an instance of Conformer in our case. + batch: + A batch of data. See `lhotse.dataset.K2SpeechRecognitionDataset()` + for the content in it. + graph_compiler: + It is used to build a decoding graph from a ctc topo and training + transcript. The training transcript is contained in the given `batch`, + while the ctc topo is built when this compiler is instantiated. + is_training: + True for training. False for validation. When it is True, this + function enables autograd during computation; when it is False, it + disables autograd. + warmup: a floating point value which increases throughout training; + values >= 1.0 are fully warmed up and have all modules present. + """ + device = model.device if isinstance(model, DDP) else next(model.parameters()).device + feature = batch["inputs"] + # at entry, feature is (N, T, C) + assert feature.ndim == 3 + feature = feature.to(device) + + supervisions = batch["supervisions"] + feature_lens = supervisions["num_frames"].to(device) + + with torch.set_grad_enabled(is_training): + nnet_output, encoder_memory, memory_mask = model( + feature, supervisions, warmup=warmup + ) + # Set the probability of OTC token as the average of non-blank tokens + # under the assumption that blank is the first and + # OTC token is the last token in tokens.txt + _, _, V = nnet_output.shape + + otc_token_log_prob = torch.logsumexp( + nnet_output[:, :, 1:], dim=-1, keepdim=True + ) - torch.log(torch.tensor([V - 1])).to(device) + + nnet_output = torch.cat([nnet_output, otc_token_log_prob], dim=-1) + + # NOTE: We need `encode_supervisions` to sort sequences with + # different duration in decreasing order, required by + # `k2.intersect_dense` called in `k2.ctc_loss` + supervision_segments, texts, utt_ids, verbatim_texts = encode_supervisions_otc( + supervisions, subsampling_factor=params.subsampling_factor + ) + + bypass_weight = graph_compiler.initial_bypass_weight * ( + graph_compiler.bypass_weight_decay ** (params.cur_epoch - 1) + ) + self_loop_weight = graph_compiler.initial_self_loop_weight * ( + graph_compiler.self_loop_weight_decay ** (params.cur_epoch - 1) + ) + + decoding_graph = graph_compiler.compile( + texts=texts, + allow_bypass_arc=params.allow_bypass_arc, + allow_self_loop_arc=params.allow_self_loop_arc, + bypass_weight=bypass_weight, + self_loop_weight=self_loop_weight, + ) + + dense_fsa_vec = k2.DenseFsaVec( + nnet_output, + supervision_segments, + allow_truncate=3, + ) + + otc_loss = k2.ctc_loss( + decoding_graph=decoding_graph, + dense_fsa_vec=dense_fsa_vec, + output_beam=params.beam_size, + reduction=params.reduction, + use_double_scores=params.use_double_scores, + ) + + assert params.att_rate == 0.0 + loss = otc_loss + + assert loss.requires_grad == is_training + + info = MetricsTracker() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + info["frames"] = (feature_lens // params.subsampling_factor).sum().item() + info["otc_loss"] = otc_loss.detach().cpu().item() + + # Note: We use reduction=sum while computing the loss. + info["loss"] = loss.detach().cpu().item() + + # `utt_duration` and `utt_pad_proportion` would be normalized by `utterances` # noqa + info["utterances"] = feature.size(0) + # averaged input duration in frames over utterances + info["utt_duration"] = feature_lens.sum().item() + # averaged padding proportion over utterances + info["utt_pad_proportion"] = ( + ((feature.size(1) - feature_lens) / feature.size(1)).sum().item() + ) + + if params.show_alignment: + if params.batch_idx_train % params.alignment_interval == 0: + for index, utt_id in enumerate(utt_ids): + verbatim_text = verbatim_texts[index] + utt_id = utt_ids[index] + + lattice = k2.intersect_dense( + decoding_graph, + dense_fsa_vec, + params.beam_size, + ) + best_path = one_best_decoding( + lattice=lattice, + use_double_scores=params.use_double_scores, + ) + hyp_ids = get_texts(best_path)[index] + hyp_text_list = [graph_compiler.token_table[i] for i in hyp_ids] + hyp_text = "".join(hyp_text_list).replace("▁", " ") + + logging.info(f"[utterance id]: {utt_id}") + logging.info(f"[verbatim text]: {verbatim_text}") + logging.info(f"[best alignment]: {hyp_text}") + logging.info(bypass_weight) + + return loss, info + + +def compute_validation_loss( + params: AttributeDict, + model: Union[nn.Module, DDP], + graph_compiler: OtcTrainingGraphCompiler, + valid_dl: torch.utils.data.DataLoader, + world_size: int = 1, +) -> MetricsTracker: + """Run the validation process.""" + model.eval() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(valid_dl): + loss, loss_info = compute_loss( + params=params, + model=model, + batch=batch, + graph_compiler=graph_compiler, + is_training=False, + ) + assert loss.requires_grad is False + tot_loss = tot_loss + loss_info + + if world_size > 1: + tot_loss.reduce(loss.device) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + if loss_value < params.best_valid_loss: + params.best_valid_epoch = params.cur_epoch + params.best_valid_loss = loss_value + + return tot_loss + + +def train_one_epoch( + params: AttributeDict, + model: Union[nn.Module, DDP], + optimizer: torch.optim.Optimizer, + graph_compiler: OtcTrainingGraphCompiler, + scheduler: LRSchedulerType, + train_dl: torch.utils.data.DataLoader, + valid_dl: torch.utils.data.DataLoader, + scaler: GradScaler, + model_avg: Optional[nn.Module] = None, + tb_writer: Optional[SummaryWriter] = None, + world_size: int = 1, + rank: int = 0, +) -> None: + """Train the model for one epoch. + + The training loss from the mean of all frames is saved in + `params.train_loss`. It runs the validation process every + `params.valid_interval` batches. + + Args: + params: + It is returned by :func:`get_params`. + model: + The model for training. + optimizer: + The optimizer we are using. + graph_compiler: + It is used to convert transcripts to FSAs. + scheduler: + The learning rate scheduler, we call step() every step. + train_dl: + Dataloader for the training dataset. + valid_dl: + Dataloader for the validation dataset. + scaler: + The scaler used for mix precision training. + model_avg: + The stored model averaged from the start of training. + tb_writer: + Writer to write log messages to tensorboard. + world_size: + Number of nodes in DDP training. If it is 1, DDP is disabled. + rank: + The rank of the node in DDP training. If no DDP is used, it should + be set to 0. + """ + model.train() + + tot_loss = MetricsTracker() + + for batch_idx, batch in enumerate(train_dl): + params.batch_idx_train += 1 + batch_size = len(batch["supervisions"]["text"]) + + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, loss_info = compute_loss( + params=params, + model=model, + batch=batch, + graph_compiler=graph_compiler, + is_training=True, + warmup=(params.batch_idx_train / params.model_warm_step), + ) + # summary stats + tot_loss = (tot_loss * (1 - 1 / params.reset_interval)) + loss_info + + # NOTE: We use reduction==sum and loss is computed over utterances + # in the batch and there is no normalization to it so far. + # scaler.scale(loss).backward() + + try: + # loss.backward() + scaler.scale(loss).backward() + except RuntimeError as e: + if "CUDA out of memory" in str(e): + logging.error(f"failing batch size:{batch_size} ") + raise + + scheduler.step_batch(params.batch_idx_train) + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + + if params.print_diagnostics and batch_idx == 30: + return + + if ( + rank == 0 + and params.batch_idx_train > 0 + and params.batch_idx_train % params.average_period == 0 + ): + update_averaged_model( + params=params, + model_cur=model, + model_avg=model_avg, + ) + + if ( + params.batch_idx_train > 0 + and params.batch_idx_train % params.save_every_n == 0 + ): + save_checkpoint_with_global_batch_idx( + out_dir=params.exp_dir, + global_batch_idx=params.batch_idx_train, + model=model, + model_avg=model_avg, + params=params, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + remove_checkpoints( + out_dir=params.exp_dir, + topk=params.keep_last_k, + rank=rank, + ) + + if batch_idx % params.log_interval == 0: + cur_lr = scheduler.get_last_lr()[0] + logging.info( + f"Epoch {params.cur_epoch}, " + f"batch {batch_idx}, loss[{loss_info}], " + f"tot_loss[{tot_loss}], batch size: {batch_size}, " + f"lr: {cur_lr:.2e}" + ) + if loss_info["otc_loss"] == float("inf"): + logging.error("Your loss contains inf, something goes wrong") + if tb_writer is not None: + tb_writer.add_scalar( + "train/learning_rate", cur_lr, params.batch_idx_train + ) + + loss_info.write_summary( + tb_writer, "train/current_", params.batch_idx_train + ) + tot_loss.write_summary(tb_writer, "train/tot_", params.batch_idx_train) + + if batch_idx > 0 and batch_idx % params.valid_interval == 0: + logging.info("Computing validation loss") + valid_info = compute_validation_loss( + params=params, + model=model, + graph_compiler=graph_compiler, + valid_dl=valid_dl, + world_size=world_size, + ) + model.train() + logging.info(f"Epoch {params.cur_epoch}, validation: {valid_info}") + if tb_writer is not None: + valid_info.write_summary( + tb_writer, "train/valid_", params.batch_idx_train + ) + + loss_value = tot_loss["loss"] / tot_loss["frames"] + params.train_loss = loss_value + if params.train_loss < params.best_train_loss: + params.best_train_epoch = params.cur_epoch + params.best_train_loss = params.train_loss + + +def run(rank, world_size, args): + """ + Args: + rank: + It is a value between 0 and `world_size-1`, which is + passed automatically by `mp.spawn()` in :func:`main`. + The node with rank 0 is responsible for saving checkpoint. + world_size: + Number of GPUs for DDP training. + args: + The return value of get_parser().parse_args() + """ + params = get_params() + params.update(vars(args)) + params.valid_interval = 1600 + + fix_random_seed(params.seed) + if world_size > 1: + setup_dist(rank, world_size, params.master_port) + + setup_logger(f"{params.exp_dir}/log/log-train") + logging.info("Training started") + logging.info(params) + + if args.tensorboard and rank == 0: + tb_writer = SummaryWriter(log_dir=f"{params.exp_dir}/tensorboard") + else: + tb_writer = None + + device = torch.device("cpu") + if torch.cuda.is_available(): + device = torch.device("cuda", rank) + + graph_compiler = OtcTrainingGraphCompiler( + params.lang_dir, + otc_token=params.otc_token, + device=device, + initial_bypass_weight=params.initial_bypass_weight, + initial_self_loop_weight=params.initial_self_loop_weight, + bypass_weight_decay=params.bypass_weight_decay, + self_loop_weight_decay=params.self_loop_weight_decay, + ) + + # remove OTC token as it is the average of all non-blank tokens + max_token_id = graph_compiler.get_max_token_id() - 1 + # add blank + num_classes = max_token_id + 1 + + logging.info("About to create model") + model = Conformer( + num_features=params.feature_dim, + nhead=params.nhead, + d_model=params.encoder_dim, + num_classes=num_classes, + subsampling_factor=params.subsampling_factor, + num_encoder_layers=params.num_encoder_layers, + num_decoder_layers=params.num_decoder_layers, + ) + + print(model) + + num_param = sum([p.numel() for p in model.parameters()]) + logging.info(f"Number of model parameters: {num_param}") + + assert params.save_every_n >= params.average_period + model_avg: Optional[nn.Module] = None + if rank == 0: + # model_avg is only used with rank 0 + model_avg = copy.deepcopy(model) + + assert params.start_epoch > 0, params.start_epoch + checkpoints = load_checkpoint_if_available( + params=params, model=model, model_avg=model_avg + ) + + model.to(device) + if world_size > 1: + logging.info("Using DDP") + model = DDP(model, device_ids=[rank]) + + optimizer = Eve(model.parameters(), lr=params.initial_lr) + + scheduler = Eden(optimizer, params.lr_batches, params.lr_epochs) + + if checkpoints and "optimizer" in checkpoints: + logging.info("Loading optimizer state dict") + optimizer.load_state_dict(checkpoints["optimizer"]) + + if ( + checkpoints + and "scheduler" in checkpoints + and checkpoints["scheduler"] is not None + ): + logging.info("Loading scheduler state dict") + scheduler.load_state_dict(checkpoints["scheduler"]) + + if params.print_diagnostics: + diagnostic = diagnostics.attach_diagnostics(model) + + librispeech = LibriSpeechAsrDataModule(args) + + train_cuts = librispeech.train_clean_100_cuts() + + def remove_short_and_long_utt(c: Cut): + # Keep only utterances with duration between 1 second and 20 seconds + # + # Caution: There is a reason to select 20.0 here. Please see + # ../local/display_manifest_statistics.py + # + # You should use ../local/display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + return 1.0 <= c.duration <= 20.0 + + train_cuts = train_cuts.filter(remove_short_and_long_utt) + + if params.start_batch > 0 and checkpoints and "sampler" in checkpoints: + # We only load the sampler's state dict when it loads a checkpoint + # saved in the middle of an epoch + sampler_state_dict = checkpoints["sampler"] + else: + sampler_state_dict = None + + train_dl = librispeech.train_dataloaders( + train_cuts, sampler_state_dict=sampler_state_dict + ) + + valid_cuts = librispeech.dev_clean_cuts() + valid_cuts += librispeech.dev_other_cuts() + valid_dl = librispeech.valid_dataloaders(valid_cuts) + + if params.print_diagnostics: + scan_pessimistic_batches_for_oom( + model=model, + train_dl=train_dl, + optimizer=optimizer, + graph_compiler=graph_compiler, + params=params, + ) + + scaler = GradScaler(enabled=params.use_fp16) + if checkpoints and "grad_scaler" in checkpoints: + logging.info("Loading grad scaler state dict") + scaler.load_state_dict(checkpoints["grad_scaler"]) + + for epoch in range(params.start_epoch, params.num_epochs + 1): + scheduler.step_epoch(epoch - 1) + fix_random_seed(params.seed + epoch - 1) + train_dl.sampler.set_epoch(epoch - 1) + + if tb_writer is not None: + tb_writer.add_scalar("train/epoch", epoch, params.batch_idx_train) + + params.cur_epoch = epoch + + train_one_epoch( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + graph_compiler=graph_compiler, + scheduler=scheduler, + train_dl=train_dl, + valid_dl=valid_dl, + scaler=scaler, + tb_writer=tb_writer, + world_size=world_size, + rank=rank, + ) + + if params.print_diagnostics: + diagnostic.print_diagnostics() + break + + save_checkpoint( + params=params, + model=model, + model_avg=model_avg, + optimizer=optimizer, + scheduler=scheduler, + sampler=train_dl.sampler, + scaler=scaler, + rank=rank, + ) + + logging.info("Done!") + + if world_size > 1: + torch.distributed.barrier() + cleanup_dist() + + +def scan_pessimistic_batches_for_oom( + model: Union[nn.Module, DDP], + train_dl: torch.utils.data.DataLoader, + optimizer: torch.optim.Optimizer, + graph_compiler: OtcTrainingGraphCompiler, + params: AttributeDict, +): + from lhotse.dataset import find_pessimistic_batches + + logging.info( + "Sanity check -- see if any of the batches in epoch 1 would cause OOM." + ) + batches, crit_values = find_pessimistic_batches(train_dl.sampler) + for criterion, cuts in batches.items(): + batch = train_dl.dataset[cuts] + try: + # warmup = 0.0 is so that the derivs for the pruned loss stay zero + # (i.e. are not remembered by the decaying-average in adam), because + # we want to avoid these params being subject to shrinkage in adam. + with torch.cuda.amp.autocast(enabled=params.use_fp16): + loss, _ = compute_loss( + params=params, + model=model, + batch=batch, + graph_compiler=graph_compiler, + is_training=True, + warmup=0.0, + ) + loss.backward() + optimizer.step() + optimizer.zero_grad() + except RuntimeError as e: + if "CUDA out of memory" in str(e): + logging.error( + "Your GPU ran out of memory with the current " + "max_duration setting. We recommend decreasing " + "max_duration and trying again.\n" + f"Failing criterion: {criterion} " + f"(={crit_values[criterion]}) ..." + ) + raise + + +def main(): + parser = get_parser() + LibriSpeechAsrDataModule.add_arguments(parser) + args = parser.parse_args() + args.exp_dir = Path(args.exp_dir) + assert "▁" not in args.otc_token + args.otc_token = f"▁{args.otc_token}" + + world_size = args.world_size + assert world_size >= 1 + if world_size > 1: + mp.spawn(run, args=(world_size, args), nprocs=world_size, join=True) + else: + run(rank=0, world_size=1, args=args) + + +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/conformer_ctc2/transformer.py b/egs/librispeech/WSASR/conformer_ctc2/transformer.py new file mode 100644 index 000000000..41e6cd357 --- /dev/null +++ b/egs/librispeech/WSASR/conformer_ctc2/transformer.py @@ -0,0 +1,1055 @@ +# Copyright 2021 University of Chinese Academy of Sciences (author: Han Zhu) +# Copyright 2022 Xiaomi Corp. (author: Quandong Wang) +# 2023 Johns Hopkins University (author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 math +from typing import Dict, List, Optional, Tuple + +import torch +import torch.nn as nn +from attention import MultiheadAttention +from label_smoothing import LabelSmoothingLoss +from scaling import ( + ActivationBalancer, + BasicNorm, + DoubleSwish, + ScaledEmbedding, + ScaledLinear, +) +from subsampling import Conv2dSubsampling +from torch.nn.utils.rnn import pad_sequence + +# Note: TorchScript requires Dict/List/etc. to be fully typed. +Supervisions = Dict[str, torch.Tensor] + + +class Transformer(nn.Module): + def __init__( + self, + num_features: int, + num_classes: int, + subsampling_factor: int = 4, + d_model: int = 256, + nhead: int = 4, + dim_feedforward: int = 2048, + num_encoder_layers: int = 12, + num_decoder_layers: int = 6, + dropout: float = 0.1, + layer_dropout: float = 0.075, + ) -> None: + """ + Args: + num_features: + The input dimension of the model. + num_classes: + The output dimension of the model. + subsampling_factor: + Number of output frames is num_in_frames // subsampling_factor. + Currently, subsampling_factor MUST be 4. + d_model: + Attention dimension. + nhead: + Number of heads in multi-head attention. + Must satisfy d_model // nhead == 0. + dim_feedforward: + The output dimension of the feedforward layers in encoder/decoder. + num_encoder_layers: + Number of encoder layers. + num_decoder_layers: + Number of decoder layers. + dropout: + Dropout in encoder/decoder. + layer_dropout (float): layer-dropout rate. + """ + super().__init__() + + self.num_features = num_features + self.num_classes = num_classes + self.subsampling_factor = subsampling_factor + if subsampling_factor != 4 and subsampling_factor != 2: + raise NotImplementedError("Support only 'subsampling_factor=4 or 2'.") + + # self.encoder_embed converts the input of shape (N, T, num_classes) + # to the shape (N, T//subsampling_factor, d_model). + # That is, it does two things simultaneously: + # (1) subsampling: T -> T//subsampling_factor + # (2) embedding: num_classes -> d_model + self.encoder_embed = Conv2dSubsampling(num_features, d_model) + + self.encoder_pos = PositionalEncoding(d_model, dropout) + + encoder_layer = TransformerEncoderLayer( + d_model=d_model, + nhead=nhead, + dim_feedforward=dim_feedforward, + dropout=dropout, + layer_dropout=layer_dropout, + ) + + self.encoder = TransformerEncoder( + encoder_layer=encoder_layer, + num_layers=num_encoder_layers, + ) + + # TODO(fangjun): remove dropout + self.encoder_output_layer = nn.Sequential( + nn.Dropout(p=dropout), ScaledLinear(d_model, num_classes, bias=True) + ) + + if num_decoder_layers > 0: + self.decoder_num_class = ( + self.num_classes + ) # bpe model already has sos/eos symbol + + self.decoder_embed = ScaledEmbedding( + num_embeddings=self.decoder_num_class, embedding_dim=d_model + ) + self.decoder_pos = PositionalEncoding(d_model, dropout) + + decoder_layer = TransformerDecoderLayer( + d_model=d_model, + nhead=nhead, + dim_feedforward=dim_feedforward, + dropout=dropout, + ) + + self.decoder = TransformerDecoder( + decoder_layer=decoder_layer, + num_layers=num_decoder_layers, + ) + + self.decoder_output_layer = ScaledLinear( + d_model, self.decoder_num_class, bias=True + ) + + self.decoder_criterion = LabelSmoothingLoss() + else: + self.decoder_criterion = None + + def forward( + self, + x: torch.Tensor, + supervision: Optional[Supervisions] = None, + warmup: float = 1.0, + ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]: + """ + Args: + x: + The input tensor. Its shape is (N, T, C). + supervision: + Supervision in lhotse format. + See https://github.com/lhotse-speech/lhotse/blob/master/lhotse/dataset/speech_recognition.py#L32 # noqa + (CAUTION: It contains length information, i.e., start and number of + frames, before subsampling) + warmup: + A floating point value that gradually increases from 0 throughout + training; when it is >= 1.0 we are "fully warmed up". It is used + to turn modules on sequentially. + + Returns: + Return a tuple containing 3 tensors: + - CTC output for ctc decoding. Its shape is (N, T, C) + - Encoder output with shape (T, N, C). It can be used as key and + value for the decoder. + - Encoder output padding mask. It can be used as + memory_key_padding_mask for the decoder. Its shape is (N, T). + It is None if `supervision` is None. + """ + + encoder_memory, memory_key_padding_mask = self.run_encoder( + x, supervision, warmup + ) + + x = self.ctc_output(encoder_memory) + return x, encoder_memory, memory_key_padding_mask + + def run_encoder( + self, + x: torch.Tensor, + supervisions: Optional[Supervisions] = None, + warmup: float = 1.0, + ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: + """Run the transformer encoder. + + Args: + x: + The model input. Its shape is (N, T, C). + supervisions: + Supervision in lhotse format. + See https://github.com/lhotse-speech/lhotse/blob/master/lhotse/dataset/speech_recognition.py#L32 # noqa + CAUTION: It contains length information, i.e., start and number of + frames, before subsampling + It is read directly from the batch, without any sorting. It is used + to compute the encoder padding mask, which is used as memory key + padding mask for the decoder. + Returns: + Return a tuple with two tensors: + - The encoder output, with shape (T, N, C) + - encoder padding mask, with shape (N, T). + The mask is None if `supervisions` is None. + It is used as memory key padding mask in the decoder. + """ + x = self.encoder_embed(x) + x = self.encoder_pos(x) + x = x.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + mask = encoder_padding_mask(x.size(0), supervisions) + mask = mask.to(x.device) if mask is not None else None + x = self.encoder(x, src_key_padding_mask=mask, warmup=warmup) # (T, N, C) + + return x, mask + + def ctc_output(self, x: torch.Tensor) -> torch.Tensor: + """ + Args: + x: + The output tensor from the transformer encoder. + Its shape is (T, N, C) + + Returns: + Return a tensor that can be used for CTC decoding. + Its shape is (N, T, C) + """ + x = self.encoder_output_layer(x) + x = x.permute(1, 0, 2) # (T, N, C) ->(N, T, C) + x = nn.functional.log_softmax(x, dim=-1) # (N, T, C) + return x + + @torch.jit.export + def decoder_forward( + self, + memory: torch.Tensor, + memory_key_padding_mask: torch.Tensor, + token_ids: List[List[int]], + sos_id: int, + eos_id: int, + ) -> torch.Tensor: + """ + Args: + memory: + It's the output of the encoder with shape (T, N, C) + memory_key_padding_mask: + The padding mask from the encoder. + token_ids: + A list-of-list IDs. Each sublist contains IDs for an utterance. + The IDs can be either phone IDs or word piece IDs. + sos_id: + sos token id + eos_id: + eos token id + + Returns: + A scalar, the **sum** of label smoothing loss over utterances + in the batch without any normalization. + """ + ys_in = add_sos(token_ids, sos_id=sos_id) + ys_in = [torch.tensor(y) for y in ys_in] + ys_in_pad = pad_sequence(ys_in, batch_first=True, padding_value=float(eos_id)) + + ys_out = add_eos(token_ids, eos_id=eos_id) + ys_out = [torch.tensor(y) for y in ys_out] + ys_out_pad = pad_sequence(ys_out, batch_first=True, padding_value=float(-1)) + + device = memory.device + ys_in_pad = ys_in_pad.to(device) + ys_out_pad = ys_out_pad.to(device) + + tgt_mask = generate_square_subsequent_mask(ys_in_pad.shape[-1]).to(device) + + tgt_key_padding_mask = decoder_padding_mask(ys_in_pad, ignore_id=eos_id) + # TODO: Use length information to create the decoder padding mask + # We set the first column to False since the first column in ys_in_pad + # contains sos_id, which is the same as eos_id in our current setting. + tgt_key_padding_mask[:, 0] = False + + tgt = self.decoder_embed(ys_in_pad) # (N, T) -> (N, T, C) + tgt = self.decoder_pos(tgt) + tgt = tgt.permute(1, 0, 2) # (N, T, C) -> (T, N, C) + pred_pad = self.decoder( + tgt=tgt, + memory=memory, + tgt_mask=tgt_mask, + tgt_key_padding_mask=tgt_key_padding_mask, + memory_key_padding_mask=memory_key_padding_mask, + ) # (T, N, C) + pred_pad = pred_pad.permute(1, 0, 2) # (T, N, C) -> (N, T, C) + pred_pad = self.decoder_output_layer(pred_pad) # (N, T, C) + + decoder_loss = self.decoder_criterion(pred_pad, ys_out_pad) + + return decoder_loss + + @torch.jit.export + def decoder_nll( + self, + memory: torch.Tensor, + memory_key_padding_mask: torch.Tensor, + token_ids: List[torch.Tensor], + sos_id: int, + eos_id: int, + ) -> torch.Tensor: + """ + Args: + memory: + It's the output of the encoder with shape (T, N, C) + memory_key_padding_mask: + The padding mask from the encoder. + token_ids: + A list-of-list IDs (e.g., word piece IDs). + Each sublist represents an utterance. + sos_id: + The token ID for SOS. + eos_id: + The token ID for EOS. + Returns: + A 2-D tensor of shape (len(token_ids), max_token_length) + representing the cross entropy loss (i.e., negative log-likelihood). + """ + # The common part between this function and decoder_forward could be + # extracted as a separate function. + if isinstance(token_ids[0], torch.Tensor): + # This branch is executed by torchscript in C++. + # See https://github.com/k2-fsa/k2/pull/870 + # https://github.com/k2-fsa/k2/blob/3c1c18400060415b141ccea0115fd4bf0ad6234e/k2/torch/bin/attention_rescore.cu#L286 + token_ids = [tolist(t) for t in token_ids] + + ys_in = add_sos(token_ids, sos_id=sos_id) + ys_in = [torch.tensor(y) for y in ys_in] + ys_in_pad = pad_sequence(ys_in, batch_first=True, padding_value=float(eos_id)) + + ys_out = add_eos(token_ids, eos_id=eos_id) + ys_out = [torch.tensor(y) for y in ys_out] + ys_out_pad = pad_sequence(ys_out, batch_first=True, padding_value=float(-1)) + + device = memory.device + ys_in_pad = ys_in_pad.to(device, dtype=torch.int64) + ys_out_pad = ys_out_pad.to(device, dtype=torch.int64) + + tgt_mask = generate_square_subsequent_mask(ys_in_pad.shape[-1]).to(device) + + tgt_key_padding_mask = decoder_padding_mask(ys_in_pad, ignore_id=eos_id) + # TODO: Use length information to create the decoder padding mask + # We set the first column to False since the first column in ys_in_pad + # contains sos_id, which is the same as eos_id in our current setting. + tgt_key_padding_mask[:, 0] = False + + tgt = self.decoder_embed(ys_in_pad) # (B, T) -> (B, T, F) + tgt = self.decoder_pos(tgt) + tgt = tgt.permute(1, 0, 2) # (B, T, F) -> (T, B, F) + pred_pad = self.decoder( + tgt=tgt, + memory=memory, + tgt_mask=tgt_mask, + tgt_key_padding_mask=tgt_key_padding_mask, + memory_key_padding_mask=memory_key_padding_mask, + ) # (T, B, F) + pred_pad = pred_pad.permute(1, 0, 2) # (T, B, F) -> (B, T, F) + pred_pad = self.decoder_output_layer(pred_pad) # (B, T, F) + # nll: negative log-likelihood + nll = torch.nn.functional.cross_entropy( + pred_pad.view(-1, self.decoder_num_class), + ys_out_pad.view(-1), + ignore_index=-1, + reduction="none", + ) + + nll = nll.view(pred_pad.shape[0], -1) + + return nll + + +class TransformerEncoderLayer(nn.Module): + """ + Modified from torch.nn.TransformerEncoderLayer. + + Args: + d_model: + the number of expected features in the input (required). + nhead: + the number of heads in the multiheadattention models (required). + dim_feedforward: + the dimension of the feedforward network model (default=2048). + dropout: + the dropout value (default=0.1). + activation: + the activation function of intermediate layer, relu or + gelu (default=relu). + + Examples:: + >>> encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8) + >>> src = torch.rand(10, 32, 512) + >>> out = encoder_layer(src) + """ + + def __init__( + self, + d_model: int, + nhead: int, + dim_feedforward: int = 2048, + dropout: float = 0.1, + layer_dropout: float = 0.075, + ) -> None: + super(TransformerEncoderLayer, self).__init__() + + self.layer_dropout = layer_dropout + + self.self_attn = MultiheadAttention(d_model, nhead, dropout=0.0) + # Implementation of Feedforward model + + self.feed_forward = nn.Sequential( + ScaledLinear(d_model, dim_feedforward), + ActivationBalancer(channel_dim=-1), + DoubleSwish(), + nn.Dropout(dropout), + ScaledLinear(dim_feedforward, d_model, initial_scale=0.25), + ) + + self.norm_final = BasicNorm(d_model) + + # try to ensure the output is close to zero-mean (or at least, zero-median). + self.balancer = ActivationBalancer( + channel_dim=-1, min_positive=0.45, max_positive=0.55, max_abs=6.0 + ) + + self.dropout = nn.Dropout(dropout) + + def forward( + self, + src: torch.Tensor, + src_mask: Optional[torch.Tensor] = None, + src_key_padding_mask: Optional[torch.Tensor] = None, + warmup: float = 1.0, + ) -> torch.Tensor: + """ + Pass the input through the encoder layer. + + Args: + src: the sequence to the encoder layer (required). + src_mask: the mask for the src sequence (optional). + src_key_padding_mask: the mask for the src keys per batch (optional) + warmup: controls selective bypass of of layers; if < 1.0, we will + bypass layers more frequently. + + Shape: + src: (S, N, E). + src_mask: (S, S). + src_key_padding_mask: (N, S). + S is the source sequence length, T is the target sequence length, + N is the batch size, E is the feature number + """ + src_orig = src + + warmup_scale = min(0.1 + warmup, 1.0) + # alpha = 1.0 means fully use this encoder layer, 0.0 would mean + # completely bypass it. + if self.training: + alpha = ( + warmup_scale + if torch.rand(()).item() <= (1.0 - self.layer_dropout) + else 0.1 + ) + else: + alpha = 1.0 + + # src_att = self.self_attn(src, src, src, src_mask) + src_att = self.self_attn( + src, + src, + src, + attn_mask=src_mask, + key_padding_mask=src_key_padding_mask, + )[0] + src = src + self.dropout(src_att) + + src = src + self.dropout(self.feed_forward(src)) + + src = self.norm_final(self.balancer(src)) + + if alpha != 1.0: + src = alpha * src + (1 - alpha) * src_orig + + return src + + +class TransformerDecoderLayer(nn.Module): + """ + Modified from torch.nn.TransformerDecoderLayer. + Add support of normalize_before, + i.e., use layer_norm before the first block. + + Args: + d_model: + the number of expected features in the input (required). + nhead: + the number of heads in the multiheadattention models (required). + dim_feedforward: + the dimension of the feedforward network model (default=2048). + dropout: + the dropout value (default=0.1). + activation: + the activation function of intermediate layer, relu or + gelu (default=relu). + + Examples:: + >>> decoder_layer = nn.TransformerDecoderLayer(d_model=512, nhead=8) + >>> memory = torch.rand(10, 32, 512) + >>> tgt = torch.rand(20, 32, 512) + >>> out = decoder_layer(tgt, memory) + """ + + def __init__( + self, + d_model: int, + nhead: int, + dim_feedforward: int = 2048, + dropout: float = 0.1, + layer_dropout: float = 0.075, + normalize_before: bool = True, + ) -> None: + super(TransformerDecoderLayer, self).__init__() + self.layer_dropout = layer_dropout + self.self_attn = MultiheadAttention(d_model, nhead, dropout=0.0) + self.src_attn = MultiheadAttention(d_model, nhead, dropout=0.0) + # Implementation of Feedforward model + self.feed_forward = nn.Sequential( + ScaledLinear(d_model, dim_feedforward), + ActivationBalancer(channel_dim=-1), + DoubleSwish(), + nn.Dropout(dropout), + ScaledLinear(dim_feedforward, d_model, initial_scale=0.25), + ) + + self.norm_final = BasicNorm(d_model) + + # try to ensure the output is close to zero-mean (or at least, zero-median). + self.balancer = ActivationBalancer( + channel_dim=-1, min_positive=0.45, max_positive=0.55, max_abs=6.0 + ) + + self.dropout = nn.Dropout(dropout) + + def forward( + self, + tgt: torch.Tensor, + memory: torch.Tensor, + tgt_mask: Optional[torch.Tensor] = None, + memory_mask: Optional[torch.Tensor] = None, + tgt_key_padding_mask: Optional[torch.Tensor] = None, + memory_key_padding_mask: Optional[torch.Tensor] = None, + warmup: float = 1.0, + ) -> torch.Tensor: + """Pass the inputs (and mask) through the decoder layer. + + Args: + tgt: + the sequence to the decoder layer (required). + memory: + the sequence from the last layer of the encoder (required). + tgt_mask: + the mask for the tgt sequence (optional). + memory_mask: + the mask for the memory sequence (optional). + tgt_key_padding_mask: + the mask for the tgt keys per batch (optional). + memory_key_padding_mask: + the mask for the memory keys per batch (optional). + warmup: controls selective bypass of of layers; if < 1.0, we will + bypass layers more frequently. + + + + Shape: + tgt: (T, N, E). + memory: (S, N, E). + tgt_mask: (T, T). + memory_mask: (T, S). + tgt_key_padding_mask: (N, T). + memory_key_padding_mask: (N, S). + S is the source sequence length, T is the target sequence length, + N is the batch size, E is the feature number + """ + tgt_orig = tgt + + warmup_scale = min(0.1 + warmup, 1.0) + # alpha = 1.0 means fully use this encoder layer, 0.0 would mean + # completely bypass it. + if self.training: + alpha = ( + warmup_scale + if torch.rand(()).item() <= (1.0 - self.layer_dropout) + else 0.1 + ) + else: + alpha = 1.0 + + # tgt_att = self.self_attn(tgt, tgt, tgt, tgt_mask) + tgt_att = self.self_attn( + tgt, + tgt, + tgt, + attn_mask=tgt_mask, + key_padding_mask=tgt_key_padding_mask, + )[0] + tgt = tgt + self.dropout(tgt_att) + + # src_att = self.src_attn(tgt, memory, memory, memory_mask) + src_att = self.src_attn( + tgt, + memory, + memory, + attn_mask=memory_mask, + key_padding_mask=memory_key_padding_mask, + )[0] + tgt = tgt + self.dropout(src_att) + + tgt = tgt + self.dropout(self.feed_forward(tgt)) + + tgt = self.norm_final(self.balancer(tgt)) + + if alpha != 1.0: + tgt = alpha * tgt + (1 - alpha) * tgt_orig + + return tgt + + +class TransformerEncoder(nn.Module): + r"""TransformerEncoder is a stack of N encoder layers + + Args: + encoder_layer: an instance of the TransformerEncoderLayer() class (required). + num_layers: the number of sub-encoder-layers in the encoder (required). + + Examples:: + >>> encoder_layer = TransformerEncoderLayer(d_model=512, nhead=8) + >>> transformer_encoder = TransformerEncoder(encoder_layer, num_layers=6) + >>> src = torch.rand(10, 32, 512) + >>> out = transformer_encoder(src) + """ + + def __init__(self, encoder_layer: nn.Module, num_layers: int) -> None: + super().__init__() + self.layers = nn.ModuleList( + [copy.deepcopy(encoder_layer) for i in range(num_layers)] + ) + self.num_layers = num_layers + + def forward( + self, + src: torch.Tensor, + mask: Optional[torch.Tensor] = None, + src_key_padding_mask: Optional[torch.Tensor] = None, + warmup: float = 1.0, + ) -> torch.Tensor: + r"""Pass the input through the encoder layers in turn. + + Args: + src: the sequence to the encoder (required). + mask: the mask for the src sequence (optional). + src_key_padding_mask: the mask for the src keys per batch (optional). + + Shape: + src: (S, N, E). + mask: (S, S). + src_key_padding_mask: (N, S). + S is the source sequence length, T is the target sequence length, N is the batch size, E is the feature number + + """ + output = src + + for mod in self.layers: + output = mod( + output, + src_mask=mask, + src_key_padding_mask=src_key_padding_mask, + warmup=warmup, + ) + + return output + + +class TransformerDecoder(nn.Module): + r"""TransformerDecoder is a stack of N decoder layers + + Args: + decoder_layer: an instance of the TransformerDecoderLayer() class (required). + num_layers: the number of sub-decoder-layers in the decoder (required). + + Examples:: + >>> decoder_layer = TransformerDecoderLayer(d_model=512, nhead=8) + >>> transformer_decoder = TransformerDecoder(decoder_layer, num_layers=6) + >>> memory = torch.rand(10, 32, 512) + >>> tgt = torch.rand(10, 32, 512) + >>> out = transformer_decoder(tgt, memory) + """ + + def __init__(self, decoder_layer: nn.Module, num_layers: int) -> None: + super().__init__() + self.layers = nn.ModuleList( + [copy.deepcopy(decoder_layer) for i in range(num_layers)] + ) + self.num_layers = num_layers + + def forward( + self, + tgt: torch.Tensor, + memory: torch.Tensor, + tgt_mask: Optional[torch.Tensor] = None, + memory_mask: Optional[torch.Tensor] = None, + tgt_key_padding_mask: Optional[torch.Tensor] = None, + memory_key_padding_mask: Optional[torch.Tensor] = None, + warmup: float = 1.0, + ) -> torch.Tensor: + r"""Pass the input through the decoder layers in turn. + + Args: + tgt: the sequence to the decoder (required). + memory: the sequence from the last layer of the encoder (required). + tgt_mask: the mask for the tgt sequence (optional). + memory_mask: the mask for the memory sequence (optional). + tgt_key_padding_mask: the mask for the tgt keys per batch (optional). + memory_key_padding_mask: the mask for the memory keys per batch (optional). + + Shape: + tgt: (S, N, E). + tgt_mask: (S, S). + tgt_key_padding_mask: (N, S). + + """ + output = tgt + + for mod in self.layers: + output = mod( + output, + memory, + tgt_mask=tgt_mask, + memory_mask=memory_mask, + tgt_key_padding_mask=tgt_key_padding_mask, + memory_key_padding_mask=memory_key_padding_mask, + warmup=warmup, + ) + + return output + + +class PositionalEncoding(nn.Module): + """This class implements the positional encoding + proposed in the following paper: + + - Attention Is All You Need: https://arxiv.org/pdf/1706.03762.pdf + + PE(pos, 2i) = sin(pos / (10000^(2i/d_modle)) + PE(pos, 2i+1) = cos(pos / (10000^(2i/d_modle)) + + Note:: + + 1 / (10000^(2i/d_model)) = exp(-log(10000^(2i/d_model))) + = exp(-1* 2i / d_model * log(100000)) + = exp(2i * -(log(10000) / d_model)) + """ + + def __init__(self, d_model: int, dropout: float = 0.1) -> None: + """ + Args: + d_model: + Embedding dimension. + dropout: + Dropout probability to be applied to the output of this module. + """ + super().__init__() + self.d_model = d_model + self.xscale = math.sqrt(self.d_model) + self.dropout = nn.Dropout(p=dropout) + # not doing: self.pe = None because of errors thrown by torchscript + self.pe = torch.zeros(1, 0, self.d_model, dtype=torch.float32) + + def extend_pe(self, x: torch.Tensor) -> None: + """Extend the time t in the positional encoding if required. + + The shape of `self.pe` is (1, T1, d_model). The shape of the input x + is (N, T, d_model). If T > T1, then we change the shape of self.pe + to (N, T, d_model). Otherwise, nothing is done. + + Args: + x: + It is a tensor of shape (N, T, C). + Returns: + Return None. + """ + if self.pe is not None: + if self.pe.size(1) >= x.size(1): + self.pe = self.pe.to(dtype=x.dtype, device=x.device) + return + pe = torch.zeros(x.size(1), self.d_model, dtype=torch.float32) + position = torch.arange(0, x.size(1), dtype=torch.float32).unsqueeze(1) + div_term = torch.exp( + torch.arange(0, self.d_model, 2, dtype=torch.float32) + * -(math.log(10000.0) / self.d_model) + ) + pe[:, 0::2] = torch.sin(position * div_term) + pe[:, 1::2] = torch.cos(position * div_term) + pe = pe.unsqueeze(0) + # Now pe is of shape (1, T, d_model), where T is x.size(1) + self.pe = pe.to(device=x.device, dtype=x.dtype) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """ + Add positional encoding. + + Args: + x: + Its shape is (N, T, C) + + Returns: + Return a tensor of shape (N, T, C) + """ + self.extend_pe(x) + x = x * self.xscale + self.pe[:, : x.size(1), :] + return self.dropout(x) + + +class Noam(object): + """ + Implements Noam optimizer. + + Proposed in + "Attention Is All You Need", https://arxiv.org/pdf/1706.03762.pdf + + Modified from + https://github.com/espnet/espnet/blob/master/espnet/nets/pytorch_backend/transformer/optimizer.py # noqa + + Args: + params: + iterable of parameters to optimize or dicts defining parameter groups + model_size: + attention dimension of the transformer model + factor: + learning rate factor + warm_step: + warmup steps + """ + + def __init__( + self, + params, + model_size: int = 256, + factor: float = 10.0, + warm_step: int = 25000, + weight_decay=0, + ) -> None: + """Construct an Noam object.""" + self.optimizer = torch.optim.Adam( + params, lr=0, betas=(0.9, 0.98), eps=1e-9, weight_decay=weight_decay + ) + self._step = 0 + self.warmup = warm_step + self.factor = factor + self.model_size = model_size + self._rate = 0 + + @property + def param_groups(self): + """Return param_groups.""" + return self.optimizer.param_groups + + def step(self): + """Update parameters and rate.""" + self._step += 1 + rate = self.rate() + for p in self.optimizer.param_groups: + p["lr"] = rate + self._rate = rate + self.optimizer.step() + + def rate(self, step=None): + """Implement `lrate` above.""" + if step is None: + step = self._step + return ( + self.factor + * self.model_size ** (-0.5) + * min(step ** (-0.5), step * self.warmup ** (-1.5)) + ) + + def zero_grad(self): + """Reset gradient.""" + self.optimizer.zero_grad() + + def state_dict(self): + """Return state_dict.""" + return { + "_step": self._step, + "warmup": self.warmup, + "factor": self.factor, + "model_size": self.model_size, + "_rate": self._rate, + "optimizer": self.optimizer.state_dict(), + } + + def load_state_dict(self, state_dict): + """Load state_dict.""" + for key, value in state_dict.items(): + if key == "optimizer": + self.optimizer.load_state_dict(state_dict["optimizer"]) + else: + setattr(self, key, value) + + +def encoder_padding_mask( + max_len: int, + subsampling_factor: Optional[int] = 4, + supervisions: Optional[Supervisions] = None, +) -> Optional[torch.Tensor]: + """Make mask tensor containing indexes of padded part. + + TODO:: + This function **assumes** that the model uses + a subsampling factor of 4 or 2. We should remove that + assumption later. + + Args: + max_len: + Maximum length of input features. + CAUTION: It is the length after subsampling. + supervisions: + Supervision in lhotse format. + See https://github.com/lhotse-speech/lhotse/blob/master/lhotse/dataset/speech_recognition.py#L32 # noqa + (CAUTION: It contains length information, i.e., start and number of + frames, before subsampling) + + Returns: + Tensor: Mask tensor of dimension (batch_size, input_length), + True denote the masked indices. + """ + if supervisions is None: + return None + + supervision_segments = torch.stack( + ( + supervisions["sequence_idx"], + supervisions["start_frame"], + supervisions["num_frames"], + ), + 1, + ).to(torch.int32) + + lengths = [0 for _ in range(int(supervision_segments[:, 0].max().item()) + 1)] + for idx in range(supervision_segments.size(0)): + # Note: TorchScript doesn't allow to unpack tensors as tuples + sequence_idx = supervision_segments[idx, 0].item() + start_frame = supervision_segments[idx, 1].item() + num_frames = supervision_segments[idx, 2].item() + lengths[sequence_idx] = start_frame + num_frames + + if subsampling_factor == 4: + lengths = [((i - 1) // 2 - 1) // 2 for i in lengths] + elif subsampling_factor == 2: + lengths = [(i - 1) // 2 - 2 for i in lengths] + bs = int(len(lengths)) + seq_range = torch.arange(0, max_len, dtype=torch.int64) + seq_range_expand = seq_range.unsqueeze(0).expand(bs, max_len) + # Note: TorchScript doesn't implement Tensor.new() + seq_length_expand = torch.tensor( + lengths, device=seq_range_expand.device, dtype=seq_range_expand.dtype + ).unsqueeze(-1) + mask = seq_range_expand >= seq_length_expand + + return mask + + +def decoder_padding_mask(ys_pad: torch.Tensor, ignore_id: int = -1) -> torch.Tensor: + """Generate a length mask for input. + + The masked position are filled with True, + Unmasked positions are filled with False. + + Args: + ys_pad: + padded tensor of dimension (batch_size, input_length). + ignore_id: + the ignored number (the padding number) in ys_pad + + Returns: + Tensor: + a bool tensor of the same shape as the input tensor. + """ + ys_mask = ys_pad == ignore_id + return ys_mask + + +def generate_square_subsequent_mask(sz: int) -> torch.Tensor: + """Generate a square mask for the sequence. The masked positions are + filled with float('-inf'). Unmasked positions are filled with float(0.0). + The mask can be used for masked self-attention. + + For instance, if sz is 3, it returns:: + + tensor([[0., -inf, -inf], + [0., 0., -inf], + [0., 0., 0]]) + + Args: + sz: mask size + + Returns: + A square mask of dimension (sz, sz) + """ + mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1) + mask = ( + mask.float() + .masked_fill(mask == 0, float("-inf")) + .masked_fill(mask == 1, float(0.0)) + ) + return mask + + +def add_sos(token_ids: List[List[int]], sos_id: int) -> List[List[int]]: + """Prepend sos_id to each utterance. + + Args: + token_ids: + A list-of-list of token IDs. Each sublist contains + token IDs (e.g., word piece IDs) of an utterance. + sos_id: + The ID of the SOS token. + + Return: + Return a new list-of-list, where each sublist starts + with SOS ID. + """ + return [[sos_id] + utt for utt in token_ids] + + +def add_eos(token_ids: List[List[int]], eos_id: int) -> List[List[int]]: + """Append eos_id to each utterance. + + Args: + token_ids: + A list-of-list of token IDs. Each sublist contains + token IDs (e.g., word piece IDs) of an utterance. + eos_id: + The ID of the EOS token. + + Return: + Return a new list-of-list, where each sublist ends + with EOS ID. + """ + return [utt + [eos_id] for utt in token_ids] + + +def tolist(t: torch.Tensor) -> List[int]: + """Used by jit""" + return torch.jit.annotate(List[int], t.tolist()) diff --git a/egs/librispeech/WSASR/figures/del.png b/egs/librispeech/WSASR/figures/del.png new file mode 100644 index 0000000000000000000000000000000000000000..38973980bec204a74d0c97730921c7e29c4ff9c2 GIT binary patch literal 14544 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 literal 0 HcmV?d00001 diff --git a/egs/librispeech/WSASR/figures/ins.png b/egs/librispeech/WSASR/figures/ins.png new file mode 100644 index 0000000000000000000000000000000000000000..2d0e807a971cc89537582178befe3a0f0d1d4ac6 GIT binary patch literal 16947 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 literal 0 HcmV?d00001 diff --git a/egs/librispeech/WSASR/figures/otc_g.png b/egs/librispeech/WSASR/figures/otc_g.png new file mode 100644 index 0000000000000000000000000000000000000000..ebad4918023445f482fce41aec6fe44447c9d978 GIT binary patch literal 33339 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 literal 0 HcmV?d00001 diff --git a/egs/librispeech/WSASR/figures/sub.png b/egs/librispeech/WSASR/figures/sub.png new file mode 100644 index 0000000000000000000000000000000000000000..5674e9febf3faabfce52a1491adcaddeb066f4fa GIT binary patch literal 15900 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 k2.Fsa: + """ + Args: + lang_dir: + The language directory, e.g., data/lang_phone or data/lang_bpe_5000. + lm: + The language stem base name. + + Return: + An FSA representing HLG. + """ + lexicon = Lexicon(lang_dir) + max_token_id = max(lexicon.tokens) + logging.info(f"Building ctc_topo. max_token_id: {max_token_id}") + H = k2.ctc_topo(max_token_id) + L = k2.Fsa.from_dict(torch.load(f"{lang_dir}/L_disambig.pt")) + + if Path(f"{lm_dir}/{lm}.pt").is_file(): + logging.info(f"Loading pre-compiled {lm}") + d = torch.load(f"{lm_dir}/{lm}.pt") + G = k2.Fsa.from_dict(d) + else: + logging.info(f"Loading {lm}.fst.txt") + with open(f"{lm_dir}/{lm}.fst.txt") as f: + G = k2.Fsa.from_openfst(f.read(), acceptor=False) + torch.save(G.as_dict(), f"{lm_dir}/{lm}.pt") + + first_token_disambig_id = lexicon.token_table["#0"] + first_word_disambig_id = lexicon.word_table["#0"] + + L = k2.arc_sort(L) + G = k2.arc_sort(G) + + logging.info("Intersecting L and G") + LG = k2.compose(L, G) + logging.info(f"LG shape: {LG.shape}") + + logging.info("Connecting LG") + LG = k2.connect(LG) + logging.info(f"LG shape after k2.connect: {LG.shape}") + + logging.info(type(LG.aux_labels)) + logging.info("Determinizing LG") + + LG = k2.determinize(LG) + logging.info(type(LG.aux_labels)) + + logging.info("Connecting LG after k2.determinize") + LG = k2.connect(LG) + + logging.info("Removing disambiguation symbols on LG") + + LG.labels[LG.labels >= first_token_disambig_id] = 0 + # See https://github.com/k2-fsa/k2/issues/874 + # for why we need to set LG.properties to None + LG.__dict__["_properties"] = None + + assert isinstance(LG.aux_labels, k2.RaggedTensor) + LG.aux_labels.values[LG.aux_labels.values >= first_word_disambig_id] = 0 + + LG = k2.remove_epsilon(LG) + logging.info(f"LG shape after k2.remove_epsilon: {LG.shape}") + + LG = k2.connect(LG) + LG.aux_labels = LG.aux_labels.remove_values_eq(0) + + logging.info("Arc sorting LG") + LG = k2.arc_sort(LG) + + logging.info("Composing H and LG") + # CAUTION: The name of the inner_labels is fixed + # to `tokens`. If you want to change it, please + # also change other places in icefall that are using + # it. + HLG = k2.compose(H, LG, inner_labels="tokens") + + logging.info("Connecting LG") + HLG = k2.connect(HLG) + + logging.info("Arc sorting LG") + HLG = k2.arc_sort(HLG) + logging.info(f"HLG.shape: {HLG.shape}") + + return HLG + + +def main(): + args = get_args() + lm_dir = Path(args.lm_dir) + lang_dir = Path(args.lang_dir) + + if (lang_dir / "HLG.pt").is_file(): + logging.info(f"{lang_dir}/HLG.pt already exists - skipping") + return + + logging.info(f"Processing {lang_dir}") + + HLG = compile_HLG(lm_dir, lang_dir, args.lm) + logging.info(f"Saving HLG.pt to {lang_dir}") + torch.save(HLG.as_dict(), f"{lang_dir}/HLG.pt") + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/WSASR/local/compute_fbank_librispeech.py b/egs/librispeech/WSASR/local/compute_fbank_librispeech.py new file mode 100755 index 000000000..a387d54c9 --- /dev/null +++ b/egs/librispeech/WSASR/local/compute_fbank_librispeech.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the LibriSpeech dataset. +It looks for manifests in the directory data/manifests. + +The generated fbank features are saved in data/fbank. +""" + +import argparse +import logging +import os +from pathlib import Path +from typing import Optional + +import sentencepiece as spm +import torch +from filter_cuts import filter_cuts +from lhotse import CutSet, Fbank, FbankConfig, LilcomChunkyWriter +from lhotse.recipes.utils import read_manifests_if_cached + +from icefall.utils import get_executor, str2bool + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def get_args(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--bpe-model", + type=str, + help="""Path to the bpe.model. If not None, we will remove short and + long utterances before extracting features""", + ) + + parser.add_argument( + "--dataset", + type=str, + help="""Dataset parts to compute fbank. If None, we will use all""", + ) + + parser.add_argument( + "--perturb-speed", + type=str2bool, + default=True, + help="""Perturb speed with factor 0.9 and 1.1 on train subset.""", + ) + + return parser.parse_args() + + +def compute_fbank_librispeech( + bpe_model: Optional[str] = None, + dataset: Optional[str] = None, + perturb_speed: Optional[bool] = True, +): + src_dir = Path("data/manifests") + output_dir = Path("data/fbank") + num_jobs = min(15, os.cpu_count()) + num_mel_bins = 80 + + if bpe_model: + logging.info(f"Loading {bpe_model}") + sp = spm.SentencePieceProcessor() + sp.load(bpe_model) + + if dataset is None: + dataset_parts = ( + "dev-clean", + "dev-other", + "test-clean", + "test-other", + "train-clean-100", + ) + else: + dataset_parts = dataset.split(" ", -1) + + prefix = "librispeech" + suffix = "jsonl.gz" + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + prefix=prefix, + suffix=suffix, + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + + extractor = Fbank(FbankConfig(num_mel_bins=num_mel_bins)) + + with get_executor() as ex: # Initialize the executor only once. + for partition, m in manifests.items(): + cuts_filename = f"{prefix}_cuts_{partition}.{suffix}" + if (output_dir / cuts_filename).is_file(): + logging.info(f"{partition} already exists - skipping.") + continue + logging.info(f"Processing {partition}") + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ) + + if "train" in partition: + if bpe_model: + cut_set = filter_cuts(cut_set, sp) + if perturb_speed: + logging.info(f"Doing speed perturb") + cut_set = ( + cut_set + + cut_set.perturb_speed(0.9) + + cut_set.perturb_speed(1.1) + ) + cut_set = cut_set.compute_and_store_features( + extractor=extractor, + storage_path=f"{output_dir}/{prefix}_feats_{partition}", + # when an executor is specified, make more partitions + num_jobs=num_jobs if ex is None else 80, + executor=ex, + storage_type=LilcomChunkyWriter, + ) + cut_set.to_file(output_dir / cuts_filename) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + args = get_args() + logging.info(vars(args)) + compute_fbank_librispeech( + bpe_model=args.bpe_model, + dataset=args.dataset, + perturb_speed=args.perturb_speed, + ) diff --git a/egs/librispeech/WSASR/local/compute_ssl_librispeech.py b/egs/librispeech/WSASR/local/compute_ssl_librispeech.py new file mode 100755 index 000000000..f405c468c --- /dev/null +++ b/egs/librispeech/WSASR/local/compute_ssl_librispeech.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# 2023 Johns Hopkins University (author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 file computes fbank features of the LibriSpeech dataset. +It looks for manifests in the directory data/manifests. + +The generated fbank features are saved in data/fbank. +""" + +import logging +import os +from pathlib import Path + +import torch +from lhotse import S3PRLSSL, CutSet, NumpyFilesWriter, S3PRLSSLConfig +from lhotse.recipes.utils import read_manifests_if_cached + +from icefall.utils import get_executor + +# Torch's multithreaded behavior needs to be disabled or +# it wastes a lot of CPU and slow things down. +# Do this outside of main() in case it needs to take effect +# even when we are not invoking the main (e.g. when spawning subprocesses). +torch.set_num_threads(1) +torch.set_num_interop_threads(1) + + +def compute_ssl_librispeech(): + src_dir = Path("data/manifests") + output_dir = Path("data/ssl") + num_jobs = 1 + + dataset_parts = ( + "dev-clean", + "dev-other", + "test-clean", + "test-other", + "train-clean-100", + ) + prefix = "librispeech" + suffix = "jsonl.gz" + manifests = read_manifests_if_cached( + dataset_parts=dataset_parts, + output_dir=src_dir, + prefix=prefix, + suffix=suffix, + ) + assert manifests is not None + + assert len(manifests) == len(dataset_parts), ( + len(manifests), + len(dataset_parts), + list(manifests.keys()), + dataset_parts, + ) + + extractor = S3PRLSSL(S3PRLSSLConfig(ssl_model="wav2vec2", device="cuda")) + + with get_executor() as ex: # Initialize the executor only once. + for partition, m in manifests.items(): + cuts_filename = f"{prefix}_cuts_{partition}.{suffix}" + if (output_dir / cuts_filename).is_file(): + logging.info(f"{partition} already exists - skipping.") + continue + logging.info(f"Processing {partition}") + cut_set = CutSet.from_manifests( + recordings=m["recordings"], + supervisions=m["supervisions"], + ) + cut_set = cut_set.compute_and_store_features( + extractor=extractor, + storage_path=f"{output_dir}/{prefix}_feats_{partition}", + storage_type=NumpyFilesWriter, + ) + cut_set.to_file(output_dir / cuts_filename) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + compute_ssl_librispeech() diff --git a/egs/librispeech/WSASR/local/filter_cuts.py b/egs/librispeech/WSASR/local/filter_cuts.py new file mode 100644 index 000000000..fbcc9e24a --- /dev/null +++ b/egs/librispeech/WSASR/local/filter_cuts.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script removes short and long utterances from a cutset. + +Caution: + You may need to tune the thresholds for your own dataset. + +Usage example: + + python3 ./local/filter_cuts.py \ + --bpe-model data/lang_bpe_500/bpe.model \ + --in-cuts data/fbank/librispeech_cuts_test-clean.jsonl.gz \ + --out-cuts data/fbank-filtered/librispeech_cuts_test-clean.jsonl.gz +""" + +import argparse +import logging +from pathlib import Path + +import sentencepiece as spm +from lhotse import CutSet, load_manifest_lazy +from lhotse.cut import Cut + + +def get_args(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--bpe-model", + type=Path, + help="Path to the bpe.model", + ) + + parser.add_argument( + "--in-cuts", + type=Path, + help="Path to the input cutset", + ) + + parser.add_argument( + "--out-cuts", + type=Path, + help="Path to the output cutset", + ) + + return parser.parse_args() + + +def filter_cuts(cut_set: CutSet, sp: spm.SentencePieceProcessor): + total = 0 # number of total utterances before removal + removed = 0 # number of removed utterances + + def remove_short_and_long_utterances(c: Cut): + """Return False to exclude the input cut""" + nonlocal removed, total + # Keep only utterances with duration between 1 second and 20 seconds + # + # Caution: There is a reason to select 20.0 here. Please see + # ./display_manifest_statistics.py + # + # You should use ./display_manifest_statistics.py to get + # an utterance duration distribution for your dataset to select + # the threshold + total += 1 + if c.duration < 1.0 or c.duration > 20.0: + logging.warning( + f"Exclude cut with ID {c.id} from training. Duration: {c.duration}" + ) + removed += 1 + return False + + # In pruned RNN-T, we require that T >= S + # where T is the number of feature frames after subsampling + # and S is the number of tokens in the utterance + + # In ./pruned_transducer_stateless2/conformer.py, the + # conv module uses the following expression + # for subsampling + if c.num_frames is None: + num_frames = c.duration * 100 # approximate + else: + num_frames = c.num_frames + + T = ((num_frames - 1) // 2 - 1) // 2 + # Note: for ./lstm_transducer_stateless/lstm.py, the formula is + # T = ((num_frames - 3) // 2 - 1) // 2 + + # Note: for ./pruned_transducer_stateless7/zipformer.py, the formula is + # T = ((num_frames - 7) // 2 + 1) // 2 + + tokens = sp.encode(c.supervisions[0].text, out_type=str) + + if T < len(tokens): + logging.warning( + f"Exclude cut with ID {c.id} from training. " + f"Number of frames (before subsampling): {c.num_frames}. " + f"Number of frames (after subsampling): {T}. " + f"Text: {c.supervisions[0].text}. " + f"Tokens: {tokens}. " + f"Number of tokens: {len(tokens)}" + ) + removed += 1 + return False + + return True + + # We use to_eager() here so that we can print out the value of total + # and removed below. + ans = cut_set.filter(remove_short_and_long_utterances).to_eager() + ratio = removed / total * 100 + logging.info( + f"Removed {removed} cuts from {total} cuts. {ratio:.3f}% data is removed." + ) + return ans + + +def main(): + args = get_args() + logging.info(vars(args)) + + if args.out_cuts.is_file(): + logging.info(f"{args.out_cuts} already exists - skipping") + return + + assert args.in_cuts.is_file(), f"{args.in_cuts} does not exist" + assert args.bpe_model.is_file(), f"{args.bpe_model} does not exist" + + sp = spm.SentencePieceProcessor() + sp.load(str(args.bpe_model)) + + cut_set = load_manifest_lazy(args.in_cuts) + assert isinstance(cut_set, CutSet) + + cut_set = filter_cuts(cut_set, sp) + logging.info(f"Saving to {args.out_cuts}") + args.out_cuts.parent.mkdir(parents=True, exist_ok=True) + cut_set.to_file(args.out_cuts) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/WSASR/local/get_words_from_lexicon.py b/egs/librispeech/WSASR/local/get_words_from_lexicon.py new file mode 100755 index 000000000..0cc740b36 --- /dev/null +++ b/egs/librispeech/WSASR/local/get_words_from_lexicon.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import argparse +from pathlib import Path + +from icefall.lexicon import read_lexicon + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--lang-dir", + type=str, + help="""Input and output directory. + It should contain a file lexicon.txt. + Generated files by this script are saved into this directory. + """, + ) + + parser.add_argument( + "--otc-token", + type=str, + help="OTC token to be added to words.txt", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + lang_dir = Path(args.lang_dir) + otc_token = args.otc_token + + lexicon = read_lexicon(lang_dir / "lexicon.txt") + ans = set() + for word, _ in lexicon: + ans.add(word) + sorted_ans = sorted(list(ans)) + words = [""] + sorted_ans + [otc_token] + ["#0", "", ""] + + words_file = lang_dir / "words.txt" + with open(words_file, "w") as wf: + for i, word in enumerate(words): + wf.write(f"{word} {i}\n") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/local/make_error_cutset.py b/egs/librispeech/WSASR/local/make_error_cutset.py new file mode 100755 index 000000000..8463a380e --- /dev/null +++ b/egs/librispeech/WSASR/local/make_error_cutset.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 + +# Copyright 2023 Johns Hopkins University (author: Dongji Gao) + +import argparse +import random +from pathlib import Path +from typing import List + +from lhotse import CutSet, load_manifest +from lhotse.cut.base import Cut + +from icefall.utils import str2bool + + +def get_args(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "--input-cutset", + type=str, + help="Supervision manifest that contains verbatim transcript", + ) + + parser.add_argument( + "--words-file", + type=str, + help="words.txt file", + ) + + parser.add_argument( + "--otc-token", + type=str, + help="OTC token in words.txt", + ) + + parser.add_argument( + "--sub-error-rate", + type=float, + default=0.0, + help="Substitution error rate", + ) + + parser.add_argument( + "--ins-error-rate", + type=float, + default=0.0, + help="Insertion error rate", + ) + + parser.add_argument( + "--del-error-rate", + type=float, + default=0.0, + help="Deletion error rate", + ) + + parser.add_argument( + "--output-cutset", + type=str, + default="", + help="Supervision manifest that contains modified non-verbatim transcript", + ) + + parser.add_argument("--verbose", type=str2bool, help="show details of errors") + return parser.parse_args() + + +def check_args(args): + total_error_rate = args.sub_error_rate + args.ins_error_rate + args.del_error_rate + assert args.sub_error_rate >= 0 and args.sub_error_rate <= 1.0 + assert args.ins_error_rate >= 0 and args.sub_error_rate <= 1.0 + assert args.del_error_rate >= 0 and args.sub_error_rate <= 1.0 + assert total_error_rate <= 1.0 + + +def get_word_list(token_path: str) -> List: + word_list = [] + with open(Path(token_path), "r") as tp: + for line in tp.readlines(): + token = line.split()[0] + assert token not in word_list + word_list.append(token) + return word_list + + +def modify_cut_text( + cut: Cut, + words_list: List, + non_words: List, + sub_ratio: float = 0.0, + ins_ratio: float = 0.0, + del_ratio: float = 0.0, +): + text = cut.supervisions[0].text + text_list = text.split() + + # We save the modified information of the original verbatim text for debugging + marked_verbatim_text_list = [] + modified_text_list = [] + + del_index_set = set() + sub_index_set = set() + ins_index_set = set() + + # We follow the order: deletion -> substitution -> insertion + for token in text_list: + marked_token = token + modified_token = token + + prob = random.random() + + if prob <= del_ratio: + marked_token = f"-{token}-" + modified_token = "" + elif prob <= del_ratio + sub_ratio + ins_ratio: + if prob <= del_ratio + sub_ratio: + marked_token = f"[{token}]" + else: + marked_verbatim_text_list.append(marked_token) + modified_text_list.append(modified_token) + marked_token = "[]" + + # get new_token + while ( + modified_token == token + or modified_token in non_words + or modified_token.startswith("#") + ): + modified_token = random.choice(words_list) + + marked_verbatim_text_list.append(marked_token) + modified_text_list.append(modified_token) + + marked_text = " ".join(marked_verbatim_text_list) + modified_text = " ".join(modified_text_list) + + if not hasattr(cut.supervisions[0], "verbatim_text"): + cut.supervisions[0].verbatim_text = marked_text + cut.supervisions[0].text = modified_text + + return cut + + +def main(): + args = get_args() + check_args(args) + + otc_token = args.otc_token + non_words = set(("sil", "", "")) + non_words.add(otc_token) + + words_list = get_word_list(args.words_file) + cutset = load_manifest(Path(args.input_cutset)) + + cuts = [] + + for cut in cutset: + modified_cut = modify_cut_text( + cut=cut, + words_list=words_list, + non_words=non_words, + sub_ratio=args.sub_error_rate, + ins_ratio=args.ins_error_rate, + del_ratio=args.del_error_rate, + ) + cuts.append(modified_cut) + + output_cutset = CutSet.from_cuts(cuts) + output_cutset.to_file(args.output_cutset) + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/local/prepare_lang.py b/egs/librispeech/WSASR/local/prepare_lang.py new file mode 100755 index 000000000..d913756a1 --- /dev/null +++ b/egs/librispeech/WSASR/local/prepare_lang.py @@ -0,0 +1,413 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script takes as input a lexicon file "data/lang_phone/lexicon.txt" +consisting of words and tokens (i.e., phones) and does the following: + +1. Add disambiguation symbols to the lexicon and generate lexicon_disambig.txt + +2. Generate tokens.txt, the token table mapping a token to a unique integer. + +3. Generate words.txt, the word table mapping a word to a unique integer. + +4. Generate L.pt, in k2 format. It can be loaded by + + d = torch.load("L.pt") + lexicon = k2.Fsa.from_dict(d) + +5. Generate L_disambig.pt, in k2 format. +""" +import argparse +import math +from collections import defaultdict +from pathlib import Path +from typing import Any, Dict, List, Tuple + +import k2 +import torch + +from icefall.lexicon import read_lexicon, write_lexicon +from icefall.utils import str2bool + +Lexicon = List[Tuple[str, List[str]]] + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--lang-dir", + type=str, + help="""Input and output directory. + It should contain a file lexicon.txt. + Generated files by this script are saved into this directory. + """, + ) + + parser.add_argument( + "--debug", + type=str2bool, + default=False, + help="""True for debugging, which will generate + a visualization of the lexicon FST. + + Caution: If your lexicon contains hundreds of thousands + of lines, please set it to False! + """, + ) + + return parser.parse_args() + + +def write_mapping(filename: str, sym2id: Dict[str, int]) -> None: + """Write a symbol to ID mapping to a file. + + Note: + No need to implement `read_mapping` as it can be done + through :func:`k2.SymbolTable.from_file`. + + Args: + filename: + Filename to save the mapping. + sym2id: + A dict mapping symbols to IDs. + Returns: + Return None. + """ + with open(filename, "w", encoding="utf-8") as f: + for sym, i in sym2id.items(): + f.write(f"{sym} {i}\n") + + +def get_tokens(lexicon: Lexicon) -> List[str]: + """Get tokens from a lexicon. + + Args: + lexicon: + It is the return value of :func:`read_lexicon`. + Returns: + Return a list of unique tokens. + """ + ans = set() + for _, tokens in lexicon: + ans.update(tokens) + sorted_ans = sorted(list(ans)) + return sorted_ans + + +def get_words(lexicon: Lexicon) -> List[str]: + """Get words from a lexicon. + + Args: + lexicon: + It is the return value of :func:`read_lexicon`. + Returns: + Return a list of unique words. + """ + ans = set() + for word, _ in lexicon: + ans.add(word) + sorted_ans = sorted(list(ans)) + return sorted_ans + + +def add_disambig_symbols(lexicon: Lexicon) -> Tuple[Lexicon, int]: + """It adds pseudo-token disambiguation symbols #1, #2 and so on + at the ends of tokens to ensure that all pronunciations are different, + and that none is a prefix of another. + + See also add_lex_disambig.pl from kaldi. + + Args: + lexicon: + It is returned by :func:`read_lexicon`. + Returns: + Return a tuple with two elements: + + - The output lexicon with disambiguation symbols + - The ID of the max disambiguation symbol that appears + in the lexicon + """ + + # (1) Work out the count of each token-sequence in the + # lexicon. + count = defaultdict(int) + for _, tokens in lexicon: + count[" ".join(tokens)] += 1 + + # (2) For each left sub-sequence of each token-sequence, note down + # that it exists (for identifying prefixes of longer strings). + issubseq = defaultdict(int) + for _, tokens in lexicon: + tokens = tokens.copy() + tokens.pop() + while tokens: + issubseq[" ".join(tokens)] = 1 + tokens.pop() + + # (3) For each entry in the lexicon: + # if the token sequence is unique and is not a + # prefix of another word, no disambig symbol. + # Else output #1, or #2, #3, ... if the same token-seq + # has already been assigned a disambig symbol. + ans = [] + + # We start with #1 since #0 has its own purpose + first_allowed_disambig = 1 + max_disambig = first_allowed_disambig - 1 + last_used_disambig_symbol_of = defaultdict(int) + + for word, tokens in lexicon: + tokenseq = " ".join(tokens) + assert tokenseq != "" + if issubseq[tokenseq] == 0 and count[tokenseq] == 1: + ans.append((word, tokens)) + continue + + cur_disambig = last_used_disambig_symbol_of[tokenseq] + if cur_disambig == 0: + cur_disambig = first_allowed_disambig + else: + cur_disambig += 1 + + if cur_disambig > max_disambig: + max_disambig = cur_disambig + last_used_disambig_symbol_of[tokenseq] = cur_disambig + tokenseq += f" #{cur_disambig}" + ans.append((word, tokenseq.split())) + return ans, max_disambig + + +def generate_id_map(symbols: List[str]) -> Dict[str, int]: + """Generate ID maps, i.e., map a symbol to a unique ID. + + Args: + symbols: + A list of unique symbols. + Returns: + A dict containing the mapping between symbols and IDs. + """ + return {sym: i for i, sym in enumerate(symbols)} + + +def add_self_loops( + arcs: List[List[Any]], disambig_token: int, disambig_word: int +) -> List[List[Any]]: + """Adds self-loops to states of an FST to propagate disambiguation symbols + through it. They are added on each state with non-epsilon output symbols + on at least one arc out of the state. + + See also fstaddselfloops.pl from Kaldi. One difference is that + Kaldi uses OpenFst style FSTs and it has multiple final states. + This function uses k2 style FSTs and it does not need to add self-loops + to the final state. + + The input label of a self-loop is `disambig_token`, while the output + label is `disambig_word`. + + Args: + arcs: + A list-of-list. The sublist contains + `[src_state, dest_state, label, aux_label, score]` + disambig_token: + It is the token ID of the symbol `#0`. + disambig_word: + It is the word ID of the symbol `#0`. + + Return: + Return new `arcs` containing self-loops. + """ + states_needs_self_loops = set() + for arc in arcs: + src, dst, ilabel, olabel, score = arc + if olabel != 0: + states_needs_self_loops.add(src) + + ans = [] + for s in states_needs_self_loops: + ans.append([s, s, disambig_token, disambig_word, 0]) + + return arcs + ans + + +def lexicon_to_fst( + lexicon: Lexicon, + token2id: Dict[str, int], + word2id: Dict[str, int], + sil_token: str = "SIL", + sil_prob: float = 0.5, + need_self_loops: bool = False, +) -> k2.Fsa: + """Convert a lexicon to an FST (in k2 format) with optional silence at + the beginning and end of each word. + + Args: + lexicon: + The input lexicon. See also :func:`read_lexicon` + token2id: + A dict mapping tokens to IDs. + word2id: + A dict mapping words to IDs. + sil_token: + The silence token. + sil_prob: + The probability for adding a silence at the beginning and end + of the word. + need_self_loops: + If True, add self-loop to states with non-epsilon output symbols + on at least one arc out of the state. The input label for this + self loop is `token2id["#0"]` and the output label is `word2id["#0"]`. + Returns: + Return an instance of `k2.Fsa` representing the given lexicon. + """ + assert sil_prob > 0.0 and sil_prob < 1.0 + # CAUTION: we use score, i.e, negative cost. + sil_score = math.log(sil_prob) + no_sil_score = math.log(1.0 - sil_prob) + + start_state = 0 + loop_state = 1 # words enter and leave from here + sil_state = 2 # words terminate here when followed by silence; this state + # has a silence transition to loop_state. + next_state = 3 # the next un-allocated state, will be incremented as we go. + arcs = [] + + assert token2id[""] == 0 + assert word2id[""] == 0 + + eps = 0 + + sil_token = token2id[sil_token] + + arcs.append([start_state, loop_state, eps, eps, no_sil_score]) + arcs.append([start_state, sil_state, eps, eps, sil_score]) + arcs.append([sil_state, loop_state, sil_token, eps, 0]) + + for word, tokens in lexicon: + assert len(tokens) > 0, f"{word} has no pronunciations" + cur_state = loop_state + + word = word2id[word] + tokens = [token2id[i] for i in tokens] + + for i in range(len(tokens) - 1): + w = word if i == 0 else eps + arcs.append([cur_state, next_state, tokens[i], w, 0]) + + cur_state = next_state + next_state += 1 + + # now for the last token of this word + # It has two out-going arcs, one to the loop state, + # the other one to the sil_state. + i = len(tokens) - 1 + w = word if i == 0 else eps + arcs.append([cur_state, loop_state, tokens[i], w, no_sil_score]) + arcs.append([cur_state, sil_state, tokens[i], w, sil_score]) + + if need_self_loops: + disambig_token = token2id["#0"] + disambig_word = word2id["#0"] + arcs = add_self_loops( + arcs, + disambig_token=disambig_token, + disambig_word=disambig_word, + ) + + final_state = next_state + arcs.append([loop_state, final_state, -1, -1, 0]) + arcs.append([final_state]) + + arcs = sorted(arcs, key=lambda arc: arc[0]) + arcs = [[str(i) for i in arc] for arc in arcs] + arcs = [" ".join(arc) for arc in arcs] + arcs = "\n".join(arcs) + + fsa = k2.Fsa.from_str(arcs, acceptor=False) + return fsa + + +def main(): + args = get_args() + lang_dir = Path(args.lang_dir) + lexicon_filename = lang_dir / "lexicon.txt" + sil_token = "SIL" + sil_prob = 0.5 + + lexicon = read_lexicon(lexicon_filename) + tokens = get_tokens(lexicon) + words = get_words(lexicon) + + lexicon_disambig, max_disambig = add_disambig_symbols(lexicon) + + for i in range(max_disambig + 1): + disambig = f"#{i}" + assert disambig not in tokens + tokens.append(f"#{i}") + + assert "" not in tokens + tokens = [""] + tokens + + assert "" not in words + assert "#0" not in words + assert "" not in words + assert "" not in words + + words = [""] + words + ["#0", "", ""] + + token2id = generate_id_map(tokens) + word2id = generate_id_map(words) + + write_mapping(lang_dir / "tokens.txt", token2id) + write_mapping(lang_dir / "words.txt", word2id) + write_lexicon(lang_dir / "lexicon_disambig.txt", lexicon_disambig) + + L = lexicon_to_fst( + lexicon, + token2id=token2id, + word2id=word2id, + sil_token=sil_token, + sil_prob=sil_prob, + ) + + L_disambig = lexicon_to_fst( + lexicon_disambig, + token2id=token2id, + word2id=word2id, + sil_token=sil_token, + sil_prob=sil_prob, + need_self_loops=True, + ) + torch.save(L.as_dict(), lang_dir / "L.pt") + torch.save(L_disambig.as_dict(), lang_dir / "L_disambig.pt") + + if args.debug: + labels_sym = k2.SymbolTable.from_file(lang_dir / "tokens.txt") + aux_labels_sym = k2.SymbolTable.from_file(lang_dir / "words.txt") + + L.labels_sym = labels_sym + L.aux_labels_sym = aux_labels_sym + L.draw(f"{lang_dir / 'L.svg'}", title="L.pt") + + L_disambig.labels_sym = labels_sym + L_disambig.aux_labels_sym = aux_labels_sym + L_disambig.draw(f"{lang_dir / 'L_disambig.svg'}", title="L_disambig.pt") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/local/prepare_otc_lang_bpe.py b/egs/librispeech/WSASR/local/prepare_otc_lang_bpe.py new file mode 100755 index 000000000..415bdff6f --- /dev/null +++ b/egs/librispeech/WSASR/local/prepare_otc_lang_bpe.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# 2023 Johns Hopkins University (author: Dongji Gao) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + + +# Copyright (c) 2021 Xiaomi Corporation (authors: Fangjun Kuang) + +""" + +This script takes as input `lang_dir`, which should contain:: + + - lang_dir/bpe.model, + - lang_dir/words.txt + +and generates the following files in the directory `lang_dir`: + + - lexicon.txt + - lexicon_disambig.txt + - L.pt + - L_disambig.pt + - tokens.txt +""" + +import argparse +from pathlib import Path +from typing import Dict, List, Tuple + +import k2 +import sentencepiece as spm +import torch +from prepare_lang import ( + Lexicon, + add_disambig_symbols, + add_self_loops, + write_lexicon, + write_mapping, +) + +from icefall.utils import str2bool + + +def lexicon_to_fst_no_sil( + lexicon: Lexicon, + token2id: Dict[str, int], + word2id: Dict[str, int], + need_self_loops: bool = False, +) -> k2.Fsa: + """Convert a lexicon to an FST (in k2 format). + + Args: + lexicon: + The input lexicon. See also :func:`read_lexicon` + token2id: + A dict mapping tokens to IDs. + word2id: + A dict mapping words to IDs. + need_self_loops: + If True, add self-loop to states with non-epsilon output symbols + on at least one arc out of the state. The input label for this + self loop is `token2id["#0"]` and the output label is `word2id["#0"]`. + Returns: + Return an instance of `k2.Fsa` representing the given lexicon. + """ + loop_state = 0 # words enter and leave from here + next_state = 1 # the next un-allocated state, will be incremented as we go + + arcs = [] + + # The blank symbol is defined in local/train_bpe_model.py + assert token2id[""] == 0 + assert word2id[""] == 0 + + eps = 0 + + for word, pieces in lexicon: + assert len(pieces) > 0, f"{word} has no pronunciations" + cur_state = loop_state + + word = word2id[word] + pieces = [token2id[i] for i in pieces] + + for i in range(len(pieces) - 1): + w = word if i == 0 else eps + arcs.append([cur_state, next_state, pieces[i], w, 0]) + + cur_state = next_state + next_state += 1 + + # now for the last piece of this word + i = len(pieces) - 1 + w = word if i == 0 else eps + arcs.append([cur_state, loop_state, pieces[i], w, 0]) + + if need_self_loops: + disambig_token = token2id["#0"] + disambig_word = word2id["#0"] + arcs = add_self_loops( + arcs, + disambig_token=disambig_token, + disambig_word=disambig_word, + ) + + final_state = next_state + arcs.append([loop_state, final_state, -1, -1, 0]) + arcs.append([final_state]) + + arcs = sorted(arcs, key=lambda arc: arc[0]) + arcs = [[str(i) for i in arc] for arc in arcs] + arcs = [" ".join(arc) for arc in arcs] + arcs = "\n".join(arcs) + + fsa = k2.Fsa.from_str(arcs, acceptor=False) + return fsa + + +def generate_otc_lexicon( + model_file: str, + words: List[str], + oov: str, + otc_token: str, +) -> Tuple[Lexicon, Dict[str, int]]: + """Generate a lexicon from a BPE model. + + Args: + model_file: + Path to a sentencepiece model. + words: + A list of strings representing words. + oov: + The out of vocabulary word in lexicon. + otc_token: + The OTC token in lexicon. + Returns: + Return a tuple with two elements: + - A dict whose keys are words and values are the corresponding + word pieces. + - A dict representing the token symbol, mapping from tokens to IDs. + """ + sp = spm.SentencePieceProcessor() + sp.load(str(model_file)) + + # Convert word to word piece IDs instead of word piece strings + # to avoid OOV tokens. + words_pieces_ids: List[List[int]] = sp.encode(words, out_type=int) + + # Now convert word piece IDs back to word piece strings. + words_pieces: List[List[str]] = [sp.id_to_piece(ids) for ids in words_pieces_ids] + + lexicon = [] + for word, pieces in zip(words, words_pieces): + lexicon.append((word, pieces)) + + lexicon.append((oov, ["▁", sp.id_to_piece(sp.unk_id())])) + token2id: Dict[str, int] = {sp.id_to_piece(i): i for i in range(sp.vocab_size())} + + # Add OTC token to the last. + lexicon.append((otc_token, [f"▁{otc_token}"])) + otc_token_index = len(token2id) + token2id[f"▁{otc_token}"] = otc_token_index + + return lexicon, token2id + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--lang-dir", + type=str, + help="""Input and output directory. + It should contain the bpe.model and words.txt + """, + ) + + parser.add_argument( + "--oov", + type=str, + default="", + help="The out of vocabulary word in lexicon.", + ) + + parser.add_argument( + "--otc-token", + type=str, + default="", + help="The OTC token in lexicon.", + ) + + parser.add_argument( + "--debug", + type=str2bool, + default=False, + help="""True for debugging, which will generate + a visualization of the lexicon FST. + + Caution: If your lexicon contains hundreds of thousands + of lines, please set it to False! + + See "test/test_bpe_lexicon.py" for usage. + """, + ) + + return parser.parse_args() + + +def main(): + args = get_args() + lang_dir = Path(args.lang_dir) + model_file = lang_dir / "bpe.model" + otc_token = args.otc_token + + word_sym_table = k2.SymbolTable.from_file(lang_dir / "words.txt") + + words = word_sym_table.symbols + + excluded = [ + "", + "!SIL", + "", + args.oov, + otc_token, + "#0", + "", + "", + ] + + for w in excluded: + if w in words: + words.remove(w) + + lexicon, token_sym_table = generate_otc_lexicon( + model_file, words, args.oov, otc_token + ) + + lexicon_disambig, max_disambig = add_disambig_symbols(lexicon) + + next_token_id = max(token_sym_table.values()) + 1 + for i in range(max_disambig + 1): + disambig = f"#{i}" + assert disambig not in token_sym_table + token_sym_table[disambig] = next_token_id + next_token_id += 1 + + word_sym_table.add("#0") + word_sym_table.add("") + word_sym_table.add("") + + write_mapping(lang_dir / "tokens.txt", token_sym_table) + + write_lexicon(lang_dir / "lexicon.txt", lexicon) + write_lexicon(lang_dir / "lexicon_disambig.txt", lexicon_disambig) + + L = lexicon_to_fst_no_sil( + lexicon, + token2id=token_sym_table, + word2id=word_sym_table, + ) + + L_disambig = lexicon_to_fst_no_sil( + lexicon_disambig, + token2id=token_sym_table, + word2id=word_sym_table, + need_self_loops=True, + ) + torch.save(L.as_dict(), lang_dir / "L.pt") + torch.save(L_disambig.as_dict(), lang_dir / "L_disambig.pt") + + if args.debug: + labels_sym = k2.SymbolTable.from_file(lang_dir / "tokens.txt") + aux_labels_sym = k2.SymbolTable.from_file(lang_dir / "words.txt") + + L.labels_sym = labels_sym + L.aux_labels_sym = aux_labels_sym + L.draw(f"{lang_dir / 'L.svg'}", title="L.pt") + + L_disambig.labels_sym = labels_sym + L_disambig.aux_labels_sym = aux_labels_sym + L_disambig.draw(f"{lang_dir / 'L_disambig.svg'}", title="L_disambig.pt") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/local/train_bpe_model.py b/egs/librispeech/WSASR/local/train_bpe_model.py new file mode 100755 index 000000000..43142aee4 --- /dev/null +++ b/egs/librispeech/WSASR/local/train_bpe_model.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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. + + +# You can install sentencepiece via: +# +# pip install sentencepiece +# +# Due to an issue reported in +# https://github.com/google/sentencepiece/pull/642#issuecomment-857972030 +# +# Please install a version >=0.1.96 + +import argparse +import shutil +from pathlib import Path + +import sentencepiece as spm + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--lang-dir", + type=str, + help="""Input and output directory. + The generated bpe.model is saved to this directory. + """, + ) + + parser.add_argument( + "--transcript", + type=str, + help="Training transcript.", + ) + + parser.add_argument( + "--vocab-size", + type=int, + help="Vocabulary size for BPE training", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + vocab_size = args.vocab_size + lang_dir = Path(args.lang_dir) + + model_type = "unigram" + + model_prefix = f"{lang_dir}/{model_type}_{vocab_size}" + train_text = args.transcript + character_coverage = 1.0 + input_sentence_size = 100000000 + + user_defined_symbols = ["", ""] + unk_id = len(user_defined_symbols) + # Note: unk_id is fixed to 2. + # If you change it, you should also change other + # places that are using it. + + model_file = Path(model_prefix + ".model") + if not model_file.is_file(): + spm.SentencePieceTrainer.train( + input=train_text, + vocab_size=vocab_size, + model_type=model_type, + model_prefix=model_prefix, + input_sentence_size=input_sentence_size, + character_coverage=character_coverage, + user_defined_symbols=user_defined_symbols, + unk_id=unk_id, + bos_id=-1, + eos_id=-1, + ) + else: + print(f"{model_file} exists - skipping") + return + + shutil.copyfile(model_file, f"{lang_dir}/bpe.model") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/local/validate_bpe_lexicon.py b/egs/librispeech/WSASR/local/validate_bpe_lexicon.py new file mode 100755 index 000000000..16a489c11 --- /dev/null +++ b/egs/librispeech/WSASR/local/validate_bpe_lexicon.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script checks that there are no OOV tokens in the BPE-based lexicon. + +Usage example: + + python3 ./local/validate_bpe_lexicon.py \ + --lexicon /path/to/lexicon.txt \ + --bpe-model /path/to/bpe.model +""" + +import argparse +from pathlib import Path +from typing import List, Tuple + +import sentencepiece as spm + +from icefall.lexicon import read_lexicon + +# Map word to word pieces +Lexicon = List[Tuple[str, List[str]]] + + +def get_args(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "--lexicon", + required=True, + type=Path, + help="Path to lexicon.txt", + ) + + parser.add_argument( + "--bpe-model", + required=True, + type=Path, + help="Path to bpe.model", + ) + + parser.add_argument( + "--otc-token", + required=True, + type=str, + help="OTC token", + ) + + return parser.parse_args() + + +def main(): + args = get_args() + assert args.lexicon.is_file(), args.lexicon + assert args.bpe_model.is_file(), args.bpe_model + + lexicon = read_lexicon(args.lexicon) + + sp = spm.SentencePieceProcessor() + sp.load(str(args.bpe_model)) + + word_pieces = set(sp.id_to_piece(list(range(sp.vocab_size())))) + word_pieces.add(f"▁{args.otc_token}") + for word, pieces in lexicon: + for p in pieces: + if p not in word_pieces: + raise ValueError(f"The word {word} contains an OOV token {p}") + + +if __name__ == "__main__": + main() diff --git a/egs/librispeech/WSASR/local/validate_manifest.py b/egs/librispeech/WSASR/local/validate_manifest.py new file mode 100755 index 000000000..f620b91ea --- /dev/null +++ b/egs/librispeech/WSASR/local/validate_manifest.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# Copyright 2022 Xiaomi Corp. (authors: Fangjun Kuang) +# +# See ../../../../LICENSE for clarification regarding multiple authors +# +# 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 script checks the following assumptions of the generated manifest: + +- Single supervision per cut +- Supervision time bounds are within cut time bounds + +We will add more checks later if needed. + +Usage example: + + python3 ./local/validate_manifest.py \ + ./data/fbank/librispeech_cuts_train-clean-100.jsonl.gz + +""" + +import argparse +import logging +from pathlib import Path + +from lhotse import CutSet, load_manifest_lazy +from lhotse.cut import Cut + + +def get_args(): + parser = argparse.ArgumentParser() + + parser.add_argument( + "manifest", + type=Path, + help="Path to the manifest file", + ) + + return parser.parse_args() + + +def validate_one_supervision_per_cut(c: Cut): + if len(c.supervisions) != 1: + raise ValueError(f"{c.id} has {len(c.supervisions)} supervisions") + + +def validate_supervision_and_cut_time_bounds(c: Cut): + s = c.supervisions[0] + if s.start < c.start: + raise ValueError( + f"{c.id}: Supervision start time {s.start} is less " + f"than cut start time {c.start}" + ) + + if s.end > c.end: + raise ValueError( + f"{c.id}: Supervision end time {s.end} is larger " + f"than cut end time {c.end}" + ) + + +def main(): + args = get_args() + + manifest = args.manifest + logging.info(f"Validating {manifest}") + + assert manifest.is_file(), f"{manifest} does not exist" + cut_set = load_manifest_lazy(manifest) + assert isinstance(cut_set, CutSet) + + for c in cut_set: + validate_one_supervision_per_cut(c) + validate_supervision_and_cut_time_bounds(c) + + +if __name__ == "__main__": + formatter = "%(asctime)s %(levelname)s [%(filename)s:%(lineno)d] %(message)s" + + logging.basicConfig(format=formatter, level=logging.INFO) + + main() diff --git a/egs/librispeech/WSASR/prepare.sh b/egs/librispeech/WSASR/prepare.sh new file mode 100755 index 000000000..f6a922fde --- /dev/null +++ b/egs/librispeech/WSASR/prepare.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash + +# fix segmentation fault reported in https://github.com/k2-fsa/icefall/issues/674 +export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python + +set -eou pipefail + +nj=15 +stage=-1 +stop_stage=100 + +# We assume dl_dir (download dir) contains the following +# directories and files. If not, they will be downloaded +# by this script automatically. +# +# - $dl_dir/LibriSpeech +# You can find BOOKS.TXT, test-clean, train-clean-360, etc, inside it. +# You can download them from https://www.openslr.org/12 +# +# - $dl_dir/lm +# This directory contains the following files downloaded from +# http://www.openslr.org/resources/11 +# +# - 3-gram.pruned.1e-7.arpa.gz +# - 3-gram.pruned.1e-7.arpa +# - 4-gram.arpa.gz +# - 4-gram.arpa +# - librispeech-vocab.txt +# - librispeech-lexicon.txt +# - librispeech-lm-norm.txt.gz +# +otc_token="" +feature_type="ssl" + +dl_dir=$PWD/download +manifests_dir="data/manifests" +feature_dir="data/${feature_type}" +lang_dir="data/lang" +lm_dir="data/lm" + +perturb_speed=false + +# ssl or fbank + +. ./cmd.sh +. shared/parse_options.sh || exit 1 + +# vocab size for sentence piece models. +# It will generate data/lang_bpe_xxx, +# data/lang_bpe_yyy if the array contains xxx, yyy +vocab_sizes=( + 200 +) + +# All files generated by this script are saved in "data". +# You can safely remove "data" and rerun this script to regenerate it. +mkdir -p data + +log() { + # This function is from espnet + local fname=${BASH_SOURCE[1]##*/} + echo -e "$(date '+%Y-%m-%d %H:%M:%S') (${fname}:${BASH_LINENO[0]}:${FUNCNAME[1]}) $*" +} + +log "dl_dir: ${dl_dir}" + +if [ $stage -le -1 ] && [ $stop_stage -ge -1 ]; then + log "Stage -1: Download LM" + mkdir -p ${dl_dir}/lm + if [ ! -e ${dl_dir}/lm/.done ]; then + ./local/download_lm.py --out-dir=${dl_dir}/lm + touch ${dl_dir}/lm/.done + fi +fi + +if [ $stage -le 0 ] && [ $stop_stage -ge 0 ]; then + log "Stage 0: Download data" + + # If you have pre-downloaded it to /path/to/LibriSpeech, + # you can create a symlink + # + # ln -sfv /path/to/LibriSpeech $dl_dir/LibriSpeech + # + if [ ! -d $dl_dir/LibriSpeech/train-clean-100 ]; then + lhotse download librispeech --full ${dl_dir} + fi +fi + +if [ $stage -le 1 ] && [ $stop_stage -ge 1 ]; then + log "Stage 1: Prepare LibriSpeech manifest" + # We assume that you have downloaded the LibriSpeech corpus + # to $dl_dir/LibriSpeech + mkdir -p data/manifests + if [ ! -e data/manifests/.librispeech.done ]; then + lhotse prepare librispeech -j ${nj} \ + -p dev-clean \ + -p dev-other \ + -p test-clean \ + -p test-other \ + -p train-clean-100 "${dl_dir}/LibriSpeech" "${manifests_dir}" + touch data/manifests/.librispeech.done + fi +fi + +if [ $stage -le 2 ] && [ $stop_stage -ge 2 ]; then + log "Stage 2: Compute ${feature_type} feature for librispeech (train-clean-100)" + mkdir -p "${feature_dir}" + if [ ! -e "${feature_dir}/.librispeech.done" ]; then + if [ "${feature_type}" = ssl ]; then + ./local/compute_ssl_librispeech.py + elif [ "${feature_type}" = fbank ]; then + ./local/compute_fbank_librispeech.py --perturb-speed ${perturb_speed} + else + log "Error: not supported --feature-type '${feature_type}'" + exit 2 + fi + + touch "${feature_dir}.librispeech.done" + fi + + if [ ! -e "${feature_dir}/.librispeech-validated.done" ]; then + log "Validating data/ssl for LibriSpeech" + parts=( + train-clean-100 + test-clean + test-other + dev-clean + dev-other + ) + for part in ${parts[@]}; do + python3 ./local/validate_manifest.py \ + "${feature_dir}/librispeech_cuts_${part}.jsonl.gz" + done + touch "${feature_dir}/.librispeech-validated.done" + fi +fi + +if [ $stage -le 3 ] && [ $stop_stage -ge 3 ]; then + log "Stage 3: Prepare words.txt" + mkdir -p ${lang_dir} + + (echo '!SIL SIL'; echo ' SPN'; echo ' SPN'; ) | + cat - $dl_dir/lm/librispeech-lexicon.txt | + sort | uniq > ${lang_dir}/lexicon.txt + + local/get_words_from_lexicon.py \ + --lang-dir ${lang_dir} \ + --otc-token ${otc_token} +fi + +if [ $stage -le 4 ] && [ $stop_stage -ge 4 ]; then + log "Stage 4: Prepare BPE based lang" + + for vocab_size in ${vocab_sizes[@]}; do + bpe_lang_dir="data/lang_bpe_${vocab_size}" + mkdir -p "${bpe_lang_dir}" + # We reuse words.txt from phone based lexicon + # so that the two can share G.pt later. + cp "${lang_dir}/words.txt" "${bpe_lang_dir}" + + if [ ! -f "${bpe_lang_dir}/transcript_words.txt" ]; then + log "Generate data for BPE training" + files=$( + find "$dl_dir/LibriSpeech/train-clean-100" -name "*.trans.txt" + find "$dl_dir/LibriSpeech/train-clean-360" -name "*.trans.txt" + find "$dl_dir/LibriSpeech/train-other-500" -name "*.trans.txt" + ) + for f in ${files[@]}; do + cat $f | cut -d " " -f 2- + done > "${bpe_lang_dir}/transcript_words.txt" + fi + + if [ ! -f ${bpe_lang_dir}/bpe.model ]; then + ./local/train_bpe_model.py \ + --lang-dir ${bpe_lang_dir} \ + --vocab-size ${vocab_size} \ + --transcript ${bpe_lang_dir}/transcript_words.txt + fi + + if [ ! -f ${bpe_lang_dir}/L_disambig.pt ]; then + ./local/prepare_otc_lang_bpe.py \ + --lang-dir "${bpe_lang_dir}" \ + --otc-token "${otc_token}" + + log "Validating ${bpe_lang_dir}/lexicon.txt" + ./local/validate_bpe_lexicon.py \ + --lexicon ${bpe_lang_dir}/lexicon.txt \ + --bpe-model ${bpe_lang_dir}/bpe.model \ + --otc-token "${otc_token}" + fi + done +fi + +if [ $stage -le 5 ] && [ $stop_stage -ge 5 ]; then + log "Stage 5: Prepare G" + # We assume you have install kaldilm, if not, please install + # it using: pip install kaldilm + + mkdir -p "${lm_dir}" + if [ ! -f ${lm_dir}/G_3_gram.fst.txt ]; then + # It is used in building HLG + python3 -m kaldilm \ + --read-symbol-table="${lang_dir}/words.txt" \ + --disambig-symbol='#0' \ + --max-order=3 \ + ${dl_dir}/lm/3-gram.pruned.1e-7.arpa > ${lm_dir}/G_3_gram.fst.txt + fi + + if [ ! -f ${lm_dir}/G_4_gram.fst.txt ]; then + # It is used for LM rescoring + python3 -m kaldilm \ + --read-symbol-table="${lang_dir}/words.txt" \ + --disambig-symbol='#0' \ + --max-order=4 \ + ${dl_dir}/lm/4-gram.arpa > ${lm_dir}/G_4_gram.fst.txt + fi +fi + +if [ $stage -le 6 ] && [ $stop_stage -ge 6 ]; then + log "Stage 6: Compile HLG" + # Note If ./local/compile_hlg.py throws OOM, + # please switch to the following command + # + # ./local/compile_hlg_using_openfst.py --lang-dir data/lang_phone + + for vocab_size in ${vocab_sizes[@]}; do + bpe_lang_dir="data/lang_bpe_${vocab_size}" + echo "LM DIR: ${lm_dir}" + ./local/compile_hlg.py \ + --lm-dir "${lm_dir}" \ + --lang-dir "${bpe_lang_dir}" + done +fi diff --git a/icefall/otc_graph_compiler.py b/icefall/otc_graph_compiler.py new file mode 100644 index 000000000..bfd679452 --- /dev/null +++ b/icefall/otc_graph_compiler.py @@ -0,0 +1,246 @@ +# Copyright 2021 Xiaomi Corp. (authors: Fangjun Kuang) +# 2023 Johns Hopkins University (author: Dongji Gao) +# +# See ../../LICENSE for clarification regarding multiple authors +# +# 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 pathlib import Path +from typing import List, Union + +import k2 +import sentencepiece as spm +import torch + +from icefall.utils import str2bool + + +class OtcTrainingGraphCompiler(object): + def __init__( + self, + lang_dir: Path, + otc_token: str, + device: Union[str, torch.device] = "cpu", + sos_token: str = "", + eos_token: str = "", + initial_bypass_weight: float = 0.0, + initial_self_loop_weight: float = 0.0, + bypass_weight_decay: float = 0.0, + self_loop_weight_decay: float = 0.0, + ) -> None: + """ + Args: + lang_dir: + This directory is expected to contain the following files: + + - bpe.model + - words.txt + otc_token: + The special token in OTC that represent all non-blank tokens + device: + It indicates CPU or CUDA. + sos_token: + The word piece that represents sos. + eos_token: + The word piece that represents eos. + """ + lang_dir = Path(lang_dir) + bpe_model_file = lang_dir / "bpe.model" + sp = spm.SentencePieceProcessor() + sp.load(str(bpe_model_file)) + self.sp = sp + self.token_table = k2.SymbolTable.from_file(lang_dir / "tokens.txt") + + self.otc_token = otc_token + assert self.otc_token in self.token_table + + self.device = device + + self.sos_id = self.sp.piece_to_id(sos_token) + self.eos_id = self.sp.piece_to_id(eos_token) + + assert self.sos_id != self.sp.unk_id() + assert self.eos_id != self.sp.unk_id() + + max_token_id = self.get_max_token_id() + ctc_topo = k2.ctc_topo(max_token_id, modified=False) + self.ctc_topo = ctc_topo.to(self.device) + + self.initial_bypass_weight = initial_bypass_weight + self.initial_self_loop_weight = initial_self_loop_weight + self.bypass_weight_decay = bypass_weight_decay + self.self_loop_weight_decay = self_loop_weight_decay + + def get_max_token_id(self): + max_token_id = 0 + for symbol in self.token_table.symbols: + if not symbol.startswith("#"): + max_token_id = max(self.token_table[symbol], max_token_id) + assert max_token_id > 0 + + return max_token_id + + def make_arc( + self, + from_state: int, + to_state: int, + symbol: Union[str, int], + weight: float, + ): + return f"{from_state} {to_state} {symbol} {weight}" + + def texts_to_ids(self, texts: List[str]) -> List[List[int]]: + """Convert a list of texts to a list-of-list of piece IDs. + + Args: + texts: + It is a list of strings. Each string consists of space(s) + separated words. An example containing two strings is given below: + + ['HELLO ICEFALL', 'HELLO k2'] + Returns: + Return a list-of-list of piece IDs. + """ + return self.sp.encode(texts, out_type=int) + + def compile( + self, + texts: List[str], + allow_bypass_arc: str2bool = True, + allow_self_loop_arc: str2bool = True, + bypass_weight: float = 0.0, + self_loop_weight: float = 0.0, + ) -> k2.Fsa: + """Build a OTC graph from a texts (list of words). + + Args: + texts: + A list of strings. Each string contains a sentence for an utterance. + A sentence consists of spaces separated words. An example `texts` + looks like: + ['hello icefall', 'CTC training with k2'] + allow_bypass_arc: + Whether to add bypass arc to training graph for substitution + and insertion errors (wrong or extra words in the transcript). + allow_self_loop_arc: + Whether to add self-loop arc to training graph for deletion + errors (missing words in the transcript). + bypass_weight: + Weight associated with bypass arc. + self_loop_weight: + Weight associated with self-loop arc. + + Return: + Return an FsaVec, which is the result of composing a + CTC topology with OTC FSAs constructed from the given texts. + """ + + transcript_fsa = self.convert_transcript_to_fsa( + texts, + self.otc_token, + allow_bypass_arc, + allow_self_loop_arc, + bypass_weight, + self_loop_weight, + ) + transcript_fsa = transcript_fsa.to(self.device) + fsa_with_self_loop = k2.remove_epsilon_and_add_self_loops(transcript_fsa) + fsa_with_self_loop = k2.arc_sort(fsa_with_self_loop) + + graph = k2.compose( + self.ctc_topo, + fsa_with_self_loop, + treat_epsilons_specially=False, + ) + assert graph.requires_grad is False + + return graph + + def convert_transcript_to_fsa( + self, + texts: List[str], + otc_token: str, + allow_bypass_arc: str2bool = True, + allow_self_loop_arc: str2bool = True, + bypass_weight: float = 0.0, + self_loop_weight: float = 0.0, + ): + otc_token_id = self.token_table[otc_token] + + transcript_fsa_list = [] + for text in texts: + text_piece_ids = [] + + for word in text.split(): + piece_ids = self.sp.encode(word, out_type=int) + text_piece_ids.append(piece_ids) + + arcs = [] + start_state = 0 + cur_state = start_state + next_state = 1 + + for piece_ids in text_piece_ids: + bypass_cur_state = cur_state + + if allow_self_loop_arc: + self_loop_arc = self.make_arc( + cur_state, + cur_state, + otc_token_id, + self_loop_weight, + ) + arcs.append(self_loop_arc) + + for piece_id in piece_ids: + arc = self.make_arc(cur_state, next_state, piece_id, 0.0) + arcs.append(arc) + + cur_state = next_state + next_state += 1 + + bypass_next_state = cur_state + if allow_bypass_arc: + bypass_arc = self.make_arc( + bypass_cur_state, + bypass_next_state, + otc_token_id, + bypass_weight, + ) + arcs.append(bypass_arc) + bypass_cur_state = cur_state + + if allow_self_loop_arc: + self_loop_arc = self.make_arc( + cur_state, + cur_state, + otc_token_id, + self_loop_weight, + ) + arcs.append(self_loop_arc) + + # Deal with final state + final_state = next_state + final_arc = self.make_arc(cur_state, final_state, -1, 0.0) + arcs.append(final_arc) + arcs.append(f"{final_state}") + sorted_arcs = sorted(arcs, key=lambda a: int(a.split()[0])) + + transcript_fsa = k2.Fsa.from_str("\n".join(sorted_arcs)) + transcript_fsa = k2.arc_sort(transcript_fsa) + transcript_fsa_list.append(transcript_fsa) + + transcript_fsa_vec = k2.create_fsa_vec(transcript_fsa_list) + + return transcript_fsa_vec diff --git a/icefall/utils.py b/icefall/utils.py index 947d79438..8fda3a4ca 100644 --- a/icefall/utils.py +++ b/icefall/utils.py @@ -263,6 +263,70 @@ def get_texts( return aux_labels.tolist() +def encode_supervisions_otc( + supervisions: dict, + subsampling_factor: int, + token_ids: Optional[List[List[int]]] = None, +) -> Tuple[torch.Tensor, Union[List[str], List[List[int]]]]: + """ + Encodes Lhotse's ``batch["supervisions"]`` dict into + a pair of torch Tensor, and a list of transcription strings or token indexes + + The supervision tensor has shape ``(batch_size, 3)``. + Its second dimension contains information about sequence index [0], + start frames [1] and num frames [2]. + + The batch items might become re-ordered during this operation -- the + returned tensor and list of strings are guaranteed to be consistent with + each other. + """ + supervision_segments = torch.stack( + ( + supervisions["sequence_idx"], + torch.div( + supervisions["start_frame"], + subsampling_factor, + rounding_mode="floor", + ), + torch.div( + supervisions["num_frames"], + subsampling_factor, + rounding_mode="floor", + ), + ), + 1, + ).to(torch.int32) + + indices = torch.argsort(supervision_segments[:, 2], descending=True) + supervision_segments = supervision_segments[indices] + + ids = [] + verbatim_texts = [] + sorted_ids = [] + sorted_verbatim_texts = [] + + for cut in supervisions["cut"]: + id = cut.id + if hasattr(cut.supervisions[0], "verbatim_text"): + verbatim_text = cut.supervisions[0].verbatim_text + else: + verbatim_text = "" + ids.append(id) + verbatim_texts.append(verbatim_text) + + for index in indices.tolist(): + sorted_ids.append(ids[index]) + sorted_verbatim_texts.append(verbatim_texts[index]) + + if token_ids is None: + texts = supervisions["text"] + res = [texts[idx] for idx in indices] + else: + res = [token_ids[idx] for idx in indices] + + return supervision_segments, res, sorted_ids, sorted_verbatim_texts + + @dataclass class DecodingResults: # timestamps[i][k] contains the frame number on which tokens[i][k]