1. 程式人生 > >ARCore之路-計算機視覺之例項(一)

ARCore之路-計算機視覺之例項(一)

  有了前面的基礎,我們接下來將親自編寫一個神經網路,我們力求程式碼簡潔並易於理解,效能則不是考慮的重點。當然,完完全全講清神經網路還是比較困難的,這需要一些前置知識,也不是我們關注的重點,我們的重點是利用神經網路來服務AR應用。在開始之前,我們再來看一下人類大腦的神經。

DavidWang原創
  這個神經結構我們可以簡化成如下圖:
DavidWang原創

  這個圖我們可以這樣看,一個簡單的神經可以由一個個的神經元細胞(Neuron )和連線神經細胞的突觸(Synapse)相互連線而成。因此,我們可以把神經元細胞抽象為下圖所示結構:

DavidWang原創

  一個神經元細胞可以有若干個輸入,也可以有若干個輸出,每個輸入都通過一個突觸與外界相連,同理,每一個輸出也通過一個突觸與外界相連。一個突觸實際就是一個通道,連線不同的神經元。所示我們把突觸抽象成如下圖:

DavidWang原創

  突觸的兩端是兩個神經元細胞,它把兩個神經元細胞連線起來,在神經細胞間傳遞資訊。基於上面的分析,我們可以分別編寫突觸與神經元細胞的程式碼如下:

public class Neuron 
{
    private static readonly System.Random Random = new System.Random();
    public List<Synapse> InputSynapses;
    public List<Synapse> OutputSynapses;
    public double Bias;
    public double BiasDelta;
    public double Gradient;
    public double Value;

    public Neuron()
    {
        InputSynapses = new List<Synapse>();
        OutputSynapses = new List<Synapse>();
        Bias = GetRandom();
    }

    public Neuron(IEnumerable<Neuron> inputNeurons) : this()
    {
        foreach (var inputNeuron in inputNeurons)
        {
            var synapse = new Synapse(inputNeuron, this);
            inputNeuron.OutputSynapses.Add(synapse);
            InputSynapses.Add(synapse);
        }
    }
}

public class Synapse
{
    public Neuron InputNeuron;
    public Neuron OutputNeuron;
    public double Weight;
    public double WeightDelta;
    public Synapse(Neuron inputNeuron, Neuron outputNeuron)
    {
        InputNeuron = inputNeuron;
        OutputNeuron = outputNeuron;
        Weight = Neuron.GetRandom();
    }
}

  在神經元類(Neuron )的定義中,我們定義了若干輸入突觸(Synapse),也定義了若干輸出突觸(Synapse),這就是public List InputSynapses 與 public List OutputSynapses;然後我們還定義了偏置值Bias。設定偏置值的目的是對結果做一個偏移(神經元的輸出其實已經一種分類法,有輸出為一類,沒有輸出是另一類)。理解偏置很簡單,如假設我們有直線方程x1+x2-3=0,畫出這個影象如下:

DavidWang原創
  在x1+x2-3=0方程裡,-3就是偏置值,如果為0,則x1+x2=0的影象如下:
DavidWang原創
  直觀的說,就是對輸出做了一個偏移。在神經元類中,BiasDelta是偏置值增量,Gradient是梯度值,用於梯度下降演算法,以收斂函式,Value為該神經元的計算值,這是一個綜合了所有輸入後神經元細胞本身的一個輸出值。神經元類Neuron有兩個建構函式,一個無參建構函式只是簡單的初始化一個無輸入無輸出的神經元細胞;另一個有參建構函式構建一個帶若干輸入的神經元細胞。

  在突觸類(Synapse)的定義很簡單,連線一個輸入神經元細胞,連線一個輸出神經元細胞。還有一個權重值以及一個權重值增量。這個類定義很簡單,不詳述。
  因為神經元還應該具備邏輯思考(計算),訓練更新突觸(Synapse)權重的作用,所以我們還需要把神經元類Neuron擴充套件成如下:

public class Neuron
{
    private static readonly System.Random Random = new System.Random();
    public List<Synapse> InputSynapses;
    public List<Synapse> OutputSynapses;
    public double Bias;
    public double BiasDelta;
    public double Gradient;
    public double Value;

    public static double GetRandom()
    {
        return 2 * Random.NextDouble() - 1;
    }

    public Neuron()
    {
        InputSynapses = new List<Synapse>();
        OutputSynapses = new List<Synapse>();
        Bias = GetRandom();
    }

    public Neuron(IEnumerable<Neuron> inputNeurons) : this()
    {
        foreach (var inputNeuron in inputNeurons)
        {
            var synapse = new Synapse(inputNeuron, this);
            inputNeuron.OutputSynapses.Add(synapse);
            InputSynapses.Add(synapse);
        }
    }

    public virtual double CalculateValue()
    {
        return Value = Sigmoid.Output(InputSynapses.Sum(a => a.Weight * a.InputNeuron.Value) + Bias);
    }

    public double CalculateError(double target)
    {
        return target - Value;
    }

    public double CalculateGradient(double? target = null)
    {
        if (target == null)
            return Gradient = OutputSynapses.Sum(a => a.OutputNeuron.Gradient * a.Weight) * Sigmoid.Derivative(Value);

        return Gradient = CalculateError(target.Value) * Sigmoid.Derivative(Value);
    }

    public void UpdateWeights(double learnRate, double momentum)
    {
        var prevDelta = BiasDelta;
        BiasDelta = learnRate * Gradient;
        Bias += BiasDelta + momentum * prevDelta;

        foreach (var synapse in InputSynapses)
        {
            prevDelta = synapse.WeightDelta;
            synapse.WeightDelta = learnRate * Gradient * synapse.InputNeuron.Value;
            synapse.Weight += synapse.WeightDelta + momentum * prevDelta;
        }
    }

}

  GetRandom()方法獲取一個[-1,1]之間的隨機值。CalculateValue()方法使用Sigmoid啟用函式獲取當前神經元細胞的輸出值。CalculateGradient()方法計算一個梯度值以便訓練調整突觸的權重值。UpdateWeights()方法用於訓練更新突觸的權重值。這裡只著重解釋一下Sigmoid啟用函式,Sigmoid啟用函式定義如下:

DavidWang原創
  Sigmoid啟用函式曲線如下:
DavidWang原創
  Sigmoid啟用函式性質:
  • 定義域:(−∞,+∞)
  • 值域:(−1,1)
  • 函式在定義域內為連續和光滑函式
  • 處處可導,導數為:f′(x)=f(x)(1−f(x))

  Sigmoid啟用函式的這些性質非常類似神經元細胞的工作方式,一個神經元細胞在獲取多個輸入後,經過其本身的邏輯思考(計算),這個神經元細胞可以有電平輸出以便傳遞資訊,也可以不輸出。事實上,通過訓練後的神經網路,對於一個未知輸入,往往不能得到100%的預測,但卻可以得到一個發生的可能性,通常我們認為超過50%則認為事件發生(即有輸出),低於50%則認為事件不發生(即無輸出)。Sigmoid類的定義如下:

public static class Sigmoid
{
    public static double Output(double x)
    {
        return x < -45.0 ? 0.0 : x > 45.0 ? 1.0 : 1.0 / (1.0 + Mathf.Exp((float)-x));
    }

    public static double Derivative(double x)
    {
        return x * (1 - x);
    }
}

  為了減輕計算壓力,輸入小於-45我們直接返回0,輸入大於45我們直接返回1,處於這兩者之間的值我們才進行計算。至此,我們已完成了神經網路最基本的神經元及突觸的模擬程式碼編寫。有了這兩個最基本的要素,下面我們將構建一個神經網路。