Coarse stroke-feature preset; log fine-feature results
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
45880f1d19
commit
334bfefa81
3 changed files with 65 additions and 19 deletions
|
|
@ -54,7 +54,12 @@ def main():
|
||||||
ap.add_argument("--test", type=int, default=2000)
|
ap.add_argument("--test", type=int, default=2000)
|
||||||
ap.add_argument("--inhib", type=float, default=255)
|
ap.add_argument("--inhib", type=float, default=255)
|
||||||
ap.add_argument("--features", action="store_true", help="network input is stroke features of the pixels")
|
ap.add_argument("--features", action="store_true", help="network input is stroke features of the pixels")
|
||||||
|
ap.add_argument("--preset", default="fine", help="stroke feature preset")
|
||||||
|
ap.add_argument("--scale", type=float, help="stroke feature scale (as printed by stroke_features.py)")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
|
stroke_features.use_preset(a.preset)
|
||||||
|
if a.scale:
|
||||||
|
stroke_features.P["scale"] = a.scale
|
||||||
w, thr = s.load_dump(a.dump)
|
w, thr = s.load_dump(a.dump)
|
||||||
imgs, labels = s.load_images(a.images, a.labels, a.first, a.test)
|
imgs, labels = s.load_images(a.images, a.labels, a.first, a.test)
|
||||||
imgs = imgs.reshape(-1, 28, 28).astype(np.int32)
|
imgs = imgs.reshape(-1, 28, 28).astype(np.int32)
|
||||||
|
|
|
||||||
|
|
@ -239,3 +239,17 @@ KNP: `aug_v_B30k` 8940/10000 = 89,4% (ориентир `oo_v_base` 92,5%). Ус
|
||||||
| `aug_v_B30k` (10 000 × 3) | 87,8% | **81,5%** | 79,1% | 74,9% |
|
| `aug_v_B30k` (10 000 × 3) | 87,8% | **81,5%** | 79,1% | 74,9% |
|
||||||
|
|
||||||
Сдвиг: +15 п.п.; на чистых картинках −4 п.п. (втрое меньше уникальных исходников); к толщине устойчивость не выросла. Ждём `aug_v_A90k` (30 000 × 3).
|
Сдвиг: +15 п.п.; на чистых картинках −4 п.п. (втрое меньше уникальных исходников); к толщине устойчивость не выросла. Ждём `aug_v_A90k` (30 000 × 3).
|
||||||
|
|
||||||
|
### Страница проверки и слой «детекторы штрихов + пулинг» (прототип вне KNP)
|
||||||
|
- Визуализатор: кнопки сдвига на 1 px и строка «Итог всех сетей» (`viz/app.html`); страница с обычной сетью и аугментацией B — `viz/aug_viz.html`, опубликована https://claude.ai/artifact/Vc1q1WCZuPY2QAHBSGtrKF. Пользователь проверил: нарисованная тройка → «2»; аугментация сдвигами не решает другой почерк.
|
||||||
|
- `features/stroke_features.py`: детекторы штрихов (Gabor-подобные ядра) + max-пулинг, 784 значения на картинку, считается обычным кодом — проверка идеи перед переносом слоя в KNP. Вариант `fine`: 4 направления, пулинг 3×3 шаг 2 → 4×14×14.
|
||||||
|
- `augment/tta_sim.py --features [--preset --scale]` — устойчивость для сетей на признаках (искажение пикселей → признаки → сеть).
|
||||||
|
|
||||||
|
| Сеть (validation, 2000 картинок, симулятор) | Как есть | Сдвиг 2 px | Толще 1 px | Тоньше 1 px |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| пиксели (`oo_v_base`) | 91,9% | 66,6% | 80,8% | 80,2% |
|
||||||
|
| пиксели + аугм. B (`aug_v_B30k`) | 87,8% | 81,5% | 79,1% | 74,9% |
|
||||||
|
| штрихи fine (`f_v_plain`, KNP 86,3%) | 85,2% | 61,4% | 75,1% | 71,6% |
|
||||||
|
| штрихи fine + аугм. B (`f_v_augB`, KNP 82,2%) | 81,0% | 74,8% | 71,9% | 68,6% |
|
||||||
|
|
||||||
|
Вывод: мелкий пулинг не помог ни точности, ни устойчивости (сдвиг 2 px = целая клетка сетки 14×14). Порог не виноват (лучший 85,3% при 60). Запущен вариант `coarse` как у HOG: 8 направлений × 2 толщины, пулинг 5×5 шаг 4 → 16×7×7, масштаб 0,3842 (`fc_v_plain`, `fc_v_augB`).
|
||||||
|
|
|
||||||
|
|
@ -13,24 +13,41 @@ import sys
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
SIDE, ORIENT, POOLED = 28, 4, 14
|
SIDE = 28
|
||||||
SCALE = 3.5 # fixed scale: 99th percentile of pooled responses on MNIST maps to 255
|
# Presets: "fine" — 4 orientations, pool 3×3 stride 2 → 4×14×14;
|
||||||
|
# "coarse" — 8 orientations × 2 stroke widths, pool 5×5 stride 4 → 16×7×7. Both give 784 values.
|
||||||
|
PRESETS = {
|
||||||
|
"fine": {"orient": 4, "sigmas": [1.4], "pool": 3, "stride": 2, "pooled": 14, "scale": 3.5,
|
||||||
|
"size": 5, "wavelength": 4.0, "norm": False},
|
||||||
|
"coarse": {"orient": 8, "sigmas": [1.0, 2.0], "pool": 5, "stride": 4, "pooled": 7, "scale": None,
|
||||||
|
"size": 7, "wavelength": None, "norm": True},
|
||||||
|
}
|
||||||
|
P = dict(PRESETS["fine"])
|
||||||
|
|
||||||
|
|
||||||
def kernels(size=5, sigma=1.4, wavelength=4.0):
|
def kernels():
|
||||||
half = size // 2
|
half = P["size"] // 2
|
||||||
y, x = np.mgrid[-half:half + 1, -half:half + 1].astype(np.float32)
|
y, x = np.mgrid[-half:half + 1, -half:half + 1].astype(np.float32)
|
||||||
out = []
|
out = []
|
||||||
for k in range(ORIENT):
|
for sigma in P["sigmas"]:
|
||||||
t = np.pi * k / ORIENT
|
for k in range(P["orient"]):
|
||||||
xr = x * np.cos(t) + y * np.sin(t)
|
t = np.pi * k / P["orient"]
|
||||||
yr = -x * np.sin(t) + y * np.cos(t)
|
xr = x * np.cos(t) + y * np.sin(t)
|
||||||
g = np.exp(-(xr ** 2 + 0.25 * yr ** 2) / (2 * sigma ** 2)) * np.cos(2 * np.pi * xr / wavelength)
|
yr = -x * np.sin(t) + y * np.cos(t)
|
||||||
out.append(g - g.mean())
|
g = np.exp(-(xr ** 2 + 0.25 * yr ** 2) / (2 * sigma ** 2)) * np.cos(2 * np.pi * xr / (P["wavelength"] or 2.9 * sigma))
|
||||||
|
g = g - g.mean()
|
||||||
|
out.append(g / np.abs(g).sum() if P["norm"] else g)
|
||||||
return np.stack(out)
|
return np.stack(out)
|
||||||
|
|
||||||
|
|
||||||
KERNELS = kernels()
|
def use_preset(name):
|
||||||
|
global KERNELS
|
||||||
|
P.clear()
|
||||||
|
P.update(PRESETS[name])
|
||||||
|
KERNELS = kernels()
|
||||||
|
|
||||||
|
|
||||||
|
KERNELS = None
|
||||||
|
|
||||||
|
|
||||||
def detect(imgs):
|
def detect(imgs):
|
||||||
|
|
@ -38,7 +55,7 @@ def detect(imgs):
|
||||||
x = imgs.astype(np.float32) / 255
|
x = imgs.astype(np.float32) / 255
|
||||||
pad = KERNELS.shape[-1] // 2
|
pad = KERNELS.shape[-1] // 2
|
||||||
xp = np.pad(x, ((0, 0), (pad, pad), (pad, pad)))
|
xp = np.pad(x, ((0, 0), (pad, pad), (pad, pad)))
|
||||||
out = np.zeros((len(x), ORIENT, SIDE, SIDE), dtype=np.float32)
|
out = np.zeros((len(x), len(KERNELS), SIDE, SIDE), dtype=np.float32)
|
||||||
for dy in range(KERNELS.shape[1]):
|
for dy in range(KERNELS.shape[1]):
|
||||||
for dx in range(KERNELS.shape[2]):
|
for dx in range(KERNELS.shape[2]):
|
||||||
patch = xp[:, dy:dy + SIDE, dx:dx + SIDE]
|
patch = xp[:, dy:dy + SIDE, dx:dx + SIDE]
|
||||||
|
|
@ -47,12 +64,14 @@ def detect(imgs):
|
||||||
|
|
||||||
|
|
||||||
def pool(resp):
|
def pool(resp):
|
||||||
"""Max pooling 3×3, stride 2, padding 1: [n, 4, 28, 28] → [n, 4, 14, 14]."""
|
"""Max pooling P['pool']×P['pool'] with stride P['stride']: [n, maps, 28, 28] → [n, maps, pooled, pooled]."""
|
||||||
p = np.pad(resp, ((0, 0), (0, 0), (1, 1), (1, 1)))
|
k, st, n = P["pool"], P["stride"], P["pooled"]
|
||||||
out = np.zeros(resp.shape[:2] + (POOLED, POOLED), dtype=np.float32)
|
pad = (k - st) // 2 + 1
|
||||||
for dy in range(3):
|
p = np.pad(resp, ((0, 0), (0, 0), (pad, pad), (pad, pad)))
|
||||||
for dx in range(3):
|
out = np.zeros(resp.shape[:2] + (n, n), dtype=np.float32)
|
||||||
out = np.maximum(out, p[:, :, dy:dy + 2 * POOLED:2, dx:dx + 2 * POOLED:2])
|
for dy in range(k):
|
||||||
|
for dx in range(k):
|
||||||
|
out = np.maximum(out, p[:, :, dy:dy + st * n:st, dx:dx + st * n:st])
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -60,7 +79,7 @@ def features(imgs, batch=5000):
|
||||||
res = []
|
res = []
|
||||||
for i in range(0, len(imgs), batch):
|
for i in range(0, len(imgs), batch):
|
||||||
f = pool(detect(imgs[i:i + batch]))
|
f = pool(detect(imgs[i:i + batch]))
|
||||||
res.append(np.clip(f * (255 / SCALE), 0, 255).astype(np.uint8).reshape(len(f), -1))
|
res.append(np.clip(f * (255 / P["scale"]), 0, 255).astype(np.uint8).reshape(len(f), -1))
|
||||||
return np.concatenate(res)
|
return np.concatenate(res)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -69,8 +88,16 @@ def main():
|
||||||
ap.add_argument("images")
|
ap.add_argument("images")
|
||||||
ap.add_argument("labels")
|
ap.add_argument("labels")
|
||||||
ap.add_argument("out")
|
ap.add_argument("out")
|
||||||
|
ap.add_argument("--preset", choices=sorted(PRESETS), default="fine")
|
||||||
|
ap.add_argument("--scale", type=float, help="response mapped to 255 (default: preset value or 99th percentile)")
|
||||||
a = ap.parse_args()
|
a = ap.parse_args()
|
||||||
|
use_preset(a.preset)
|
||||||
imgs = np.fromfile(a.images, dtype=np.uint8).reshape(-1, SIDE, SIDE)
|
imgs = np.fromfile(a.images, dtype=np.uint8).reshape(-1, SIDE, SIDE)
|
||||||
|
if a.scale:
|
||||||
|
P["scale"] = a.scale
|
||||||
|
elif P["scale"] is None:
|
||||||
|
P["scale"] = float(np.percentile(pool(detect(imgs[:2000])), 99))
|
||||||
|
print(f"scale (99th percentile of first 2000 images): {P['scale']:.4g}", file=sys.stderr)
|
||||||
f = features(imgs)
|
f = features(imgs)
|
||||||
f.tofile(a.out + ".bin")
|
f.tofile(a.out + ".bin")
|
||||||
shutil.copyfile(a.labels, a.out + ".target")
|
shutil.copyfile(a.labels, a.out + ".target")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue