Initial commit: FunASR Speech Recognition Toolkit
Update API Documentation / build-api-docs (push) Has been cancelled

Add complete FunASR codebase including models, runtime, and documentation.
This commit is contained in:
freedakgmail
2026-07-09 22:38:58 +08:00
commit 6116b1f3c6
3683 changed files with 990984 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
all:
include ../kaldi.mk
EXTRA_CXXFLAGS += -Wno-sign-compare
TESTFILES = kaldi-lattice-test push-lattice-test minimize-lattice-test \
determinize-lattice-pruned-test word-align-lattice-lexicon-test
OBJFILES = kaldi-lattice.o lattice-functions.o \
lattice-functions-transition-model.o word-align-lattice.o \
phone-align-lattice.o word-align-lattice-lexicon.o sausages.o \
push-lattice.o minimize-lattice.o determinize-lattice-pruned.o \
confidence.o compose-lattice-pruned.o
LIBNAME = kaldi-lat
ADDLIBS = ../hmm/kaldi-hmm.a ../tree/kaldi-tree.a ../util/kaldi-util.a \
../matrix/kaldi-matrix.a ../base/kaldi-base.a
include ../makefiles/default_rules.mk
@@ -0,0 +1,153 @@
// lat/arctic-weight.h
// Copyright 2012 Johns Hopkins University (Author: Guoguo Chen)
// 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.
#ifndef KALDI_LAT_ARCTIC_WEIGHT_H_
#define KALDI_LAT_ARCTIC_WEIGHT_H_
#include "fst/float-weight.h"
namespace fst {
// Arctic semiring: (max, +, inf, 0)
// We define the Arctic semiring T' = (R \cup {-inf, +inf}, max, +, -inf, 0).
// The term "Arctic" came from Keith Kintzley (kintzley@jhu.edu), as opposite
// to the Tropical semiring.
template <class T>
class ArcticWeightTpl : public FloatWeightTpl<T> {
public:
using FloatWeightTpl<T>::Value;
typedef ArcticWeightTpl<T> ReverseWeight;
ArcticWeightTpl() : FloatWeightTpl<T>() {}
ArcticWeightTpl(T f) : FloatWeightTpl<T>(f) {}
ArcticWeightTpl(const ArcticWeightTpl<T> &w) : FloatWeightTpl<T>(w) {}
static const ArcticWeightTpl<T> Zero() {
return ArcticWeightTpl<T>(-std::numeric_limits<T>::infinity()); }
static const ArcticWeightTpl<T> One() {
return ArcticWeightTpl<T>(0.0F); }
static const std::string &Type() {
static const std::string type = std::string("arctic") +
FloatWeightTpl<T>::GetPrecisionString();
return type;
}
static ArcticWeightTpl<T> NoWeight() {
return ArcticWeightTpl<T>(std::numeric_limits<T>::infinity());
}
bool Member() const {
// First part fails for IEEE NaN
return Value() == Value() && Value() != std::numeric_limits<T>::infinity();
}
ArcticWeightTpl<T> Quantize(float delta = kDelta) const {
if (Value() == -std::numeric_limits<T>::infinity() ||
Value() == std::numeric_limits<T>::infinity() ||
Value() != Value())
return *this;
else
return ArcticWeightTpl<T>(floor(Value()/delta + 0.5F) * delta);
}
ArcticWeightTpl<T> Reverse() const { return *this; }
static uint64 Properties() {
return kLeftSemiring | kRightSemiring | kCommutative |
kPath | kIdempotent;
}
};
// Single precision arctic weight
typedef ArcticWeightTpl<float> ArcticWeight;
template <class T>
inline ArcticWeightTpl<T> Plus(const ArcticWeightTpl<T> &w1,
const ArcticWeightTpl<T> &w2) {
return w1.Value() > w2.Value() ? w1 : w2;
}
inline ArcticWeightTpl<float> Plus(const ArcticWeightTpl<float> &w1,
const ArcticWeightTpl<float> &w2) {
return Plus<float>(w1, w2);
}
inline ArcticWeightTpl<double> Plus(const ArcticWeightTpl<double> &w1,
const ArcticWeightTpl<double> &w2) {
return Plus<double>(w1, w2);
}
template <class T>
inline ArcticWeightTpl<T> Times(const ArcticWeightTpl<T> &w1,
const ArcticWeightTpl<T> &w2) {
T f1 = w1.Value(), f2 = w2.Value();
if (f1 == -std::numeric_limits<T>::infinity())
return w1;
else if (f2 == -std::numeric_limits<T>::infinity())
return w2;
else
return ArcticWeightTpl<T>(f1 + f2);
}
inline ArcticWeightTpl<float> Times(const ArcticWeightTpl<float> &w1,
const ArcticWeightTpl<float> &w2) {
return Times<float>(w1, w2);
}
inline ArcticWeightTpl<double> Times(const ArcticWeightTpl<double> &w1,
const ArcticWeightTpl<double> &w2) {
return Times<double>(w1, w2);
}
template <class T>
inline ArcticWeightTpl<T> Divide(const ArcticWeightTpl<T> &w1,
const ArcticWeightTpl<T> &w2,
DivideType typ = DIVIDE_ANY) {
T f1 = w1.Value(), f2 = w2.Value();
if (f2 == -std::numeric_limits<T>::infinity())
return std::numeric_limits<T>::quiet_NaN();
else if (f1 == -std::numeric_limits<T>::infinity())
return -std::numeric_limits<T>::infinity();
else
return ArcticWeightTpl<T>(f1 - f2);
}
inline ArcticWeightTpl<float> Divide(const ArcticWeightTpl<float> &w1,
const ArcticWeightTpl<float> &w2,
DivideType typ = DIVIDE_ANY) {
return Divide<float>(w1, w2, typ);
}
inline ArcticWeightTpl<double> Divide(const ArcticWeightTpl<double> &w1,
const ArcticWeightTpl<double> &w2,
DivideType typ = DIVIDE_ANY) {
return Divide<double>(w1, w2, typ);
}
} // namespace fst
#endif // KALDI_LAT_ARCTIC_WEIGHT_H_
@@ -0,0 +1,955 @@
// lat/compose-lattice-pruned.cc
// Copyright 2009-2012 Microsoft Corporation
// 2012-2013 Johns Hopkins University (Author: Daniel Povey)
// 2014 Guoguo Chen
// 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 "lat/compose-lattice-pruned.h"
#include "lat/lattice-functions.h"
namespace kaldi {
/**
PrunedCompactLatticeComposer implements an algorithm for pruned composition.
It uses a heuristic (like the heuristics used in A*) to estimate the
cost to the end of a graph, of the best path that we might get if
we expand a particular transition out of a particular state. This enables
us to use a priority queue to expand arcs in the composed result in an
order roughly from most promising to least promising.
Because some of the quantities used in the heuristic are hard to efficiently
keep updated as the composed output is incrementally added to, we
periodically recompute these quantities (c.f. RecomputePruningInfo()).
In order to prevent this periodic recomputation from dominating the time
taken to produce the lattice, we recompute these things on a schedule where,
between each computation, we allow the size of the output to grow by
a constant factor (default: 1.5). Since the time taken to do the
recomputation of quantities used in the heuristic takes time linear in the
size of the so-far existing composed output, doing so on this type of schedule
will add no more than a constant factor to the runtime.
*/
class PrunedCompactLatticeComposer {
public:
PrunedCompactLatticeComposer(
const ComposeLatticePrunedOptions &opts,
const CompactLattice &clat,
fst::DeterministicOnDemandFst<fst::StdArc> *det_fst,
CompactLattice* composed_clat);
// Does the composition. You must call this just once per object.
void Compose();
private:
// Gets the num-arcs limit for this iteration of the algorithm, which will be
// opts_.initial_num_arcs if there are currently no arcs; or otherwise
// opts_.growth_ration * the current number of arcs (subject to the
// opts_.max_arcs limit if we have already reached a final-state). This helps
// ensure that we call RecomputePruningInfo() on an appropriate schedule.
int32 GetCurrentArcLimit() const;
// This function, called just once at the start, computes all the static
// information about the input lattice 'clat', in lat_state_info_. (however,
// the 'composed_states' members are just set to the empty vector for now.
void ComputeLatticeStateInfo();
// Called just once at the start, this sets up the first state in the
// composed output.
void AddFirstState();
// This function processes the next un-expanded transition (or final-state)
// out of the composed state numbered 'composed_state_to_expand'.
void ProcessQueueElement(int32 composed_state_to_expand);
// This is a part of ProcessQueueElements() that has been broken out
// for clarity. it process the arc_index'th arc out of this source state.
void ProcessTransition(int32 composed_src_state,
int32 arc_index);
// This function recomputes certain members of the ComposedStateInfo relating
// to the output states: namely, 'forward_cost', 'backward_cost' and
// 'delta_backward_cost'. In between calls to this function, we try to
// keep those quantities as accurate as possible, but they aren't
// completely accurate (see comments by their declarations for more info).
void RecomputePruningInfo();
// Sets '*composed_states' to a list of the states that currently
// exist in the composed output, in topologically sorted order.
// At exit, *composed_states will be a permutation of numbers
// [0, 1, ... clat_out_->NumStates() - 1], beginning with the
// start-state 0.
void GetTopsortedStateList(std::vector<int32> *composed_states) const;
// Called from RecomputePruningInfo(), this computes all the 'forward_cost'
// and 'prev_composed_state' members of the ComposedStateInfo.
// @param [in] composed_states This is expected to be a list,
// in topological order, of all currently existing composed states,
// as produced by GetTopsortedStateList().
void ComputeForwardCosts(const std::vector<int32> &composed_states);
// Called from RecomputePruningInfo(), this computes all the 'backward_cost'
// members of the ComposedStateInfo. It also sets 'output_best_cost_'.
// 'composed_states' is expected to be a list, in topological order, of all
// currently existing composed states, as produced by GetTopsortedStateList().
void ComputeBackwardCosts(const std::vector<int32> &composed_states);
// Called from RecomputePruningInfo(), this computes all the
// 'delta_backward_cost' members of the ComposedStateInfo. 'composed_states'
// is expected to be a list, in topological order, of all currently existing
// composed states, as produced by GetTopsortedStateList(). It also computes
// the 'expected_cost_offset' values for all states, and uses them recreate
// 'composed_state_queue_'.
void ComputeDeltaBackwardCosts(const std::vector<int32> &composed_states);
// This struct contains information about a state of the input lattice.
struct LatticeStateInfo {
// 'backward_cost' is the total cost of the best path from this state to
// the final state in the source lattice, including the final-prob.
double backward_cost;
// 'arc_delta_costs' is an array, one for each arc (and the final-prob, if
// present), showing how much the cost to the final-state for the best path
// starting in this state and exiting through each arc (or final-prob),
// differs from 'backward_cost'. Specifically, it contains pairs
// (delta_cost, arc_index), where delta_cost >= 0 and arc_index is
// either the index into this state's array of arcs (for arcs), or -1
// if this represents the final-prob.
//
// 'arc_delta_costs' will be sorted, so that the first element has
// .first=0.0 and the delta-costs will be increasing order. This means that
// we expand them from the start of the array, in order to process the best
// arcs first.
// lat_state_info_[i].arc_delta_costs.size() will equal will equal
// clat_.NumStates(i), plus one if clat_.Final(i) != Zero().
std::vector<std::pair<BaseFloat, int32> > arc_delta_costs;
// 'composed_states' is a list of the state-ids in the composed output
// that correspond to this state in the lattice, so we expect
// that composed_state_info_[composed_states[i]].lat_state
// equals the index of this lattice state. This is helpful in
// accessing the states in the output lattice in topological
// order.
std::vector<int32> composed_states;
};
// This struct contains information about a state of the composed
// output.
struct ComposedStateInfo {
// 'lat_state' and 'lm_state' form the pair of states in the two FSTs
// that this state corresponds to. The unordered map 'pair_to_state_' maps these
// state-pairs to the index of the composed state (the state-index in clat_out_).
int32 lat_state;
int32 lm_state;
// the number of arcs on the path from the start state to this state, in the
// composed lattice, by which this state was first reached.
int32 depth;
// If you have just called RecomputePruningInfo(), then
// 'forward_cost' will equal the cost of the best path from the start-state
// to this state, in the composed output.
//
// In between calls to RecomputePruningInfo() it may not always be fully up
// to date; instead it will be an upper bound on what it would be if you had
// just called RecomputePruningInfo(); it will be the cost of some path but
// not necessarily the best path.
double forward_cost;
// 'backward_cost' relates to the cost from this state to the final-state in
// the composed FST. (By this we mean, more precisely, the cost of the best
// path from this state to any final state, including the final-prob in that
// final state).
//
// If we have just called RecomputePruningInfo(), then the following rules
// specify what the value of 'backward_cost' will be:
// - If a final state is reachable from this state, backward_cost
// will contain the cost of the best path from this state to the
// final state (including the corresponding final-prob).
// - Otherwise, it will contain +infinity.
//
// If RecomputePruningInfo() has not just been called), it may contain any
// value that is >= the value the the rules above specify (since, for
// existing states, we don't modify it between calls to
// RecomputePruningInfo()). For states that have been added since
// RecomputePruningInfo() was last called, it will be infinity.
double backward_cost;
// 'delta_backward_cost' is a quantity that is used in our heuristic of the
// cost to an end-state from expanding a previously un-expanded arc. It is
// an estimate of the difference between the backward cost in this struct
// (this->backward_cost) and the backward cost in the input lattice
// (LatticeStateInfo::backward_cost). This term reflects the anticipated
// extra costs from 'det_fst_', which, while fairly close to zero, may be
// substantial enough to want to correct for.
//
// The following is the value that 'delta_backward_cost' will have if
// RecomputePruningInfo() has just been called:
// - If backward_cost is finite (this state in the composed result can
// reach the final state via currently expanded states), then
// delta_backward_cost is this->backward_cost minus
// lat_state_info_[lat_state].backward_cost. (It will mostly, but
// not always, be <= 0, reflecting that the new LM is better than
// the old LM).
// - On the other hand, if backward_cost is infinite: delta_backward_cost
// is set to the delta_backward_cost of the previous state on the best
// path from the start state of the composed result to this state (or
// zero if this is the start state).
//
// If RecomputePruningInfo() has not just been called, then:
// - For states created since RecomputePruningInfo() was last called,
// delta_backward_cost will be inherited from the source state from
// which the new state was expanded.
// - For other states, delta_backward_cost will be unchanged since
// RecomputePruningInfo() was last called.
// The above rules may make the delta_backward_cost a less accurate, but
// still probably reasonable, heuristic. What it is a heuristic for,
// is: if we were to successfully reach an end-state of the composed output
// from this state, what would be the resulting backward_cost
// minus lat_state_info_[lat_state].backward_cost.
BaseFloat delta_backward_cost;
// 'prev_composed_state' is the previous state on the best path from
// the start-state to the current state (or -1 if this is the start state).
// It is computed in RecomputePruningInfo() when setting up 'forward_cost',
// and then used to compute delta_backward_cost. It is not otherwise
// used.
int32 prev_composed_state;
// 'sorted_arc_index' is an index into the 'arc_delta_costs' array which is
// a member of the LatticeStateInfo object corresponding to the lattice
// state 'lat_state'. It corresponds to the next arc (or final-prob) out of
// the input lattice that we have yet to expand in the composition; or -1 if
// we have expanded all of them. When we first reach a composed state,
// 'sorted_arc_index' will be zero; then it will increase one at a time as
// we expand arcs until either the composition terminates or we have
// expanded all the arcs and it becomes -1.
int32 sorted_arc_index;
// 'arc_delta_cost' is a derived quantity that we store here for easier
// access. Suppose this_lat_info is lat_state_info_[lat_state]; then
// if sorted_arc_index >= 0, then:
// arc_delta_cost == this_lat_info.arc_delta_costs[sorted_arc_index].first
// else: arc_delta_cost == +infinity.
//
// what 'arc_delta_cost' represents (or is a heuristic for), is the expected
// cost of a path to the final-state leaving through the arc we're about to
// expand, minus the expected cost of any path to the final-state starting
// from this state.
BaseFloat arc_delta_cost;
// view 'expected_cost_offset' a phantom field of this struct, that has
// been optimized out. It's clearer if we act like it's a field, but
// actually it's not stored.
//
// 'expected_cost_offset' is a derived quantity that reflects the expected
// cost (according to our heuristic) of the best path we might encounter
// when expanding the next previously unseen arc (or final-prob),
// corresponding to 'sorted_arc_index'. (This is the expected cost of a
// successful path, from the beginning to the end of the lattice, but
// constrained to be a path that contains the arc we're about to expand).
//
// The 'offset' part is about subtracting the best cost of the lattice, so we
// can cast to float without too much loss of accuracy:
// expected_cost_offset = expected_cost - lat_best_cost_.
//
// We define expected_cost_offset by defining the 'expected_cost' part;
// for clarity:
// First, let lat_backward_cost equal the backward_cost of the LatticeStateInfo
// corresponding to 'lat_state', i.e.
// lat_backward_cost = lat_state_info_[lat_state].backward_cost. Then:
// expected_cost = forward_cost + lat_backward_cost +
// delta_backward_cost + arc_delta_cost.
// expected_cost_offset will always equal the above minus lat_best_cost_.
//
// The formula for expected_cost above is a pretty good heuristic for what
// the cost to the end-state will be. If the costs in det_fst_ were zero,
// then the expression (forward_cost + lat_backward_cost + arc_delta_cost)
// would be exact, and we would expand things in the ideal, best-first
// order. "delta_backward_cost" is a reasonable approximation for the extra
// costs from 'det_fst_'.
// BaseFloat expected_cost_offset;
};
// This bool variable is initialized to false, and will be updated to true
// the first time a Final() function is called on the det_fst_. Then we will
// immediately call RecomputeRruningInfo() so that the output_best_cost_ is
// changed from +inf to a finite value, to be used in beam search. This is the
// only time the RecomputeRruningInfo() function is called manually; otherwise
// it always follows an automatic schedule based on the num-arcs of the output
// lattice.
bool output_reached_final_;
// This variable, which we set initially to -1000, makes sure that in the
// beginning of the algorithm, we always prioritize exploring the lattice
// in a depth-first way. Once we find a path reaching a final state, this
// variable will be reset to 0.
// The reason we do this is because the beam-search depends on a good estimate
// of the composed-best-cost, which before we reach a final state, we instead
// borrow the value from best-cost from the input lattice, which is usually
// systematically worse than the RNNLM scores, and makes the algorithm spend
// a lot of time before reaching any final state, especially if the input
// lattices are large.
float depth_penalty_;
const ComposeLatticePrunedOptions &opts_;
const CompactLattice &clat_in_;
fst::DeterministicOnDemandFst<fst::StdArc> *det_fst_;
CompactLattice *clat_out_;
// This counter keeps track of the number of arcs in the output lattice
// clat_out_. When it exceeds max_arcs,
int32 num_arcs_out_;
std::vector<LatticeStateInfo> lat_state_info_;
// 'lat_best_cost' is the cost of the best path in the input lattice,
// equal to lat_state_info_[0].backward_cost (we check that 0 is the
// start state in the input lattice).
double lat_best_cost_;
// 'output_best_cost_' is the cost of the best successful path in the output
// lattice 'clat_out_'; or +infinity if 'clat_out_' does not yet have any
// successful paths. It is updated only when RecomputePruningInfo() is
// called.
double output_best_cost_;
// current_cutoff_ is a value used in deciding which composed states
// need to be included in the queue. Each time RecomputePruningInfo()
// called, current_cutoff_ is set to
// (output_best_cost_ - lat_best_cost_ + opts_.lattice_compose_beam).
// It will be +infinity if the output lattice doesn't yet have any
// successful paths. It decreases with time. You can compare the
// phantom 'expected_cost_offset' members of ComposedStateInfo with this
// value; if they are more than this value, then there is no need
// to enter the corresponding state into the queue.
BaseFloat current_cutoff_;
typedef std::priority_queue<std::pair<BaseFloat, int32>,
std::vector<std::pair<BaseFloat, int32> >,
std::greater<std::pair<BaseFloat, int32> > > QueueType;
// composed_state_queue_ is a priority queue of the composed states
// that we are intending to expand. It contains pairs
// (expected_cost_offset, composed_state_index),
// where expected_cost_offset == the phantom variable
// composed_state_info_[composed_state_index].expected_cost_offset.
// We process the states from lowest cost first.
// Every time RecomputePruningInfo() is called, this is cleared and repopulated
// (since the states' expected_cost_offset values may change), and in between
// calls to RecomputePruningInfo(), we do insert elements for newly created
// states.
QueueType composed_state_queue_;
std::vector<ComposedStateInfo> composed_state_info_;
// This maps a pair (lat_state, lm_state) to the index of the
// state in the composed FST. That would correspond to a state-id in
// clat_out_, and also to an index into 'composed_state_info_'.
unordered_map<std::pair<int32,int32>,
int32, PairHasher<int32> > pair_to_state_;
// This contains the set of state-indexes of the input lattice that already
// have states in the composed output (i.e. is in accessed_lat_states_ if and
// only if !lat_state_info_[i].composed_states.empty(). The point is to be
// able to enumerate, in order or in reverse order, just those states in the
// lattice that appear in the composed output (it's an efficiency thing that
// will matter more for early iterations of the composition, when we need
// to access the output lattice in topological order).
std::set<int32> accessed_lat_states_;
};
void PrunedCompactLatticeComposer::GetTopsortedStateList(
std::vector<int32> *composed_states) const {
composed_states->clear();
composed_states->reserve(clat_out_->NumStates());
std::set<int32>::const_iterator iter = accessed_lat_states_.begin(),
end = accessed_lat_states_.end();
for (; iter != end; ++iter) {
int32 lat_state = *iter;
const LatticeStateInfo &input_lat_info = lat_state_info_[lat_state];
composed_states->insert(composed_states->end(),
input_lat_info.composed_states.begin(),
input_lat_info.composed_states.end());
}
KALDI_ASSERT((*composed_states)[0] == 0 &&
static_cast<int32>(composed_states->size()) ==
clat_out_->NumStates());
}
int32 PrunedCompactLatticeComposer::GetCurrentArcLimit() const {
int32 current_num_arcs = num_arcs_out_;
if (current_num_arcs == 0) {
return opts_.initial_num_arcs;
} else {
KALDI_ASSERT(opts_.growth_ratio > 1.0);
int32 ans = static_cast<int32>(current_num_arcs *
opts_.growth_ratio);
if (ans == current_num_arcs) // make sure the target increases.
ans = current_num_arcs + 1;
// if we have already reached a final state, then
// apply the max_arcs limit.
if (output_best_cost_ - output_best_cost_ == 0.0 &&
ans > opts_.max_arcs)
ans = opts_.max_arcs;
return ans;
}
}
void PrunedCompactLatticeComposer::RecomputePruningInfo() {
std::vector<int32> all_composed_states;
GetTopsortedStateList(&all_composed_states);
ComputeForwardCosts(all_composed_states);
ComputeBackwardCosts(all_composed_states);
ComputeDeltaBackwardCosts(all_composed_states);
}
void PrunedCompactLatticeComposer::ComputeForwardCosts(
const std::vector<int32> &composed_states) {
KALDI_ASSERT(composed_states[0] == 0);
// Note: when we initialized composed_state_info_[0]
// we set forward_cost = 0.0, prev_composed_state = -1.
std::vector<ComposedStateInfo>::iterator
state_iter = composed_state_info_.begin(),
state_end = composed_state_info_.end();
state_iter->depth = 0; // start state has depth 0
++state_iter; // Skip over the start state.
// Set all other forward_cost fields to infinity and prev_composed_state to
// -1.
for (; state_iter != state_end; ++state_iter) {
state_iter->forward_cost = std::numeric_limits<double>::infinity();
state_iter->prev_composed_state = -1;
}
std::vector<int32>::const_iterator state_index_iter = composed_states.begin(),
state_index_end = composed_states.end();
for (; state_index_iter != state_index_end; ++state_index_iter) {
int32 composed_state_index = *state_index_iter;
const ComposedStateInfo &info = composed_state_info_[
composed_state_index];
double forward_cost = info.forward_cost;
// The next line is a check for infinity. If infinities have appeared, it
// either means there is a bug in the algorithm or there were infinities or
// NaN's in the lattice.
KALDI_ASSERT(forward_cost - forward_cost == 0.0);
fst::ArcIterator<CompactLattice> aiter(*clat_out_,
composed_state_index);
for (; !aiter.Done(); aiter.Next()) {
const CompactLatticeArc &arc = aiter.Value();
double arc_cost = ConvertToCost(arc.weight),
next_forward_cost = forward_cost + arc_cost;
ComposedStateInfo &next_info = composed_state_info_[arc.nextstate];
if (next_info.forward_cost > next_forward_cost) {
next_info.forward_cost = next_forward_cost;
next_info.prev_composed_state = composed_state_index;
next_info.depth = composed_state_info_[composed_state_index].depth + 1;
}
}
}
}
void PrunedCompactLatticeComposer::ComputeBackwardCosts(
const std::vector<int32> &composed_states) {
// Access the composed states in reverse topological order from latest to
// earliest.
std::vector<int32>::const_reverse_iterator iter = composed_states.rbegin(),
end = composed_states.rend();
for (; iter != end; ++iter) {
int32 composed_state_index = *iter;
ComposedStateInfo &info = composed_state_info_[composed_state_index];
double backward_cost =
ConvertToCost(clat_out_->Final(composed_state_index));
fst::ArcIterator<CompactLattice> aiter(*clat_out_,
composed_state_index);
for (; !aiter.Done(); aiter.Next()) {
const CompactLatticeArc &arc = aiter.Value();
double arc_cost = ConvertToCost(arc.weight),
next_backward_cost = composed_state_info_[arc.nextstate].backward_cost,
this_backward_cost = arc_cost + next_backward_cost;
if (this_backward_cost < backward_cost)
backward_cost = this_backward_cost;
}
// It's OK if at this point, backward_cost is still +infinity. This means
// that this state cannot reach the end yet, which means we have not yet
// expanded any path from this state all the way to a final-state of the
// output.
info.backward_cost = backward_cost;
}
output_best_cost_ = composed_state_info_[0].backward_cost;
// See the declaration of current_cutoff_ for more information. Note: on
// early iterations, before any path reaches a final state of the composed
// lattice, current_cutoff_ may be +infinity, and this is OK.
current_cutoff_ =
output_best_cost_ - lat_best_cost_ + opts_.lattice_compose_beam;
}
void PrunedCompactLatticeComposer::ComputeDeltaBackwardCosts(
const std::vector<int32> &composed_states) {
int32 num_states = clat_out_->NumStates();
for (int32 composed_state_index = 0; composed_state_index < num_states;
++composed_state_index) {
ComposedStateInfo &info = composed_state_info_[composed_state_index];
int32 lat_state = info.lat_state;
// Note: delta_backward_cost will be +infinity at this stage if the
// backward_cost was +infinity. This is OK; we'll set them all to
// finite values later in this function.
info.delta_backward_cost =
info.backward_cost - lat_state_info_[lat_state].backward_cost + info.depth * depth_penalty_;
}
// 'queue_elements' is a list of items (expected_cost_offset,
// composed_state_index) that we are going to add to composed_state_queue_,
// after clearing it. It's more efficient to accumulate them as a vector
// and add them all at once, than adding them one by one (search online for
// "heapify" if this seems confusing).
std::vector<std::pair<BaseFloat, int32> > queue_elements;
queue_elements.reserve(num_states);
double lat_best_cost = lat_best_cost_;
BaseFloat current_cutoff = current_cutoff_;
std::vector<int32>::const_iterator iter = composed_states.begin(),
end = composed_states.end();
for (; iter != end; ++iter) {
int32 composed_state_index = *iter;
ComposedStateInfo &info = composed_state_info_[composed_state_index];
if (info.delta_backward_cost - info.delta_backward_cost != 0) {
// if info.delta_backward_cost is +infinity...
int32 prev_composed_state = info.prev_composed_state;
if (prev_composed_state < 0) {
KALDI_ASSERT(composed_state_index == 0);
info.delta_backward_cost = 0.0;
} else {
const ComposedStateInfo &prev_info =
composed_state_info_[prev_composed_state];
// Check that prev_info.delta_backward_cost is finite.
KALDI_ASSERT(prev_info.delta_backward_cost -
prev_info.delta_backward_cost == 0.0);
info.delta_backward_cost = prev_info.delta_backward_cost + depth_penalty_;
}
}
double lat_backward_cost = lat_state_info_[info.lat_state].backward_cost;
// See the formula by where expected_cost_offset is declared in the
// struct for explanation.
BaseFloat expected_cost_offset =
info.forward_cost + lat_backward_cost + info.delta_backward_cost +
info.arc_delta_cost - lat_best_cost;
// If info.expected_cost_offset were real, we'd set it here:
//info.expected_cost_offset = expected_cost_offset;
// At this point expected_cost_offset may be infinite, if arc_delta_cost was
// infinite (reflecting that we processed all the arcs, and the final-state
// if applicable, of the lattice state corresponding to this composed state.
if (expected_cost_offset < current_cutoff) {
queue_elements.push_back(std::pair<BaseFloat, int32>(
expected_cost_offset, composed_state_index));
}
}
// Reinitialize composed_state_queue_ from 'queue_elements'.
QueueType temp_queue(queue_elements.begin(), queue_elements.end());
composed_state_queue_.swap(temp_queue);
}
void PrunedCompactLatticeComposer::ComputeLatticeStateInfo() {
KALDI_ASSERT(clat_in_.Properties(fst::kTopSorted, true) ==
fst::kTopSorted && clat_in_.NumStates() > 0 &&
clat_in_.Start() == 0);
int32 num_lat_states = clat_in_.NumStates();
lat_state_info_.resize(num_lat_states);
for (int32 s = num_lat_states - 1; s >= 0; s--) {
LatticeStateInfo &info = lat_state_info_[s];
std::vector<std::pair<double, int32> > arc_costs;
double backward_cost = ConvertToCost(clat_in_.Final(s));
if (backward_cost != std::numeric_limits<double>::infinity())
arc_costs.push_back(std::pair<BaseFloat,int32>(backward_cost, -1));
fst::ArcIterator<CompactLattice> aiter(clat_in_, s);
int32 arc_index = 0;
for (; !aiter.Done(); aiter.Next(), ++arc_index) {
const CompactLatticeArc &arc = aiter.Value();
KALDI_ASSERT(arc.nextstate > s);
backward_cost = lat_state_info_[arc.nextstate].backward_cost +
ConvertToCost(arc.weight);
KALDI_ASSERT(backward_cost - backward_cost == 0.0 &&
"Possibly not all states of input lattice are co-accessible?");
arc_costs.push_back(std::pair<BaseFloat,int32>(backward_cost, arc_index));
}
std::sort(arc_costs.begin(), arc_costs.end());
KALDI_ASSERT(!arc_costs.empty() &&
"Possibly not all states of input lattice are co-accessible?");
backward_cost = arc_costs[0].first;
info.backward_cost = backward_cost; // this is the state's backward_cost,
// reflecting the best path to the end.
info.arc_delta_costs.resize(arc_costs.size());
std::vector<std::pair<double, int32> >::const_iterator
src_iter = arc_costs.begin(), src_end = arc_costs.end();
std::vector<std::pair<BaseFloat, int32> >::iterator
dest_iter = info.arc_delta_costs.begin();
for (; src_iter != src_end; ++src_iter, ++dest_iter) {
dest_iter->first = BaseFloat(src_iter->first - backward_cost);
dest_iter->second = src_iter->second;
}
}
lat_best_cost_ = lat_state_info_[0].backward_cost;
}
PrunedCompactLatticeComposer::PrunedCompactLatticeComposer(
const ComposeLatticePrunedOptions &opts,
const CompactLattice &clat_in,
fst::DeterministicOnDemandFst<fst::StdArc> *det_fst,
CompactLattice* composed_clat): output_reached_final_(false),
opts_(opts), clat_in_(clat_in), det_fst_(det_fst),
clat_out_(composed_clat),
num_arcs_out_(0),
output_best_cost_(std::numeric_limits<double>::infinity()),
current_cutoff_(std::numeric_limits<double>::infinity()) {
clat_out_->DeleteStates();
depth_penalty_ = -1000;
}
void PrunedCompactLatticeComposer::AddFirstState() {
int32 state_id = clat_out_->AddState();
clat_out_->SetStart(state_id);
KALDI_ASSERT(state_id == 0);
composed_state_info_.resize(1);
ComposedStateInfo &composed_state = composed_state_info_[0];
composed_state.lat_state = 0;
composed_state.lm_state = det_fst_->Start();
composed_state.depth = 0;
composed_state.forward_cost = 0.0;
composed_state.backward_cost = std::numeric_limits<double>::infinity();
composed_state.delta_backward_cost = 0.0;
composed_state.prev_composed_state = -1;
composed_state.sorted_arc_index = 0;
composed_state.arc_delta_cost = 0.0; // the first arc_delta_cost is always 0.0
// due to sorting; no need to look it up.
lat_state_info_[0].composed_states.push_back(state_id);
accessed_lat_states_.insert(state_id);
pair_to_state_[std::pair<int32, int32>(0, det_fst_->Start())] = state_id;
BaseFloat expected_cost_offset = 0.0; // the formula simplifies to zero
// in this case.
composed_state_queue_.push(
std::pair<BaseFloat, int32>(expected_cost_offset,
state_id)); // actually (0.0, 0).
}
void PrunedCompactLatticeComposer::ProcessQueueElement(
int32 src_composed_state) {
KALDI_ASSERT(static_cast<size_t>(src_composed_state) <
composed_state_info_.size());
ComposedStateInfo &src_composed_state_info = composed_state_info_[
src_composed_state];
int32 lat_state = src_composed_state_info.lat_state;
const LatticeStateInfo &lat_state_info =
lat_state_info_[lat_state];
int32 sorted_arc_index = src_composed_state_info.sorted_arc_index,
num_sorted_arcs = lat_state_info.arc_delta_costs.size();
// note: num_sorted_arcs will be the number of arcs from this
// lattice state; plus one if there is a final-prob.
KALDI_ASSERT(sorted_arc_index >= 0);
{ // this block update the state's 'sorted_arc_index', 'arc_delta_cost' and
// 'expected_cost_offset' to reflect the fact that (by the time we exit from
// this function) we will have processed this arc (or the final-prob);
// it also re-inserts this state into the queue, if appropriate.
BaseFloat expected_cost_offset;
if (sorted_arc_index + 1 == num_sorted_arcs) {
src_composed_state_info.sorted_arc_index = -1;
src_composed_state_info.arc_delta_cost =
std::numeric_limits<BaseFloat>::infinity();
expected_cost_offset =
std::numeric_limits<BaseFloat>::infinity();
} else {
src_composed_state_info.sorted_arc_index = sorted_arc_index + 1;
src_composed_state_info.arc_delta_cost =
lat_state_info.arc_delta_costs[sorted_arc_index+1].first;
expected_cost_offset =
(src_composed_state_info.forward_cost +
lat_state_info.backward_cost +
src_composed_state_info.delta_backward_cost +
src_composed_state_info.arc_delta_cost - lat_best_cost_);
}
// We do '<' here rather than '<=', so that if current_cutoff_ is infinity
// and expected_cost_offset is infinity (because we've exhausted all the
// transitions from this state, and sorted_arc_index is now -1), we don't
// add this element to the queue.
if (expected_cost_offset < current_cutoff_) {
// this state has another exit arc (or final prob) that is good
// enough to re-enter into the queue. Note: if we are processing
// an arc out of this state and the destination state is new,
// we may also add something new to the queue at that time.
// the following call should be equivalent to
// composed_state_queue_.push(std::pair<BaseFloat,int32>(...)) with
// the same pair of args.
composed_state_queue_.emplace(
expected_cost_offset, src_composed_state);
}
}
int32 arc_index = lat_state_info.arc_delta_costs[sorted_arc_index].second;
if (arc_index < 0) { // This (arc_index == -1) means it is not really an arc
// index; it's a final-prob.
int32 lm_state = src_composed_state_info.lm_state;
BaseFloat lm_final_cost = det_fst_->Final(lm_state).Value();
if (lm_final_cost != std::numeric_limits<BaseFloat>::infinity()) {
// If there is a final-prob on this LM state (note: there always will be
// for conventional language models), then add the final-prob of this
// state...
CompactLattice::Weight final_weight = clat_in_.Final(lat_state);
// assume 'final_weight' is not Zero(); otherwise the final-prob should
// not have been present in 'arc_delta_costs'.
Lattice::Weight final_lat_weight = final_weight.Weight();
final_lat_weight.SetValue1(final_lat_weight.Value1() +
lm_final_cost);
final_weight.SetWeight(final_lat_weight);
clat_out_->SetFinal(src_composed_state, final_weight);
double final_cost = ConvertToCost(final_lat_weight);
if (final_cost < src_composed_state_info.backward_cost)
src_composed_state_info.backward_cost = final_cost;
if (!output_reached_final_) {
output_reached_final_ = true;
depth_penalty_ = 0.0;
RecomputePruningInfo();
}
}
} else {
// It really was an arc. This code is very complicated, so we make it its
// own function.
ProcessTransition(src_composed_state, arc_index);
}
}
void PrunedCompactLatticeComposer::ProcessTransition(int32 src_composed_state,
int32 arc_index) {
// Make src_composed_state a const pointer not a reference, as we may have to
// modify the pointer if composed_state_info_ is resized.
const ComposedStateInfo *src_info = &(composed_state_info_[
src_composed_state]);
int32 src_lat_state = src_info->lat_state;
// Get the arc we are going to expand.
fst::ArcIterator<CompactLattice> aiter(clat_in_, src_lat_state);
aiter.Seek(arc_index);
const CompactLatticeArc &lat_arc = aiter.Value();
// Note: this code is for CompactLatticeArc, in which the ilabel and olabel
// are the same, but we're writing it in such a way that it will naturally
// generalize to LatticeArc, so there are separate variables for the ilabel
// and the olabel.
int32 dest_lat_state = lat_arc.nextstate,
ilabel = lat_arc.ilabel,
olabel = lat_arc.olabel;
// Note: we expect that ilabel == olabel, since this is a CompactLattice, but this
// may not be so if we extend this to work with Lattice.
fst::StdArc lm_arc;
// the input lattice might have epsilons
if (olabel == 0) {
lm_arc.ilabel = 0;
lm_arc.olabel = 0;
lm_arc.nextstate = src_info->lm_state;
lm_arc.weight = fst::StdArc::Weight(0.0);
} else if (!det_fst_->GetArc(src_info->lm_state, olabel, &lm_arc)) {
// for normal language models we don't expect this to happen, but the
// appropriate behavior is to do nothing; the composed arc does not exist,
// so there is no arc to add and no new state to create.
return;
}
int32 dest_lm_state = lm_arc.nextstate;
// The following assertion is necessary because CompactLattice cannot support
// different ilabel vs. olabel; and also it's an expectation about
// language-models.
KALDI_ASSERT(lm_arc.ilabel == lm_arc.olabel);
LatticeStateInfo &dest_lat_state_info =
lat_state_info_[dest_lat_state];
int32 dest_composed_state;
ComposedStateInfo *dest_info;
{ // The next block works out 'dest_composed_state' and
// 'dest_info', and if the destination state did not already
// exist, creates a new composed state.
typedef std::unordered_map<std::pair<int32,int32>, int32,
PairHasher<int32> > MapType;
int32 new_composed_state = clat_out_->NumStates();
std::pair<const std::pair<int32,int32>, int32> value(
std::pair<int32,int32>(dest_lat_state, dest_lm_state), new_composed_state);
std::pair<MapType::iterator, bool> ret =
pair_to_state_.insert(value);
if (ret.second) {
// Successfully inserted: this dest-state did not already exist. Most of
// the rest of this block deals with the consequences of adding a new
// state.
int32 ans = clat_out_->AddState();
KALDI_ASSERT(ans == new_composed_state);
dest_composed_state = new_composed_state;
composed_state_info_.resize(dest_composed_state + 1);
dest_info = &(composed_state_info_[dest_composed_state]);
// Re-assign src_composed_state as the vector might have been reallocated.
src_info = &(composed_state_info_[src_composed_state]);
if (dest_lat_state_info.composed_states.empty())
accessed_lat_states_.insert(dest_lat_state);
dest_lat_state_info.composed_states.push_back(new_composed_state);
dest_info->lat_state = dest_lat_state;
dest_info->lm_state = dest_lm_state;
dest_info->depth = src_info->depth + 1;
dest_info->forward_cost =
src_info->forward_cost +
ConvertToCost(lat_arc.weight) + lm_arc.weight.Value();
dest_info->backward_cost =
std::numeric_limits<double>::infinity();
dest_info->delta_backward_cost =
src_info->delta_backward_cost + dest_info->depth * depth_penalty_;
// The 'prev_composed_state' field will not be read again until after it's
// overwritten; we set it as below only for debugging purposes (the
// negation is also for debugging purposes).
dest_info->prev_composed_state = -src_composed_state;
dest_info->sorted_arc_index = 0;
dest_info->arc_delta_cost = 0.0;
// Note: in the expression below, which can be understood with reference
// to the comment by the declaration of the phantom variable
// 'expected_cost_offset', 'arc_delta_cost' is known to equal 0.0 so it
// has been removed.
BaseFloat expected_cost_offset =
(dest_info->forward_cost +
dest_lat_state_info.backward_cost +
dest_info->delta_backward_cost -
lat_best_cost_);
if (expected_cost_offset < current_cutoff_) {
// the following call should be equivalent to
// composed_state_queue_.push(std::pair<BaseFloat,int32>(...)) with
// the same pair of args.
composed_state_queue_.emplace(expected_cost_offset,
dest_composed_state);
}
} else { // the destination composed state already existed.
dest_composed_state = ret.first->second;
dest_info = &(composed_state_info_[dest_composed_state]);
}
}
// Add the arc from the src to dest state in the composed output.
CompactLatticeArc new_arc;
new_arc.nextstate = dest_composed_state;
// Actually the ilabel and olabel are the same, but writing it this way will
// generalize better to type Lattice if we need that later.
new_arc.ilabel = ilabel;
new_arc.olabel = olabel;
new_arc.weight = lat_arc.weight;
// 'weight' is the weight part, as opposed to the string part.
LatticeArc::Weight weight = new_arc.weight.Weight();
// include the LM-arc's weight in the weight of the new arc.
weight.SetValue1(fst::Times(weight.Value1(), lm_arc.weight).Value());
new_arc.weight.SetWeight(weight);
clat_out_->AddArc(src_composed_state, new_arc);
num_arcs_out_++;
}
static int32 TotalNumArcs(const CompactLattice &clat) {
int32 num_states = clat.NumStates(),
num_arcs = 0;
for (int32 s = 0; s < num_states; s++)
num_arcs += clat.NumArcs(s);
return num_arcs;
}
void PrunedCompactLatticeComposer::Compose() {
if (clat_in_.NumStates() == 0) {
KALDI_WARN << "Input lattice to composition is empty.";
return;
}
ComputeLatticeStateInfo();
AddFirstState();
// while (we have not reached final state ||
// num-arcs produced < target num-arcs) { ...
while (output_best_cost_ == std::numeric_limits<double>::infinity() ||
num_arcs_out_ < opts_.max_arcs) {
RecomputePruningInfo();
int32 this_iter_arc_limit = GetCurrentArcLimit();
while (num_arcs_out_ < this_iter_arc_limit &&
!composed_state_queue_.empty()) {
int32 src_composed_state = composed_state_queue_.top().second;
composed_state_queue_.pop();
ProcessQueueElement(src_composed_state);
}
if (composed_state_queue_.empty())
break;
}
fst::Connect(clat_out_);
TopSortCompactLatticeIfNeeded(clat_out_);
if (GetVerboseLevel() >= 2) {
int32 num_arcs_in = TotalNumArcs(clat_in_),
orig_num_arcs_out = num_arcs_out_,
num_arcs_out = TotalNumArcs(*clat_out_),
num_states_in = clat_in_.NumStates(),
orig_num_states_out = composed_state_info_.size(),
num_states_out = clat_out_->NumStates();
std::ostringstream os;
os << "Input lattice had " << num_arcs_in << '/' << num_states_in
<< " arcs/states; output lattice has " << num_arcs_out << '/'
<< num_states_out;
if (num_arcs_out != orig_num_arcs_out) {
os << " (before pruning: " << orig_num_arcs_out << '/'
<< orig_num_states_out << ")";
}
if (!composed_state_queue_.empty()) {
// Below, composed_state_queue_.top().first + lat_best_cost is an
// expected-cost of the best path from the composed output that we *did
// not* expand. This, minus the best cost in the output compact lattice,
// can be interpreted as the beam that we effecctively pruned the output
// lattice to.
BaseFloat effective_beam =
composed_state_queue_.top().first + lat_best_cost_ - output_best_cost_;
os << ". Effective beam was " << effective_beam;
}
KALDI_VLOG(2) << os.str();
}
if (clat_out_->NumStates() == 0) {
KALDI_WARN << "Composed lattice has no states: something went wrong.";
}
}
void ComposeCompactLatticePruned(
const ComposeLatticePrunedOptions &opts,
const CompactLattice &clat,
fst::DeterministicOnDemandFst<fst::StdArc> *det_fst,
CompactLattice* composed_clat) {
PrunedCompactLatticeComposer composer(opts, clat, det_fst, composed_clat);
composer.Compose();
}
} // namespace kaldi
@@ -0,0 +1,179 @@
// lat/compose-lattice-pruned.h
// Copyright 2017 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.
#ifndef KALDI_LAT_COMPOSE_LATTICE_PRUNED_H_
#define KALDI_LAT_COMPOSE_LATTICE_PRUNED_H_
#include <fst/fstlib.h>
#include <fst/fst-decl.h>
#include <algorithm>
#include <map>
#include <set>
#include <vector>
#include "fstext/lattice-weight.h"
#include "itf/options-itf.h"
#include "lat/kaldi-lattice.h"
namespace kaldi {
/*
This header implements pruned lattice composition, via the functions
ComposeCompactLatticePruned (we may later add ComposeLatticePruned if
needed).
ComposeCompactLatticePruned does composition of a CompactLattice with a
DeterministicOnDemandFst<StdArc>, producing a CompactLattice. It's
intended for language model rescoring of lattices.
The scenario is that you have produced a Lattice or CompactLattice via
conventional decoding, and you want to replace (or partially replace) the
language model scores in the lattice (which will probably will come from the
LM used to generate the HCLG.fst) with the language model scores from a
larger language model.
The simpler alternative to using ComposeCompactLatticePruned is to use
ComposeCompactLatticeDeterministic. The advantages of ComposedCompactLatticePruned are:
(1) The alternative might be too slow, because when you compose a lattice
with a high-order n-gram language model (or an RNNLM with a high-order
n-gram approximation) it can generate a lot more arcs than were present
in the original lattice.
(2) For RNNLM rescoring, the n-gram approximation may not always
be choosing a very good history. In the n-gram approximation,
the LM score for a particular word given a history is taken
from a history that is the same as the desired history up to
the last, say, 4 words, but beyond that may differ. The
advantage of ComposeCompactLatticePruned functions over the alternative is
that it will often take, in a suitable sense, the "best" history
(instead of an arbitrary history); this happens simply because the
paths that are expected to be the best paths are visited first.
We now describe how you are expected to get the thing to compose with,
i.e. the DeterministicOnDemandFst<StdArc> that corrects the LM weights. It
will normally contain the LM used to create the original HCLG, with a
negative weight, composed with the LM you want to use, with a positive
weights (these weights might not be -1 and 1 if there is interpolation in the
picture). The LM we want to use will often be e.g. a 4-gram ARPA-type LM
(stored as a regular FST or, more compactly, as a .carpa file which is a
ConstArpaFst), or it will be some kind of RNNLM. You would use a
ComposeDeterministicOnDemandFst<StdArc> to combine the "base" language model
(with a negative weight, using either ConstArpaLm or
BackoffDeterministicOnDemandFst wrapped in ScaleDeterministicOnDemandFst)
with the RNNLM language model (the name of FST TBD, Hainan needs to write
this).
*/
// This options class is used for ComposeCompactLatticePruned,
// and if in future we write a function ComposeLatticePruned, we'll
// use the same options class.
// Note: the binary that uses this may want to use an --acoustic-scale
// option, in case the acoustics need to be scaled down before this
// composition, because it will make a difference to which paths
// are explored in the lattice.
struct ComposeLatticePrunedOptions {
// 'lattice_compose_beam' is a beam that determines
// how much of a given composition space we will expand (at least,
// until we hit the limit imposed by 'max_arcs'.. This
// beam is applied using heuristically-estimated expected costs
// to the end of the lattice, so if you specify, for example,
// beam=5.0, it doesn't guarantee that all paths with best-cost
// within 5.0 of the best path in the composed output will be
// retained (However, this would be exact if the LM we were
// rescoring with had zero costs).
float lattice_compose_beam;
// 'max_arcs' is the maximum number of arcs that we are willing to expand per
// lattice; once this limit is reached, we terminate the composition (however,
// this limit is not applied until at least one path to a final-state has been
// produced).
int32 max_arcs;
// 'initial_num_arcs' is the number of arcs we use on the first outer
// iteration of the algorithm. This is so unimportant that we do not expose
// it on the command line.
int32 initial_num_arcs;
// 'growth_ratio' determines how much we allow the num-arcs to grow on each
// outer iteration of the algorithm. 1.5 is a reasonable value; if it is set
// too small, too much time will be taken in RecomputePruningInfo(), and if
// too large, the paths searched may be less optimal than they could be (the
// heuristics will be less accurate).
BaseFloat growth_ratio;
ComposeLatticePrunedOptions(): lattice_compose_beam(6.0),
max_arcs(100000),
initial_num_arcs(100),
growth_ratio(1.5) { }
void Register(OptionsItf *po) {
po->Register("lattice-compose-beam", &lattice_compose_beam,
"Beam used in pruned lattice composition, which determines how "
"large the composed lattice may be.");
po->Register("max-arcs", &max_arcs, "Maximum number of arcs we allow in "
"any given lattice, during pruned composition (limits max size "
"of lattices; also see lattice-compose-beam).");
po->Register("growth-ratio", &growth_ratio, "Factor used in the lattice "
"composition algorithm; must be >1.0. Affects speed vs. "
"the optimality of the best-first composition.");
}
};
/**
Does pruned composition of a lattice 'clat' with a DeterministicOnDemandFst
'det_fst'; implements LM rescoring.
@param [in] opts Class containing options
@param [in] clat The input lattice, which is expected to already have a
reasonable acoustic scale applied (e.g. 0.1 if it's a normal
cross-entropy system, but 1.0 for a chain system); this scale
affects the pruning.
@param [in] det_fst The on-demand FST that we are composing with; its
ilabels will correspond to words and it should be an acceptor
in practice (ilabel == olabel). Will often contain a
weighted difference of language model scores, with scores
of the form alpha * new - alpha * old, where alpha
is the interpolation weight for the 'new' language model
(e.g. 0.5 or 0.8). It's non-const because 'det_fst' is
on-demand.
@param [out] composed_clat The output, which is a result of composing
'clat' with '*det_fst'. Notionally, '*det_fst' is on the
right, although both are acceptors so it doesn't really
matter in practice.
Although the two FSTs are of different types, the code
manually does the conversion. The weights in '*det_fst'
will be interpreted as graph weights (Value1()) in the
lattice semiring.
*/
void ComposeCompactLatticePruned(
const ComposeLatticePrunedOptions &opts,
const CompactLattice &clat,
fst::DeterministicOnDemandFst<fst::StdArc> *det_fst,
CompactLattice* composed_clat);
} // namespace kaldi
#endif
+117
View File
@@ -0,0 +1,117 @@
// lat/confidence.cc
// Copyright 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 "lat/confidence.h"
#include "lat/lattice-functions.h"
#include "lat/determinize-lattice-pruned.h"
namespace kaldi {
BaseFloat SentenceLevelConfidence(const CompactLattice &clat,
int32 *num_paths,
std::vector<int32> *best_sentence,
std::vector<int32> *second_best_sentence) {
/* It may seem strange that the first thing we do is to convert the
CompactLattice to a Lattice, given that we may have just created the
CompactLattice by determinizing a Lattice. However, this is not just
a circular conversion; "lat" will have the property that distinct
paths have distinct word sequences.
Below, we could run NbestAsFsts on a CompactLattice, but the time
taken would be quadratic in the length in words of the CompactLattice,
because of the alignment information getting appended as vectors.
That's why we convert back to Lattice.
*/
Lattice lat;
ConvertLattice(clat, &lat);
std::vector<Lattice> lats;
NbestAsFsts(lat, 2, &lats);
int32 n = lats.size();
KALDI_ASSERT(n >= 0 && n <= 2);
if (num_paths != NULL) *num_paths = n;
if (best_sentence != NULL) best_sentence->clear();
if (second_best_sentence != NULL) second_best_sentence->clear();
LatticeWeight weight1, weight2;
if (n >= 1)
fst::GetLinearSymbolSequence<LatticeArc,int32>(lats[0], NULL,
best_sentence,
&weight1);
if (n >= 2)
fst::GetLinearSymbolSequence<LatticeArc,int32>(lats[1], NULL,
second_best_sentence,
&weight2);
if (n == 0) {
return 0; // this seems most appropriate because it will be interpreted as
// zero confidence, and something definitely went wrong for this
// to happen.
} else if (n == 1) {
// If there is only one sentence in the lattice, we interpret this as there
// being perfect confidence
return std::numeric_limits<BaseFloat>::infinity();
} else {
BaseFloat best_cost = ConvertToCost(weight1),
second_best_cost = ConvertToCost(weight2);
BaseFloat ans = second_best_cost - best_cost;
if (!(ans >= -0.001 * (fabs(best_cost) + fabs(second_best_cost)))) {
// Answer should be positive. Make sure it's at at least not
// substantially negative. This would be very strange.
KALDI_WARN << "Very negative difference." << ans;
}
if (ans < 0) ans = 0;
return ans;
}
}
BaseFloat SentenceLevelConfidence(const Lattice &lat,
int32 *num_paths,
std::vector<int32> *best_sentence,
std::vector<int32> *second_best_sentence) {
int32 max_sentence_length = LongestSentenceLength(lat);
fst::DeterminizeLatticePrunedOptions determinize_opts;
// The basic idea of expanding only up to "max_sentence_length * 2" arcs,
// is that that should be sufficient to get the best and second-best paths
// through the lattice, which is all we need for this particular application.
// "safety_term" is just in case there is some reason why we might need a few
// extra arcs, e.g. in case of a tie on the weights of the second-best path.
int32 safety_term = 4 + max_sentence_length;
determinize_opts.max_arcs = max_sentence_length * 2 + safety_term;
// set prune_beam to a large value... we don't really rely on the beam; we
// rely on the max_arcs variable to limit the size of the lattice.
double prune_beam = std::numeric_limits<double>::infinity();
CompactLattice clat;
// We ignore the return status of DeterminizeLatticePruned. It will likely
// return false, but this is expected because the expansion is limited
// by "max_arcs" not "prune_beam".
Lattice inverse_lat(lat);
fst::Invert(&inverse_lat); // Swap input and output symbols.
DeterminizeLatticePruned(inverse_lat, prune_beam, &clat, determinize_opts);
// Call the version of this function that takes a CompactLattice.
return SentenceLevelConfidence(clat, num_paths,
best_sentence, second_best_sentence);
}
} // namespace kaldi
+77
View File
@@ -0,0 +1,77 @@
// lat/confidence.h
// Copyright 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.
#ifndef KALDI_LAT_CONFIDENCE_H_
#define KALDI_LAT_CONFIDENCE_H_
#include <vector>
#include <map>
#include "base/kaldi-common.h"
#include "util/common-utils.h"
#include "fstext/fstext-lib.h"
#include "lat/kaldi-lattice.h"
namespace kaldi {
/// Caution: this function is not the only way to get confidences in Kaldi.
/// This only gives you sentence-level (utterance-level) confidence. You can
/// get word-by-word confidence within a sentence, along with Minimum Bayes Risk
/// decoding, by looking at sausages.h.
/// Caution: confidences estimated using this type of method are not very
/// accurate.
/// This function will return the difference between the best path in clat and
/// the second-best path in clat (a positive number), or zero if clat was
/// equivalent to the empty FST (no successful paths), or infinity if there
/// was only one path in "clat". It will output to "num_paths" (if non-NULL)
/// a number n = 0, 1 or 2 saying how many n-best paths (up to two) were found.
/// If n >= 1 it outputs to "best_sentence" (if non-NULL) the best word-sequence;
/// if n == 2 it outputs to "second_best_sentence" (if non-NULL) the second best
/// word-sequence (this may be useful for testing whether the two best word
/// sequences are somehow equivalent for the task at hand). If you need more
/// information than this or want to look deeper inside the n-best list, then
/// look at the implementation of this function.
/// This function requires that distinct paths in "lat" have distinct word
/// sequences; this will automatically be the case if you generated "clat"
/// in any normal way, such as from a decoder, because a deterministic FST
/// has this property.
/// This function assumes that any acoustic scaling you want to apply,
/// has already been applied.
BaseFloat SentenceLevelConfidence(const CompactLattice &clat,
int32 *num_paths,
std::vector<int32> *best_sentence,
std::vector<int32> *second_best_sentence);
/// This version of SentenceLevelConfidence takes as input a state-level lattice.
/// It needs to determinize it first, but it does so in a "smart" way that only generates
/// about as many output paths as it needs.
BaseFloat SentenceLevelConfidence(const Lattice &lat,
int32 *num_paths,
std::vector<int32> *best_sentence,
std::vector<int32> *second_best_sentence);
} // namespace kaldi
#endif // KALDI_LAT_CONFIDENCE_H_
@@ -0,0 +1,147 @@
// lat/determinize-lattice-pruned-test.cc
// Copyright 2009-2012 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 "lat/determinize-lattice-pruned.h"
#include "fstext/lattice-utils.h"
#include "fstext/fst-test-utils.h"
#include "lat/kaldi-lattice.h"
#include "lat/lattice-functions.h"
namespace fst {
// Caution: these tests are not as generic as you might think from all the
// templates in the code. They are basically only valid for LatticeArc.
// This is partly due to the fact that certain templates need to be instantiated
// in other .cc files in this directory.
// test that determinization proceeds correctly on general
// FSTs (not guaranteed determinzable, but we use the
// max-states option to stop it getting out of control).
template<class Arc> void TestDeterminizeLatticePruned() {
typedef kaldi::int32 Int;
typedef typename Arc::Weight Weight;
typedef ArcTpl<CompactLatticeWeightTpl<Weight, Int> > CompactArc;
for(int i = 0; i < 100; i++) {
RandFstOptions opts;
opts.n_states = 4;
opts.n_arcs = 10;
opts.n_final = 2;
opts.allow_empty = false;
opts.weight_multiplier = 0.5; // impt for the randomly generated weights
opts.acyclic = true;
// to be exactly representable in float,
// or this test fails because numerical differences can cause symmetry in
// weights to be broken, which causes the wrong path to be chosen as far
// as the string part is concerned.
VectorFst<Arc> *fst = RandPairFst<Arc>(opts);
bool sorted = TopSort(fst);
KALDI_ASSERT(sorted);
ILabelCompare<Arc> ilabel_comp;
if (kaldi::Rand() % 2 == 0)
ArcSort(fst, ilabel_comp);
std::cout << "FST before lattice-determinizing is:\n";
{
FstPrinter<Arc> fstprinter(*fst, NULL, NULL, NULL, false, true, "\t");
fstprinter.Print(&std::cout, "standard output");
}
VectorFst<Arc> det_fst;
try {
DeterminizeLatticePrunedOptions lat_opts;
lat_opts.max_mem = ((kaldi::Rand() % 2 == 0) ? 100 : 1000);
lat_opts.max_states = ((kaldi::Rand() % 2 == 0) ? -1 : 20);
lat_opts.max_arcs = ((kaldi::Rand() % 2 == 0) ? -1 : 30);
bool ans = DeterminizeLatticePruned<Weight>(*fst, 10.0, &det_fst, lat_opts);
std::cout << "FST after lattice-determinizing is:\n";
{
FstPrinter<Arc> fstprinter(det_fst, NULL, NULL, NULL, false, true, "\t");
fstprinter.Print(&std::cout, "standard output");
}
KALDI_ASSERT(det_fst.Properties(kIDeterministic, true) & kIDeterministic);
// OK, now determinize it a different way and check equivalence.
// [note: it's not normal determinization, it's taking the best path
// for any input-symbol sequence....
VectorFst<Arc> pruned_fst(*fst);
if (pruned_fst.NumStates() != 0)
kaldi::PruneLattice(10.0, &pruned_fst);
VectorFst<CompactArc> compact_pruned_fst, compact_pruned_det_fst;
ConvertLattice<Weight, Int>(pruned_fst, &compact_pruned_fst, false);
std::cout << "Compact pruned FST is:\n";
{
FstPrinter<CompactArc> fstprinter(compact_pruned_fst, NULL, NULL, NULL, false, true, "\t");
fstprinter.Print(&std::cout, "standard output");
}
ConvertLattice<Weight, Int>(det_fst, &compact_pruned_det_fst, false);
std::cout << "Compact version of determinized FST is:\n";
{
FstPrinter<CompactArc> fstprinter(compact_pruned_det_fst, NULL, NULL, NULL, false, true, "\t");
fstprinter.Print(&std::cout, "standard output");
}
if (ans)
KALDI_ASSERT(RandEquivalent(compact_pruned_det_fst, compact_pruned_fst, 5/*paths*/, 0.01/*delta*/, kaldi::Rand()/*seed*/, 100/*path length, max*/));
} catch (...) {
std::cout << "Failed to lattice-determinize this FST (probably not determinizable)\n";
}
delete fst;
}
}
// test that determinization proceeds without crash on acyclic FSTs
// (guaranteed determinizable in this sense).
template<class Arc> void TestDeterminizeLatticePruned2() {
typedef typename Arc::Weight Weight;
RandFstOptions opts;
opts.acyclic = true;
for(int i = 0; i < 100; i++) {
VectorFst<Arc> *fst = RandPairFst<Arc>(opts);
std::cout << "FST before lattice-determinizing is:\n";
{
FstPrinter<Arc> fstprinter(*fst, NULL, NULL, NULL, false, true, "\t");
fstprinter.Print(&std::cout, "standard output");
}
VectorFst<Arc> ofst;
DeterminizeLatticePruned<Weight>(*fst, 10.0, &ofst);
std::cout << "FST after lattice-determinizing is:\n";
{
FstPrinter<Arc> fstprinter(ofst, NULL, NULL, NULL, false, true, "\t");
fstprinter.Print(&std::cout, "standard output");
}
delete fst;
}
}
} // end namespace fst
int main() {
using namespace fst;
TestDeterminizeLatticePruned<kaldi::LatticeArc>();
TestDeterminizeLatticePruned2<kaldi::LatticeArc>();
std::cout << "Tests succeeded\n";
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,296 @@
// lat/determinize-lattice-pruned.h
// Copyright 2009-2012 Microsoft Corporation
// 2012-2013 Johns Hopkins University (Author: Daniel Povey)
// 2014 Guoguo Chen
// 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.
#ifndef KALDI_LAT_DETERMINIZE_LATTICE_PRUNED_H_
#define KALDI_LAT_DETERMINIZE_LATTICE_PRUNED_H_
#include <fst/fstlib.h>
#include <fst/fst-decl.h>
#include <algorithm>
#include <map>
#include <set>
#include <vector>
#include "fstext/lattice-weight.h"
#include "itf/transition-information.h"
#include "itf/options-itf.h"
#include "lat/kaldi-lattice.h"
namespace fst {
/// \addtogroup fst_extensions
/// @{
// For example of usage, see test-determinize-lattice-pruned.cc
/*
DeterminizeLatticePruned implements a special form of determinization with
epsilon removal, optimized for a phase of lattice generation. This algorithm
also does pruning at the same time-- the combination is more efficient as it
somtimes prevents us from creating a lot of states that would later be pruned
away. This allows us to increase the lattice-beam and not have the algorithm
blow up. Also, because our algorithm processes states in order from those
that appear on high-scoring paths down to those that appear on low-scoring
paths, we can easily terminate the algorithm after a certain specified number
of states or arcs.
The input is an FST with weight-type BaseWeightType (usually a pair of floats,
with a lexicographical type of order, such as LatticeWeightTpl<float>).
Typically this would be a state-level lattice, with input symbols equal to
words, and output-symbols equal to p.d.f's (so like the inverse of HCLG). Imagine representing this as an
acceptor of type CompactLatticeWeightTpl<float>, in which the input/output
symbols are words, and the weights contain the original weights together with
strings (with zero or one symbol in them) containing the original output labels
(the p.d.f.'s). We determinize this using acceptor determinization with
epsilon removal. Remember (from lattice-weight.h) that
CompactLatticeWeightTpl has a special kind of semiring where we always take
the string corresponding to the best cost (of type BaseWeightType), and
discard the other. This corresponds to taking the best output-label sequence
(of p.d.f.'s) for each input-label sequence (of words). We couldn't use the
Gallic weight for this, or it would die as soon as it detected that the input
FST was non-functional. In our case, any acyclic FST (and many cyclic ones)
can be determinized.
We assume that there is a function
Compare(const BaseWeightType &a, const BaseWeightType &b)
that returns (-1, 0, 1) according to whether (a < b, a == b, a > b) in the
total order on the BaseWeightType... this information should be the
same as NaturalLess would give, but it's more efficient to do it this way.
You can define this for things like TropicalWeight if you need to instantiate
this class for that weight type.
We implement this determinization in a special way to make it efficient for
the types of FSTs that we will apply it to. One issue is that if we
explicitly represent the strings (in CompactLatticeWeightTpl) as vectors of
type vector<IntType>, the algorithm takes time quadratic in the length of
words (in states), because propagating each arc involves copying a whole
vector (of integers representing p.d.f.'s). Instead we use a hash structure
where each string is a pointer (Entry*), and uses a hash from (Entry*,
IntType), to the successor string (and a way to get the latest IntType and the
ancestor Entry*). [this is the class LatticeStringRepository].
Another issue is that rather than representing a determinized-state as a
collection of (state, weight), we represent it in a couple of reduced forms.
Suppose a determinized-state is a collection of (state, weight) pairs; call
this the "canonical representation". Note: these collections are always
normalized to remove any common weight and string part. Define end-states as
the subset of states that have an arc out of them with a label on, or are
final. If we represent a determinized-state a the set of just its (end-state,
weight) pairs, this will be a valid and more compact representation, and will
lead to a smaller set of determinized states (like early minimization). Call
this collection of (end-state, weight) pairs the "minimal representation". As
a mechanism to reduce compute, we can also consider another representation.
In the determinization algorithm, we start off with a set of (begin-state,
weight) pairs (where the "begin-states" are initial or have a label on the
transition into them), and the "canonical representation" consists of the
epsilon-closure of this set (i.e. follow epsilons). Call this set of
(begin-state, weight) pairs, appropriately normalized, the "initial
representation". If two initial representations are the same, the "canonical
representation" and hence the "minimal representation" will be the same. We
can use this to reduce compute. Note that if two initial representations are
different, this does not preclude the other representations from being the same.
*/
struct DeterminizeLatticePrunedOptions {
float delta; // A small offset used to measure equality of weights.
int max_mem; // If >0, determinization will fail and return false
// when the algorithm's (approximate) memory consumption crosses this threshold.
int max_loop; // If >0, can be used to detect non-determinizable input
// (a case that wouldn't be caught by max_mem).
int max_states;
int max_arcs;
float retry_cutoff;
DeterminizeLatticePrunedOptions(): delta(kDelta),
max_mem(-1),
max_loop(-1),
max_states(-1),
max_arcs(-1),
retry_cutoff(0.5) { }
void Register (kaldi::OptionsItf *opts) {
opts->Register("delta", &delta, "Tolerance used in determinization");
opts->Register("max-mem", &max_mem, "Maximum approximate memory usage in "
"determinization (real usage might be many times this)");
opts->Register("max-arcs", &max_arcs, "Maximum number of arcs in "
"output FST (total, not per state");
opts->Register("max-states", &max_states, "Maximum number of arcs in output "
"FST (total, not per state");
opts->Register("max-loop", &max_loop, "Option used to detect a particular "
"type of determinization failure, typically due to invalid input "
"(e.g., negative-cost loops)");
opts->Register("retry-cutoff", &retry_cutoff, "Controls pruning un-determinized "
"lattice and retrying determinization: if effective-beam < "
"retry-cutoff * beam, we prune the raw lattice and retry. Avoids "
"ever getting empty output for long segments.");
}
};
struct DeterminizeLatticePhonePrunedOptions {
// delta: a small offset used to measure equality of weights.
float delta;
// max_mem: if > 0, determinization will fail and return false when the
// algorithm's (approximate) memory consumption crosses this threshold.
int max_mem;
// phone_determinize: if true, do a first pass determinization on both phones
// and words.
bool phone_determinize;
// word_determinize: if true, do a second pass determinization on words only.
bool word_determinize;
// minimize: if true, push and minimize after determinization.
bool minimize;
DeterminizeLatticePhonePrunedOptions(): delta(kDelta),
max_mem(50000000),
phone_determinize(true),
word_determinize(true),
minimize(false) {}
void Register (kaldi::OptionsItf *opts) {
opts->Register("delta", &delta, "Tolerance used in determinization");
opts->Register("max-mem", &max_mem, "Maximum approximate memory usage in "
"determinization (real usage might be many times this).");
opts->Register("phone-determinize", &phone_determinize, "If true, do an "
"initial pass of determinization on both phones and words (see"
" also --word-determinize)");
opts->Register("word-determinize", &word_determinize, "If true, do a second "
"pass of determinization on words only (see also "
"--phone-determinize)");
opts->Register("minimize", &minimize, "If true, push and minimize after "
"determinization.");
}
};
/**
This function implements the normal version of DeterminizeLattice, in which the
output strings are represented using sequences of arcs, where all but the
first one has an epsilon on the input side. It also prunes using the beam
in the "prune" parameter. The input FST must be topologically sorted in order
for the algorithm to work. For efficiency it is recommended to sort ilabel as well.
Returns true on success, and false if it had to terminate the determinization
earlier than specified by the "prune" beam-- that is, if it terminated because
of the max_mem, max_loop or max_arcs constraints in the options.
CAUTION: you may want to use the version below which outputs to CompactLattice.
*/
template<class Weight>
bool DeterminizeLatticePruned(
const ExpandedFst<ArcTpl<Weight> > &ifst,
double prune,
MutableFst<ArcTpl<Weight> > *ofst,
DeterminizeLatticePrunedOptions opts = DeterminizeLatticePrunedOptions());
/* This is a version of DeterminizeLattice with a slightly more "natural" output format,
where the output sequences are encoded using the CompactLatticeArcTpl template
(i.e. the sequences of output symbols are represented directly as strings The input
FST must be topologically sorted in order for the algorithm to work. For efficiency
it is recommended to sort the ilabel for the input FST as well.
Returns true on normal success, and false if it had to terminate the determinization
earlier than specified by the "prune" beam-- that is, if it terminated because
of the max_mem, max_loop or max_arcs constraints in the options.
CAUTION: if Lattice is the input, you need to Invert() before calling this,
so words are on the input side.
*/
template<class Weight, class IntType>
bool DeterminizeLatticePruned(
const ExpandedFst<ArcTpl<Weight> >&ifst,
double prune,
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *ofst,
DeterminizeLatticePrunedOptions opts = DeterminizeLatticePrunedOptions());
/** This function takes in lattices and inserts phones at phone boundaries. It
uses the transition model to work out the transition_id to phone map. The
returning value is the starting index of the phone label. Typically we pick
(maximum_output_label_index + 1) as this value. The inserted phones are then
mapped to (returning_value + original_phone_label) in the new lattice. The
returning value will be used by DeterminizeLatticeDeletePhones() where it
works out the phones according to this value.
*/
template<class Weight>
typename ArcTpl<Weight>::Label DeterminizeLatticeInsertPhones(
const kaldi::TransitionInformation &trans_model,
MutableFst<ArcTpl<Weight> > *fst);
/** This function takes in lattices and deletes "phones" from them. The "phones"
here are actually any label that is larger than first_phone_label because
when we insert phones into the lattice, we map the original phone label to
(first_phone_label + original_phone_label). It is supposed to be used
together with DeterminizeLatticeInsertPhones()
*/
template<class Weight>
void DeterminizeLatticeDeletePhones(
typename ArcTpl<Weight>::Label first_phone_label,
MutableFst<ArcTpl<Weight> > *fst);
/** This function is a wrapper of DeterminizeLatticePhonePrunedFirstPass() and
DeterminizeLatticePruned(). If --phone-determinize is set to true, it first
calls DeterminizeLatticePhonePrunedFirstPass() to do the initial pass of
determinization on the phone + word lattices. If --word-determinize is set
true, it then does a second pass of determinization on the word lattices by
calling DeterminizeLatticePruned(). If both are set to false, then it gives
a warning and copying the lattices without determinization.
Note: the point of doing first a phone-level determinization pass and then
a word-level determinization pass is that it allows us to determinize
deeper lattices without "failing early" and returning a too-small lattice
due to the max-mem constraint. The result should be the same as word-level
determinization in general, but for deeper lattices it is a bit faster,
despite the fact that we now have two passes of determinization by default.
*/
template<class Weight, class IntType>
bool DeterminizeLatticePhonePruned(
const kaldi::TransitionInformation &trans_model,
const ExpandedFst<ArcTpl<Weight> > &ifst,
double prune,
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *ofst,
DeterminizeLatticePhonePrunedOptions opts
= DeterminizeLatticePhonePrunedOptions());
/** "Destructive" version of DeterminizeLatticePhonePruned() where the input
lattice might be changed.
*/
template<class Weight, class IntType>
bool DeterminizeLatticePhonePruned(
const kaldi::TransitionInformation &trans_model,
MutableFst<ArcTpl<Weight> > *ifst,
double prune,
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *ofst,
DeterminizeLatticePhonePrunedOptions opts
= DeterminizeLatticePhonePrunedOptions());
/** This function is a wrapper of DeterminizeLatticePhonePruned() that works for
Lattice type FSTs. It simplifies the calling process by calling
TopSort() Invert() and ArcSort() for you.
Unlike other determinization routines, the function
requires "ifst" to have transition-id's on the input side and words on the
output side.
This function can be used as the top-level interface to all the determinization
code.
*/
bool DeterminizeLatticePhonePrunedWrapper(
const kaldi::TransitionInformation &trans_model,
MutableFst<kaldi::LatticeArc> *ifst,
double prune,
MutableFst<kaldi::CompactLatticeArc> *ofst,
DeterminizeLatticePhonePrunedOptions opts
= DeterminizeLatticePhonePrunedOptions());
/// @} end "addtogroup fst_extensions"
} // end namespace fst
#endif
@@ -0,0 +1,173 @@
// lat/kaldi-lattice-test.cc
// Copyright 2009-2011 Microsoft Corporation
// 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 "lat/kaldi-lattice.h"
#include "fstext/rand-fst.h"
namespace kaldi {
CompactLattice *RandCompactLattice() {
Lattice *fst = fst::RandPairFst<LatticeArc>();
CompactLattice *cfst = new CompactLattice;
ConvertLattice(*fst, cfst);
delete fst;
return cfst;
}
Lattice *RandLattice() {
Lattice *fst = fst::RandPairFst<LatticeArc>();
return fst;
}
void TestCompactLatticeTable(bool binary) {
CompactLatticeWriter writer(binary ? "ark:tmpf" : "ark,t:tmpf");
int N = 10;
std::vector<CompactLattice*> lat_vec(N);
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
CompactLattice *fst = RandCompactLattice();
lat_vec[i] = fst;
writer.Write(key, *fst);
}
writer.Close();
RandomAccessCompactLatticeReader reader("ark:tmpf");
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
const CompactLattice &fst = reader.Value(key);
KALDI_ASSERT(fst::Equal(fst, *(lat_vec[i])));
delete lat_vec[i];
}
}
// Write as CompactLattice, read as Lattice.
void TestCompactLatticeTableCross(bool binary) {
CompactLatticeWriter writer(binary ? "ark:tmpf" : "ark,t:tmpf");
int N = 10;
std::vector<CompactLattice*> lat_vec(N);
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
CompactLattice *fst = RandCompactLattice();
lat_vec[i] = fst;
writer.Write(key, *fst);
}
writer.Close();
RandomAccessLatticeReader reader("ark:tmpf");
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
const Lattice &fst = reader.Value(key);
CompactLattice fst2;
ConvertLattice(fst, &fst2);
KALDI_ASSERT(fst::Equal(fst2, *(lat_vec[i])));
delete lat_vec[i];
}
}
// Lattice, binary.
void TestLatticeTable(bool binary) {
LatticeWriter writer(binary ? "ark:tmpf" : "ark,t:tmpf");
int N = 10;
std::vector<Lattice*> lat_vec(N);
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
Lattice *fst = RandLattice();
lat_vec[i] = fst;
writer.Write(key, *fst);
}
writer.Close();
RandomAccessLatticeReader reader("ark:tmpf");
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
const Lattice &fst = reader.Value(key);
KALDI_ASSERT(fst::Equal(fst, *(lat_vec[i])));
delete lat_vec[i];
}
}
// Write as Lattice, read as CompactLattice.
void TestLatticeTableCross(bool binary) {
LatticeWriter writer(binary ? "ark:tmpf" : "ark,t:tmpf");
int N = 10;
std::vector<Lattice*> lat_vec(N);
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
Lattice *fst = RandLattice();
lat_vec[i] = fst;
writer.Write(key, *fst);
}
writer.Close();
RandomAccessCompactLatticeReader reader("ark:tmpf");
for (int i = 0; i < N; i++) {
char buf[2];
buf[0] = '0' + i;
buf[1] = '\0';
std::string key = "key" + std::string(buf);
const CompactLattice &fst = reader.Value(key);
Lattice fst2;
ConvertLattice(fst, &fst2);
KALDI_ASSERT(fst::RandEquivalent(fst2, *(lat_vec[i]), 5, 0.01, Rand(), 10));
delete lat_vec[i];
}
}
} // end namespace kaldi
int main() {
using namespace kaldi;
for (int i = 0; i < 2; i++) {
bool binary = (i%2 == 0);
TestCompactLatticeTable(binary);
TestCompactLatticeTableCross(binary);
TestLatticeTable(binary);
TestLatticeTableCross(binary);
}
std::cout << "Test OK\n";
unlink("tmpf");
}
@@ -0,0 +1,506 @@
// lat/kaldi-lattice.cc
// Copyright 2009-2011 Microsoft Corporation
// 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 "lat/kaldi-lattice.h"
#include "fst/script/print-impl.h"
namespace kaldi {
/// Converts lattice types if necessary, deleting its input.
template<class OrigWeightType>
CompactLattice* ConvertToCompactLattice(fst::VectorFst<OrigWeightType> *ifst) {
if (!ifst) return NULL;
CompactLattice *ofst = new CompactLattice();
ConvertLattice(*ifst, ofst);
delete ifst;
return ofst;
}
// This overrides the template if there is no type conversion going on
// (for efficiency).
template<>
CompactLattice* ConvertToCompactLattice(CompactLattice *ifst) {
return ifst;
}
/// Converts lattice types if necessary, deleting its input.
template<class OrigWeightType>
Lattice* ConvertToLattice(fst::VectorFst<OrigWeightType> *ifst) {
if (!ifst) return NULL;
Lattice *ofst = new Lattice();
ConvertLattice(*ifst, ofst);
delete ifst;
return ofst;
}
// This overrides the template if there is no type conversion going on
// (for efficiency).
template<>
Lattice* ConvertToLattice(Lattice *ifst) {
return ifst;
}
bool WriteCompactLattice(std::ostream &os, bool binary,
const CompactLattice &t) {
if (binary) {
fst::FstWriteOptions opts;
// Leave all the options default. Normally these lattices wouldn't have any
// osymbols/isymbols so no point directing it not to write them (who knows what
// we'd want to if we had them).
return t.Write(os, opts);
} else {
// Text-mode output. Note: we expect that t.InputSymbols() and
// t.OutputSymbols() would always return NULL. The corresponding input
// routine would not work if the FST actually had symbols attached.
// Write a newline after the key, so the first line of the FST appears
// on its own line.
os << '\n';
bool acceptor = true, write_one = false;
fst::FstPrinter<CompactLatticeArc> printer(t, t.InputSymbols(),
t.OutputSymbols(),
NULL, acceptor, write_one, "\t");
printer.Print(&os, "<unknown>");
if (os.fail())
KALDI_WARN << "Stream failure detected.";
// Write another newline as a terminating character. The read routine will
// detect this [this is a Kaldi mechanism, not somethig in the original
// OpenFst code].
os << '\n';
return os.good();
}
}
/// LatticeReader provides (static) functions for reading both Lattice
/// and CompactLattice, in text form.
class LatticeReader {
typedef LatticeArc Arc;
typedef LatticeWeight Weight;
typedef CompactLatticeArc CArc;
typedef CompactLatticeWeight CWeight;
typedef Arc::Label Label;
typedef Arc::StateId StateId;
public:
// everything is static in this class.
/** This function reads from the FST text format; it does not know in advance
whether it's a Lattice or CompactLattice in the stream so it tries to
read both formats until it becomes clear which is the correct one.
*/
static std::pair<Lattice*, CompactLattice*> ReadText(
std::istream &is) {
typedef std::pair<Lattice*, CompactLattice*> PairT;
using std::string;
using std::vector;
Lattice *fst = new Lattice();
CompactLattice *cfst = new CompactLattice();
string line;
size_t nline = 0;
string separator = FLAGS_fst_field_separator + "\r\n";
while (std::getline(is, line)) {
nline++;
vector<string> col;
// on Windows we'll write in text and read in binary mode.
SplitStringToVector(line, separator.c_str(), true, &col);
if (col.size() == 0) break; // Empty line is a signal to stop, in our
// archive format.
if (col.size() > 5) {
KALDI_WARN << "Reading lattice: bad line in FST: " << line;
delete fst;
delete cfst;
return PairT(static_cast<Lattice*>(NULL),
static_cast<CompactLattice*>(NULL));
}
StateId s;
if (!ConvertStringToInteger(col[0], &s)) {
KALDI_WARN << "FstCompiler: bad line in FST: " << line;
delete fst;
delete cfst;
return PairT(static_cast<Lattice*>(NULL),
static_cast<CompactLattice*>(NULL));
}
if (fst)
while (s >= fst->NumStates())
fst->AddState();
if (cfst)
while (s >= cfst->NumStates())
cfst->AddState();
if (nline == 1) {
if (fst) fst->SetStart(s);
if (cfst) cfst->SetStart(s);
}
if (fst) { // we still have fst; try to read that arc.
bool ok = true;
Arc arc;
Weight w;
StateId d = s;
switch (col.size()) {
case 1 :
fst->SetFinal(s, Weight::One());
break;
case 2:
if (!StrToWeight(col[1], true, &w)) ok = false;
else fst->SetFinal(s, w);
break;
case 3: // 3 columns not ok for Lattice format; it's not an acceptor.
ok = false;
break;
case 4:
ok = ConvertStringToInteger(col[1], &arc.nextstate) &&
ConvertStringToInteger(col[2], &arc.ilabel) &&
ConvertStringToInteger(col[3], &arc.olabel);
if (ok) {
d = arc.nextstate;
arc.weight = Weight::One();
fst->AddArc(s, arc);
}
break;
case 5:
ok = ConvertStringToInteger(col[1], &arc.nextstate) &&
ConvertStringToInteger(col[2], &arc.ilabel) &&
ConvertStringToInteger(col[3], &arc.olabel) &&
StrToWeight(col[4], false, &arc.weight);
if (ok) {
d = arc.nextstate;
fst->AddArc(s, arc);
}
break;
default:
ok = false;
}
while (d >= fst->NumStates())
fst->AddState();
if (!ok) {
delete fst;
fst = NULL;
}
}
if (cfst) {
bool ok = true;
CArc arc;
CWeight w;
StateId d = s;
switch (col.size()) {
case 1 :
cfst->SetFinal(s, CWeight::One());
break;
case 2:
if (!StrToCWeight(col[1], true, &w)) ok = false;
else cfst->SetFinal(s, w);
break;
case 3: // compact-lattice is acceptor format: state, next-state, label.
ok = ConvertStringToInteger(col[1], &arc.nextstate) &&
ConvertStringToInteger(col[2], &arc.ilabel);
if (ok) {
d = arc.nextstate;
arc.olabel = arc.ilabel;
arc.weight = CWeight::One();
cfst->AddArc(s, arc);
}
break;
case 4:
ok = ConvertStringToInteger(col[1], &arc.nextstate) &&
ConvertStringToInteger(col[2], &arc.ilabel) &&
StrToCWeight(col[3], false, &arc.weight);
if (ok) {
d = arc.nextstate;
arc.olabel = arc.ilabel;
cfst->AddArc(s, arc);
}
break;
case 5: default:
ok = false;
}
while (d >= cfst->NumStates())
cfst->AddState();
if (!ok) {
delete cfst;
cfst = NULL;
}
}
if (!fst && !cfst) {
KALDI_WARN << "Bad line in lattice text format: " << line;
// read until we get an empty line, so at least we
// have a chance to read the next one (although this might
// be a bit futile since the calling code will get unhappy
// about failing to read this one.
while (std::getline(is, line)) {
SplitStringToVector(line, separator.c_str(), true, &col);
if (col.empty()) break;
}
return PairT(static_cast<Lattice*>(NULL),
static_cast<CompactLattice*>(NULL));
}
}
return PairT(fst, cfst);
}
static bool StrToWeight(const std::string &s, bool allow_zero, Weight *w) {
std::istringstream strm(s);
strm >> *w;
if (!strm || (!allow_zero && *w == Weight::Zero())) {
return false;
}
return true;
}
static bool StrToCWeight(const std::string &s, bool allow_zero, CWeight *w) {
std::istringstream strm(s);
strm >> *w;
if (!strm || (!allow_zero && *w == CWeight::Zero())) {
return false;
}
return true;
}
};
CompactLattice *ReadCompactLatticeText(std::istream &is) {
std::pair<Lattice*, CompactLattice*> lat_pair = LatticeReader::ReadText(is);
if (lat_pair.second != NULL) {
delete lat_pair.first;
return lat_pair.second;
} else if (lat_pair.first != NULL) {
// note: ConvertToCompactLattice frees its input.
return ConvertToCompactLattice(lat_pair.first);
} else {
return NULL;
}
}
Lattice *ReadLatticeText(std::istream &is) {
std::pair<Lattice*, CompactLattice*> lat_pair = LatticeReader::ReadText(is);
if (lat_pair.first != NULL) {
delete lat_pair.second;
return lat_pair.first;
} else if (lat_pair.second != NULL) {
// note: ConvertToLattice frees its input.
return ConvertToLattice(lat_pair.second);
} else {
return NULL;
}
}
bool ReadCompactLattice(std::istream &is, bool binary,
CompactLattice **clat) {
KALDI_ASSERT(*clat == NULL);
if (binary) {
fst::FstHeader hdr;
if (!hdr.Read(is, "<unknown>")) {
KALDI_WARN << "Reading compact lattice: error reading FST header.";
return false;
}
if (hdr.FstType() != "vector") {
KALDI_WARN << "Reading compact lattice: unsupported FST type: "
<< hdr.FstType();
return false;
}
fst::FstReadOptions ropts("<unspecified>",
&hdr);
typedef fst::CompactLatticeWeightTpl<fst::LatticeWeightTpl<float>, int32> T1;
typedef fst::CompactLatticeWeightTpl<fst::LatticeWeightTpl<double>, int32> T2;
typedef fst::LatticeWeightTpl<float> T3;
typedef fst::LatticeWeightTpl<double> T4;
typedef fst::VectorFst<fst::ArcTpl<T1> > F1;
typedef fst::VectorFst<fst::ArcTpl<T2> > F2;
typedef fst::VectorFst<fst::ArcTpl<T3> > F3;
typedef fst::VectorFst<fst::ArcTpl<T4> > F4;
CompactLattice *ans = NULL;
if (hdr.ArcType() == T1::Type()) {
ans = ConvertToCompactLattice(F1::Read(is, ropts));
} else if (hdr.ArcType() == T2::Type()) {
ans = ConvertToCompactLattice(F2::Read(is, ropts));
} else if (hdr.ArcType() == T3::Type()) {
ans = ConvertToCompactLattice(F3::Read(is, ropts));
} else if (hdr.ArcType() == T4::Type()) {
ans = ConvertToCompactLattice(F4::Read(is, ropts));
} else {
KALDI_WARN << "FST with arc type " << hdr.ArcType()
<< " cannot be converted to CompactLattice.\n";
return false;
}
if (ans == NULL) {
KALDI_WARN << "Error reading compact lattice (after reading header).";
return false;
}
*clat = ans;
return true;
} else {
// The next line would normally consume the \r on Windows, plus any
// extra spaces that might have got in there somehow.
while (std::isspace(is.peek()) && is.peek() != '\n') is.get();
if (is.peek() == '\n') is.get(); // consume the newline.
else { // saw spaces but no newline.. this is not expected.
KALDI_WARN << "Reading compact lattice: unexpected sequence of spaces "
<< " at file position " << is.tellg();
return false;
}
*clat = ReadCompactLatticeText(is); // that routine will warn on error.
return (*clat != NULL);
}
}
bool CompactLatticeHolder::Read(std::istream &is) {
Clear(); // in case anything currently stored.
int c = is.peek();
if (c == -1) {
KALDI_WARN << "End of stream detected reading CompactLattice.";
return false;
} else if (isspace(c)) { // The text form of the lattice begins
// with space (normally, '\n'), so this means it's text (the binary form
// cannot begin with space because it starts with the FST Type() which is not
// space).
return ReadCompactLattice(is, false, &t_);
} else if (c != 214) { // 214 is first char of FST magic number,
// on little-endian machines which is all we support (\326 octal)
KALDI_WARN << "Reading compact lattice: does not appear to be an FST "
<< " [non-space but no magic number detected], file pos is "
<< is.tellg();
return false;
} else {
return ReadCompactLattice(is, true, &t_);
}
}
bool WriteLattice(std::ostream &os, bool binary, const Lattice &t) {
if (binary) {
fst::FstWriteOptions opts;
// Leave all the options default. Normally these lattices wouldn't have any
// osymbols/isymbols so no point directing it not to write them (who knows what
// we'd want to do if we had them).
return t.Write(os, opts);
} else {
// Text-mode output. Note: we expect that t.InputSymbols() and
// t.OutputSymbols() would always return NULL. The corresponding input
// routine would not work if the FST actually had symbols attached.
// Write a newline after the key, so the first line of the FST appears
// on its own line.
os << '\n';
bool acceptor = false, write_one = false;
fst::FstPrinter<LatticeArc> printer(t, t.InputSymbols(),
t.OutputSymbols(),
NULL, acceptor, write_one, "\t");
printer.Print(&os, "<unknown>");
if (os.fail())
KALDI_WARN << "Stream failure detected.";
// Write another newline as a terminating character. The read routine will
// detect this [this is a Kaldi mechanism, not somethig in the original
// OpenFst code].
os << '\n';
return os.good();
}
}
bool ReadLattice(std::istream &is, bool binary,
Lattice **lat) {
KALDI_ASSERT(*lat == NULL);
if (binary) {
fst::FstHeader hdr;
if (!hdr.Read(is, "<unknown>")) {
KALDI_WARN << "Reading lattice: error reading FST header.";
return false;
}
if (hdr.FstType() != "vector") {
KALDI_WARN << "Reading lattice: unsupported FST type: "
<< hdr.FstType();
return false;
}
fst::FstReadOptions ropts("<unspecified>",
&hdr);
typedef fst::CompactLatticeWeightTpl<fst::LatticeWeightTpl<float>, int32> T1;
typedef fst::CompactLatticeWeightTpl<fst::LatticeWeightTpl<double>, int32> T2;
typedef fst::LatticeWeightTpl<float> T3;
typedef fst::LatticeWeightTpl<double> T4;
typedef fst::VectorFst<fst::ArcTpl<T1> > F1;
typedef fst::VectorFst<fst::ArcTpl<T2> > F2;
typedef fst::VectorFst<fst::ArcTpl<T3> > F3;
typedef fst::VectorFst<fst::ArcTpl<T4> > F4;
Lattice *ans = NULL;
if (hdr.ArcType() == T1::Type()) {
ans = ConvertToLattice(F1::Read(is, ropts));
} else if (hdr.ArcType() == T2::Type()) {
ans = ConvertToLattice(F2::Read(is, ropts));
} else if (hdr.ArcType() == T3::Type()) {
ans = ConvertToLattice(F3::Read(is, ropts));
} else if (hdr.ArcType() == T4::Type()) {
ans = ConvertToLattice(F4::Read(is, ropts));
} else {
KALDI_WARN << "FST with arc type " << hdr.ArcType()
<< " cannot be converted to Lattice.\n";
return false;
}
if (ans == NULL) {
KALDI_WARN << "Error reading lattice (after reading header).";
return false;
}
*lat = ans;
return true;
} else {
// The next line would normally consume the \r on Windows, plus any
// extra spaces that might have got in there somehow.
while (std::isspace(is.peek()) && is.peek() != '\n') is.get();
if (is.peek() == '\n') is.get(); // consume the newline.
else { // saw spaces but no newline.. this is not expected.
KALDI_WARN << "Reading compact lattice: unexpected sequence of spaces "
<< " at file position " << is.tellg();
return false;
}
*lat = ReadLatticeText(is); // that routine will warn on error.
return (*lat != NULL);
}
}
/* Since we don't write the binary headers for this type of holder,
we use a different method to work out whether we're in binary mode.
*/
bool LatticeHolder::Read(std::istream &is) {
Clear(); // in case anything currently stored.
int c = is.peek();
if (c == -1) {
KALDI_WARN << "End of stream detected reading Lattice.";
return false;
} else if (isspace(c)) { // The text form of the lattice begins
// with space (normally, '\n'), so this means it's text (the binary form
// cannot begin with space because it starts with the FST Type() which is not
// space).
return ReadLattice(is, false, &t_);
} else if (c != 214) { // 214 is first char of FST magic number,
// on little-endian machines which is all we support (\326 octal)
KALDI_WARN << "Reading compact lattice: does not appear to be an FST "
<< " [non-space but no magic number detected], file pos is "
<< is.tellg();
return false;
} else {
return ReadLattice(is, true, &t_);
}
}
} // end namespace kaldi
@@ -0,0 +1,156 @@
// lat/kaldi-lattice.h
// Copyright 2009-2011 Microsoft Corporation
// 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.
#ifndef KALDI_LAT_KALDI_LATTICE_H_
#define KALDI_LAT_KALDI_LATTICE_H_
#include "fstext/fstext-lib.h"
#include "base/kaldi-common.h"
//#include "util/common-utils.h"
namespace kaldi {
// will import some things above...
typedef fst::LatticeWeightTpl<BaseFloat> LatticeWeight;
// careful: kaldi::int32 is not always the same C type as fst::int32
typedef fst::CompactLatticeWeightTpl<LatticeWeight, int32> CompactLatticeWeight;
typedef fst::CompactLatticeWeightCommonDivisorTpl<LatticeWeight, int32>
CompactLatticeWeightCommonDivisor;
typedef fst::ArcTpl<LatticeWeight> LatticeArc;
typedef fst::ArcTpl<CompactLatticeWeight> CompactLatticeArc;
typedef fst::VectorFst<LatticeArc> Lattice;
typedef fst::VectorFst<CompactLatticeArc> CompactLattice;
// The following functions for writing and reading lattices in binary or text
// form are provided here in case you need to include lattices in larger,
// Kaldi-type objects with their own Read and Write functions. Caution: these
// functions return false on stream failure rather than throwing an exception as
// most similar Kaldi functions would do.
bool WriteCompactLattice(std::ostream &os, bool binary,
const CompactLattice &clat);
bool WriteLattice(std::ostream &os, bool binary,
const Lattice &lat);
// the following function requires that *clat be
// NULL when called.
bool ReadCompactLattice(std::istream &is, bool binary,
CompactLattice **clat);
// the following function requires that *lat be
// NULL when called.
bool ReadLattice(std::istream &is, bool binary,
Lattice **lat);
class CompactLatticeHolder {
public:
typedef CompactLattice T;
CompactLatticeHolder() { t_ = NULL; }
static bool Write(std::ostream &os, bool binary, const T &t) {
// Note: we don't include the binary-mode header when writing
// this object to disk; this ensures that if we write to single
// files, the result can be read by OpenFst.
return WriteCompactLattice(os, binary, t);
}
bool Read(std::istream &is);
static bool IsReadInBinary() { return true; }
T &Value() {
KALDI_ASSERT(t_ != NULL && "Called Value() on empty CompactLatticeHolder");
return *t_;
}
void Clear() { delete t_; t_ = NULL; }
void Swap(CompactLatticeHolder *other) {
std::swap(t_, other->t_);
}
bool ExtractRange(const CompactLatticeHolder &other, const std::string &range) {
KALDI_ERR << "ExtractRange is not defined for this type of holder.";
return false;
}
~CompactLatticeHolder() { Clear(); }
private:
T *t_;
};
class LatticeHolder {
public:
typedef Lattice T;
LatticeHolder() { t_ = NULL; }
static bool Write(std::ostream &os, bool binary, const T &t) {
// Note: we don't include the binary-mode header when writing
// this object to disk; this ensures that if we write to single
// files, the result can be read by OpenFst.
return WriteLattice(os, binary, t);
}
bool Read(std::istream &is);
static bool IsReadInBinary() { return true; }
T &Value() {
KALDI_ASSERT(t_ != NULL && "Called Value() on empty LatticeHolder");
return *t_;
}
void Clear() { delete t_; t_ = NULL; }
void Swap(LatticeHolder *other) {
std::swap(t_, other->t_);
}
bool ExtractRange(const LatticeHolder &other, const std::string &range) {
KALDI_ERR << "ExtractRange is not defined for this type of holder.";
return false;
}
~LatticeHolder() { Clear(); }
private:
T *t_;
};
//typedef TableWriter<LatticeHolder> LatticeWriter;
//typedef SequentialTableReader<LatticeHolder> SequentialLatticeReader;
//typedef RandomAccessTableReader<LatticeHolder> RandomAccessLatticeReader;
//
//typedef TableWriter<CompactLatticeHolder> CompactLatticeWriter;
//typedef SequentialTableReader<CompactLatticeHolder> SequentialCompactLatticeReader;
//typedef RandomAccessTableReader<CompactLatticeHolder> RandomAccessCompactLatticeReader;
} // namespace kaldi
#endif // KALDI_LAT_KALDI_LATTICE_H_
@@ -0,0 +1,262 @@
// lat/lattice-functions-transition-model.cc
// Copyright 2009-2011 Saarland University (Author: Arnab Ghoshal)
// 2012-2013 Johns Hopkins University (Author: Daniel Povey); Chao Weng;
// Bagher BabaAli
// 2013 Cisco Systems (author: Neha Agrawal) [code modified
// from original code in ../gmmbin/gmm-rescore-lattice.cc]
// 2014 Guoguo Chen
// 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 "lat/lattice-functions-transition-model.h"
#include "hmm/hmm-utils.h"
#include "hmm/transition-model.h"
#include "lat/lattice-functions.h"
namespace kaldi {
BaseFloat LatticeForwardBackwardMmi(
const TransitionModel &tmodel,
const Lattice &lat,
const std::vector<int32> &num_ali,
bool drop_frames,
bool convert_to_pdf_ids,
bool cancel,
Posterior *post) {
// First compute the MMI posteriors.
Posterior den_post;
BaseFloat ans = LatticeForwardBackward(lat,
&den_post,
NULL);
Posterior num_post;
AlignmentToPosterior(num_ali, &num_post);
// Now negate the MMI posteriors and add the numerator
// posteriors.
ScalePosterior(-1.0, &den_post);
if (convert_to_pdf_ids) {
Posterior num_tmp;
ConvertPosteriorToPdfs(tmodel, num_post, &num_tmp);
num_tmp.swap(num_post);
Posterior den_tmp;
ConvertPosteriorToPdfs(tmodel, den_post, &den_tmp);
den_tmp.swap(den_post);
}
MergePosteriors(num_post, den_post,
cancel, drop_frames, post);
return ans;
}
bool CompactLatticeToWordProns(
const TransitionModel &tmodel,
const CompactLattice &clat,
std::vector<int32> *words,
std::vector<int32> *begin_times,
std::vector<int32> *lengths,
std::vector<std::vector<int32> > *prons,
std::vector<std::vector<int32> > *phone_lengths) {
words->clear();
begin_times->clear();
lengths->clear();
prons->clear();
phone_lengths->clear();
typedef CompactLattice::Arc Arc;
typedef Arc::Label Label;
typedef CompactLattice::StateId StateId;
typedef CompactLattice::Weight Weight;
using namespace fst;
StateId state = clat.Start();
int32 cur_time = 0;
if (state == kNoStateId) {
KALDI_WARN << "Empty lattice.";
return false;
}
while (1) {
Weight final = clat.Final(state);
size_t num_arcs = clat.NumArcs(state);
if (final != Weight::Zero()) {
if (num_arcs != 0) {
KALDI_WARN << "Lattice is not linear.";
return false;
}
if (! final.String().empty()) {
KALDI_WARN << "Lattice has alignments on final-weight: probably "
"was not word-aligned (alignments will be approximate)";
}
return true;
} else {
if (num_arcs != 1) {
KALDI_WARN << "Lattice is not linear: num-arcs = " << num_arcs;
return false;
}
fst::ArcIterator<CompactLattice> aiter(clat, state);
const Arc &arc = aiter.Value();
Label word_id = arc.ilabel; // Note: ilabel==olabel, since acceptor.
// Also note: word_id may be zero; we output it anyway.
int32 length = arc.weight.String().size();
words->push_back(word_id);
begin_times->push_back(cur_time);
lengths->push_back(length);
const std::vector<int32> &arc_alignment = arc.weight.String();
std::vector<std::vector<int32> > split_alignment;
SplitToPhones(tmodel, arc_alignment, &split_alignment);
std::vector<int32> phones(split_alignment.size());
std::vector<int32> plengths(split_alignment.size());
for (size_t i = 0; i < split_alignment.size(); i++) {
KALDI_ASSERT(!split_alignment[i].empty());
phones[i] = tmodel.TransitionIdToPhone(split_alignment[i][0]);
plengths[i] = split_alignment[i].size();
}
prons->push_back(phones);
phone_lengths->push_back(plengths);
cur_time += length;
state = arc.nextstate;
}
}
}
// Returns true if this vector of transition-ids could be a valid
// word. Note: for testing, we assume that the lexicon always
// has the same input-word and output-word. The other case is complex
// to test.
static bool IsPlausibleWord(const WordAlignLatticeLexiconInfo &lexicon_info,
const TransitionModel &tmodel,
int32 word_id,
const std::vector<int32> &transition_ids) {
std::vector<std::vector<int32> > split_alignment; // Split into phones.
if (!SplitToPhones(tmodel, transition_ids, &split_alignment)) {
KALDI_WARN << "Could not split word into phones correctly (forced-out?)";
}
std::vector<int32> phones(split_alignment.size());
for (size_t i = 0; i < split_alignment.size(); i++) {
KALDI_ASSERT(!split_alignment[i].empty());
phones[i] = tmodel.TransitionIdToPhone(split_alignment[i][0]);
}
std::vector<int32> lexicon_entry;
lexicon_entry.push_back(word_id);
lexicon_entry.insert(lexicon_entry.end(), phones.begin(), phones.end());
if (!lexicon_info.IsValidEntry(lexicon_entry)) {
std::ostringstream ostr;
for (size_t i = 0; i < lexicon_entry.size(); i++)
ostr << lexicon_entry[i] << ' ';
KALDI_WARN << "Invalid arc in aligned lattice (code error?) lexicon-entry is " << ostr.str();
return false;
} else {
return true;
}
}
/// Testing code; map word symbols in the lattice "lat" using the equivalence-classes
/// obtained from the lexicon, using the function EquivalenceClassOf in the lexicon_info
/// object.
static void MapSymbols(const WordAlignLatticeLexiconInfo &lexicon_info,
CompactLattice *lat) {
typedef CompactLattice::StateId StateId;
for (StateId s = 0; s < lat->NumStates(); s++) {
for (fst::MutableArcIterator<CompactLattice> aiter(lat, s);
!aiter.Done(); aiter.Next()) {
CompactLatticeArc arc (aiter.Value());
KALDI_ASSERT(arc.ilabel == arc.olabel);
arc.ilabel = lexicon_info.EquivalenceClassOf(arc.ilabel);
arc.olabel = arc.ilabel;
aiter.SetValue(arc);
}
}
}
bool TestWordAlignedLattice(const WordAlignLatticeLexiconInfo &lexicon_info,
const TransitionModel &tmodel,
CompactLattice clat,
CompactLattice aligned_clat,
bool allow_duplicate_paths) {
int32 max_err = 5, num_err = 0;
{ // We test whether the forward-backward likelihoods differ; this is intended
// to detect when we have duplicate paths in the aligned lattice, for some path
// in the input lattice (e.g. due to epsilon-sequencing problems).
Posterior post;
Lattice lat, aligned_lat;
ConvertLattice(clat, &lat);
ConvertLattice(aligned_clat, &aligned_lat);
TopSort(&lat);
TopSort(&aligned_lat);
BaseFloat like_before = LatticeForwardBackward(lat, &post),
like_after = LatticeForwardBackward(aligned_lat, &post);
if (fabs(like_before - like_after) >
1.0e-04 * (fabs(like_before) + fabs(like_after))) {
KALDI_WARN << "Forward-backward likelihoods differ in word-aligned lattice "
<< "testing, " << like_before << " != " << like_after;
if (!allow_duplicate_paths)
num_err++;
}
}
// Do a check on the arcs of the aligned lattice, that each arc corresponds
// to an entry in the lexicon.
for (CompactLattice::StateId s = 0; s < aligned_clat.NumStates(); s++) {
for (fst::ArcIterator<CompactLattice> aiter(aligned_clat, s);
!aiter.Done(); aiter.Next()) {
const CompactLatticeArc &arc (aiter.Value());
KALDI_ASSERT(arc.ilabel == arc.olabel);
int32 word_id = arc.ilabel;
const std::vector<int32> &tids = arc.weight.String();
if (word_id == 0 && tids.empty()) continue; // We allow epsilon arcs.
if (num_err < max_err)
if (!IsPlausibleWord(lexicon_info, tmodel, word_id, tids))
num_err++;
// Note: IsPlausibleWord will warn if there is an error.
}
if (!aligned_clat.Final(s).String().empty()) {
KALDI_WARN << "Aligned lattice has nonempty string on its final-prob.";
return false;
}
}
// Next we'll do an equivalence test.
// First map symbols into equivalence classes, so that we don't wrongly fail
// due to the capability of the framework to map words to other words.
// (e.g. mapping <eps> on silence arcs to SIL).
MapSymbols(lexicon_info, &clat);
MapSymbols(lexicon_info, &aligned_clat);
/// Check equivalence.
int32 num_paths = 5, seed = Rand(), max_path_length = -1;
BaseFloat delta = 0.2; // some lattices have large costs -> use large delta.
FLAGS_v = GetVerboseLevel(); // set the OpenFst verbose level to the Kaldi
// verbose level.
if (!RandEquivalent(clat, aligned_clat, num_paths, delta, seed, max_path_length)) {
KALDI_WARN << "Equivalence test failed during lattice alignment.";
return false;
}
FLAGS_v = 0;
return (num_err == 0);
}
} // namespace kaldi
@@ -0,0 +1,104 @@
// lat/lattice-functions-transition-model.h
// Copyright 2009-2012 Saarland University (author: Arnab Ghoshal)
// 2012-2013 Johns Hopkins University (Author: Daniel Povey);
// Bagher BabaAli
// 2014 Guoguo Chen
// 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.
#ifndef KALDI_LAT_LATTICE_FUNCTIONS_TRANSITION_MODEL_H_
#define KALDI_LAT_LATTICE_FUNCTIONS_TRANSITION_MODEL_H_
#include <map>
#include <vector>
#include "base/kaldi-common.h"
#include "fstext/fstext-lib.h"
#include "hmm/transition-model.h"
#include "hmm/hmm-utils.h"
#include "hmm/posterior.h"
#include "itf/decodable-itf.h"
#include "lat/kaldi-lattice.h"
#include "lat/word-align-lattice-lexicon.h"
namespace kaldi {
/**
This function can be used to compute posteriors for MMI, with a positive contribution
for the numerator and a negative one for the denominator. This function is not actually
used in our normal MMI training recipes, where it's instead done using various command
line programs that each do a part of the job. This function was written for use in
neural-net MMI training.
@param [in] trans The transition model. Used to map the
transition-ids to phones or pdfs.
@param [in] lat The denominator lattice
@param [in] num_ali The numerator alignment
@param [in] drop_frames If "drop_frames" is true, it will not compute any
posteriors on frames where the num and den have disjoint
pdf-ids.
@param [in] convert_to_pdf_ids If "convert_to_pdfs_ids" is true, it will
convert the output to be at the level of pdf-ids, not
transition-ids.
@param [in] cancel If "cancel" is true, it will cancel out any positive and
negative parts from the same transition-id (or pdf-id,
if convert_to_pdf_ids == true).
@param [out] arc_post The output MMI posteriors of transition-ids (or
pdf-ids if convert_to_pdf_ids == true) at each frame
i.e. the difference between the numerator
and denominator posteriors.
It returns the forward-backward likelihood of the lattice. */
BaseFloat LatticeForwardBackwardMmi(
const TransitionModel &trans,
const Lattice &lat,
const std::vector<int32> &num_ali,
bool drop_frames,
bool convert_to_pdf_ids,
bool cancel,
Posterior *arc_post);
/// This function takes a CompactLattice that should only contain a single
/// linear sequence (e.g. derived from lattice-1best), and that should have been
/// processed so that the arcs in the CompactLattice align correctly with the
/// word boundaries (e.g. by lattice-align-words). It outputs 4 vectors of the
/// same size, which give, for each word in the lattice (in sequence), the word
/// label, the begin time and length in frames, and the pronunciation (sequence
/// of phones). This is done even for zero words, corresponding to optional
/// silences -- if you don't want them, just ignore them in the output.
/// This function will print a warning and return false, if the lattice
/// did not have the correct format (e.g. if it is empty or it is not
/// linear).
bool CompactLatticeToWordProns(
const TransitionModel &tmodel,
const CompactLattice &clat,
std::vector<int32> *words,
std::vector<int32> *begin_times,
std::vector<int32> *lengths,
std::vector<std::vector<int32> > *prons,
std::vector<std::vector<int32> > *phone_lengths);
bool TestWordAlignedLattice(const WordAlignLatticeLexiconInfo &lexicon_info,
const TransitionModel &tmodel,
CompactLattice clat,
CompactLattice aligned_clat,
bool allow_duplicate_paths);
} // namespace kaldi
#endif // KALDI_LAT_LATTICE_FUNCTIONS_TRANSITION_MODEL_H_
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,455 @@
// lat/lattice-functions.h
// Copyright 2009-2012 Saarland University (author: Arnab Ghoshal)
// 2012-2013 Johns Hopkins University (Author: Daniel Povey);
// Bagher BabaAli
// 2014 Guoguo Chen
// 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.
#ifndef KALDI_LAT_LATTICE_FUNCTIONS_H_
#define KALDI_LAT_LATTICE_FUNCTIONS_H_
#include <vector>
#include <map>
#include "base/kaldi-common.h"
// #include "hmm/posterior.h"
#include "fstext/fstext-lib.h"
// #include "hmm/transition-model.h"
#include "lat/kaldi-lattice.h"
// #include "itf/decodable-itf.h"
namespace kaldi {
// /**
// This function extracts the per-frame log likelihoods from a linear
// lattice (which we refer to as an 'nbest' lattice elsewhere in Kaldi code).
// The dimension of *per_frame_loglikes will be set to the
// number of input symbols in 'nbest'. The elements of
// '*per_frame_loglikes' will be set to the .Value2() elements of the lattice
// weights, which represent the acoustic costs; you may want to scale this
// vector afterward by -1/acoustic_scale to get the original loglikes.
// If there are acoustic costs on input-epsilon arcs or the final-prob in 'nbest'
// (and this should not normally be the case in situations where it makes
// sense to call this function), they will be included to the cost of the
// preceding input symbol, or the following input symbol for input-epsilons
// encountered prior to any input symbol. If 'nbest' has no input symbols,
// 'per_frame_loglikes' will be set to the empty vector.
// **/
// void GetPerFrameAcousticCosts(const Lattice &nbest,
// Vector<BaseFloat> *per_frame_loglikes);
//
// /// This function iterates over the states of a topologically sorted lattice and
// /// counts the time instance corresponding to each state. The times are returned
// /// in a vector of integers 'times' which is resized to have a size equal to the
// /// number of states in the lattice. The function also returns the maximum time
// /// in the lattice (this will equal the number of frames in the file).
// int32 LatticeStateTimes(const Lattice &lat, std::vector<int32> *times);
//
// /// As LatticeStateTimes, but in the CompactLattice format. Note: must
// /// be topologically sorted. Returns length of the utterance in frames, which
// /// might not be the same as the maximum time in the lattice, due to frames
// /// in the final-prob.
// int32 CompactLatticeStateTimes(const CompactLattice &clat,
// std::vector<int32> *times);
//
// /// This function does the forward-backward over lattices and computes the
// /// posterior probabilities of the arcs. It returns the total log-probability
// /// of the lattice. The Posterior quantities contain pairs of (transition-id, weight)
// /// on each frame.
// /// If the pointer "acoustic_like_sum" is provided, this value is set to
// /// the sum over the arcs, of the posterior of the arc times the
// /// acoustic likelihood [i.e. negated acoustic score] on that link.
// /// This is used in combination with other quantities to work out
// /// the objective function in MMI discriminative training.
// BaseFloat LatticeForwardBackward(const Lattice &lat,
// Posterior *arc_post,
// double *acoustic_like_sum = NULL);
//
// // This function is something similar to LatticeForwardBackward(), but it is on
// // the CompactLattice lattice format. Also we only need the alpha in the forward
// // path, not the posteriors.
// bool ComputeCompactLatticeAlphas(const CompactLattice &lat,
// std::vector<double> *alpha);
//
// // A sibling of the function CompactLatticeAlphas()... We compute the beta from
// // the backward path here.
// bool ComputeCompactLatticeBetas(const CompactLattice &lat,
// std::vector<double> *beta);
//
//
// // Computes (normal or Viterbi) alphas and betas; returns (total-prob, or
// // best-path negated cost) Note: in either case, the alphas and betas are
// // negated costs. Requires that lat be topologically sorted. This code
// // will work for either CompactLattice or Latice.
// template<typename LatticeType>
// double ComputeLatticeAlphasAndBetas(const LatticeType &lat,
// bool viterbi,
// std::vector<double> *alpha,
// std::vector<double> *beta);
//
//
// /// Topologically sort the compact lattice if not already topologically sorted.
// /// Will crash if the lattice cannot be topologically sorted.
// void TopSortCompactLatticeIfNeeded(CompactLattice *clat);
//
//
// /// Topologically sort the lattice if not already topologically sorted.
// /// Will crash if lattice cannot be topologically sorted.
// void TopSortLatticeIfNeeded(Lattice *clat);
//
// /// Returns the depth of the lattice, defined as the average number of arcs (or
// /// final-prob strings) crossing any given frame. Returns 1 for empty lattices.
// /// Requires that clat is topologically sorted!
// BaseFloat CompactLatticeDepth(const CompactLattice &clat,
// int32 *num_frames = NULL);
//
// /// This function returns, for each frame, the number of arcs crossing that
// /// frame.
// void CompactLatticeDepthPerFrame(const CompactLattice &clat,
// std::vector<int32> *depth_per_frame);
//
//
// /// This function limits the depth of the lattice, per frame: that means, it
// /// does not allow more than a specified number of arcs active on any given
// /// frame. This can be used to reduce the size of the "very deep" portions of
// /// the lattice.
// void CompactLatticeLimitDepth(int32 max_arcs_per_frame,
// CompactLattice *clat);
//
//
// /// Given a lattice, and a transition model to map pdf-ids to phones,
// /// outputs for each frame the set of phones active on that frame. If
// /// sil_phones (which must be sorted and uniq) is nonempty, it excludes
// /// phones in this list.
// void LatticeActivePhones(const Lattice &lat, const TransitionModel &trans,
// const std::vector<int32> &sil_phones,
// std::vector<std::set<int32> > *active_phones);
//
// /// Given a lattice, and a transition model to map pdf-ids to phones,
// /// replace the output symbols (presumably words), with phones; we
// /// use the TransitionModel to work out the phone sequence. Note
// /// that the phone labels are not exactly aligned with the phone
// /// boundaries. We put a phone label to coincide with any transition
// /// to the final, nonemitting state of a phone (this state always exists,
// /// we ensure this in HmmTopology::Check()). This would be the last
// /// transition-id in the phone if reordering is not done (but typically
// /// we do reorder).
// /// Also see PhoneAlignLattice, in phone-align-lattice.h.
// void ConvertLatticeToPhones(const TransitionModel &trans_model,
// Lattice *lat);
/// Prunes a lattice or compact lattice. Returns true on success, false if
/// there was some kind of failure.
template<class LatticeType>
bool PruneLattice(BaseFloat beam, LatticeType *lat);
//
// /// Given a lattice, and a transition model to map pdf-ids to phones,
// /// replace the sequences of transition-ids with sequences of phones.
// /// Note that this is different from ConvertLatticeToPhones, in that
// /// we replace the transition-ids not the words.
// void ConvertCompactLatticeToPhones(const TransitionModel &trans_model,
// CompactLattice *clat);
//
// /// Boosts LM probabilities by b * [number of frame errors]; equivalently, adds
// /// -b*[number of frame errors] to the graph-component of the cost of each arc/path.
// /// There is a frame error if a particular transition-id on a particular frame
// /// corresponds to a phone not matching transcription's alignment for that frame.
// /// This is used in "margin-inspired" discriminative training, esp. Boosted MMI.
// /// The TransitionModel is used to map transition-ids in the lattice
// /// input-side to phones; the phones appearing in
// /// "silence_phones" are treated specially in that we replace the frame error f
// /// (either zero or 1) for a frame, with the minimum of f or max_silence_error.
// /// For the normal recipe, max_silence_error would be zero.
// /// Returns true on success, false if there was some kind of mismatch.
// /// At input, silence_phones must be sorted and unique.
// bool LatticeBoost(const TransitionModel &trans,
// const std::vector<int32> &alignment,
// const std::vector<int32> &silence_phones,
// BaseFloat b,
// BaseFloat max_silence_error,
// Lattice *lat);
//
//
// /**
// This function implements either the MPFE (minimum phone frame error) or SMBR
// (state-level minimum bayes risk) forward-backward, depending on whether
// "criterion" is "mpfe" or "smbr". It returns the MPFE
// criterion of SMBR criterion for this utterance, and outputs the posteriors (which
// may be positive or negative) into "post".
//
// @param [in] trans The transition model. Used to map the
// transition-ids to phones or pdfs.
// @param [in] silence_phones A list of integer ids of silence phones. The
// silence frames i.e. the frames where num_ali
// corresponds to a silence phones are treated specially.
// The behavior is determined by 'one_silence_class'
// being false (traditional behavior) or true.
// Usually in our setup, several phones including
// the silence, vocalized noise, non-spoken noise
// and unk are treated as "silence phones"
// @param [in] lat The denominator lattice
// @param [in] num_ali The numerator alignment
// @param [in] criterion The objective function. Must be "mpfe" or "smbr"
// for MPFE (minimum phone frame error) or sMBR
// (state minimum bayes risk) training.
// @param [in] one_silence_class Determines how the silence frames are treated.
// Setting this to false gives the old traditional behavior,
// where the silence frames (according to num_ali) are
// treated as incorrect. However, this means that the
// insertions are not penalized by the objective.
// Setting this to true gives the new behaviour, where we
// treat silence as any other phone, except that all pdfs
// of silence phones are collapsed into a single class for
// the frame-error computation. This can possible reduce
// the insertions in the trained model. This is closer to
// the WER metric that we actually care about, since WER is
// generally computed after filtering out noises, but
// does penalize insertions.
// @param [out] post The "MBR posteriors" i.e. derivatives w.r.t to the
// pseudo log-likelihoods of states at each frame.
// */
// BaseFloat LatticeForwardBackwardMpeVariants(
// const TransitionModel &trans,
// const std::vector<int32> &silence_phones,
// const Lattice &lat,
// const std::vector<int32> &num_ali,
// std::string criterion,
// bool one_silence_class,
// Posterior *post);
//
// /**
// This function can be used to compute posteriors for MMI, with a positive contribution
// for the numerator and a negative one for the denominator. This function is not actually
// used in our normal MMI training recipes, where it's instead done using various command
// line programs that each do a part of the job. This function was written for use in
// neural-net MMI training.
//
// @param [in] trans The transition model. Used to map the
// transition-ids to phones or pdfs.
// @param [in] lat The denominator lattice
// @param [in] num_ali The numerator alignment
// @param [in] drop_frames If "drop_frames" is true, it will not compute any
// posteriors on frames where the num and den have disjoint
// pdf-ids.
// @param [in] convert_to_pdf_ids If "convert_to_pdfs_ids" is true, it will
// convert the output to be at the level of pdf-ids, not
// transition-ids.
// @param [in] cancel If "cancel" is true, it will cancel out any positive and
// negative parts from the same transition-id (or pdf-id,
// if convert_to_pdf_ids == true).
// @param [out] arc_post The output MMI posteriors of transition-ids (or
// pdf-ids if convert_to_pdf_ids == true) at each frame
// i.e. the difference between the numerator
// and denominator posteriors.
//
// It returns the forward-backward likelihood of the lattice. */
// BaseFloat LatticeForwardBackwardMmi(
// const TransitionModel &trans,
// const Lattice &lat,
// const std::vector<int32> &num_ali,
// bool drop_frames,
// bool convert_to_pdf_ids,
// bool cancel,
// Posterior *arc_post);
//
//
// /// This function takes a CompactLattice that should only contain a single
// /// linear sequence (e.g. derived from lattice-1best), and that should have been
// /// processed so that the arcs in the CompactLattice align correctly with the
// /// word boundaries (e.g. by lattice-align-words). It outputs 3 vectors of the
// /// same size, which give, for each word in the lattice (in sequence), the word
// /// label and the begin time and length in frames. This is done even for zero
// /// (epsilon) words, generally corresponding to optional silence-- if you don't
// /// want them, just ignore them in the output.
// /// This function will print a warning and return false, if the lattice
// /// did not have the correct format (e.g. if it is empty or it is not
// /// linear).
// bool CompactLatticeToWordAlignment(const CompactLattice &clat,
// std::vector<int32> *words,
// std::vector<int32> *begin_times,
// std::vector<int32> *lengths);
//
// /// This function takes a CompactLattice that should only contain a single
// /// linear sequence (e.g. derived from lattice-1best), and that should have been
// /// processed so that the arcs in the CompactLattice align correctly with the
// /// word boundaries (e.g. by lattice-align-words). It outputs 4 vectors of the
// /// same size, which give, for each word in the lattice (in sequence), the word
// /// label, the begin time and length in frames, and the pronunciation (sequence
// /// of phones). This is done even for zero words, corresponding to optional
// /// silences -- if you don't want them, just ignore them in the output.
// /// This function will print a warning and return false, if the lattice
// /// did not have the correct format (e.g. if it is empty or it is not
// /// linear).
// bool CompactLatticeToWordProns(
// const TransitionModel &tmodel,
// const CompactLattice &clat,
// std::vector<int32> *words,
// std::vector<int32> *begin_times,
// std::vector<int32> *lengths,
// std::vector<std::vector<int32> > *prons,
// std::vector<std::vector<int32> > *phone_lengths);
//
//
// /// A form of the shortest-path/best-path algorithm that's specially coded for
// /// CompactLattice. Requires that clat be acyclic.
// void CompactLatticeShortestPath(const CompactLattice &clat,
// CompactLattice *shortest_path);
//
// /// This function expands a CompactLattice to ensure high-probability paths
// /// have unique histories. Arcs with posteriors larger than epsilon get splitted.
// void ExpandCompactLattice(const CompactLattice &clat,
// double epsilon,
// CompactLattice *expand_clat);
//
// /// For each state, compute forward and backward best (viterbi) costs and its
// /// traceback states (for generating best paths later). The forward best cost
// /// for a state is the cost of the best path from the start state to the state.
// /// The traceback state of this state is its predecessor state in the best path.
// /// The backward best cost for a state is the cost of the best path from the
// /// state to a final one. Its traceback state is the successor state in the best
// /// path in the forward direction.
// /// Note: final weights of states are in backward_best_cost_and_pred.
// /// Requires the input CompactLattice clat be acyclic.
// typedef std::vector<std::pair<double,
// CompactLatticeArc::StateId> > CostTraceType;
// void CompactLatticeBestCostsAndTracebacks(
// const CompactLattice &clat,
// CostTraceType *forward_best_cost_and_pred,
// CostTraceType *backward_best_cost_and_pred);
//
// /// This function adds estimated neural language model scores of words in a
// /// minimal list of hypotheses that covers a lattice, to the graph scores on the
// /// arcs. The list of hypotheses are generated by latbin/lattice-path-cover.
// typedef unordered_map<std::pair<int32, int32>, double, PairHasher<int32> > MapT;
// void AddNnlmScoreToCompactLattice(const MapT &nnlm_scores,
// CompactLattice *clat);
//
// /// This function add the word insertion penalty to graph score of each word
// /// in the compact lattice
// void AddWordInsPenToCompactLattice(BaseFloat word_ins_penalty,
// CompactLattice *clat);
//
// /// This function *adds* the negated scores obtained from the Decodable object,
// /// to the acoustic scores on the arcs. If you want to replace them, you should
// /// use ScaleCompactLattice to first set the acoustic scores to zero. Returns
// /// true on success, false on error (typically some kind of mismatched inputs).
// bool RescoreCompactLattice(DecodableInterface *decodable,
// CompactLattice *clat);
//
//
// /// This function returns the number of words in the longest sentence in a
// /// CompactLattice (i.e. the the maximum of any path, of the count of
// /// olabels on that path).
// int32 LongestSentenceLength(const Lattice &lat);
//
// /// This function returns the number of words in the longest sentence in a
// /// CompactLattice, i.e. the the maximum of any path, of the count of
// /// labels on that path... note, in CompactLattice, the ilabels and olabels
// /// are identical because it is an acceptor.
// int32 LongestSentenceLength(const CompactLattice &lat);
//
//
// /// This function is like RescoreCompactLattice, but it is modified to avoid
// /// computing probabilities on most frames where all the pdf-ids are the same.
// /// (it needs the transition-model to work out whether two transition-ids map to
// /// the same pdf-id, and it assumes that the lattice has transition-ids on it).
// /// The naive thing would be to just set all probabilities to zero on frames
// /// where all the pdf-ids are the same (because this value won't affect the
// /// lattice posterior). But this would become confusing when we compute
// /// corpus-level diagnostics such as the MMI objective function. Instead,
// /// imagine speedup_factor = 100 (it must be >= 1.0)... with probability (1.0 /
// /// speedup_factor) we compute those likelihoods and multiply them by
// /// speedup_factor; otherwise we set them to zero. This gives the right
// /// expected probability so our corpus-level diagnostics will be about right.
// bool RescoreCompactLatticeSpeedup(
// const TransitionModel &tmodel,
// BaseFloat speedup_factor,
// DecodableInterface *decodable,
// CompactLattice *clat);
//
//
// /// This function *adds* the negated scores obtained from the Decodable object,
// /// to the acoustic scores on the arcs. If you want to replace them, you should
// /// use ScaleCompactLattice to first set the acoustic scores to zero. Returns
// /// true on success, false on error (e.g. some kind of mismatched inputs).
// /// The input labels, if nonzero, are interpreted as transition-ids or whatever
// /// other index the Decodable object expects.
// bool RescoreLattice(DecodableInterface *decodable,
// Lattice *lat);
//
// /// This function Composes a CompactLattice format lattice with a
// /// DeterministicOnDemandFst<fst::StdFst> format fst, and outputs another
// /// CompactLattice format lattice. The first element (the one that corresponds
// /// to LM weight) in CompactLatticeWeight is used for composition.
// ///
// /// Note that the DeterministicOnDemandFst interface is not "const", therefore
// /// we cannot use "const" for <det_fst>.
// void ComposeCompactLatticeDeterministic(
// const CompactLattice& clat,
// fst::DeterministicOnDemandFst<fst::StdArc>* det_fst,
// CompactLattice* composed_clat);
//
// /// This function computes the mapping from the pair
// /// (frame-index, transition-id) to the pair
// /// (sum-of-acoustic-scores, num-of-occurences) over all occurences of the
// /// transition-id in that frame.
// /// frame-index in the lattice.
// /// This function is useful for retaining the acoustic scores in a
// /// non-compact lattice after a process like determinization where the
// /// frame-level acoustic scores are typically lost.
// /// The function ReplaceAcousticScoresFromMap is used to restore the
// /// acoustic scores computed by this function.
// ///
// /// @param [in] lat Input lattice. Expected to be top-sorted. Otherwise the
// /// function will crash.
// /// @param [out] acoustic_scores
// /// Pointer to a map from the pair (frame-index,
// /// transition-id) to a pair (sum-of-acoustic-scores,
// /// num-of-occurences).
// /// Usually the acoustic scores for a pdf-id (and hence
// /// transition-id) on a frame will be the same for all the
// /// occurences of the pdf-id in that frame.
// /// But if not, we will take the average of the acoustic
// /// scores. Hence, we store both the sum-of-acoustic-scores
// /// and the num-of-occurences of the transition-id in that
// /// frame.
// void ComputeAcousticScoresMap(
// const Lattice &lat,
// unordered_map<std::pair<int32, int32>, std::pair<BaseFloat, int32>,
// PairHasher<int32> > *acoustic_scores);
//
// /// This function restores acoustic scores computed using the function
// /// ComputeAcousticScoresMap into the lattice.
// ///
// /// @param [in] acoustic_scores
// /// A map from the pair (frame-index, transition-id) to a
// /// pair (sum-of-acoustic-scores, num-of-occurences) of
// /// the occurences of the transition-id in that frame.
// /// See the comments for ComputeAcousticScoresMap for
// /// details.
// /// @param [out] lat Pointer to the output lattice.
// void ReplaceAcousticScoresFromMap(
// const unordered_map<std::pair<int32, int32>, std::pair<BaseFloat, int32>,
// PairHasher<int32> > &acoustic_scores,
// Lattice *lat);
} // namespace kaldi
#endif // KALDI_LAT_LATTICE_FUNCTIONS_H_
@@ -0,0 +1,421 @@
// lat/phone-align-lattice.cc
// Copyright 2012-2013 Microsoft Corporation
// 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 "lat/phone-align-lattice.h"
#include "util/stl-utils.h"
namespace kaldi {
class LatticePhoneAligner {
public:
typedef CompactLatticeArc::StateId StateId;
typedef CompactLatticeArc::Label Label;
class ComputationState { /// The state of the computation in which,
/// along a single path in the lattice, we work out the phone
/// boundaries and output phone-aligned arcs. [These may or may not have
/// words on them; the word symbols are not aligned with anything.
public:
/// Advance the computation state by adding the symbols and weights
/// from this arc. Gets rid of the weight and puts it in "weight" which
/// will be put on the output arc; this keeps the state-space small.
void Advance(const CompactLatticeArc &arc, const PhoneAlignLatticeOptions &opts,
LatticeWeight *weight) {
const std::vector<int32> &string = arc.weight.String();
transition_ids_.insert(transition_ids_.end(),
string.begin(), string.end());
if (arc.ilabel != 0 && !opts.replace_output_symbols) // note: arc.ilabel==arc.olabel (acceptor)
word_labels_.push_back(arc.ilabel);
*weight = Times(weight_, arc.weight.Weight());
weight_ = LatticeWeight::One();
}
/// If it can output a whole phone, it will do so, will put it in arc_out,
/// and return true; else it will return false. If it detects an error
/// condition and *error = false, it will set *error to true and print
/// a warning. In this case it will still output phone arcs, they will
/// just be inaccurate. Of course once *error is set, something has gone
/// wrong so don't trust the output too fully.
/// Note: the "next_state" of the arc will not be set, you have to do that
/// yourself.
bool OutputPhoneArc(const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLatticeArc *arc_out,
bool *error);
/// This will succeed (and output the arc) if we have >1 word in words_;
/// the arc won't have any transition-ids on it. This is intended to fix
/// a particular pathology where too many words were pending and we had
/// blowup.
bool OutputWordArc(const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLatticeArc *arc_out,
bool *error);
bool IsEmpty() { return (transition_ids_.empty() && word_labels_.empty()); }
/// FinalWeight() will return "weight" if both transition_ids
/// and word_labels are empty, otherwise it will return
/// Weight::Zero().
LatticeWeight FinalWeight() { return (IsEmpty() ? weight_ : LatticeWeight::Zero()); }
/// This function may be called when you reach the end of
/// the lattice and this structure hasn't voluntarily
/// output words using "OutputArc". If IsEmpty() == false,
/// then you can call this function and it will output
/// an arc. The only
/// non-error state in which this happens, is when a word
/// (or silence) has ended, but we don't know that it's
/// ended because we haven't seen the first transition-id
/// from the next word. Otherwise (error state), the output
/// will consist of partial words, and this will only
/// happen for lattices that were somehow broken, i.e.
/// had not reached the final state.
void OutputArcForce(const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLatticeArc *arc_out,
bool *error);
size_t Hash() const {
VectorHasher<int32> vh;
return vh(transition_ids_) + 90647 * vh(word_labels_);
// 90647 is an arbitrary largish prime number.
// We don't bother including the weight in the hash--
// we don't really expect duplicates with the same vectors
// but different weights, and anyway, this is only an
// efficiency issue.
}
// Just need an arbitrary complete order.
bool operator == (const ComputationState &other) const {
return (transition_ids_ == other.transition_ids_
&& word_labels_ == other.word_labels_
&& weight_ == other.weight_);
}
ComputationState(): weight_(LatticeWeight::One()) { } // initial state.
ComputationState(const ComputationState &other):
transition_ids_(other.transition_ids_), word_labels_(other.word_labels_),
weight_(other.weight_) { }
private:
std::vector<int32> transition_ids_;
std::vector<int32> word_labels_;
LatticeWeight weight_; // contains two floats.
};
struct Tuple {
Tuple(StateId input_state, ComputationState comp_state):
input_state(input_state), comp_state(comp_state) {}
StateId input_state;
ComputationState comp_state;
};
struct TupleHash {
size_t operator() (const Tuple &state) const {
return state.input_state + 102763 * state.comp_state.Hash();
// 102763 is just an arbitrary prime number
}
};
struct TupleEqual {
bool operator () (const Tuple &state1, const Tuple &state2) const {
// treat this like operator ==
return (state1.input_state == state2.input_state
&& state1.comp_state == state2.comp_state);
}
};
typedef unordered_map<Tuple, StateId, TupleHash, TupleEqual> MapType;
StateId GetStateForTuple(const Tuple &tuple, bool add_to_queue) {
MapType::iterator iter = map_.find(tuple);
if (iter == map_.end()) { // not in map.
StateId output_state = lat_out_->AddState();
map_[tuple] = output_state;
if (add_to_queue)
queue_.push_back(std::make_pair(tuple, output_state));
return output_state;
} else {
return iter->second;
}
}
void ProcessFinal(Tuple tuple, StateId output_state) {
// ProcessFinal is only called if the input_state has
// final-prob of One(). [else it should be zero. This
// is because we called CreateSuperFinal().]
if (tuple.comp_state.IsEmpty()) { // computation state doesn't have
// anything pending.
std::vector<int32> empty_vec;
CompactLatticeWeight cw(tuple.comp_state.FinalWeight(), empty_vec);
lat_out_->SetFinal(output_state, Plus(lat_out_->Final(output_state), cw));
} else {
// computation state has something pending, i.e. input or
// output symbols that need to be flushed out. Note: OutputArc() would
// have returned false or we wouldn't have been called, so we have to
// force it out.
CompactLatticeArc lat_arc;
// Note: the next call will change the computation-state of the tuple,
// so it becomes a different tuple.
tuple.comp_state.OutputArcForce(tmodel_, opts_, &lat_arc, &error_);
lat_arc.nextstate = GetStateForTuple(tuple, true); // true == add to queue.
// The final-prob stuff will get called again from ProcessQueueElement().
// Note: because we did CreateSuperFinal(), this final-state on the input
// lattice will have no output arcs (and unit final-prob), so there will be
// no complications with processing the arcs from this state (there won't
// be any).
KALDI_ASSERT(output_state != lat_arc.nextstate);
lat_out_->AddArc(output_state, lat_arc);
}
}
void ProcessQueueElement() {
KALDI_ASSERT(!queue_.empty());
Tuple tuple = queue_.back().first;
StateId output_state = queue_.back().second;
queue_.pop_back();
// First thing is-- we see whether the computation-state has something
// pending that it wants to output. In this case we don't do
// anything further. This is a chosen behavior similar to the
// epsilon-sequencing rules encoded by the filters in
// composition.
CompactLatticeArc lat_arc;
if (tuple.comp_state.OutputPhoneArc(tmodel_, opts_, &lat_arc, &error_) ||
tuple.comp_state.OutputWordArc(tmodel_, opts_, &lat_arc, &error_)) {
// note: the functions OutputPhoneArc() and OutputWordArc() change the
// tuple (when they return true).
lat_arc.nextstate = GetStateForTuple(tuple, true); // true == add to
// queue, if not
// already present.
KALDI_ASSERT(output_state != lat_arc.nextstate);
lat_out_->AddArc(output_state, lat_arc);
} else {
// when there's nothing to output, we'll process arcs from the input-state.
// note: it would in a sense be valid to do both (i.e. process the stuff
// above, and also these), but this is a bit like the epsilon-sequencing
// stuff in composition: we avoid duplicate arcs by doing it this way.
if (lat_.Final(tuple.input_state) != CompactLatticeWeight::Zero()) {
KALDI_ASSERT(lat_.Final(tuple.input_state) == CompactLatticeWeight::One());
// ... since we did CreateSuperFinal.
ProcessFinal(tuple, output_state);
}
// Now process the arcs. Note: final-states shouldn't have any arcs.
for(fst::ArcIterator<CompactLattice> aiter(lat_, tuple.input_state);
!aiter.Done(); aiter.Next()) {
const CompactLatticeArc &arc = aiter.Value();
Tuple next_tuple(tuple);
LatticeWeight weight;
next_tuple.comp_state.Advance(arc, opts_, &weight);
next_tuple.input_state = arc.nextstate;
StateId next_output_state = GetStateForTuple(next_tuple, true); // true == add to queue,
// if not already present.
// We add an epsilon arc here (as the input and output happens
// separately)... the epsilons will get removed later.
KALDI_ASSERT(next_output_state != output_state);
lat_out_->AddArc(output_state,
CompactLatticeArc(0, 0,
CompactLatticeWeight(weight, std::vector<int32>()),
next_output_state));
}
}
}
LatticePhoneAligner(const CompactLattice &lat,
const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLattice *lat_out):
lat_(lat), tmodel_(tmodel), opts_(opts), lat_out_(lat_out),
error_(false) {
fst::CreateSuperFinal(&lat_); // Creates a super-final state, so the
// only final-probs are One().
}
// Removes epsilons; also removes unreachable states...
// not sure if these would exist if original was connected.
// This also replaces the temporary symbols for the silence
// and partial-words, with epsilons, if we wanted epsilons.
void RemoveEpsilonsFromLattice() {
RmEpsilon(lat_out_, true); // true = connect.
}
bool AlignLattice() {
lat_out_->DeleteStates();
if (lat_.Start() == fst::kNoStateId) {
KALDI_WARN << "Trying to word-align empty lattice.";
return false;
}
ComputationState initial_comp_state;
Tuple initial_tuple(lat_.Start(), initial_comp_state);
StateId start_state = GetStateForTuple(initial_tuple, true); // True = add this to queue.
lat_out_->SetStart(start_state);
while (!queue_.empty())
ProcessQueueElement();
if (opts_.remove_epsilon)
RemoveEpsilonsFromLattice();
return !error_;
}
CompactLattice lat_;
const TransitionInformation &tmodel_;
const PhoneAlignLatticeOptions &opts_;
CompactLattice *lat_out_;
std::vector<std::pair<Tuple, StateId> > queue_;
MapType map_; // map from tuples to StateId.
bool error_;
};
bool LatticePhoneAligner::ComputationState::OutputPhoneArc(
const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLatticeArc *arc_out,
bool *error) {
if (transition_ids_.empty()) return false;
int32 phone = tmodel.TransitionIdToPhone(transition_ids_[0]);
// we assume the start of transition_ids_ is the start of the phone;
// this is a precondition.
size_t len = transition_ids_.size(), i;
// Keep going till we reach a "final" transition-id; note, if
// reorder==true, we have to go a bit further after this.
for (i = 0; i < len; i++) {
int32 tid = transition_ids_[i];
int32 this_phone = tmodel.TransitionIdToPhone(tid);
if (this_phone != phone && ! *error) { // error condition: should have
// reached final transition-id first.
*error = true;
KALDI_WARN << phone << " -> " << this_phone;
KALDI_WARN << "Phone changed before final transition-id found "
"[broken lattice or mismatched model or wrong --reorder option?]";
}
if (tmodel.IsFinal(tid))
break;
}
if (i == len) return false; // fell off loop.
i++; // go past the one for which IsFinal returned true.
if (opts.reorder) // we have to consume the following self-loop transition-ids.
while (i < len && tmodel.IsSelfLoop(transition_ids_[i])) i++;
if (i == len) return false; // we don't know if it ends here... so can't output arc.
// interpret i as the number of transition-ids to consume.
std::vector<int32> tids_out(transition_ids_.begin(),
transition_ids_.begin()+i);
Label output_label = 0;
if (!word_labels_.empty()) {
output_label = word_labels_[0];
word_labels_.erase(word_labels_.begin(), word_labels_.begin()+1);
}
if (opts.replace_output_symbols)
output_label = phone;
*arc_out = CompactLatticeArc(output_label, output_label,
CompactLatticeWeight(weight_, tids_out),
fst::kNoStateId);
transition_ids_.erase(transition_ids_.begin(), transition_ids_.begin()+i);
weight_ = LatticeWeight::One(); // we just output the weight.
return true;
}
bool LatticePhoneAligner::ComputationState::OutputWordArc(
const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLatticeArc *arc_out,
bool *error) {
// output a word but no phones.
if (word_labels_.size() < 2) return false;
int32 output_label = word_labels_[0];
word_labels_.erase(word_labels_.begin(), word_labels_.begin()+1);
*arc_out = CompactLatticeArc(output_label, output_label,
CompactLatticeWeight(weight_, std::vector<int32>()),
fst::kNoStateId);
weight_ = LatticeWeight::One(); // we just output the weight, so set it to one.
return true;
}
void LatticePhoneAligner::ComputationState::OutputArcForce(
const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLatticeArc *arc_out,
bool *error) {
KALDI_ASSERT(!IsEmpty());
int32 phone = -1; // This value -1 will never be used,
// although it might not be obvious from superficially checking
// the code. IsEmpty() would be true if we had transition_ids_.empty()
// and opts.replace_output_symbols, so we would already die by assertion;
// in fact, this function would never be called.
if (!transition_ids_.empty()) { // Do some checking here.
int32 tid = transition_ids_[0];
phone = tmodel.TransitionIdToPhone(tid);
int32 num_final = 0;
for (int32 i = 0; i < transition_ids_.size(); i++) { // A check.
int32 this_tid = transition_ids_[i];
int32 this_phone = tmodel.TransitionIdToPhone(this_tid);
bool is_final = tmodel.IsFinal(this_tid); // should be exactly one.
if (is_final) num_final++;
if (this_phone != phone && ! *error) {
KALDI_WARN << "Mismatch in phone: error in lattice or mismatched transition model?";
*error = true;
}
}
if (num_final != 1 && ! *error) {
KALDI_WARN << "Problem phone-aligning lattice: saw " << num_final
<< " final-states in last phone in lattice (forced out?) "
<< "Producing partial lattice.";
*error = true;
}
}
Label output_label = 0;
if (!word_labels_.empty()) {
output_label = word_labels_[0];
word_labels_.erase(word_labels_.begin(), word_labels_.begin()+1);
}
if (opts.replace_output_symbols)
output_label = phone;
*arc_out = CompactLatticeArc(output_label, output_label,
CompactLatticeWeight(weight_, transition_ids_),
fst::kNoStateId);
transition_ids_.clear();
weight_ = LatticeWeight::One(); // we just output the weight.
}
bool PhoneAlignLattice(const CompactLattice &lat,
const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLattice *lat_out) {
LatticePhoneAligner aligner(lat, tmodel, opts, lat_out);
return aligner.AlignLattice();
}
} // namespace kaldi
@@ -0,0 +1,68 @@
// lat/phone-align-lattice.h
// Copyright 2009-2012 Microsoft Corporation 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.
#ifndef KALDI_LAT_PHONE_ALIGN_LATTICE_H_
#define KALDI_LAT_PHONE_ALIGN_LATTICE_H_
#include <fst/fstlib.h>
#include <fst/fst-decl.h>
#include "base/kaldi-common.h"
#include "util/common-utils.h"
#include "fstext/fstext-lib.h"
#include "itf/transition-information.h"
#include "lat/kaldi-lattice.h"
namespace kaldi {
struct PhoneAlignLatticeOptions {
bool reorder;
bool remove_epsilon;
bool replace_output_symbols;
PhoneAlignLatticeOptions(): reorder(true),
remove_epsilon(true),
replace_output_symbols(false) { }
void Register(OptionsItf *opts) {
opts->Register("reorder", &reorder, "True if lattice was created from HCLG with "
"--reorder=true option.");
opts->Register("remove-epsilon", &remove_epsilon, "If true, removes epsilons from "
"the phone lattice; if replace-output-symbols==false, this will "
"mean that an arc can have multiple phones on it.");
opts->Register("replace-output-symbols", &replace_output_symbols, "If true, "
"the output symbols (typically words) will be replaced with "
"phones.");
}
};
/// Outputs a lattice in which the arcs correspond exactly to sequences of
/// phones, so the boundaries between the arcs correspond to the boundaries
/// between phones If remove-epsilon == false and replace-output-symbols ==
/// false, but an arc may have >1 phone on it, but the boundaries will still
/// correspond with the boundaries between phones. Note: it's possible
/// to have arcs with words on them but no transition-ids at all. Returns true if
/// everything was OK, false if some kind of error was detected (e.g. the
/// "reorder" option was incorrectly specified.)
bool PhoneAlignLattice(const CompactLattice &lat,
const TransitionInformation &tmodel,
const PhoneAlignLatticeOptions &opts,
CompactLattice *lat_out);
} // end namespace kaldi
#endif
@@ -0,0 +1,120 @@
// lat/push-lattice-test.cc
// Copyright 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 "lat/kaldi-lattice.h"
#include "lat/push-lattice.h"
#include "fstext/rand-fst.h"
namespace kaldi {
using namespace fst;
CompactLattice *RandCompactLattice() {
RandFstOptions opts;
opts.acyclic = true;
Lattice *fst = fst::RandPairFst<LatticeArc>(opts);
CompactLattice *cfst = new CompactLattice;
ConvertLattice(*fst, cfst);
delete fst;
return cfst;
}
void TestPushCompactLatticeStrings() {
CompactLattice *clat = RandCompactLattice();
CompactLattice clat2(*clat);
PushCompactLatticeStrings(&clat2);
KALDI_ASSERT(fst::RandEquivalent(*clat, clat2, 5, 0.001, Rand(), 10));
for (CompactLatticeArc::StateId s = 0; s < clat2.NumStates(); s++) {
if (s == 0)
continue; // We don't check state zero, as the "leftover string" stays
// there.
int32 first_label = -1;
bool ok = false;
bool first_label_set = false;
for (ArcIterator<CompactLattice> aiter(clat2, s); !aiter.Done();
aiter.Next()) {
if (aiter.Value().weight.String().size() == 0) {
ok = true;
} else {
int32 this_label = aiter.Value().weight.String().front();
if (first_label_set) {
if (this_label != first_label) ok = true;
} else {
first_label = this_label;
first_label_set = true;
}
}
}
if (clat2.Final(s) != CompactLatticeWeight::Zero()) {
if (clat2.Final(s).String().size() == 0) ok = true;
else {
int32 this_label = clat2.Final(s).String().front();
if (first_label_set && this_label != first_label) ok = true;
}
}
KALDI_ASSERT(ok);
}
delete clat;
}
void TestPushCompactLatticeWeights() {
CompactLattice *clat = RandCompactLattice();
CompactLattice clat2(*clat);
PushCompactLatticeWeights(&clat2);
KALDI_ASSERT(fst::RandEquivalent(*clat, clat2, 5, 0.001, Rand(), 10));
for (CompactLatticeArc::StateId s = 0; s < clat2.NumStates(); s++) {
if (s == 0)
continue; // We don't check state zero, as the "leftover string" stays
// there.
LatticeWeight sum = clat2.Final(s).Weight();
for (ArcIterator<CompactLattice> aiter(clat2, s); !aiter.Done();
aiter.Next()) {
sum = Plus(sum, aiter.Value().weight.Weight());
}
if (!ApproxEqual(sum, LatticeWeight::One())) {
{
fst::FstPrinter<CompactLatticeArc> printer(clat2, NULL, NULL,
NULL, true, true, "\t");
printer.Print(&std::cerr, "<unknown>");
}
{
fst::FstPrinter<CompactLatticeArc> printer(*clat, NULL, NULL,
NULL, true, true, "\t");
printer.Print(&std::cerr, "<unknown>");
}
KALDI_ERR << "Bad lattice being pushed.";
}
}
delete clat;
}
} // end namespace kaldi
int main() {
using namespace kaldi;
using kaldi::int32;
for (int32 i = 0; i < 15; i++) {
TestPushCompactLatticeStrings();
TestPushCompactLatticeWeights();
}
KALDI_LOG << "Success.";
}
@@ -0,0 +1,290 @@
// lat/push-lattice.cc
// Copyright 2009-2011 Saarland University (Author: Arnab Ghoshal)
// 2012-2013 Johns Hopkins University (Author: Daniel Povey); Chao Weng;
// Bagher BabaAli
// 2014 Guoguo Chen
// 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 "lat/push-lattice.h"
#include "util/stl-utils.h"
namespace fst {
template<class Weight, class IntType> class CompactLatticePusher {
public:
typedef CompactLatticeWeightTpl<Weight, IntType> CompactWeight;
typedef ArcTpl<CompactWeight> CompactArc;
typedef typename CompactArc::StateId StateId;
CompactLatticePusher(MutableFst<CompactArc> *clat): clat_(clat) { }
bool Push() {
if (clat_->Properties(kTopSorted, true) == 0) {
if (!TopSort(clat_)) {
KALDI_WARN << "Topological sorting of state-level lattice failed "
"(probably your lexicon has empty words or your LM has epsilon cycles; this "
" is a bad idea.)";
return false;
}
}
ComputeShifts();
ApplyShifts();
return true;
}
// Gets the string of length [end - begin], starting at this
// state and taking arc "arc_idx" (and thereafter an arbitrary sequence).
// Note: here, arc_idx == -1 means take an arbitrary path.
static void GetString(const ExpandedFst<CompactArc> &clat,
StateId state,
size_t arc_idx,
typename std::vector<IntType>::iterator begin,
typename std::vector<IntType>::iterator end) {
CompactWeight final = clat.Final(state);
size_t len = end - begin;
KALDI_ASSERT(len >= 0);
if (len == 0) return;
if (arc_idx == -1 && final != CompactWeight::Zero()) {
const std::vector<IntType> &string = final.String();
KALDI_ASSERT(string.size() >= len &&
"Either code error, or paths in lattice have inconsistent lengths");
std::copy(string.begin(), string.begin() + len, begin);
return;
}
ArcIterator<ExpandedFst<CompactArc> > aiter(clat, state);
if (arc_idx != -1)
aiter.Seek(arc_idx);
KALDI_ASSERT(!aiter.Done() &&
"Either code error, or paths in lattice are inconsistent in length.");
const CompactArc &arc = aiter.Value();
size_t arc_len = arc.weight.String().size();
if (arc_len >= len) {
std::copy(arc.weight.String().begin(), arc.weight.String().begin() + len, begin);
} else {
std::copy(arc.weight.String().begin(), arc.weight.String().end(), begin);
// Recurse.
GetString(clat, arc.nextstate, -1, begin + arc_len, end);
}
}
void CheckForConflict(const CompactWeight &final,
StateId state,
int32 *shift) {
if (shift == NULL) return;
// At input, "shift" has the maximum value that we could shift back assuming
// there is no conflict between the values of the strings. We need to check
// if there is conflict, and if so, reduce the "shift".
bool is_final = (final != CompactWeight::Zero());
size_t num_arcs = clat_->NumArcs(state);
if (num_arcs + (is_final ? 1 : 0) > 1 && *shift > 0) {
// There is potential for conflict between string values, because >1
// [arc or final-prob]. Find the longest shift up to and including the
// current shift, that gives no conflict.
std::vector<IntType> string(*shift), compare_string(*shift);
size_t arc;
if (is_final) {
KALDI_ASSERT(final.String().size() >= *shift);
std::copy(final.String().begin(), final.String().begin() + *shift,
string.begin());
arc = 0;
} else {
// set "string" to string if we take 1st arc.
GetString(*clat_, state, 0, string.begin(), string.end());
arc = 1;
}
for (; arc < num_arcs; arc++) { // for the other arcs..
GetString(*clat_, state, arc,
compare_string.begin(), compare_string.end());
std::pair<typename std::vector<IntType>::iterator,
typename std::vector<IntType>::iterator> pr =
std::mismatch(string.begin(), string.end(),
compare_string.begin());
if (pr.first != string.end()) { // There was a mismatch. Reduce the shift
// to a value where they will match.
*shift = pr.first - string.begin();
string.resize(*shift);
compare_string.resize(*shift);
}
}
}
}
void ComputeShifts() {
StateId num_states = clat_->NumStates();
shift_vec_.resize(num_states, 0);
// The for loop will only work if StateId is signed, so assert this.
KALDI_COMPILE_TIME_ASSERT(static_cast<StateId>(-1) < static_cast<StateId>(0));
// We rely on the topological sorting, so clat_->Start() should be zero or
// at least any preceding states should be non-accessible. We leave the
// shift at zero for the start state because we can't shift to before that.
for (StateId state = num_states - 1; state > clat_->Start(); state--) {
size_t num_arcs = clat_->NumArcs(state);
CompactWeight final = clat_->Final(state);
if (num_arcs == 0) {
// we can shift back by the number of transition-ids on the
// final-prob, if any.
shift_vec_[state] = final.String().size();
} else { // We have arcs ...
int32 shift = std::numeric_limits<int32>::max();
size_t num_arcs = 0;
bool is_final = (final != CompactWeight::Zero());
if (is_final)
shift = std::min(shift, static_cast<int32>(final.String().size()));
for (ArcIterator<MutableFst<CompactArc> > aiter(*clat_, state);
!aiter.Done(); aiter.Next(), num_arcs++) {
const CompactArc &arc (aiter.Value());
shift = std::min(shift, shift_vec_[arc.nextstate] +
static_cast<int32>(arc.weight.String().size()));
}
CheckForConflict(final, state, &shift);
shift_vec_[state] = shift;
}
}
}
void ApplyShifts() {
StateId num_states = clat_->NumStates();
for (StateId state = 0; state < num_states; state++) {
int32 shift = shift_vec_[state];
std::vector<IntType> string;
for (MutableArcIterator<MutableFst<CompactArc> > aiter(clat_, state);
!aiter.Done(); aiter.Next()) {
CompactArc arc(aiter.Value());
KALDI_ASSERT(arc.nextstate > state && "Cyclic lattice");
string = arc.weight.String();
size_t orig_len = string.size(), next_shift = shift_vec_[arc.nextstate];
// extend "string" by next_shift.
string.resize(string.size() + next_shift);
// The next command sets the last "next_shift" elements of 'string' to
// the string starting from arc.nextstate (taking an arbitrary path).
GetString(*clat_, arc.nextstate, -1,
string.begin() + orig_len, string.end());
// Remove the first "shift" elements of this string and set the
// arc-weight string to this.
arc.weight.SetString(std::vector<IntType>(string.begin() + shift,
string.end()));
aiter.SetValue(arc);
}
CompactWeight final = clat_->Final(state);
if (final != CompactWeight::Zero()) {
// Erase first "shift" elements of final-prob.
final.SetString(std::vector<IntType>(final.String().begin() + shift,
final.String().end()));
clat_->SetFinal(state, final);
}
}
}
private:
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *clat_;
// For each state s, shift_vec_[s] >= 0 is how much we will shift the
// transition-ids back at this state.
std::vector<int32> shift_vec_;
};
template<class Weight, class IntType>
bool PushCompactLatticeStrings(
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *clat) {
CompactLatticePusher<Weight, IntType> pusher(clat);
return pusher.Push();
}
template<class Weight, class IntType>
bool PushCompactLatticeWeights(
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *clat) {
if (clat->Properties(kTopSorted, true) == 0) {
if (!TopSort(clat)) {
KALDI_WARN << "Topological sorting of state-level lattice failed "
"(probably your lexicon has empty words or your LM has epsilon cycles; this "
" is a bad idea.)";
return false;
}
}
typedef CompactLatticeWeightTpl<Weight, IntType> CompactWeight;
typedef ArcTpl<CompactWeight> CompactArc;
typedef typename CompactArc::StateId StateId;
StateId num_states = clat->NumStates();
if (num_states == 0) {
KALDI_WARN << "Pushing weights of empty compact lattice";
return true; // this is technically success because an empty
// lattice is already pushed.
}
std::vector<Weight> weight_to_end(num_states); // Note: LatticeWeight
// contains two floats.
for (StateId s = num_states - 1; s >= 0; s--) {
Weight this_weight_to_end = clat->Final(s).Weight();
for (ArcIterator<MutableFst<CompactArc> > aiter(*clat, s);
!aiter.Done(); aiter.Next()) {
const CompactArc &arc = aiter.Value();
KALDI_ASSERT(arc.nextstate > s && "Cyclic lattices not allowed.");
this_weight_to_end = Plus(this_weight_to_end,
Times(aiter.Value().weight.Weight(),
weight_to_end[arc.nextstate]));
}
if (this_weight_to_end == Weight::Zero()) {
KALDI_WARN << "Lattice has non-coaccessible states.";
}
weight_to_end[s] = this_weight_to_end;
}
weight_to_end[0] = Weight::One(); // We leave the "leftover weight" on
// the start state, which won't
// necessarily end up summing to one.
for (StateId s = 0; s < num_states; s++) {
Weight this_weight_to_end = weight_to_end[s];
if (this_weight_to_end == Weight::Zero())
continue;
for (MutableArcIterator<MutableFst<CompactArc> > aiter(clat, s);
!aiter.Done(); aiter.Next()) {
CompactArc arc = aiter.Value();
Weight next_weight_to_end = weight_to_end[arc.nextstate];
if (next_weight_to_end != Weight::Zero()) {
arc.weight.SetWeight(Times(arc.weight.Weight(),
Divide(next_weight_to_end,
this_weight_to_end)));
aiter.SetValue(arc);
}
}
CompactWeight final_weight = clat->Final(s);
if (final_weight != CompactWeight::Zero()) {
final_weight.SetWeight(Divide(final_weight.Weight(), this_weight_to_end));
clat->SetFinal(s, final_weight);
}
}
return true;
}
// Instantiate for CompactLattice.
template
bool PushCompactLatticeStrings<kaldi::LatticeWeight, kaldi::int32>(
MutableFst<kaldi::CompactLatticeArc> *clat);
template
bool PushCompactLatticeWeights<kaldi::LatticeWeight, kaldi::int32>(
MutableFst<kaldi::CompactLatticeArc> *clat);
} // namespace fst
@@ -0,0 +1,61 @@
// lat/push-lattice.h
// Copyright 2013 Johns Hopkins University (Author: Daniel Povey)
// 2014 Guoguo Chen
// 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.
#ifndef KALDI_LAT_PUSH_LATTICE_H_
#define KALDI_LAT_PUSH_LATTICE_H_
#include <vector>
#include <map>
#include "base/kaldi-common.h"
#include "util/common-utils.h"
#include "fstext/fstext-lib.h"
#include "lat/kaldi-lattice.h"
namespace fst {
/// This function pushes the transition-ids as far towards the start as they
/// will go. It can be useful prior to lattice-align-words (for non-linear
/// lattices). We can't use the generic OpenFst "push" function because
/// it uses the sum as the divisor, which is not appropriate in this case
/// (a+b generally won't divide a or b in this semiring).
/// It returns true on success, false if it failed due to TopSort failing,
/// which should never happen, but we handle it gracefully by just leaving the
/// lattice the same.
/// This function used to be called just PushCompactLattice.
template<class Weight, class IntType>
bool PushCompactLatticeStrings(
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *clat);
/// This function pushes the weights in the CompactLattice so that all states
/// except possibly the start state, have Weight components (of type
/// LatticeWeight) that "sum to one" in the LatticeWeight (i.e. interpreting the
/// weights as negated log-probs). It returns true on success, false if it
/// failed due to TopSort failing, which should never happen, but we handle it
/// gracefully by just leaving the lattice the same.
template<class Weight, class IntType>
bool PushCompactLatticeWeights(
MutableFst<ArcTpl<CompactLatticeWeightTpl<Weight, IntType> > > *clat);
} // namespace fst
#endif // KALDI_LAT_PUSH_LATTICE_H_
+432
View File
@@ -0,0 +1,432 @@
// lat/sausages.cc
// Copyright 2012 Johns Hopkins University (Author: Daniel Povey)
// 2015 Guoguo Chen
// 2019 Dogan Can
// 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 "lat/sausages.h"
#include "lat/lattice-functions.h"
namespace kaldi {
// this is Figure 6 in the paper.
void MinimumBayesRisk::MbrDecode() {
for (size_t counter = 0; ; counter++) {
NormalizeEps(&R_);
AccStats(); // writes to gamma_
double delta_Q = 0.0; // change in objective function.
one_best_times_.clear();
one_best_confidences_.clear();
// Caution: q in the line below is (q-1) in the algorithm
// in the paper; both R_ and gamma_ are indexed by q-1.
for (size_t q = 0; q < R_.size(); q++) {
if (opts_.decode_mbr) { // This loop updates R_ [indexed same as gamma_].
// gamma_[i] is sorted in reverse order so most likely one is first.
const std::vector<std::pair<int32, BaseFloat> > &this_gamma = gamma_[q];
double old_gamma = 0, new_gamma = this_gamma[0].second;
int32 rq = R_[q], rhat = this_gamma[0].first; // rq: old word, rhat: new.
for (size_t j = 0; j < this_gamma.size(); j++)
if (this_gamma[j].first == rq) old_gamma = this_gamma[j].second;
delta_Q += (old_gamma - new_gamma); // will be 0 or negative; a bound on
// change in error.
if (rq != rhat)
KALDI_VLOG(2) << "Changing word " << rq << " to " << rhat;
R_[q] = rhat;
}
// build the outputs (time, confidences),
if (R_[q] != 0 || opts_.print_silence) {
// see which 'item' from the sausage-bin should we select,
// (not necessarily the 1st one when MBR decoding disabled)
int32 s = 0;
for (int32 j=0; j<gamma_[q].size(); j++) {
if (gamma_[q][j].first == R_[q]) {
s = j;
break;
}
}
one_best_times_.push_back(times_[q][s]);
// post-process the times,
size_t i = one_best_times_.size();
if (i > 1 && one_best_times_[i-2].second > one_best_times_[i-1].first) {
// It's quite possible for this to happen, but it seems like it would
// have a bad effect on the downstream processing, so we fix it here.
// We resolve overlaps by redistributing the available time interval.
BaseFloat prev_right = i > 2 ? one_best_times_[i-3].second : 0.0;
BaseFloat left = std::max(prev_right,
std::min(one_best_times_[i-2].first,
one_best_times_[i-1].first));
BaseFloat right = std::max(one_best_times_[i-2].second,
one_best_times_[i-1].second);
BaseFloat first_dur =
one_best_times_[i-2].second - one_best_times_[i-2].first;
BaseFloat second_dur =
one_best_times_[i-1].second - one_best_times_[i-1].first;
BaseFloat mid = first_dur > 0 ? left + (right - left) * first_dur /
(first_dur + second_dur) : left;
one_best_times_[i-2].first = left;
one_best_times_[i-2].second = one_best_times_[i-1].first = mid;
one_best_times_[i-1].second = right;
}
BaseFloat confidence = 0.0;
for (int32 j = 0; j < gamma_[q].size(); j++) {
if (gamma_[q][j].first == R_[q]) {
confidence = gamma_[q][j].second;
break;
}
}
one_best_confidences_.push_back(confidence);
}
}
KALDI_VLOG(2) << "Iter = " << counter << ", delta-Q = " << delta_Q;
if (delta_Q == 0) break;
if (counter > 100) {
KALDI_WARN << "Iterating too many times in MbrDecode; stopping.";
break;
}
}
if (!opts_.print_silence) RemoveEps(&R_);
}
struct Int32IsZero {
bool operator() (int32 i) { return (i == 0); }
};
// static
void MinimumBayesRisk::RemoveEps(std::vector<int32> *vec) {
Int32IsZero pred;
vec->erase(std::remove_if (vec->begin(), vec->end(), pred),
vec->end());
}
// static
void MinimumBayesRisk::NormalizeEps(std::vector<int32> *vec) {
RemoveEps(vec);
vec->resize(1 + vec->size() * 2);
int32 s = vec->size();
for (int32 i = s/2 - 1; i >= 0; i--) {
(*vec)[i*2 + 1] = (*vec)[i];
(*vec)[i*2 + 2] = 0;
}
(*vec)[0] = 0;
}
double MinimumBayesRisk::EditDistance(int32 N, int32 Q,
Vector<double> &alpha,
Matrix<double> &alpha_dash,
Vector<double> &alpha_dash_arc) {
alpha(1) = 0.0; // = log(1). Line 5.
alpha_dash(1, 0) = 0.0; // Line 5.
for (int32 q = 1; q <= Q; q++)
alpha_dash(1, q) = alpha_dash(1, q-1) + l(0, r(q)); // Line 7.
for (int32 n = 2; n <= N; n++) {
double alpha_n = kLogZeroDouble;
for (size_t i = 0; i < pre_[n].size(); i++) {
const Arc &arc = arcs_[pre_[n][i]];
alpha_n = LogAdd(alpha_n, alpha(arc.start_node) + arc.loglike);
}
alpha(n) = alpha_n; // Line 10.
// Line 11 omitted: matrix was initialized to zero.
for (size_t i = 0; i < pre_[n].size(); i++) {
const Arc &arc = arcs_[pre_[n][i]];
int32 s_a = arc.start_node, w_a = arc.word;
BaseFloat p_a = arc.loglike;
for (int32 q = 0; q <= Q; q++) {
if (q == 0) {
alpha_dash_arc(q) = // line 15.
alpha_dash(s_a, q) + l(w_a, 0, true);
} else { // a1,a2,a3 are the 3 parts of min expression of line 17.
int32 r_q = r(q);
double a1 = alpha_dash(s_a, q-1) + l(w_a, r_q),
a2 = alpha_dash(s_a, q) + l(w_a, 0, true),
a3 = alpha_dash_arc(q-1) + l(0, r_q);
alpha_dash_arc(q) = std::min(a1, std::min(a2, a3));
}
// line 19:
alpha_dash(n, q) += Exp(alpha(s_a) + p_a - alpha(n)) * alpha_dash_arc(q);
}
}
}
return alpha_dash(N, Q); // line 23.
}
// Figure 5 in the paper.
void MinimumBayesRisk::AccStats() {
using std::map;
int32 N = static_cast<int32>(pre_.size()) - 1,
Q = static_cast<int32>(R_.size());
Vector<double> alpha(N+1); // index (1...N)
Matrix<double> alpha_dash(N+1, Q+1); // index (1...N, 0...Q)
Vector<double> alpha_dash_arc(Q+1); // index 0...Q
Matrix<double> beta_dash(N+1, Q+1); // index (1...N, 0...Q)
Vector<double> beta_dash_arc(Q+1); // index 0...Q
std::vector<char> b_arc(Q+1); // integer in {1,2,3}; index 1...Q
std::vector<map<int32, double> > gamma(Q+1); // temp. form of gamma.
// index 1...Q [word] -> occ.
// The tau maps below are the sums over arcs with the same word label
// of the tau_b and tau_e timing quantities mentioned in Appendix C of
// the paper... we are using these to get averaged times for both the
// the sausage bins and the 1-best output.
std::vector<map<int32, double> > tau_b(Q+1), tau_e(Q+1);
double Ltmp = EditDistance(N, Q, alpha, alpha_dash, alpha_dash_arc);
if (L_ != 0 && Ltmp > L_) { // L_ != 0 is to rule out 1st iter.
KALDI_WARN << "Edit distance increased: " << Ltmp << " > "
<< L_;
}
L_ = Ltmp;
KALDI_VLOG(2) << "L = " << L_;
// omit line 10: zero when initialized.
beta_dash(N, Q) = 1.0; // Line 11.
for (int32 n = N; n >= 2; n--) {
for (size_t i = 0; i < pre_[n].size(); i++) {
const Arc &arc = arcs_[pre_[n][i]];
int32 s_a = arc.start_node, w_a = arc.word;
BaseFloat p_a = arc.loglike;
alpha_dash_arc(0) = alpha_dash(s_a, 0) + l(w_a, 0, true); // line 14.
for (int32 q = 1; q <= Q; q++) { // this loop == lines 15-18.
int32 r_q = r(q);
double a1 = alpha_dash(s_a, q-1) + l(w_a, r_q),
a2 = alpha_dash(s_a, q) + l(w_a, 0, true),
a3 = alpha_dash_arc(q-1) + l(0, r_q);
if (a1 <= a2) {
if (a1 <= a3) { b_arc[q] = 1; alpha_dash_arc(q) = a1; }
else { b_arc[q] = 3; alpha_dash_arc(q) = a3; }
} else {
if (a2 <= a3) { b_arc[q] = 2; alpha_dash_arc(q) = a2; }
else { b_arc[q] = 3; alpha_dash_arc(q) = a3; }
}
}
beta_dash_arc.SetZero(); // line 19.
for (int32 q = Q; q >= 1; q--) {
// line 21:
beta_dash_arc(q) += Exp(alpha(s_a) + p_a - alpha(n)) * beta_dash(n, q);
switch (static_cast<int>(b_arc[q])) { // lines 22 and 23:
case 1:
beta_dash(s_a, q-1) += beta_dash_arc(q);
// next: gamma(q, w(a)) += beta_dash_arc(q)
AddToMap(w_a, beta_dash_arc(q), &(gamma[q]));
// next: accumulating times, see decl for tau_b,tau_e
AddToMap(w_a, state_times_[s_a] * beta_dash_arc(q), &(tau_b[q]));
AddToMap(w_a, state_times_[n] * beta_dash_arc(q), &(tau_e[q]));
break;
case 2:
beta_dash(s_a, q) += beta_dash_arc(q);
break;
case 3:
beta_dash_arc(q-1) += beta_dash_arc(q);
// next: gamma(q, epsilon) += beta_dash_arc(q)
AddToMap(0, beta_dash_arc(q), &(gamma[q]));
// next: accumulating times, see decl for tau_b,tau_e
// WARNING: there was an error in Appendix C. If we followed
// the instructions there the next line would say state_times_[sa], but
// it would be wrong. I will try to publish an erratum.
AddToMap(0, state_times_[n] * beta_dash_arc(q), &(tau_b[q]));
AddToMap(0, state_times_[n] * beta_dash_arc(q), &(tau_e[q]));
break;
default:
KALDI_ERR << "Invalid b_arc value"; // error in code.
}
}
beta_dash_arc(0) += Exp(alpha(s_a) + p_a - alpha(n)) * beta_dash(n, 0);
beta_dash(s_a, 0) += beta_dash_arc(0); // line 26.
}
}
beta_dash_arc.SetZero(); // line 29.
for (int32 q = Q; q >= 1; q--) {
beta_dash_arc(q) += beta_dash(1, q);
beta_dash_arc(q-1) += beta_dash_arc(q);
AddToMap(0, beta_dash_arc(q), &(gamma[q]));
// the statements below are actually redundant because
// state_times_[1] is zero.
AddToMap(0, state_times_[1] * beta_dash_arc(q), &(tau_b[q]));
AddToMap(0, state_times_[1] * beta_dash_arc(q), &(tau_e[q]));
}
for (int32 q = 1; q <= Q; q++) { // a check (line 35)
double sum = 0.0;
for (map<int32, double>::iterator iter = gamma[q].begin();
iter != gamma[q].end(); ++iter) sum += iter->second;
if (fabs(sum - 1.0) > 0.1)
KALDI_WARN << "sum of gamma[" << q << ",s] is " << sum;
}
// The next part is where we take gamma, and convert
// to the class member gamma_, which is using a different
// data structure and indexed from zero, not one.
gamma_.clear();
gamma_.resize(Q);
for (int32 q = 1; q <= Q; q++) {
for (map<int32, double>::iterator iter = gamma[q].begin();
iter != gamma[q].end(); ++iter)
gamma_[q-1].push_back(
std::make_pair(iter->first, static_cast<BaseFloat>(iter->second)));
// sort gamma_[q-1] from largest to smallest posterior.
GammaCompare comp;
std::sort(gamma_[q-1].begin(), gamma_[q-1].end(), comp);
}
// We do the same conversion for the state times tau_b and tau_e:
// they get turned into the times_ data member, which has zero-based
// indexing.
times_.clear();
times_.resize(Q);
sausage_times_.clear();
sausage_times_.resize(Q);
for (int32 q = 1; q <= Q; q++) {
double t_b = 0.0, t_e = 0.0;
for (std::vector<std::pair<int32, BaseFloat>>::iterator iter = gamma_[q-1].begin();
iter != gamma_[q-1].end(); ++iter) {
double w_b = tau_b[q][iter->first], w_e = tau_e[q][iter->first];
if (w_b > w_e)
KALDI_WARN << "Times out of order"; // this is quite bad.
times_[q-1].push_back(
std::make_pair(static_cast<BaseFloat>(w_b / iter->second),
static_cast<BaseFloat>(w_e / iter->second)));
t_b += w_b;
t_e += w_e;
}
sausage_times_[q-1].first = t_b;
sausage_times_[q-1].second = t_e;
if (sausage_times_[q-1].first > sausage_times_[q-1].second)
KALDI_WARN << "Times out of order"; // this is quite bad.
if (q > 1 && sausage_times_[q-2].second > sausage_times_[q-1].first) {
// We previously had a warning here, but now we'll just set both
// those values to their average. It's quite possible for this
// condition to happen, but it seems like it would have a bad effect
// on the downstream processing, so we fix it.
sausage_times_[q-2].second = sausage_times_[q-1].first =
0.5 * (sausage_times_[q-2].second + sausage_times_[q-1].first);
}
}
}
void MinimumBayesRisk::PrepareLatticeAndInitStats(CompactLattice *clat) {
KALDI_ASSERT(clat != NULL);
CreateSuperFinal(clat); // Add super-final state to clat... this is
// one of the requirements of the MBR algorithm, as mentioned in the
// paper (i.e. just one final state).
// Topologically sort the lattice, if not already sorted.
kaldi::uint64 props = clat->Properties(fst::kFstProperties, false);
if (!(props & fst::kTopSorted)) {
if (fst::TopSort(clat) == false)
KALDI_ERR << "Cycles detected in lattice.";
}
CompactLatticeStateTimes(*clat, &state_times_); // work out times of
// the states in clat
state_times_.push_back(0); // we'll convert to 1-based numbering.
for (size_t i = state_times_.size()-1; i > 0; i--)
state_times_[i] = state_times_[i-1];
// Now we convert the information in "clat" into a special internal
// format (pre_, post_ and arcs_) which allows us to access the
// arcs preceding any given state.
// Note: in our internal format the states will be numbered from 1,
// which involves adding 1 to the OpenFst states.
int32 N = clat->NumStates();
pre_.resize(N+1);
// Careful: "Arc" is a class-member struct, not an OpenFst type of arc as one
// would normally assume.
for (int32 n = 1; n <= N; n++) {
for (fst::ArcIterator<CompactLattice> aiter(*clat, n-1);
!aiter.Done();
aiter.Next()) {
const CompactLatticeArc &carc = aiter.Value();
Arc arc; // in our local format.
arc.word = carc.ilabel; // == carc.olabel
arc.start_node = n;
arc.end_node = carc.nextstate + 1; // convert to 1-based.
arc.loglike = - (carc.weight.Weight().Value1() +
carc.weight.Weight().Value2());
// loglike: sum graph/LM and acoustic cost, and negate to
// convert to loglikes. We assume acoustic scaling is already done.
pre_[arc.end_node].push_back(arcs_.size()); // record index of this arc.
arcs_.push_back(arc);
}
}
}
MinimumBayesRisk::MinimumBayesRisk(const CompactLattice &clat_in,
MinimumBayesRiskOptions opts) : opts_(opts) {
CompactLattice clat(clat_in); // copy.
PrepareLatticeAndInitStats(&clat);
// We don't need to look at clat.Start() or clat.Final(state):
// we know clat.Start() == 0 since it's topologically sorted,
// and clat.Final(state) is Zero() except for One() at the last-
// numbered state, thanks to CreateSuperFinal and the topological
// sorting.
{ // Now set R_ to one best in the FST.
RemoveAlignmentsFromCompactLattice(&clat); // will be more efficient
// in best-path if we do this.
Lattice lat;
ConvertLattice(clat, &lat); // convert from CompactLattice to Lattice.
fst::VectorFst<fst::StdArc> fst;
ConvertLattice(lat, &fst); // convert from lattice to normal FST.
fst::VectorFst<fst::StdArc> fst_shortest_path;
fst::ShortestPath(fst, &fst_shortest_path); // take shortest path of FST.
std::vector<int32> alignment, words;
fst::TropicalWeight weight;
GetLinearSymbolSequence(fst_shortest_path, &alignment, &words, &weight);
KALDI_ASSERT(alignment.empty()); // we removed the alignment.
R_ = words;
L_ = 0.0; // Set current edit-distance to 0 [just so we know
// when we're on the 1st iter.]
}
MbrDecode();
}
MinimumBayesRisk::MinimumBayesRisk(const CompactLattice &clat_in,
const std::vector<int32> &words,
MinimumBayesRiskOptions opts) : opts_(opts) {
CompactLattice clat(clat_in); // copy.
PrepareLatticeAndInitStats(&clat);
R_ = words;
L_ = 0.0;
MbrDecode();
}
MinimumBayesRisk::MinimumBayesRisk(const CompactLattice &clat_in,
const std::vector<int32> &words,
const std::vector<std::pair<BaseFloat,BaseFloat> > &times,
MinimumBayesRiskOptions opts) : opts_(opts) {
CompactLattice clat(clat_in); // copy.
PrepareLatticeAndInitStats(&clat);
R_ = words;
sausage_times_ = times;
L_ = 0.0;
MbrDecode();
}
} // namespace kaldi
+270
View File
@@ -0,0 +1,270 @@
// lat/sausages.h
// Copyright 2012 Johns Hopkins University (Author: Daniel Povey)
// 2015 Guoguo Chen
// 2019 Dogan Can
// 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.
#ifndef KALDI_LAT_SAUSAGES_H_
#define KALDI_LAT_SAUSAGES_H_
#include <vector>
#include <map>
#include "base/kaldi-common.h"
#include "util/common-utils.h"
#include "fstext/fstext-lib.h"
#include "lat/kaldi-lattice.h"
namespace kaldi {
/// The implementation of the Minimum Bayes Risk decoding method described in
/// "Minimum Bayes Risk decoding and system combination based on a recursion for
/// edit distance", Haihua Xu, Daniel Povey, Lidia Mangu and Jie Zhu, Computer
/// Speech and Language, 2011
/// This is a slightly more principled way to do Minimum Bayes Risk (MBR) decoding
/// than the standard "Confusion Network" method. Note: MBR decoding aims to
/// minimize the expected word error rate, assuming the lattice encodes the
/// true uncertainty about what was spoken; standard Viterbi decoding gives the
/// most likely utterance, which corresponds to minimizing the expected sentence
/// error rate.
///
/// In addition to giving the MBR output, we also provide a way to get a
/// "Confusion Network" or informally "sausage"-like structure. This is a
/// linear sequence of bins, and in each bin, there is a distribution over
/// words (or epsilon, meaning no word). This is useful for estimating
/// confidence. Note: due to the way these sausages are made, typically there
/// will be, between each bin representing a high-confidence word, a bin
/// in which epsilon (no word) is the most likely word. Inside these bins
/// is where we put possible insertions.
struct MinimumBayesRiskOptions {
/// Boolean configuration parameter: if true, we actually update the hypothesis
/// to do MBR decoding (if false, our output is the MAP decoded output, but we
/// output the stats too (i.e. the confidences)).
bool decode_mbr;
/// Boolean configuration parameter: if true, the 1-best path will 'keep' the <eps> bins,
bool print_silence;
MinimumBayesRiskOptions() : decode_mbr(true), print_silence(false)
{ }
void Register(OptionsItf *opts) {
opts->Register("decode-mbr", &decode_mbr, "If true, do Minimum Bayes Risk "
"decoding (else, Maximum a Posteriori)");
opts->Register("print-silence", &print_silence, "Keep the inter-word '<eps>' "
"bins in the 1-best output (ctm, <eps> can be a 'silence' or a 'deleted' word)");
}
};
/// This class does the word-level Minimum Bayes Risk computation, and gives you
/// either the 1-best MBR output together with the expected Bayes Risk,
/// or a sausage-like structure.
class MinimumBayesRisk {
public:
/// Initialize with compact lattice-- any acoustic scaling etc., is assumed
/// to have been done already.
/// This does the whole computation. You get the output with
/// GetOneBest(), GetBayesRisk(), and GetSausageStats().
MinimumBayesRisk(const CompactLattice &clat,
MinimumBayesRiskOptions opts = MinimumBayesRiskOptions());
// Uses the provided <words> as <R_> instead of using the lattice best path.
// Note that the default value of opts.decode_mbr is true. If you provide 1-best
// hypothesis from MAP decoding, the output ctm from MBR decoding may be
// mismatched with the provided <words> (<words> would be used as the starting
// point of optimization).
MinimumBayesRisk(const CompactLattice &clat,
const std::vector<int32> &words,
MinimumBayesRiskOptions opts = MinimumBayesRiskOptions());
// Uses the provided <words> as <R_> and <times> of bins instead of using the lattice best path.
// Note that the default value of opts.decode_mbr is true. If you provide 1-best
// hypothesis from MAP decoding, the output ctm from MBR decoding may be
// mismatched with the provided <words> (<words> would be used as the starting
// point of optimization).
MinimumBayesRisk(const CompactLattice &clat,
const std::vector<int32> &words,
const std::vector<std::pair<BaseFloat,BaseFloat> > &times,
MinimumBayesRiskOptions opts = MinimumBayesRiskOptions());
const std::vector<int32> &GetOneBest() const { // gets one-best (with no epsilons)
return R_;
}
const std::vector<std::vector<std::pair<BaseFloat, BaseFloat> > > GetTimes() const {
return times_; // returns average (start,end) times for each word in each
// bin. These are raw averages without any processing, i.e. time intervals
// from different bins can overlap.
}
const std::vector<std::pair<BaseFloat, BaseFloat> > GetSausageTimes() const {
return sausage_times_; // returns average (start,end) times for each bin.
// This is typically the weighted average of the times in GetTimes() but can
// be slightly different if the times for the bins overlap, in which case
// the times returned by this method do not overlap unlike the times
// returned by GetTimes().
}
const std::vector<std::pair<BaseFloat, BaseFloat> > &GetOneBestTimes() const {
return one_best_times_; // returns average (start,end) times for each word
// corresponding to an entry in the one-best output. This is typically the
// appropriate subset of the times in GetTimes() but can be slightly
// different if the times for the one-best words overlap, in which case
// the times returned by this method do not overlap unlike the times
// returned by GetTimes().
}
/// Outputs the confidences for the one-best transcript.
const std::vector<BaseFloat> &GetOneBestConfidences() const {
return one_best_confidences_;
}
/// Returns the expected WER over this sentence (assuming model correctness).
BaseFloat GetBayesRisk() const { return L_; }
const std::vector<std::vector<std::pair<int32, BaseFloat> > > &GetSausageStats() const {
return gamma_;
}
private:
void PrepareLatticeAndInitStats(CompactLattice *clat);
/// Minimum-Bayes-Risk Decode. Top-level algorithm. Figure 6 of the paper.
void MbrDecode();
/// Without the 'penalize' argument this gives us the basic edit-distance
/// function l(a,b), as in the paper.
/// With the 'penalize' argument it can be interpreted as the edit distance
/// plus the 'delta' from the paper, except that we make a kind of conceptual
/// bug-fix and only apply the delta if the edit-distance was not already
/// zero. This bug-fix was necessary in order to force all the stats to show
/// up, that should show up, and applying the bug-fix makes the sausage stats
/// significantly less sparse.
inline double l(int32 a, int32 b, bool penalize = false) {
if (a == b) return 0.0;
else return (penalize ? 1.0 + delta() : 1.0);
}
/// returns r_q, in one-based indexing, as in the paper.
inline int32 r(int32 q) { return R_[q-1]; }
/// Figure 4 of the paper; called from AccStats (Fig. 5)
double EditDistance(int32 N, int32 Q,
Vector<double> &alpha,
Matrix<double> &alpha_dash,
Vector<double> &alpha_dash_arc);
/// Figure 5 of the paper. Outputs to gamma_ and L_.
void AccStats();
/// Removes epsilons (symbol 0) from a vector
static void RemoveEps(std::vector<int32> *vec);
// Ensures that between each word in "vec" and at the beginning and end, is
// epsilon (0). (But if no words in vec, just one epsilon)
static void NormalizeEps(std::vector<int32> *vec);
// delta() is a constant used in the algorithm, which penalizes
// the use of certain epsilon transitions in the edit-distance which would cause
// words not to show up in the accumulated edit-distance statistics.
// There has been a conceptual bug-fix versus the way it was presented in
// the paper: we now add delta only if the edit-distance was not already
// zero.
static inline BaseFloat delta() { return 1.0e-05; }
/// Function used to increment map.
static inline void AddToMap(int32 i, double d, std::map<int32, double> *gamma) {
if (d == 0) return;
std::pair<const int32, double> pr(i, d);
std::pair<std::map<int32, double>::iterator, bool> ret = gamma->insert(pr);
if (!ret.second) // not inserted, so add to contents.
ret.first->second += d;
}
struct Arc {
int32 word;
int32 start_node;
int32 end_node;
BaseFloat loglike;
};
MinimumBayesRiskOptions opts_;
/// Arcs in the topologically sorted acceptor form of the word-level lattice,
/// with one final-state. Contains (word-symbol, log-likelihood on arc ==
/// negated cost). Indexed from zero.
std::vector<Arc> arcs_;
/// For each node in the lattice, a list of arcs entering that node. Indexed
/// from 1 (first node == 1).
std::vector<std::vector<int32> > pre_;
std::vector<int32> state_times_; // time of each state in the word lattice,
// indexed from 1 (same index as into pre_)
std::vector<int32> R_; // current 1-best word sequence, normalized to have
// epsilons between each word and at the beginning and end. R in paper...
// caution: indexed from zero, not from 1 as in paper.
double L_; // current averaged edit-distance between lattice and R_.
// \hat{L} in paper.
std::vector<std::vector<std::pair<int32, BaseFloat> > > gamma_;
// The stats we accumulate; these are pairs of (posterior, word-id), and note
// that word-id may be epsilon. Caution: indexed from zero, not from 1 as in
// paper. We sort in reverse order on the second member (posterior), so more
// likely word is first.
std::vector<std::vector<std::pair<BaseFloat, BaseFloat> > > times_;
// The average start and end times for words in each confusion-network bin.
// This is like an average over arcs, of the tau_b and tau_e quantities in
// Appendix C of the paper. Indexed from zero, like gamma_ and R_.
std::vector<std::pair<BaseFloat, BaseFloat> > sausage_times_;
// The average start and end times for each confusion-network bin. This
// is like an average over words, of the tau_b and tau_e quantities in
// Appendix C of the paper. Indexed from zero, like gamma_ and R_.
std::vector<std::pair<BaseFloat, BaseFloat> > one_best_times_;
// The average start and end times for words in the one best output. This
// is like an average over the arcs, of the tau_b and tau_e quantities in
// Appendix C of the paper. Indexed from zero, like gamma_ and R_.
std::vector<BaseFloat> one_best_confidences_;
// vector of confidences for the 1-best output (which could be
// the MAP output if opts_.decode_mbr == false, or the MBR output otherwise).
// Indexed by the same index as one_best_times_.
struct GammaCompare{
// should be like operator <. But we want reverse order
// on the 2nd element (posterior), so it'll be like operator
// > that looks first at the posterior.
bool operator () (const std::pair<int32, BaseFloat> &a,
const std::pair<int32, BaseFloat> &b) const {
if (a.second > b.second) return true;
else if (a.second < b.second) return false;
else return a.first > b.first;
}
};
};
} // namespace kaldi
#endif // KALDI_LAT_SAUSAGES_H_
@@ -0,0 +1,250 @@
// lat/word-align-lattice-lexicon-test.cc
// Copyright 2015 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 "lat/determinize-lattice-pruned.h"
#include "fstext/lattice-utils.h"
#include "fstext/fst-test-utils.h"
#include "lat/kaldi-lattice.h"
#include "lat/lattice-functions.h"
#include "lat/lattice-functions-transition-model.h"
#include "hmm/hmm-test-utils.h"
#include "lat/word-align-lattice-lexicon.h"
#include <random>
namespace kaldi {
// This function generates a lexicon in the same format that
// WordAlignLatticeLexicon uses: (original-word-id), (new-word-id), (phone-seq).
void GenerateLexicon(const std::vector<int32> &phones,
bool allow_zero_words,
bool allow_empty_word,
bool allow_multiple_prons,
std::vector<std::vector<int32> > *lexicon) {
KALDI_ASSERT(!phones.empty());
lexicon->clear();
int32 num_words = RandInt(1, 20);
for (int32 word = 1; word <= num_words; word++) {
int32 num_prons = RandInt(1, (allow_multiple_prons ? 2 : 1));
bool is_zero_word = allow_zero_words && (RandInt(1, 5) == 1);
for (int32 j = 0; j < num_prons; j++) {
// don't allow empty pron if this word isn't labeled in the lattice (zero word,
// like optional silence). This doesn't make sense.
int32 pron_length = RandInt(((allow_empty_word && !is_zero_word) ? 0 : 1),
4);
std::vector<int32> this_entry;
this_entry.push_back(is_zero_word ? 0 : word);
this_entry.push_back(word);
for (int32 p = 0; p < pron_length; p++)
this_entry.push_back(phones[RandInt(0, phones.size() - 1)]);
lexicon->push_back(this_entry);
}
}
SortAndUniq(lexicon);
// randomize the order.
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(lexicon->begin(), lexicon->end(), g);
for (size_t i = 0; i < lexicon->size(); i++) {
if ((*lexicon)[i].size() > 2) {
// ok, this lexicon has at least one nonempty word: potentially OK. Do
// further check that the info object doesn't complain.
try {
WordAlignLatticeLexiconInfo info(*lexicon);
return; // OK, we're satisfied with this lexicon.
} catch (...) {
break; // will re-try, see below.
}
}
}
// there were no nonempty words in the lexicon -> try again.
// recursing is the easiest way.
GenerateLexicon(phones, allow_zero_words, allow_empty_word, allow_multiple_prons,
lexicon);
}
static void PrintLexicon(const std::vector<std::vector<int32> > &lexicon) {
KALDI_LOG << "Lexicon is: ";
for (size_t i = 0; i < lexicon.size(); i++) {
KALDI_ASSERT(lexicon[i].size() >= 2);
const std::vector<int32> &entry = lexicon[i];
std::cerr << entry[0] << "\t" << entry[1] << "\t";
for (size_t j = 2; j < entry.size(); j++)
std::cerr << entry[j] << " ";
std::cerr << "\n";
}
}
static void PrintWordsAndPhones(const std::vector<int32> &words,
const std::vector<int32> &phones) {
std::ostringstream word_str, phone_str;
for (size_t i = 0; i < words.size(); i++)
word_str << words[i] << " ";
for (size_t i = 0; i < phones.size(); i++)
phone_str << phones[i] << " ";
KALDI_LOG << "Word-sequence is: " << word_str.str();
KALDI_LOG << "Phone-sequence is: " << phone_str.str();
}
// generates a phone and word sequence together from the lexicon. Not
// guaranteed nonempty.
void GenerateWordAndPhoneSequence(std::vector<std::vector<int32> > &lexicon,
std::vector<int32> *phone_seq,
std::vector<int32> *word_seq) {
int32 num_words = RandInt(0, 5);
phone_seq->clear();
word_seq->clear();
for (int32 i = 0; i < num_words; i++) {
const std::vector<int32> &lexicon_entry =
lexicon[RandInt(0, lexicon.size() - 1)];
// the zeroth element of 'lexicon_entry' is how it appears in
// the lattice prior to word alignment.
int32 word = lexicon_entry[0];
if (word != 0) word_seq->push_back(word);
// add everything from position 2 in the lexicon entry, to the
// phone sequence.
phone_seq->insert(phone_seq->end(),
lexicon_entry.begin() + 2,
lexicon_entry.end());
}
}
void GenerateCompactLatticeRandomly(const std::vector<int32> &alignment,
const std::vector<int32> &words,
CompactLattice *clat) {
clat->DeleteStates();
clat->AddState();
clat->SetStart(0);
int32 cur_state = 0;
size_t word_start = 0, alignment_start = 0,
num_words = words.size(), num_transition_ids = alignment.size();
for (; word_start < num_words; word_start++) {
int32 word = words[word_start];
int32 ali_length = RandInt(0, num_transition_ids - alignment_start);
std::vector<int32> this_ali(ali_length);
for (int32 i = 0; i < ali_length; i++)
this_ali[i] = alignment[alignment_start + i];
alignment_start += ali_length;
CompactLatticeWeight weight(LatticeWeight::One(), this_ali);
int32 ilabel = word;
int32 next_state = clat->AddState();
CompactLatticeArc arc(ilabel, ilabel, weight, next_state);
clat->AddArc(cur_state, arc);
cur_state = next_state;
}
if (alignment_start < alignment.size()) {
int32 ali_length = num_transition_ids - alignment_start;
std::vector<int32> this_ali(ali_length);
for (int32 i = 0; i < ali_length; i++)
this_ali[i] = alignment[alignment_start + i];
alignment_start += ali_length;
CompactLatticeWeight weight(LatticeWeight::One(), this_ali);
int32 ilabel = 0;
int32 next_state = clat->AddState();
CompactLatticeArc arc(ilabel, ilabel, weight, next_state);
clat->AddArc(cur_state, arc);
cur_state = next_state;
}
clat->SetFinal(cur_state, CompactLatticeWeight::One());
}
void TestWordAlignLatticeLexicon() {
ContextDependency *ctx_dep;
TransitionModel *trans_model = GenRandTransitionModel(&ctx_dep);
bool allow_zero_words = true;
bool allow_empty_word = true;
bool allow_multiple_prons = true;
const std::vector<int32> &phones = trans_model->GetPhones();
std::vector<std::vector<int32> > lexicon;
GenerateLexicon(phones, allow_zero_words, allow_empty_word,
allow_multiple_prons, &lexicon);
std::vector<int32> phone_seq;
std::vector<int32> word_seq;
while (phone_seq.empty())
GenerateWordAndPhoneSequence(lexicon, &phone_seq, &word_seq);
PrintLexicon(lexicon);
PrintWordsAndPhones(word_seq, phone_seq);
std::vector<int32> alignment;
bool reorder = (RandInt(0, 1) == 0);
GenerateRandomAlignment(*ctx_dep, *trans_model, reorder,
phone_seq, &alignment);
CompactLattice clat;
GenerateCompactLatticeRandomly(alignment, word_seq, &clat);
KALDI_LOG << "clat is ";
WriteCompactLattice(std::cerr, false, clat);
WordAlignLatticeLexiconOpts opts;
WordAlignLatticeLexiconInfo lexicon_info(lexicon);
opts.reorder = reorder;
CompactLattice aligned_clat;
bool allow_duplicate_paths = true;
bool ans = WordAlignLatticeLexicon(clat, *trans_model, lexicon_info, opts,
&aligned_clat);
if (ans) { // We only test if it succeeded.
if (!TestWordAlignedLattice(lexicon_info, *trans_model, clat, aligned_clat,
allow_duplicate_paths)) {
KALDI_WARN << "Lattice failed test (activated because --test=true). "
<< "Probable code error, please contact Kaldi maintainers.";
ans = false;
}
}
KALDI_LOG << "Aligned clat is ";
WriteCompactLattice(std::cerr, false, aligned_clat);
KALDI_ASSERT(ans);
Lattice lat;
ConvertLattice(clat, &lat);
int32 n = 1000; // a maximum.
Lattice nbest_lat;
std::vector<Lattice> nbest_lats;
fst::ShortestPath(lat, &nbest_lat, n);
fst::ConvertNbestToVector(nbest_lat, &nbest_lats);
KALDI_LOG << "Word-aligned lattice has " << nbest_lats.size() << " paths.";
delete ctx_dep;
delete trans_model;
}
} // end namespace kaldi
int main() {
for (int32 i = 0; i < 3; i++)
kaldi::TestWordAlignLatticeLexicon();
std::cout << "Tests succeeded\n";
}
@@ -0,0 +1,935 @@
// lat/word-align-lattice-lexicon.cc
// Copyright 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 "lat/phone-align-lattice.h"
#include "lat/word-align-lattice-lexicon.h"
#include "lat/lattice-functions.h"
#include "hmm/transition-model.h"
#include "hmm/hmm-utils.h"
#include "util/stl-utils.h"
namespace kaldi {
const int kTemporaryEpsilon = -2;
const int kNumStatesOffset = 1000; // relates to how we apply the
// max-states to the lattices; relates to the --max-expand option which
// stops this blowing up for pathological cases or in case of a mismatch.
class LatticeLexiconWordAligner {
public:
typedef CompactLatticeArc::StateId StateId;
typedef CompactLatticeArc::Label Label;
typedef WordAlignLatticeLexiconInfo::ViabilityMap ViabilityMap;
typedef WordAlignLatticeLexiconInfo::LexiconMap LexiconMap;
typedef WordAlignLatticeLexiconInfo::NumPhonesMap NumPhonesMap;
/*
The Freshness enum is applied to phone and word-sequences in the computation
state; it is related to the epsilon sequencing problem. If a phone or word
is new (added by the latest transition), it is fresh. We are only concerned
with the freshness of the left-most word (i.e. word index 0) in words_, and
the freshness of that can take only two values, kNotFresh or kFresh. As
regards the phones_ variable, the difference between kFresh and kAllFresh
is: if we just appended a phone it's kFresh, but if we just shifted off a
phone or phones by outputting a nonempty word it's kAllFresh, meaning that
all sub-sequences of the phone sequence are new. Note: if a phone or
word-sequence is empty the freshness of that sequence does not matter or is
not defined; we'll let it default to kNotFresh.
*/
typedef enum {
kNotFresh,
kFresh,
kAllFresh
} Freshness;
class ComputationState {
/// The state of the computation in which,
/// along a single path in the lattice, we work out the word
/// boundaries and output aligned arcs.
public:
/// Advance the computation state by adding the symbols and weights from
/// this arc. Outputs weight to "leftover_weight" and sets the weight to
/// 1.0 (this helps keep the state space small). Note: because we
/// previously did PhoneAlignLattice, we can assume this arc corresponds to
/// exactly one or zero phones.
void Advance(const CompactLatticeArc &arc,
const TransitionInformation &tmodel,
LatticeWeight *leftover_weight);
/// Returns true if, assuming we were to add one or more phones by calling
/// Advance one or more times on this, we might be able later to
/// successfully call TakeTransition. It's a kind of co-accessibility test
/// that avoids us creating an exponentially large number of states that
/// would contribute nothing to the final output.
bool ViableIfAdvanced(const ViabilityMap &viability_map) const;
int32 NumPhones() const { return phones_.size(); }
int32 NumWords() const { return words_.size(); }
int32 PendingWord() const { KALDI_ASSERT(!words_.empty()); return words_[0]; }
Freshness WordFreshness() const { return word_fresh_; }
Freshness PhoneFreshness() const { return phone_fresh_; }
/// This may be called at the end of a lattice, if it was forced
/// out. Note: we will only use "partial_word_label" if there are
/// phones without corresponding words; otherwise we'll use the
/// word label that was there.
void TakeForcedTransition(int32 partial_word_label,
ComputationState *next_state,
CompactLatticeArc *arc_out) const;
/// Take a transition, if possible; consume "num_phones" phones and (if
/// word_id != 0) the word "word_id" which must be the first word in words_.
/// Returns true if we could take the transition.
bool TakeTransition(const LexiconMap &lexicon_map,
int32 word_id,
int32 num_phones,
ComputationState *next_state,
CompactLatticeArc *arc_out) const;
bool IsEmpty() const { return (transition_ids_.empty() && words_.empty()); }
/// FinalWeight() will return "weight" if both transition_ids
/// and word_labels are empty, otherwise it will return
/// Weight::Zero().
LatticeWeight FinalWeight() const {
return (IsEmpty() ? weight_ : LatticeWeight::Zero());
}
size_t Hash() const {
VectorHasher<int32> vh;
const int32 p1 = 11117, p2 = 90647, p3 = 3967, p4 = 3557; // primes.
size_t ans = 0;
for (int32 i = 0; i < static_cast<int32>(transition_ids_.size()); i++) {
ans *= p1;
ans += vh(transition_ids_[i]);
}
ans += p2 * vh(words_)
+ static_cast<int32>(word_fresh_) * p3
+ static_cast<int32>(phone_fresh_) * p4;
// phones_ is determined by transition-id sequence so we don't
// need to include it in the hash.
return ans;
}
bool operator == (const ComputationState &other) const {
// phones_ is determined by transition-id sequence so don't
// need to compare it.
return (transition_ids_ == other.transition_ids_ &&
words_ == other.words_ &&
weight_ == other.weight_ &&
phone_fresh_ == other.phone_fresh_ &&
word_fresh_ == other.word_fresh_);
}
ComputationState(): phone_fresh_(kNotFresh), word_fresh_(kNotFresh),
weight_(LatticeWeight::One()) { } // initial state.
ComputationState(const ComputationState &other):
phones_(other.phones_), words_(other.words_),
phone_fresh_(other.phone_fresh_), word_fresh_(other.word_fresh_),
transition_ids_(other.transition_ids_), weight_(other.weight_) { }
private:
std::vector<int32> phones_; // sequence of pending phones
std::vector<int32> words_; // sequence of pending words.
// The following variables tell us whether the phones_ and/or words_
// variables were modified by the last operation on the computation state.
// This is used to make sure we don't have multiple ways of handling the
// same sequence, by taking transitions at multiple points (see code for
// more details). It's related to the epsilon sequencing problem.
// See the declaration above of the enum "Freshness".
Freshness phone_fresh_;
Freshness word_fresh_;
std::vector<std::vector<int32> > transition_ids_; // sequence of transition-ids for each phone..
LatticeWeight weight_; // contains two floats.
};
static void AppendVectors(
std::vector<std::vector<int32> >::const_iterator input_begin,
std::vector<std::vector<int32> >::const_iterator input_end,
std::vector<int32> *output);
struct Tuple {
Tuple(StateId input_state, ComputationState comp_state):
input_state(input_state), comp_state(comp_state) {}
Tuple() {}
StateId input_state;
ComputationState comp_state;
};
struct TupleHash {
size_t operator() (const Tuple &state) const {
return state.input_state + 102763 * state.comp_state.Hash();
// 102763 is just an arbitrary prime number
}
};
struct TupleEqual {
bool operator () (const Tuple &state1, const Tuple &state2) const {
// treat this like operator ==
return (state1.input_state == state2.input_state
&& state1.comp_state == state2.comp_state);
}
};
typedef unordered_map<Tuple, StateId, TupleHash, TupleEqual> MapType;
// This function may alter queue_.
StateId GetStateForTuple(const Tuple &tuple) {
MapType::iterator iter = map_.find(tuple);
if (iter == map_.end()) { // not in map.
StateId output_state = lat_out_->AddState();
map_[tuple] = output_state;
queue_.push_back(std::make_pair(tuple, output_state));
return output_state;
} else {
return iter->second;
}
}
// This function may alter queue_, via GetStateForTuple.
void ProcessTransition(StateId prev_output_state, // state-id of from-state in output lattice
const Tuple &next_tuple,
CompactLatticeArc *arc) { // arc to add (must first modify it by adding "nextstate")
arc->nextstate = GetStateForTuple(next_tuple); // adds it to queue_ if new.
lat_out_->AddArc(prev_output_state, *arc);
}
// Process any epsilon transitions out of this state. This refers to
// filler-words, such as silence, which have epsilon as the symbol in the
// original lattice, or no symbol at all (typically the original lattice
// will be determinized with epsilon-removal so there is no separate arc,
// just one or more extra phones that don't match up with any word.
void ProcessEpsilonTransitions(const Tuple &tuple, StateId output_state);
// Process any non-epsilon transitions out of this state in the output lattice.
void ProcessWordTransitions(const Tuple &tuple, StateId output_state);
// Take any transitions that correspond to advancing along arcs arc in the
// original FST.
void PossiblyAdvanceArc(const Tuple &tuple, StateId output_state);
/// Process all final-probs (normal case, no forcing-out).
/// returns true if we had at least one final-prob.
bool ProcessFinal();
/// This function returns true if the state "output_state" in the output
/// lattice has arcs out that have either a non-epsilon symbol or transition-ids
/// in the string of the weight.
bool HasNonEpsArcsOut(StateId output_state);
/// Creates arcs from all the tuples that were final in the original lattice
/// but have no arcs out of them in the output lattice that consume words or
/// phones-- does so by "forcing out" any words and phones there are pending
/// in the computation states. This function is only called if no states were
/// "naturally" final; this will only happen for lattices that were forced out
/// during decoding.
void ProcessFinalForceOut();
// Process all final-probs -- a wrapper function that handles the forced-out case.
void ProcessFinalWrapper() {
if (final_queue_.empty()) {
KALDI_WARN << "No final-probs to process.";
error_ = true;
return;
}
if (ProcessFinal()) return;
error_ = true;
KALDI_WARN << "Word-aligning lattice: lattice was forced out, will have partial words at end.";
ProcessFinalForceOut();
if (ProcessFinal()) return;
KALDI_WARN << "Word-aligning lattice: had no final-states even after forcing out "
<< "(result will be empty). This probably indicates wrong input.";
return;
}
void ProcessQueueElement() {
KALDI_ASSERT(!queue_.empty());
Tuple tuple = queue_.back().first;
StateId output_state = queue_.back().second;
queue_.pop_back();
ProcessEpsilonTransitions(tuple, output_state);
ProcessWordTransitions(tuple, output_state);
PossiblyAdvanceArc(tuple, output_state);
// Note: we'll do a bit more filtering in ProcessFinal(), meaning
// that we won't necessarily give a final-prob to all of the things
// that go onto final_queue_.
if (lat_in_.Final(tuple.input_state) != CompactLatticeWeight::Zero())
final_queue_.push_back(std::make_pair(tuple, output_state));
}
LatticeLexiconWordAligner(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordAlignLatticeLexiconInfo &lexicon_info,
int32 max_states,
int32 partial_word_label,
CompactLattice *lat_out):
lat_in_(lat), tmodel_(tmodel), lexicon_info_(lexicon_info),
max_states_(max_states),
lat_out_(lat_out),
partial_word_label_(partial_word_label == 0 ?
kTemporaryEpsilon : partial_word_label),
error_(false) {
// lat_in_ is after PhoneAlignLattice, it is not deterministic and contains epsilons
fst::CreateSuperFinal(&lat_in_); // Creates a super-final state, so the
// only final-probs are One(). Note: the member lat_in_ is not a reference.
}
// Removes epsilons; also removes unreachable states...
// not sure if these would exist if original was connected.
// This also replaces the temporary symbols for the silence
// and partial-words, with epsilons, if we wanted epsilons.
void RemoveEpsilonsFromLattice() {
Connect(lat_out_);
RmEpsilon(lat_out_, true); // true = connect.
std::vector<int32> syms_to_remove;
syms_to_remove.push_back(kTemporaryEpsilon);
RemoveSomeInputSymbols(syms_to_remove, lat_out_);
Project(lat_out_, fst::PROJECT_INPUT);
}
bool AlignLattice() {
lat_out_->DeleteStates();
if (lat_in_.Start() == fst::kNoStateId) {
KALDI_WARN << "Trying to word-align empty lattice.";
return false;
}
ComputationState initial_comp_state;
Tuple initial_tuple(lat_in_.Start(), initial_comp_state);
StateId start_state = GetStateForTuple(initial_tuple);
lat_out_->SetStart(start_state);
while (!queue_.empty()) {
if (max_states_ > 0 && lat_out_->NumStates() > max_states_) {
KALDI_WARN << "Number of states in lattice exceeded max-states of "
<< max_states_ << ", original lattice had "
<< lat_in_.NumStates() << " states. Returning empty lattice.";
lat_out_->DeleteStates();
return false;
}
ProcessQueueElement();
}
ProcessFinalWrapper();
RemoveEpsilonsFromLattice();
return !error_;
}
CompactLattice lat_in_;
const TransitionInformation &tmodel_;
const WordAlignLatticeLexiconInfo &lexicon_info_;
int32 max_states_;
CompactLattice *lat_out_;
std::vector<std::pair<Tuple, StateId> > queue_;
std::vector<std::pair<Tuple, StateId> > final_queue_; // as queue_, but
// just contains states that may have final-probs to process. We process these
// all at once, at the end.
MapType map_; // map from tuples to StateId.
int32 partial_word_label_;
bool error_;
};
// static
void LatticeLexiconWordAligner::AppendVectors(
std::vector<std::vector<int32> >::const_iterator input_begin,
std::vector<std::vector<int32> >::const_iterator input_end,
std::vector<int32> *output) {
size_t size = 0;
for (std::vector<std::vector<int32> >::const_iterator iter = input_begin;
iter != input_end;
++iter)
size += iter->size();
output->clear();
output->reserve(size);
for (std::vector<std::vector<int32> >::const_iterator iter = input_begin;
iter != input_end;
++iter)
output->insert(output->end(), iter->begin(), iter->end());
}
void LatticeLexiconWordAligner::ProcessEpsilonTransitions(
const Tuple &tuple, StateId output_state) {
const ComputationState &comp_state = tuple.comp_state;
StateId input_state = tuple.input_state;
StateId zero_word = 0;
NumPhonesMap::const_iterator iter =
lexicon_info_.num_phones_map_.find(zero_word);
if (iter == lexicon_info_.num_phones_map_.end()) {
return; // No epsilons to match; this can only happen if the lexicon
// we were provided had no lines with 0 as the first entry, i.e. no
// optional silences or the like.
}
// Now decide what range of phone-lengths we must process. This is all
// about only getting a single opportunity to process any given sequence of
// phones.
int32 min_num_phones, max_num_phones;
if (comp_state.PhoneFreshness() == kAllFresh) {
// All sub-sequences of the phone sequence are fresh because we just
// shifted some phones off, so we do this for all lengths. We can limit
// ourselves to the range of possible lengths for the epsilon symbol,
// in the lexicon.
min_num_phones = iter->second.first;
max_num_phones = std::min(iter->second.second, comp_state.NumPhones());
} else if (comp_state.PhoneFreshness() == kFresh) {
// only last phone is "fresh", so only consider the sequence of all
// phones including the last one.
int32 num_phones = comp_state.NumPhones();
if (num_phones >= iter->second.first &&
num_phones <= iter->second.second) {
min_num_phones = num_phones;
max_num_phones = num_phones;
} else {
return;
}
} else { // kNotFresh
return;
}
if (min_num_phones == 0)
KALDI_ERR << "Lexicon error: epsilon transition that produces no output:";
for (int32 num_phones = min_num_phones;
num_phones <= max_num_phones;
num_phones++) {
Tuple next_tuple;
next_tuple.input_state = input_state; // We're not taking a transition in the
// input FST so this stays the same.
CompactLatticeArc arc;
if (comp_state.TakeTransition(lexicon_info_.lexicon_map_,
zero_word,
num_phones,
&next_tuple.comp_state,
&arc)) {
ProcessTransition(output_state, next_tuple, &arc);
}
}
}
void LatticeLexiconWordAligner::ProcessWordTransitions(
const Tuple &tuple, StateId output_state) {
const ComputationState &comp_state = tuple.comp_state;
StateId input_state = tuple.input_state;
if (comp_state.NumWords() > 0) {
int32 min_num_phones, max_num_phones;
int32 word_id = comp_state.PendingWord();
if (comp_state.WordFreshness() == kFresh ||
comp_state.PhoneFreshness() == kAllFresh) {
// Just saw word, or shifted phones,
// so 1st opportunity to process phone-sequences of all possible sizes,
// with this word.
NumPhonesMap::const_iterator iter =
lexicon_info_.num_phones_map_.find(word_id);
if (iter == lexicon_info_.num_phones_map_.end()) {
KALDI_ERR << "Word " << word_id << " is not present in the lexicon.";
}
min_num_phones = iter->second.first;
max_num_phones = std::min(iter->second.second,
comp_state.NumPhones());
} else if (comp_state.PhoneFreshness() == kFresh) {
// just the latest phone is new -> just try to process the
// phone-sequence of all the phones we have.
min_num_phones = comp_state.NumPhones();
max_num_phones = min_num_phones;
} else {
return; // Nothing to do, since neither the word nor the phones are fresh.
}
for (int32 num_phones = min_num_phones;
num_phones <= max_num_phones;
num_phones++) {
Tuple next_tuple;
next_tuple.input_state = input_state; // We're not taking a transition in the
// input FST so this stays the same.
CompactLatticeArc arc;
if (comp_state.TakeTransition(lexicon_info_.lexicon_map_,
word_id,
num_phones,
&next_tuple.comp_state,
&arc)) {
ProcessTransition(output_state, next_tuple, &arc);
}
}
}
}
void LatticeLexiconWordAligner::PossiblyAdvanceArc(
const Tuple &tuple, StateId output_state) {
if (tuple.comp_state.ViableIfAdvanced(lexicon_info_.viability_map_)) {
for(fst::ArcIterator<CompactLattice> aiter(lat_in_, tuple.input_state);
!aiter.Done(); aiter.Next()) {
const CompactLatticeArc &arc_in = aiter.Value();
Tuple next_tuple(arc_in.nextstate, tuple.comp_state);
LatticeWeight arc_weight;
next_tuple.comp_state.Advance(arc_in, tmodel_, &arc_weight);
// Note: GetStateForTuple will add the tuple to the queue,
// if necessary.
StateId next_output_state = GetStateForTuple(next_tuple);
CompactLatticeArc arc_out(0, 0,
CompactLatticeWeight(arc_weight,
std::vector<int32>()),
next_output_state);
lat_out_->AddArc(output_state,
arc_out);
}
}
}
bool LatticeLexiconWordAligner::ProcessFinal() {
bool saw_final = false;
// Find final-states...
for (size_t i = 0; i < final_queue_.size(); i++) {
const Tuple &tuple = final_queue_[i].first;
StateId output_state = final_queue_[i].second;
KALDI_ASSERT(lat_in_.Final(tuple.input_state) == CompactLatticeWeight::One());
LatticeWeight final_weight = tuple.comp_state.FinalWeight();
if (final_weight != LatticeWeight::Zero()) {
// note: final_weight is only nonzero if there are no
// pending transition-ids, so there is no string component.
std::vector<int32> empty_vec;
lat_out_->SetFinal(output_state,
CompactLatticeWeight(final_weight, empty_vec));
saw_final = true;
}
}
return saw_final;
}
bool LatticeLexiconWordAligner::HasNonEpsArcsOut(StateId output_state) {
for (fst::ArcIterator<CompactLattice> aiter(*lat_out_, output_state);
!aiter.Done(); aiter.Next()) {
const CompactLatticeArc &arc = aiter.Value();
if (arc.ilabel != 0 || arc.olabel != 0 || !arc.weight.String().empty())
return true;
}
return false;
}
void LatticeLexiconWordAligner::ProcessFinalForceOut() {
KALDI_ASSERT(queue_.empty());
std::vector<std::pair<Tuple, StateId> > new_final_queue_;
new_final_queue_.reserve(final_queue_.size());
for (size_t i = 0; i < final_queue_.size();i++) { // note: all the states will
// be final in the orig. lattice
const Tuple &tuple = final_queue_[i].first;
StateId output_state = final_queue_[i].second;
if (!HasNonEpsArcsOut(output_state)) { // This if-statement
// avoids forcing things out too early, when they had words
// that could naturally have been put out. [without it,
// we'd have multiple alternate paths at the end.]
CompactLatticeArc arc;
Tuple next_tuple;
next_tuple.input_state = tuple.input_state;
tuple.comp_state.TakeForcedTransition(partial_word_label_,
&next_tuple.comp_state,
&arc);
// Note: the following call may add to queue_, but we'll clear it,
// we don't want to process these states.
StateId new_state = GetStateForTuple(next_tuple);
arc.nextstate = new_state;
lat_out_->AddArc(output_state, arc);
new_final_queue_.push_back(std::make_pair(next_tuple, new_state));
}
}
queue_.clear();
std::swap(final_queue_, new_final_queue_);
}
void LatticeLexiconWordAligner::ComputationState::Advance(
const CompactLatticeArc &arc, const TransitionInformation &tmodel,
LatticeWeight *weight) {
const std::vector<int32> &tids = arc.weight.String();
int32 phone;
if (tids.empty()) phone = 0;
else {
phone = tmodel.TransitionIdToPhone(tids.front());
KALDI_ASSERT(phone == tmodel.TransitionIdToPhone(tids.back()) &&
"Error: lattice is not phone-aligned.");
}
if (arc.ilabel != 0) { // note: arc.ilabel==arc.olabel (acceptor)
words_.push_back(arc.ilabel);
// Note: the word freshness only applies to the word in position 0,
// so only if the word-sequence is now of size 1, is it fresh.
if (words_.size() == 1) word_fresh_ = kFresh;
else word_fresh_ = kNotFresh;
} else { // No word added -> word not fresh.
word_fresh_ = kNotFresh;
}
if (phone != 0) {
phones_.push_back(phone);
transition_ids_.push_back(tids);
phone_fresh_ = kFresh;
} else {
phone_fresh_ = kNotFresh;
}
*weight = Times(weight_, arc.weight.Weight()); // will go on arc in output lattice
weight_ = LatticeWeight::One();
}
bool LatticeLexiconWordAligner::ComputationState::ViableIfAdvanced(
const ViabilityMap &viability_map) const {
/* This will ideally to return true if and only if we can ever take
any kind of transition out of this state after "advancing" it by adding
words and/or phones. It's OK to return true in some cases where the
condition is false, though, if it's a pain to check, because the result
will just be doing extra work for nothing (those states won't be
co-accessible in the output).
*/
if (phones_.empty()) return true;
if (words_.empty()) return true;
else {
// neither phones_ or words_ is empty. Return true if a longer sequence
// than this phone sequence can have either zero (<eps>/epsilon) or the
// first element of words_, as an entry in the lexicon with that phone
// sequence.
ViabilityMap::const_iterator iter = viability_map.find(phones_);
if (iter == viability_map.end()) return false;
else {
const std::vector<int32> &this_set = iter->second; // sorted vector.
// Return true if either 0 or words_[0] is in the set. If 0 is
// in the set, it will be the 1st element of the vector, because it's
// the lowest element.
return (this_set.front() == 0 ||
std::binary_search(this_set.begin(), this_set.end(), words_[0]));
}
}
}
void LatticeLexiconWordAligner::ComputationState::TakeForcedTransition(
int32 partial_word_label,
ComputationState *next_state,
CompactLatticeArc *arc_out) const {
KALDI_ASSERT(!IsEmpty());
next_state->phones_.clear();
next_state->words_.clear();
next_state->transition_ids_.clear();
// neither of the following variables should matter, actually,
// they will never be inspected. So just set them to kFresh for consistency,
// so they end up at the same place in the tuple-map_.
next_state->word_fresh_ = kFresh;
next_state->phone_fresh_ = kFresh;
next_state->weight_ = LatticeWeight::One();
int32 word_id;
if (words_.size() >= 1) {
word_id = words_[0];
if (words_.size() > 1)
KALDI_WARN << "Word-aligning lattice: discarding extra word at end of lattice"
<< "(forced-out).";
} else {
word_id = partial_word_label;
}
KALDI_ASSERT(word_id != 0); // any zeros would have been replaced with
// 'temporary epsilon' = 2.
std::vector<int32> appended_transition_ids;
AppendVectors(transition_ids_.begin(),
transition_ids_.end(),
&appended_transition_ids);
arc_out->ilabel = word_id;
arc_out->olabel = word_id;
arc_out->weight = CompactLatticeWeight(weight_,
appended_transition_ids);
// arc_out->nextstate will be set by the calling code.
}
bool LatticeLexiconWordAligner::ComputationState::TakeTransition(
const LexiconMap &lexicon_map, int32 word_id, int32 num_phones,
ComputationState *next_state, CompactLatticeArc *arc_out) const {
KALDI_ASSERT(word_id == 0 || (!words_.empty() && word_id == words_[0]));
KALDI_ASSERT(num_phones <= static_cast<int32>(phones_.size()));
std::vector<int32> lexicon_key;
lexicon_key.reserve(1 + num_phones);
lexicon_key.push_back(word_id); // put 1st word in lexicon_key.
lexicon_key.insert(lexicon_key.end(),
phones_.begin(), phones_.begin() + num_phones);
LexiconMap::const_iterator iter = lexicon_map.find(lexicon_key);
if (iter == lexicon_map.end()) { // no such entry
return false;
} else { // Entry exists. We'll create an arc.
next_state->phones_.assign(phones_.begin() + num_phones, phones_.end());
next_state->words_.assign(words_.begin() + (word_id == 0 ? 0 : 1),
words_.end());
next_state->transition_ids_.assign(transition_ids_.begin() + num_phones,
transition_ids_.end());
next_state->word_fresh_ =
(word_id != 0 && !next_state->words_.empty()) ? kFresh : kNotFresh;
next_state->phone_fresh_ =
(next_state->phones_.empty() || num_phones == 0) ? kNotFresh : kAllFresh;
// this next thing is a bit hard to explain. If we just consumed a word with
// no phones, we treat the phones as fresh. The idea is that if we need to
// both consume a word with no phones and a phone with no words (e.g.
// an empty word and then silence), we need to have the phones marked
// as fresh in order for this to be possible.
if (num_phones == 0 && word_id != 0 && !next_state->phones_.empty())
next_state->phone_fresh_ = kAllFresh;
next_state->weight_ = LatticeWeight::One();
if (GetVerboseLevel() >= 5) {
std::ostringstream ostr;
for (size_t i = 0; i < num_phones; i++)
ostr << phones_[i] << " ";
KALDI_VLOG(5) << "Taking arc with word = " << word_id
<< " and phones = " << ostr.str()
<< ", output-word = " << iter->second
<< ", dest-state has num-words = " << next_state->words_.size()
<< " and num-phones = " << next_state->phones_.size();
}
// Set arc_out:
Label word_id = iter->second; // word_id will typically be
// the same as words_[0], i.e. the
// word we consumed.
KALDI_ASSERT(word_id != 0); // we replaced zeros with 'temporary epsilon' = -2.
std::vector<int32> appended_transition_ids;
AppendVectors(transition_ids_.begin(),
transition_ids_.begin() + num_phones,
&appended_transition_ids);
arc_out->ilabel = word_id;
arc_out->olabel = word_id;
arc_out->weight = CompactLatticeWeight(weight_,
appended_transition_ids);
// arc_out->nextstate will be set in the calling code.
return true;
}
}
void WordAlignLatticeLexiconInfo::UpdateViabilityMap(
const std::vector<int32> &lexicon_entry) {
int32 word = lexicon_entry[0]; // note: word may be zero.
int32 num_phones = static_cast<int32>(lexicon_entry.size()) - 2;
std::vector<int32> phones;
if (num_phones > 0)
phones.reserve(num_phones - 1);
// for each nonempty sequence of phones that is a strict prefix of the phones
// in the lexicon entry (i.e. lexicon_entry [2 ... ]), add the word to the set
// in viability_map_[phones].
for (int32 n = 0; n < num_phones - 1; n++) {
phones.push_back(lexicon_entry[n + 2]); // first phone is at position 2.
// n+1 is the length of the sequence of phones
viability_map_[phones].push_back(word);
}
}
void WordAlignLatticeLexiconInfo::FinalizeViabilityMap() {
for (ViabilityMap::iterator iter = viability_map_.begin();
iter != viability_map_.end();
++iter) {
std::vector<int32> &words = iter->second;
SortAndUniq(&words);
KALDI_ASSERT(words[0] >= 0 && "Error: negative labels in lexicon.");
}
}
/// Update the map from a vector (orig-word-symbol phone1 phone2 ... ) to the
/// new word-symbol. The new word-symbol must always be nonzero; we'll replace
/// it with kTemporaryEpsilon = -2, if it was zero.
void WordAlignLatticeLexiconInfo::UpdateLexiconMap(
const std::vector<int32> &lexicon_entry) {
KALDI_ASSERT(lexicon_entry.size() >= 2);
std::vector<int32> key;
key.reserve(lexicon_entry.size() - 1);
// add the original word:
key.push_back(lexicon_entry[0]);
// add the phones:
key.insert(key.end(), lexicon_entry.begin() + 2, lexicon_entry.end());
int32 new_word = lexicon_entry[1]; // This will typically be the same as
// the original word at lexicon_entry[0] but is allowed to differ.
if (new_word == 0) new_word = kTemporaryEpsilon; // replace 0's with -2;
// we'll revert the change at the end.
if (lexicon_map_.count(key) != 0) {
if (lexicon_map_[key] == new_word)
KALDI_WARN << "Duplicate entry in lexicon map for word " << lexicon_entry[0];
else
KALDI_ERR << "Duplicate entry in lexicon map for word " << lexicon_entry[0]
<< " with inconsistent to-word.";
}
lexicon_map_[key] = new_word;
if (lexicon_entry[0] != lexicon_entry[1]) {
// Add reverse lexicon entry, this time with no 0 -> -2 mapping.
key[0] = lexicon_entry[1];
// Note: we ignore the situation where there are conflicting
// entries in reverse_lexicon_map_, as we never actually inspect
// the contents so it won't matter.
reverse_lexicon_map_[key] = lexicon_entry[0];
}
}
void WordAlignLatticeLexiconInfo::UpdateNumPhonesMap(
const std::vector<int32> &lexicon_entry) {
int32 num_phones = static_cast<int32>(lexicon_entry.size()) - 2;
int32 word = lexicon_entry[0];
if (num_phones_map_.count(word) == 0)
num_phones_map_[word] = std::make_pair(num_phones, num_phones);
else {
std::pair<int32, int32> &pr = num_phones_map_[word];
pr.first = std::min(pr.first, num_phones); // update min-num-phones
pr.second = std::max(pr.second, num_phones); // update max-num-phones
if (pr.first == 0 && word == 0)
KALDI_ERR << "Zero word with empty pronunciation is not allowed.";
}
}
/// Entry contains new-word-id phone1 phone2 ...
/// equivalent to all but the 1st entry on a line of the input file.
bool WordAlignLatticeLexiconInfo::IsValidEntry(const std::vector<int32> &entry) const {
KALDI_ASSERT(!entry.empty());
LexiconMap::const_iterator iter = lexicon_map_.find(entry);
if (iter != lexicon_map_.end()) {
int32 tgt_word = (iter->second == kTemporaryEpsilon ? 0 : iter->second);
if (tgt_word == entry[0]) return true; // symmetric entry.
// this means that that there would be an output-word with this
// value, and this sequence of phones.
}
// For entries that were not symmetric:
return (reverse_lexicon_map_.count(entry) != 0);
}
int32 WordAlignLatticeLexiconInfo::EquivalenceClassOf(int32 word) const {
unordered_map<int32, int32>::const_iterator iter =
equivalence_map_.find(word);
if (iter == equivalence_map_.end()) return word; // not in map.
else return iter->second;
}
void WordAlignLatticeLexiconInfo::UpdateEquivalenceMap(
const std::vector<std::vector<int32> > &lexicon) {
std::vector<std::pair<int32, int32> > equiv_pairs; // pairs of
// (lower,higher) words that are equivalent.
for (size_t i = 0; i < lexicon.size(); i++) {
KALDI_ASSERT(lexicon[i].size() >= 2);
int32 w1 = lexicon[i][0], w2 = lexicon[i][1];
if (w1 == w2) continue; // They are the same; this provides no information
// about equivalence, since any word is equivalent
// to itself.
if (w1 > w2) std::swap(w1, w2); // make sure w1 < w2.
equiv_pairs.push_back(std::make_pair(w1, w2));
}
SortAndUniq(&equiv_pairs);
equivalence_map_.clear();
for (size_t i = 0; i < equiv_pairs.size(); i++) {
int32 w1 = equiv_pairs[i].first, w2 = equiv_pairs[i].second,
w1dash = EquivalenceClassOf(w1);
equivalence_map_[w2] = w1dash;
}
}
WordAlignLatticeLexiconInfo::WordAlignLatticeLexiconInfo(
const std::vector<std::vector<int32> > &lexicon) {
for (size_t i = 0; i < lexicon.size(); i++) {
const std::vector<int32> &lexicon_entry = lexicon[i];
KALDI_ASSERT(lexicon_entry.size() >= 2);
UpdateViabilityMap(lexicon_entry);
UpdateLexiconMap(lexicon_entry);
UpdateNumPhonesMap(lexicon_entry);
}
FinalizeViabilityMap();
UpdateEquivalenceMap(lexicon);
}
// This is the wrapper function for users to call.
bool WordAlignLatticeLexicon(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordAlignLatticeLexiconInfo &lexicon_info,
const WordAlignLatticeLexiconOpts &opts,
CompactLattice *lat_out) {
PhoneAlignLatticeOptions phone_align_opts;
phone_align_opts.reorder = opts.reorder;
phone_align_opts.replace_output_symbols = false;
phone_align_opts.remove_epsilon = false;
// Input Lattice should be deterministic and w/o epsilons.
bool test = true;
uint64 props = lat.Properties(fst::kIDeterministic|fst::kIEpsilons, test);
if (props != fst::kIDeterministic) {
KALDI_WARN << "[Lattice has input epsilons and/or is not input-deterministic "
<< "(in Mohri sense)]-- i.e. lattice is not deterministic. "
<< "Word-alignment may be slow and-or blow up in memory.";
}
CompactLattice phone_aligned_lat;
bool ans = PhoneAlignLattice(lat, tmodel, phone_align_opts,
&phone_aligned_lat);
// 'phone_aligned_lat' is no longer deterministic and contains epsilons.
int32 max_states;
if (opts.max_expand <= 0) {
max_states = -1;
} else {
// The 1000 is a fixed offset to give it more wiggle room for very
// small inputs.
max_states = kNumStatesOffset + opts.max_expand * phone_aligned_lat.NumStates();
}
// If ans == false, we hope this is due to a forced-out lattice, and we try to
// continue.
LatticeLexiconWordAligner aligner(phone_aligned_lat, tmodel, lexicon_info,
max_states, opts.partial_word_label, lat_out);
// We'll let the calling code warn if this is false; it will know the utterance-id.
ans = aligner.AlignLattice() && ans;
return ans;
}
bool ReadLexiconForWordAlign (std::istream &is,
std::vector<std::vector<int32> > *lexicon) {
lexicon->clear();
std::string line;
while (std::getline(is, line)) {
std::vector<int32> this_entry;
if (!SplitStringToIntegers(line, " \t\r", false, &this_entry) ||
this_entry.size() < 2) {
KALDI_WARN << "Lexicon line '" << line << "' is invalid";
return false;
}
lexicon->push_back(this_entry);
}
return (!lexicon->empty());
}
} // namespace kaldi
@@ -0,0 +1,160 @@
// lat/word-align-lattice-lexicon.h
// Copyright 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.
#ifndef KALDI_LAT_WORD_ALIGN_LATTICE_LEXICON_H_
#define KALDI_LAT_WORD_ALIGN_LATTICE_LEXICON_H_
#include <fst/fstlib.h>
#include <fst/fst-decl.h>
#include "base/kaldi-common.h"
#include "util/common-utils.h"
#include "fstext/fstext-lib.h"
#include "hmm/transition-model.h"
#include "lat/kaldi-lattice.h"
namespace kaldi {
/** Read the lexicon in the special format required for word alignment. Each line has
a series of integers on it (at least two on each line), representing:
<old-word-id> <new-word-id> [<phone-id-1> [<phone-id-2> ... ] ]
Here, <old-word-id> is the word-id that appears in the lattice before alignment, and
<new-word-id> is the word-is that should appear in the lattice after alignment. This
is mainly useful when the lattice may have no symbol for the optional-silence arcs
(so <old-word-id> would equal zero), but we want it to be output with a symbol on those
arcs (so <new-word-id> would be nonzero).
If the silence should not be added to the lattice, both <old-word-id> and <new-word-id>
may be zero.
This function is very simple: it just reads in a series of lines from a text file,
each with at least two integers on them.
*/
bool ReadLexiconForWordAlign (std::istream &is,
std::vector<std::vector<int32> > *lexicon);
/// This class extracts some information from the lexicon and stores it
/// in a suitable form for the word-alignment code to use.
class WordAlignLatticeLexiconInfo {
public:
WordAlignLatticeLexiconInfo(const std::vector<std::vector<int32> > &lexicon);
/// Returns true if this lexicon-entry can appear, intepreted as
/// (output-word phone1 phone2 ...). This is just used in testing code.
bool IsValidEntry(const std::vector<int32> &entry) const;
/// Purely for the testing code, we map words into equivalence classes derived
/// from the mappings in the first two fields of each line in the lexicon. This
/// function maps from each word-id to the lowest member of its equivalence class.
int32 EquivalenceClassOf(int32 word) const;
protected:
friend class LatticeLexiconWordAligner;
void UpdateViabilityMap(const std::vector<int32> &lexicon_entry);
void UpdateLexiconMap(const std::vector<int32> &lexicon_entry);
void UpdateNumPhonesMap(const std::vector<int32> &lexicon_entry);
void UpdateEquivalenceMap(const std::vector<std::vector<int32> > &lexicon);
void FinalizeViabilityMap(); // sorts the vectors.
/// The type ViabilityMap maps from sequences of phones (excluding the empty
/// sequence), to the sets of all word-labels [on the input lattice] that
/// could correspond to phone sequences that start with s [but are longer than
/// s]. The sets of word-labels are represented as sorted vectors of int32
/// Note: the zero word-label is included here. This is used in a kind
/// of co-accessibility test, to see whether it is worth extending this state
/// by traversing arcs in the input lattice.
typedef unordered_map<std::vector<int32>,
std::vector<int32>,
VectorHasher<int32> > ViabilityMap;
/// This is a map from a vector (orig-word-symbol phone1 phone2 ... ) to
/// the new word-symbol. [todo: make sure the new word-symbol is always nonzero.]
typedef unordered_map<std::vector<int32>, int32,
VectorHasher<int32> > LexiconMap;
/// This is a map from the word-id (as present in the original lattice)
/// to the minimum and maximum #phones of lexicon entries for that word.
/// It helps improve efficiency.
typedef unordered_map<int32, std::pair<int32, int32> > NumPhonesMap;
/// This is used only in testing code; it defines a mapping from a word
/// to the primary member of that word's equivalence-class.
typedef unordered_map<int32, int32> EquivalenceMap;
// The following three variables represent various types of information
// gathered from the lexicon.
LexiconMap lexicon_map_;
NumPhonesMap num_phones_map_;
ViabilityMap viability_map_;
// As lexicon_map but in reverse sense w.r.t. words [we only
// do this for asymmetric entries.] Used only in testing code.
LexiconMap reverse_lexicon_map_;
// This is used only in testing code; it defines a mapping from a word
// to the primary member of that word's equivalence-class. If an index
// is not present in the map, it's assumed to map to itself.
EquivalenceMap equivalence_map_;
};
struct WordAlignLatticeLexiconOpts {
int32 partial_word_label;
bool reorder;
BaseFloat max_expand;
WordAlignLatticeLexiconOpts(): partial_word_label(0), reorder(true),
max_expand(-1.0) { }
void Register(OptionsItf *opts) {
opts->Register("partial-word-label", &partial_word_label, "Numeric id of "
"word symbol that is to be used for arcs in the word-aligned "
"lattice corresponding to partial words at the end of "
"\"forced-out\" utterances (zero is OK)");
opts->Register("reorder", &reorder, "True if the lattices were generated "
"from graphs that had the --reorder option true, relating to "
"reordering self-loops (typically true)");
opts->Register("max-expand", &max_expand, "If >0.0, the maximum ratio "
"by which we allow the lattice-alignment code to increase the #states "
"in a lattice (vs. the phone-aligned lattice) before we fail and "
"refuse to align the lattice. This is helpful in order to "
"prevent 'pathological' lattices from causing the program to "
"exhaust memory. Actual max-states is 1000 + max-expand * "
"orig-num-states.");
}
};
/// Align lattice so that each arc has the transition-ids on it
/// that correspond to the word that is on that arc. [May also have
/// epsilon arcs for optional silences.]
/// Returns true if everything was OK, false if there was any kind of
/// error including when the the lattice seems to have been "forced out"
/// (did not reach end state, resulting in partial word at end).
bool WordAlignLatticeLexicon(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordAlignLatticeLexiconInfo &lexicon_info,
const WordAlignLatticeLexiconOpts &opts,
CompactLattice *lat_out);
} // namespace kaldi
#endif
@@ -0,0 +1,927 @@
// lat/word-align-lattice.cc
// Copyright 2011-2012 Microsoft Corporation 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 "lat/word-align-lattice.h"
#include "util/stl-utils.h"
namespace kaldi {
class LatticeWordAligner {
public:
typedef CompactLatticeArc::StateId StateId;
typedef CompactLatticeArc::Label Label;
class ComputationState { /// The state of the computation in which,
/// along a single path in the lattice, we work out the word
/// boundaries and output aligned arcs.
public:
/// Advance the computation state by adding the symbols and weights
/// from this arc. We'll put the weight on the output arc; this helps
/// keep the state-space smaller.
void Advance(const CompactLatticeArc &arc, LatticeWeight *weight) {
const std::vector<int32> &string = arc.weight.String();
transition_ids_.insert(transition_ids_.end(),
string.begin(), string.end());
if (arc.ilabel != 0) // note: arc.ilabel==arc.olabel (acceptor)
word_labels_.push_back(arc.ilabel);
*weight = Times(weight_, arc.weight.Weight());
weight_ = LatticeWeight::One();
}
/// If it can output a whole word, it will do so, will put it in arc_out,
/// and return true; else it will return false. If it detects an error
/// condition and *error = false, it will set *error to true and print
/// a warning. In this case it may or may not [output an arc and return true],
/// depending on what we think is most likely the right thing to do. Of
/// course once *error is set, something has gone wrong so don't trust
/// the output too fully.
/// Note: the "next_state" of the arc will not be set, you have to do that
/// yourself.
bool OutputArc(const WordBoundaryInfo &info,
const TransitionInformation &tmodel,
CompactLatticeArc *arc_out,
bool *error) {
// order of this ||-expression doesn't matter for
// function behavior, only for efficiency, since the
// cases are disjoint.
return OutputNormalWordArc(info, tmodel, arc_out, error) ||
OutputSilenceArc(info, tmodel, arc_out, error) ||
OutputOnePhoneWordArc(info, tmodel, arc_out, error);
}
bool OutputSilenceArc(const WordBoundaryInfo &info,
const TransitionInformation &tmodel,
CompactLatticeArc *arc_out,
bool *error);
bool OutputOnePhoneWordArc(const WordBoundaryInfo &info,
const TransitionInformation &tmodel,
CompactLatticeArc *arc_out,
bool *error);
bool OutputNormalWordArc(const WordBoundaryInfo &info,
const TransitionInformation &tmodel,
CompactLatticeArc *arc_out,
bool *error);
bool IsEmpty() { return (transition_ids_.empty() && word_labels_.empty()); }
/// FinalWeight() will return "weight" if both transition_ids
/// and word_labels are empty, otherwise it will return
/// Weight::Zero().
LatticeWeight FinalWeight() { return (IsEmpty() ? weight_ : LatticeWeight::Zero()); }
/// This function may be called when you reach the end of
/// the lattice and this structure hasn't voluntarily
/// output words using "OutputArc". If IsEmpty() == false,
/// then you can call this function and it will output
/// an arc. The only
/// non-error state in which this happens, is when a word
/// (or silence) has ended, but we don't know that it's
/// ended because we haven't seen the first transition-id
/// from the next word. Otherwise (error state), the output
/// will consist of partial words, and this will only
/// happen for lattices that were somehow broken, i.e.
/// had not reached the final state.
void OutputArcForce(const WordBoundaryInfo &info,
const TransitionInformation &tmodel,
CompactLatticeArc *arc_out,
bool *error);
size_t Hash() const {
VectorHasher<int32> vh;
return vh(transition_ids_) + 90647 * vh(word_labels_);
// 90647 is an arbitrary largish prime number.
// We don't bother including the weight in the hash--
// we don't really expect duplicates with the same vectors
// but different weights, and anyway, this is only an
// efficiency issue.
}
// Just need an arbitrary complete order.
bool operator == (const ComputationState &other) const {
return (transition_ids_ == other.transition_ids_
&& word_labels_ == other.word_labels_
&& weight_ == other.weight_);
}
ComputationState(): weight_(LatticeWeight::One()) { } // initial state.
ComputationState(const ComputationState &other):
transition_ids_(other.transition_ids_), word_labels_(other.word_labels_),
weight_(other.weight_) { }
private:
std::vector<int32> transition_ids_;
std::vector<int32> word_labels_;
LatticeWeight weight_; // contains two floats.
};
struct Tuple {
Tuple(StateId input_state, ComputationState comp_state):
input_state(input_state), comp_state(comp_state) {}
StateId input_state;
ComputationState comp_state;
};
struct TupleHash {
size_t operator() (const Tuple &state) const {
return state.input_state + 102763 * state.comp_state.Hash();
// 102763 is just an arbitrary prime number
}
};
struct TupleEqual {
bool operator () (const Tuple &state1, const Tuple &state2) const {
// treat this like operator ==
return (state1.input_state == state2.input_state
&& state1.comp_state == state2.comp_state);
}
};
typedef unordered_map<Tuple, StateId, TupleHash, TupleEqual> MapType;
StateId GetStateForTuple(const Tuple &tuple, bool add_to_queue) {
MapType::iterator iter = map_.find(tuple);
if (iter == map_.end()) { // not in map.
StateId output_state = lat_out_->AddState();
map_[tuple] = output_state;
if (add_to_queue)
queue_.push_back(std::make_pair(tuple, output_state));
return output_state;
} else {
return iter->second;
}
}
void ProcessFinal(Tuple tuple, StateId output_state) {
// ProcessFinal is only called if the input_state has
// final-prob of One(). [else it should be zero. This
// is because we called CreateSuperFinal().]
if (tuple.comp_state.IsEmpty()) { // computation state doesn't have
// anything pending.
std::vector<int32> empty_vec;
CompactLatticeWeight cw(tuple.comp_state.FinalWeight(), empty_vec);
lat_out_->SetFinal(output_state, Plus(lat_out_->Final(output_state), cw));
} else {
// computation state has something pending, i.e. input or
// output symbols that need to be flushed out. Note: OutputArc() would
// have returned false or we wouldn't have been called, so we have to
// force it out.
CompactLatticeArc lat_arc;
tuple.comp_state.OutputArcForce(info_, tmodel_, &lat_arc, &error_);
// True in the next line means add it to the queue.
lat_arc.nextstate = GetStateForTuple(tuple, true);
// The final-prob stuff will get called again from ProcessQueueElement().
// Note: because we did CreateSuperFinal(), this final-state on the input
// lattice will have no output arcs (and unit final-prob), so there will be
// no complications with processing the arcs from this state (there won't
// be any).
KALDI_ASSERT(output_state != lat_arc.nextstate);
lat_out_->AddArc(output_state, lat_arc);
}
}
void ProcessQueueElement() {
KALDI_ASSERT(!queue_.empty());
Tuple tuple = queue_.back().first;
StateId output_state = queue_.back().second;
queue_.pop_back();
// First thing is-- we see whether the computation-state has something
// pending that it wants to output. In this case we don't do
// anything further. This is a chosen behavior similar to the
// epsilon-sequencing rules encoded by the filters in
// composition.
CompactLatticeArc lat_arc;
if (tuple.comp_state.OutputArc(info_, tmodel_, &lat_arc, &error_)) {
// note: this function changes the tuple (when it returns true).
lat_arc.nextstate = GetStateForTuple(tuple, true); // true == add to queue,
// if not already present.
KALDI_ASSERT(output_state != lat_arc.nextstate);
lat_out_->AddArc(output_state, lat_arc);
} else {
// when there's nothing to output, we'll process arcs from the input-state.
// note: it would in a sense be valid to do both (i.e. process the stuff
// above, and also these), but this is a bit like the epsilon-sequencing
// stuff in composition: we avoid duplicate arcs by doing it this way.
if (lat_.Final(tuple.input_state) != CompactLatticeWeight::Zero()) {
KALDI_ASSERT(lat_.Final(tuple.input_state) == CompactLatticeWeight::One());
// ... since we did CreateSuperFinal.
ProcessFinal(tuple, output_state);
}
// Now process the arcs. Note: final-state shouldn't have any arcs.
for (fst::ArcIterator<CompactLattice> aiter(lat_, tuple.input_state);
!aiter.Done(); aiter.Next()) {
const CompactLatticeArc &arc = aiter.Value();
Tuple next_tuple(tuple);
LatticeWeight weight;
next_tuple.comp_state.Advance(arc, &weight);
next_tuple.input_state = arc.nextstate;
StateId next_output_state = GetStateForTuple(next_tuple, true); // true == add to queue,
// if not already present.
// We add an epsilon arc here (as the input and output happens
// separately)... the epsilons will get removed later.
KALDI_ASSERT(next_output_state != output_state);
lat_out_->AddArc(output_state,
CompactLatticeArc(0, 0,
CompactLatticeWeight(weight, std::vector<int32>()),
next_output_state));
}
}
}
LatticeWordAligner(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordBoundaryInfo &info,
int32 max_states,
CompactLattice *lat_out):
lat_(lat), tmodel_(tmodel), info_in_(info), info_(info),
max_states_(max_states), lat_out_(lat_out),
error_(false) {
bool test = true;
uint64 props = lat_.Properties(fst::kIDeterministic|fst::kIEpsilons, test);
if (props != fst::kIDeterministic) {
KALDI_WARN << "[Lattice has input epsilons and/or is not input-deterministic "
<< "(in Mohri sense)]-- i.e. lattice is not deterministic. "
<< "Word-alignment may be slow and-or blow up in memory.";
}
fst::CreateSuperFinal(&lat_); // Creates a super-final state, so the
// only final-probs are One().
// Inside this class, we don't want to use zero for the silence
// or partial-word labels, as this will interfere with the RmEpsilon
// stage, where we don't want the arcs corresponding to silence or
// partial words to be removed-- only the arcs with nothing at all
// on them.
if (info_.partial_word_label == 0 || info_.silence_label == 0) {
int32 unused_label = 1 + HighestNumberedOutputSymbol(lat);
if (info_.partial_word_label >= unused_label)
unused_label = info_.partial_word_label + 1;
if (info_.silence_label >= unused_label)
unused_label = info_.silence_label + 1;
KALDI_ASSERT(unused_label > 0);
if (info_.partial_word_label == 0)
info_.partial_word_label = unused_label++;
if (info_.silence_label == 0)
info_.silence_label = unused_label;
}
}
// Removes epsilons; also removes unreachable states...
// not sure if these would exist if original was connected.
// This also replaces the temporary symbols for the silence
// and partial-words, with epsilons, if we wanted epsilons.
void RemoveEpsilonsFromLattice() {
// Remove epsilon arcs from output lattice.
RmEpsilon(lat_out_, true); // true = connect.
std::vector<int32> syms_to_remove;
if (info_in_.partial_word_label == 0)
syms_to_remove.push_back(info_.partial_word_label);
if (info_in_.silence_label == 0)
syms_to_remove.push_back(info_.silence_label);
if (!syms_to_remove.empty()) {
RemoveSomeInputSymbols(syms_to_remove, lat_out_);
Project(lat_out_, fst::PROJECT_INPUT);
}
}
bool AlignLattice() {
lat_out_->DeleteStates();
if (lat_.Start() == fst::kNoStateId) {
KALDI_WARN << "Trying to word-align empty lattice.";
return false;
}
ComputationState initial_comp_state;
Tuple initial_tuple(lat_.Start(), initial_comp_state);
StateId start_state = GetStateForTuple(initial_tuple, true); // True = add this to queue.
lat_out_->SetStart(start_state);
while (!queue_.empty()) {
if (max_states_ > 0 && lat_out_->NumStates() > max_states_) {
KALDI_WARN << "Number of states in lattice exceeded max-states of "
<< max_states_ << ", original lattice had "
<< lat_.NumStates() << " states. Returning what we have.";
RemoveEpsilonsFromLattice();
return false;
}
ProcessQueueElement();
}
RemoveEpsilonsFromLattice();
return !error_;
}
CompactLattice lat_;
const TransitionInformation &tmodel_;
const WordBoundaryInfo &info_in_;
WordBoundaryInfo info_;
int32 max_states_;
CompactLattice *lat_out_;
std::vector<std::pair<Tuple, StateId> > queue_;
MapType map_; // map from tuples to StateId.
bool error_;
};
bool LatticeWordAligner::ComputationState::OutputSilenceArc(
const WordBoundaryInfo &info, const TransitionInformation &tmodel,
CompactLatticeArc *arc_out, bool *error) {
if (transition_ids_.empty()) return false;
int32 phone = tmodel.TransitionIdToPhone(transition_ids_[0]);
if (info.TypeOfPhone(phone) != WordBoundaryInfo::kNonWordPhone) return false;
// we assume the start of transition_ids_ is the start of the phone [silence];
// this is a precondition.
size_t len = transition_ids_.size(), i;
// Keep going till we reach a "final" transition-id; note, if
// reorder==true, we have to go a bit further after this.
for (i = 0; i < len; i++) {
int32 tid = transition_ids_[i];
int32 this_phone = tmodel.TransitionIdToPhone(tid);
if (this_phone != phone && ! *error) { // error condition: should have reached final transition-id first.
*error = true;
KALDI_WARN << "Phone changed before final transition-id found "
"[broken lattice or mismatched model or wrong --reorder option?]";
}
if (tmodel.IsFinal(tid))
break;
}
if (i == len) return false; // fell off loop.
i++; // go past the one for which IsFinal returned true.
if (info.reorder) // we have to consume the following self-loop transition-ids.
while (i < len && tmodel.IsSelfLoop(transition_ids_[i])) i++;
if (i == len) return false; // we don't know if it ends here... so can't output arc.
if (tmodel.TransitionIdToPhone(transition_ids_[i-1]) != phone
&& ! *error) { // another check.
KALDI_WARN << "Phone changed unexpectedly in lattice "
"[broken lattice or mismatched model?]";
}
// interpret i as the number of transition-ids to consume.
std::vector<int32> tids_out(transition_ids_.begin(), transition_ids_.begin()+i);
// consumed transition ids from our internal state.
*arc_out = CompactLatticeArc(info.silence_label, info.silence_label,
CompactLatticeWeight(weight_, tids_out), fst::kNoStateId);
transition_ids_.erase(transition_ids_.begin(), transition_ids_.begin()+i); // delete these
weight_ = LatticeWeight::One(); // we just output the weight.
return true;
}
bool LatticeWordAligner::ComputationState::OutputOnePhoneWordArc(
const WordBoundaryInfo &info, const TransitionInformation &tmodel,
CompactLatticeArc *arc_out, bool *error) {
if (transition_ids_.empty()) return false;
if (word_labels_.empty()) return false;
int32 phone = tmodel.TransitionIdToPhone(transition_ids_[0]);
if (info.TypeOfPhone(phone) != WordBoundaryInfo::kWordBeginAndEndPhone)
return false;
// we assume the start of transition_ids_ is the start of the phone.
// this is a precondition.
size_t len = transition_ids_.size(), i;
for (i = 0; i < len; i++) {
int32 tid = transition_ids_[i];
int32 this_phone = tmodel.TransitionIdToPhone(tid);
if (this_phone != phone && ! *error) { // error condition: should have reached final transition-id first.
KALDI_WARN << "Phone changed before final transition-id found "
"[broken lattice or mismatched model or wrong --reorder option?]";
// just continue, ignoring this-- we'll probably output something...
}
if (tmodel.IsFinal(tid))
break;
}
if (i == len) return false; // fell off loop.
i++; // go past the one for which IsFinal returned true.
if (info.reorder) // we have to consume the following self-loop transition-ids.
while (i < len && tmodel.IsSelfLoop(transition_ids_[i])) i++;
if (i == len) return false; // we don't know if it ends here... so can't output arc.
if (tmodel.TransitionIdToPhone(transition_ids_[i-1]) != phone
&& ! *error) { // another check.
KALDI_WARN << "Phone changed unexpectedly in lattice "
"[broken lattice or mismatched model?]";
*error = true;
}
// interpret i as the number of transition-ids to consume.
std::vector<int32> tids_out(transition_ids_.begin(),
transition_ids_.begin() + i);
// consumed transition ids from our internal state.
int32 word = word_labels_[0];
*arc_out = CompactLatticeArc(word, word,
CompactLatticeWeight(weight_, tids_out), fst::kNoStateId);
transition_ids_.erase(transition_ids_.begin(),
transition_ids_.begin() + i); // delete these
// Remove the word that we just output.
word_labels_.erase(word_labels_.begin(), word_labels_.begin() + 1);
weight_ = LatticeWeight::One(); // we just output the weight.
return true;
}
/// This function tries to see if it can output a normal word arc--
/// one with at least two phones in it.
bool LatticeWordAligner::ComputationState::OutputNormalWordArc(
const WordBoundaryInfo &info, const TransitionInformation &tmodel,
CompactLatticeArc *arc_out, bool *error) {
if (transition_ids_.empty()) return false;
if (word_labels_.empty()) return false;
int32 begin_phone = tmodel.TransitionIdToPhone(transition_ids_[0]);
if (info.TypeOfPhone(begin_phone) != WordBoundaryInfo::kWordBeginPhone)
return false;
// we assume the start of transition_ids_ is the start of the phone.
// this is a precondition.
size_t len = transition_ids_.size(), i;
// Eat up the transition-ids of this word-begin phone until we get to the
// "final" transition-id. [there may be self-loops following this though,
// if reorder==true]
for (i = 0; i < len && !tmodel.IsFinal(transition_ids_[i]); i++);
if (i == len) return false;
i++; // Skip over this final-transition.
if (info.reorder) // Skip over any reordered self-loops for this final-transition
for (; i < len && tmodel.IsSelfLoop(transition_ids_[i]); i++);
if (i == len) return false;
if (tmodel.TransitionIdToPhone(transition_ids_[i-1]) != begin_phone
&& ! *error) { // another check.
KALDI_WARN << "Phone changed unexpectedly in lattice "
"[broken lattice or mismatched model?]";
*error = true;
}
// Now keep going till we hit a word-ending phone.
// Note: we don't expect anything except word-internal phones
// here, but we'll just print a warning if we get something
// else.
for (; i < len; i++) {
int32 this_phone = tmodel.TransitionIdToPhone(transition_ids_[i]);
if (info.TypeOfPhone(this_phone) == WordBoundaryInfo::kWordEndPhone)
break;
if (info.TypeOfPhone(this_phone) != WordBoundaryInfo::kWordInternalPhone
&& !*error) {
KALDI_WARN << "Unexpected phone " << this_phone
<< " found inside a word.";
*error = true;
}
}
if (i == len) return false;
// OK, we hit a word-ending phone. Continue till we get to
// a "final-transition".
// this variable just used for checks.
int32 final_phone = tmodel.TransitionIdToPhone(transition_ids_[i]);
for (; i < len; i++) {
int32 this_phone = tmodel.TransitionIdToPhone(transition_ids_[i]);
if (this_phone != final_phone && ! *error) {
*error = true;
KALDI_WARN << "Phone changed before final transition-id found "
"[broken lattice or mismatched model or wrong --reorder option?]";
}
if (tmodel.IsFinal(transition_ids_[i])) break;
}
if (i == len) return false;
i++;
// We got to the final-transition of the final phone;
// if reorder==true, continue eating up the self-loop.
if (info.reorder == true)
while (i < len && tmodel.IsSelfLoop(transition_ids_[i])) i++;
if (i == len) return false;
if (tmodel.TransitionIdToPhone(transition_ids_[i-1]) != final_phone
&& ! *error) {
*error = true;
KALDI_WARN << "Phone changed while following final self-loop "
"[broken lattice or mismatched model or wrong --reorder option?]";
}
// OK, we're ready to output the word.
// Interpret i as the number of transition-ids to consume.
std::vector<int32> tids_out(transition_ids_.begin(),
transition_ids_.begin() + i);
// consumed transition ids from our internal state.
int32 word = word_labels_[0];
*arc_out = CompactLatticeArc(word, word,
CompactLatticeWeight(weight_, tids_out),
fst::kNoStateId);
transition_ids_.erase(transition_ids_.begin(),
transition_ids_.begin() + i); // delete these
// Remove the word that we just output.
word_labels_.erase(word_labels_.begin(),
word_labels_.begin() + 1);
weight_ = LatticeWeight::One(); // we just output the weight.
return true;
}
// Returns true if this vector of transition-ids could be a valid
// word. Note: the checks are not 100% exhaustive.
static bool IsPlausibleWord(const WordBoundaryInfo &info,
const TransitionInformation &tmodel,
const std::vector<int32> &transition_ids) {
if (transition_ids.empty()) return false;
int32 first_phone = tmodel.TransitionIdToPhone(transition_ids.front()),
last_phone = tmodel.TransitionIdToPhone(transition_ids.back());
if ( (info.TypeOfPhone(first_phone) == WordBoundaryInfo::kWordBeginAndEndPhone
&& first_phone == last_phone)
||
(info.TypeOfPhone(first_phone) == WordBoundaryInfo::kWordBeginPhone &&
info.TypeOfPhone(last_phone) == WordBoundaryInfo::kWordEndPhone) ) {
if (! info.reorder) {
return (tmodel.IsFinal(transition_ids.back()));
} else {
int32 i = transition_ids.size() - 1;
while (i > 0 && tmodel.IsSelfLoop(transition_ids[i])) i--;
return tmodel.IsFinal(transition_ids[i]);
}
} else return false;
}
void LatticeWordAligner::ComputationState::OutputArcForce(
const WordBoundaryInfo &info, const TransitionInformation &tmodel,
CompactLatticeArc *arc_out, bool *error) {
KALDI_ASSERT(!IsEmpty());
if (!word_labels_.empty()
&& !transition_ids_.empty()) { // We have at least one word to
// output, and some transition-ids. We assume that the normal OutputArc was called
// and failed, so this means we didn't see the end of that
// word.
int32 word = word_labels_[0];
if (! *error && !IsPlausibleWord(info, tmodel, transition_ids_)) {
*error = true;
KALDI_WARN << "Invalid word at end of lattice [partial lattice, forced out?]";
}
CompactLatticeWeight cw(weight_, transition_ids_);
*arc_out = CompactLatticeArc(word, word, cw, fst::kNoStateId);
weight_ = LatticeWeight::One();
transition_ids_.clear();
word_labels_.erase(word_labels_.begin(), word_labels_.begin()+1);
} else if (!word_labels_.empty() && transition_ids_.empty()) {
// We won't create arcs with these word labels on, as most likely
// this will cause errors down the road. This is an error
// condition anyway, in some sense.
if (! *error) {
*error = true;
KALDI_WARN << "Discarding word-ids at the end of a sentence, "
"that don't have alignments.";
}
CompactLatticeWeight cw(weight_, transition_ids_);
// This creates an epsilon arc with a weight on it, but
// no transition-ids since the vector is empty.
// The word labels are discarded.
*arc_out = CompactLatticeArc(0, 0, cw, fst::kNoStateId);
weight_ = LatticeWeight::One();
word_labels_.clear();
} else if (!transition_ids_.empty() && word_labels_.empty()) {
// Transition-ids but no word label-- either silence or partial word.
int32 first_phone = tmodel.TransitionIdToPhone(transition_ids_[0]);
if (info.TypeOfPhone(first_phone) == WordBoundaryInfo::kNonWordPhone) {
// first phone is silence...
if (first_phone != tmodel.TransitionIdToPhone(transition_ids_.back())
&& ! *error) {
*error = true;
// Phone changed-- this is a code error, because the regular OutputArc
// should have output an arc (a silence arc) if that phone finished.
// So we make it fatal.
KALDI_ERR << "Broken silence arc at end of utterance (the phone "
"changed); code error";
}
if (!*error) { // Check that it ends at the end state of silence; error otherwise.
int32 i = transition_ids_.size() - 1;
if (info.reorder)
while (tmodel.IsSelfLoop(transition_ids_[i]) && i > 0)
i--;
if (!tmodel.IsFinal(transition_ids_[i])) {
*error = true;
KALDI_WARN << "Broken silence arc at end of utterance (does not "
"reach end of silence)";
}
}
CompactLatticeWeight cw(weight_, transition_ids_);
*arc_out = CompactLatticeArc(info.silence_label, info.silence_label,
cw, fst::kNoStateId);
} else {
// Not silence phone -- treat as partial word (with no word label).
// This is in itself an error condition, i.e. the lattice was maybe
// forced out.
if (! *error) {
*error = true;
KALDI_WARN << "Partial word detected at end of utterance";
}
CompactLatticeWeight cw(weight_, transition_ids_);
*arc_out = CompactLatticeArc(info.partial_word_label, info.partial_word_label,
cw, fst::kNoStateId);
}
transition_ids_.clear();
weight_ = LatticeWeight::One();
} else {
KALDI_ERR << "Code error, word-aligning lattice"; // this shouldn't
// be able to happen; we don't call this function of they're both empty.
}
}
// This code will eventually be removed.
void WordBoundaryInfo::SetOptions(const std::string int_list, PhoneType phone_type) {
KALDI_ASSERT(!int_list.empty() && phone_type != kNoPhone);
std::vector<int32> phone_list;
if (!kaldi::SplitStringToIntegers(int_list, ":",
false,
&phone_list)
|| phone_list.empty())
KALDI_ERR << "Invalid argument to --*-phones option: " << int_list;
for (size_t i= 0; i < phone_list.size(); i++) {
if (phone_to_type.size() <= phone_list[i])
phone_to_type.resize(phone_list[i]+1, kNoPhone);
if (phone_to_type[phone_list[i]] != kNoPhone)
KALDI_ERR << "Phone " << phone_list[i] << "was given two incompatible "
"assignments.";
phone_to_type[phone_list[i]] = phone_type;
}
}
// This initializer will be deleted eventually.
WordBoundaryInfo::WordBoundaryInfo(const WordBoundaryInfoOpts &opts) {
SetOptions(opts.wbegin_phones, kWordBeginPhone);
SetOptions(opts.wend_phones, kWordEndPhone);
SetOptions(opts.wbegin_and_end_phones, kWordBeginAndEndPhone);
SetOptions(opts.winternal_phones, kWordInternalPhone);
SetOptions(opts.silence_phones, (opts.silence_has_olabels ?
kWordBeginAndEndPhone : kNonWordPhone));
reorder = opts.reorder;
silence_label = opts.silence_label;
partial_word_label = opts.partial_word_label;
}
WordBoundaryInfo::WordBoundaryInfo(const WordBoundaryInfoNewOpts &opts) {
reorder = opts.reorder;
silence_label = opts.silence_label;
partial_word_label = opts.partial_word_label;
}
WordBoundaryInfo::WordBoundaryInfo(const WordBoundaryInfoNewOpts &opts,
std::string word_boundary_file) {
reorder = opts.reorder;
silence_label = opts.silence_label;
partial_word_label = opts.partial_word_label;
bool binary_in;
Input ki(word_boundary_file, &binary_in);
KALDI_ASSERT(!binary_in && "Not expecting binary word-boundary file.");
Init(ki.Stream());
}
void WordBoundaryInfo::Init(std::istream &stream) {
std::string line;
while (std::getline(stream, line)) {
std::vector<std::string> split_line;
SplitStringToVector(line, " \t\r", true, &split_line);// split the line by space or tab
int32 p = 0;
if (split_line.size() != 2 ||
!ConvertStringToInteger(split_line[0], &p))
KALDI_ERR << "Invalid line in word-boundary file: " << line;
KALDI_ASSERT(p > 0);
if (phone_to_type.size() <= static_cast<size_t>(p))
phone_to_type.resize(p+1, kNoPhone);
std::string t = split_line[1];
if (t == "nonword") phone_to_type[p] = kNonWordPhone;
else if (t == "begin") phone_to_type[p] = kWordBeginPhone;
else if (t == "singleton") phone_to_type[p] = kWordBeginAndEndPhone;
else if (t == "end") phone_to_type[p] = kWordEndPhone;
else if (t == "internal") phone_to_type[p] = kWordInternalPhone;
else
KALDI_ERR << "Invalid line in word-boundary file: " << line;
}
if (phone_to_type.empty())
KALDI_ERR << "Empty word-boundary file";
}
bool WordAlignLattice(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordBoundaryInfo &info,
int32 max_states,
CompactLattice *lat_out) {
LatticeWordAligner aligner(lat, tmodel, info, max_states, lat_out);
return aligner.AlignLattice();
}
class WordAlignedLatticeTester {
public:
WordAlignedLatticeTester(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordBoundaryInfo &info,
const CompactLattice &aligned_lat):
lat_(lat), tmodel_(tmodel), info_(info), aligned_lat_(aligned_lat) { }
void Test() {
// First test that each aligned arc is valid.
typedef CompactLattice::StateId StateId ;
for (StateId s = 0; s < aligned_lat_.NumStates(); s++) {
for (fst::ArcIterator<CompactLattice> iter(aligned_lat_, s);
!iter.Done();
iter.Next()) {
TestArc(iter.Value());
}
if (aligned_lat_.Final(s) != CompactLatticeWeight::Zero()) {
TestFinal(aligned_lat_.Final(s));
}
}
TestEquivalent();
}
private:
void TestArc(const CompactLatticeArc &arc) {
if (! (TestArcSilence(arc) || TestArcNormalWord(arc) || TestArcOnePhoneWord(arc)
|| TestArcEmpty(arc)))
KALDI_ERR << "Invalid arc in aligned CompactLattice: "
<< arc.ilabel << " " << arc.olabel << " " << arc.nextstate
<< " " << arc.weight;
}
bool TestArcEmpty(const CompactLatticeArc &arc) {
if (arc.ilabel != 0) return false; // Check there is no label. Note, ilabel==olabel.
const std::vector<int32> &tids = arc.weight.String();
return tids.empty();
}
bool TestArcSilence(const CompactLatticeArc &arc) {
// This only applies when silence doesn't have word labels.
if (arc.ilabel != info_.silence_label) return false; // Check the label is
// the silence label. Note, ilabel==olabel.
const std::vector<int32> &tids = arc.weight.String();
if (tids.empty()) return false;
int32 first_phone = tmodel_.TransitionIdToPhone(tids.front());
if (info_.TypeOfPhone(first_phone) != WordBoundaryInfo::kNonWordPhone)
return false;
for (size_t i = 0; i < tids.size(); i++)
if (tmodel_.TransitionIdToPhone(tids[i]) != first_phone) return false;
if (!info_.reorder) return tmodel_.IsFinal(tids.back());
else {
for (size_t i = 0; i < tids.size(); i++) {
if (tmodel_.IsFinal(tids[i])) { // got the "final" transition, which is
// reordered to actually not be final. Make sure that all the
// rest of the transition ids are the self-loop of that same
// transition-state.
for (size_t j = i+1; j < tids.size(); j++) {
if (!tmodel_.TransitionIdsEquivalent(tids[j], tids[i])) return false;
}
return true;
}
}
return false; // fell off loop. No final-state present.
}
}
bool TestArcOnePhoneWord(const CompactLatticeArc &arc) {
if (arc.ilabel == 0) return false; // Check there's a label. Note, ilabel==olabel.
const std::vector<int32> &tids = arc.weight.String();
if (tids.empty()) return false;
int32 first_phone = tmodel_.TransitionIdToPhone(tids.front());
if (info_.TypeOfPhone(first_phone) !=
WordBoundaryInfo::kWordBeginAndEndPhone) return false;
for (size_t i = 0; i < tids.size(); i++)
if (tmodel_.TransitionIdToPhone(tids[i]) != first_phone) return false;
if (!info_.reorder) return tmodel_.IsFinal(tids.back());
else {
for (size_t i = 0; i < tids.size(); i++) {
if (tmodel_.IsFinal(tids[i])) { // got the "final" transition, which is
// reordered to actually not be final. Make sure that all the
// rest of the transition ids are the self-loop of that same
// transition-state.
for (size_t j = i+1; j < tids.size(); j++) {
if (!tmodel_.TransitionIdsEquivalent(tids[j], tids[i])) return false;
}
return true;
}
}
return false; // fell off loop. No final-state present.
}
}
bool TestArcNormalWord(const CompactLatticeArc &arc) {
if (arc.ilabel == 0) return false; // Check there's a label. Note, ilabel==olabel.
const std::vector<int32> &tids = arc.weight.String();
if (tids.empty()) return false;
int32 first_phone = tmodel_.TransitionIdToPhone(tids.front());
if (info_.TypeOfPhone(first_phone) != WordBoundaryInfo::kWordBeginPhone)
return false;
size_t i;
{ // first phone.
int num_final = 0;
for (i = 0; i < tids.size(); i++) {
if (tmodel_.TransitionIdToPhone(tids[i]) != first_phone) break;
if (tmodel_.IsFinal(tids[i])) num_final++;
}
if (num_final != 1)
return false; // Something went wrong-- perhaps we
// got two beginning phones in a row.
}
{ // middle phones. Skip over them.
while (i < tids.size() &&
info_.TypeOfPhone(tmodel_.TransitionIdToPhone(tids[i]))
== WordBoundaryInfo::kWordInternalPhone)
i++;
}
if (i == tids.size()) return false;
int32 final_phone = tmodel_.TransitionIdToPhone(tids[i]);
if (info_.TypeOfPhone(final_phone) != WordBoundaryInfo::kWordEndPhone)
return false; // not word-ending.
for (size_t j = i; j < tids.size(); j++) // make sure only this final phone till end.
if (tmodel_.TransitionIdToPhone(tids[j]) != final_phone)
return false; // Other phones after final phone.
for (size_t j = i; j < tids.size(); j++) {
if (tmodel_.IsFinal(tids[j])) { // Found "final transition".. Note:
// may be "reordered" with its self loops.
if (!info_.reorder) return (j+1 == tids.size());
else {
// Make sure the only thing that follows this is self-loops
// of the final transition-state.
for (size_t k = j + 1; k < tids.size(); k++)
if (!tmodel_.TransitionIdsEquivalent(tids[k], tids[j])
|| !tmodel_.IsSelfLoop(tids[k]))
return false;
return true;
}
}
}
return false; // Found no final state.
}
bool TestArcPartialWord(const CompactLatticeArc &arc) {
if (arc.ilabel != info_.partial_word_label) return false; // label should
// be the partial-word label.
const std::vector<int32> &tids = arc.weight.String();
if (tids.empty()) return false;
return true; // We're pretty liberal when it comes to partial words here.
}
void TestFinal(const CompactLatticeWeight &w) {
if (!w.String().empty())
KALDI_ERR << "Expect to have no strings on final-weights of lattices.";
}
void TestEquivalent() {
CompactLattice aligned_lat(aligned_lat_);
if (info_.silence_label != 0) { // remove silence labels.
std::vector<int32> to_remove;
to_remove.push_back(info_.silence_label);
RemoveSomeInputSymbols(to_remove, &aligned_lat);
Project(&aligned_lat, fst::PROJECT_INPUT);
}
if (!RandEquivalent(lat_, aligned_lat, 5/*paths*/, 1.0e+10/*delta*/, Rand()/*seed*/,
200/*path length (max?)*/))
KALDI_ERR << "Equivalence test failed (testing word-alignment of lattices.) "
<< "Make sure your model and lattices match!";
}
const CompactLattice &lat_;
const TransitionInformation &tmodel_;
const WordBoundaryInfo &info_;
const CompactLattice &aligned_lat_;
};
/// You should only test a lattice if WordAlignLattice returned true (i.e. it
/// succeeded and it wasn't a forced-out lattice); otherwise the test will most
/// likely fail.
void TestWordAlignedLattice(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordBoundaryInfo &info,
const CompactLattice &aligned_lat) {
WordAlignedLatticeTester t(lat, tmodel, info, aligned_lat);
t.Test();
}
} // namespace kaldi
@@ -0,0 +1,211 @@
// lat/word-align-lattice.h
// Copyright 2009-2012 Microsoft Corporation 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.
#ifndef KALDI_LAT_WORD_ALIGN_LATTICE_H_
#define KALDI_LAT_WORD_ALIGN_LATTICE_H_
#include <fst/fstlib.h>
#include <fst/fst-decl.h>
#include "base/kaldi-common.h"
#include "util/common-utils.h"
#include "fstext/fstext-lib.h"
#include "itf/transition-information.h"
#include "lat/kaldi-lattice.h"
namespace kaldi {
struct WordBoundaryInfoOpts {
// Note: use of this structure
// is deprecated, see WordBoundaryInfoNewOpts.
// Note: this structure (and the code in word-align-lattice.{h,cc}
// makes stronger assumptions than the rest of the Kaldi toolkit:
// that is, it assumes you have word-position-dependent phones,
// with disjoint subsets of phones for (word-begin, word-end,
// word-internal, word-begin-and-end), and of course silence,
// which is assumed not to be inside a word [it will just print
// a warning if it is, though, and should give the right output
// as long as it's not at the beginning or end of a word].
std::string wbegin_phones;
std::string wend_phones;
std::string wbegin_and_end_phones;
std::string winternal_phones;
std::string silence_phones;
int32 silence_label;
int32 partial_word_label;
bool reorder;
bool silence_may_be_word_internal;
bool silence_has_olabels;
WordBoundaryInfoOpts(): silence_label(0), partial_word_label(0),
reorder(true), silence_may_be_word_internal(false),
silence_has_olabels(false) { }
void Register(OptionsItf *opts) {
opts->Register("wbegin-phones", &wbegin_phones, "Colon-separated list of "
"numeric ids of phones that begin a word");
opts->Register("wend-phones", &wend_phones, "Colon-separated list of "
"numeric ids of phones that end a word");
opts->Register("winternal-phones", &winternal_phones, "Colon-separated list "
"of numeric ids of phones that are internal to a word");
opts->Register("wbegin-and-end-phones", &wbegin_and_end_phones, "Colon-separated "
"list of numeric ids of phones that are used for "
"single-phone words.");
opts->Register("silence-phones", &silence_phones, "Colon-separated list of "
"numeric ids of phones that are used for silence (and other "
"non-word events such as noise - anything that doesn't have "
"a corresponding symbol in the lexicon.");
opts->Register("silence-label", &silence_label, "Numeric id of word symbol "
"that is to be used for silence arcs in the word-aligned "
"lattice (zero is OK)");
opts->Register("partial-word-label", &partial_word_label, "Numeric id of "
"word symbol that is to be used for arcs in the word-aligned "
"lattice corresponding to partial words at the end of "
"\"forced-out\" utterances (zero is OK)");
opts->Register("reorder", &reorder, "True if the lattices were generated "
"from graphs that had the --reorder option true, relating to "
"reordering self-loops (typically true)");
opts->Register("silence-may-be-word-internal", &silence_may_be_word_internal,
"If true, silence may appear inside words' prons (but not at begin/end!)\n");
opts->Register("silence-has-olabels", &silence_has_olabels,
"If true, silence phones have output labels in the lattice, just\n"
"like regular words. [This means you can't have un-labeled silences]");
}
};
// This structure is to be used for newer code, from s5 scripts on.
struct WordBoundaryInfoNewOpts {
int32 silence_label;
int32 partial_word_label;
bool reorder;
WordBoundaryInfoNewOpts(): silence_label(0), partial_word_label(0),
reorder(true) { }
void Register(OptionsItf *opts) {
opts->Register("silence-label", &silence_label, "Numeric id of word symbol "
"that is to be used for silence arcs in the word-aligned "
"lattice (zero is OK)");
opts->Register("partial-word-label", &partial_word_label, "Numeric id of "
"word symbol that is to be used for arcs in the word-aligned "
"lattice corresponding to partial words at the end of "
"\"forced-out\" utterances (zero is OK)");
opts->Register("reorder", &reorder, "True if the lattices were generated "
"from graphs that had the --reorder option true, relating to "
"reordering self-loops (typically true)");
}
};
struct WordBoundaryInfo {
// This initializer will be deleted eventually.
WordBoundaryInfo(const WordBoundaryInfoOpts &opts); // Initialize from
// options class. Note: this throws. Don't try to catch this error
// and continue; catching errors thrown from initializers is dangerous.
// Note: the following vectors are initialized from the corresponding
// options strings in the options class, but if silence_may_be_word_internal=true
// or silence_has_olabels=true, we modify them as needed to make
// silence phones behave in this way.
// This initializer is to be used in future.
WordBoundaryInfo(const WordBoundaryInfoNewOpts &opts);
WordBoundaryInfo(const WordBoundaryInfoNewOpts &opts,
std::string word_boundary_file);
void Init(std::istream &stream);
enum PhoneType {
kNoPhone = 0,
kWordBeginPhone,
kWordEndPhone,
kWordBeginAndEndPhone,
kWordInternalPhone,
kNonWordPhone // non-word phones are typically silence phones; but the point
// is that there is
// no word label associated with them in the lattice. If a silence phone
// had a word label with it, we'd have to call it kWordBeginAndEndPhone.
};
PhoneType TypeOfPhone(int32 p) const {
if ((p < 0 || p > phone_to_type.size()))
KALDI_ERR << "Phone " << p << " was not specified in "
"word-boundary file (or options)";
return phone_to_type[p];
}
std::vector<PhoneType> phone_to_type;
int32 silence_label; // The integer label we give to silence words.
// (May be zero).
int32 partial_word_label; // The label we give to partially
// formed words that we might get at the end of the utterance
// if the lattice was "forced out" (no end state was reached).
bool reorder; // True if the "reordering" of self-loops versus
// forward-transition was done during graph creation (will
// normally be true.
private:
// This is to be removed eventually, when we all move to s5 scripts.
void SetOptions(const std::string int_list, PhoneType phone_type);
};
/// Align lattice so that each arc has the transition-ids on it
/// that correspond to the word that is on that arc. [May also have
/// epsilon arcs for optional silences.]
/// Returns true if everything was OK, false if some kind of
/// error was detected (e.g. the words didn't have the kinds of
/// sequences we would expect if the WordBoundaryInfo was
/// correct). Note: we don't expect silence inside words,
/// or empty words (words with no phones), and we expect
/// the word to start with a wbegin_phone, to end with
/// a wend_phone, and to possibly have winternal_phones
/// inside (or to consist of just one wbegin_and_end_phone).
/// Note: if it returns false, it doesn't mean the lattice
/// that the output is necessarily bad: it might just be that
/// the lattice was "forced out" as the end-state was not
/// reached during decoding, and in this case the output might
/// be usable.
/// If max_states > 0, if this code detects that the #states
/// of the output will be greater than max_states, it will
/// abort the computation, return false and produce an empty
/// lattice out.
bool WordAlignLattice(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordBoundaryInfo &info,
int32 max_states,
CompactLattice *lat_out);
/// This function is designed to crash if something went wrong with the
/// word-alignment of the lattice. It verifies
/// that arcs are of 4 types:
/// properly-aligned word arcs, with a word label.
/// partial-word arcs, with the partial-word label.
/// silence arcs, with the silence label.
void TestWordAlignedLattice(const CompactLattice &lat,
const TransitionInformation &tmodel,
const WordBoundaryInfo &info,
const CompactLattice &aligned_lat);
} // end namespace kaldi
#endif