實質上鼓勵一下吧

之前寫的類神經程式因為考慮到多執行緒,許多人看不懂,這一篇用C#寫,使用陣列來表達神經元,並且完全不考慮多執行緒的問題,應該比較容易理解。

class Element {
    public double Value;
    public double GateValue;
    public double[] Synapse;

    internal double diffentValue;
    internal double fixGateValue;
    internal double[] fixSynapse;

    public Element(int UpperLayerSize) {
        Synapse = new double[UpperLayerSize];
        fixSynapse = new double[UpperLayerSize];

        Random rnd = new Random(DateTime.Today.Millisecond);
        if (UpperLayerSize > 0) {
            GateValue = rnd.NextDouble() * 2 - 1;
            for (int s = 0; s < Synapse.Length; s++) {
                Synapse[s] = rnd.NextDouble() * 2 - 1;
            }
        }
    }
}

首先當然就是設計神經元,Value為神經元輸出值,如果神經元位於輸入層,它同時也是輸入值,這樣設計是為了後面計算的程式好寫。神經元初始化的時候必須要告訴他上一層的神經元個數,這樣他才能準備好神經鍵陣列Synapse。初始化的時候讓閥值(GateValue)、神經鍵(Synapse)都使用亂數設定初始值,根據經驗如果使用固定值,也就是1與-1交錯的初始值,在某些案例中可能不容易收斂。

同時神經元也包含誤差值(diffentValue)、閥值修正(fixGateValue)、神經鍵修正(fixSynapse[]),這樣把所有的值都放一起應該比較容易懂了吧。

class Network {
    public Element[][] Elements;
    public double[] Standar;
    public double DiffentValue;

    public Element[] OutputLayer {
        get { return Elements[2]; }
    }

    public Element[] InputLayer {
        get { return Elements[0]; }
    }

    public Network(int InputLayerSize, int HiddenLayerSize, int OutputLayerSize) {
        Elements = new Element[3][];
        Elements[0] = new Element[InputLayerSize];
        Elements[1] = new Element[HiddenLayerSize];
        Elements[2] = new Element[OutputLayerSize];
        Standar = new double[OutputLayerSize];

        int upperLayerSize = 0;
        for (int l = 0; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e] = new Element(upperLayerSize);
            }
            upperLayerSize = Elements[l].Length;
        }
    }

    public void ClearValue() {
        DiffentValue = 0;
        for (int l = 0; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e].Value = 0;
                Elements[l][e].diffentValue = 0;
                Elements[l][e].fixGateValue = 0;
                for (int s = 0; s < Elements[l][e].fixSynapse.Length; s++) {
                    Elements[l][e].fixSynapse[s] = 0;
                }
            }
        }
        for (int h = 0; h < LastHidden.Length; h++) {
            LastHidden[h] = 0;
        }
    }

    public void Summation() {
        for (int l = 1; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                double outvalue = -Elements[l][e].GateValue;
                for (int s = 0; s < Elements[l][e].Synapse.Length; s++) {
                    outvalue += Elements[l - 1][s].Value * Elements[l][e].Synapse[s];
                }
                Elements[l][e].Value = (double)(1 / (1 + Math.Exp(-outvalue)));
            }
        }
    }

    public void CalcDiffent() {
        //output layer
        for (int e = 0; e < OutputLayer.Length; e++) {
            //給電腦看的用標準差公式
            OutputLayer[e].diffentValue = (Standar[e] - OutputLayer[e].Value) * (OutputLayer[e].Value * (1 - OutputLayer[e].Value) + 0.01);
            //給人看的用傳統公式
            DiffentValue += Math.Abs(Standar[e] - OutputLayer[e].Value);
        }
        //hidden layer
        for (int l = Elements.Length - 2; l > 0; l--) {
            for (int e = 0; e < Elements[l].Length; e++) {
                double sumDiff = 0;
                for (int ne = 0; ne < Elements[l + 1].Length; ne++) {
                    sumDiff += Elements[l + 1][ne].Synapse[e] * Elements[l + 1][ne].diffentValue;
                }
                Elements[l][e].diffentValue = (Elements[l][e].Value * (1 - Elements[l][e].Value)) * sumDiff;
            }
        }
    }

    public void CalcFixValue(double LearnSpeed) {
        for (int l = Elements.Length - 1; l > 0; l--) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e].fixGateValue = -LearnSpeed * Elements[l][e].diffentValue;
                for (int ue = 0; ue < Elements[l - 1].Length; ue++) {
                    Elements[l][e].fixSynapse[ue] += LearnSpeed * Elements[l][e].diffentValue * Elements[l - 1][ue].Value;
                }
            }
        }
    }

    public void FixNetwork(int SampleCount) {
        for (int l = 1; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e].GateValue += Elements[l][e].fixGateValue / Math.Sqrt(SampleCount);
                for (int s = 0; s < Elements[l][e].fixSynapse.Length; s++) {
                    Elements[l][e].Synapse[s] += Elements[l][e].fixSynapse[s] / Math.Sqrt(SampleCount);
                }
            }
        }
    }
}

接著解釋網路元件的設計。

    public Element[][] Elements;

用一個二維的動態陣列來儲存神經元,陣列第一個註腳就是神經層,第二個註腳就是每個神經層的神經元。

    public Network(int InputLayerSize, int HiddenLayerSize, int OutputLayerSize) {
        Elements = new Element[3][];
        Elements[0] = new Element[InputLayerSize];
        Elements[1] = new Element[HiddenLayerSize];
        Elements[2] = new Element[OutputLayerSize];
        Standar = new double[OutputLayerSize];

        int upperLayerSize = 0;
        for (int l = 0; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e] = new Element(upperLayerSize);
            }
            upperLayerSize = Elements[l].Length;
        }
    }

由於C#的動態陣列允許下層陣列是不同長度的,因此使用動態的方式宣告每一層的大小,這樣也比較符合實際網路架構。

    public Element[] OutputLayer {
        get { return Elements[2]; }
    }

    public Element[] InputLayer {
        get { return Elements[0]; }
    }

為了程式方便,設計兩個屬性,直接傳回輸入層和輸出層。

    public void ClearValue() {
        DiffentValue = 0;
        for (int l = 0; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e].Value = 0;
                Elements[l][e].diffentValue = 0;
                Elements[l][e].fixGateValue = 0;
                for (int s = 0; s < Elements[l][e].fixSynapse.Length; s++) {
                    Elements[l][e].fixSynapse[s] = 0;
                }
            }
        }
    }

因為網路的修正動作是將所有的範例都計算過修正值之後,再一次進行修正,並且再用新的網路進行計算,因此必須在計算之前將網路的動態值歸零,也就是說上面這段程式裏面所歸零的值都是加總的值,必須在新的週期開始的時候清除掉。

    public void Summation() {
        for (int l = 1; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                double outvalue = -Elements[l][e].GateValue;
                for (int s = 0; s < Elements[l][e].Synapse.Length; s++) {
                    outvalue += Elements[l - 1][s].Value * Elements[l][e].Synapse[s];
                }
                Elements[l][e].Value = (double)(1 / (1 + Math.Exp(-outvalue)));
            }
        }
    }

計算網路的輸出值,看過書的應該可以看得懂,數學公式實在不好貼,等我想到好方法再補上吧。

    public void CalcDiffent() {
        //output layer
        for (int e = 0; e < OutputLayer.Length; e++) {
            //給電腦看的用標準差公式
            OutputLayer[e].diffentValue = (Standar[e] - OutputLayer[e].Value) * (OutputLayer[e].Value * (1 - OutputLayer[e].Value) + 0.01);
            //給人看的用傳統公式
            DiffentValue += Math.Abs(Standar[e] - OutputLayer[e].Value);
        }
        //hidden layer
        for (int l = Elements.Length - 2; l > 0; l--) {
            for (int e = 0; e < Elements[l].Length; e++) {
                double sumDiff = 0;
                for (int ne = 0; ne < Elements[l + 1].Length; ne++) {
                    sumDiff += Elements[l + 1][ne].Synapse[e] * Elements[l + 1][ne].diffentValue;
                }
                Elements[l][e].diffentValue = (Elements[l][e].Value * (1 - Elements[l][e].Value)) * sumDiff;
            }
        }
    }

計算網路誤差值,這裡我把公式分成兩種,一種是用來計算修正網路值用的,使用書上所寫的公式。而另一個是給人看的,同時也是輸出值夠精密到可以跳出的依據。為甚麼要這麼做請參考這一篇,雖然很多人來看,還是沒有人告訴我答案,所以我用土方法解決這個問題。

    public void CalcFixValue(double LearnSpeed) {
        for (int l = Elements.Length - 1; l > 0; l--) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e].fixGateValue += -LearnSpeed * Elements[l][e].diffentValue;
                for (int ue = 0; ue < Elements[l - 1].Length; ue++) {
                    Elements[l][e].fixSynapse[ue] += LearnSpeed * Elements[l][e].diffentValue * Elements[l - 1][ue].Value;
                }
            }
        }
    }

得到誤差值之後就可以依照誤差值來得到修正值,因為要所有的範例都學習過才進行網路修正,所以所有的修正都是累加的。公式還是請自行參考書上說明。事實上這一段程式可以跟計算誤差值的程式合併,分開來寫比較容易看得懂,如果想要效能好一點就請將它合併在一起。

    public void FixNetwork(int SampleCount) {
        for (int l = 1; l < Elements.Length; l++) {
            for (int e = 0; e < Elements[l].Length; e++) {
                Elements[l][e].GateValue += Elements[l][e].fixGateValue / Math.Sqrt(SampleCount);
                for (int s = 0; s < Elements[l][e].fixSynapse.Length; s++) {
                    Elements[l][e].Synapse[s] += Elements[l][e].fixSynapse[s] / Math.Sqrt(SampleCount);
                }
            }
        }
    }

當所有的範例都學習完成之後,當然就是要實際修正網路內容,修正值取平均,所以需要傳入範例個數來進行運算。

abstract class RobotBase {
    public int SampleCount;

    internal int inputLayerSize, outputLayerSize;
    internal Network worknet;
    internal int noBestCount;
    internal int learnSamples;
    internal double bestDiffent = 10000;

    public abstract void LoadSample();
    public abstract void LoadData(int SampleNo);

    public delegate void OnCycleFinish(int CycleNo, double BestDiffent, double NewDiffent);
    public event OnCycleFinish EventCycleFinish;

    public delegate void OnBadLearning(int NoBestCount);
    public event OnBadLearning EventBadLearning;

    public virtual void Learning(double LearnSpeed, int HiddenLayerSize, int NoBestLimit, double Precision) {
        worknet = new Network(inputLayerSize, HiddenLayerSize, outputLayerSize);
        bestDiffent = 10000;
        int cycle = 0;
        noBestCount = 0;
        while ((noBestCount < NoBestLimit) && (bestDiffent > Precision)) {
            cycle++;
            worknet.ClearValue();
            for (int sampleNo = 0; sampleNo < learnSamples; sampleNo++) {
                LoadData(sampleNo);
                worknet.Summation();
                worknet.CalcDiffent();
                worknet.CalcFixValue(LearnSpeed);
            }
            double newDiffent = worknet.DiffentValue / learnSamples;
            if (newDiffent<bestDiffent) {
                bestDiffent = newDiffent;
                noBestCount = 0;
            }
            else {
                noBestCount++;
            }
            if (EventBadLearning != null) EventBadLearning(noBestCount);
            worknet.FixNetwork(learnSamples);
            if (EventCycleFinish != null) EventCycleFinish(cycle, bestDiffent, newDiffent);
        }
    }
}

到這裡網路就建構完成了,應該容易懂多了吧,程式都很短。接下來就是學習機器人的程式了。

abstract class RobotBase {

    public abstract void LoadSample();
    public abstract void LoadData(int SampleNo);

這個類是個基礎類,因為載入資料和將資料放進輸入層的動作,每一個案例都不相同,因此使用必須繼承的關鍵字abstract宣告這個類別,並且宣告兩個方法LoadSample和LoadData為必須實做的。

    public delegate void OnCycleFinish(int CycleNo, double BestDiffent, double NewDiffent);
    public event OnCycleFinish EventCycleFinish;

因為程式會執行比較久,當然要讓使用者知道現在的進行狀況,所以宣告了一些事件。

    if (EventCycleFinish != null) EventCycleFinish(cycle, bestDiffent, newDiffent);

使用這樣就可以觸發事件,而前面的判斷是要確定這個事件確實有被實做。

RobotXOR Robot = new RobotXOR();

Robot.EventCycleFinish += new RobotBase.OnCycleFinish(Form1_EventCycleFinish);

void Form1_EventCycleFinish(int CycleNo, double BestDiffent, double NewDiffent) {
    lbCycle.Text = string.Format("週期:{0}", CycleNo);
    lbBestDiffent.Text = string.Format("最佳:{0:F6}", BestDiffent);
    lbNewDiffent.Text = string.Format("最新:{0:F6}", NewDiffent);
    Application.DoEvents();
}

前端可以用這樣的方法去處理事件。因為程式是持續一直在運作,如果使用的是單核心的CPU會看到CPU一直是100%在運作,所以當程式有資訊要顯示在視窗上,就必須用Application.DoEvents(),這樣才能讓CPU資源暫時的釋放出來,視窗才會刷新。

    public virtual void Learning(double LearnSpeed, int HiddenLayerSize, int NoBestLimit, double Precision) {
        worknet = new Network(inputLayerSize, HiddenLayerSize, outputLayerSize);
        bestDiffent = 10000;
        int cycle = 0;
        noBestCount = 0;
        while ((noBestCount < NoBestLimit) && (bestDiffent > Precision)) {
            cycle++;
            worknet.ClearValue();
            for (int sampleNo = 0; sampleNo < learnSamples; sampleNo++) {
                LoadData(sampleNo);
                worknet.Summation();
                worknet.CalcDiffent();
                worknet.CalcFixValue(LearnSpeed);
            }
            double newDiffent = worknet.DiffentValue / learnSamples;
            if (newDiffent<bestDiffent) {
                bestDiffent = newDiffent;
                noBestCount = 0;
            }
            else {
                noBestCount++;
            }
            if (EventBadLearning != null) EventBadLearning(noBestCount);
            worknet.FixNetwork(learnSamples);
            if (EventCycleFinish != null) EventCycleFinish(cycle, bestDiffent, newDiffent);
        }
    }

最後就是最重要的學習過程了,基本程式和前面VB.Net的寫法一樣,就是用【找不到最佳值的次數】或【達到預定的精密度】來決定學習是否完成。不過有的時候誤差值調整的幅度很小,可能導致跑了幾十萬次都還出不來,可以考慮再增加一個對cycle的限制。

其他都是呼叫前面所寫的物件,應該可以看得懂了。

public override void Learning(double LearnSpeed, int HiddenLayerSize, int CycleLimit, double Precision) {
        while (bestDiffent > Precision) {
            if (EventHiddenLayerChange != null) EventHiddenLayerChange(HiddenLayerSize);
            base.Learning(LearnSpeed, HiddenLayerSize, CycleLimit, Precision);
            if (bestDiffent > Precision) {
                HiddenLayerSize = (int)(HiddenLayerSize * 1.2);
            }
        }
}

通常隱藏層的寬度可以設置為hiddenLayerSize=(inputLayerSize+outputLayerSize)/2,但是許多案例並不能滿足需求,而是要用嘗試錯誤法去求得合適的隱藏層寬度。前面Learning宣告為可以被重新包裝的,就是爲了這個,當然你也可以把它包裝在一起。如果一個學習週期跳出後,精密度不夠就試著調整隱藏層寬度。

這樣的程式可以應用在許多資料間沒有時間關係的案例之中,例如簡單的XOR、七節顯示器,或許也可以拿來做紫薇斗數的排盤,但是實際上我們找到的案例都是跟時間有關係的,而這樣的網路處理時間關係只能夠讓輸入層個數加倍來解決,否則就需要想辦法讓前面的時間關係以向量來表示,例如股票的N日線,但N日線也還只是個數值,必須再轉換成向量,這樣所得到的準確度是值得商榷的。

當然漠哥已經開始研究時間駐列資料的處理方法,市面上也有相關的產品已經在販售了。我的程式是寫出來了,但是沒有資料來源,如果能夠拿到台灣股市的收盤歷史資料,或許就能證實類神經網路是否真的那麼神奇,可以讓某些證券公司獲利數百倍了。

創作者介紹

人生四十宅開始 二號宅

漠哥 發表在 痞客邦 留言(18) 人氣()


留言列表 (18)

發表留言
  • asusp4b533
  • 最近要寫防火牆
    想用類神經網路來處理攻擊偵測

    這幾篇文章,讓我受益良多
  • 蚯蚓
  • 關於AI

    恩...是說我最近想要寫黑白棋的AI
    但是因為強度遲遲無法提升,
    想說,試試看利用類神經網路來做
    請問該從何入門最好?
  • 類神經網路是必須知道正確答案的尋求建議解決方案,因為我也會下圍棋,曾經思考過要怎麼應用,但是實在找不到好的方法去實做,所以我覺得似乎應該尋求別的方法。
    棋類遊戲不外乎就是看你的程式能夠在短時間內思考多少步,並且對每一種走法都給予評價,或許將第一步可能的走法都做成多線程(多執行緒)的方式,朝這個方向似乎是比較合理。

    漠哥 於 2009/10/22 11:56 回覆

  • 過路人
  • 關於程式

    我最近在研究倒傳遞神經網路
    所以我需要更完整的資料及範例
    希望筆者可以再把相關資訊在繼續PO上來
  • 智慧工人
  • 好感動, 找了好久, 總算找到我看得懂的類神經網路程式範例.
    一般類神經網路的書大多偏重數學或硬體電路,
    難得有程式實作範例, 還附詳盡解說, 不留言感謝一下, 實在對不起您!
  • 這都是我自己玩出來的,但是理論實在相當艱深,貼出來的部份都是儘量抽取能讓中上程式設計師看得懂的,請笑納!
    正在考慮是否要將複雜版本也貼出來,不過似乎簡單版已經沒有幾個看得懂了。

    漠哥 於 2009/12/01 18:54 回覆

  • 很想學
  • 請問這是用哪種程式撰寫的 vb ?? c/c++??
    如果可以 能不能教我呢?!!
  • 這是使用VB.NET所寫的程式,當然用傳統程式一樣可以寫得出來,但是VB.NET提供了物件以及比較結構化的程式寫法。
    事實上,我後來所寫的都是改用C#來寫了,因為vb.net的物件導向看起來就是不順眼!

    漠哥 於 2010/01/13 09:02 回覆

  • 很想學
  • 可否請大大傳授我幾招呢??!!
    不是為了專題那些 存署我個人興趣
  • 悄悄話
  • 學生
  • 請問一下!!
    如果要應用在圖形辨識上,方法也是一樣嗎?
  • 智慧工人
  • 請問我可以把這程式轉譯成 C++ 來用嗎?
    我願意公開轉譯出來的原始碼.
  • 歡迎改寫呀,我把程式碼貼出來就是不怕人抄咯。
    這個程式我實際在運作的是多執行緒版本,而且我也寫了RNN模型的程式碼,
    不過總是很倒楣的會在運作了一段時間中病毒或是硬碟損壞,
    最近正在將自己的開發方向改成PHP+MYSQL(跟類神經無關),
    所以也懶得重寫了,等哪天有心再回頭改寫一次了。

    漠哥 於 2010/11/10 05:06 回覆

  • 學習中
  • 謝謝大大的分享
    學校有開累神經的課程
    但是沒有人知道那隻什麼東西><
    謝謝你
    我決定去修這們課看看^^
  • 啊啊啊,這我也算誤人子弟了吧!

    漠哥 於 2010/12/02 13:50 回覆

  • David
  • 大大您真是太偉大了...
    類神經網路大約25年前我就很感興趣,但是就是找不到好的學習資料,
    又錯過了出國學習的機會,看到今天的文章就像挖到寶一樣,
    尤其大大最後又用C#改寫,真是感動,不然看VB都快昏頭了,
    這幾天會好好研究大大的文章,希望可以實現多年前的願望,
    自己架構出類神經網路,因為很久前就見過國外應用在許多領域的文章。

    PS. 程式設計到今年也快滿30年了,寫資料庫系統更是我賴以維生的專長,
    如果大大有需要支援的地方,請不要客氣盡量找我幫忙,拜謝。
  • 其實我最擅長的是資料庫哦,只是沒有花時間去寫文章而已,用得太多有些可能是技巧性的東西,對我來說已經是定理,自己反而對它沒有感覺了。

    漠哥 於 2011/01/04 13:40 回覆

  • David
  • 我使用 Delphi 也很久了,也改寫過一套大陸人寫的 KTV 系統,
    我想我最不熟悉的語言就是 Java,因為沒機會去寫 Java 的案子,
    不過最近要開始寫 Andriod 系統,就有機會再練練 Java,
    今天很高興有機會看到漠哥寫的類神經網路系統開發實例,
    這個是我在 DOS 時代就很想接觸的領域,^^
  • delphi是個很好的開發工具,pascal是我學習的第二套程式語言。

    漠哥 於 2011/01/04 14:55 回覆

  • 大學生
  • 請問,漠大 假如寫一個類神經網路的程式
    輸入字串來判斷該字串是否為髒話的程式,請問該如何撰寫
    我卡在一開始的地方 像是xor gate都是數字很容易代入,
    那字串我該如何有有規則的將他數值化?
    將字串轉成輸入端
    謝謝!
  • 每一個字是有內碼的,只是句子有長有短,你可能需要規定句子的長度吧,不過拿這個例子來玩類神經,我還真沒想到呢!
    寫隔離罵髒話的程式,首先你必須要先罵一遍......

    漠哥 於 2011/02/28 15:30 回覆

  • 大學生
  • 漠大~謝謝
    嗯,我大致上了解!您說的方式,
    可是總感覺規定句子的長度很怪!
    請問還可能有無其他方式可以判斷,就是不用限制長度的!
    我稍微想一下,可是不知道能不能,就是計算句子的平均分數
    每個字都有自己的分數,就是內碼的值,然後算平均分數,
    可是會導致如1+4=5但是2+3=5是一樣的情況,所以在加上
    Stander標準差以及Variance變異數等這樣就有三個值了,
    依據這三個值來進行學習,您覺得如何?
    還是有其他的方法能解決~謝謝!
  • nobody
  • 您好 , 想請問 LastHidden 宣告在哪裡 ? 找不到宣告定義
    public void ClearValue() {
    ...略....
    for (int h = 0; h < LastHidden.Length; h++) {
    LastHidden[h] = 0;
    }
    }
  • LastHidden就是最後一個隱藏層的大小,可能是編排的時候漏掉了,請自己設定一下就好了!

    漠哥 於 2012/01/06 11:38 回覆

  • Farmer Lu
  • 請問
    1.有沒有 connection 結構, 以做出 非 "全連結" 的網路 ?
    2. 有沒有把 weights 存檔和讀檔的功能?

  • Oliver
  • 漠哥謝謝
    Crazy Stone 及電腦圍棋的巨大成功讓人實際認識到類神經網路算法的不可思議威力
    個人也正想研究這塊領域
    謝謝您的無私分享
    Regards, Oliver