ARCore之路-計算機視覺之例項(一)
有了前面的基礎,我們接下來將親自編寫一個神經網路,我們力求程式碼簡潔並易於理解,效能則不是考慮的重點。當然,完完全全講清神經網路還是比較困難的,這需要一些前置知識,也不是我們關注的重點,我們的重點是利用神經網路來服務AR應用。在開始之前,我們再來看一下人類大腦的神經。
這個圖我們可以這樣看,一個簡單的神經可以由一個個的神經元細胞(Neuron )和連線神經細胞的突觸(Synapse)相互連線而成。因此,我們可以把神經元細胞抽象為下圖所示結構:
一個神經元細胞可以有若干個輸入,也可以有若干個輸出,每個輸入都通過一個突觸與外界相連,同理,每一個輸出也通過一個突觸與外界相連。一個突觸實際就是一個通道,連線不同的神經元。所示我們把突觸抽象成如下圖:
突觸的兩端是兩個神經元細胞,它把兩個神經元細胞連線起來,在神經細胞間傳遞資訊。基於上面的分析,我們可以分別編寫突觸與神經元細胞的程式碼如下:
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,畫出這個影象如下:
在突觸類(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啟用函式定義如下:
- 定義域:(−∞,+∞)
- 值域:(−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,處於這兩者之間的值我們才進行計算。至此,我們已完成了神經網路最基本的神經元及突觸的模擬程式碼編寫。有了這兩個最基本的要素,下面我們將構建一個神經網路。