参考地址:Token passing
(1)取src/fstext/deterministic-fst-test.cc,描述了怎么创建fst。
StdVectorFst* CreateBackoffFst() {
StdVectorFst *fst = new StdVectorFst();
fst->AddState(); // state 0
fst->SetStart(0);
fst->AddArc(0, StdArc(10, 10, 0.0, 1));
// AddArc(0, StdArc(10, 10, 0.0, 1))的5个数字的意思分别是:源节点、输入的transition-id、
// 输出的id(可能是word-id、phones-id等)、权重、目标节点
fst->AddState(); // state 1
fst->AddArc(1, StdArc(12, 12, 0.0, 4));
fst->AddArc(1, StdArc(0,0, 0.1,2)); // backoff from 1 to 2
fst->AddState(); // state 2
fst->AddArc(2, StdArc(13, 13, 0.2, 4));
fst->AddArc(2, StdArc(0,0, 0.3,3)); // backoff from 2 to 3
fst->AddState(); // state 3
fst->AddArc(3, StdArc(14, 14, 0.4, 4));
fst->AddState(); // state 4
fst->AddArc(4, StdArc(15, 15, 0.5, 5));
fst->AddState(); // state 5
fst->SetFinal(5, 0.6);
return fst;
}
该函数构造的fst图形如下:
(2)数据结构:
Token包含哪些内容?
4.cost(新的token cost=前向指针指向的token的cost+边的cost+该token的cost);
中间变量:
1、一个map存储当前帧解码的current token(state-id、token);
2、一个map存储前一帧解码的previous token(state-id、token);
class SimpleDecoder {
public:
// StdArc : 边(输入、输出、代价)
typedef fst::StdArc StdArc;
typedef StdArc::Weight StdWeight; // 代价
typedef StdArc::Label Label; // 输出
typedef StdArc::StateId StateId; // 输入
// 构造方法
SimpleDecoder(const fst::Fst &fst, BaseFloat beam): fst_(fst), beam_(beam) { }
~SimpleDecoder();
/// Decode this utterance.
/// Returns true if any tokens reached the end of the file (regardless of
/// whether they are in a final state); query ReachedFinal() after Decode()
/// to see whether we reached a final state.
/// 解码一段语音
bool Decode(DecodableInterface *decodable);
// 判断到没到终止节点
// 对1和2有疑惑:
// 1、是否到了解码图的终止节点
// 2、一段语音分为好多帧,这个是判断一个utt-id音频分出来的帧是否到了最后一帧
bool ReachedFinal() const;
// GetBestPath gets the decoding traceback. If "use_final_probs" is true
// AND we reached a final state, it limits itself to final states;
// otherwise it gets the most likely token not taking into account final-probs.
// fst_out will be empty (Start() == kNoStateId) if nothing was available due to
// search error.
// If Decode() returned true, it is safe to assume GetBestPath will return true.
// It returns true if the output lattice was nonempty (i.e. had states in it);
// using the return value is deprecated.
// 获得最后输出的最优路径
bool GetBestPath(Lattice *fst_out, bool use_final_probs = true) const;
/// *** The next functions are from the "new interface". ***
/// FinalRelativeCost() serves the same function as ReachedFinal(), but gives
/// more information. It returns the difference between the best (final-cost plus
/// cost) of any token on the final frame, and the best cost of any token
/// on the final frame. If it is infinity it means no final-states were present
/// on the final frame. It will usually be nonnegative.
/// 返回全局最优路径和到达终止节点的最优路径的差值
BaseFloat FinalRelativeCost() const;
/// InitDecoding initializes the decoding, and should only be used if you
/// intend to call AdvanceDecoding(). If you call Decode(), you don't need
/// to call this. You can call InitDecoding if you have already decoded an
/// utterance and want to start with a new utterance.
/// 初始化解码器,一段音频进来之后,就要先初始化解码器
void InitDecoding();
/// This will decode until there are no more frames ready in the decodable
/// object, but if max_num_frames is >= 0 it will decode no more than
/// that many frames. If it returns false, then no tokens are alive,
/// which is a kind of error state.
/// 解码一段语音
void AdvanceDecoding(DecodableInterface *decodable,
int32 max_num_frames = -1);
/// Returns the number of frames already decoded.
/// 已解码语音的帧数
int32 NumFramesDecoded() const { return num_frames_decoded_; }
private:
class Token {
// 带有令牌的维特比算法
public:
LatticeArc arc_; // We use LatticeArc so that we can separately
// store the acoustic and graph cost, in case
// we need to produce lattice-formatted output.
Token *prev_; // 前向指针,用来回溯节点(最优路径)
int32 ref_count_; // 前面有多少个节点
double cost_; // accumulated total cost up to this point.
Token(const StdArc &arc,
BaseFloat acoustic_cost,
Token *prev): prev_(prev), ref_count_(1) {
arc_.ilabel = arc.ilabel; // 输入标签
arc_.olabel = arc.olabel; // 输出标签
arc_.weight = LatticeWeight(arc.weight.Value(), acoustic_cost); // 代价权重
arc_.nextstate = arc.nextstate; // 边指向的下一个状态
if (prev) {
prev->ref_count_++; // 有前向节点,则++
cost_ = prev->cost_ + (arc.weight.Value() + acoustic_cost);
} else {
cost_ = arc.weight.Value() + acoustic_cost;
}
}
// 运算符重载,比较运算符
bool operator < (const Token &other) {
return cost_ > other.cost_;
}
static void TokenDelete(Token *tok) {
while (--tok->ref_count_ == 0) {
// 如果前面的节点只有一个,就删除【跳过这个token】;否则就不删除前面的token
Token *prev = tok->prev_;
delete tok;
if (prev == NULL)
return;
else
tok = prev;
}
#ifdef KALDI_PARANOID
KALDI_ASSERT(tok->ref_count_ > 0);
#endif
}
};
// ProcessEmitting decodes the frame num_frames_decoded_ of the
// decodable object, then increments num_frames_decoded_.
// 拓展实边(transitions-id不为0的)
void ProcessEmitting(DecodableInterface *decodable);
// 拓展实边(transitions-id为0的)
void ProcessNonemitting();
unordered_map cur_toks_; // 当前解码的token
unordered_map prev_toks_; // 前一帧解码的token
const fst::Fst &fst_; // HCLG
BaseFloat beam_;
// Keep track of the number of frames decoded in the current file.
int32 num_frames_decoded_; // 已经解码了多少帧
// 清除token
static void ClearToks(unordered_map &toks);
static void PruneToks(BaseFloat beam, unordered_map *toks);
KALDI_DISALLOW_COPY_AND_ASSIGN(SimpleDecoder);
};
实现步骤:
(1)初始化(ProcessNonemitting):
[1.1]、初始化一个token(0,0,0,HCLG.fst的start state),将其加入到current token中。
[1.2]、新建一个queue队列,遍历current token中的每一个元素,将每个元素的state-id加入到queue中,并且找到该current token中的最小cost,最小cost+beam(beam为一个数值,由自己设定)作为一个剪枝的阈值cost。
[1.3]、遍历步骤[1.2]生成的队列queue,针对queue中的每一个元素start-id,得到该state-id对应的token(记为token1),在HCLG.fst中遍历它的所有出边。针对它的所有出边:
[1.3.1]如果出边的输入标签不为零,则跳过;
[1.3.2]如果出边的输入标签为零,则新建一个token(前一个token就是token1),比较新token的cost与阈值cost的大小,
[1.3.2.1]比阈值cost大时,就删除该token;
[1.3.2.2]比阈值cost小时:
[1.3.2.2.1].如果该出边指向的下一个state-id没有token,就把(start-id、新token)加入到current token中,把state-id加入到queue中。
[1.3.2.2.2]如果该出边指向的下一个state-id有token,比较新旧token cost,保存cost小的token,删除cost大的token(相当于此处会进行token的更新)。
[1.4]、遍历完queue,直至为空。步骤[3]中会生成一个current token的map1,map1中存放了初始化过程中遇到的所有state-id token。类似于一个二叉树广度优先搜索,但是只搜索满足条件的一些节点,并为每一个节点建了一个token,token中存放达到该节点的最小cost、和到达该节点cost最小的路径中的前一个节点。
(2)寻优(AdvanceDecoding):
[2.1]、以帧为单位循环遍历,每一个帧经过如下处理:1.清空previous token,交换previous token和current token、2.遍历出边不为零的state-id(ProcessEmitting)、3.遍历出边为零的state-id(ProcessNonemitting)、4.剪枝token。
[2.2]、清空previous token,交换previous token和current token。
[2.3]、遍历出边不为零的state-id(ProcessEmitting):
[2.3.1].初始化cost为无穷大,遍历previous token中的每一个元素,得到state-id和token2。
[2.3.2].遍历token1的所有出边:
[2.3.2.1].如果出边的输入标签为零,则跳过;
[2.3.2.2].如果到出边指向的下一个节点的cost比无穷大还大,则跳过;
[2.3.2.3].如果出边的输入标签不为零,则新建一个token(前一个token就是token2);
[2.3.2.3.1].如果该出边指向的下一个state-id没有token,就把(start-id、新token)加入到current token中。
[2.3.2.3.2].如果该出边指向的下一个state-id有token,比较新旧token cost,保存cost小的token,删除cost大的token(相当于此处会进行token的更新)。
[2.4]、遍历出边为零的state-id(ProcessNonemitting),和初始化一样。
[2.5]、剪枝token(处理current token)。
[2.5.1].遍历current token,找到cost的最小值。
[2.5.2].cost的最小值+beam得到一个阈值cost。
[2.5.3].再一次遍历current token,删除所有cost大于阈值cost的token,得到一个新的current token,用于第[6]中的计算。
[2.6]、循环步骤[2]、步骤[3]、步骤[4],步骤[5]。
Token Passing的步骤如上,是基于Kaldi简单解码得出的步骤。
总结1:Token Passing类似于对一个二叉树进行广度优先搜索,但是只搜索满足条件的一些节点,并为每一个节点建了一个token,token中存放达到该节点路径的最小cost、和到达该节点cost最小的路径中的前一个节点的指针。其中token会在每次遍历过程中更新。Token Passing是以帧为单位进行遍历的,会有很多次的广度优先搜索。
总结2:Token Passing遍历过程中,遍历输入标签为零的出边时,会进行广度优先遍历+深度优先遍历(会在遍历过程中添加遍历项);遍历输入标签不为零的出边时,只遍历固定的节点(上次遍历结束设定好的)。
// decoder/simple-decoder.cc
// Copyright 2009-2011 Microsoft Corporation
// 2012-2013 Johns Hopkins University (author: Daniel Povey)
// See ../../COPYING 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
//
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
// WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
// MERCHANTABLITY OR NON-INFRINGEMENT.
// See the Apache 2 License for the specific language governing permissions and
// limitations under the License.
#include "decoder/simple-decoder.h"
#include "fstext/remove-eps-local.h"
#include
namespace kaldi {
SimpleDecoder::~SimpleDecoder() {
ClearToks(cur_toks_);
ClearToks(prev_toks_);
}
// 由gmm-decode-simple.cc中的decoder.Decode(&gmm_decodable);调用
bool SimpleDecoder::Decode(DecodableInterface *decodable) {
std::cout<<"--simple decoder start."<NumFramesReady();
// num_frames_ready must be >= num_frames_decoded, or else
// the number of frames ready must have decreased (which doesn't
// make sense) or the decodable object changed between calls
// (which isn't allowed).
// num_frames_decoded_ : 已经解码的帧数 【保证 音频的帧数大于等于当前解码的帧数】
KALDI_ASSERT(num_frames_ready >= num_frames_decoded_);
int32 target_frames_decoded = num_frames_ready;
// max_num_frames为-1,该循环不走
if (max_num_frames >= 0)
target_frames_decoded = std::min(target_frames_decoded,
num_frames_decoded_ + max_num_frames);
// 循环解码【一帧一帧的解码】
while (num_frames_decoded_ < target_frames_decoded) {
// note: ProcessEmitting() increments num_frames_decoded_
// 清除了prev token
ClearToks(prev_toks_);
// current toekn和prev token交换位置
cur_toks_.swap(prev_toks_);
ProcessEmitting(decodable);
ProcessNonemitting();
// 处理令牌(剪枝)
PruneToks(beam_, &cur_toks_);
}
}
bool SimpleDecoder::ReachedFinal() const {
for (unordered_map::const_iterator iter = cur_toks_.begin();
iter != cur_toks_.end();
++iter) {
if (iter->second->cost_ != std::numeric_limits::infinity() &&
fst_.Final(iter->first) != StdWeight::Zero())
return true;
}
return false;
}
BaseFloat SimpleDecoder::FinalRelativeCost() const {
// as a special case, if there are no active tokens at all (e.g. some kind of
// pruning failure), return infinity.
double infinity = std::numeric_limits::infinity();
if (cur_toks_.empty())
return infinity;
double best_cost = infinity,
best_cost_with_final = infinity;
for (unordered_map::const_iterator iter = cur_toks_.begin();
iter != cur_toks_.end();
++iter) {
// Note: Plus is taking the minimum cost, since we're in the tropical
// semiring.
best_cost = std::min(best_cost, iter->second->cost_);
best_cost_with_final = std::min(best_cost_with_final,
iter->second->cost_ +
fst_.Final(iter->first).Value());
}
BaseFloat extra_cost = best_cost_with_final - best_cost;
if (extra_cost != extra_cost) { // NaN. This shouldn't happen; it indicates some
// kind of error, most likely.
KALDI_WARN << "Found NaN (likely search failure in decoding)";
return infinity;
}
// Note: extra_cost will be infinity if no states were final.
return extra_cost;
}
// Outputs an FST corresponding to the single best path
// through the lattice.
bool SimpleDecoder::GetBestPath(Lattice *fst_out, bool use_final_probs) const {
fst_out->DeleteStates();
Token *best_tok = NULL;
bool is_final = ReachedFinal();
if (!is_final) {
for (unordered_map::const_iterator iter = cur_toks_.begin();
iter != cur_toks_.end();
++iter)
if (best_tok == NULL || *best_tok < *(iter->second) )
best_tok = iter->second;
} else {
double infinity =std::numeric_limits::infinity();
double best_cost = infinity;
for (unordered_map::const_iterator iter = cur_toks_.begin();
iter != cur_toks_.end();
++iter) {
double this_cost = iter->second->cost_ + fst_.Final(iter->first).Value();
if (this_cost != infinity && this_cost < best_cost) {
best_cost = this_cost;
best_tok = iter->second;
}
}
}
if (best_tok == NULL)
return false; // No output.
// 由best token通过prev_指针,往回找,直到找到最初的开始点,形成一个链,保存在arcs_reverse中
std::vector arcs_reverse; // arcs in reverse order. 最优路径
for (Token *tok = best_tok; tok != NULL; tok = tok->prev_)
arcs_reverse.push_back(tok->arc_);
KALDI_ASSERT(arcs_reverse.back().nextstate == fst_.Start());
arcs_reverse.pop_back(); // that was a "fake" token... gives no info.
// 把最优路径保存在fst_out中。
StateId cur_state = fst_out->AddState();
fst_out->SetStart(cur_state);
for (ssize_t i = static_cast(arcs_reverse.size())-1; i >= 0; i--) {
LatticeArc arc = arcs_reverse[i];
arc.nextstate = fst_out->AddState();
fst_out->AddArc(cur_state, arc);
cur_state = arc.nextstate;
}
if (is_final && use_final_probs)
fst_out->SetFinal(cur_state, LatticeWeight(fst_.Final(best_tok->arc_.nextstate).Value(), 0.0));
else
fst_out->SetFinal(cur_state, LatticeWeight::One());
fst::RemoveEpsLocal(fst_out);
return true;
}
void SimpleDecoder::ProcessEmitting(DecodableInterface *decodable) {
int32 frame = num_frames_decoded_;
// Processes emitting arcs for one frame. Propagates from
// prev_toks_ to cur_toks_.
// 初始化剪枝cost为正无穷大
double cutoff = std::numeric_limits::infinity();
// 遍历prev token
for (unordered_map::iterator iter = prev_toks_.begin();
iter != prev_toks_.end();
++iter) {
StateId state = iter->first;
Token *tok = iter->second;
KALDI_ASSERT(state == tok->arc_.nextstate);
// 遍历当前token的出边【遍历从state-id节点出发的每一条弧】
for (fst::ArcIterator > aiter(fst_, state);
!aiter.Done(); aiter.Next()) {
const StdArc &arc = aiter.Value();
// 如果出边的输入transition-id不为0时,
if (arc.ilabel != 0) { // propagate..
//
BaseFloat acoustic_cost = -decodable->LogLikelihood(frame, arc.ilabel);
// 总cost = token的cost(0) + 边的cost + 声学模型cost
double total_cost = tok->cost_ + arc.weight.Value() + acoustic_cost;
// 剪枝去掉total cost大的出边
if (total_cost >= cutoff)
continue;
//
if (total_cost + beam_ < cutoff)
cutoff = total_cost + beam_; // 更新剪枝cost:cutoff
// 新建一个token:规定走那条边,代价,上一个token
Token *new_tok = new Token(arc, acoustic_cost, tok);
// 同出边的目的state-id去找下一个token:find_iter
unordered_map::iterator find_iter
= cur_toks_.find(arc.nextstate); // 查找这条边的下一个节点是否有令牌
// 如果没有令牌,就新建一个令牌,保存到cur_toks_中
if (find_iter == cur_toks_.end()) {
cur_toks_[arc.nextstate] = new_tok;
} else {
// 如果有令牌,比较cost的大小,如果新的cost小,就删除老的,更新称新的
if ( *(find_iter->second) < *new_tok ) {
Token::TokenDelete(find_iter->second);
find_iter->second = new_tok;
} else {
// 如果新的cost大,就把新的删掉
Token::TokenDelete(new_tok);
}
}
}
}
}
num_frames_decoded_++;
}
void SimpleDecoder::ProcessNonemitting() {
// Processes nonemitting arcs for one frame. Propagates within
// cur_toks_.
// 新建一个队列,局部变量
std::vector queue;
// 一开始的代价为无穷大
double infinity = std::numeric_limits::infinity();
double best_cost = infinity;
// 遍历current tokens【cur_toks_是一个map,first为StateId,second为Token*】
// 遍历current tokens,可以得到cur_token_中的所有节点,组成一个队列;然后再遍历这个节点的每一个出边
for (unordered_map::iterator iter = cur_toks_.begin();
iter != cur_toks_.end();
++iter) {
// 将current token的state-id放在队列中
queue.push_back(iter->first);
// 找到最小的代价
best_cost = std::min(best_cost, iter->second->cost_);
}
// 进行剪枝
double cutoff = best_cost + beam_;
// 遍历队列【队列中存放的是current token的state-id】
while (!queue.empty()) {
StateId state = queue.back();
queue.pop_back();
Token *tok = cur_toks_[state]; // 由state-id得到token
KALDI_ASSERT(tok != NULL && state == tok->arc_.nextstate);
// 遍历token的出边【通过HCLG】
for (fst::ArcIterator > aiter(fst_, state);
!aiter.Done();
aiter.Next()) {
const StdArc &arc = aiter.Value();
// 如果出边的输入transition-id为0时,
if (arc.ilabel == 0) { // propagate nonemitting only...
const BaseFloat acoustic_cost = 0.0;
Token *new_tok = new Token(arc, acoustic_cost, tok);
if (new_tok->cost_ > cutoff) {
// 如果新边的cost大于最大的剪枝cost,就把这个出边剪枝掉了
Token::TokenDelete(new_tok);
} else {
unordered_map::iterator find_iter = cur_toks_.find(arc.nextstate);
用nextstate去找,发现没有找到,返回了end()。
if (find_iter == cur_toks_.end()) {
// 表示如果下一个状态上还没有令牌token,那么就将此令牌放到下一个状态上。
cur_toks_[arc.nextstate] = new_tok;
// 更新队列,感觉就会打HCLG.fst中的所有non-emitting的全部遍历一遍
queue.push_back(arc.nextstate);
} else {
if ( *(find_iter->second) < *new_tok ) {
// 如果下一个状态的令牌不比新令牌cost小,则用新令牌替换下一个状态的令牌
Token::TokenDelete(find_iter->second);
find_iter->second = new_tok;
queue.push_back(arc.nextstate);
} else {
// 如果下一个状态的令牌比新令牌cost小,则删除新令牌
Token::TokenDelete(new_tok);
}
}
}
}
}
}
}
// static
void SimpleDecoder::ClearToks(unordered_map &toks) {
for (unordered_map::iterator iter = toks.begin();
iter != toks.end(); ++iter) {
Token::TokenDelete(iter->second);
}
toks.clear();
}
// static
void SimpleDecoder::PruneToks(BaseFloat beam, unordered_map *toks) {
if (toks->empty()) {
KALDI_VLOG(2) << "No tokens to prune.\n";
return;
}
double best_cost = std::numeric_limits::infinity();
for (unordered_map::iterator iter = toks->begin();
iter != toks->end(); ++iter)
// 遍历当前token map,找到cost最小的那一个;入参toks为刚新生成的token map
best_cost = std::min(best_cost, iter->second->cost_);
std::vector retained;
double cutoff = best_cost + beam;
for (unordered_map::iterator iter = toks->begin();
iter != toks->end(); ++iter) {
if (iter->second->cost_ < cutoff)
retained.push_back(iter->first);
else
Token::TokenDelete(iter->second);
}
unordered_map tmp;
for (size_t i = 0; i < retained.size(); i++) {
tmp[retained[i]] = (*toks)[retained[i]];
}
KALDI_VLOG(2) << "Pruned to " << (retained.size()) << " toks.\n";
// 保留剪枝后的token map
tmp.swap(*toks);
}
} // end namespace kaldi.
假如HCLG如下图所示:
1、HCLG.fst的起始节点state-id=0,新建token1(输入标签:0,输出标签:0,cost:0,下一个state-id就是HCLG.fst的起始节点:0),则current token中就只有一个元素state-id:0 token1。
2、新建一个queue,保存current token中每一个元素的state-id;这里只有一个元素state-id:0。遍历queue的每一个元素,针对每一个元素在HCLG.fst中遍历它的所有输入标签为零的出边,这里没有,则遍历结束。最终current token中只有一个元素state-id=0 token1 cost=0。
3、将current token中的元素全部放入previous token中,则previous中也只有一个元素:token1。遍历previous token中所有元素,则token1的所有输入标签不为零的出边,这里只有一个出边1(10,10,0,1),由于state-id=1上没有token,就新建了一个token2(10,10,0,token1),这里current token中只有一个元素state-id=1 token2 cost=0。
4、新建一个queue,保存current token中每一个元素的state-id;这里只有一个元素state-id:1。遍历queue的每一个元素,针对每一个元素在HCLG.fst中遍历它的所有输入标签为零的出边,这里只有一个出边2(0,0,0.1,2)。由于state-id=2上没有token,就新建了一个token3(0,0,0.1,token2)cost=0.1,这里current token中就有两个元素state-id=1 token2 cost=0和state-id=2 token3 cost=0.1。然后将state-id=2加入到queue中,再次遍历queue中,由于state-id=2有一个输入标签为零的出边3(0,0,0.3,3),由于state-id=3上没有token,新建token4(0,0,0.4,token3),这里current token中就有三个元素state-id=1 token2 cost=0、state-id=2 token3 cost=0.1和state-id=3 token4 cost=0.4。然后将state-id=3加入到queue中,遍历queue的每一个state-id的所有输入标签为零的出边,这里state-id=3没有,则遍历结束。这时current token中就有三个元素state-id=1 token2 cost=0、state-id=2 token3 cost=0.1和state-id=3 token4 cost=0.4。
5、剪枝current token,如果beam比较大,这里的token都满足条件;如果beam比较小,则state-id=2 token3和state-id=4 token4的cost比state-id=1 token2的cost大,会被剪枝掉。这里使用剪枝操作,后面会更好理解。
6、将current token中的元素全部放入previous token中,则previous中也只有一个元素:state-id=1 token2 cost=0。遍历previous token中所有元素,则token1的所有输入标签不为零的出边,这里只有一个出边4(12,2,0,4),由于state-id=4上没有token,就新建了一个token5(12,2,0,token2),这里current token中只有一个元素state-id=4 token5 cost=0。
7、新建一个queue,保存current token中每一个元素的state-id;这里只有一个元素state-id:4。遍历queue的每一个元素,针对每一个元素在HCLG.fst中遍历它的所有输入标签不为零的出边,这里没有,则遍历结束。最终current token中只有一个元素state-id=4 token5 cost=0。
8、剪枝current token,这里不使用剪枝操作,后面会更好理解。
9、将current token中的元素全部放入previous token中,则previous中也只有一个元素:token5。遍历previous token中所有元素,则token5的所有输入标签不为零的出边,这里只有一个出边5(15,15,0.5,5),由于state-id=5上没有token,就新建了一个token6(15,15,0.5,token5),这里current token中只有一个元素state-id=5 token6 cost=0.5。因为state-id=5是HCLG.fst的结束节点。遍历到这里就结束了。
10、路径中有token6((15,15,0.5),token5)、token5((0,0,0),token2)、token2((0,0,0),token1)、token1((0,0,0),NULL)。路径从结束节点往前回溯,找到最优路径上的每一个节点。