Как я могу гарантировать, что конвергенция всегда происходит в нейронной сети, решающей XOR? - PullRequest
0 голосов
/ 10 октября 2019

У меня есть сеть с двумя входами, двумя скрытыми узлами в одном слое и узлом вывода.

NN Topography

Я пытаюсь решитьПроблема XOR:

| i0 | i1 | desired output |
----------------------------
| 0  | 0  |        0       |
| 1  | 0  |        1       |
| 0  | 1  |        1       |
| 1  | 1  |        0       |

С моим текущим кодом я запускаю все 4 записи выше в одну эпоху. Затем я повторяю эпоху 20000 раз. Я вычисляю ошибку после каждой записи, а не каждой эпохи, и я распространяю ошибку в это же время.

Я использую только сигмоид в выходном слое, так как я понимаю, что хочу получить результат между 0и 1.

Моя сеть большую часть времени сходится. В других случаях это не так.

Я пытался использовать сигмоид и танх в скрытом слое, но ни один из них не гарантирует конвергенции.

Я пытался случайным образом генерировать веса между 0и 1, а также между -1 и 1 с использованием равномерного распределения. Я попытался использовать Xavier Initialisation как для равномерного, так и для нормального распределения. Кажется, что ни один из них не препятствует сближению сети. Я пробовал разные комбинации функции активации и генерации веса.

Вот мой полный код:

#include <iostream>
#include <array>
#include <random>
#include <chrono>
#include <iomanip>
#include <fstream>
#include <algorithm>
#include <iomanip>

typedef float DataType;
typedef DataType (*ActivationFuncPtr)(const DataType&);

const DataType learningRate = std::sqrt(2.f);
const DataType momentum = 0.25f;
const std::size_t numberEpochs = 20000;

DataType sigmoid(const DataType& x)
{
    return DataType(1) / (DataType(1) + std::exp(-x));
}

DataType sigmoid_derivative(const DataType& x)
{
    return x * (DataType(1) - x);
}

DataType relu(const DataType& x)
{
    return x <= 0 ? 0 : x;
}

DataType relu_derivative(const DataType& x)
{
    return x <= 0 ? 0 : 1;
}

DataType tanh(const DataType& x)
{
    return std::tanh(x);
}

DataType tanh_derivative(const DataType& x)
{
    return DataType(1) - x * x;
}

DataType leaky_relu(const DataType& x)
{
    return x <= 0 ? DataType(0.01) * x : x;
}

DataType leaky_relu_derivative(const DataType& x)
{
    return x <= 0 ? DataType(0.01) : 1;
}

template<std::size_t NumInputs>
class Neuron
{
public:

    Neuron(ActivationFuncPtr activationFunction, ActivationFuncPtr derivativeFunc)
    :
        m_activationFunction(activationFunction),
        m_derivativeFunction(derivativeFunc)
    {
        RandomiseWeights();
    }

    void RandomiseWeights()
    {
        std::generate(m_weights.begin(),m_weights.end(),[&]()
        {
            return m_xavierNormalDis(m_mt);
        });
        m_biasWeight = m_xavierNormalDis(m_mt);

        for(std::size_t i = 0; i < NumInputs+1; ++i)
            m_previousWeightUpdates[i] = 0;
    }

    void FeedForward(const std::array<DataType,NumInputs>& inputValues)
    {
        DataType sum = m_biasWeight;
        for(std::size_t i = 0; i < inputValues.size(); ++i)
            sum += inputValues[i] * m_weights[i];
        m_output = m_activationFunction(sum);

        m_netInput = sum;
    }

    DataType GetOutput() const
    {
        return m_output;
    }

    DataType GetNetInput() const
    {
        return m_netInput;
    }

    std::array<DataType,NumInputs> Backpropagate(const DataType& error,
                           const std::array<DataType,NumInputs>& inputValues,
                           std::array<DataType,NumInputs+1>& weightAdjustments)
    {
        DataType errorOverOutput = error;
        DataType outputOverNetInput = m_derivativeFunction(m_output);

        std::array<DataType,NumInputs> netInputOverWeight;
        for(std::size_t i = 0; i < NumInputs; ++i)
        {
            netInputOverWeight[i] = inputValues[i];
        }

        DataType netInputOverBias = DataType(1);

        std::array<DataType,NumInputs> errorOverWeight;
        for(std::size_t i = 0; i < NumInputs; ++i)
        {
            errorOverWeight[i] = errorOverOutput * outputOverNetInput * netInputOverWeight[i];
        }

        DataType errorOverBias = errorOverOutput * outputOverNetInput * netInputOverBias;

        for(std::size_t i = 0; i < NumInputs; ++i)
        {
            weightAdjustments[i] = errorOverWeight[i];
        }
        weightAdjustments[NumInputs] = errorOverBias;

        DataType errorOverNetInput = errorOverOutput * outputOverNetInput;

        std::array<DataType,NumInputs> errorWeights;
        for(std::size_t i = 0; i < NumInputs; ++i)
        {
            errorWeights[i] = errorOverNetInput * m_weights[i];
        }

        return errorWeights;
    }

    void AdjustWeights(const std::array<DataType,NumInputs+1>& adjustments)
    {
        for(std::size_t i = 0; i < NumInputs; ++i)
        {
            m_weights[i] = m_weights[i] - learningRate * adjustments[i] + momentum * m_previousWeightUpdates[i];
            m_previousWeightUpdates[i] = learningRate * adjustments[i] + momentum * m_previousWeightUpdates[i];
        }
        m_biasWeight = m_biasWeight - learningRate * adjustments[NumInputs] + momentum * m_previousWeightUpdates[NumInputs];
        m_previousWeightUpdates[NumInputs] = learningRate * adjustments[NumInputs] + momentum * m_previousWeightUpdates[NumInputs];
    }

    const std::array<DataType,NumInputs>& GetWeights() const { return m_weights; }
    const DataType& GetBiasWeight() const { return m_biasWeight; }

protected:

    static std::mt19937 m_mt;
    static std::uniform_real_distribution<DataType> m_uniformDisRandom;
    static std::uniform_real_distribution<DataType> m_xavierUniformDis;
    static std::normal_distribution<DataType> m_xavierNormalDis;

    std::array<DataType,NumInputs> m_weights;
    DataType m_biasWeight;

    ActivationFuncPtr m_activationFunction;
    ActivationFuncPtr m_derivativeFunction;

    DataType m_output;
    DataType m_netInput;
    std::array<DataType,NumInputs+1> m_previousWeightUpdates;
};

template<std::size_t NumInputs>
std::mt19937 Neuron<NumInputs>::m_mt(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count());

template<std::size_t NumInputs>
std::uniform_real_distribution<DataType> Neuron<NumInputs>::m_uniformDisRandom(-1,1);

template<std::size_t NumInputs>
std::uniform_real_distribution<DataType> Neuron<NumInputs>::m_xavierUniformDis(-std::sqrt(6.f / NumInputs+1),std::sqrt(6.f / NumInputs+1));

template<std::size_t NumInputs>
std::normal_distribution<DataType> Neuron<NumInputs>::m_xavierNormalDis(0,std::sqrt(2.f / NumInputs+1));

main()
{
    std::ofstream file("error_out.csv", std::ios::out | std::ios::trunc);
    if(!file.is_open())
    {
        std::cout << "couldn't open file" << std::endl;
        return 0;
    }

    file << std::fixed << std::setprecision(80);

    std::array<std::array<DataType,2>,4> inputData = {{{0,0},{0,1},{1,0},{1,1}}};
    std::array<std::array<DataType,1>,4> desiredOutputs = {{{0},{1},{1},{0}}};
    std::array<Neuron<2>*,2> hiddenLayer1 =
    {{
        new Neuron<2>(tanh, tanh_derivative),
        new Neuron<2>(tanh, tanh_derivative)
    }};
    std::array<Neuron<2>*,1> outputLayer =
    {{
        new Neuron<2>(sigmoid, sigmoid_derivative)
    }};

    std::cout << std::fixed << std::setprecision(80);

    std::cout << "Initial Weights: " << std::endl;
    const std::array<DataType,2>& outputWeights = outputLayer[0]->GetWeights();
    const DataType& outputBias = outputLayer[0]->GetBiasWeight();
    const std::array<DataType,2>& hidden1Weights = hiddenLayer1[0]->GetWeights();
    const DataType& hidden1Bias = hiddenLayer1[0]->GetBiasWeight();
    const std::array<DataType,2>& hidden2Weights = hiddenLayer1[1]->GetWeights();
    const DataType& hidden2Bias = hiddenLayer1[1]->GetBiasWeight();

    std::cout   << "W0: " << hidden1Weights[0] << "\n"
                << "W1: " << hidden1Weights[1] << "\n"
                << "B0: " << hidden1Bias << "\n"
                << "W2: " << hidden2Weights[0] << "\n"
                << "W3: " << hidden2Weights[1] << "\n"
                << "B1: " << hidden2Bias << "\n"
                << "W4: " << outputWeights[0] << "\n"
                << "W5: " << outputWeights[1] << "\n"
                << "B2: " << outputBias << "\n" << std::endl;

    DataType finalMSE = 0;

    std::size_t epochNumber = 0;
    while(epochNumber < numberEpochs)
    {
        DataType epochMSE = 0;

        for(std::size_t row = 0; row < inputData.size(); ++row)
        {
            const std::array<DataType,2>& dataRow = inputData[row];
            const std::array<DataType,1>& outputRow = desiredOutputs[row];

            // Feed the values through to the output layer

            hiddenLayer1[0]->FeedForward(dataRow);
            hiddenLayer1[1]->FeedForward(dataRow);

            DataType output0 = hiddenLayer1[0]->GetOutput();
            DataType output1 = hiddenLayer1[1]->GetOutput();

            outputLayer[0]->FeedForward({output0,output1});

            DataType finalOutput0 = outputLayer[0]->GetOutput();

            // if there was more than 1 output neuron these errors need to be summed together first to create total error
            DataType totalError = 0.5 * std::pow(outputRow[0] - finalOutput0,2.f);
            epochMSE += totalError * totalError;

            DataType propagateError = -(outputRow[0] - finalOutput0);

            std::array<DataType,3> weightAdjustmentsOutput;
            std::array<DataType,2> outputError = outputLayer[0]->Backpropagate(propagateError,
                                                                   {output0,output1},
                                                                   weightAdjustmentsOutput);

            std::array<DataType,3> weightAdjustmentsHidden1;
            hiddenLayer1[0]->Backpropagate(outputError[0],dataRow,weightAdjustmentsHidden1);

            std::array<DataType,3> weightAdjustmentsHidden2;
            hiddenLayer1[1]->Backpropagate(outputError[1],dataRow,weightAdjustmentsHidden2);

            outputLayer[0]->AdjustWeights(weightAdjustmentsOutput);
            hiddenLayer1[0]->AdjustWeights(weightAdjustmentsHidden1);
            hiddenLayer1[1]->AdjustWeights(weightAdjustmentsHidden2);
        }

        epochMSE *= DataType(1) / inputData.size();

        file << epochNumber << "," << epochMSE << std::endl;
        finalMSE = epochMSE;

        ++epochNumber;
    }

    std::cout << std::fixed << std::setprecision(80)
                << "\n\n====================================\n"
                << "   TRAINING COMPLETE"
                << "\n\n====================================" << std::endl;
    std::cout << "Final Error: " << finalMSE << std::endl;
    std::cout << "Number epochs: " << epochNumber << "/" << numberEpochs << std::endl;

    // output tests
    std::cout << std::fixed << std::setprecision(2)
                << "\n\n====================================\n"
                << "   FINAL TESTS"
                << "\n\n====================================" << std::endl;

    for(std::size_t row = 0; row < inputData.size(); ++row)
    {
        const std::array<DataType,2>& dataRow = inputData[row];
        const std::array<DataType,1>& outputRow = desiredOutputs[row];
        std::cout << dataRow[0] << "," << dataRow[1] << " (" << outputRow[0] << ")  :  ";

        // Feed the values through to the output layer

        hiddenLayer1[0]->FeedForward(dataRow);
        hiddenLayer1[1]->FeedForward(dataRow);

        DataType output0 = hiddenLayer1[0]->GetOutput();
        DataType output1 = hiddenLayer1[1]->GetOutput();

        outputLayer[0]->FeedForward({output0,output1});

        DataType finalOutput0 = outputLayer[0]->GetOutput();

        std::cout << finalOutput0 << std::endl;
    }

    file.close();

    return 0;
}

Когда все работает, я получаю вывод вроде:

====================================
   FINAL TESTS

====================================
0.00,0.00 (0.00)  :  0.00
0.00,1.00 (1.00)  :  0.99
1.00,0.00 (1.00)  :  0.99
1.00,1.00 (0.00)  :  0.00

Когда он не работает, я получаю вывод:

====================================
   FINAL TESTS

====================================
0.00,0.00 (0.00)  :  0.57
0.00,1.00 (1.00)  :  0.57
1.00,0.00 (1.00)  :  1.00
1.00,1.00 (0.00)  :  0.00

Когда он работает, ошибка для каждой эпохи выглядит так:

enter image description here

Начальные веса были:

W0: -0.47551780939102172851562500000000000000000000000000000000000000000000000000000000
W1: 0.40949764847755432128906250000000000000000000000000000000000000000000000000000000
B0: 2.33756542205810546875000000000000000000000000000000000000000000000000000000000000
W2: 2.16713166236877441406250000000000000000000000000000000000000000000000000000000000
W3: -2.74766492843627929687500000000000000000000000000000000000000000000000000000000000
B1: 0.34863436222076416015625000000000000000000000000000000000000000000000000000000000
W4: -0.53460156917572021484375000000000000000000000000000000000000000000000000000000000
W5: 0.04940851405262947082519531250000000000000000000000000000000000000000000000000000
B2: 0.97842389345169067382812500000000000000000000000000000000000000000000000000000000

Но когда это не работает, ошибка для каждой эпохи выглядит так:

enter image description here

начальные веса в данном конкретном случае были:

W0: 1.16670060157775878906250000000000000000000000000000000000000000000000000000000000
W1: -2.37987256050109863281250000000000000000000000000000000000000000000000000000000000
B0: 0.41097882390022277832031250000000000000000000000000000000000000000000000000000000
W2: -0.23449644446372985839843750000000000000000000000000000000000000000000000000000000
W3: -1.99990248680114746093750000000000000000000000000000000000000000000000000000000000
B1: 1.77582693099975585937500000000000000000000000000000000000000000000000000000000000
W4: 1.98818421363830566406250000000000000000000000000000000000000000000000000000000000
W5: 2.71223402023315429687500000000000000000000000000000000000000000000000000000000000
B2: -0.79067271947860717773437500000000000000000000000000000000000000000000000000000000

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

Вопрос : Что я могу сделать, чтобы обеспечить сходимость?

Нужно ли изменить инициализацию веса? Нужно ли использовать разные функции активации? Нужно ли больше слоев или другое количество узлов?

1 Ответ

0 голосов
/ 10 октября 2019

Я не прочитал весь ваш код, потому что он довольно длинный, но:

  • Было бы неплохо иметь класс NeuralNetwork и класс Connection, чтобы не писать вселогика в main.
  • Мне нравится ActivationFuncPtr typedef, который вы можете использовать, чтобы попытаться смешать различные функции активации для разных Neurons (возможно, с генетическим алгоритмом)?

Теперь, чтобы ответить на ваш вопрос, на самом деле нет однозначных ответов, но я могу дать вам несколько советов:

  • Инициализация с заранее определенным набором весов действительно должна помочь предотвратить попадание в локальныйминимум. Вы можете попробовать разные наборы весов и посмотреть, какие из них работают лучше, и что происходит, когда вы меняете конкретный (вы все равно делаете контролируемое обучение.) Если вы проводите исследования, это даст вам несколько свободных абзацев;)
  • Различные функции активации обычно не сильно помогают в конвергенции, но стоит попробовать. Например, вы можете настроить сигмоид на 1/(1+exp(-4*x)), например, 4. Произвольный.
  • XOR был решен с меньшим количеством узлов, чем этот (см. Neat Paper, Neuro Evolution NN ). Увеличение количества узлов может затруднить сходимость.
  • Один (грязный) способ предотвратить раннюю сходимость - это обнаружить, что если вы упали в локальный минимум, то перезапустите с новыми случайными весами.
  • Другим способом было бы использование генетического алгоритма (я немного предвзят, потому что это моя область знаний).
...