Или как я потратила некоторое время на доказательство временного парадокса: 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-х были ИИ-способными. Мы просто не знали как.

Выводы

  1. Ограничения рождают инновации. Отсутствие умножения заставило придумать новую архитектуру.

  2. Старое железо != бесполезное железо. Ваш музейный экспонат может быть спящим ИИ.

  3. Алгоритмы важнее железа. 49 лет мы думали, что Z80 слишком слаб. Оказалось, мы были слишком глупы.

  4. Документируйте всё. 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. Просьба помочь с проверкой и тестированием результатов на разных датасетах =)

Комментарии (3)


  1. oisee Автор
    18.07.2025 12:33

    Ну что это может значить на практике?

    inference можно производить на edge-девайсах =)
    наверное что-то ещё можно придумать.

    Давайте трансформер к bCNN сведём и заживём!


  1. oisee Автор
    18.07.2025 12:33

    А ещё CPU быстрее в битовых операциях чем GPU => для многих алгоритмов GPU может быть и не нужен вовсе (экономия! как по энергопотреблению, так и по всяким другим параметрам!)


  1. Flammmable
    18.07.2025 12:33

    Год регистрации: 2007. Ого!