Или как я потратила выходные на доказательство временного парадокса: 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%, но всё равно круто :) ¯\_(ツ)_/¯

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