65.9K
CodeProject 正在变化。 阅读更多。
Home

重塑神经网络 - 第三部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (7投票s)

2018 年 2 月 19 日

CPOL

3分钟阅读

viewsIcon

19751

downloadIcon

838

现在我们已经完成了基础知识,是时候进行改进了!

完整系列

  • 第 1 部分:我们从头开始创建整个 NeuralNetwork 类。
  • 第 2 部分:我们在 Unity 中创建一个环境,以便在该环境中测试神经网络。
  • 第 3 部分:我们通过向代码添加一种新型的变异,对已经创建的神经网络进行重大改进。

引言

在我开始之前,我得承认我 16 岁了,因为我有点注意到人们在知道这件事后会打嗝,所以我想...也许如果人们知道,我会从中得到更多。只是说说而已。

欢迎回来伙计们!大约一个月前,我发布了本系列的第 2 部分。之后,我真的忙于做一堆其他的事情,这让我无法继续做我真正想做的事情(对此感到抱歉)。然而,几天前,Michael Bartlett 向我发送了一封电子邮件,询问一种名为安全变异(或简称 SA)的 GA 运算符(参考这篇论文)。

背景

为了理解本文,您需要具备基本的 C# 和 Unity 编程知识。 另外,您需要阅读本系列的第 1 部分第 2 部分

理解理论

根据 Michael 发送的论文,似乎有两种类型的 SM 运算符

  1. 通过重新缩放的安全变异:简而言之,这就像稍微调整权重以了解这个位对输出的影响有多大,并据此进行明智的变异。
  2. 通过梯度的安全变异:这与反向传播中使用的获取每个权重的梯度的方法类似,并据此也进行明智的变异。

然而,这仍然没有给我足够的开始编码的信息,所以我一直寻找直到我找到这篇论文。 这篇论文表明,如果您仅仅使用一些就在您面前的信息,您可以获得多么大的改进。 如果您查看该论文中的图 4,您会看到无偏突变会产生最差的结果,而节点突变会产生指数级更好的结果! 好吧,它甚至可以匹配交叉!

它甚至与安全变异(通过梯度)和反向传播在图 7 和 8 中进行了比较。 它要么与安全变异打成平手,而节点变异击败了反向传播......

好吧... 那么什么是神奇的“Mutate Nodes”运算符? 好吧... 普通的突变只是选择一些权重并按如下方式对它们进行突变

然而,节点突变选择一些节点并突变所有进入它的权重

Using the Code

好吧,代码非常简单。 首先,将 MutateNodes 函数添加到 NeuralSection

        /// <summary>
        /// Mutate the NeuralSection's Nodes.
        /// </summary>
        /// <param name="MutationProbablity">The probability that a node is going 
        /// to be mutated. (Ranges 0-1)</param>
        /// <param name="MutationAmount">The maximum amount 
        /// a Mutated Weight would change.</param>
        public void MutateNodes(double MutationProbablity, double MutationAmount)
        {
            for (int j = 0; j < Weights[0].Length; j++)              // For each output node
            {
                if (TheRandomizer.NextDouble() < MutationProbablity) // Check if we are going 
                                                                     // to mutate this node
                {
                    for (int i = 0; i < Weights.Length; i++)         // For each input node 
                                                                     // connected to the 
                                                                     // current output node
                    {
                        Weights[i][j] = TheRandomizer.NextDouble() * 
                                        (MutationAmount * 2) - MutationAmount; // Mutate the 
                                                               // weight connecting both nodes
                    }
                }
            }
        }

然后,将调用函数添加到 NeuralNetwork

    /// <summary>
    /// Mutate the NeuralNetwork's Nodes.
    /// </summary>
    /// <param name="MutationProbablity">The probability that a node 
    /// is going to be mutated. (Ranges 0-1)</param>
    /// <param name="MutationAmount">
    /// The maximum amount a Mutated Weight would change.</param>
    public void MutateNodes(double MutationProbablity = 0.3, double MutationAmount = 2.0)
    {
        // Mutate each section
        for (int i = 0; i < Sections.Length; i++)
        {
            Sections[i].MutateNodes(MutationProbablity, MutationAmount);
        }
    }

这几乎就是它了! 这就是 NeuralNetwork.cs 现在应该的样子

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

public class NeuralNetwork
{
    public UInt32[] Topology // Returns the topology in the form of an array
    {
        get
        {
            UInt32[] Result = new UInt32[TheTopology.Count];

            TheTopology.CopyTo(Result, 0);

            return Result;
        }
    }

    ReadOnlyCollection<UInt32> TheTopology; // Contains the topology of the NeuralNetwork
    NeuralSection[] Sections;               // Contains the all the sections of the NeuralNetwork

    Random TheRandomizer;                   // It is the Random instance used 
                                            // to mutate the NeuralNetwork

    private class NeuralSection
    {
        private double[][] Weights;         // Contains all the weights of the section  
                                            // where [i][j] represents the weight from 
                                            // neuron i in the input layer and neuron j 
                                            // in the output layer

        private Random TheRandomizer;       // Contains a reference to the Random instance 
                                            // of the NeuralNetwork

        /// <summary>
        /// Initiate a NeuralSection from a topology and a seed.
        /// </summary>
        /// <param name="InputCount">The number of input neurons in the section.</param>
        /// <param name="OutputCount">The number of output neurons in the section.</param>
        /// <param name="Randomizer">The Ransom instance of the NeuralNetwork.</param>
        public NeuralSection(UInt32 InputCount, UInt32 OutputCount, Random Randomizer)
        {
            // Validation Checks
            if (InputCount == 0)
                throw new ArgumentException("You cannot create a Neural Layer 
                                             with no input neurons.", "InputCount");
            else if (OutputCount == 0)
                throw new ArgumentException("You cannot create a Neural Layer 
                                             with no output neurons.", "OutputCount");
            else if (Randomizer == null)
                throw new ArgumentException("The randomizer cannot be set to null.", 
                                            "Randomizer");

            // Set Randomizer
            TheRandomizer = Randomizer;

            // Initialize the Weights array
            Weights = new double[InputCount + 1][]; // +1 for the Bias Neuron

            for (int i = 0; i < Weights.Length; i++)
                Weights[i] = new double[OutputCount];

            // Set random weights
            for (int i = 0; i < Weights.Length; i++)
                for (int j = 0; j < Weights[i].Length; j++)
                    Weights[i][j] = TheRandomizer.NextDouble() - 0.5f;
        }

        /// <summary>
        /// Initiates an independent Deep-Copy of the NeuralSection provided.
        /// </summary>
        /// <param name="Main">The NeuralSection that should be cloned.</param>
        public NeuralSection(NeuralSection Main)
        {
            // Set Randomizer
            TheRandomizer = Main.TheRandomizer;

            // Initialize Weights
            Weights = new double[Main.Weights.Length][];

            for (int i = 0; i < Weights.Length; i++)
                Weights[i] = new double[Main.Weights[0].Length];

            // Set Weights
            for (int i = 0; i < Weights.Length; i++)
            {
                for (int j = 0; j < Weights[i].Length; j++)
                {
                    Weights[i][j] = Main.Weights[i][j];
                }
            }
        }

        /// <summary>
        /// Feed input through the NeuralSection and get the output.
        /// </summary>
        /// <param name="Input">The values to set the input neurons.</param>
        /// <returns>The values in the output neurons after propagation.</returns>
        public double[] FeedForward(double[] Input)
        {
            // Validation Checks
            if (Input == null)
                throw new ArgumentException("The input array cannot be set to null.", "Input");
            else if (Input.Length != Weights.Length - 1)
                throw new ArgumentException
                ("The input array's length does not match the number of neurons 
                  in the input layer.", "Input");

            // Initialize Output Array
            double[] Output = new double[Weights[0].Length];

            // Calculate Value
            for (int i = 0; i < Weights.Length; i++)
            {
                for (int j = 0; j < Weights[i].Length; j++)
                {
                    if (i == Weights.Length - 1)    // If is Bias Neuron
                        Output[j] += Weights[i][j]; // Then, the value of the neuron 
                                                    // is equal to one
                    else
                        Output[j] += Weights[i][j] * Input[i];
                }
            }

            // Apply Activation Function
            for (int i = 0; i < Output.Length; i++)
                Output[i] = ReLU(Output[i]);

            // Return Output
            return Output;
        }

        /// <summary>
        /// Mutate the NeuralSection.
        /// </summary>
        /// <param name="MutationProbablity">The probability that a weight 
        /// is going to be mutated. (Ranges 0-1)</param>
        /// <param name="MutationAmount">The maximum amount a Mutated Weight 
        /// would change.</param>
        public void Mutate(double MutationProbablity, double MutationAmount)
        {
            for (int i = 0; i < Weights.Length; i++)
            {
                for (int j = 0; j < Weights[i].Length; j++)
                {
                    if (TheRandomizer.NextDouble() < MutationProbablity)
                        Weights[i][j] = TheRandomizer.NextDouble() * 
                                        (MutationAmount * 2) - MutationAmount;
                }
            }
        }

        /// <summary>
        /// Mutate the NeuralSection's Nodes.
        /// </summary>
        /// <param name="MutationProbablity">The probability that a node 
        /// is going to be mutated. (Ranges 0-1)</param>
        /// <param name="MutationAmount">The maximum amount a Mutated Weight 
        /// would change.</param>
        public void MutateNodes(double MutationProbablity, double MutationAmount)
        {
            for (int j = 0; j < Weights[0].Length; j++) // For each output node
            {
                if (TheRandomizer.NextDouble() < MutationProbablity) // Check if we are 
                                                             // going to mutate this node
                {
                    for (int i = 0; i < Weights.Length; i++) // For each input node 
                                                             // connected to the current 
                                                             // output node
                    {
                        Weights[i][j] = TheRandomizer.NextDouble() * 
                        (MutationAmount * 2) - MutationAmount; // Mutate the weight 
                                                               // connecting both nodes
                    }
                }
            }
        }

        /// <summary>
        /// Puts a double through the activation function ReLU.
        /// </summary>
        /// <param name="x">The value to put through the function.</param>
        /// <returns>x after it is put through ReLU.</returns>
        private double ReLU(double x)
        {
            if (x >= 0)
                return x;
            else
                return x / 20;
        }
    }

    /// <summary>
    /// Initiates a NeuralNetwork from a Topology and a Seed.
    /// </summary>
    /// <param name="Topology">The Topology of the Neural Network.</param>
    /// <param name="Seed">The Seed of the Neural Network. 
    /// Set to 'null' to use a Timed Seed.</param>
    public NeuralNetwork(UInt32[] Topology, Int32? Seed = 0)
    {
        // Validation Checks
        if (Topology.Length < 2)
            throw new ArgumentException("A Neural Network cannot contain 
                                         less than 2 Layers.", "Topology");

        for (int i = 0; i < Topology.Length; i++)
        {
            if (Topology[i] < 1)
                throw new ArgumentException("A single layer of neurons must contain, 
                                             at least, one neuron.", "Topology");
        }

        // Initialize Randomizer
        if (Seed.HasValue)
            TheRandomizer = new Random(Seed.Value);
        else
            TheRandomizer = new Random();

        // Set Topology
        TheTopology = new List<uint>(Topology).AsReadOnly();

        // Initialize Sections
        Sections = new NeuralSection[TheTopology.Count - 1];

        // Set the Sections
        for (int i = 0; i < Sections.Length; i++)
        {
            Sections[i] = new NeuralSection
                          (TheTopology[i], TheTopology[i + 1], TheRandomizer);
        }
    }

    /// <summary>
    /// Initiates an independent Deep-Copy of the Neural Network provided.
    /// </summary>
    /// <param name="Main">The Neural Network that should be cloned.</param>
    public NeuralNetwork(NeuralNetwork Main)
    {
        // Initialize Randomizer
        TheRandomizer = new Random(Main.TheRandomizer.Next());

        // Set Topology
        TheTopology = Main.TheTopology;

        // Initialize Sections
        Sections = new NeuralSection[TheTopology.Count - 1];

        // Set the Sections
        for (int i = 0; i < Sections.Length; i++)
        {
            Sections[i] = new NeuralSection(Main.Sections[i]);
        }
    }

    /// <summary>
    /// Feed Input through the NeuralNetwork and Get the Output.
    /// </summary>
    /// <param name="Input">The values to set the Input Neurons.</param>
    /// <returns>The values in the output neurons after propagation.</returns>
    public double[] FeedForward(double[] Input)
    {
        // Validation Checks
        if (Input == null)
            throw new ArgumentException("The input array cannot be set to null.", "Input");
        else if (Input.Length != TheTopology[0])
            throw new ArgumentException("The input array's length does not match 
                  the number of neurons in the input layer.", "Input");

        double[] Output = Input;

        // Feed values through all sections
        for (int i = 0; i < Sections.Length; i++)
        {
            Output = Sections[i].FeedForward(Output);
        }

        return Output;
    }

    /// <summary>
    /// Mutate the NeuralNetwork.
    /// </summary>
    /// <param name="MutationProbablity">The probability that a weight 
    /// is going to be mutated. (Ranges 0-1)</param>
    /// <param name="MutationAmount">The maximum amount a mutated weight would change.</param>
    public void Mutate(double MutationProbablity = 0.3, double MutationAmount = 2.0)
    {
        // Mutate each section
        for (int i = 0; i < Sections.Length; i++)
        {
            Sections[i].Mutate(MutationProbablity, MutationAmount);
        }
    }

    /// <summary>
    /// Mutate the NeuralNetwork's Nodes.
    /// </summary>
    /// <param name="MutationProbablity">The probability that a node 
    /// is going to be mutated. (Ranges 0-1)</param>
    /// <param name="MutationAmount">The maximum amount a Mutated Weight would change.</param>
    public void MutateNodes(double MutationProbablity = 0.3, double MutationAmount = 2.0)
    {
        // Mutate each section
        for (int i = 0; i < Sections.Length; i++)
        {
            Sections[i].MutateNodes(MutationProbablity, MutationAmount);
        }
    }
}

而且... 不,我不会就这样离开你。 现在是时候对之前在 Unity 中制作的汽车演示进行一些小小的改动,这样我们就可以亲眼看到差异。 让我们首先转到我们 Unity 项目中的 EvolutionManager.cs 并在脚本的开头添加这个变量

    [SerializeField] bool UseNodeMutation = true; // Should we use node mutation?

让我们也通过将 StartGeneration() 函数中对 Car.NextNetwork.Mutate() 的调用替换为以下内容来使用这个变量

	if(UseNodeMutation)                // Should we use Node Mutation
     	Car.NextNetwork.MutateNodes(); // Mutate its nodes
	else
        Car.NextNetwork.Mutate();      // Mutate its weights

这样,EvolutionManager.cs 最终应该看起来像这样

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class EvolutionManager : MonoBehaviour
{
    public static EvolutionManager Singleton = null; // The current EvolutionManager Instance

    [SerializeField] bool UseNodeMutation = true;    // Should we use node mutation?

    [SerializeField] int CarCount = 100;             // The number of cars per generation
    [SerializeField] GameObject CarPrefab;           // The Prefab of the car to be created 
                                                     // for each instance
    [SerializeField] Text GenerationNumberText;      // Some text to write the generation number

    int GenerationCount = 0;                         // The current generation number

    List<Car> Cars = new List<Car>();                // This list of cars currently alive

    NeuralNetwork BestNeuralNetwork = null;          // The best NeuralNetwork 
                                                     // currently available
    int BestFitness = -1;                            // The FItness of the best 
                                                     // NeuralNetwork ever created
    
    // On Start
    private void Start()
    {
        if (Singleton == null)             // If no other instances were created
            Singleton = this;              // Make the only instance this one
        else
            gameObject.SetActive(false);   // There is another instance already in place. 
                                           // Make this one inactive.

        BestNeuralNetwork = new NeuralNetwork(Car.NextNetwork); // Set the BestNeuralNetwork
                                                                // to a random new network

        StartGeneration(); 
    }

    // Sarts a whole new generation
    void StartGeneration ()
    {
        GenerationCount++; // Increment the generation count
        GenerationNumberText.text = "Generation: " + 
                                     GenerationCount; // Update generation text

        for (int i = 0; i < CarCount; i++)
        {
            if (i == 0)
                Car.NextNetwork = BestNeuralNetwork; // Make sure one car uses the best network
            else
            {
                Car.NextNetwork = new NeuralNetwork(BestNeuralNetwork); // Clone the best 
                                        // neural network and set it to be for the next car
                if(UseNodeMutation)     // Should we use Node Mutation
                    Car.NextNetwork.MutateNodes(); // Mutate its nodes
                else
                    Car.NextNetwork.Mutate();      // Mutate its weights
            }

            Cars.Add(Instantiate(CarPrefab, transform.position, 
                     Quaternion.identity, transform).GetComponent<Car>()); // Instantiate 
                                                   // a new car and add it to the list of cars
        }
    }

    // Gets called by cars when they die
    public void CarDead (Car DeadCar, int Fitness)
    {
        Cars.Remove(DeadCar); // Remove the car from the list
        Destroy(DeadCar.gameObject); // Destroy the dead car

        if (Fitness > BestFitness) // If it is better that the current best car
        {
            BestNeuralNetwork = DeadCar.TheNetwork; // Make sure it becomes the best car
            BestFitness = Fitness; // And also set the best fitness
        }

        if (Cars.Count <= 0)       // If there are no cars left
            StartGeneration();     // Create a new generation
    }
}

将所有内容搅拌均匀并点击播放后,您将得到这个

关注点

仅仅通过进行一个非常简单的添加,就可以看到与上一篇文章相比,训练有了如此巨大的改进,真是太棒了。 既然我们有了一些改进,现在轮到你告诉我你对这一切的看法了。 你认为我下一步应该做什么? 而且,我应该制作关于人工智能之类的 youtube 视频,还是应该坚持写文章?

历史

  • 版本 1.0:主要实现
© . All rights reserved.