Обучилась сетка - бинарник у вас на почте.
Теперь немного нудной теории.
Бинарник этот содержит уже целочисленные значения всех весов сетки. Чтобы сделать их такими из float32 из питона я (помимо специальных ограничений сетки в процессе обучения) домножал оригинальные веса на специальные константы и округлял получившееся до целочисленного значения. В процессе внедрения нам, соответственно, нужно будет проводить обратную процедуру - деления получившихся целочисленных результатов на те же самые константы. При этом, конечно, часть точности сети теряется. Но не так уж и много. Зато для полученной сетки уже можно использовать технологию NNUE и даже ускорять ее Intel SIMD ассемблерными командами. Бинарник уже оптимизирован под их использование и мы с вами в Делфи, думаю, финальным аккордом их тоже напишем. С точки зрения вашего обучения и резкого ускорения движка. Все по порядку.
Конфигурация сетки. Тут взял максимально близкую к тому что мы с вами для неподвижного короля обучали. Но , опять же, с важными нюансами для последующего внедрения. Итак : пройдемся по слоям.
1 слой. Входной слой. Содержит нолики и единички. В неподвижном короле он имел размер 192 (3х64), у нас в полноценных шахматах формула уже такая : 12*64+4=772 . В шахматах у нас уже 12 типов фигур (за оба цвета) и 4 признака рокировки для обоих королей в обе стороны. Карту расположения каждого типа фигур приведу позже. Признаки рокировок несколько усложнят нам внедрение (можно было бы и без них), но уж слишком геморно мне было их "выкусывать" из фреймворка - хорошо вглубь все зашил.
2 Слой. Слой признаков (FeatureLayer или FLayer). Содержит веса, которые будут умножаться на эти единички и нолики и впоследствии именно с ними мы будем работать обновляя наш будущий аккумулятор за оба цвета после каждого хода из корня вниз по древу. Размер слоя 256 нейронов, количество весов (256,772)=256*772=197632 целочисленных int16 значений. Сразу в int16 для будущий упражнений с Intel Simd. Плюс, конечно, еще 256 биасов для каждого нейрона 256 целочисленных int16 значений. ВАЖНО. Веса этого слоя выгружены в "транспонированном" матричном виде : постолбцово. Сначала 256 значений весов первого из 772 признака, потом 256 весов второго из 772 признака и так далее. Так удобнее будет потом использовать в оптимизированном SIMD виде.
3 Слой. Скрытый слой (hidden). Содержит 8 нейронов. Количество весов (8,256)=8*256=2048 int8 значений. Это самый требовавтельный по быстродействию слой - эту матрицу в движке мы будем именно умножать и делать это часто, поэтому сетка изначально обучается и оптимизируется так, чтобы в этом слое веса помещались не просто в целочисленное число, а в 8-битное. SIMD процедура умножения потом чудеса сделает с точки зрения затраченного на умножение времени. Плюс 8 биасов для этих нейронов в виде 8 int32 чисел. Тут уже можем себе позволить 32-разрядные целочисленные - "собирать" итоговый результат будем именно такой. Матрица тоже сохранена в транспонированном виде постолбцово для удобства последующего умножения.
4.Слой. Выходной. (outlayer). Содержит единственный нейрон на котором "собирается" итоговый результат. Имеет , стало быть, 8 весов (int32 числа) и 1 биас (тоже int32).
Всего , таким образом, файл содержит 197632+2048+8= 199688 целочисленных весов (разнокалиберных) и 256+8+1=265 биасов для нашей сетки, формула которой 256-8-1. Количество байт в нем, учитывая различные формы целочисленных значений: 197632*2(int16)+256*2(int16)+2048*1(int8)+8*4(int32)+8*4(int32)+1*4(int32)=397892 байта
Теперь про активационные функции нейронов всех слоев. В "неподвижном короле" мы использовали сигмоиду. Хорошая функция, но очень "дорогая" с точки быстродействия. Тут у нас я спользовал более простую - clipped relu. Она принимает значения по следующим правилам :
1. Если входное значение (от нашего сумматора нейрона) меньше нуля, то 0.
2. Если значение превышает пороговое значение Х , то Х
3. Если лежит внутри диапазона [0..X] то это значение и берется.
Подбор параметра Х при обучении был таким, чтобы в нашей уже оцифрованной целочисленной сетке максимальным пороговым значением было число 255. Максимальное целое, которое помещается в байт. Ну а 0 он и в Африке 0.
У меня есть вопрос по Конфигурация сетки, 1 слой.
В шахматах у нас уже 12 типов фигур (за оба цвета) и 4 признака рокировки для обоих королей в обе стороны.
Нужно ли нам учитывать еще и взятие на проходе?
Почему возник этот вопрос, как раз когда разбирался с Polyglot'ом.
Там типы фигур, рокировка и взятие на проходе индитифицируют позицию.
В общем то мне надо переварить приведенную инфу , (составить схемку, аль чертеж - для себя)
Признак взятия на проходе для оценки позиции не нужен. Он не слишком информативен потому что крайне редкий. Кк и вообще возможные в позиции взятия фигур. Их сетка "увидит" и так. А вот про то : имеют ли стороны право на рокировку или нет сетка "глядя на позицию" определить не может сама. Надо подсказать. А вот они - встречаются часто. В каждом дебюте
Переваривайте . А я пока соберусь силами и юнит смастерю с комментариями.
Не - не важно. Дело в специфике использования оценочной функции в движке. В сильных движках (а мы пишем как раз такой) она будет вызываться лишь в модели форсированной игры в процессе перебора возможных взятий. Именно там и среди прочих будет рассмотрено взятия на проходе на наличие тактической угрозы в позиции. И только в относительно "тихой" позиции будет оценка позиции со всеми ее позиционными факторами.
Оценочная функция нам нужна в идеале такая, чтобы в ней у нас выявлялись лишь долгоиграющие факторы позиции. А мелкую тактику решала бы переборная функция. В этом плане какое-нибудь "давление" фианкеттированного слона на изолированную (но пока защищенную) пешку противника для нас интересно и должно быть учтено, как фактор в позиции (через 20 ходов после грамотных разменов может уже и нечем будет защищать), а мелкая тактика "пешка атакуют прямо сейчас пешку" (как вариант - взятие на проходе) не имеет долгоиграющей ценности и должен быть отработан в переборе (модели форсированной игры).
Не - не важно. Дело в специфике использования оценочной функции в движке. В сильных движках (а мы пишем как раз такой) она будет вызываться лишь в модели форсированной игры в процессе перебора возможных взятий. Именно там и среди прочих будет рассмотрено взятия на проходе на наличие тактической угрозы в позиции. И только в относительно "тихой" позиции будет оценка позиции со всеми ее позиционными факторами.
А как сетка должна оценивать позицию, где взятие может быть, а может уже и нет?
Что будет, если подсунуть такую позицию для анализа?
Позиции для обучения у нас ведь не с воздуха берутся, а с точно такого-же сильного работающего движка, который в процессе игры с самим собой отрабатывает точно такую же модель форсированной игры, выкидывает нафиг позиции с мелкой тактикой (которые точно так же выкинутся нафиг и в будущем движке в той же самой процедуре ) и сохранит для нас только "тихую" позицию, ровно которые мы и будем тоже оценивать в дальнейшем.
Если на пальцах : когда попадется нам в корне позиция со взятием на проходе, то прежде чем движок ее будет оценивать он сначала попробует взятия. Если взятие на походе - важный тактический фактор, то перебор это увидит, возьмет на проходе в коротком форсированном переборе и в уже получившейся позиции без взятия на проходе запустит оценочную функцию. Либо НЕ возьмет на проходе (если это не имеет тактической ценности в позиции) и оценит , таким образом, нашу "тихую" (без тактической ценности) позиции уже не принимая этот фактор во внимание.
В смысле "равнозначные"? О том что в позиции есть взятие на проходе отвечает специально поле в структуре доски. То есть переборная функция будет точно знать: взятие или есть или нет. И отработает его обязательно (часть алгоритма) в случае если оно есть. Точно так же как она отработала бы ситуацию "висит ферзь в один ход" не отправляя эту позицию в сетку, а сначала забрав ферзя, а уж потом оценив в получившуюся тихую позицию. Весь смысл движка в том, чтобы оценочная функция вызывалась по возможности ТОЛЬКО в тихих позициях и учитывала бы ТОЛЬКО долгоиграющие факторы. А перебор бы оценивал мелкую тактику ( с относительно крупными материальными колебаниями) ПРЕЖДЕ чем оценивать. Чем лучше это удается реализовать тем лучше будет играть движок. Тактические позицие сильно "зашумливают" сетку. От них надо избавляться как при обучении (убирать их из выборки), так и в переборной функции.
Тактические позицие сильно "зашумливают" сетку. От них надо избавляться как при обучении (убирать их из выборки), так и в переборной функции.
Ну создателям движков виднее
Я бы не стал считать взятие на проходе тактическим шумом.
Возможность взятия принципиально меняет позицию стратегически.
Например, тут даже не надо считать варианты
Я и без всякого счета, даже на один ход, вижу, что если взятие есть, то рыба. Нет - победа.
8/8/3k4/8/3Pp3/4P3/4K3/8 b - - 0 1
Поэтому, возможно, имеет смысл добавить входной нейрон для этого.
"Создатели движков" тоже неплохо в шахматы играют и все это видят . Но компьютер играет по другому. И сильнее.Играет согласно математической модели, которая (в двух словах) говорит что "пироги должен печь пирожник, а сапоги чинить - сапожник. Главное - не перепутай , Кутузов " . Оценочная функция тем эффективнее чем больше содержит в себе долгоиграющих признаков и тем меньше - сиюминутных, которые через ход вообще исчезнут. В этом смысле для оценки приведенной вами позиции движок сделает маленький перебор в модели форсированной игры и оценит уже получившуюся после взятия на проходе тихую позицию. И тоже бодро крикнет "рыба"! Саму возможность такого взятия видит переборная функция. Считайте ее тоже частью оценочной, если угодно - она тоже на оценку работает .
Верно. Не нужно. Как не нужно оценивать позиции с другими висячими фигурами. Лучше вместо этого сеткой оценить до десятка возникающих уже после этих взятий тихих позиций и подняться минимаксом в исходную. Эти дебаты еще в 70-х годах прошлого века проходили.
Кинул вам на почту первый макет того как это все работает. С комментариями. Уже есть оценочная функция, уже с помощью NNUE технологии, но пока аккумуляторы вычисляются каждый раз заново, а не апдейтятся : у нас и перебора-то никакого нет пока где бы это можно было делать. Посмотрите код - поиграйтесь с оценочной. Не пугайтесь выдаваемых оценок - обучалась сетка на партиях моего движка. А он резкий как квас летом
Для меня пока многое выглядит как черный ящик.
не понятно
Function ReSigma(y:real):integer;
// По значению вероятности [0..1] преобразует целочисленную оценку позиции в "сотых долях пешки"
begin
If y<0.000001 then y:=0.000001;
if y>0.999999 then y:=0.999999;
result:=round(-410*ln((1/y)-1));
end;
Function HashSigma(y:integer):integer;
// Быстрый поиск значения оценки по сырому выходу из нейросети
begin
If y<0 then y:=0;
if y>128*1024-1 then y:=128*1024-1;
Result:=Sigma[y];
end;
Ну и оценки позиций
r2r2n1/1p3pk1/p2pb1p1/q1pNn3/4P3/2N2P2/PPPQ2P1/2KR1B1R w - - 0 1
Evaluate(MainBoard)=5664
Я вам исправленный исходник кинул. В первом помарку сделал в одной строке кода. Ну а начальную позицию он так оценивает, да. Забавно. Близко к равной но даже чуть меньше . Ну вот так обучился. Сеть я небольшую брал специально - не страшно это.
Ну а то что черный ящик - это тоже не страшно. Разберетесь.
Процедуры, которые вы привели - про оценку. Наша нейросеть обучается на оценке, которая специально приводится в диапазон [0..1] где ближе к 0 - "проиграно", а "ближе к 1" - выиграно, а ближе к 0,5 - равенство. Так сеть учится лучше всего. Перевод этот из "обычной" оценки осуществляется той же самой старой-доброй разновидностью сигмоиды. y=1/(1+exp(-x/410)). В неподвижном короле было чуть по другому . Где x "стандартная" оценка движка в сотых долях пешки. Формула достаточно известная и хорошо себя зарекомендовала. Соответственно когда мы от нейросети получаем целочисленный сырой ответ мы его сначала приводим в диапазон [0..1] делением на наши магический константы (там 2 штуки в коде),а потом эту оценку из диапазона [0..1] должны как-то преобразовать обратно в сотые доли пешки. Вот собственно процедура Resigma этим и занимается. Вот этот вот все : result:=round(-410*ln((1/y)-1)); Это обратная функция к сигмоиде по ее результату находящая аргумент. Там - экспонента, здесь - натуральный логарифм. Все по честному .
А вторая процедура - уже маленькая оптимизация. Дело в том что каждый раз вычислять все эти логарифмы (а вычислять их надо при каждой оценки позиции же) это "дорого" по времени. Если миллионы раз в секунду это делать. Но мы пользуемся тем, что наша нейросеть может нам выдать не бесконечное количество всевозможных значений, а лишь в определенном диапазоне (она же целочисленная, а это всегда диапазон, а не континуум). Диапазон этот нам известен и задается произведением тех самых двух магических констант : у меня 127.5 и 1024. Вот собственно мы при загрузке сети берем массив данных от 0 до 128*1024 (округляем в большую сторону) и для каждого этого целочисленного значения вычисляем сначала соответствующее ему число в диапазоне [0..1],а затем - для этого числа вычисляем соответствующую ему оценку в сотых долях пешки. Все. Других значений нейросеть не даст. Все - в массиве. Потом движок будет уже пользоваться целочисленным ответом от нейросети как индексом чтобы взять готовую оценку без этих наших логарифмов каждый раз.
А за помарку - сорри. Я отключил проверку на выход за пределы диапазона и сделал описку в цикле умножения матриц сделав на 1 проход больше чем нужно. От того и оценки дикие.
Теперь для позиции выше оценка равна 271
Прокомментируйте оценку.
TRUE
8 r . . r . . n .
7 . p . . . p k .
6 p . . p b . p .
5 q . p N n . . .
4 . . . . P . . .
3 . . N . . P . .
2 P P P Q . . P .
1 . . K R . B . R
a b c d e f g h
Evaluate=271
А зачем ЗДЕСЬ сигмоид? Он был нужен (и находится) в фреймворке для обучения сети, когда позиции, которые наиграл сам с собой мой движок от движковской оценки переводились в диапазон [0..1] и передавались в питон для обучения. На стороне движка нам нужно наоборот : получать ответ от нейросети и преобразовывать его обратной сигмоиде функией ReSigma обратно в позиционную оценку.
Что касается комментирования статической оценки от нейросети - тут увольте . Я могу прокомментировать КАК ЭТО СТРОИТЬ, но вот что о позиции после обучения будет думать новая форма жизни - уже не могу. Наверное сумма позиционных признаков с точки зрения белых кажется логике нейросетки более предпочтительным чем аргументы (позиционные) черных. И изрядненько так. Ради интереса поставил позу своему движку - так он с первых милисекунд перебора дает Кf6! С в 3 раза бОльшей оценкой . Так что , наверное, нейросеть что-то да понимает
А что касается активационной функции для слоев нейросети, то, как я и говорил, вместо сигмоиды (какую мы использовали в неподвижном короле) я использую более скоростную clipped relu. В теории сигмоида, конечно, является более гладкой функцией (и ее производная - тоже) что сулит якобы чуть большую обучаемость. Но тут копеечки не считают - в скорости мы проиграем куда больше если экспоненту будем на стороне движка для каждого божьего нейрона вычислять при каждой оценки позиции.
Чтобы получить уже не промежуточный, а какой-то толковый , полезный результат нам осталось написать маленькую, простую, неповоротливую, но добротную модельку перебора. В самом простом виде - перебор на заданное количество полуходов, с альфа-бетой, моделью форсированной игры и оценкой позиции по его результатом с указанием главного варианта. Уже к этому вы можете прикрутить доску с выбором параметра ("количество полуходов для игры"), решать задачи на нахождение мата , решать комбинации движком и даже самостоятельно с движком играть в шахматы. При глубине перебора в 7-9 полуходов, полагаю, мало уже может и не показаться
Я пару дней буду занят по бизнесу, ближе к концу недели - выходным смогу макетик слепить. И останется , по сути, уже только оптимизации всего. Ну и хеш, конечно. Чтобы тот же каскадный перебор с увеличением полуходов перебора слепить, протокол присобачить и за матчами движка наблюдать.
Конечно я не удержался и попробовал сетку в деле
Без альфа бета, просто оценивает позицию после своего хода, и выбирает тот, где оценка за противника меньше.
Ну и в дебюте начинает по книге, если ход в книге не найден, то сетка начинает играть