@@ -110,90 +110,6 @@ std::optional<ReadingGrid::NodePtr> ReadingGrid::findInSpan(
110110
111111namespace {
112112
113- // Defines a vertex of a DAG. This is a mutable data structure used for both
114- // DAG construction and single-source shortest-path computation.
115- struct Vertex {
116- explicit Vertex (ReadingGrid::NodePtr nodePtr) : node(std::move(nodePtr)) {
117- edges.reserve (ReadingGrid::kMaximumSpanLength );
118- }
119- ReadingGrid::NodePtr node;
120- std::vector<Vertex*> edges;
121-
122- // Used during topological-sort.
123- bool topologicallySorted = false ;
124-
125- // Used during shortest-path computation. We are actually computing the
126- // path with the *largest* weight, hence distance's initial value being
127- // negative infinity. If we were to compute the *shortest* weight/distance,
128- // we would have initialized this to infinity.
129- double distance = -std::numeric_limits<double >::infinity();
130- Vertex* prev = nullptr ;
131- };
132-
133- // Cormen et al. 2001 explains the historical origin of the term "relax."
134- void Relax (Vertex* u, Vertex* v) {
135- // The distance from u to w is simply v's score.
136- double w = v->node ->score ();
137-
138- // Since we are computing the largest weight, we update v's distance and prev
139- // if the current distance to v is *less* than that of u's plus the distance
140- // to v (which is represented by w).
141- if (v->distance < u->distance + w) {
142- v->distance = u->distance + w;
143- v->prev = u;
144- }
145- }
146-
147- using VertexSpan = std::vector<Vertex>;
148-
149- // Topological-sorts a DAG that has a single root and returns the vertices in
150- // topological order. Here, a non-recursive version is implemented using our own
151- // stack and state definitions, so that we are not constrained by the current
152- // thread's stack size. This is the equivalent to this recursive version:
153- //
154- // void TopologicalSort(Vertex* v) {
155- // for (Vertex* nv : v->edges) {
156- // if (!nv->topologicallySorted) {
157- // dfs(nv, result);
158- // }
159- // }
160- // v->topologicallySorted = true;
161- // result.push_back(v);
162- // }
163- //
164- // The recursive version is similar to the TOPOLOGICAL-SORT algorithm found in
165- // Cormen et al. 2001.
166- std::vector<Vertex*> TopologicalSort (Vertex* root) {
167- std::vector<Vertex*> result;
168- struct State {
169- explicit State (Vertex* vv) : v(vv), edgeIter(v->edges.begin()) {}
170- Vertex* v;
171- std::vector<Vertex*>::iterator edgeIter;
172- };
173- std::stack<State> stack;
174- stack.emplace (root);
175-
176- while (!stack.empty ()) {
177- State& state = stack.top ();
178- Vertex* v = state.v ;
179-
180- if (state.edgeIter != v->edges .end ()) {
181- Vertex* nv = *state.edgeIter ;
182- ++state.edgeIter ;
183- if (!nv->topologicallySorted ) {
184- stack.emplace (nv);
185- continue ;
186- }
187- }
188-
189- v->topologicallySorted = true ;
190- result.push_back (v);
191- stack.pop ();
192- }
193-
194- return result;
195- }
196-
197113int64_t GetEpochNowInMicroseconds () {
198114 auto now = std::chrono::system_clock::now ();
199115 int64_t timestamp =
@@ -206,8 +122,8 @@ int64_t GetEpochNowInMicroseconds() {
206122} // namespace
207123
208124// Find the weightiest path in the grid graph. The path represents the most
209- // likely hidden chain of events from the observations. We use the
210- // DAG-SHORTEST-PATHS algorithm in Cormen et al. 2001 to compute such path.
125+ // likely hidden chain of events from the observations.
126+ // We use the Viterbi algorithm to compute such path.
211127// Instead of computing the path with the shortest distance, though, we compute
212128// the path with the longest distance (so the weightiest), since with log
213129// probability a larger value means a larger probability. The algorithm runs in
@@ -220,67 +136,68 @@ ReadingGrid::WalkResult ReadingGrid::walk() {
220136 }
221137 int64_t start = GetEpochNowInMicroseconds ();
222138
223- std::vector<VertexSpan> vspans (spans_.size (), VertexSpan ());
224- size_t vertices = 0 ;
225- size_t edges = 0 ;
226- for (size_t i = 0 , len = spans_.size (); i < len; ++i) {
139+ // Defines a state in the DP table. This structure tracks the maximum
140+ // accumulated score and the back-pointer required for path reconstruction in
141+ // the Viterbi algorithm.
142+ struct State {
143+ size_t fromIndex = 0 ;
144+ ReadingGrid::NodePtr fromNode = nullptr ;
145+ double maxScore = -std::numeric_limits<double >::infinity();
146+ };
147+
148+ const size_t readingLen = readings_.size();
149+ std::vector<State> viterbi (readingLen + 1 );
150+ viterbi[0 ].maxScore = 0.0 ;
151+
152+ // Iterate through the grid and compute the maximum accumulated score for each
153+ // reachable position. Since the grid is a lattice where edges only point
154+ // forward, processing nodes in index order is equivalent to processing them
155+ // in topological order.
156+ size_t reachableStates = 0 ;
157+ size_t evaluatedEdges = 0 ;
158+ for (size_t i = 0 ; i < readingLen; ++i) {
159+ ++reachableStates;
160+
227161 const ReadingGrid::Span& span = spans_[i];
228- for (size_t j = 1 , maxSpanLen = span.maxLength (); j <= maxSpanLen; ++j) {
229- NodePtr p = span.nodeOf (j);
230- if (p != nullptr ) {
231- vspans[i].emplace_back (std::move (p));
232- ++vertices;
233- }
234- }
235- }
236- result.vertices = vertices;
162+ const size_t maxSpanLen = span.maxLength ();
237163
238- Vertex terminal (std::make_shared<ReadingGrid::Node>(
239- " _TERMINAL_" , 0 , std::vector<LanguageModel::Unigram>()));
240- for (size_t i = 0 , vspansLen = vspans.size (); i < vspansLen; ++i) {
241- for (Vertex& v : vspans[i]) {
242- size_t nextVertexPos = i + v.node ->spanningLength ();
243- if (nextVertexPos == vspansLen) {
244- v.edges .push_back (&terminal);
164+ for (size_t spanLen = 1 ; spanLen <= maxSpanLen; ++spanLen) {
165+ const ReadingGrid::NodePtr& node = span.nodeOf (spanLen);
166+ if (node == nullptr ) {
245167 continue ;
246168 }
247-
248- for (Vertex& nv : vspans[nextVertexPos]) {
249- v.edges .push_back (&nv);
250- ++edges;
169+ ++evaluatedEdges;
170+
171+ // Performs a relaxation on a transition. This updates the destination
172+ // state if the path through the current node yields a higher score than
173+ // the previously known best path. This is the core operation of the
174+ // Viterbi algorithm, adapted for finding the maximum likelihood path.
175+ double score = viterbi[i].maxScore + node->score ();
176+ State& target = viterbi[i + spanLen];
177+ if (score > target.maxScore ) {
178+ target.maxScore = score;
179+ target.fromNode = node;
180+ target.fromIndex = i;
251181 }
252182 }
253183 }
254- result.edges = edges;
184+ // Vertices are the reachable states
185+ // Edges are the candidate word transitions
186+ result.vertices = reachableStates;
187+ result.edges = evaluatedEdges;
255188
256- Vertex root (std::make_shared<ReadingGrid::Node>(
257- " _ROOT_" , 0 , std::vector<LanguageModel::Unigram>()));
258- root.distance = 0 ;
259- for (Vertex& v : vspans[0 ]) {
260- root.edges .push_back (&v);
261- }
262-
263- std::vector<Vertex*> ordered = TopologicalSort (&root);
264- for (auto it = ordered.rbegin (), rend = ordered.rend (); it != rend; ++it) {
265- Vertex* u = *it;
266- for (Vertex* v : u->edges ) {
267- Relax (u, v);
268- }
269- }
270-
271- std::vector<NodePtr> walked;
189+ // Reconstruct the most likely path by tracing back from the end of the grid
190+ // to the root using the back-pointers
272191 size_t totalReadingLen = 0 ;
273- Vertex* it = &terminal;
274- while (it->prev != nullptr ) {
275- walked.push_back (it->prev ->node );
276- it = it->prev ;
277- totalReadingLen += it->node ->spanningLength ();
192+ for (size_t curr = readingLen; curr > 0 ; curr = viterbi[curr].fromIndex) {
193+ assert (viterbi[curr].fromNode != nullptr );
194+ totalReadingLen += viterbi[curr].fromNode ->spanningLength ();
195+ result.nodes .emplace_back (std::move (viterbi[curr].fromNode ));
278196 }
279-
280- assert (totalReadingLen == readings_.size ());
281- assert (walked.size () >= 2 );
197+ std::reverse (result.nodes.begin(), result.nodes.end());
198+ assert (totalReadingLen == readingLen);
282199 result.totalReadings = totalReadingLen;
283- result. nodes = std::vector<NodePtr>(walked. rbegin () + 1 , walked. rend ());
200+
284201 result.elapsedMicroseconds = GetEpochNowInMicroseconds() - start;
285202 return result;
286203}
0 commit comments