examples/ is based on KasperskyLab/knp at 55470ed6 with local changes to the mnist-learn altai/blifat models, cuda-backend CMake and model_loader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
15 KiB
Markdown
173 lines
15 KiB
Markdown
# Отчёт: специализация колонок в модели altai
|
||
|
||
Oct 12, 2026
|
||
|
||
Чтобы 20 нейронов колонки выучили разные варианты написания цифры, хватило небольшого случайного разброса стартовых весов и торможения между нейронами на этапе проверки. На полном MNIST модель altai теперь даёт 91,5% верных ответов вместо 82,5%. Попутно найдена и исправлена ошибка в самом KNP, из-за которой примерно каждое 8–12-е обучение молча ломалось.
|
||
|
||
## Что было
|
||
|
||
В сети примера `mnist-learn` на каждую цифру отведена колонка из 20 входных нейронов. В прошлых отчётах выяснилось, что веса у всех 20 нейронов колонки идентичны и колонка работает как один нейрон. Причины:
|
||
|
||
- у всех нейронов одинаковые стартовые веса;
|
||
- на каждую картинку они получают одинаковый вход;
|
||
- сигнал метки через популяцию GATE заставляет сработать всю колонку разом, поэтому обновления весов у всех 20 одинаковые.
|
||
|
||
Лучший результат altai до этой работы: 82,5% на полном MNIST (подобранный порог, без добавки к порогу).
|
||
|
||
## Результаты
|
||
|
||
Все прогоны на одних и тех же 10 000 тестовых картинках, однопоточный CPU-бэкенд. Настройки выбраны на валидационной выборке (обучающие картинки 50 000–59 999), тестовые картинки в подборе не участвовали.
|
||
|
||
| Обучение | altai, лучший порог | Колонки, разные seed | В среднем |
|
||
| --- | --- | --- | --- |
|
||
| 3 000 | 84,2% | 84,6 / 84,8 / 83,8 / 85,7% | 84,7% |
|
||
| 30 000 | 84,1% | 92,3 / 91,5 / 91,7% | **91,8%** |
|
||
| 60 000 | 82,5% | 92,2 / 90,7% | **91,5%** |
|
||
|
||
- Seed задаёт случайный разброс стартовых весов. Разные seed показаны, чтобы был виден разброс результата, а не только лучший случай.
|
||
- На коротком обучении (3 000 картинок) колонки почти не помогают: специализация раскрывается при длинном обучении.
|
||
- Настройки колонок: разброс весов σ=20, порог на проверке `act=60` (для 3 000 — `act=100`), без добавки к порогу, торможение на проверке 255.
|
||
|
||
### Полный MNIST, колонки, seed 42: результаты по цифрам
|
||
|
||
| Цифра | Верно | Не отвечено | Ложные срабатывания | Precision | Recall |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| 0 | 932 | 23 | 25 | 0,974 | 0,976 |
|
||
| 1 | 1122 | 0 | 13 | 0,989 | 1,000 |
|
||
| 2 | 931 | 25 | 76 | 0,925 | 0,974 |
|
||
| 3 | 862 | 36 | 112 | 0,885 | 0,960 |
|
||
| 4 | 901 | 10 | 71 | 0,927 | 0,989 |
|
||
| 5 | 807 | 9 | 76 | 0,914 | 0,989 |
|
||
| 6 | 907 | 11 | 40 | 0,958 | 0,988 |
|
||
| 7 | 945 | 6 | 77 | 0,925 | 0,994 |
|
||
| 8 | 916 | 16 | 42 | 0,956 | 0,983 |
|
||
| 9 | 900 | 8 | 101 | 0,899 | 0,991 |
|
||
| **Итого** | **9223 (92,2%)** | **144** | **633** | | |
|
||
|
||
Самая слабая цифра — 3, её чаще всего путают с другими. Самая надёжная — 1.
|
||
|
||
## Что сработало, а что нет
|
||
|
||
Проверены три изменения обучения. Все включаются переменными окружения, без них сеть строится как раньше.
|
||
|
||
| Изменение | Что делает | Результат |
|
||
| --- | --- | --- |
|
||
| Разброс стартовых весов (`KNP_COL_JITTER`) | веса «растр → вход» стартуют не с 0, а со случайного значения N(0, σ) | **сработало** |
|
||
| Торможение внутри колонки при обучении (`KNP_COL_LATERAL`) | сработавший нейрон на следующем шаге тормозит соседей по колонке | хуже на 5–10 п.п. |
|
||
| Метка несколькими импульсами (`KNP_COL_GATE`) | сигнал метки подаётся не одним ударом, а двумя слабыми | хуже на 5–10 п.п. |
|
||
|
||
- **Почему работает разброс весов.** Когда нейроны колонки разные, на картинку первыми срабатывают самые похожие на неё, награду получают в основном они, и колонка расходится по стилям написания. Специальной конкуренции для этого не понадобилось.
|
||
- **Специализация есть на самом деле.** Проверка без порогов: если классифицировать по лучшему нейрону колонки, получается 93,4% на 30 000 картинках, а по среднему по колонке — 82,7%. У исходной сети оба способа дают одинаковые 87,0%, потому что нейроны в колонке одинаковые.
|
||
- **Сила разброса.** σ=20 и σ=40 дают на 30 000 одинаковые 92,5% на валидации; σ=60 на 3 000 хуже. Выбран σ=20.
|
||
|
||
## Торможение на проверке
|
||
|
||
Без дополнительной правки выигрыш от специализации почти теряется при считывании ответа. Голос колонки засчитывается за шаг, если сработал хотя бы один её нейрон, поэтому за шаг колонка даёт не больше одного голоса. При низком пороге несколько колонок срабатывают на каждом шаге, а при равенстве голосов побеждает меньшая цифра.
|
||
|
||
Решение: на этапе проверки добавить тормозящие связи между всеми входными нейронами (вес −255 после квантования, задержка 1). Первый нейрон, дошедший до порога, гасит все остальные, и ответ определяет самый похожий на картинку нейрон. На чипе это реализуется обычными синапсами.
|
||
|
||
| Сеть (валидация, 3 000 картинок) | Без торможения | С торможением |
|
||
| --- | --- | --- |
|
||
| исходная | 84,5% | 84,4% |
|
||
| колонки, 4 seed (среднее) | 84,0% | 85,7% |
|
||
|
||
На исходной сети торможение ничего не меняет, а сетям со специализацией помогает.
|
||
|
||
## Ошибка в KNP: обучение иногда молча не происходит
|
||
|
||
### Симптом
|
||
|
||
Примерно в 1 запуске из 8–12 сеть после обучения выдаёт результат на уровне случайного угадывания. Веса при этом остаются стартовыми, хотя обучение идёт положенное время и ошибок не выдаёт. Одинаковые запуски с одинаковыми параметрами дают разный результат.
|
||
|
||
### Причина
|
||
|
||
`ModelLoader::init_channels` (`knp/base-framework/impl/model_loader.cpp`) обходит `std::unordered_multimap` каналов по хеш-корзинам. Код предполагает, что в одной корзине лежат записи только одного канала: берёт UID первой записи и подписывает на этот канал **все** проекции из корзины.
|
||
|
||
```cpp
|
||
for (size_t i = 0; i < channels.bucket_count(); ++i)
|
||
{
|
||
auto bucket_iter = channels.begin(i);
|
||
if (channels.end(i) == bucket_iter) continue;
|
||
auto channel_uid = bucket_iter->first; // UID только первой записи
|
||
... // а проекции берутся из всей корзины
|
||
```
|
||
|
||
UID каналов генерируются случайно. В обучении MNIST два входных канала: картинки и метки. Когда их UID попадают в одну корзину, проекция картинок подписывается на канал меток. Сеть получает вместо пикселей номер цифры (спайки на нейронах растра 0–9), канал картинок не создаётся, и обучение не происходит.
|
||
|
||
### Как подтверждено
|
||
|
||
- В сломанных запусках входные нейроны ни разу не срабатывают сами, только по принуждению меткой. В логе нет спайков канала картинок.
|
||
- У сломанного прогона blifat на 60 000 картинок у каждого нейрона ровно один вес на максимуме — это «пиксель» с индексом метки.
|
||
- До исправления на двух обучающих картинках расходились 2 запуска из 24, после исправления 72 из 72 совпали до бита.
|
||
|
||
### Исправление
|
||
|
||
Обход по уникальным ключам: в `unordered_multimap` записи с одинаковым ключом гарантированно идут подряд.
|
||
|
||
```cpp
|
||
for (auto iter = channels.begin(); iter != channels.end();)
|
||
{
|
||
const auto channel_uid = iter->first;
|
||
const auto range = channels.equal_range(channel_uid);
|
||
std::vector<core::UID> p_uids;
|
||
std::transform(range.first, range.second, std::back_inserter(p_uids),
|
||
[](const auto &entry) { return entry.second; });
|
||
iter = range.second;
|
||
(this->*channel_gen)(model, channel_uid, p_uids);
|
||
}
|
||
```
|
||
|
||
### Что это меняет в прошлых отчётах
|
||
|
||
- Схлопнувшийся прогон blifat на 60 000 картинок объясняется этой ошибкой, а не случайным выбором в WTA, как я предполагал в первом отчёте.
|
||
- Ошибка срабатывает по принципу «всё или ничего», поэтому результаты удачных прогонов в прошлых отчётах остаются в силе.
|
||
- Обучение после исправления детерминировано: в 72 повторных запусках не нашлось ни одного расхождения.
|
||
|
||
Об ошибке стоит сообщить разработчикам KNP: https://github.com/KasperskyLab/knp
|
||
|
||
## Как проверял
|
||
|
||
1. Выгружал веса и пороги после обучения и оценивал специализацию без порогов: классификация по лучшему нейрону колонки против среднего по колонке.
|
||
2. Расширил офлайн-симулятор нейрона AltAI: у каждого из 200 нейронов свои веса, есть торможение внутри колонки или между всеми нейронами. Совпадение с KNP: 8663 / 1336 / 1 в KNP и 8665 / 1334 / 1 в симуляторе.
|
||
3. Пороги и силу торможения подбирал на симуляторе по валидационной выборке.
|
||
4. Выбранные варианты подтвердил полными прогонами в KNP на тесте; все цифры в таблицах результатов — из KNP.
|
||
5. Ошибку в загрузчике нашёл, сравнивая логи одинаковых запусков с разным исходом, и подтвердил серией повторных запусков до и после исправления.
|
||
|
||
## Что изменено
|
||
|
||
Изменения видны в `git diff` в `C:\KNP\examples`, их можно откатить.
|
||
|
||
| Файл | Что изменено |
|
||
| --- | --- |
|
||
| `knp/base-framework/impl/model_loader.cpp` | исправлена ошибка обхода каналов (библиотека пересобрана) |
|
||
| `examples/mnist-learn/models/altai/construct_network.cpp` | переменные `KNP_COL_JITTER`, `KNP_COL_SEED`, `KNP_COL_LATERAL`, `KNP_COL_GATE` |
|
||
| `examples/mnist-learn/models/altai/prepare_network_for_inference.cpp` | торможение на проверке `KNP_MNIST_INFER_INHIB`; квантование масштабирует популяцию один раз, даже если у неё несколько входящих связей, а большие веса обрезаются до ±255 |
|
||
|
||
Для исходной сети новое квантование проверено: результат совпал до картинки (7622 / 1545 / 833).
|
||
|
||
| Переменная | Что делает |
|
||
| --- | --- |
|
||
| `KNP_COL_JITTER=<σ>` | случайный разброс стартовых весов N(0, σ), в единицах ×1000 |
|
||
| `KNP_COL_SEED=<n>` | seed разброса (по умолчанию 42) |
|
||
| `KNP_COL_LATERAL=<w>` | торможение внутри колонки при обучении, вес −w (ухудшает) |
|
||
| `KNP_COL_GATE=<g1,g2,…>` | метка импульсами с весами g1, g2, … и задержками 1, 2, … (ухудшает) |
|
||
| `KNP_MNIST_INFER_INHIB=<w>` | торможение между всеми входными нейронами на проверке, вес −w после квантования |
|
||
|
||
## Как запускать
|
||
|
||
```bash
|
||
cd ~/run/exp
|
||
# лучший вариант для полного MNIST
|
||
MODEL=altai ./run.sh myrun 60000 KNP_COL_JITTER=20 \
|
||
KNP_MNIST_THR=60 KNP_MNIST_ADD_THR_SCALE=0 KNP_MNIST_INFER_INHIB=255
|
||
cat myrun/result.csv
|
||
```
|
||
|
||
Полный прогон на 60 000 картинок занимает 15–40 минут в зависимости от загрузки машины.
|
||
|
||
## Следующие шаги
|
||
|
||
- [ ] Сообщить разработчикам KNP об ошибке в `ModelLoader::init_channels`
|
||
- [ ] Исправить порог в самом обучении, а не только на проверке
|
||
- [ ] Проверить, не даст ли больше нейронов в колонке (сейчас 20) дополнительный прирост на длинном обучении
|
||
- [ ] Запустить сеть на GPU, когда заработает GPU-бэкенд
|