FlanChanXwO commited on
Commit
e5bd297
·
verified ·
1 Parent(s): 9a4d01f

Delete files docs/TRAINING.md docs/INFERENCE.md docs/EVALUATION.md with huggingface_hub

Browse files
Files changed (3) hide show
  1. docs/EVALUATION.md +0 -127
  2. docs/INFERENCE.md +0 -253
  3. docs/TRAINING.md +0 -181
docs/EVALUATION.md DELETED
@@ -1,127 +0,0 @@
1
- # 评估方法 / Evaluation
2
-
3
- ---
4
-
5
- ## 中文
6
-
7
- 本文档说明 **87.6% 登录命中率** 是如何测量的,保证结果可复现、可信。
8
-
9
- ### 核心原则
10
-
11
- 南+ 验证码没有公开的 ground truth 数据集,因此使用**真实论坛登录**作为唯一、100% 可靠的判定标准:
12
-
13
- > **论坛返回"登录成功" = 模型识别正确**
14
- > **论坛返回"认证码不正确或已过期" = 模型识别错误**
15
-
16
- 每次判定都是干净的二元结果,不依赖任何主观估计。
17
-
18
- ### 测试流程(逐次尝试)
19
-
20
- 1. **抓取登录页** → 解析表单与验证码 URL(带会话 cookie)
21
- 2. **抓取验证码图片**(带时间戳防缓存,150x60 PNG)
22
- 3. **模型推理** → 输出 4 位数字
23
- 4. **提交登录**(带会话 cookie + 表单隐藏字段 + 识别结果)
24
- 5. **读论坛响应** → 成功 / "认证码不正确"
25
- 6. **清空本地会话** → 下一次以全新游客身份开始
26
-
27
- 保证:
28
- - 每张验证码绑定独立会话,识别与提交严格一一对应,无会话错位
29
- - 失败响应用 `SP_DEBUG_LOGIN` 落盘逐条核对,确认为真实"认证码不正确",而非网络/风控
30
- - 网络层干扰不计入统计
31
-
32
- ### 测试环境
33
-
34
- | 项 | 值 |
35
- |---|---|
36
- | 站点 | south-plus.net |
37
- | 网络 | 家庭 IP + Cloudflare IP 钉定(绕过 DNS 污染) |
38
- | 请求间隔 | 7 秒(尊重论坛限流) |
39
- | 账号 | 真实有效账号 |
40
- | 模型 | captcha_1000_raw.onnx (88.6% val) |
41
-
42
- ### 结果
43
-
44
- **100 次登录(最终正式测试)**
45
-
46
- | 结果 | 次数 |
47
- |---|---|
48
- | 登录成功 | **85** |
49
- | 认证码不正确 | 12 |
50
- | 被论坛风控拦截(不计入) | 3 |
51
- | 合计有效 | **97** |
52
-
53
- **单次命中率 = 85/97 = 87.6%**
54
-
55
- 早期小样本:15 次测试 13/15 = 86.7%(供参考)。
56
-
57
- **重试收益**:`P(6次内至少成功1次) = 1 - (1 - 0.876)^6 ≈ 99.9997%`,即平均 1 万次登录中约 3 次会在 6 次重试内全部失败。
58
-
59
- ### 置信度
60
-
61
- - 100 次大样本 + 每次二元硬判定 → 统计置信度高
62
- - 与 15 次小样本(86.7%)高度一致 → 结果稳定
63
- - 留出验证集(79 张)88.6% 与登录命中 87.6% 接近 → 交叉印证
64
-
65
- ---
66
-
67
- ## English
68
-
69
- This document explains how the **87.6% login success rate** was measured, so the result is reproducible and trustworthy.
70
-
71
- ### Core principle
72
-
73
- There is no public ground-truth dataset for this captcha, so we use **real forum logins** as the sole, 100%-reliable judge:
74
-
75
- > **"Login success" from the forum = the model was correct**
76
- > **"Code incorrect or expired" = the model was wrong**
77
-
78
- Every attempt is a clean binary verdict — no subjective estimation.
79
-
80
- ### Test flow (per attempt)
81
-
82
- 1. Fetch the login page → parse the form and captcha URL (with session cookie)
83
- 2. Fetch the captcha image (timestamp-cache-busted, 150x60 PNG)
84
- 3. Model inference → 4 digits
85
- 4. Submit login (session cookie + hidden form fields + the code)
86
- 5. Read the forum response → success / "code incorrect"
87
- 6. Clear the local session → next attempt starts as a fresh guest
88
-
89
- Guarantees:
90
- - Each captcha is bound to its own session; recognition and submission are 1:1, no session mismatch
91
- - Failed responses are dumped via `SP_DEBUG_LOGIN` and checked one by one to confirm genuine "code incorrect", not network/rate-limit pages
92
- - Network-level interference is excluded from statistics
93
-
94
- ### Test environment
95
-
96
- | Item | Value |
97
- |---|---|
98
- | Site | south-plus.net |
99
- | Network | home IP + Cloudflare IP pinning (bypasses DNS poisoning) |
100
- | Request interval | 7 seconds (respecting forum rate limits) |
101
- | Account | a real, valid account |
102
- | Model | captcha_1000_raw.onnx (88.6% val) |
103
-
104
- ### Results
105
-
106
- **100 logins (final formal test)**
107
-
108
- | Result | Count |
109
- |---|---|
110
- | Login success | **85** |
111
- | Code incorrect | 12 |
112
- | Rate-limited (excluded) | 3 |
113
- | Valid total | **97** |
114
-
115
- **Per-attempt success = 85/97 = 87.6%**
116
-
117
- Early small sample: 13/15 = 86.7% (for reference).
118
-
119
- **With retries**: `P(success within 6 tries) = 1 - (1 - 0.876)^6 ≈ 99.9997%` — about 3 in 10,000 logins fail after 6 retries.
120
-
121
- ### Confidence
122
-
123
- - 100-attempt sample with hard binary verdicts → high statistical confidence
124
- - Consistent with the 15-attempt sample (86.7%) → stable result
125
- - Held-out validation set (79 images) at 88.6% cross-confirms the login figure (87.6%)
126
-
127
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/INFERENCE.md DELETED
@@ -1,253 +0,0 @@
1
- # 推理指南 / Inference Guide
2
-
3
- ---
4
-
5
- ## 中文
6
-
7
- ### 规格速览
8
-
9
- | 项 | 值 |
10
- |---|---|
11
- | 输入名 | `input` |
12
- | 输入形状 | `[batch, 3, 64, 160]` |
13
- | 输入类型 | float32, 值域 [0,1] (RGB) |
14
- | 输出名 | `logits` |
15
- | 输出形状 | `[batch, 4, 10]` |
16
- | 解码 | 每位置 argmax → 数字 |
17
- | ONNX opset | 18 |
18
-
19
- 预处理只有 3 步,无其他:RGB → 缩放到 160x64(双线性)→ 除以 255。
20
- 不做灰度、不去噪、不做均值方差归一化。
21
-
22
- ### Python
23
-
24
- ```bash
25
- pip install onnxruntime pillow numpy
26
- ```
27
-
28
- ```python
29
- import numpy as np
30
- import onnxruntime as ort
31
- from PIL import Image
32
-
33
- _SESSION = None
34
-
35
- def get_session(model_path="model.onnx"):
36
- global _SESSION
37
- if _SESSION is None:
38
- _SESSION = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
39
- return _SESSION
40
-
41
- def solve_captcha(img_bytes_or_path, model_path="model.onnx"):
42
- sess = get_session(model_path)
43
- im = Image.open(img_bytes_or_path).convert("RGB").resize((160, 64), Image.BILINEAR)
44
- x = np.asarray(im, dtype=np.float32) / 255.0
45
- x = x.transpose(2, 0, 1)[None] # (1,3,64,160)
46
- logits = sess.run(None, {"input": x})[0] # (1,4,10)
47
- return "".join(str(int(logits[0, p].argmax())) for p in range(4))
48
-
49
- code = solve_captcha("captcha.png") # "4821"
50
- ```
51
-
52
- ### Go (onnxruntime_go)
53
-
54
- 依赖:`github.com/yalue/onnxruntime_go` + onnxruntime 共享库(.dylib/.so)。
55
-
56
- ```go
57
- package main
58
-
59
- import (
60
- "bytes"
61
- "fmt"
62
- "image"
63
- _ "image/png"
64
-
65
- "golang.org/x/image/draw"
66
- ort "github.com/yalue/onnxruntime_go"
67
- )
68
-
69
- const (
70
- inW, inH = 160, 64
71
- nDigits = 4
72
- )
73
-
74
- type Solver struct {
75
- sess *ort.Session[float32]
76
- in *ort.Tensor[float32]
77
- out *ort.Tensor[float32]
78
- }
79
-
80
- func NewSolver(modelPath, libPath string) (*Solver, error) {
81
- if libPath != "" { ort.SetSharedLibraryPath(libPath) }
82
- if err := ort.InitializeEnvironment(); err != nil { return nil, err }
83
- in, err := ort.NewEmptyTensor[float32](ort.NewShape(1, 3, inH, inW))
84
- if err != nil { return nil, err }
85
- out, err := ort.NewEmptyTensor[float32](ort.NewShape(1, nDigits, 10))
86
- if err != nil { in.Destroy(); return nil, err }
87
- sess, err := ort.NewSession[float32](modelPath,
88
- []string{"input"}, []string{"logits"},
89
- []*ort.Tensor[float32]{in}, []*ort.Tensor[float32]{out})
90
- if err != nil { in.Destroy(); out.Destroy(); return nil, err }
91
- return &Solver{sess, in, out}, nil
92
- }
93
-
94
- func (s *Solver) Solve(png []byte) (string, error) {
95
- src, _, err := image.Decode(bytes.NewReader(png))
96
- if err != nil { return "", err }
97
- dst := image.NewRGBA(image.Rect(0, 0, inW, inH))
98
- draw.ApproxBiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
99
-
100
- data := s.in.GetData()
101
- idx := 0
102
- // 模型输入为 NCHW:先写全部 R,再 G,再 B
103
- for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { r,_,_,_ := dst.At(x,y).RGBA(); data[idx]=float32(r>>8)/255; idx++ } }
104
- for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { _,g,_,_ := dst.At(x,y).RGBA(); data[idx]=float32(g>>8)/255; idx++ } }
105
- for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { _,_,b,_ := dst.At(x,y).RGBA(); data[idx]=float32(b>>8)/255; idx++ } }
106
-
107
- if err := s.sess.Run(); err != nil { return "", err }
108
- got := s.out.GetData()
109
- code := make([]byte, nDigits)
110
- for p := 0; p < nDigits; p++ {
111
- best := 0
112
- for c := 1; c < 10; c++ { if got[p*10+c] > got[p*10+best] { best = c } }
113
- code[p] = '0' + byte(best)
114
- }
115
- return string(code), nil
116
- }
117
- ```
118
-
119
- > 注意:`onnxruntime_go.NewSession` 从文件读模型。若用 `go:embed` 内嵌模型,
120
- > 改用 `NewSessionWithONNXData` 传字节。
121
-
122
- ### 性能
123
-
124
- - 单张推理:约 5-15ms(CPU,M1/现代 x86)
125
- - 全流程(加载+预处理+推理):约 10-30ms
126
- - 无 GPU 依赖,内存占用 < 50MB
127
-
128
- ---
129
-
130
- ## English
131
-
132
- ### Specification
133
-
134
- | Item | Value |
135
- |---|---|
136
- | Input name | `input` |
137
- | Input shape | `[batch, 3, 64, 160]` |
138
- | Input type | float32, range [0,1] (RGB) |
139
- | Output name | `logits` |
140
- | Output shape | `[batch, 4, 10]` |
141
- | Decode | argmax per position → digit |
142
- | ONNX opset | 18 |
143
-
144
- Preprocessing is only three steps: RGB → resize to 160x64 (bilinear) → divide by 255.
145
- No grayscale, no denoising, no mean/std normalization.
146
-
147
- ### Python
148
-
149
- ```bash
150
- pip install onnxruntime pillow numpy
151
- ```
152
-
153
- ```python
154
- import numpy as np
155
- import onnxruntime as ort
156
- from PIL import Image
157
-
158
- _SESSION = None
159
-
160
- def get_session(model_path="model.onnx"):
161
- global _SESSION
162
- if _SESSION is None:
163
- _SESSION = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
164
- return _SESSION
165
-
166
- def solve_captcha(img_bytes_or_path, model_path="model.onnx"):
167
- sess = get_session(model_path)
168
- im = Image.open(img_bytes_or_path).convert("RGB").resize((160, 64), Image.BILINEAR)
169
- x = np.asarray(im, dtype=np.float32) / 255.0
170
- x = x.transpose(2, 0, 1)[None] # (1,3,64,160)
171
- logits = sess.run(None, {"input": x})[0] # (1,4,10)
172
- return "".join(str(int(logits[0, p].argmax())) for p in range(4))
173
-
174
- code = solve_captcha("captcha.png") # "4821"
175
- ```
176
-
177
- ### Go (onnxruntime_go)
178
-
179
- Deps: `github.com/yalue/onnxruntime_go` + the onnxruntime shared library (.dylib/.so).
180
-
181
- ```go
182
- package main
183
-
184
- import (
185
- "bytes"
186
- "fmt"
187
- "image"
188
- _ "image/png"
189
-
190
- "golang.org/x/image/draw"
191
- ort "github.com/yalue/onnxruntime_go"
192
- )
193
-
194
- const (
195
- inW, inH = 160, 64
196
- nDigits = 4
197
- )
198
-
199
- type Solver struct {
200
- sess *ort.Session[float32]
201
- in *ort.Tensor[float32]
202
- out *ort.Tensor[float32]
203
- }
204
-
205
- func NewSolver(modelPath, libPath string) (*Solver, error) {
206
- if libPath != "" { ort.SetSharedLibraryPath(libPath) }
207
- if err := ort.InitializeEnvironment(); err != nil { return nil, err }
208
- in, err := ort.NewEmptyTensor[float32](ort.NewShape(1, 3, inH, inW))
209
- if err != nil { return nil, err }
210
- out, err := ort.NewEmptyTensor[float32](ort.NewShape(1, nDigits, 10))
211
- if err != nil { in.Destroy(); return nil, err }
212
- sess, err := ort.NewSession[float32](modelPath,
213
- []string{"input"}, []string{"logits"},
214
- []*ort.Tensor[float32]{in}, []*ort.Tensor[float32]{out})
215
- if err != nil { in.Destroy(); out.Destroy(); return nil, err }
216
- return &Solver{sess, in, out}, nil
217
- }
218
-
219
- func (s *Solver) Solve(png []byte) (string, error) {
220
- src, _, err := image.Decode(bytes.NewReader(png))
221
- if err != nil { return "", err }
222
- dst := image.NewRGBA(image.Rect(0, 0, inW, inH))
223
- draw.ApproxBiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
224
-
225
- data := s.in.GetData()
226
- idx := 0
227
- // NCHW layout: all R, then all G, then all B
228
- for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { r,_,_,_ := dst.At(x,y).RGBA(); data[idx]=float32(r>>8)/255; idx++ } }
229
- for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { _,g,_,_ := dst.At(x,y).RGBA(); data[idx]=float32(g>>8)/255; idx++ } }
230
- for y := 0; y < inH; y++ { for x := 0; x < inW; x++ { _,_,b,_ := dst.At(x,y).RGBA(); data[idx]=float32(b>>8)/255; idx++ } }
231
-
232
- if err := s.sess.Run(); err != nil { return "", err }
233
- got := s.out.GetData()
234
- code := make([]byte, nDigits)
235
- for p := 0; p < nDigits; p++ {
236
- best := 0
237
- for c := 1; c < 10; c++ { if got[p*10+c] > got[p*10+best] { best = c } }
238
- code[p] = '0' + byte(best)
239
- }
240
- return string(code), nil
241
- }
242
- ```
243
-
244
- > Note: `onnxruntime_go.NewSession` reads the model from a file. If you embed it with
245
- > `go:embed`, use `NewSessionWithONNXData` with the bytes instead.
246
-
247
- ### Performance
248
-
249
- - Single inference: ~5-15ms (CPU, M1/modern x86)
250
- - Full flow (load + preprocess + infer): ~10-30ms
251
- - No GPU needed, memory < 50MB
252
-
253
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/TRAINING.md DELETED
@@ -1,181 +0,0 @@
1
- # 训练与微调 / Training & Fine-tuning
2
-
3
- ---
4
-
5
- ## 中文
6
-
7
- ### 环境
8
-
9
- ```bash
10
- pip install torch onnx onnxscript pillow numpy opencv-python
11
- ```
12
-
13
- ### 从零训练
14
-
15
- **1. 准备数据**
16
-
17
- 标签格式 `labels.json`:
18
- ```json
19
- {
20
- "captcha_001.png": {"label": "4821"},
21
- "captcha_002.png": {"label": "9037"}
22
- }
23
- ```
24
-
25
- 图片目录:150x60 左右的 RGB 验证码 PNG,文件名与标签键一致。
26
-
27
- **2. 训练**
28
-
29
- ```bash
30
- python scripts/train_fixed.py \
31
- <labels.json> <图片目录> <输出.onnx> \
32
- 0.08 # val 比例
33
- 500 # epochs
34
- 1 # 增强开关 (1=开)
35
- rgb # 输入 RGB (关键: 不要用灰度)
36
- ```
37
-
38
- 示例:
39
- ```bash
40
- python scripts/train_fixed.py \
41
- data/1000_labels.json data/1000_raw \
42
- models/my_model.onnx \
43
- 0.08 500 1 rgb
44
- ```
45
-
46
- 产物:
47
- - `<输出>.onnx` — 最终 ONNX 模型
48
- - `<输出>.onnx.pt` — 最优 epoch 的 PyTorch 权重 (best val)
49
-
50
- **3. 推理验证**
51
-
52
- ```python
53
- import numpy as np, onnxruntime as ort
54
- from PIL import Image
55
- sess = ort.InferenceSession("my_model.onnx", providers=["CPUExecutionProvider"])
56
- im = Image.open("captcha.png").convert("RGB").resize((160,64), Image.BILINEAR)
57
- x = np.asarray(im, dtype=np.float32).transpose(2,0,1)[None] / 255.0
58
- logits = sess.run(None, {"input": x})[0]
59
- code = "".join(str(int(logits[0,p].argmax())) for p in range(4))
60
- ```
61
-
62
- ### 微调 (Fine-tune)
63
-
64
- 从 checkpoint 续训(`train_fixed.py` 检测到 `<输出>.onnx.pt` 存在会自动加载权重):
65
-
66
- ```bash
67
- cp models/captcha_1000_raw.onnx.pt models/finetuned.onnx.pt
68
- python scripts/train_fixed.py <新标签.json> <新图目录> models/finetuned.onnx 0.08 200 1 rgb
69
- ```
70
-
71
- - 只标少量新样本(50-200 张)即可适配小幅变化
72
- - 生成器变化大时建议新旧数据混合训练,避免灾难性遗忘
73
- - 保持同样的预处理(RGB 160x64 /255,不去噪)和增强配置
74
-
75
- ### 参数说明
76
-
77
- | 参数 | 默认 | 说明 |
78
- |---|---|---|
79
- | val_frac | 0.1 | 验证集比例 |
80
- | epochs | 400 | 总训练轮数 |
81
- | aug | 1 | 增强开关 |
82
- | rgb | - | 传 `rgb` 启用 3 通道输入(推荐) |
83
-
84
- ### 关键经验(踩过的坑)
85
-
86
- 1. **必须用 RGB,不要灰度** — 验证码数字是彩色的,灰度丢信息。
87
- 2. **不要图像去噪** — 形态学开运算砍掉细笔画,val 从 88.6% 掉到 79.8%。数据量足够时模型自己学会过滤干扰线。
88
- 3. **位置保留架构,不要 Global Average Pooling** — GAP 丢空间位置导致 val 归零;按列分组池化(20列→4位置)后立即泛化。
89
- 4. **人类标注 >> 视觉模型自动标注** — 自动标注 ~35% 噪声会把模型带崩到 0% 命中;手工 100% 干净标签才能学到真实分布。
90
- 5. **增强不要加高斯噪声** — 验证码本身有干扰线,再加随机噪声会让训练样本面目全非。
91
-
92
- ---
93
-
94
- ## English
95
-
96
- ### Environment
97
-
98
- ```bash
99
- pip install torch onnx onnxscript pillow numpy opencv-python
100
- ```
101
-
102
- ### Training from scratch
103
-
104
- **1. Prepare data**
105
-
106
- Label format `labels.json`:
107
- ```json
108
- {
109
- "captcha_001.png": {"label": "4821"},
110
- "captcha_002.png": {"label": "9037"}
111
- }
112
- ```
113
-
114
- Image dir: ~150x60 RGB captcha PNGs; filenames must match label keys.
115
-
116
- **2. Train**
117
-
118
- ```bash
119
- python scripts/train_fixed.py \
120
- <labels.json> <image_dir> <output.onnx> \
121
- 0.08 # val fraction
122
- 500 # epochs
123
- 1 # augmentation on/off (1=on)
124
- rgb # RGB input (important: do NOT use grayscale)
125
- ```
126
-
127
- Example:
128
- ```bash
129
- python scripts/train_fixed.py \
130
- data/1000_labels.json data/1000_raw \
131
- models/my_model.onnx \
132
- 0.08 500 1 rgb
133
- ```
134
-
135
- Outputs:
136
- - `<output>.onnx` — final ONNX model
137
- - `<output>.onnx.pt` — best-epoch PyTorch weights
138
-
139
- **3. Verify inference**
140
-
141
- ```python
142
- import numpy as np, onnxruntime as ort
143
- from PIL import Image
144
- sess = ort.InferenceSession("my_model.onnx", providers=["CPUExecutionProvider"])
145
- im = Image.open("captcha.png").convert("RGB").resize((160,64), Image.BILINEAR)
146
- x = np.asarray(im, dtype=np.float32).transpose(2,0,1)[None] / 255.0
147
- logits = sess.run(None, {"input": x})[0]
148
- code = "".join(str(int(logits[0,p].argmax())) for p in range(4))
149
- ```
150
-
151
- ### Fine-tuning
152
-
153
- `train_fixed.py` auto-loads an existing `<output>.onnx.pt` and resumes from it:
154
-
155
- ```bash
156
- cp models/captcha_1000_raw.onnx.pt models/finetuned.onnx.pt
157
- python scripts/train_fixed.py <new_labels.json> <new_image_dir> models/finetuned.onnx 0.08 200 1 rgb
158
- ```
159
-
160
- - 50-200 new labeled samples are enough for small changes.
161
- - Mix old + new data when the generator changes a lot, to avoid catastrophic forgetting.
162
- - Keep the same preprocessing (RGB 160x64 /255, no denoise) and augmentation.
163
-
164
- ### Parameters
165
-
166
- | Param | Default | Description |
167
- |---|---|---|
168
- | val_frac | 0.1 | validation fraction |
169
- | epochs | 400 | total epochs |
170
- | aug | 1 | augmentation toggle |
171
- | rgb | - | pass `rgb` for 3-channel input (recommended) |
172
-
173
- ### Key lessons
174
-
175
- 1. **Use RGB, not grayscale** — the digits are colored; grayscale loses information.
176
- 2. **Do NOT denoise** — morphological opening removes thin strokes, dropping val from 88.6% to 79.8%. With enough data the model learns to ignore noise lines itself.
177
- 3. **Position-preserving architecture, never global average pooling** — GAP destroys spatial position (val → 0%); per-column grouped pooling (20 cols → 4 positions) restores generalization.
178
- 4. **Human labels >> auto vision labels** — auto labels with ~35% noise crashed the model to 0%; clean manual labels are essential.
179
- 5. **No Gaussian noise augmentation** — the captcha already has noise lines; adding more obscures the training signal.
180
-
181
- ---