Или как я потратила выходные на доказательство временного парадокса: Z80 1976 года решает CAPTCHA 2010-х в 2025 году
Вступление
Представьте: вы открываете сундук и находите пыльный ZX Spectrum. «В музей Яндекса», — думаете вы. А что если я скажу, что эта железка с 48 килобайтами памяти может с 95.5% точностью распознавать рукописные цифры и проходить те самые CAPTCHA-тесты «Я не робот» из 2010-х?
Более того: технически она могла это делать с момента выпуска в 1982 году.
<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 статей документации (да, я немного ёкнулась на документировании процесса).
(Карта для навигации по этим статьям: https://github.com/oisee/mnist-z80/blob/master/META_JOURNEY_MAP.md )
Главная проблема: 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 примеров (всего набор MNIST-z80 состоит из 15k образцов).
Философский вопрос
Если компьютер 1982 года может доказать, что он «не робот» сайтам 2010 года... что вообще означает слово «интеллект»?
Получается, тест Тьюринга — это не о том, как машины становятся людьми. Это о том, как мы обнаруживаем, что они всегда ими могли быть ^_^
Как повторить эксперимент
Требования
Python 3.8+ с NumPy и scikit-learn
Golang
Эмулятор Spectrum (Fuse, SpecEmu) или реальное железо
sjasmplus для сборки Z80 кода
Быстрый старт
# Клонировать репозиторий
git clone https://github.com/oisee/mnist-z80
cd mnist-z80
go build z80_mnist_demo.go
./z80_mnist_demo
Структура проекта
mnist-z80/
│ ├── META_JOURNEY_MAP.md # 70 статей - вся история
│ └── ALGORITHM_DETAILED.md # Подробности алгоритмов
├── src/training/
│ ├── train_fuzzy_majority.py # Обучение модели
│ └── validate_accuracy.py # Проверка точности
├── z80/
│ ├── fuzzy_match.asm # Нечёткое сравнение
│ ├── majority_vote.asm # Голосование
│ └── popcount_lut.asm # Таблица popcount
*...
Что дальше?
Работа идёт над портированием на другие 8-битные системы, а также над интерактивной демой для zx:
ZX Spectrum (z80) — интерактивная демонстрация алгоритма.
Apple II (6502) — другая архитектура, те же принципы
Commodore 64 (6510) — 64КБ для экспериментов
БК-0010 (К1801ВМ1) — советская 16-битная PDP-11 совместимая
Атари 7800 (6502C) — игровая консоль как ИИ-платформа
Каждый порт доказывает, что все домашние компьютеры конца 70-х были ИИ-способными. (Особенно при условии наличия достаточного объёма памяти.)
Исходники: github.com/oisee/mnist-z80
Хотите портировать на свою любимую ретро-систему? Welcome to pull requests!
P.S. Просьба помочь с проверкой и тестированием результатов на разных датасетах =)
Claude Code - великолепный ускоритель экспериментов, проверка гипотез занимает минуты.
Документация экспериментов также осуществлена с помощью LLM.
N.B. Оригинал статьи на Английском:
https://github.com/oisee/mnist-z80/blob/master/071_ZX_SPECTRUM_PASSES_TURING_TEST.md
N.B. В первоначальном варианте статьи присутствовали артефакты машинного перевода с неточностями и ошибками. Спасибо активным комментаторам, в первую очередь @purplesyringa за помощь в исправлениях ^_^
P.P.S. Кажется результат с 95% был локальным максимумом на подмножестве примеров. Более разнообразная выборка показывает стабилизацию в районе 85%, что звучит не так круто как 95%, но всё равно круто :) ¯\_(ツ)_/¯