...

пятница, 6 декабря 2019 г.

[Из песочницы] Почему нам везде хочется видеть золотое сечение? Попытка (неудачная) эволюционного анализа при помощи нейросетей на C++

Недавно я задался вопросом: связано ли как-то наше желание везде видеть золотое сечение с какими-то сугубо культурными вещами, или же в этом скрыта какая-то более глубокая закономерность, связанная с устройством нашего мозга? Чтобы разобраться в этом вопросе, я решил сделать несколько вещей:
  1. Сформулировать конкретную гипотезу относительно данной закономерности. Я решил, что лучше всего подойдёт предположение, что наш мозг использует систему счисления, основанную на разложении чисел на степени золотого сечения, так как некоторые её особенности очень близки работе примитивных нейросетей: дело в том, что степени золотого сечения более высокого порядка можно разложить бесконечным числом способов в суммы степеней менее высокого порядка и даже отрицательных степеней. Таким образом, более высокая степень как бы «возбуждается» от нескольких низших степеней, тем самым проявляя то самое сходство с нейросетью.
  2. Описать конкретный способ её проверки: я выбрал мат. моделирование эволюции мозга посредством случайных изменений в простейшей возможной нейросети — матрице линейного оператора.
  3. Составить критерии подтверждения гипотезы. Моим критерием было то, что система счисления, основанная на золотом сечении, реализуется на нейросетевом движке при тех же объёмах информации с меньшим числом ошибок, чем двоичная.

Так как речь идёт о программировании, опишу поподробнее второй и третий пункты.
Для моделирования случайных изменений в мозге в процессе эволюции я использовал функцию rand_s(), так как она криптографически устойчива, и, соответственно, даст «более случайный» результат. Также я использовал в качестве критерия того, что нейросеть достигла при обучении наименьшего числа ошибок то, что при отклонении матрицы в случайные стороны на небольшие значения её произведение на вектор меняется на примерно одинаковый модуль.

Что же касается кодировки данных в самом векторе, то я использовал 28-мерный вектор для двух 14-значных бинарных чисел и их суммы (после первых 14 знаков в сумме идёт просто 14 нулей для заполнения) и 40-мерный вектор для двух чисел в системе с золотым сечением.

Входной файл же имеет следующий формат.

Первая строка — два целых числа, разделённых пробелом, размерность вектора и количество элементов в обучающей выборке.

Все последующие строки: первая строка — вход нейросети, вторая — правильный результат обработки.

Вот фрагмент кода нейросети, отвечающий за её обучение на выборке из входных данных и соответствующих им правильным результатам:

  while (((d-mu)*(d-mu)>0.01)||(q<10)) //Нейросеть обучается, пока отклонение результата от правильного не станет "топтаться" на ровном месте
        {
                s=0; //Инициализирую переменную для хранения суммы квадратов разностей результата нейросети с правильным
                for (k=0;k<m;k++)
                {
                        for (i=0;i<n;i++)
                        {
                                        (*(z+k*n+i))=0;
                        }
                        for (i=0;i<n;i++)
                        {
                                for (j=0;j<n;j++)
                                {
                                        (*(z+k*n+i))=(*(z+k*n+i))+(*(a+i*n+j))*(*(x+k*n+j));
                                }
                        } // Вычисляем результат умножения матрицы нейросети на вектор

                        for (i=0;i<n;i++)
                        {
                                s=s+((*(z+k*n+i))-(*(y+k*n+i)))*((*(z+k*n+i))-(*(y+k*n+i)));
                        } //Вычисляем сумму квадратов разностей между результатом нейросети и правильным
                        s1=s+1;
                }
                while (s<s1) //Нейросеть пробует изменять матрицу в разные стороны, пока сумма квадратов не будет меньше, чем была раньше
                {
                        s1=0; //Инициализируем переменную для новой суммы квадратов
                        for (k=0;k<m;k++)
                        {
                                for (i=0;i<n;i++)
                                {
                                        (*(z+k*n+i))=0;
                                }
                        }//Инициализируем массив (вектор) для хранения результата умножения матрицы
                        rand_s(&p);
                        k1 = (int) (p/((int) (UINT_MAX/n)));
                        rand_s(&p);
                        k2 = (int) (p/((int) (UINT_MAX/n)));
//Генерируем координаты изменяемой "связи" между нейронами, то есть элемента матрицы
                        rand_s(&p);
                        h=((double) p/UINT_MAX)-0.5; //Генерируем шаг
                        h1=1;
                        rand_s(&p);
                        l=((int) ((double) p/UINT_MAX)*20);
                        for (i=0;i<l;i++)
                        {
                                h1=h1/10;
                        }
                        h=h*h1;
//Делаем, чтобы шаг более равномерно пробегал различные порядки
                        for (k=0;k<m;k++)
                        {
                                for (i=0;i<n;i++)
                                {
                                        for (j=0;j<n;j++)
                                        {
                                                if ((i==k1)&&(j==k2))
                                                        (*(z+k*n+i))=(*(z+k*n+i))+(*(a+i*n+j))*(*(x+k*n+j))+h*(*(x+k*n+j));
                                                else
                                                        (*(z+k*n+i))=(*(z+k*n+i))+(*(a+i*n+j))*(*(x+k*n+j));
                                        }
                                }
//Вычисляем результат умножения изменённой матрицы на вектор
                                for (i=0;i<n;i++)
                                {
                                        s1=s1+((*(z+k*n+i))-(*(y+k*n+i)))*((*(z+k*n+i))-(*(y+k*n+i)));
                                }//Вычисляем сумму квадратов разностей между результатом изменённой нейросети и правильным результатом
                        }
                }
                (*(a+k1*n+k2))=(*(a+k1*n+k2))+h;
//Изменяем нейросеть после нахождения успешного варианта с меньшей ошибкой в результате
                s1=0;
                d=0;
                for (k1=0;k1<n;k1++)
                {
                        for (k2=0;k2<n;k2++)
                        {
                                for (k=0;k<m;k++)
                                {
                                        for (i=0;i<n;i++)
                                        {
                                                (*(z+k*n+i))=0;
                                        }
                                }
                                for (k=0;k<m;k++)
                                {
                                        for (i=0;i<n;i++)
                                        {
                                                for (j=0;j<n;j++)
                                                {
                                                        if ((i==k1)&&(j==k2))
                                                                (*(z+k*n+i))=(*(z+k*n+i))+((*(a+i*n+j))+0.1)*(*(x+k*n+j));
                                                        else
                                                                (*(z+k*n+i))=(*(z+k*n+i))+(*(a+i*n+j))*(*(x+k*n+j));
                                                }
                                        }
                                }
                                s1=0;
                                for (k=0;k<m;k++)
                                {
                                        for (i=0;i<n;i++)
                                        {
                                                s1=s1+((*(z+k*n+i))-(*(y+k*n+i)))*((*(z+k*n+i))-(*(y+k*n+i)));
                                        }
                                }
                                d=d+(s1-s)*(s1-s)/(n*m);// Вычисляем средний квадрат изменения результата при изменении элемента матрицы
                        }
                }
                mu=mu*((double) q/(q+1))+((double) d/(q+1));//Вычисляем среднее значение квадрата изменения за несколько прошедших циклов
                q=q+1;
                printf("%lf \n",mu);//Выводим на экран это самое среднее для отладки кода
        }

Входные данные я также генерировал случайным образом, это были вещественные числа от нуля до единицы. Также, кроме обучающей выборки я сгенерировал ещё и тестовую выборку, на которой испытал свою нейросеть. Кроме того, я для каждого полученного нейросетью результата вычислил среднеквадратическую ошибку, то есть корень из среднего квадрата разности между элементами вектора, полученного нейросетью и вектора, содержащего правильный результат.
В результате у меня получилось по 1000 средних ошибок для результата работы нейросети со сложением в двоичной и основанной на золотом сечении системах счисления. Размерность вектора я подобрал таким образом, чтобы в них хранилось примерно одинаковое число информации как внутри системы счисления, так и между ними.

Я сравнил ошибки в разных системах счисления парными t-тестами и вот, что у меня получилось.

Сравнение: Золотое сечение — двоичная система
Гипотеза: Ошибка при золотом сечении в среднем меньше.
Результаты:
t = -22.033
df = 999
p<0.001
Cohen's d = -0.697 (При золотом сечении ошибка меньше)
99% доверительный интервал для Cohen's d:
от -inf до -0.615
Тест на нормальность распределения Шапиро — Уилка:
W = 0.998 p=0.382 (распределения примерно соответствуют нормальному)
Дескриптивная статистика:
Золотое сечение:
Среднее арифметическое: 0.365
Стандартное отклонение: 0.044
Двоичная система:
Среднее арифметическое: 0.414
Стандартное отклонение: 0.055

Все данные, использованные в данном небольшом кустарном исследовании, я решил оставить пока у себя как доказательства того, что я не нарисовал цифры с потолка. Кто попросит, тому могу отправить.

Теперь к выводам. Так как нейросети, обучение которых построено на случайном изменении связей между нейронами и отборе наилучших из них (как во время эволюции), в данном случае показали, что они значимо лучше справляются с золотым сечением, чем с двойкой в качестве основания системы счисления при одинаковом количестве информации, то можно предположить, что и эволюция мозга животных, и, в частности, человека, шла по похожему пути.

UPD. С момента публикации автор провёл новое исследование, в котором учёл поправку на количество измерений и влияние основания системы счисления отдельно от его расстояния до золотого сечения при помощи линейной регрессии. Результат оказался неутешительным: близость основания к золотому сечению скорее увеличивает ошибку, чем уменьшает её, так что сенсация, как всегда, сорвалась.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий