// Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "COPYING.APACHE2.0"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "ctc_beam_search_decoder.h" #include #include #include #include #include #include #include "ThreadPool.h" #include "fst/fstlib.h" #include "decoder_utils.h" #include "path_trie.h" using FSTMATCH = fst::SortedMatcher; std::vector> ctc_beam_search_decoding( const std::vector> &probs_seq, const std::vector &vocabulary, size_t beam_size, double cutoff_prob, size_t cutoff_top_n, Scorer *ext_scorer, size_t blank_id) { // dimension check size_t num_time_steps = probs_seq.size(); for (size_t i = 0; i < num_time_steps; ++i) { VALID_CHECK_EQ(probs_seq[i].size(), // vocabulary.size() + 1, vocabulary.size(), "The shape of probs_seq does not match with " "the shape of the vocabulary"); } // assign space id auto it = std::find(vocabulary.begin(), vocabulary.end(), kSPACE); int space_id = it - vocabulary.begin(); // if no space in vocabulary if ((size_t)space_id >= vocabulary.size()) { space_id = -2; } // init prefixes' root PathTrie root; root.score = root.log_prob_b_prev = 0.0; std::vector prefixes; prefixes.push_back(&root); if (ext_scorer != nullptr && !ext_scorer->is_character_based()) { auto fst_dict = static_cast(ext_scorer->dictionary); fst::StdVectorFst *dict_ptr = fst_dict->Copy(true); root.set_dictionary(dict_ptr); auto matcher = std::make_shared(*dict_ptr, fst::MATCH_INPUT); root.set_matcher(matcher); } // prefix search over time for (size_t time_step = 0; time_step < num_time_steps; ++time_step) { auto &prob = probs_seq[time_step]; float min_cutoff = -NUM_FLT_INF; bool full_beam = false; if (ext_scorer != nullptr) { size_t num_prefixes = std::min(prefixes.size(), beam_size); std::sort(prefixes.begin(), prefixes.begin() + num_prefixes, prefix_compare); min_cutoff = prefixes[num_prefixes - 1]->score + std::log(prob[blank_id]) - std::max(0.0, ext_scorer->beta); full_beam = (num_prefixes == beam_size); } std::vector> log_prob_idx = get_pruned_log_probs(prob, cutoff_prob, cutoff_top_n); // loop over chars for (size_t index = 0; index < log_prob_idx.size(); index++) { auto c = log_prob_idx[index].first; auto log_prob_c = log_prob_idx[index].second; for (size_t i = 0; i < prefixes.size() && i < beam_size; ++i) { auto prefix = prefixes[i]; if (full_beam && log_prob_c + prefix->score < min_cutoff) { break; } // blank if (c == blank_id) { prefix->log_prob_b_cur = log_sum_exp( prefix->log_prob_b_cur, log_prob_c + prefix->score); continue; } // repeated character if (c == prefix->character) { prefix->log_prob_nb_cur = log_sum_exp(prefix->log_prob_nb_cur, log_prob_c + prefix->log_prob_nb_prev); } // get new prefix auto prefix_new = prefix->get_path_trie(c); if (prefix_new != nullptr) { float log_p = -NUM_FLT_INF; if (c == prefix->character && prefix->log_prob_b_prev > -NUM_FLT_INF) { log_p = log_prob_c + prefix->log_prob_b_prev; } else if (c != prefix->character) { log_p = log_prob_c + prefix->score; } // language model scoring if (ext_scorer != nullptr && (c == space_id || ext_scorer->is_character_based())) { PathTrie *prefix_to_score = nullptr; // skip scoring the space if (ext_scorer->is_character_based()) { prefix_to_score = prefix_new; } else { prefix_to_score = prefix; } float score = 0.0; std::vector ngram; ngram = ext_scorer->make_ngram(prefix_to_score); score = ext_scorer->get_log_cond_prob(ngram) * ext_scorer->alpha; log_p += score; log_p += ext_scorer->beta; } prefix_new->log_prob_nb_cur = log_sum_exp(prefix_new->log_prob_nb_cur, log_p); } } // end of loop over prefix } // end of loop over vocabulary prefixes.clear(); // update log probs root.iterate_to_vec(prefixes); // only preserve top beam_size prefixes if (prefixes.size() >= beam_size) { std::nth_element(prefixes.begin(), prefixes.begin() + beam_size, prefixes.end(), prefix_compare); for (size_t i = beam_size; i < prefixes.size(); ++i) { prefixes[i]->remove(); } } } // end of loop over time // score the last word of each prefix that doesn't end with space if (ext_scorer != nullptr && !ext_scorer->is_character_based()) { for (size_t i = 0; i < beam_size && i < prefixes.size(); ++i) { auto prefix = prefixes[i]; if (!prefix->is_empty() && prefix->character != space_id) { float score = 0.0; std::vector ngram = ext_scorer->make_ngram(prefix); score = ext_scorer->get_log_cond_prob(ngram) * ext_scorer->alpha; score += ext_scorer->beta; prefix->score += score; } } } size_t num_prefixes = std::min(prefixes.size(), beam_size); std::sort( prefixes.begin(), prefixes.begin() + num_prefixes, prefix_compare); // compute approximate ctc score as the return score, without affecting the // return order of decoding result. To delete when decoder gets stable. for (size_t i = 0; i < beam_size && i < prefixes.size(); ++i) { double approx_ctc = prefixes[i]->score; if (ext_scorer != nullptr) { std::vector output; prefixes[i]->get_path_vec(output); auto prefix_length = output.size(); auto words = ext_scorer->split_labels(output); // remove word insert approx_ctc = approx_ctc - prefix_length * ext_scorer->beta; // remove language model weight: approx_ctc -= (ext_scorer->get_sent_log_prob(words)) * ext_scorer->alpha; } prefixes[i]->approx_ctc = approx_ctc; } return get_beam_search_result(prefixes, vocabulary, beam_size); } std::vector>> ctc_beam_search_decoding_batch( const std::vector>> &probs_split, const std::vector &vocabulary, size_t beam_size, size_t num_processes, double cutoff_prob, size_t cutoff_top_n, Scorer *ext_scorer, size_t blank_id) { VALID_CHECK_GT(num_processes, 0, "num_processes must be nonnegative!"); // thread pool ThreadPool pool(num_processes); // number of samples size_t batch_size = probs_split.size(); // enqueue the tasks of decoding std::vector>>> res; for (size_t i = 0; i < batch_size; ++i) { res.emplace_back(pool.enqueue(ctc_beam_search_decoding, probs_split[i], vocabulary, beam_size, cutoff_prob, cutoff_top_n, ext_scorer, blank_id)); } // get decoding results std::vector>> batch_results; for (size_t i = 0; i < batch_size; ++i) { batch_results.emplace_back(res[i].get()); } return batch_results; } void ctc_beam_search_decode_chunk_begin(PathTrie *root, Scorer *ext_scorer) { if (ext_scorer != nullptr && !ext_scorer->is_character_based()) { auto fst_dict = static_cast(ext_scorer->dictionary); fst::StdVectorFst *dict_ptr = fst_dict->Copy(true); root->set_dictionary(dict_ptr); auto matcher = std::make_shared(*dict_ptr, fst::MATCH_INPUT); root->set_matcher(matcher); } } void ctc_beam_search_decode_chunk( PathTrie *root, std::vector &prefixes, const std::vector> &probs_seq, const std::vector &vocabulary, size_t beam_size, double cutoff_prob, size_t cutoff_top_n, Scorer *ext_scorer, size_t blank_id) { // dimension check size_t num_time_steps = probs_seq.size(); for (size_t i = 0; i < num_time_steps; ++i) { VALID_CHECK_EQ(probs_seq[i].size(), // vocabulary.size() + 1, vocabulary.size(), "The shape of probs_seq does not match with " "the shape of the vocabulary"); } // assign space id auto it = std::find(vocabulary.begin(), vocabulary.end(), kSPACE); int space_id = it - vocabulary.begin(); // if no space in vocabulary if ((size_t)space_id >= vocabulary.size()) { space_id = -2; } // init prefixes' root // // prefix search over time for (size_t time_step = 0; time_step < num_time_steps; ++time_step) { auto &prob = probs_seq[time_step]; float min_cutoff = -NUM_FLT_INF; bool full_beam = false; if (ext_scorer != nullptr) { size_t num_prefixes = std::min(prefixes.size(), beam_size); std::sort(prefixes.begin(), prefixes.begin() + num_prefixes, prefix_compare); min_cutoff = prefixes[num_prefixes - 1]->score + std::log(prob[blank_id]) - std::max(0.0, ext_scorer->beta); full_beam = (num_prefixes == beam_size); } std::vector> log_prob_idx = get_pruned_log_probs(prob, cutoff_prob, cutoff_top_n); // loop over chars for (size_t index = 0; index < log_prob_idx.size(); index++) { auto c = log_prob_idx[index].first; auto log_prob_c = log_prob_idx[index].second; for (size_t i = 0; i < prefixes.size() && i < beam_size; ++i) { auto prefix = prefixes[i]; if (full_beam && log_prob_c + prefix->score < min_cutoff) { break; } // blank if (c == blank_id) { prefix->log_prob_b_cur = log_sum_exp( prefix->log_prob_b_cur, log_prob_c + prefix->score); continue; } // repeated character if (c == prefix->character) { prefix->log_prob_nb_cur = log_sum_exp(prefix->log_prob_nb_cur, log_prob_c + prefix->log_prob_nb_prev); } // get new prefix auto prefix_new = prefix->get_path_trie(c); if (prefix_new != nullptr) { float log_p = -NUM_FLT_INF; if (c == prefix->character && prefix->log_prob_b_prev > -NUM_FLT_INF) { log_p = log_prob_c + prefix->log_prob_b_prev; } else if (c != prefix->character) { log_p = log_prob_c + prefix->score; } // language model scoring if (ext_scorer != nullptr && (c == space_id || ext_scorer->is_character_based())) { PathTrie *prefix_to_score = nullptr; // skip scoring the space if (ext_scorer->is_character_based()) { prefix_to_score = prefix_new; } else { prefix_to_score = prefix; } float score = 0.0; std::vector ngram; ngram = ext_scorer->make_ngram(prefix_to_score); score = ext_scorer->get_log_cond_prob(ngram) * ext_scorer->alpha; log_p += score; log_p += ext_scorer->beta; } prefix_new->log_prob_nb_cur = log_sum_exp(prefix_new->log_prob_nb_cur, log_p); } } // end of loop over prefix } // end of loop over vocabulary prefixes.clear(); // update log probs root->iterate_to_vec(prefixes); // only preserve top beam_size prefixes if (prefixes.size() >= beam_size) { std::nth_element(prefixes.begin(), prefixes.begin() + beam_size, prefixes.end(), prefix_compare); for (size_t i = beam_size; i < prefixes.size(); ++i) { prefixes[i]->remove(); } } } // end of loop over time return; } std::vector> get_decode_result( std::vector &prefixes, const std::vector &vocabulary, size_t beam_size, Scorer *ext_scorer) { auto it = std::find(vocabulary.begin(), vocabulary.end(), kSPACE); int space_id = it - vocabulary.begin(); // if no space in vocabulary if ((size_t)space_id >= vocabulary.size()) { space_id = -2; } // score the last word of each prefix that doesn't end with space if (ext_scorer != nullptr && !ext_scorer->is_character_based()) { for (size_t i = 0; i < beam_size && i < prefixes.size(); ++i) { auto prefix = prefixes[i]; if (!prefix->is_empty() && prefix->character != space_id) { float score = 0.0; std::vector ngram = ext_scorer->make_ngram(prefix); score = ext_scorer->get_log_cond_prob(ngram) * ext_scorer->alpha; score += ext_scorer->beta; prefix->score += score; } } } size_t num_prefixes = std::min(prefixes.size(), beam_size); std::sort( prefixes.begin(), prefixes.begin() + num_prefixes, prefix_compare); // compute aproximate ctc score as the return score, without affecting the // return order of decoding result. To delete when decoder gets stable. for (size_t i = 0; i < beam_size && i < prefixes.size(); ++i) { double approx_ctc = prefixes[i]->score; if (ext_scorer != nullptr) { std::vector output; prefixes[i]->get_path_vec(output); auto prefix_length = output.size(); auto words = ext_scorer->split_labels(output); // remove word insert approx_ctc = approx_ctc - prefix_length * ext_scorer->beta; // remove language model weight: approx_ctc -= (ext_scorer->get_sent_log_prob(words)) * ext_scorer->alpha; } prefixes[i]->approx_ctc = approx_ctc; } std::vector> res = get_beam_search_result(prefixes, vocabulary, beam_size); // pay back the last word of each prefix that doesn't end with space (for // decoding by chunk) if (ext_scorer != nullptr && !ext_scorer->is_character_based()) { for (size_t i = 0; i < beam_size && i < prefixes.size(); ++i) { auto prefix = prefixes[i]; if (!prefix->is_empty() && prefix->character != space_id) { float score = 0.0; std::vector ngram = ext_scorer->make_ngram(prefix); score = ext_scorer->get_log_cond_prob(ngram) * ext_scorer->alpha; score += ext_scorer->beta; prefix->score -= score; } } } return res; } void free_storage(std::unique_ptr &storage) { storage = nullptr; } CtcBeamSearchDecoderBatch::~CtcBeamSearchDecoderBatch() {} CtcBeamSearchDecoderBatch::CtcBeamSearchDecoderBatch( const std::vector &vocabulary, size_t batch_size, size_t beam_size, size_t num_processes, double cutoff_prob, size_t cutoff_top_n, Scorer *ext_scorer, size_t blank_id) : batch_size(batch_size), beam_size(beam_size), num_processes(num_processes), cutoff_prob(cutoff_prob), cutoff_top_n(cutoff_top_n), ext_scorer(ext_scorer), blank_id(blank_id) { VALID_CHECK_GT(this->beam_size, 0, "beam_size must be greater than 0!"); VALID_CHECK_GT( this->num_processes, 0, "num_processes must be nonnegative!"); this->vocabulary = vocabulary; for (size_t i = 0; i < batch_size; i++) { this->decoder_storage_vector.push_back( std::unique_ptr( new CtcBeamSearchDecoderStorage())); ctc_beam_search_decode_chunk_begin( this->decoder_storage_vector[i]->root, ext_scorer); } }; /** * Input * probs_split: shape [B, T, D] */ void CtcBeamSearchDecoderBatch::next( const std::vector>> &probs_split, const std::vector &has_value) { VALID_CHECK_GT(num_processes, 0, "num_processes must be nonnegative!"); // thread pool size_t num_has_value = 0; for (int i = 0; i < has_value.size(); i++) if (has_value[i] == "true") num_has_value += 1; ThreadPool pool(std::min(num_processes, num_has_value)); // number of samples size_t probs_num = probs_split.size(); VALID_CHECK_EQ(this->batch_size, probs_num, "The batch size of the current input data should be same " "with the input data before"); // enqueue the tasks of decoding std::vector> res; for (size_t i = 0; i < batch_size; ++i) { if (has_value[i] == "true") { res.emplace_back(pool.enqueue( ctc_beam_search_decode_chunk, std::ref(this->decoder_storage_vector[i]->root), std::ref(this->decoder_storage_vector[i]->prefixes), probs_split[i], this->vocabulary, this->beam_size, this->cutoff_prob, this->cutoff_top_n, this->ext_scorer, this->blank_id)); } } for (size_t i = 0; i < batch_size; ++i) { res[i].get(); } return; }; /** * Return * batch_result: shape[B, beam_size,(-approx_ctc score, string)] */ std::vector>> CtcBeamSearchDecoderBatch::decode() { VALID_CHECK_GT( this->num_processes, 0, "num_processes must be nonnegative!"); // thread pool ThreadPool pool(this->num_processes); // number of samples // enqueue the tasks of decoding std::vector>>> res; for (size_t i = 0; i < this->batch_size; ++i) { res.emplace_back( pool.enqueue(get_decode_result, std::ref(this->decoder_storage_vector[i]->prefixes), this->vocabulary, this->beam_size, this->ext_scorer)); } // get decoding results std::vector>> batch_results; for (size_t i = 0; i < this->batch_size; ++i) { batch_results.emplace_back(res[i].get()); } return batch_results; } /** * reset the state of ctcBeamSearchDecoderBatch */ void CtcBeamSearchDecoderBatch::reset_state(size_t batch_size, size_t beam_size, size_t num_processes, double cutoff_prob, size_t cutoff_top_n) { this->batch_size = batch_size; this->beam_size = beam_size; this->num_processes = num_processes; this->cutoff_prob = cutoff_prob; this->cutoff_top_n = cutoff_top_n; VALID_CHECK_GT(this->beam_size, 0, "beam_size must be greater than 0!"); VALID_CHECK_GT( this->num_processes, 0, "num_processes must be nonnegative!"); // thread pool ThreadPool pool(this->num_processes); // number of samples // enqueue the tasks of decoding std::vector> res; size_t storage_size = decoder_storage_vector.size(); for (size_t i = 0; i < storage_size; i++) { res.emplace_back(pool.enqueue( free_storage, std::ref(this->decoder_storage_vector[i]))); } for (size_t i = 0; i < storage_size; ++i) { res[i].get(); } std::vector>().swap( decoder_storage_vector); for (size_t i = 0; i < this->batch_size; i++) { this->decoder_storage_vector.push_back( std::unique_ptr( new CtcBeamSearchDecoderStorage())); ctc_beam_search_decode_chunk_begin( this->decoder_storage_vector[i]->root, this->ext_scorer); } }