Reza2kn commited on
Commit
b117fc2
·
verified ·
1 Parent(s): ef16ea3

Add portable Stage 6 native-bbox recovery bundle

Browse files
training-code/stage6-native-v2/README.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Bina 0.2 Stage 6 portable recovery bundle
2
+
3
+ This bundle resumes the native-bbox corrective lane on an intact or replacement
4
+ eight-GPU cluster.
5
+
6
+ ## Immutable inputs
7
+
8
+ - Hugging Face artifact:
9
+ `Reza2kn/Bina-0.2-RizehPizeh/training-data/native-replay-v1.tar`
10
+ - Archive SHA-256:
11
+ `2cb4b0507a1f6fc1fcf6619b679de5fd35a52e2ee8f55c57ce5ebf00c52ffc40`
12
+ - Accepted replay:
13
+ 7,041 hash-matched pages and 206,917 samples
14
+ - Fixed compositor:
15
+ 6,984 pages and 205,289 samples
16
+ - Archived compositor fallback:
17
+ 57 pages and 1,628 samples
18
+ - Excluded:
19
+ 485 pages that matched neither known production compositor
20
+
21
+ ## Cluster prerequisites
22
+
23
+ The cluster root is `/workspace/bina02-ppocr` and must contain:
24
+
25
+ - `PaddleOCR/`
26
+ - `venv/`
27
+ - `bina02_stage5_hardforms.yml`
28
+ - `watch_bina02_stage4_eval.py`
29
+ - `run/train-stage4-clean2m-lr3e6/best_accuracy`
30
+ - `data/corpus-v4/balanced`
31
+
32
+ The existing RTL label-encoding patch must remain active in PaddleOCR.
33
+
34
+ ## Recovery sequence
35
+
36
+ 1. Copy the three executable files in this directory to
37
+ `/workspace/bina02-ppocr`.
38
+ 2. Obtain a fresh final CDN URL for the Hugging Face replay archive.
39
+ 3. Run `fetch_native_replay_v2.sh "$SIGNED_CDN_URL"` under supervisor.
40
+ 4. Run `run_bina02_stage6_v2.sh` under supervisor.
41
+ 5. Export the best accepted checkpoint.
42
+ 6. Run the exact cached-Surya Persian OCR Triple Threat benchmark.
43
+ 7. Publish only if printed S3 and handwriting S3 are both below 10 with zero
44
+ missing predictions.
45
+
46
+ The training mix is approximately 30 percent exact native forms, 35 percent
47
+ general handwriting replay, and 35 percent printed replay. It uses a 1e-6
48
+ learning rate, frozen batch normalization, trainable backbone blocks 4 and 5
49
+ plus CTC layers, aspect-ratio-preserving resize and padding, and no geometric
50
+ `RecAug`.
training-code/stage6-native-v2/build_bina02_stage6_native_mix_v2.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ import lmdb
10
+ import yaml
11
+
12
+ ROOT = Path("/workspace/bina02-ppocr")
13
+ TRANSFER = Path(
14
+ os.environ.get(
15
+ "BINA_NATIVE_REPLAY_TRANSFER",
16
+ ROOT / "data/corpus-v5-native-replay/replay-transfer-v2",
17
+ )
18
+ )
19
+ FIXED_ROOT = TRANSFER / "full-fixed-v2"
20
+ STALE_ROOT = TRANSFER / "full-stale-fallback-v1"
21
+ FIXED = FIXED_ROOT / "lmdb"
22
+ STALE = STALE_ROOT / "lmdb"
23
+ BALANCED = ROOT / "data/corpus-v4/balanced"
24
+ MIX = ROOT / "data/corpus-v5-native-replay/stage6-mix-v2"
25
+ CONFIG = ROOT / "bina02_stage6_nativeforms_v2.yml"
26
+ COMBINED_EVAL_CONFIG = ROOT / "bina02_stage6_combined_eval_stage4_v2.yml"
27
+ NATIVE_EVAL_CONFIG = ROOT / "bina02_stage6_native_eval_stage4_v2.yml"
28
+ RUN = ROOT / "run/train-stage6-nativeforms-v2-30-35-35-lr1e6"
29
+ STAGE4 = ROOT / "run/train-stage4-clean2m-lr3e6/best_accuracy"
30
+
31
+ EXPECTED_SUMMARIES = {
32
+ FIXED_ROOT: {
33
+ "pages": 6984,
34
+ "samples": 205289,
35
+ "train_samples": 198483,
36
+ "eval_samples": 6806,
37
+ "rejected": 0,
38
+ "mismatched": 542,
39
+ },
40
+ STALE_ROOT: {
41
+ "pages": 57,
42
+ "samples": 1628,
43
+ "train_samples": 1628,
44
+ "eval_samples": 0,
45
+ "rejected": 0,
46
+ "mismatched": 485,
47
+ },
48
+ }
49
+
50
+
51
+ def verify_summary(root: Path, expected: dict[str, int]) -> None:
52
+ summary_path = root / "SUMMARY.json"
53
+ if not summary_path.exists():
54
+ raise SystemExit(f"missing replay summary: {summary_path}")
55
+ summary = json.loads(summary_path.read_text())
56
+ actual = {key: summary.get(key) for key in expected}
57
+ if actual != expected:
58
+ raise SystemExit(
59
+ f"replay summary mismatch for {root}: expected={expected}, actual={actual}"
60
+ )
61
+
62
+
63
+ def leaves(root: Path) -> list[Path]:
64
+ return sorted({path.parent for path in root.rglob("data.mdb")})
65
+
66
+
67
+ def count(path: Path) -> int:
68
+ env = lmdb.open(
69
+ str(path),
70
+ readonly=True,
71
+ lock=False,
72
+ readahead=False,
73
+ max_readers=1,
74
+ )
75
+ with env.begin() as txn:
76
+ value = txn.get(b"num-samples")
77
+ result = int(value) if value else 0
78
+ env.close()
79
+ return result
80
+
81
+
82
+ def stable(paths: list[Path], tag: str) -> list[Path]:
83
+ return sorted(
84
+ paths,
85
+ key=lambda path: hashlib.sha256((tag + str(path)).encode()).hexdigest(),
86
+ )
87
+
88
+
89
+ def choose(paths: list[Path], target: int, tag: str) -> list[tuple[Path, int]]:
90
+ selected: list[tuple[Path, int]] = []
91
+ total = 0
92
+ for path in stable(paths, tag):
93
+ samples = count(path)
94
+ if samples <= 0:
95
+ continue
96
+ selected.append((path, samples))
97
+ total += samples
98
+ if total >= target:
99
+ break
100
+ if total < target:
101
+ raise SystemExit(f"{tag} has only {total} samples; target is {target}")
102
+ return selected
103
+
104
+
105
+ def link_group(
106
+ group: str,
107
+ entries: list[tuple[Path, int]],
108
+ ) -> list[dict[str, object]]:
109
+ base = MIX / group
110
+ base.mkdir(parents=True, exist_ok=False)
111
+ rows: list[dict[str, object]] = []
112
+ for index, (source, samples) in enumerate(entries):
113
+ destination = base / f"{index:04d}-{source.parent.name}-{source.name}"
114
+ destination.mkdir()
115
+ os.link(source / "data.mdb", destination / "data.mdb")
116
+ rows.append(
117
+ {
118
+ "source": str(source),
119
+ "linked": str(destination),
120
+ "samples": samples,
121
+ }
122
+ )
123
+ return rows
124
+
125
+
126
+ if MIX.exists():
127
+ raise SystemExit(f"refusing to overwrite existing mix: {MIX}")
128
+ for replay_root, expected in EXPECTED_SUMMARIES.items():
129
+ verify_summary(replay_root, expected)
130
+ for required in (FIXED, STALE, BALANCED, STAGE4):
131
+ if not required.exists():
132
+ raise SystemExit(f"missing required input: {required}")
133
+
134
+ native_train = []
135
+ for path in leaves(FIXED / "train") + leaves(STALE / "train"):
136
+ samples = count(path)
137
+ if samples > 0:
138
+ native_train.append((path, samples))
139
+
140
+ native_train_total = sum(samples for _, samples in native_train)
141
+ if native_train_total != 200111:
142
+ raise SystemExit(
143
+ f"native train count mismatch: expected 200111, got {native_train_total}"
144
+ )
145
+
146
+ replay_target = (native_train_total * 35 + 29) // 30
147
+ handwriting_replay = choose(
148
+ leaves(BALANCED / "train/00_handwriting"),
149
+ replay_target,
150
+ "stage6-v2-handwriting",
151
+ )
152
+ printed_replay = choose(
153
+ leaves(BALANCED / "train/01_printed"),
154
+ replay_target,
155
+ "stage6-v2-printed",
156
+ )
157
+
158
+ train_rows = {
159
+ "native_forms": link_group("train/00_nativeforms", native_train),
160
+ "handwriting_replay": link_group(
161
+ "train/01_handwriting_replay",
162
+ handwriting_replay,
163
+ ),
164
+ "printed_replay": link_group(
165
+ "train/02_printed_replay",
166
+ printed_replay,
167
+ ),
168
+ }
169
+
170
+ native_eval = []
171
+ for path in leaves(FIXED / "eval") + leaves(STALE / "eval"):
172
+ samples = count(path)
173
+ if samples > 0:
174
+ native_eval.append((path, samples))
175
+ handwriting_eval = [
176
+ (path, count(path))
177
+ for path in leaves(BALANCED / "eval/00_handwriting")
178
+ if count(path) > 0
179
+ ]
180
+ printed_eval = [
181
+ (path, count(path))
182
+ for path in leaves(BALANCED / "eval/01_printed")
183
+ if count(path) > 0
184
+ ]
185
+
186
+ eval_rows = {
187
+ "native_forms": link_group("eval/00_nativeforms", native_eval),
188
+ "handwriting": link_group("eval/01_handwriting", handwriting_eval),
189
+ "printed": link_group("eval/02_printed", printed_eval),
190
+ }
191
+
192
+ train_counts = {
193
+ key: sum(row["samples"] for row in rows)
194
+ for key, rows in train_rows.items()
195
+ }
196
+ eval_counts = {
197
+ key: sum(row["samples"] for row in rows)
198
+ for key, rows in eval_rows.items()
199
+ }
200
+ if eval_counts["native_forms"] != 6806:
201
+ raise SystemExit(
202
+ f"native eval count mismatch: expected 6806, got {eval_counts['native_forms']}"
203
+ )
204
+
205
+ train_total = sum(train_counts.values())
206
+ handwriting_domain_rows = (
207
+ eval_counts["native_forms"] + eval_counts["handwriting"]
208
+ )
209
+ manifest = {
210
+ "schema": "bina02.stage6-native-mix.v2",
211
+ "replay_archive_sha256": (
212
+ "2cb4b0507a1f6fc1fcf6619b679de5fd35a52e2ee8f55c57ce5ebf00c52ffc40"
213
+ ),
214
+ "train": train_rows,
215
+ "eval": eval_rows,
216
+ "train_counts": train_counts,
217
+ "eval_counts": eval_counts,
218
+ "train_total": train_total,
219
+ "train_proportions": {
220
+ key: value / train_total for key, value in train_counts.items()
221
+ },
222
+ "handwriting_domain_rows": handwriting_domain_rows,
223
+ "printed_domain_rows": eval_counts["printed"],
224
+ "preprocessing": (
225
+ "native bbox crops with context; aspect-ratio-preserving resize+padding; "
226
+ "RecAug disabled"
227
+ ),
228
+ }
229
+ (MIX / "MIX_MANIFEST.json").write_text(
230
+ json.dumps(manifest, ensure_ascii=False, indent=2) + "\n"
231
+ )
232
+
233
+ base = yaml.safe_load((ROOT / "bina02_stage5_hardforms.yml").read_text())
234
+ base["Global"]["epoch_num"] = 2
235
+ base["Global"]["save_model_dir"] = str(RUN)
236
+ base["Global"]["pretrained_model"] = str(STAGE4)
237
+ base["Global"]["checkpoints"] = None
238
+ base["Global"]["eval_batch_step"] = [0, 200]
239
+ base["Global"].pop("stage5_mix_manifest", None)
240
+ base["Global"]["stage6_mix_manifest"] = str(MIX / "MIX_MANIFEST.json")
241
+ base["Global"]["eval_handwriting_rows"] = handwriting_domain_rows
242
+ base["Global"]["eval_printed_rows"] = eval_counts["printed"]
243
+ base["Optimizer"]["lr"]["learning_rate"] = 1.0e-6
244
+ base["Metric"]["domain_split_index"] = handwriting_domain_rows
245
+ base["Train"]["dataset"]["data_dir"] = str(MIX / "train")
246
+ base["Train"]["dataset"]["transforms"] = [
247
+ transform
248
+ for transform in base["Train"]["dataset"]["transforms"]
249
+ if "RecAug" not in transform
250
+ ]
251
+ base["Eval"]["dataset"]["data_dir"] = str(MIX / "eval")
252
+ CONFIG.write_text(yaml.safe_dump(base, sort_keys=False, allow_unicode=True))
253
+
254
+ combined_eval = copy.deepcopy(base)
255
+ combined_eval["Global"]["pretrained_model"] = None
256
+ combined_eval["Global"]["checkpoints"] = str(STAGE4)
257
+ COMBINED_EVAL_CONFIG.write_text(
258
+ yaml.safe_dump(combined_eval, sort_keys=False, allow_unicode=True)
259
+ )
260
+
261
+ native_eval_config = copy.deepcopy(combined_eval)
262
+ native_eval_config["Eval"]["dataset"]["data_dir"] = str(
263
+ MIX / "eval/00_nativeforms"
264
+ )
265
+ native_eval_config["Metric"] = {
266
+ "name": "RecMetric",
267
+ "main_indicator": "norm_edit_dis",
268
+ "ignore_space": False,
269
+ }
270
+ NATIVE_EVAL_CONFIG.write_text(
271
+ yaml.safe_dump(native_eval_config, sort_keys=False, allow_unicode=True)
272
+ )
273
+
274
+ print(
275
+ json.dumps(
276
+ {
277
+ "manifest": str(MIX / "MIX_MANIFEST.json"),
278
+ "config": str(CONFIG),
279
+ "combined_eval_config": str(COMBINED_EVAL_CONFIG),
280
+ "native_eval_config": str(NATIVE_EVAL_CONFIG),
281
+ "train_counts": train_counts,
282
+ "eval_counts": eval_counts,
283
+ "train_total": train_total,
284
+ "train_proportions": manifest["train_proportions"],
285
+ "handwriting_domain_rows": handwriting_domain_rows,
286
+ },
287
+ ensure_ascii=False,
288
+ indent=2,
289
+ )
290
+ )
training-code/stage6-native-v2/fetch_native_replay_v2.sh ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ ROOT=/workspace/bina02-ppocr/data/corpus-v5-native-replay
5
+ EXPECTED_SHA256=2cb4b0507a1f6fc1fcf6619b679de5fd35a52e2ee8f55c57ce5ebf00c52ffc40
6
+ URL=${1:?usage: fetch_native_replay_v2.sh SIGNED_CDN_URL}
7
+ PARTIAL="$ROOT/native-replay-v1.tar.part"
8
+ ARCHIVE="$ROOT/native-replay-v1.tar"
9
+ DEST="$ROOT/replay-transfer-v2"
10
+
11
+ if [ -e "$ARCHIVE" ] || [ -e "$DEST" ]; then
12
+ echo "final archive or extraction destination already exists; refusing to overwrite"
13
+ exit 3
14
+ fi
15
+
16
+ mkdir -p "$ROOT"
17
+ resume=()
18
+ if [ -s "$PARTIAL" ]; then
19
+ resume=(-C -)
20
+ fi
21
+
22
+ curl -4 -fL \
23
+ --connect-timeout 15 \
24
+ --retry 30 \
25
+ --retry-delay 5 \
26
+ --retry-all-errors \
27
+ "${resume[@]}" \
28
+ -o "$PARTIAL" \
29
+ "$URL"
30
+
31
+ printf '%s %s\n' "$EXPECTED_SHA256" "$PARTIAL" | sha256sum -c -
32
+ mv "$PARTIAL" "$ARCHIVE"
33
+ mkdir "$DEST"
34
+ tar --sparse -xf "$ARCHIVE" -C "$DEST"
35
+ printf '{"schema":"bina02.native-replay-fetch.v2","sha256":"%s","completed_at":"%s"}\n' \
36
+ "$EXPECTED_SHA256" \
37
+ "$(date -Iseconds)" \
38
+ > "$DEST/FETCH_COMPLETE.json"
39
+ echo "$(date -Iseconds) FETCH_AND_EXTRACT_COMPLETE"
training-code/stage6-native-v2/run_bina02_stage6_v2.sh ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ ROOT=/workspace/bina02-ppocr
5
+ RUN="$ROOT/run/train-stage6-nativeforms-v2-30-35-35-lr1e6"
6
+ CONFIG="$ROOT/bina02_stage6_nativeforms_v2.yml"
7
+ COMBINED_EVAL_CONFIG="$ROOT/bina02_stage6_combined_eval_stage4_v2.yml"
8
+ NATIVE_EVAL_CONFIG="$ROOT/bina02_stage6_native_eval_stage4_v2.yml"
9
+ BUILDER="$ROOT/build_bina02_stage6_native_mix_v2.py"
10
+ VENV="$ROOT/venv"
11
+
12
+ if [ -e "$RUN/status.log" ]; then
13
+ echo "Stage 6 v2 status already exists; refusing to duplicate the run"
14
+ exit 3
15
+ fi
16
+
17
+ mkdir -p "$RUN" "$RUN/distributed-logs"
18
+ "$VENV/bin/python" "$BUILDER" > "$RUN/mix-build.json"
19
+
20
+ cd "$ROOT/PaddleOCR"
21
+ export PYTHONPATH="$ROOT/PaddleOCR"
22
+ export FLAGS_allocator_strategy=auto_growth
23
+ export OMP_NUM_THREADS=4
24
+
25
+ CUDA_VISIBLE_DEVICES=0 "$VENV/bin/python" tools/eval.py \
26
+ -c "$COMBINED_EVAL_CONFIG" \
27
+ > "$RUN/stage4-combined-baseline.log" 2>&1 &
28
+ combined_eval_pid=$!
29
+
30
+ CUDA_VISIBLE_DEVICES=1 "$VENV/bin/python" tools/eval.py \
31
+ -c "$NATIVE_EVAL_CONFIG" \
32
+ > "$RUN/stage4-native-baseline.log" 2>&1 &
33
+ native_eval_pid=$!
34
+
35
+ wait "$combined_eval_pid"
36
+ wait "$native_eval_pid"
37
+
38
+ read -r baseline_handwriting baseline_printed < <(
39
+ "$VENV/bin/python" - "$RUN/stage4-combined-baseline.log" <<'PY'
40
+ import re
41
+ import sys
42
+ from pathlib import Path
43
+
44
+ text = Path(sys.argv[1]).read_text(errors="replace")
45
+ matches = re.findall(
46
+ r"cur metric,.*?handwriting_norm_edit_dis:\s*([0-9.eE+-]+)"
47
+ r".*?printed_norm_edit_dis:\s*([0-9.eE+-]+)",
48
+ text,
49
+ )
50
+ if not matches:
51
+ raise SystemExit("could not parse Stage 4 combined-domain baseline")
52
+ print(*matches[-1])
53
+ PY
54
+ )
55
+
56
+ printf 'BASELINE_HANDWRITING=%s\nBASELINE_PRINTED=%s\n' \
57
+ "$baseline_handwriting" \
58
+ "$baseline_printed" \
59
+ > "$RUN/baselines.env"
60
+
61
+ cp "$CONFIG" "$RUN/launch-config.yml"
62
+ printf '%s TRAIN_START\n' "$(date -Iseconds)" > "$RUN/status.log"
63
+
64
+ setsid "$VENV/bin/python" -m paddle.distributed.launch \
65
+ --gpus "0,1,2,3,4,5,6,7" \
66
+ --log_dir "$RUN/distributed-logs" \
67
+ tools/train.py \
68
+ -c "$RUN/launch-config.yml" \
69
+ > "$RUN/train-launch.log" 2>&1 &
70
+ trainer=$!
71
+ printf '%s TRAIN_PID=%s\n' "$(date -Iseconds)" "$trainer" >> "$RUN/status.log"
72
+
73
+ "$VENV/bin/python" "$ROOT/watch_bina02_stage4_eval.py" \
74
+ --log "$RUN/train-launch.log" \
75
+ --pgid "$trainer" \
76
+ --state "$RUN/eval-gate-state.json" \
77
+ --baseline-handwriting "$baseline_handwriting" \
78
+ --baseline-printed "$baseline_printed" \
79
+ --handwriting-margin 0.003 \
80
+ --printed-margin 0.002 \
81
+ --patience 2 \
82
+ --poll-seconds 5 \
83
+ > "$RUN/eval-gate.log" 2>&1 &
84
+ watcher=$!
85
+
86
+ set +e
87
+ wait "$trainer"
88
+ rc=$?
89
+ set -e
90
+ kill "$watcher" 2>/dev/null || true
91
+ wait "$watcher" 2>/dev/null || true
92
+ printf '%s TRAIN_EXIT_CODE=%s\n' "$(date -Iseconds)" "$rc" >> "$RUN/status.log"
93
+ exit "$rc"