// НС с выбором основных параметров: число скрытых слоев, количество нейронов на каждом слое
        public NeuralNetwork(List <ReadMnist.DigitImage> _patterns, int[] cntNeuronsOnHiddenLayers, int outSize, List <ReadMnist.DigitImage> _tests)
        {
            patterns = _patterns;
            tests    = _tests;
            int outLayerSize = outSize;
            int inputSize    = patterns.Count;

            if (isBias)
            {
                inputSize++;
            }

            input = new NNLayer(inputSize, 0, 'l');
            int prevSize = inputSize;

            foreach (int size in cntNeuronsOnHiddenLayers)
            {
                int sizeLayer = size;

                if (isBias)
                {
                    sizeLayer++;
                }

                hidden.Add(new NNLayer(sizeLayer, prevSize, 's'));
                prevSize = sizeLayer;
            }
            output = new NNLayer(outLayerSize, prevSize, 's');

            cntLayers = 2 + hidden.Count;
        }
        // НС по умолчанию с 1 скрытым слоем
        public NeuralNetwork(List <ReadMnist.DigitImage> _patterns, int _cntHiddenLayers, int[] _cntNeuronsOnHiddenLayers, List <ReadMnist.DigitImage> _tests)
        {
            int outLayerSize; // Размер выходного слоя
            int sizeLayer;    // Размер текущего слоя (кол-во нейронов на нем);

            cntLayers    = 2 + _cntHiddenLayers;
            patterns     = _patterns;
            tests        = _tests;
            outLayerSize = sizeLayer = trainSetSize = patterns[0].Pixels.Count;

            if (isBias)
            {
                sizeLayer++;
            }

            input = new NNLayer(sizeLayer, 0, 'l');
            hidden.Add(new NNLayer(sizeLayer, sizeLayer, 's'));
            output = new NNLayer(10, sizeLayer, 's');
        }
        // По значениям предыдущего слоя получаем вход для следующего (Сумма произведений сигнал * вес)
        // Манипуляции с весами, сигналами и функцией активации
        private List <double> FromInputToOutputOnNeurons(List <double> inputInfo, NNLayer nextLayer)
        {
            List <double> outputInfo = new List <double>(nextLayer.cntNeurons);

            for (int i = 0; i < nextLayer.cntNeurons; ++i)
            {
                double sum            = 0;
                int    lastInputIndex = inputInfo.Count - 1;
                for (int j = 0; j < lastInputIndex; ++j)
                {
                    double signal = inputInfo[j];
                    sum += signal * nextLayer.weight[i * inputInfo.Count + j];
                }

                // Если есть нейрона смещения, то добавляем вес нейрона смещения,
                // иначе - вес последнего нейрона в слое
                sum += (isBias ? 1 : inputInfo[lastInputIndex]) *
                       nextLayer.weight[i * inputInfo.Count + lastInputIndex];

                outputInfo.Add(nextLayer.func(sum));
            }
            return(outputInfo);
        }
        // Изменение весов
        // Метод обратного распространения ошибки (один обратный проход с изменением весов и подсчетом дельт)
        private void ChangeWeights(List <double> idealOutput, List <double> outputInfo)
        {
            NNLayer       nextLayer = output;
            List <double> deltaNext = new List <double>(idealOutput.Count);

            // Производная ф-ии активации от входного значения нейрона
            double fin = 0;

            // Находим дельту для выходного слоя
            // delta = (OUTideal - OUTcurrent) * fin
            for (int i = 0; i < idealOutput.Count; ++i)
            {
                // Производная ф-ии активации (здесь : гиперболический тангенс)
                fin = GetDerivativeFuncActivation('s', outputInfo[i]);
                deltaNext.Add((idealOutput[i] - outputInfo[i]) * fin);
            }

            // Находим дельты для скрытых слоев
            for (int i = hidden.Count - 1; i >= -1; --i)
            {
                NNLayer currentLayer;
                currentLayer = i != -1 ? hidden[i] : input;
                List <double> newWeights = new List <double> (nextLayer.weight.Count);
                int           index      = 0;

                // пересчет веса (на каждой связи нейронов текущего слоя и нейронов следующего слоя)
                for (int k = 0; k < nextLayer.cntNeurons; ++k)
                {
                    for (int j = 0; j < currentLayer.cntNeurons; ++j)
                    {
                        // градиент j, k
                        double grad = deltaNext[k] * currentLayer.outputs[j];
                        // дельта веса
                        double deltaWeight = learningRate * grad;
                        newWeights.Add(nextLayer.weight[index++] + deltaWeight);
                    }
                }
                // ---

                List <double> deltaCurrent = new List <double>(deltaNext);
                deltaNext.Clear();

                // Подстчет дельт данного слоя
                // delta = (fin * sum(W[i] * Delta[i]))
                for (int j = 0; j < currentLayer.cntNeurons; ++j)
                {
                    double sum = 0;
                    for (int k = 0; k < nextLayer.cntNeurons; ++k)
                    {
                        sum += nextLayer.weight[currentLayer.cntNeurons * k + j] * deltaCurrent[k];
                    }

                    // Производная ф-ии активации (здесь : гиперболический тангенс)
                    fin = GetDerivativeFuncActivation('s', currentLayer.outputs[j]);
                    deltaNext.Add(fin * sum);
                }
                for (int wI = 0; wI < nextLayer.cntWeights; ++wI)
                {
                    nextLayer.weight[wI] = newWeights[wI];
                }
                nextLayer = currentLayer;
                // ---
            }
        }