C##에서 간단한 신경망 구현

우리는 종종 우리가 거인의 어깨 위에 서 있다는 사실을 잊습니다. 머신 러닝, 딥 러닝 및 인공 지능은 많은 프레임워크를 사용할 수 있을 만큼 견인력을 얻었습니다. 오늘날 우리가 선택한 프레임워크(예:  ML.NET )를 선택하고 우리가 해결하려는 문제에 완전히 초점을 맞춘 프로젝트를 시작하는 것은 정말 쉽습니다 . 그러나 때때로 우리가 사용하고 있는 것과 그것이 실제로 어떻게 작동하는지에 대해 멈추고 실제로 생각하는 것이 가장 좋습니다.

저는 기계 학습으로 전환하는 소프트웨어 개발자이기 때문에 객체 지향 프로그래밍과 C#을 사용하여 처음부터 신경망을 구축하기로 결정했습니다. 신경망의 구성 요소를 분해하고 이미 알고 있는 도구를 사용하여 이에 대해 자세히 알아보고 싶었기 때문입니다. 이렇게 해서 나는 두 가지가 아니라 한 가지를 배웠다. 그 이후로 저는 이 솔루션을 .NET 개발자에게 딥 러닝 개념을 설명하는 데 자주 사용했습니다. 그래서 이 글에서는 그 해결책을 소개하고자 한다.

이 문서에서는 다음 내용을 다룹니다.

  1. 인공 신경망과 객체 지향 프로그래밍?
  2. 인공 신경망 - 생물학에서 영감을 얻은 아이디어
  3. 인공 신경망의 주요 구성 요소
  4. 입력 기능 구현 활성화 기능 뉴런 연결 계층 모든 것을 하나로 모으기 작업 흐름

내가 이 분야에 깊이 있는 사람들에게 이 솔루션을 제안할 때마다 질문은 항상 "예, 하지만 왜 그렇습니까? 사실 이것은 신경망을 구축하는 전문적인 방법이 아닙니다. 예, 우리는 모든 가중치를 둘 수 있습니다. 편향은 행렬로 추상화됩니다 예, PyTorch와 같이 이러한 행렬에 대한 작업을 잘 처리하는 프레임워크를 사용할 수 있습니다.

예, 일부 사람들은 이 정신적 훈련이 불필요하다고 생각할 수 있습니다. 그들이 옳을 수도 있습니다. 그러나 저는 소프트웨어 개발 배경이 있는 사람들에게 기본 개념을 설명하고 심화하기 위해 이 솔루션을 몇 번이고 사용했고 그들은 그것을 좋아했습니다. 학습 곡선이 더 빠릅니다. 좋은 기반이 마련되면 추가 추상화가 나타납니다. 그러니 저를 참아주세요

이 기사에서는 Python 및 R에 대한 일반적인 스크립팅 관점 대신 덜 표준적이고 OO 접근 방식을 사용합니다. 이런 방식으로 이 구현을 수행하는 정말 좋은 기사가 있으며 살펴보는 것이 좋습니다.

내가 원하는 것은 각 구성 요소와 각 작업을 분리하는 것입니다. 생각 연습으로 시작한 것이 정말 멋진 미니 사이드 프로젝트로 성장했습니다. 다시 말하지만 이것은 일반적으로 네트워킹을 구현하는 방식이 아니라는 점을 강조하고 싶습니다. 전체 프로세스를 최적화하려면 더 많은 수학 및 행렬 곱셈 형식을 사용해야 합니다.

그 외에도 구현된 네트워크는 단순화되고 가장 기본적인 형태의 신경망을 나타냅니다. 그럼에도 불구하고 이러한 방식으로 인공 신경망의 모든 구성 요소와 요소를 볼 수 있고 개념에 더 익숙해질 수 있습니다.

ANN은 우리의 신경계에 흡수됩니다. 일반적인 생각은 우리가 뇌의 구조를 복제하면 학습 기계를 만들 수 있다는 것입니다. 아시다시피 신경계의 가장 작은 단위는 뉴런입니다. 이들은 유사하고 단순한 구조를 가진 세포입니다.

그러나 끊임없는 통신을 통해 이러한 셀은 엄청난 처리 능력을 얻습니다. 간단히 말해서 뉴런은 스위치일 뿐입니다. 이 스위치가 일정량의 입력 자극을 받으면 출력 신호가 생성됩니다. 이 출력 신호는 다른 뉴런에 대한 입력입니다.

각 뉴런에는 다음과 같은 구성 요소가 있습니다.

  • 가지 결정
  • 축삭

뉴런의 몸체(soma)는 뉴런의 기본 생명 과정을 실행합니다. 각 뉴런에는 축삭이 있습니다. 이것은 세포의 긴 부분이며, 실제로 일부는 척추 전체 길이에 걸쳐 있습니다. 마치 전선처럼 뉴런의 출력입니다. 반면에 수상돌기는 뉴런의 입력이며 각 뉴런에는 여러 개의 수상돌기가 있습니다. 서로 다른 뉴런의 이러한 입력 및 출력, 축색 돌기 및 수상 돌기는 서로 가까이 있어도 서로 닿지 않습니다.

축삭과 수상돌기 사이의 이러한 간격을 시냅스라고 합니다. 이러한 시냅스를 통해 신호는 신경전달물질 분자에 의해 전달됩니다. 다양한 신경 전달 물질 화학 물질이 있으며 각각 다른 유형의 뉴런을 제공합니다. 여기에는 유명한 세로토닌과 도파민이 포함됩니다. 이러한 화학 물질의 양과 유형은 뉴런 입력의 "강도"를 결정합니다. 그리고 모든 수상돌기에 충분한 입력이 있으면 세포체는 축삭에 신호를 "발사"하여 다음 뉴런으로 전달합니다.

코드를 살펴보기 전에 인공 신경망의 구조를 이해해 봅시다. 앞서 언급했듯이 인공 신경망은 생물학적으로 구동됩니다. 즉, 실제 신경계의 동작을 모방하려고 합니다.

실제 신경계에서 가장 작은 빌딩 블록이 뉴런 인 것처럼 인공 신경망도 마찬가지입니다. 가장 작은 빌딩 블록은 인공 뉴런 입니다 . 실제 신경계에서 이러한 뉴런은 시냅스로 서로 연결되어 전체 시스템에 엄청난 처리 능력, 학습 능력 및 뛰어난 유연성을 제공합니다. 인공 신경망은 동일한 원리를 적용합니다.

인공 뉴런을 연결하여 유사한 시스템을 만드는 것을 목표로 합니다. 그들은 뉴런을 계층으로 그룹화한 다음 각 계층의 뉴런을 연결했습니다. 또한 각 연결에 가중치를  할당하여 중요하지 않은 연결에서 중요한 연결을 필터링할 수 있습니다.

인공 뉴런의 구조도 실제 뉴런의 거울상이다. 여러 개의 입력(예: 입력 연결)을 가질 수 있으므로 이 데이터를 수집하는 특수 함수인  입력 함수 가 사용됩니다 . 일반적으로 뉴런에 대한 입력 함수로 사용되는 함수는 입력 연결에서 활성화된 모든 가중 입력을 합산하는 함수인 가중 입력 함수입니다 .

모든 인공 뉴런의 또 다른 중요한 부분은 활성화 기능 입니다 . 이 함수는 이 뉴런이 출력으로 신호를 보낼지 여부와 출력으로 전파될 값을 정의합니다. 기본적으로 이 함수는 입력 함수에서 값을 받고 그 값을 기반으로 출력 값을 생성하고 이를 출력으로 전파합니다.

따라서 이전 장에서 볼 수 있듯이 추상화할 수 있는 몇 가지 중요한 엔터티에 주의를 기울여야 합니다. 그것들은 뉴런, 연결, 계층 및 기능입니다. 이 솔루션에서는 별도의 클래스가 이러한 각 엔터티를 구현합니다. 그런 다음 모두 함께 배치하고 그 위에 역전파 알고리즘을 추가하여 이 간단한 신경망을 구현합니다.

앞에서 언급했듯이 뉴런의 핵심 부분은 입력 기능과 활성화 기능입니다. 입력 함수를 살펴보겠습니다. 먼저, 뉴런 구현에서 나중에 쉽게 변경할 수 있도록 이 함수에 대한 인터페이스를 만들었습니다.

public interface IInputFunction
{
    double CalculateInput(List<ISynapse> inputs);
}

이러한 함수에는 ISynapse 인터페이스 에 설명된 연결 목록을 수신하는  CalculateInput 메서드만 있습니다  . 나중에 이 추상화를 소개할 것입니다. 지금까지 알아야 할 것은 이 인터페이스가 뉴런 간의 연결을 나타낸다는 것입니다. 계산된 입력 방법은 연결된 목록에 포함된 데이터를 기반으로 어떤 종류의 값을 반환해야 합니다. 그런 다음 입력 함수인 가중 합계 함수를 구체적으로 구현했습니다 .

public class WeightedSumFunction : IInputFunction
{
    public double CalculateInput(List<ISynapse> inputs) {
        return inputs.Select(x => x.Weight * x.GetOutput()).Sum();
    }
}

이 함수는 목록에 전달된 모든 연결의 가중치를 합산합니다.

입력 함수 구현과 동일한 방법을 사용하여 먼저 활성화 함수의 인터페이스를 구현합니다.

public interface IActivationFunction
{
    double CalculateOutput(double input);
}

그 후에 특정 구현을 수행할 수 있습니다. 출력 계산 방법은 뉴런이 입력 함수에서 가져오는 입력 값을 기반으로 뉴런의 출력 값을 반환해야 합니다. 저는 옵션이 있는 것을 좋아하기 때문에 이전 블로그 게시물에서 언급한 모든 기능을 구현했습니다 . 단계 기능은 다음과 같습니다.

public class StepActivationFunction : IActivationFunction
{
    private double _treshold;

    public StepActivationFunction(double treshold) {
        _treshold = treshold;
    }

    public double CalculateOutput(double input) {
        return Convert.ToDouble(input > _treshold);
    }
}

간단하죠? 임계값은 개체 구성 중에 정의되며  CalculateOutput은  입력 값이 임계값을 초과하면 1을 반환하고 그렇지 않으면 0을 반환합니다.

다른 기능도 쉽습니다. 다음은 시그모이드 활성화 함수의 구현입니다.

public class SigmoidActivationFunction : IActivationFunction
{
    private double _coeficient;

    public SigmoidActivationFunction(double coeficient) {
        _coeficient = coeficient;
    }

    public double CalculateOutput(double input) {
        return (1 / (1 + Math.Exp(-input * _coeficient)));
    }
}

다음은 정류기 활성화 기능 구현입니다.

public class RectifiedActivationFuncion : IActivationFunction
{
    public double CalculateOutput(double input) {
        return Math.Max(0, input);
    }
}

지금까지는 입력 및 활성화 기능을 구현했으며 네트워크의 더 까다로운 부분인 뉴런 및 연결을 구현하는 단계로 넘어갈 수 있습니다.

뉴런이 따라야 하는 작업 흐름은 다음과 같습니다. 하나 이상의 가중치 입력 연결에서 입력 값을 받습니다. 이러한 값을 수집하고 뉴런의 출력 값을 계산하는 활성화 함수에 전달합니다. 이 값을 뉴런의 출력으로 보냅니다. 뉴런 워크플로 추상화를 기반으로 다음이 생성됩니다.

    public interface INeuron
    {
        Guid Id { get; }
        double PreviousPartialDerivate { get; set; }

        List<ISynapse> Inputs { get; set; }
        List<ISynapse> Outputs { get; set; }

        void AddInputNeuron(INeuron inputNeuron);
        void AddOutputNeuron(INeuron inputNeuron);
        double CalculateOutput();

        void AddInputSynapse(double inputValue);
        void PushValueOnInput(double inputValue);
    }

각 속성과 메서드를 설명하기 전에 Neuron의 특정 구현을 살펴보겠습니다. 이렇게 하면 작동 방식이 더 명확해집니다.

public class Neuron : INeuron
{
    private IActivationFunction _activationFunction;
    private IInputFunction _inputFunction;

    /// <summary>/// Input connections of the neuron./// </summary>public List<ISynapse> Inputs { get; set; }

    /// <summary>/// Output connections of the neuron./// </summary>public List<ISynapse> Outputs { get; set; }

    public Guid Id { get; private set; }

    /// <summary>/// Calculated partial derivate in previous iteration of training process./// </summary>public double PreviousPartialDerivate { get; set; }

    public Neuron(IActivationFunction activationFunction, IInputFunction inputFunction)
    {
        Id = Guid.NewGuid();
        Inputs = new List<ISynapse>();
        Outputs = new List<ISynapse>();

        _activationFunction = activationFunction;
        _inputFunction = inputFunction;
    }

    /// <summary>/// Connect two neurons. /// This neuron is the output neuron of the connection./// </summary>/// <param name="inputNeuron">Neuron that will be input neuron of the newly created connection.</param>public void AddInputNeuron(INeuron inputNeuron)
    {
        var synapse = new Synapse(inputNeuron, this);
        Inputs.Add(synapse);
        inputNeuron.Outputs.Add(synapse);
    }

    /// <summary>/// Connect two neurons. /// This neuron is the input neuron of the connection./// </summary>/// <param name="outputNeuron">Neuron that will be output neuron of the newly created connection.</param>public void AddOutputNeuron(INeuron outputNeuron)
    {
        var synapse = new Synapse(this, outputNeuron);
        Outputs.Add(synapse);
        outputNeuron.Inputs.Add(synapse);
    }

    /// <summary>/// Calculate output value of the neuron./// </summary>/// <returns>/// Output of the neuron./// </returns>public double CalculateOutput()
    {
        return _activationFunction.CalculateOutput(_inputFunction.CalculateInput(this.Inputs));
    }

    /// <summary>/// Input Layer neurons just receive input values./// For this they need to have connections./// This function adds this kind of connection to the neuron./// </summary>/// <param name="inputValue">/// Initial value that will be "pushed" as an input to connection./// </param>public void AddInputSynapse(double inputValue)
    {
        var inputSynapse = new InputSynapse(this, inputValue);
        Inputs.Add(inputSynapse);
    }

    /// <summary>/// Sets new value on the input connections./// </summary>/// <param name="inputValue">/// New value that will be "pushed" as an input to connection./// </param>public void PushValueOnInput(double inputValue)
    {
        ((InputSynapse)Inputs.First()).Output = inputValue;
    }
}

각 뉴런에는 고유 식별자인 Id가 있습니다  . 이 속성은 나중에 역전파 알고리즘에서 사용됩니다. 역전파 목적으로 추가된 또 다른 속성은  PreviousPartial Derivate 이지만 이에 대해 자세히 알아볼 것입니다. 뉴런에는 두 개의 목록이 있습니다. 하나는 입력 연결(  입력)이고 다른 하나는 출력 연결(  출력)입니다. 또한 이전 장에서 설명한 각 기능에 대해 하나씩 두 개의 필드가 있습니다. 이들은 생성자를 통해 초기화됩니다. 이러한 방식으로 입력 및 활성화 기능이 다른 뉴런을 만들 수 있습니다.

이 클래스에는 몇 가지 흥미로운 메서드도 있습니다. AddInputNeuron  및  AddOutputNeuron은  뉴런 간의 연결을 생성하는 데 사용됩니다. 첫 번째는 특정 뉴런에 대한 입력 연결을 추가하고 두 번째는 특정 뉴런에 대한 출력 연결을 추가합니다. AddInputSynapse는 특별한 유형의 연결인 뉴런에 InputSynapse를 추가합니다. 이들은 뉴런의 입력 계층에서만 사용되는 특수 연결입니다. 즉, 전체 시스템에 입력을 추가하는 데만 사용됩니다. 이에 대해서는 다음 장에서 자세히 다룰 것입니다.

마지막으로 출력 계산 방법은 출력 계산 의 연쇄 반응을 활성화하는 데 사용됩니다 . 이 함수가 호출되면 어떻게 됩니까? 글쎄, 이것은 모든 입력 연결에서 값을 요청하는 입력 기능을 호출합니다. 차례로 이러한 연결은 이러한 연결의 입력 뉴런에서 출력 값, 즉 이전 계층의 뉴런에서 출력 값을 요청합니다. 이 프로세스는 입력 레이어에 도달하고 시스템을 통해 입력 값을 전파할 때까지 계속됩니다.

연결은  ISynapse 인터페이스를 통해  추상화됩니다 .

public interface ISynapse
{
    double Weight { get; set; }
    double PreviousWeight { get; set; }
    double GetOutput();

    bool IsFromNeuron(Guid fromNeuronId);
    void UpdateWeight(double learningRate, double delta);
}

각 연결에는 동일한 이름의 속성으로 표시되는 가중치가 있습니다. 추가 속성인  PreviousWeight  가 추가되고 시스템을 통한 오류 역전파 중에 사용됩니다. 현재 가중치 업데이트 및 이전 가중치 저장은  도우미 함수 UpdateWeight에서 수행됩니다.

특정 뉴런이 연결의 입력 뉴런인지 여부를 감지하는 IsFromNeuron 이라는 또 다른 도우미 함수가 있습니다  . 물론 연결의 출력 값을 가져오는 방법이 있습니다 - GetOutput 연결 구현은 다음과 같습니다.

public class Synapse : ISynapse
{
    internal INeuron _fromNeuron;
    internal INeuron _toNeuron;

    /// <summary>/// Weight of the connection./// </summary>public double Weight { get; set; }

    /// <summary>/// Weight that connection had in previous itteration./// Used in training process./// </summary>public double PreviousWeight { get; set; }

    public Synapse(INeuron fromNeuraon, INeuron toNeuron, double weight)
    {
        _fromNeuron = fromNeuraon;
        _toNeuron = toNeuron;

        Weight = weight;
        PreviousWeight = 0;
    }

    public Synapse(INeuron fromNeuraon, INeuron toNeuron)
    {
        _fromNeuron = fromNeuraon;
        _toNeuron = toNeuron;

        var tmpRandom = new Random();
        Weight = tmpRandom.NextDouble();
        PreviousWeight = 0;
    }

    /// <summary>/// Get output value of the connection./// </summary>/// <returns>/// Output value of the connection./// </returns>public double GetOutput()
    {
        return _fromNeuron.CalculateOutput();
    }

    /// <summary>/// Checks if Neuron has a certain number as an input neuron./// </summary>/// <param name="fromNeuronId">Neuron Id.</param>/// <returns>/// True - if the neuron is the input of the connection./// False - if the neuron is not the input of the connection. /// </returns>public bool IsFromNeuron(Guid fromNeuronId)
    {
        return _fromNeuron.Id.Equals(fromNeuronId);
    }

    /// <summary>/// Update weight./// </summary>/// <param name="learningRate">Chossen learning rate.</param>/// <param name="delta">Calculated difference for which weight of the connection needs to be modified.</param>public void UpdateWeight(double learningRate, double delta)
    {
        PreviousWeight = Weight;
        Weight += learningRate * delta;
    }
}

이 시냅스가 연결된 뉴런을 정의하는 _fromNeuron 및 _toNeuron 필드에 주목하십시오 . 이 연결 구현 외에도 지난 장에서 언급한 뉴런의 또 다른 구현이 있습니다. 시스템에 대한 입력으로 사용되는 입력 시냅스 입니다 . 이러한 연결의 가중치는 항상 1이며 훈련 중에 업데이트되지 않습니다. 구현은 다음과 같습니다.

public class InputSynapse : ISynapse
{
    internal INeuron _toNeuron;

    public double Weight { get; set; }
    public double Output { get; set; }
    public double PreviousWeight { get; set; }

    public InputSynapse(INeuron toNeuron)
    {
        _toNeuron = toNeuron;
        Weight = 1;
    }

    public InputSynapse(INeuron toNeuron, double output)
    {
        _toNeuron = toNeuron;
        Output = output;
        Weight = 1;
        PreviousWeight = 1;
    }

    public double GetOutput()
    {
        return Output;
    }

    public bool IsFromNeuron(Guid fromNeuronId)
    {
        return false;
    }

    public void UpdateWeight(double learningRate, double delta)
    {
        throw new InvalidOperationException("It is not allowed to call this method on Input Connecion");
    }
}

여기에서 신경 계층의 구현은 매우 간단합니다.

public class NeuralLayer {
    public List<INeuron> Neurons;

    public NeuralLayer()
    {
        Neurons = new List<INeuron>();
    }

    /// <summary>/// Connecting two layers./// </summary>public void ConnectLayers(NeuralLayer inputLayer)
    {
        var combos = Neurons.SelectMany(neuron => inputLayer.Neurons, (neuron, input) => new { neuron, input });
        combos.ToList().ForEach(x => x.neuron.AddInputNeuron(x.input));
    }
}

여기에는 레이어에 사용된 뉴런 목록과 두 레이어를 함께 붙이는 데 사용되는 ConnectLayers 메서드가 포함됩니다 .

이제 이 모든 것을 함께 넣고 역전파를 추가해 봅시다. 네트워크 자체의 구현을 살펴보십시오.

public class SimpleNeuralNetwork
{
    private NeuralLayerFactory _layerFactory;

    internal List<NeuralLayer> _layers;
    internal double _learningRate;
    internal double[][] _expectedResult;

    /// <summary>/// Constructor of the Neural Network./// Note:/// Initialy input layer with defined number of inputs will be created./// </summary>/// <param name="numberOfInputNeurons">/// Number of neurons in input layer./// </param>public SimpleNeuralNetwork(int numberOfInputNeurons)
    {
        _layers = new List<NeuralLayer>();
        _layerFactory = new NeuralLayerFactory();

        // Create input layer that will collect inputs.
        CreateInputLayer(numberOfInputNeurons);

        _learningRate = 2.95;
    }

    /// <summary>/// Add layer to the neural network./// Layer will automatically be added as the output layer to the last layer in the neural network./// </summary>public void AddLayer(NeuralLayer newLayer)
    {
        if (_layers.Any())
        {
            var lastLayer = _layers.Last();
            newLayer.ConnectLayers(lastLayer);
        }

        _layers.Add(newLayer);
    }

    /// <summary>/// Push input values to the neural network./// </summary>public void PushInputValues(double[] inputs)
    {
        _layers.First().Neurons.ForEach(x => x.PushValueOnInput(inputs[_layers.First().Neurons.IndexOf(x)]));
    }

    /// <summary>/// Set expected values for the outputs./// </summary>public void PushExpectedValues(double[][] expectedOutputs)
    {
        _expectedResult = expectedOutputs;
    }

    /// <summary>/// Calculate output of the neural network./// </summary>/// <returns></returns>public List<double> GetOutput()
    {
        var returnValue = new List<double>();

        _layers.Last().Neurons.ForEach(neuron =>
        {
             returnValue.Add(neuron.CalculateOutput());
        });

        return returnValue;
    }

    /// <summary>/// Train neural network./// </summary>/// <param name="inputs">Input values.</param>/// <param name="numberOfEpochs">Number of epochs.</param>public void Train(double[][] inputs, int numberOfEpochs)
    {
        double totalError = 0;

        for(int i = 0; i < numberOfEpochs; i++)
        {
            for(int j = 0; j < inputs.GetLength(0); j ++)
            {
                PushInputValues(inputs[j]);

                var outputs = new List<double>();

                // Get outputs.
                _layers.Last().Neurons.ForEach(x =>
                {
                    outputs.Add(x.CalculateOutput());
                });

                // Calculate error by summing errors on all output neurons.
                totalError = CalculateTotalError(outputs, j);
                HandleOutputLayer(j);
                HandleHiddenLayers();
            }
        }
    }

    /// <summary>/// Hellper function that creates input layer of the neural network./// </summary>private void CreateInputLayer(int numberOfInputNeurons)
    {
        var inputLayer = _layerFactory.CreateNeuralLayer(numberOfInputNeurons, new RectifiedActivationFuncion(), new WeightedSumFunction());
        inputLayer.Neurons.ForEach(x => x.AddInputSynapse(0));
        this.AddLayer(inputLayer);
    }

    /// <summary>/// Hellper function that calculates total error of the neural network./// </summary>private double CalculateTotalError(List<double> outputs, int row)
    {
        double totalError = 0;

        outputs.ForEach(output =>
        {
            var error = Math.Pow(output - _expectedResult[row][outputs.IndexOf(output)], 2);
            totalError += error;
        });

        return totalError;
    }

    /// <summary>/// Hellper function that runs backpropagation algorithm on the output layer of the network./// </summary>/// <param name="row">/// Input/Expected output row./// </param>private void HandleOutputLayer(int row)
    {
        _layers.Last().Neurons.ForEach(neuron =>
        {
            neuron.Inputs.ForEach(connection =>
            {
                var output = neuron.CalculateOutput();
                var netInput = connection.GetOutput();

                var expectedOutput = _expectedResult[row][_layers.Last().Neurons.IndexOf(neuron)];

                var nodeDelta = (expectedOutput - output) * output * (1 - output);
                var delta = -1 * netInput * nodeDelta;

                connection.UpdateWeight(_learningRate, delta);

                neuron.PreviousPartialDerivate = nodeDelta;
            });
        });
    }

    /// <summary>/// Hellper function that runs backpropagation algorithm on the hidden layer of the network./// </summary>/// <param name="row">/// Input/Expected output row./// </param>private void HandleHiddenLayers()
    {
        for (int k = _layers.Count - 2; k > 0; k--)
        {
            _layers[k].Neurons.ForEach(neuron =>
            {
                neuron.Inputs.ForEach(connection =>
                {
                    var output = neuron.CalculateOutput();
                    var netInput = connection.GetOutput();
                    double sumPartial = 0;

                    _layers[k + 1].Neurons
                    .ForEach(outputNeuron =>
                    {
                        outputNeuron.Inputs.Where(i => i.IsFromNeuron(neuron.Id))
                        .ToList()
                        .ForEach(outConnection =>
                        {
                            sumPartial += outConnection.PreviousWeight * outputNeuron.PreviousPartialDerivate;
                        });
                    });

                    var delta = -1 * netInput * sumPartial * output * (1 - output);
                    connection.UpdateWeight(_learningRate, delta);
                });
            });
        }
    }
}

이 클래스에는 신경 계층 및 계층 팩토리(새 계층을 생성하기 위한 클래스) 목록이 포함되어 있습니다. 객체 구성 중에 초기 입력 계층이 네트워크에 추가됩니다.  추가 레이어는 현재 레이어 목록 위에 전달된 레이어를 추가하는 AddLayer 함수를 통해 추가됩니다  . GetOutput  메서드는 네트워크의 출력 계층을 활성화하여 네트워크를 통해 연쇄 반응을 시작합니다.

또한 이 클래스에는  훈련 중에 전달될 훈련 세트에 대해 원하는 값을 설정하기 위한  PushExpectValues ​​네요 네요

이 클래스의 가장 중요한 메서드는  Train  메서드입니다. 트레이닝 세트와 에포크 수를 받습니다. 이 백서에 설명된 대로 각 시대에 대해 네트워크를 통해 전체 훈련 세트를 실행합니다 . 그런 다음 출력을 원하는 출력과 비교하고  HandleOutputLayer  및  HandleHiddenLayer 함수를 호출합니다 . 이러한 함수는 이 문서에서 설명하는 역전파 알고리즘을 구현합니다.

일반적인 워크플로우는 테스트 중 하나인
Train_RuningTraining_NetworkIsTrained에서 볼 수 있습니다. 다음과 같습니다.

var network = new SimpleNeuralNetwork(3);

var layerFactory = new NeuralLayerFactory();
network.AddLayer(layerFactory.CreateNeuralLayer(3, new RectifiedActivationFuncion(), new WeightedSumFunction()));
network.AddLayer(layerFactory.CreateNeuralLayer(1, new SigmoidActivationFunction(0.7), new WeightedSumFunction()));

network.PushExpectedValues(
    new double[][] {
        new double[] { 0 },
        new double[] { 1 },
        new double[] { 1 },
        new double[] { 0 },
        new double[] { 1 },
        new double[] { 0 },
        new double[] { 0 },
    });

network.Train(
    new double[][] {
        new double[] { 150, 2, 0 },
        new double[] { 1002, 56, 1 },
        new double[] { 1060, 59, 1 },
        new double[] { 200, 3, 0 },
        new double[] { 300, 3, 1 },
        new double[] { 120, 1, 0 },
        new double[] { 80, 1, 0 },
    }, 10000);

network.PushInputValues(new double[] { 1054, 54, 1 });
var outputs = network.GetOutput();

먼저 신경망 개체를 만듭니다. 생성자에서 입력 레이어에 세 개의 뉴런이 있다고 정의합니다. 그런 다음 add layer 및 layer factory 함수를 사용하여 두 개의 레이어를 추가합니다 . 각 레이어에 대해 뉴런 수와 뉴런당 피처를 정의합니다. 이 부분을 완료한 후 예상 출력이 정의되고  입력 트레이닝 세트 및 에포크 수와 함께 Train  함수가 호출됩니다.

이러한 신경망 구현은 최적과는 거리가 멉니다. 확실히 제대로 수행되지 않는 중첩된 for 루프가 많이 있음을 알 수 있습니다. 또한 이 솔루션을 단순화하기 위해 예를 들어 신경망의 일부 구성 요소는 구현의 첫 번째 반복에서 모멘텀 및 편향을 도입하지 않았습니다. 그러나 고성능 네트워크를 달성하는 목표는 각 인공 신경망이 가지고 있는 중요한 요소와 추상화를 분석하고 보여주는 것이 아닙니다.

추천

출처blog.csdn.net/youyi300200/article/details/130650594