Skip to content

Commit 02b06d1

Browse files
tianjianjiangclaude
andcommitted
docs(algorithm): update walk algorithm from DAG to Viterbi (#777)
Replace the DAG shortest-path algorithm documentation with the Viterbi implementation merged in #777. The new section documents the linear lattice forward pass, relaxation, and backward path reconstruction. - Replace 4-step DAG section with 2-step Viterbi (forward + backward) - Update code reference table: remove TopologicalSort/Relax rows - Update references: Jurafsky & Martin + vene.ro lattice Viterbi - Fix TOC link to match new heading - Bump version to 1.3 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e246baa commit 02b06d1

1 file changed

Lines changed: 47 additions & 87 deletions

File tree

algorithm.md

Lines changed: 47 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
- [演算法流程](#演算法流程)
2828
- [插入注音時的處理](#插入注音時的處理)
2929
- [節點更新機制](#節點更新機制)
30-
- [最佳路徑演算法:DAG 最短路徑](#最佳路徑演算法dag-最短路徑)
30+
- [最佳路徑演算法:Viterbi](#最佳路徑演算法viterbi)
3131
- [實際範例](#實際範例)
3232
- [語言模型架構](#語言模型架構)
3333
- [McBopomofoLM:統一介面](#mcbopomofolm統一介面)
@@ -209,108 +209,69 @@ void ReadingGrid::update() {
209209
}
210210
```
211211

212-
#### 最佳路徑演算法:DAG 最短路徑
212+
#### 最佳路徑演算法:Viterbi
213213

214-
`walk()` 方法使用 **有向無環圖(DAG)最短路徑演算法** 找出分數最高的路徑(`reading_grid.cpp:216`):
214+
`walk()` 方法使用 **Viterbi 演算法** 找出分數最高的路徑(`reading_grid.cpp`)。
215215

216-
**步驟 1:建立 DAG**
216+
Reading Grid 本身是一個線性格架(lattice):每個位置只能連接到更後方的位置,構成天然的拓撲順序。因此不需要額外的拓撲排序,直接按位置順序前向掃描即可。
217217

218-
```cpp
219-
ReadingGrid::WalkResult ReadingGrid::walk() {
220-
// 1. 將所有 Node 轉換為圖的 Vertex(頂點)
221-
std::vector<VertexSpan> vspans(spans_.size());
222-
for (size_t i = 0; i < spans_.size(); i++) {
223-
const Span& span = spans_[i];
224-
for (size_t j = 1; j <= span.maxLength(); j++) {
225-
NodePtr node = span.nodeOf(j);
226-
if (node != nullptr) {
227-
vspans[i].emplace_back(Vertex(node));
228-
}
229-
}
230-
}
218+
演算法使用一個動態規劃(DP)表格,每個 entry 記錄到達該位置的最大累計分數和回溯指標:
231219

232-
// 2. 建立邊(Edge):連接相鄰節點
233-
for (size_t i = 0; i < vspans.size(); i++) {
234-
for (Vertex& v : vspans[i]) {
235-
size_t nextPos = i + v.node->spanningLength();
236-
// 連接到下一個位置的所有節點
237-
for (Vertex& nv : vspans[nextPos]) {
238-
v.edges.push_back(&nv);
239-
}
240-
}
241-
}
220+
```cpp
221+
struct State {
222+
size_t fromIndex = 0;
223+
ReadingGrid::NodePtr fromNode = nullptr;
224+
double maxScore = -std::numeric_limits<double>::infinity();
225+
};
242226
```
243227
244-
**步驟 2:拓撲排序**
228+
**步驟 1:前向傳遞(Forward Pass)**
245229
246-
使用非遞迴的深度優先搜尋(DFS)進行拓撲排序(`reading_grid.cpp:166`):
230+
從位置 0 開始,依序掃描每個位置。對於每個位置上所有可能的候選詞節點,計算「到達該詞末端位置」的累計分數,並執行鬆弛(relaxation):
247231
248232
```cpp
249-
std::vector<Vertex*> TopologicalSort(Vertex* root) {
250-
std::vector<Vertex*> result;
251-
std::stack<State> stack;
252-
stack.emplace(root);
253-
254-
while (!stack.empty()) {
255-
State& state = stack.top();
256-
Vertex* v = state.v;
257-
258-
if (state.edgeIter != v->edges.end()) {
259-
Vertex* nv = *state.edgeIter;
260-
++state.edgeIter;
261-
if (!nv->topologicallySorted) {
262-
stack.emplace(nv);
263-
continue;
264-
}
233+
const size_t readingLen = readings_.size();
234+
std::vector<State> viterbi(readingLen + 1);
235+
viterbi[0].maxScore = 0.0;
236+
237+
for (size_t i = 0; i < readingLen; ++i) {
238+
const ReadingGrid::Span& span = spans_[i];
239+
const size_t maxSpanLen = span.maxLength();
240+
241+
for (size_t spanLen = 1; spanLen <= maxSpanLen; ++spanLen) {
242+
const ReadingGrid::NodePtr& node = span.nodeOf(spanLen);
243+
if (node == nullptr) {
244+
continue;
265245
}
266246
267-
v->topologicallySorted = true;
268-
result.push_back(v);
269-
stack.pop();
247+
// 鬆弛操作:若經由目前節點到達目標位置的分數更高,則更新
248+
double score = viterbi[i].maxScore + node->score();
249+
State& target = viterbi[i + spanLen];
250+
if (score > target.maxScore) {
251+
target.maxScore = score;
252+
target.fromNode = node;
253+
target.fromIndex = i;
254+
}
270255
}
271-
return result;
272256
}
273257
```
274258

275-
**步驟 3:鬆弛演算法(Relaxation)**
276-
277-
對拓撲排序後的頂點依序執行鬆弛操作,找出最大權重路徑(`reading_grid.cpp:134`):
259+
核心觀察:因為使用對數機率(log probability),分數越大代表機率越高,所以鬆弛操作使用 `>` 而非 `<`
278260

279-
```cpp
280-
void Relax(Vertex* u, Vertex* v) {
281-
double w = v->node->score(); // 獲取節點的對數機率
261+
**步驟 2:回溯路徑(Backward Pass)**
282262

283-
// 因為我們要找最大權重,所以用 > 而非 <
284-
if (v->distance < u->distance + w) {
285-
v->distance = u->distance + w;
286-
v->prev = u; // 記錄前驅節點
287-
}
288-
}
263+
從 DP 表格的末端回溯,透過 `fromIndex``fromNode` 指標重建最佳路徑:
289264

290-
// 主流程
291-
std::vector<Vertex*> ordered = TopologicalSort(&root);
292-
for (auto it = ordered.rbegin(); it != ordered.rend(); ++it) {
293-
Vertex* u = *it;
294-
for (Vertex* v : u->edges) {
295-
Relax(u, v);
296-
}
265+
```cpp
266+
for (size_t curr = readingLen; curr > 0; curr = viterbi[curr].fromIndex) {
267+
result.nodes.emplace_back(std::move(viterbi[curr].fromNode));
297268
}
269+
std::reverse(result.nodes.begin(), result.nodes.end());
298270
```
299271
300-
**步驟 4:回溯路徑**
301-
302-
從終點回溯找出完整路徑:
272+
**複雜度與效能**
303273
304-
```cpp
305-
std::vector<NodePtr> walked;
306-
Vertex* it = &terminal;
307-
while (it->prev != nullptr) {
308-
walked.push_back(it->prev->node);
309-
it = it->prev;
310-
}
311-
// 反轉得到正確順序
312-
result.nodes = std::vector<NodePtr>(walked.rbegin() + 1, walked.rend());
313-
```
274+
演算法的時間複雜度為 O(V+E),其中 V 是可達的格架位置數,E 是候選詞轉移數。由於省去了圖的建構(Vertex/Edge)、拓撲排序等步驟,實測在 8001 個注音的壓力測試中,從原本 DAG 版本的約 8ms 降至約 1ms,提升約 8 倍。
314275
315276
### 實際範例
316277
@@ -878,9 +839,7 @@ LC_ALL=C sort -o phrase.occ phrase.occ
878839
| Reading Grid 主邏輯 | `Source/Engine/gramambular2/reading_grid.h` | `ReadingGrid` |
879840
| 插入注音處理 | `Source/Engine/gramambular2/reading_grid.cpp:51` | `insertReading()` |
880841
| 節點更新 | `Source/Engine/gramambular2/reading_grid.cpp:417` | `update()` |
881-
| 最佳路徑演算法 | `Source/Engine/gramambular2/reading_grid.cpp:216` | `walk()` |
882-
| 拓撲排序 | `Source/Engine/gramambular2/reading_grid.cpp:166` | `TopologicalSort()` |
883-
| 鬆弛演算法 | `Source/Engine/gramambular2/reading_grid.cpp:134` | `Relax()` |
842+
| 最佳路徑演算法(Viterbi) | `Source/Engine/gramambular2/reading_grid.cpp` | `walk()` |
884843

885844
### 語言模型
886845

@@ -929,10 +888,11 @@ LC_ALL=C sort -o phrase.occ phrase.occ
929888
- [Wiki: Gramambular 演算法](https://github.com/openvanilla/McBopomofo/wiki/程式架構_Gramambular)
930889
- [Wiki: 詞庫開發說明](https://github.com/openvanilla/McBopomofo/wiki/詞庫開發說明)
931890
- [X/Twitter 演算法說明串](https://x.com/McBopomofo/status/1559356063622631424)
932-
- DAG 最短路徑演算法:Cormen et al., *Introduction to Algorithms*, 3rd Edition
891+
- Viterbi 演算法:Jurafsky & Martin, *Speech and Language Processing*, 3rd Edition, Chapter 8
892+
- 格架上的 Viterbi:[vene.ro/blog/shortest-paths-in-lattices](https://vene.ro/blog/shortest-paths-in-lattices.html)
933893

934894
---
935895

936-
**文件版本**:1.2
937-
**最後更新**2025-10-12T13:12:00+08:00
896+
**文件版本**:1.3
897+
**最後更新**2026-02-14T22:49:00+08:00
938898
**適用版本**:McBopomofo 2.x 及以上

0 commit comments

Comments
 (0)