image


Здравствуй, Хабр! Данная статья предназначена для тех, кто приблизительно шарит в математических принципах работы нейронных сетей и в их сути вообще, поэтому советую ознакомиться с этим перед прочтением. Хоть как-то понять, что происходит можно сначала здесь, потом тут.


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


На самом деле, несмотря на обилие математики, она не такая уж и запредельно сложная. Понять сатанистские каракули и письмена этого пособия сможет среднестатистический 11-классник товарищ-физмат или 1~2-курсник технарьской шараги. Помимо этого, пусть книга достаточно объёмная и трудная для восприятия, но вещи, написанные в ней, реально объясняют, что "твориться у тачки под капотом". Как вы поняли я крайне рекомендую(ни в коем случае не рекламирую) "Нейронные сети. Полный курс" Саймона Хайкина к прочтению в том случае, если вам придётся столкнуться с применением/написанием/разработкой нейросетей и прочего подобного stuff'а. Хотя в ней нет материала про новомодные свёрточные сети, никто не мешает загуглить лекции от какого-нибудь харизматичного работника Yandex/Mail.ru/etc. никто не мешает.


Конечно, осознав устройство сеток, я не мог просто остановиться, так как впереди предстояло написание кода. В связи со своим параллельным занятием, заключающемся в создани игр на Unity, языком реализации оказался ламповый и няшный шарпей 7 версии(ибо она последняя актуальная). Именно в этот момент, оказавшись на просторах интернета, я понял, что число внятных туториалов по написанию нейросетей с нуля(без ваших фреймворков) на шарпе бесконечно мало. Ладно. Я мог использовать, всякие Theano и Tensor Flow, НО под капотом моей смерть-машины в моём ноутбуке стоит "красная" видеокарта без особой поддержки API, через которые обращаются к мощи GPU(ведь именно их и используют Theano/Tensor Flow/etc.).


Помогите школоте прошариться:

Моя видеокарта называется ATI Radeon HD Mobility 4570. И если кто знает, как обратиться к её мощностям для параллелизации нейросетевых вычислений, пожалуйста, напишите в комментарии. Тогда вы поможете мне, и возможно у этой статьи появится продолжение. Не осуждается предложение других ЯП.


Просто, как я понял, она настолько старая, что нифига не поддерживает. Может быть я не прав.


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


Здесь я не буду рассматривать код сети для распознования цифр(как упоминалось ранее), ибо я оставил его на флэшке, удалив с ноута, а искать сей носитель информации мне лень, и в связи с этим я помогу вам сконструировать многослойный полносвязный персептрон для решения задачи XOR и XAND(XNOR, хз как ещё).


Прежде чем начать программировать это, можнонужно нарисовать на бумаге, дабы облегчить представление структуры и работы нейронки. Моё воображение вылилось в следующую картинку. И да, кстати, это консольное приложение в Visual Studio 2017, с .NET Framework версии 4.7.


Краткая инфа по сетке(для тех, кому это хоть о чём-то говорит)

Многослойный полносвязный персептрон.
Один скрытый слой.
4 нейрона в скрытом слое(на этом количестве персептрон сошёлся).
Алгоритм обучения — backpropagation.
Критерий останова — преодоление порогового значения среднеквадратичной ошибки по эпохе.(0.001)
Скорость обучения — 0.1.
Функция активации — логистическая сигмоидальная.


image
Потом надо осознать, что нам нужно куда-то записывать веса, проводить вычисления, немного дебажить, ну и кортежи поюзать(но для них юзинг мне не нужен). Соответственно, using'и у нас такие.


Also

В папке release||debug этого прожекта располагаются файлы(на каждый слой по одному) по имени типа (fieldname)_memory.xml сами знаете для чего. Они создаются заранее с учётом общего количества весов каждого слоя. Знаю, что XML — это не лучший выбор для парсинга, просто времени было немного на это дело.


using System.Xml;
using static System.Math;
using static System.Console;

Также вычислительные нейроны у нас двух типов: скрытые и выходные. А веса могут считываться или записываться в память. Реализуем сию концепцию двумя перечислениями.


enum MemoryMode { GET, SET }
enum NeuronType { Hidden, Output }

Всё остальное будет происходить внутри пространства имён, которое я назову просто: Neural Network.


namespace NeuralNetwork
{
//всё, что будет описано ниже, располагается здесь
}

Прежде всего, важно понимать, почему нейроны входного слоя я изобразил квадратами. Ответ прост. Они ничего не вычисляют, а лишь улавливают информацию из внешнего мира, то есть получают сигнал, который будет пропущен через сеть. Вследствие этого, входной слой имеет мало общего с остальными слоями. Вот почему стоит вопрос: делать для него отдельный класс или нет? На самом деле, при обработке изображений, видео, звука стоит его сделать, лишь для размещения логики по преобразованию и нормализации этих данных к виду, подаваемому на вход сети. Вот почему я всё-таки напишу класс InputLayer. В нём находиться обучающая выборка организованная необычной структурой. Первый массив в кортеже — это сигналы-комбинации 1 и 0, а второй массив — это пара результатов этих сигналов после проведения операций XOR и XAND(сначала XOR, потом XAND).


class InputLayer
    {
        private (double[], double[])[] _trainset = new(double[], double[])[]//да-да, массив кортежей из 2 массивов
        {
            (new double[]{ 0, 0 }, new double[]{ 0, 1 }),
            (new double[]{ 0, 1 }, new double[]{ 1, 0 }),
            (new double[]{ 1, 0 }, new double[]{ 1, 0 }),
            (new double[]{ 1, 1 }, new double[]{ 0, 1 })
        };
        //инкапсуляция едрид-мадрид
        public (double[], double[])[] Trainset { get => _trainset; }//такие няшные свойства нынче в C# 7
     }

Теперь реализуем самое важное, то без чего ни одна нейронная сеть не станет терминатором, а именно — нейрон. Я не буду использовать смещения, потому что просто не хочу. Нейрон будет напоминать модель МакКаллока-Питтса, но иметь другую функцию активации(не пороговую), методы для вычисления градиентов и производных, свой тип и совмещенные линейные и нелинейные преобразователи. Естественно без конструктора уже не обойтись.


class Neuron
    {
        public Neuron(double[] inputs, double[] weights, NeuronType type)
        {
            _type = type;
            _weights = weights;
            _inputs = inputs;
        }
        private NeuronType _type;
        private double[] _weights;
        private double[] _inputs;
        public double[] Weights { get => _weights; set => _weights = value; }
        public double[] Inputs { get => _inputs; set => _inputs = value; }
        public double Output { get => Activator(_inputs, _weights); }
        private double Activator(double[] i, double[] w)//преобразования
        {
            double sum = 0;
            for (int l = 0; l < i.Length; ++l)
                sum += i[l] * w[l];//линейные
            return Pow(1 + Exp(-sum), -1);//нелинейные
        }
        public double Derivativator(double outsignal) => outsignal * (1 - outsignal);//формула производной для текущей функции активации уже выведена в ранее упомянутой книге
        public double Gradientor(double error, double dif, double g_sum) => (_type == NeuronType.Output) ? error * dif : g_sum * dif;//g_sum - это сумма градиентов следующего слоя
    }

Ладно у нас есть нейроны, но их необходимо объединить в слои для вычислений. Возвращаясь к моей схеме выше, хочу объяснить наличие чёрного пунктира. Он разделяет слои так, чтобы показать, что они содержат. То есть один вычислительный слой содержит нейроны и веса для связи с нейронами предыдущего слоя. Нейроны объединяются массивом, а не списком, так как это менее ресурсоёмко. Веса организованы матрицей(двумерным массивом) размера(нетрудно догадаться) [число нейронов текущего слоя X число нейронов предыдущего слоя]. Естественно, слой инициализирует нейроны, иначе словим null reference. При этом эти слои очень похожи друг на друга, но имеют различия в логике, поэтому скрытые и выходной слои должны быть реализованы наследниками одного базового класса, который кстати оказывается абстрактным.


abstract class Layer//модификаторы protected стоят для внутрииерархического использования членов класса
    {//type используется для связи с одноимённым полю слоя файлом памяти
        protected Layer(int non, int nopn, NeuronType nt, string type)
        {//увидите это в WeightInitialize
            numofneurons = non;
            numofprevneurons = nopn;
            Neurons = new Neuron[non];
            double[,] Weights = WeightInitialize(MemoryMode.GET, type);
            for (int i = 0; i < non; ++i)
            {
                double[] temp_weights = new double[nopn];
                for (int j = 0; j < nopn; ++j)
                    temp_weights[j] = Weights[i, j];
                Neurons[i] = new Neuron(null, temp_weights, nt);//про подачу null на входы ниже
            }
        }
        protected int numofneurons;//число нейронов текущего слоя
        protected int numofprevneurons;//число нейронов предыдущего слоя
        protected const double learningrate = 0.1d;//скорость обучения
        Neuron[] _neurons;
        public Neuron[] Neurons { get => _neurons; set => _neurons = value; }
        public double[] Data//я подал null на входы нейронов, так как
        {//сначала нужно будет преобразовать информацию
            set//(видео, изображения, etc.)
            {//а загружать input'ы нейронов слоя надо не сразу,
                for (int i = 0; i < Neurons.Length; ++i)
                    Neurons[i].Inputs = value;
            }//а только после вычисления выходов предыдущего слоя
        }
        public double[,] WeightInitialize(MemoryMode mm, string type)
        {
            double[,] _weights = new double[numofneurons, numofprevneurons];
            WriteLine($"{type} weights are being initialized...");
            XmlDocument memory_doc = new XmlDocument();
            memory_doc.Load($"{type}_memory.xml");
            XmlElement memory_el = memory_doc.DocumentElement;
            switch (mm)
            {
                case MemoryMode.GET:
                    for (int l = 0; l < _weights.GetLength(0); ++l)
                        for (int k = 0; k < _weights.GetLength(1); ++k)
                            _weights[l, k] = double.Parse(memory_el.ChildNodes.Item(k + _weights.GetLength(1) * l).InnerText.Replace(',', '.'), System.Globalization.CultureInfo.InvariantCulture);//parsing stuff
                    break;
                case MemoryMode.SET:
                    for (int l = 0; l < Neurons.Length; ++l)
                        for (int k = 0; k < numofprevneurons; ++k)
                            memory_el.ChildNodes.Item(k + numofprevneurons * l).InnerText = Neurons[l].Weights[k].ToString();
                    break;
            }
            memory_doc.Save($"{type}_memory.xml");
            WriteLine($"{type} weights have been initialized...");
            return _weights;
        }
        abstract public void Recognize(Network net, Layer nextLayer);//для прямых проходов
        abstract public double[] BackwardPass(double[] stuff);//и обратных
    }

Соль абстрактных классов

Класс Layer — это абстрактный класс, поэтому нельзя создавать его экземпляры. Это значит, что наше желание сохранить свойства "слоя" выполняется путём наследования родительского конструктора через ключевое слово base и пустой конструктор наследника в одну строчку(ибо вся логика конструктора определена в базовом классе, и её не надо переписывать).


Теперь непосредственно классы-наследники: Hidden и Output. Сразу два класса в цельном куске кода.


class HiddenLayer : Layer
    {
        public HiddenLayer(int non, int nopn, NeuronType nt, string type) : base(non, nopn, nt, type){}
        public override void Recognize(Network net, Layer nextLayer)
        {
            double[] hidden_out = new double[Neurons.Length];
            for (int i = 0; i < Neurons.Length; ++i)
                hidden_out[i] = Neurons[i].Output;
            nextLayer.Data = hidden_out;
        }
        public override double[] BackwardPass(double[] gr_sums)
        {
            double[] gr_sum = null;
            //сюда можно всунуть вычисление градиентных сумм для других скрытых слоёв
            //но градиенты будут вычисляться по-другому, то есть
            //через градиентные суммы следующего слоя и производные
            for (int i = 0; i < numofneurons; ++i)
                for (int n = 0; n < numofprevneurons; ++n)
                    Neurons[i].Weights[n] += learningrate * Neurons[i].Inputs[n] * Neurons[i].Gradientor(0, Neurons[i].Derivativator(Neurons[i].Output), gr_sums[i]);//коррекция весов
            return gr_sum;
        }
    }
class OutputLayer : Layer
    {
        public OutputLayer(int non, int nopn, NeuronType nt, string type) : base(non, nopn, nt, type){}
        public override void Recognize(Network net, Layer nextLayer)
        {
            for (int i = 0; i < Neurons.Length; ++i)
                net.fact[i] = Neurons[i].Output;
        }
        public override double[] BackwardPass(double[] errors)
        {
            double[] gr_sum = new double[numofprevneurons];
            for (int j = 0; j < gr_sum.Length; ++j)//вычисление градиентных сумм выходного слоя
            {
                double sum = 0;
                for (int k = 0; k < Neurons.Length; ++k)
                    sum += Neurons[k].Weights[j] * Neurons[k].Gradientor(errors[k], Neurons[k].Derivativator(Neurons[k].Output), 0);//через ошибку и производную
                gr_sum[j] = sum;
            }
            for (int i = 0; i < numofneurons; ++i)
                for (int n = 0; n < numofprevneurons; ++n)
                    Neurons[i].Weights[n] += learningrate * Neurons[i].Inputs[n] * Neurons[i].Gradientor(errors[i], Neurons[i].Derivativator(Neurons[i].Output), 0);//коррекция весов
            return gr_sum;
        }
    }

В принципе, всё самое важное я описал в комментариях. У нас есть все компоненты: обучающие и тестовые данные, вычислительные элементы, их "конгламераты". Теперь настало время всё связать обучением. Алгоритм обучения — backpropagation, следовательно критерий останова выбираю я, и выбор мой — есть преодоление порогового значения среднеквадратичной ошибки по эпохе, которое я выбрал равным 0.001. Для поставленной цели я написал класс Network, описывающий состояние сети, которое принимается в качестве параметра многих методов, как вы могли заметить.


class Network
    {
        //все слои сети
        InputLayer input_layer = new InputLayer();
        public HiddenLayer hidden_layer = new HiddenLayer(4, 2, NeuronType.Hidden, nameof(hidden_layer));
        public OutputLayer output_layer = new OutputLayer(2, 4, NeuronType.Output, nameof(output_layer));
        //массив для хранения выхода сети
        public double[] fact = new double[2];//не ругайте за 2 пожалуйста
        //ошибка одной итерации обучения
        double GetMSE(double[] errors)
        {
            double sum = 0;
            for (int i = 0; i < errors.Length; ++i)
                sum += Pow(errors[i], 2);
            return 0.5d * sum;
        }
        //ошибка эпохи
        double GetCost(double[] mses)
        {
            double sum = 0;
            for (int i = 0; i < mses.Length; ++i)
                sum += mses[i];
            return (sum / mses.Length);
        }
        //непосредственно обучение
        static void Train(Network net)//backpropagation method
        {
            const double threshold = 0.001d;//порог ошибки
            double[] temp_mses = new double[4];//массив для хранения ошибок итераций
            double temp_cost = 0;//текущее значение ошибки по эпохе
            do
            {
                for (int i = 0; i < net.input_layer.Trainset.Length; ++i)
                {
                    //прямой проход
                    net.hidden_layer.Data = net.input_layer.Trainset[i].Item1;
                    net.hidden_layer.Recognize(null, net.output_layer);
                    net.output_layer.Recognize(net, null);
                    //вычисление ошибки по итерации
                    double[] errors = new double[net.input_layer.Trainset[i].Item2.Length];
                    for (int x = 0; x < errors.Length; ++x)
                        errors[x] = net.input_layer.Trainset[i].Item2[x] - net.fact[x];
                    temp_mses[i] = net.GetMSE(errors);
                    //обратный проход и коррекция весов
                    double[] temp_gsums = net.output_layer.BackwardPass(errors);
                    net.hidden_layer.BackwardPass(temp_gsums);
                }
                temp_cost = net.GetCost(temp_mses);//вычисление ошибки по эпохе
                //debugging
                WriteLine($"{temp_cost}");
            } while (temp_cost > threshold);
            //загрузка скорректированных весов в "память"
            net.hidden_layer.WeightInitialize(MemoryMode.SET, nameof(hidden_layer));
            net.output_layer.WeightInitialize(MemoryMode.SET, nameof(output_layer));
        }
        //тестирование сети
        static void Test(Network net)
        {
            for (int i = 0; i < net.input_layer.Trainset.Length; ++i)
            {
                net.hidden_layer.Data = net.input_layer.Trainset[i].Item1;
                net.hidden_layer.Recognize(null, net.output_layer);
                net.output_layer.Recognize(net, null);
                for (int j = 0; j < net.fact.Length; ++j)
                    WriteLine($"{net.fact[j]}");
                WriteLine();
            }
        }
        //запуск сети
        static void Main(string[] args)
        {
            Network net = new Network();
            Train(net);
            Test(net);
            ReadKey();//чтоб консоль не закрывалась :)
        }
    }

Результат обучения.
image


Итого, путём насилования мозга несложных манипуляций, мы получили основу работающей нейронной сети. Для того, чтобы заставить её делать что-либо другое, достаточно поменять класс InputLayer и подобрать параметры сети для новой задачи. Через время(какое конкретно не знаю) напишу продолжение этой статьи с руководством по созданию с нуля свёрточной нейронной сети на C# и здесь сделаю апдейт этой с ссылками на MLP-рекогнитор картинок MNIST(но это не точно) и код статьи на Python(точно, но дольше ждать).


За сим всё, буду рад ответить на вопросы в комментариях, а пока извольте, новые дела ждут.
P.S.: Для желающих помацать код клацать.
P.P.S.: Сеть по ссылке выше — потненькая необученная няша-стесняша.

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


  1. Wingear
    07.08.2017 13:13
    +1

    Вторая ссылка, где про почитать, вдет туда же, что и первая.


    1. Stefanio Автор
      07.08.2017 17:03

      Спасибо, исправил.


  1. Rambalac
    07.08.2017 13:13
    -10

    Математик?


    20% кода можно было сократить linq.
    Что за ссылка на какой-то левый и невероятно тормозной "yadi.sk"?


    1. MoreBeauty
      07.08.2017 13:31
      +6

      Можно сократить в linq и сеть потеряет в производительности.


      1. masai
        07.08.2017 21:07
        +2

        Справедливости ради хочу заметить, что странно говорить о падении производительности от использования Linq на сети с 16 (шестнадцатью) параметрами. Вы просто не почувствуете разницу.


        1. MoreBeauty
          08.08.2017 05:12
          +3

          Это же проект для тренировки. Позже окажется, что добавив немного кода, можно получить сеть поумнее. И так по нарастающей. Я видел, как такие вот «программулины для примера» превращаются в коммерческое ПО. Вот только оптимизация уже отнимает больше сил и времени. Поэтому изначально лучше не сокращать код жертвуя производительностью, особенно когда он не упрощается, а именно сокращается. Неужели linq тут выглядел бы понятнее? Ну разве что чуть-чуть.


  1. MoreBeauty
    07.08.2017 13:34
    +4

    Даже такой простой код написан так, что и комментарии не помогают читать его не возвращаясь несколько раз назад. :(

    Я не перфекционист, но блин… )


  1. Tiberiumk
    07.08.2017 14:52
    +11

    Очень интересный путь к проекту, который видно в заголовке консоли :)


    1. temaemelyan
      07.08.2017 17:02
      +8

      Мамка не разрешает прогать, и он прячет VS похлеще порнухи =)


      1. Stefanio Автор
        07.08.2017 17:03
        +2

        Развеселило)


  1. marckel
    07.08.2017 17:03
    +1

    скромный школьный проект…


  1. DenisXINVEST
    07.08.2017 20:29
    +2

    О, ссылка на моё видео) автор, посмотри OpenCL под C#, возможно это ускорит твою сеть. =)


  1. masai
    07.08.2017 21:04
    +1

    Вставлю-ка и я свои пять копеек.


    "Нейронные сети. Полный курс" Саймона Хайкина к прочтению в том случае, если вам придётся столкнуться с применением/написанием/разработкой нейросетей и прочего подобного stuff'а. Хотя в ней нет материала про новомодные свёрточные сети, никто не мешает загуглить лекции от какого-нибудь харизматичного работника Yandex/Mail.ru/etc. никто не мешает.

    Книжка Хайкина, конечно, классная, но порядком подустарела. Не зря свёрточные сети так популярны — они реально работают на больших данных. Персептроном на практике много не насчитаешь.


    Я мог использовать, всякие Theano и Tensor Flow, НО под капотом моей смерть-машины в моём ноутбуке стоит "красная" видеокарта без особой поддержки API, через которые обращаются к мощи GPU(ведь именно их и используют Theano/Tensor Flow/etc.).

    Они прекрасно работают на CPU. Но медленнее, конечно. Хотя для того же распознавания циферок из MNIST вполне и CPU хватит. Если персептрон, то за пару минут обучится.


    Я не буду использовать смещения, потому что просто не хочу.

    Если бы вы обучали не XOR(a, b), а NOT(XOR(a, b)), то без смещений у вас бы мало что вышло.


    ReadKey();//чтоб консоль не закрывалась :)

    Чтоб консоль не закрывалась можно нажать Ctrl-F5 в Visual Studio. В консольных приложениях обычно паузу в конце не ставят, ведь их запускают из консоли. ;)


    По поводу самой статьи. Слишком много шуток-прибауток и прочей мишуры и слишком мало разговоров по сути. Если бы я всё это не знал, мне было бы сложно разобраться.


    1. Stefanio Автор
      07.08.2017 21:38

      Если бы вы обучали не XOR(a, b), а NOT(XOR(a, b)), то без смещений у вас бы мало что вышло.

      Если бы вы были повнимательнее, то заметили бы, что я обучил сеть не только XOR, но и NXOR. И это «вышло» без смещений. Нет, ну правда, зачем мне 2 нейрона на выходном слое?
      Книжка Хайкина, конечно, классная, но порядком подустарела. Не зря свёрточные сети так популярны — они реально работают на больших данных. Персептроном на практике много не насчитаешь.

      Я рассматриваю книжку Хайкина не в качестве «пилюли от всех болезней», а лишь как учебное пособие для построения фундамента знаний по этой теме. Я знаю, что свёрточные сети показывают результаты намного лучше, поэтому я напишу продолжение этой статьи об этом виде сетей.
      Слишком много шуток-прибауток и прочей мишуры и слишком мало разговоров по сути.

      Сама по себе статья не очень большая, код занимает ~35%. Так что сколько сути, столько и разговоров о ней) А шуток-прибауток, соответственно, ещё меньше.

      Все равно спасибо за замечания, учёл для написания будущих материалов)


      1. rpsv
        08.08.2017 07:42
        +1

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

        Если она будет в стиле данной статьи, то не стоит.
        Вы пробежались по верхам темы, не очень запарились с реализацией, и как бы качество материалы вышло «тяпь-ляпь».
        Лучше убейте пару тройку дней (недель) на более плотное изучение материала, чтобы можно было лаконично и понятно для всех все это изложить и показать качественный код.
        А то так ценность статьи дико падает, т.к. статей уже куча на эту тему, и все реализуют персептрон для XOR.
        Хорош уже приводить академические примеры, персептрон можно легко и непринужденно использовать для управления и моделирование процессами (именно процессами, а не операциями типа XOR).

        Сама по себе статья не очень большая, код занимает ~35%. Так что сколько сути, столько и разговоров о ней) А шуток-прибауток, соответственно, ещё меньше.

        Теории в ней очень мало.
        Почему структура именно такая а не иная? (потому что так в книжке написано?)
        Почему функция активации именно такая и чем она лучше других? (а кстати каких?)
        Каково качество обучения и обобщения полученной сети?
        Все эти вопросы быстро бы возникли, если бы взяли задачу посложнее XOR.
        Как по мне, все быстренько реализуют XOR, считают что карасавчеги и потом пишут в резюме «нейронные сети», а по факту когда встает реальная задача, возникает куча вопросов, на которые они не могут дать ответы и бегут читать книжку Хайкена.
        Грустно товарищи, грустно!


      1. masai
        08.08.2017 10:56

        Если бы вы были повнимательнее, то заметили бы, что я обучил сеть не только XOR, но и NXOR. И это «вышло» без смещений. Нет, ну правда, зачем мне 2 нейрона на выходном слое?

        Да, в самом деле невнимательно читал. XAND — редкое обозначение на самом деле, не обратил внимание.


        Но разве у вас «вышло»? Вот 4 нейрона в скрытом слое, скажем, зачем вам? Если сеть для XOR требует 3 нейронов максимум на всё (ну или 4, если хотите ещё второй выход добавить). Связей больше, обучается медленнее.


        Ну вот сами посудите. Допустим, на входе все нули. Если нет смещений, на выходах нейронов первого слоя всегда будет 0.5. И за всех начинает отдуваться скрытый слой. Если добавлять нейроны, то, подозреваю, некоторые просто начнут играть роль смещения.


        Вот вы раз в Unity программируете сами же видели, что в компьютерной графике матрицы 4?4, хотя координат 3. Вот это как раз из-за смещений. Чтобы вместо аффинных преобразований были линейные. С ними проще и лучше.


    1. rpsv
      08.08.2017 07:35
      +1

      Книжка Хайкина, конечно, классная, но порядком подустарела. Не зря свёрточные сети так популярны — они реально работают на больших данных. Персептроном на практике много не насчитаешь.

      Сверточные сети разве не для ограниченного круга задач (свертки изображений по средствам фильтра)
      Это вопрос, и это не точно)))

      Даже сверточные сети по итогу сводятся к персептрону, так что я думаю немного не корректно писать что перцептрон не очень.


      1. masai
        08.08.2017 10:46

        Сверточные сети разве не для ограниченного круга задач (свертки изображений по средствам фильтра)
        Это вопрос, и это не точно)))

        Про свёрточные сети я вспомнил, так как о них вспомнил автор. А круг у них хоть и ограничен, но очень широк. Я занимаюсь цифровой обработкой изображений с их помощью. Мне для работы хватает и ещё остаётся. :) Конечно, есть и другие архитектуры. Скажем, в обработке текста популярны LSTM/GRU.


        Даже сверточные сети по итогу сводятся к персептрону, так что я думаю немного не корректно писать что перцептрон не очень.

        Ну, не сказал бы, что прямо так сводятся. Архитектуры разные. Разве что вы какой-то обобщённый персептрон имеете в виду. Иначе можно было бы говорить, что все сети вообще сводятся к матрицам, но мы же не говорим о матрицах.


        Персептрон в самом деле не очень, так как глубокую сеть пришлось бы обучать до скончания веков. Глубокое обучение и интерес к нейронным сетям появился именно после развития других архитектур. Хотя, конечно, как элемент большой сети персептроны используются. Например, как финальные слои свёрточного классификатора.


        Хотя, конечно, это моё мнение, и все рассуждения в контексте именно глубокого обучения. А так персептроны можно много где использовать, если задача не слишком большая и сложная.


  1. AdAbsurdum
    07.08.2017 21:08
    +1

    Можно ещё добавить в коррекцию весов предыдущее изменение веса с некоторым коэффициентом, как в пункте 4 на вики.


    1. masai
      07.08.2017 21:12

      Вот, кстати, хорошее замечание. Инерция в стохастическом градиентном спуске заметно ускоряет сходимость.


    1. Stefanio Автор
      07.08.2017 21:36

      Для такой сложной задачи на мой взгляд не требуется добавление момента инерционности. Вот в апдейте с рекогнитором картинок это будет.


      1. masai
        08.08.2017 15:12

        рекогнитором

        Классификатором. Ну или хотя бы рекогнайзером, хотя никто так их не называет. (Неологизмы Derivativator и Gradientor тоже порядком удивили.)


        1. Stefanio Автор
          08.08.2017 16:43

          Всё началось с активатора и сумматора)


  1. Benedict
    07.08.2017 22:17

    Справедливости ради стоит отметить что на самом деле у Хайкина все-таки есть раздел про сверточные сети, хоть и небольшой. См. 4.19 во втором издании, 4.17 в третьем


    1. Stefanio Автор
      07.08.2017 23:37

      Информация о сетях свёртки в книге Хайкина, фактически, просто свидетельствует о существовании такого рода сетей.


    1. masai
      08.08.2017 15:08

      Нынешние свёрточные сети во многом выросли из неокогнитрона, которому сто лет в обед. Так что они и в старых книжка упоминаются. Но подходы, которые сейчас стали повсеместными, появились сравнительно недавно. Скажем, ResNet в 2015, GAN в 2014 (опять же, я про обработку изображений больше, так как в этой области сейчас работаю). И всё это постоянно развивается и совершенствуется. На arXiv ежедневно что-то интересное и новое появляется, не успеваешь читать.


      Хотя спорить не буду, когда-то основы по Хайкину и сам учил. Но вот не пригодилось.


      P.S. Тут, книжку Гудфеллоу по глубокому обучению на русский перевели. Я теперь, если спрашивают, её рекомендую. А ещё лучше бесплатно на английском, так как перевод косячный местами.


  1. rpsv
    08.08.2017 07:30

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

    В код не вдавался, кинул взгляд, но вот как то так, дерзайте :-)


  1. G-M-A-X
    08.08.2017 10:25
    -2

    1. У нейросетей бывает когнитивный диссонанс? :)
    2. Не кажется ли хабровчанам, что люди — тоже нейросети? :)