Или как я потратила некоторое время на доказательство временного парадокса: 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. Просьба помочь с проверкой и тестированием результатов на разных датасетах =)
Комментарии (2)
oisee Автор
18.07.2025 12:33А ещё CPU быстрее в битовых операциях чем GPU => для многих алгоритмов GPU может быть и не нужен вовсе (экономия! как по энергопотреблению, так и по всяким другим параметрам!)
oisee Автор
Ну что это может значить на практике?
inference можно производить на edge-девайсах =)
наверное что-то ещё можно придумать.
Давайте трансформер к bCNN сведём и заживём!