Delete files docs/TRAINING.md docs/INFERENCE.md docs/EVALUATION.md with huggingface_hub
Browse files- docs/EVALUATION.md +0 -127
- docs/INFERENCE.md +0 -253
- 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 |
-
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|