Skip to content

Commit 4199b23

Browse files
authored
Merge pull request #777 from ChiahongHong/viterbi
Refactor DAG shortest path algorithm using Viterbi
2 parents ff4907a + 9fa53a6 commit 4199b23

1 file changed

Lines changed: 53 additions & 136 deletions

File tree

Source/Engine/gramambular2/reading_grid.cpp

Lines changed: 53 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -110,90 +110,6 @@ std::optional<ReadingGrid::NodePtr> ReadingGrid::findInSpan(
110110

111111
namespace {
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-
197113
int64_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

Comments
 (0)