Всем привет, это моя первая статья на Хабре и я решил посвятить ее своему недавнему мини‑проекту, сутью которого является обучение небольшого перцептрона 2-5-1 с помощью Python без сторонних библиотек (типа NumPy), и его последующий инференс на непрограммируемом инженерном калькуляторе Casio‑Fx-82-Es Plus (2nd edition).
В качестве задачи для перцептрона я выбрал определение того, находится ли точка в пределах графика следующей лемнискаты Бернулли: (x² + y²)² — 2a²(x² — y²) = 0 (с a = sqrt(0.5), то есть вообще без коэффициента 2a²), с минимально приемлемой вероятностью (70–85%)

Зачем это нужно?
Я всегда интересовался инференсом различных ИИ моделей на предельно слабом оборудовании (по большей части LLM, но об этом в другой статье) и оборудовании с ограничениями по мощности и архитектуре. И в определенный момент я понял, что теоретически для запуска не очень большого перцептрона совершенно не обязательно использовать циклы и напрямую условные операторы, поэтому и решил попробовать. Я думаю что подобные небольшие проекты позволяют задуматься, что мы еще далеки от идеала в вопросах оптимизации софта, в частности в ИИ моделях.
Что я подразумеваю под запуском/инференсом
По моему мнению задача будет выполнена, если в конечном итоге я смогу записать значения в 2 переменных, выполнить 1–2 выражения и получить ответ. Важный момент: выражения должны влезть в буфер последних выполненных выражений (должно остаться место под запись переменных) и должна остаться возможность повторно их использовать.
Технические ограничения
Калькулятор принимает только математические выражения. Построчное программирование, циклы и условные операторы отсутствуют. Для записи пользовательских значений и использования в выражениях доступно 9 переменных. Память для одного выражения всего 99 байт, всего под выражения можно использовать около ~128-150 байт, чтобы хватило места под запись входных данных во входные переменные, без потери выражений.
Важное признание
Я не являюсь специалистом в области машинного обучения и не считаю себя хорошим программистом. Я скорее энтузиаст. Обучение перцептрона я представляю относительно поверхностно и обладаю лишь базовыми знаниями в матанализе, в частности имею лишь общее представление о градиентах и градиентном спуске. Поэтому для написания кода для обучения модели я использовал LLM (DeepSeek), а затем во время попыток обучения модели, я занимался лишь тем, что мне интересно и что я могу нормально сделать, а именно редактировал гипер‑параметры и параметры для генерации датасета, адаптировал выражения, вручную квантовал и оптимизировал их.
Коротко о задаче и архитектуре
Почему именно лемниската Бернулли?
По моему мнению лемниската Бернулли одновременно имеет достаточно интересную и сложную границу, но при этом является достаточно наглядной и поддается отоносительно простой логике, доступной для сети всего с 5 скрытыми нейронами.
Ограничения архитектуры
— 2 входа для координат X и Y
— Скрытый слой на 5 нейронов (были эксперименты на 3 нейрона, но сеть не достаточно хорошо справлялась с задачей)
— 1 выход (вероятность принадлежности точки к лемнискате)
— Функция активации — логистическая сигмоида (1/(1+e‑x))
Архитектура выбиралась по принципу «Чем больше — тем лучше», но запихнуть в скрытый слой еще больше нейронов или добавить еще один слой было бы проблематично, так как памяти и так хватило в упор. Логистическая сигмоида была выбрана, так как неплохо подходит к такой архитектуре, не требует большого количества знаков для написания и не нуждается в прямом использовании условных переходов.
Коротко о генерации данных и обучении
Датасет
Является набором случайных точек в квадрате [-1.5, 1.5] x [-1.5, 1.5]. Метка точки вычисляется по уравнению лемнискаты:
val = (x**2 + y**2)**2 - 1.0 * (x**2 - y**2)
label = 1 if val <= 0 else 0
Я выбрал количество обучающих примеров n=5000 и 500 для тестовых:
train_data = generate_dataset(n=5000)
test_data = generate_dataset(n=500, bias_right=0.5)
Обучение
Чистый Python без библиотек, бинарная кросс‑энтропия, обратное распространение ошибки.
train(train_prepared, w, b, v, b_out, lr=0.01, epochs=10000)
Конечная точность неквантованной модели 96% на обучающих данных и 96.6 на тестовых.
Конечные выражения выглядят так:

Если отразить полученную модель на трехмерном графике, используя входы X и Y в качестве соответствующих осей, а выход в качестве оси Z, то получится следующее:
Считаю, что точность не плохая для такой небольшой модели.
Адаптация к калькулятору
Проблема
Напомню, что размер одного выражения всего 99 байт, а задача была в том, чтобы все поместилось в памяти и запускалось с одной‑двух кнопок, не заставляя пользователя думать в каком порядке что нужно запустить и в какую переменную что нужно записать. А у нас на данный момент 6 выражений, в каждом из которых числа с достаточно большим количеством знаков после запятой.
Упрощение выражений и удаление лишних знаков / битва за байты
Для начала я перенес все выражения в Desmos3D, чтобы во время упрощений не совершить ошибку. Так же в будущем это будет полезно чтобы примерно оценивать влияние квантования конкретных коэффициентов на общую точность.
Для начала перемножаем все минусы, переносим все слагаемые так, чтобы у нас не возникало лишних минусов (то есть ‑a+b → b‑a), удаляем знаки умножения и лишние скобки:

Округление значений, уменьшение количества знаков / впихивание невпихуемого
Заметим смещения или значения с коэффициентами близкими к нулю:



Попробуем удалить их:

Ничего страшного не произошло.
Далее в каждом коэффициенте по‑очереди стараемся уменьшить число знаков до минимума, стараясь терять минимум точности, пробуем округлять в большую или в меньшую сторону. Если что‑то не влияет на точность или влияет минимально — пытаемся удалить. Небольшие двузначные значения(например ~12.87) пробуем заменить на 9, чтобы сэкономить дополнительный знак. Все оставшиеся значения более чем с 1 знаком записываем в свободные переменные, и используем в выражении их.
Получаем следующее:
Запуск на калькуляторе (насилие)
1) По очереди задаем значения всем переменным A‑E: [Значение] [SHIFT] [RCL(STO)] [Клавиша с буквой переменной]; 2) Затем вводим само выражение после «a=» и нажимаем «=». Не обращаем внимание на вывод калькулятора, вне зависимости от того какой он; 3) Далее вводим второе выражение после «z=» заменяя переменную «a» на Ans и снова нажимем «=»

Готово! Для использования перцептрона необходимо задать значения X и Y тем же методом, что и первые переменные, а затем, последовательно выполнить первое и второе выражение, возвращаясь к ним стрелками вверх и вниз, как в терминале.
Для теста возьму несколько точек.
1) (1;1):

Видим что по мнению перцептрона вероятность нахождения точки в пределах лемнискаты близка к 0, что правда:

2) (0.5;0):

Значение почти совпадает с 1, значит вероятность попадания точки в лемнискату почти 100%. Результат верный:

Ну и возьмем точку (0.6;0.25):

Значение ~= 0.8. На точках близких к границе точность определения падает, но все же определено верно:

Заключение
У меня получилась вполне рабочая нейросеть запущенная на инженерном непрограммируемом калькуляторе. Пусть и с ограничениями, но действительно выполняющая свои задачи.
Здесь еще есть что доработать и улучшить. Думаю, что при желании расчёт скрытого слоя и выходного нейрона можно попробовать уместить в одно выражение, можно заняться файнтюнингом модели и дообучить ее, улучшив определения точки по краям и в центре лемнискаты. Возможно получится уместить в выражение дополнительные нейроны, заменить функцию активации или как‑то по другому улучшить архитектуру модели, но я доволен текущем результатом, который показывает, что модель в принципе работает. Надеюсь, кто‑то захочет это повторить или улучшить. Github с полезными файлами.
Автор: NoName12332112


