Initial commit: FunASR Speech Recognition Toolkit
Update API Documentation / build-api-docs (push) Has been cancelled
Update API Documentation / build-api-docs (push) Has been cancelled
Add complete FunASR codebase including models, runtime, and documentation.
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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_
|
||||
+147
@@ -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_
|
||||
+262
@@ -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
|
||||
+104
@@ -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_
|
||||
@@ -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> > ×,
|
||||
MinimumBayesRiskOptions opts) : opts_(opts) {
|
||||
CompactLattice clat(clat_in); // copy.
|
||||
|
||||
PrepareLatticeAndInitStats(&clat);
|
||||
|
||||
R_ = words;
|
||||
sausage_times_ = times;
|
||||
L_ = 0.0;
|
||||
|
||||
MbrDecode();
|
||||
}
|
||||
|
||||
|
||||
} // namespace kaldi
|
||||
@@ -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> > ×,
|
||||
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_
|
||||
+250
@@ -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
|
||||
Reference in New Issue
Block a user