Или как я потратила некоторое время на доказательство временного парадокса: Z80 1976 года решает CAPTCHA 2010-х в 2025 году
Вступление
Представьте: вы открываете чердак и находите пыльный ZX Spectrum. «Музейный экспонат», — думаете вы. А что если я скажу, что эта коробка с 48 килобайтами памяти может с 95.5% точностью распознавать рукописные цифры и проходить те самые CAPTCHA-тесты «Я не робот» из 2010-х?
Более того: технически она могла это делать с момента выпуска в 1982 году. Мы просто не знали правильный алгоритм 43 года.
<cut />
Временной парадокс в трёх актах
1976: Рождение героя
Компания Zilog выпускает процессор Z80. 8-битный, 3.5 МГц, набор инструкций включает AND, XOR, ADD. Никто не подозревает, что этого достаточно для нейросетей.
2010-2015: Появление врага
Веб-сайты начинают использовать CAPTCHA с искажёнными цифрами. «Докажите, что вы человек». Порог прохождения — около 70% точности распознавания.
2025: Разрешение парадокса
Я доказываю, что любой компьютер с Z80 (ZX Spectrum, Amstrad CPC, MSX) может проходить эти тесты. Железо было готово с 1976 года. Не хватало только… алгоритма.
Путешествие: от 9.3% к 95.5%
График эволюции точности выглядит как американские горки:
Точность | Что произошло ---------|-------------------------------------------------- 9.3% | Наивные правила: "много пикселей внизу = цифра 2" 50.1% | Прорыв: обучение с учителем заработало 65.6% | Sparse binary features (автоматические AND-комбинации) 70.9% | Больше данных + L2-регуляризация 75.5% | Сделала Z80-совместимой (самый сложный этап!) 83.0% | Революция: fuzzy matching через XOR+popcount 95.5% | Финал: простое голосование 9 перспектив
Каждый процент — это куча экспериментов. Всего получилось 70 статей документации (да, я немного ёкнулась на документировании процесса).
Главная проблема: Z80 не умеет умножать
Традиционные нейросети используют логистическую регрессию:
probability = 1 / (1 + exp(-score)) # Z80: "Что такое exp()?"
У Z80 нет инструкций для:
-
Умножения (MUL)
-
Деления (DIV)
-
Экспоненты (EXP)
-
Логарифма (LOG)
Решение: ансамбль линейных регрессий
Вместо одной логистической модели я создала 10 линейных (по одной на цифру):
# Традиционный подход (нужны умножения): score = w0*x0 + w1*x1 + w2*x2 + ... + b # Мой подход (только сложения): score = b for i in range(len(features)): if features[i] == 1: # Бинарный признак score += weights[i]
Использование исключительно бинарных признаков (0 или 1) превращает умножение в условное сложение!
Архитектура: как уместить нейросеть в 48КБ
Структура сети
Вход: 16×16 бинарное изображение ↓ [Скользящие окна] Слой 1: 594 признака • 169 окон 4×4 • 196 окон 3×3 ↓ [Магическое соотношение] Слой 2: 384 признака (55% AND + 45% XOR пар) ↓ [Только AND] Слой 3: 256 признаков ↓ [Только AND] Слой 4: 128 признаков ↓ [Линейный классификатор] Выход: 10 оценок → argmax
Итого: 1,362 бинарных признака, веса в int16, всё помещается в 28КБ.
«Совиный алгоритм»
Вдохновившись тем, как совы поворачивают голову для лучшего обзора, я реализовала просмотр с 9 ракурсов:
(-1,-1) (-1,0) (-1,+1) ( 0,-1) ( 0,0) ( 0,+1) (+1,-1) (+1,0) (+1,+1)
Каждый сдвиг голосует за свою цифру. Побеждает большинство. Удивительно, но простое голосование работает лучше взвешенного!
Ключевые трюки для Z80
1. Popcount через таблицу поиска
; Подсчёт единичных битов за O(1) ; Вход: A = байт ; Выход: A = количество единиц POPCOUNT_LUT: EQU $C000 ; Выровнено на границу страницы popcount: LD H,POPCOUNT_LUT>>8 ; Старший байт адреса LD L,A ; Байт как индекс LD A,(HL) ; Результат одной командой! RET ; Таблица 256 байт с предвычисленными значениями ; Адрес $C000 выбран для скорости доступа
2. Fuzzy matching (нечёткое сравнение)
; Традиционно: паттерн совпал, если ВСЕ биты равны ; Fuzzy: паттерн совпал, если различаются ≤2 бита check_pattern: LD A,(window) ; Текущее окно 4×4 XOR (HL) ; XOR с эталонным паттерном CALL popcount ; Сколько битов отличается? CP 3 ; Сравнить с порогом+1 RET C ; C=1 если ≤2 различия (совпадение!)
3. Линейная регрессия без умножений
; score = intercept + sum(weights[i] где features[i]==1) ; Веса хранятся как int16 с масштабом 1024 compute_score: LD HL,(intercept) ; Начальное смещение LD IX,features ; Указатель на признаки LD IY,weights ; Указатель на веса LD BC,1362 ; Количество признаков .loop: LD A,(IX+0) ; Загрузить признак OR A ; Это 0? JR Z,.skip ; Да - пропустить вес ; Добавить вес к счёту (16 бит) LD E,(IY+0) LD D,(IY+1) ADD HL,DE ; score += weight .skip: INC IX ; Следующий признак INC IY INC IY ; Следующий вес (16 бит) DEC BC LD A,B OR C JR NZ,.loop ; HL = финальная оценка для цифры RET
Результаты: Давид vs Голиаф
|
Параметр |
SGI Octane 1998 |
ZX Spectrum 1982 |
|---|---|---|
|
Процессор |
MIPS R10000 @ 250МГц |
Z80 @ 3.5МГц |
|
RAM |
512МБ |
48КБ |
|
Цена |
$30,000 |
£175 |
|
Точность MNIST |
98% |
95.5%* |
|
Может пройти CAPTCHA |
Конечно |
Тоже да! |
|
Потребление |
~100Вт |
<2Вт |
*На валидационном наборе из 3000 примеров
Философский вопрос
Если компьютер 1982 года может доказать, что он «не робот» сайтам 2010 года… что вообще означает слово «интеллект»?
Получается, тест Тьюринга — это не о том, как машины становятся людьми. Это о том, как мы обнаруживаем, что они всегда ими могли быть. Просто не хватало правильной программы.
Как повторить мой эксперимент
Требования
-
Python 3.8+ с NumPy и scikit-learn
-
Эмулятор Spectrum (Fuse, SpecEmu) или реальное железо
-
sjasmplus для сборки Z80 кода
-
Терпение и любовь к ретро-технике
Быстрый старт
# Клонировать репозиторий git clone https://github.com/oisee/mnist-z80 cd mnist-z80 # Обучить модель и создать веса для Z80 python train_fuzzy_majority.py python export_z80_weights.py # Собрать для Spectrum sjasmplus zx_mnist_demo.asm # Запустить в эмуляторе fuse mnist_demo.tap
Структура проекта
mnist-z80/ ├── docs/ │ ├── META_JOURNEY_MAP.md # 70 статей - вся история │ └── ALGORITHM_DETAILED.md # Подробности алгоритмов ├── python/ │ ├── train_fuzzy_majority.py # Обучение модели │ └── validate_accuracy.py # Проверка точности ├── z80/ │ ├── fuzzy_match.asm # Нечёткое сравнение │ ├── majority_vote.asm # Голосование │ └── popcount_lut.asm # Таблица popcount └── models/ └── weights_int16.bin # Веса в формате Z80
Что дальше?
Сейчас я работаю над портированием на другие 8-битные системы:
-
Apple II (6502) — другая архитектура, те же принципы
-
Commodore 64 (6510) — 64КБ для экспериментов!
-
БК-0010 (К1801ВМ1) — советская 16-битная PDP-11 совместимая
-
Атари 800 (6502) — игровая консоль как ИИ-платформа
Каждый порт доказывает: ВСЕ компьютеры конца 70-х были ИИ-способными. Мы просто не знали как.
Выводы
-
Ограничения рождают инновации. Отсутствие умножения заставило придумать новую архитектуру.
-
Старое железо != бесполезное железо. Ваш музейный экспонат может быть спящим ИИ.
-
Алгоритмы важнее железа. 49 лет мы думали, что Z80 слишком слаб. Оказалось, мы были слишком глупы.
-
Документируйте всё. 70 статей может показаться избыточным, но каждая фиксирует важный шаг.
P.S. Ответы на ожидаемые вопросы
Q: Это правда работает на реальном железе? A: Да! Проверено на нескольких ZX Spectrum 48K. Загрузка с ленты занимает ~3 минуты.
Q: Почему не 98% как у LeNet? A: Потому что никаких умножений! Ridge-регрессия вместо логистической стоит ~7% точности.
Q: Можно ли улучшить? A: Теоретический предел для этого подхода ~85% на полном тесте. Но 95.5% хватает для CAPTCHA!
Q: Где взять обученные веса? A: В репозитории есть предобученная модель. Можно сразу собрать и запустить.
Исходники: github.com/oisee/mnist-z80
Хотите портировать на свою любимую ретро-систему? Welcome to pull requests!
P.S. Просьба помочь с проверкой и тестированием результатов на разных датасетах =)
ссылка на оригинал статьи https://habr.com/ru/articles/928964/
Добавить комментарий