<cite id="ffb66"></cite><cite id="ffb66"><track id="ffb66"></track></cite>
      <legend id="ffb66"><li id="ffb66"></li></legend>
      色婷婷久,激情色播,久久久无码专区,亚洲中文字幕av,国产成人A片,av无码免费,精品久久国产,99视频精品3
      網(wǎng)易首頁(yè) > 網(wǎng)易號(hào) > 正文 申請(qǐng)入駐

      游戲AI行為決策——MLP(多層感知機(jī)/人工神經(jīng)網(wǎng)絡(luò))

      0
      分享至


      【USparkle專欄】如果你深懷絕技,愛“搞點(diǎn)研究”,樂于分享也博采眾長(zhǎng),我們期待你的加入,讓智慧的火花碰撞交織,讓知識(shí)的傳遞生生不息!

      這是侑虎科技第1902篇文章,感謝作者狐王駕虎供稿。歡迎轉(zhuǎn)發(fā)分享,未經(jīng)作者授權(quán)請(qǐng)勿轉(zhuǎn)載。如果您有任何獨(dú)到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群:793972859)

      作者主頁(yè):

      https://home.cnblogs.com/u/OwlCat

      你一定聽說(shuō)過神經(jīng)網(wǎng)絡(luò)的大名,你有想過將它用于游戲AI的行為決策上嗎?其實(shí)在(2010年發(fā)布的)《最高指揮官2》中就有應(yīng)用了,今天請(qǐng)?jiān)试S我班門弄斧一番,與大家一同用C# 實(shí)現(xiàn)最經(jīng)典的神經(jīng)網(wǎng)絡(luò) —— 多層感知機(jī)(Multilayer Perceptron,簡(jiǎn)稱MLP)。


      一、前言

      神經(jīng)網(wǎng)絡(luò)或者深度學(xué)習(xí),總給人一種「量子力學(xué)」的感覺,總感覺它神秘?zé)o比,又無(wú)所不能。我未學(xué)習(xí)神經(jīng)網(wǎng)絡(luò)之前,總以為它是某種能夠修改自身代碼的代碼,否則怎么能做到從「不會(huì)」變成「會(huì)」的呢?但在親自學(xué)習(xí)后才會(huì)明白,它并沒有做到這種地步,但依舊十分神奇。多層感知機(jī)是最基礎(chǔ)的神經(jīng)網(wǎng)絡(luò),很多其它類別的神經(jīng)網(wǎng)絡(luò)都是在這之上的變形。可以說(shuō),學(xué)會(huì)它是邁入深度學(xué)習(xí)的第一步。

      多層感知機(jī)雖說(shuō)經(jīng)典,但并不過時(shí)。提到神經(jīng)網(wǎng)絡(luò),大多數(shù)人腦海里想到的大概也就是類似這樣的圖片:


      這就是一張典型的多層感知機(jī)結(jié)構(gòu)圖,看著好像很復(fù)雜,但實(shí)現(xiàn)它所需要用到的數(shù)學(xué)原理和編程知識(shí)都不難,早年間,研究神經(jīng)網(wǎng)絡(luò)的學(xué)者們還用C語(yǔ)言實(shí)現(xiàn)呢!

      二、什么是多層感知機(jī)

      現(xiàn)在進(jìn)入正題,我們先來(lái)簡(jiǎn)單講講MLP的原理(如果你對(duì)此十分熟悉,只是對(duì)代碼實(shí)現(xiàn)感興趣,那可以跳過這部分)。

      既然叫「多層感知機(jī)」,那有單個(gè)的感知機(jī)嗎?那是自然,單個(gè)感知機(jī)的結(jié)構(gòu)十分簡(jiǎn)單:


      它其實(shí)就是個(gè)算式(為方便理解,我將其分成兩部分):


      將傳入感知機(jī)的多個(gè)輸入x,與對(duì)應(yīng)的權(quán)重w相乘(輸入的數(shù)量與權(quán)重的數(shù)量是一樣的,且數(shù)量是任意的,本例中用了3個(gè)),再加上偏置b就可以得出一個(gè)計(jì)算值sum。再將這個(gè)計(jì)算值傳入一個(gè)函數(shù)f(x)就可得到感知機(jī)的最終輸出out。

      相信你肯定能理解,只是可能對(duì)f(x)函數(shù)有些好奇,它具體內(nèi)容是什么呢?這個(gè)函數(shù)也被稱為「激活函數(shù)」,為什么叫這個(gè)名字?這就得提感知機(jī)的另一個(gè)名字 —— 人工神經(jīng)元。其實(shí)感知機(jī)正是受神經(jīng)元結(jié)構(gòu)啟發(fā)而被提出來(lái)的:


      神經(jīng)元會(huì)通過樹突接受輸入信號(hào)并匯總,而神經(jīng)元對(duì)于各個(gè)輸入刺激的響應(yīng)強(qiáng)度并不相同,所以我們給各個(gè)輸入設(shè)置了相應(yīng)權(quán)重來(lái)模擬這個(gè)現(xiàn)象。之后,將處理的信息通過軸突傳給末梢(終端)。但實(shí)際上,只有匯總的信號(hào)強(qiáng)度大于一定程度時(shí)神經(jīng)元才會(huì)向末梢傳遞信號(hào),而模擬這個(gè)現(xiàn)象的就是「激活函數(shù)」。

      那偏置b又是模擬什么的?其實(shí)它是從數(shù)學(xué)角度考慮的、方便調(diào)整輸入加權(quán)和的變量值而已。

      既然感知機(jī)被稱為「人工神經(jīng)元」,那多層感知機(jī)豈不就是「人工神經(jīng)網(wǎng)絡(luò)」?一點(diǎn)沒錯(cuò),我們現(xiàn)在所說(shuō)的「神經(jīng)網(wǎng)絡(luò)」,基本都是指人工神經(jīng)網(wǎng)絡(luò),而不是真正的、生物的神經(jīng)網(wǎng)絡(luò)。而「神經(jīng)網(wǎng)絡(luò)」起初就是指多層感知機(jī),只不過現(xiàn)在種類多了,定義也變寬泛了。

      結(jié)合我們對(duì)單個(gè)感知機(jī)的認(rèn)識(shí),再看看多層感知機(jī):


      但這里的單個(gè)感知機(jī)(后面用「神經(jīng)元」來(lái)代稱)怎么輸出了多個(gè)值(看標(biāo)藍(lán)色的那部分)?這種結(jié)構(gòu)圖可能會(huì)誤導(dǎo)某些人,我做個(gè)解釋,這里的每一條線并不是輸出,可以看到它是有箭頭的,每條線表示它所指向的那個(gè)神經(jīng)元的一個(gè)權(quán)重。加上箭頭是為了表示數(shù)據(jù)傳遞的方向。

      不難看出,它就是將多個(gè)感知機(jī)以層為單位進(jìn)行了組合,每層都有任意數(shù)量(每層的數(shù)量可以不同)的感知機(jī),并將一層感知機(jī)的輸出作為下一層的輸入,依次套娃下去。像圖中,下一層神經(jīng)元的權(quán)重?cái)?shù)量 = 上一層的神經(jīng)元數(shù)量,稱為全連接,是神經(jīng)網(wǎng)絡(luò)中常見的連接方式,本文也只考慮這種連接方式。

      這里的「輸入層」其實(shí)就是輸入的數(shù)據(jù)(是的,這一層不是神經(jīng)元),類似之前的x0、x1、x2;「輸出層」就是用于輸出的神經(jīng)元所組成的層,有了多個(gè)感知機(jī),我們也可以得到多個(gè)輸出;夾在「輸入層」與「輸出層」之間的就叫「隱藏層」,因?yàn)樵趯?shí)際使用神經(jīng)網(wǎng)絡(luò)時(shí),就只是輸入一組數(shù)值作為「輸入層」,再看看「輸出層」得到的結(jié)果,并不關(guān)心中間的運(yùn)算。

      我們常說(shuō)的「深度學(xué)習(xí)」里的「深度」指的就是神經(jīng)網(wǎng)絡(luò)中「隱藏層」的層數(shù)(只不過現(xiàn)在這個(gè)詞有點(diǎn)被炒作了),當(dāng)一個(gè)神經(jīng)網(wǎng)絡(luò)的隱藏層超過3層時(shí),它就是「深度神經(jīng)網(wǎng)絡(luò)」。

      通過改變神經(jīng)網(wǎng)絡(luò)的結(jié)構(gòu)或者調(diào)節(jié)神經(jīng)網(wǎng)絡(luò)的權(quán)重和偏置,我們可以用神經(jīng)網(wǎng)絡(luò)近似任何的函數(shù)、甚至是一些摸不著頭腦的規(guī)律。

      比如影響小明今天玩不玩網(wǎng)游的因素有:今日作業(yè)量、心情、本月剩余流量、今天是星期幾,但我們并不知道這些因素與小明玩不玩的具體數(shù)學(xué)關(guān)系,只能大概地推斷:今天小明作業(yè)多,不會(huì)玩游戲;又或者今天是星期六,雖然作業(yè)還有很多,但他還是會(huì)玩游戲……可一旦知道具體數(shù)學(xué)關(guān)系,我們就可以通過計(jì)算準(zhǔn)確預(yù)測(cè)小明是否會(huì)玩游戲,就像我們知道了牛頓力學(xué)公式,就可以根據(jù)物體的質(zhì)量和被射出的力來(lái)計(jì)算它的運(yùn)動(dòng)軌跡一樣。


      所以我們所關(guān)心的、實(shí)際所使用的都是這種已經(jīng)設(shè)置好正確權(quán)重和偏置的神經(jīng)網(wǎng)絡(luò),像在與GPT聊天時(shí)怕「污染數(shù)據(jù)庫(kù)」這類事就不用操心了。

      要如何為神經(jīng)網(wǎng)絡(luò)的各個(gè)神經(jīng)元的各個(gè)權(quán)重設(shè)置正確的值,使它能夠輸出我們預(yù)期的結(jié)果呢?手動(dòng)調(diào)肯定不現(xiàn)實(shí),所以我們會(huì)運(yùn)用一些數(shù)學(xué)知識(shí)讓程序自行調(diào)整權(quán)重,這個(gè)過程就是「訓(xùn)練/學(xué)習(xí)」

      我們會(huì)給出一些輸入以及該輸入所對(duì)應(yīng)的正確輸出,比如我們可以記錄小明上個(gè)學(xué)期玩網(wǎng)游時(shí)的各因素值以及不玩時(shí)的各因素值,這些作為「訓(xùn)練集」。然后設(shè)計(jì)一個(gè)「損失函數(shù)」評(píng)判當(dāng)前神經(jīng)網(wǎng)絡(luò)的輸出與正確輸出之間的差距。而程序就是不斷地調(diào)節(jié)各個(gè)權(quán)重,使差距越來(lái)越小,這種調(diào)節(jié)的根據(jù)是「導(dǎo)數(shù)」,但在這里我就不展開了。總之,如果訓(xùn)練得當(dāng),神經(jīng)網(wǎng)絡(luò)的損失就會(huì)越來(lái)越小,直到停在一個(gè)值附近,這就是「收斂」


      篇幅所限,我刻意沒有講相關(guān)的數(shù)學(xué)原理,如果你對(duì)此感興趣,又或者對(duì)MLP的運(yùn)作仍有困惑,可以看看以下兩個(gè)視頻。如果準(zhǔn)備好了,下面就進(jìn)入代碼實(shí)現(xiàn)環(huán)節(jié)吧。

      視頻1:


      https://www.bilibili.com/video/BV1bx411M7Zx/?spm_id_from=333.999.0.0&vd_source=c9a1131d04faacd4a397411965ea21f4

      視頻2:


      https://www.bilibili.com/video/BV1o64y1i7yw/?spm_id_from=333.788&vd_source=c9a1131d04faacd4a397411965ea21f4

      三、代碼實(shí)現(xiàn)

      1. 相關(guān)數(shù)學(xué)

      關(guān)于數(shù)學(xué)部分,我只進(jìn)行簡(jiǎn)要說(shuō)明,不講它們的數(shù)學(xué)原理,也不過多注釋。如果你只是想將神經(jīng)網(wǎng)絡(luò)應(yīng)用到游戲中,那這部分完全可以不必深究原理,弄清它們應(yīng)用的場(chǎng)合即可。

      a. 初始化權(quán)重函數(shù)

      神經(jīng)網(wǎng)絡(luò)權(quán)重的初始化十分重要,它會(huì)影響你的神經(jīng)網(wǎng)絡(luò)最后能否訓(xùn)練成功。這里實(shí)現(xiàn)了3種典型的初始化方法:

      • 隨機(jī)初始化(std = 0.01):是比較普通的方法,深度學(xué)習(xí)新手接觸的第一個(gè)初始化方式。

      • Xavier初始化:適用于激活函數(shù)為Sigmoid和Tanh的場(chǎng)合。

      • He初始化:適用于激活函數(shù)為ReLU及其衍生函數(shù),如Leaky ReLU的場(chǎng)合。


      這里我還用了枚舉,方便在編輯時(shí)切換初始化的方法(后續(xù)幾類數(shù)學(xué)函數(shù)也會(huì)用這種方法):

                                                                 using System;


      namespaceJufGame.AI.ANN
      {
      publicstaticclassInitWFunc
      {
      publicenum Type
      {
      Random, Xavier, He, None
      }
      public static void InitWeights(Type initWFunc, Neuron neuron)
      {
      switch(initWFunc)
      {
      case Type.Xavier:
      XavierInitWeights(neuron.Weights);
      break;
      case Type.He:
      HeInitWeights(neuron.Weights);
      break;
      case Type.Random:
      RandomInitWeights(neuron.Weights);
      break;
      default:
      break;
      }
      }
      private static void RandomInitWeights(float[] weightsList)
      {
      var rand = new Random();
      for (int i = 0; i < weightsList.Length; ++i)
      {
      //使用較小的標(biāo)準(zhǔn)差,適合普通的隨機(jī)初始化
      weightsList[i] = (float)(rand.NextGaussian() * 0.01);
      }
      }
      private static void XavierInitWeights(float[] weightsList)
      {
      var rand = new Random();
      var scale = 1f / MathF.Sqrt(weightsList.Length);
      for (int i = 0; i < weightsList.Length; ++i)
      {
      weightsList[i] = (float)(rand.NextDouble() * 2 * scale - scale);
      }
      }
      private static void HeInitWeights(float[] weightsList)
      {
      var rand = new Random();
      var stdDev = MathF.Sqrt(2f / weightsList.Length); //計(jì)算標(biāo)準(zhǔn)差
      for (int i = 0; i < weightsList.Length; ++i)
      {
      //生成服從正態(tài)分布的隨機(jī)數(shù),并乘以標(biāo)準(zhǔn)差
      weightsList[i] = (float)(rand.NextGaussian() * stdDev);
      }
      }
      // 用于生成服從標(biāo)準(zhǔn)正態(tài)分布的隨機(jī)數(shù)的輔助方法
      private static double NextGaussian(this Random rand)
      {
      double u1 = 1.0 - rand.NextDouble(); // 生成 [0, 1) 之間的隨機(jī)數(shù)
      double u2 = 1.0 - rand.NextDouble();
      // 使用 Box-Muller 變換生成正態(tài)分布的隨機(jī)數(shù)
      return Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2);
      }
      }
      }

      b. 激活函數(shù)

      一般神經(jīng)網(wǎng)絡(luò)中所有隱藏層都使用同一種激活函數(shù),輸出層根據(jù)問題需求可能會(huì)使用和隱藏層不一樣的激活函數(shù)。激活函數(shù)都有非線性且可導(dǎo)的特點(diǎn),我也實(shí)現(xiàn)了一些典型的激活函數(shù):


      • 直接輸出(Identify):不做處理直接輸出,用于輸出層。

      • Sigmoid:早期的主流,現(xiàn)在一般用于輸出層需要將輸出值限制在0~1的場(chǎng)合,或者是只有兩個(gè)輸出的二分問題。

      • Tanh:相當(dāng)于Sigmoid的改造,將輸出限制在了-1~1。

      • ReLU:當(dāng)今的主流激活函數(shù),長(zhǎng)得十分友好,甚至不用加減運(yùn)算。一般選它準(zhǔn)沒錯(cuò)。

      • Leaky ReLU:ReLU的改造,使得對(duì)負(fù)數(shù)輸入也有響應(yīng),但并沒有說(shuō)它一定好于ReLU。如果你用ReLU訓(xùn)練出現(xiàn)問題,可以換這個(gè)試試。

      • Softmax:把一系列輸出轉(zhuǎn)為總和為1的小數(shù),并且維持彼此的大小關(guān)系,相當(dāng)于把輸出結(jié)果轉(zhuǎn)為了概率。適用于多分類問題,但一定要搭配交叉熵?fù)p失函數(shù)使用

                                                                 using System;

      namespaceJufGame.AI.ANN
      {
      publicstaticclassActivationFunc
      {
      private delegate float FuncCalc(float x);
      privatestatic FuncCalc curAcFunc;
      publicenum Type
      {
      Identify, Softmax, Tanh, Sigmoid, ReLU, LeakyReLU
      }
      //按層使用激活函數(shù)計(jì)算
      public static void Calc(Type funcType, Layer layer)
      {
      if(funcType == Type.Softmax)
      {
      Softmax_Calc(layer);
      }
      else
      {
      curAcFunc = funcType switch
      {
      Type.Sigmoid => Sigmoid_Calc,
      Type.Tanh => Tanh_Calc,
      Type.ReLU => ReLU_Calc,
      Type.LeakyReLU => LeakyReLU_Calc,
      _ => Identify_Calc,
      };
      for(int i = 0; i < layer.Neurons.Length; ++i)
      {
      layer.Output[i] = curAcFunc(layer.Neurons[i].Sum);
      }
      }
      }
      //根據(jù)傳入下標(biāo)index選取層中神經(jīng)元,并進(jìn)行求導(dǎo)
      public static float Diff(Type funcType, Layer layer, int index)
      {
      return funcType switch
      {
      Type.Softmax => Softmax_Diff(layer, index),
      Type.Sigmoid => Sigmoid_Diff(layer, index),
      Type.Tanh => Tanh_Diff(layer, index),
      Type.ReLU => ReLU_Diff(layer, index),
      Type.LeakyReLU => LeakyReLU_Diff(layer, index),
      _ => Identify_Diff(),
      };
      }

      #region 直接輸出
      private static float Identify_Calc(float x)
      {
      return x;
      }
      private static float Identify_Diff()
      {
      return1;
      }
      #endregion

      #region Softmax
      private static void Softmax_Calc(Layer layer)
      {
      var neurons = layer.Neurons;
      var expSum = 0.0f;
      for(int i = 0; i < neurons.Length; ++i)
      {
      layer.Output[i] = MathF.Exp(neurons[i].Sum);
      expSum += layer.Output[i];
      }
      for(int i = 0; i < neurons.Length; ++i)
      {
      layer.Output[i] /= expSum;
      }
      }
      private static float Softmax_Diff(Layer outLayer, int index)
      {
      return outLayer.Output[index] * (1 - outLayer.Output[index]);
      }
      #endregion

      #region Sigmoid
      private static float Sigmoid_Calc(float x)
      {
      return1.0f / (1.0f + MathF.Exp(-x));
      }
      private static float Sigmoid_Diff(Layer outLayer, int index)
      {
      return outLayer.Output[index] * (1 - outLayer.Output[index]);
      }
      #endregion

      #region Tanh
      private static float Tanh_Calc(float x)
      {
      var expVal = MathF.Exp(-x);
      return (1.0f - expVal) / (1.0f + expVal);
      }
      private static float Tanh_Diff(Layer outLayer, int index)
      {
      return1.0f - MathF.Pow(outLayer.Output[index], 2.0f);
      }
      #endregion

      #region ReLU
      public static float ReLU_Calc(float x)
      {
      return x > 0 ? x : 0;
      }
      public static float ReLU_Diff(Layer outLayer, int index)
      {
      return outLayer.Neurons[index].Sum > 0 ? 1 : 0;
      }
      #endregion

      #region LeakyReLU
      private static float LeakyReLU_Calc(float x)
      {
      return x > 0 ? x : 0.01f * x;
      }
      private static float LeakyReLU_Diff(Layer outLayer, int index)
      {
      return outLayer.Neurons[index].Sum > 0 ? 1 : 0.01f;
      }
      #endregion
      }
      }

      c. 更新權(quán)重函數(shù)

      權(quán)重的更新涉及一些「超參數(shù)」,比如學(xué)習(xí)率、最大迭代次數(shù)等。這些參數(shù)是程序不會(huì)進(jìn)行更新的,只能人工提前設(shè)置好。在神經(jīng)網(wǎng)絡(luò)的學(xué)習(xí)中,學(xué)習(xí)率的值很重要,過小會(huì)導(dǎo)致訓(xùn)練費(fèi)時(shí);過大則會(huì)導(dǎo)致學(xué)習(xí)發(fā)散而不能正確進(jìn)行。但好在后面人們想出來(lái)更好的權(quán)重更新函數(shù),它們對(duì)「超參數(shù)」的依賴會(huì)減小很多。我們所實(shí)現(xiàn)的有:


      • SGD:隨機(jī)梯度下降,最簡(jiǎn)單的一種更新方法,但有時(shí)并不是這么高效,容易陷入局部最優(yōu)解。

      • Movement:基于物理上的動(dòng)量概念,它會(huì)在更新權(quán)重的過程中考慮先前的更新步驟,需要為每個(gè)權(quán)重設(shè)置額外參數(shù)(用m表示)來(lái)記錄「動(dòng)量」。

      • AdaGrad:運(yùn)用了學(xué)習(xí)率衰減的技巧,為每個(gè)權(quán)重適當(dāng)?shù)卣{(diào)整學(xué)習(xí)率,相當(dāng)于給每個(gè)權(quán)重都設(shè)置了獨(dú)立的學(xué)習(xí)率,也需要額外參數(shù)(用v表示)記錄。

      • Adam:將Movment與AdaGrad結(jié)合了起來(lái),通過組合二者的優(yōu)點(diǎn),有望實(shí)現(xiàn)參數(shù)空間的高效搜索。

      當(dāng)然,上述4個(gè)方法各有優(yōu)劣,可以優(yōu)先考慮SGD和Adam。

      順帶一提,權(quán)重的更新都是建立在「梯度」之上的,「梯度」可以理解為對(duì)神經(jīng)網(wǎng)絡(luò)整體權(quán)重的變化趨勢(shì)。你想,有這么多權(quán)重要更新,有時(shí)訓(xùn)練一個(gè)樣本A后,會(huì)要求權(quán)重w0+ = 0.01、w1- = 0.05以減小誤差,但訓(xùn)練下一個(gè)樣本B時(shí),又要求w0- = 0.02、w1+ = 0.04,一個(gè)訓(xùn)練集有這么多樣本,要以哪個(gè)樣本訓(xùn)練時(shí)產(chǎn)生的權(quán)重變化為準(zhǔn)呢?


      答案是累加每個(gè)樣本帶來(lái)的誤差并取平均值。如果覺得還不清楚,可以看看這個(gè)視頻:


      https://www.bilibili.com/video/BV16x411V7Qg/?spm_id_from=333.999.0.0&vd_source=c9a1131d04faacd4a397411965ea21f4

      還有一點(diǎn),偏置b也是隨著權(quán)重更新的,它可以視為一個(gè)輸入始終為1,權(quán)重為b的權(quán)重。在后續(xù)的實(shí)現(xiàn)中,我將偏置放在儲(chǔ)存權(quán)重的列表的最后一位。(但后來(lái)才知道不提倡這種寫法。)

                                                                 using System;

      namespaceJufGame.AI.ANN
      {
      publicstaticclassUpdateWFunc
      {
      privateconstfloat MinDelta = 1e-7f;
      privateconstfloat beta1 = 0.9f;
      privateconstfloat beta2 = 0.999f;
      private delegate void UpdateLayer(Neuron curNeuron, float learningRate, int curEpochs, int samplesCount);
      publicenum Type
      {
      SGD, Momentum, AdaGrad, Adam
      }
      public static void UpdateNetWeights(Type type, NeuralNet net, int samplesCount)
      {
      UpdateLayer updateLayerFunc = type switch
      {
      Type.Momentum => Momentum_UpdateW,
      Type.AdaGrad => AdaGrad_UpdateW,
      Type.Adam => Adam_UpdateW,
      _ => SGD_UpdateW,
      };
      var curLayer = net.OutLayer;
      for(int j = 0; j < curLayer.Neurons.Length; ++j)
      {
      updateLayerFunc(curLayer.Neurons[j], net.LearningRate, net.CurEpochs ,samplesCount);
      }
      for(int i = 0; i < net.HdnLayers.Length; ++i)
      {
      curLayer = net.HdnLayers[i];
      for(int j = 0; j < curLayer.Neurons.Length; ++j)
      {
      updateLayerFunc(curLayer.Neurons[j], net.LearningRate, net.CurEpochs, samplesCount);
      }
      }
      }

      #region 各參數(shù)損失貢獻(xiàn)計(jì)算
      //計(jì)算各參數(shù)對(duì)損失的貢獻(xiàn)程度(也是各參數(shù)的變化的值)
      public static void CalcDelta(NeuralNet net, float[] input)
      {
      var lastInput = input;
      for(int i = 0; i < net.HdnLayers.Length; ++i)
      {
      var curLayer = net.HdnLayers[i];
      CalcLayerDelta(curLayer, lastInput);
      lastInput = curLayer.Output;
      }
      CalcLayerDelta(net.OutLayer, lastInput);
      }
      private static void CalcLayerDelta(Layer curLayer, float[] lastInput)
      {
      for(int j = 0, k; j < curLayer.Neurons.Length; ++j)
      {
      var curNeuron = curLayer.Neurons[j];
      for(k = 0; k < lastInput.Length; ++k)
      {
      //通過反向傳播時(shí)神經(jīng)元的損失,計(jì)算每個(gè)權(quán)重的貢獻(xiàn)貢獻(xiàn)并累加
      curNeuron.WeightParams["Delta"][k] += curNeuron.Params["Error"] * lastInput[k];
      }
      //同理計(jì)算偏置的損失貢獻(xiàn)
      curNeuron.WeightParams["Delta"][k] += curNeuron.Params["Error"];
      }
      }
      #endregion

      #region SGD
      private static void SGD_UpdateW(Neuron curNeuron, float learningRate, int curEpochs, int samplesCount)
      {
      for(int k = 0; k < curNeuron.Weights.Length; ++k)
      {
      var gradient = curNeuron.WeightParams["Delta"][k] / samplesCount;
      curNeuron.Weights[k] -= learningRate * gradient;
      curNeuron.WeightParams["Delta"][k] = 0;
      }
      }
      #endregion

      #region Momentum
      private static void Momentum_UpdateW(Neuron curNeuron, float learningRate, int curEpochs, int samplesCount)
      {
      for(int k = 0; k < curNeuron.Weights.Length; ++k)
      {
      var gradient = curNeuron.WeightParams["Delta"][k] / samplesCount;
      curNeuron.WeightParams["m"][k] = beta1 * curNeuron.WeightParams["m"][k] - learningRate * gradient;
      curNeuron.Weights[k] += curNeuron.WeightParams["m"][k];
      curNeuron.WeightParams["Delta"][k] = 0;
      }
      }
      #endregion

      #region AdaGrad
      private static void AdaGrad_UpdateW(Neuron curNeuron, float learningRate, int curEpochs, int samplesCount)
      {
      for(int k = 0; k < curNeuron.Weights.Length; ++k)
      {
      var gradient = curNeuron.WeightParams["Delta"][k] / samplesCount;
      curNeuron.WeightParams["v"][k] += gradient * gradient;
      curNeuron.Weights[k] -= learningRate * gradient / MathF.Sqrt(curNeuron.WeightParams["v"][k] + MinDelta);
      curNeuron.WeightParams["Delta"][k] = 0;
      }
      }
      #endregion

      #region Adam
      private static void Adam_UpdateW(Neuron curNeuron, float learningRate, int curEpochs, int samplesCount)
      {
      for(int k = 0; k < curNeuron.Weights.Length; ++k)
      {
      var gradient = curNeuron.WeightParams["Delta"][k] / samplesCount;
      curNeuron.WeightParams["m"][k] = beta1 * curNeuron.WeightParams["m"][k] + (1 - beta1) * gradient;
      curNeuron.WeightParams["v"][k] = beta2 * curNeuron.WeightParams["v"][k] + (1 - beta2) * gradient * gradient;
      var mHat = curNeuron.WeightParams["m"][k] / (1 - MathF.Pow(beta1, curEpochs));
      var vHat = curNeuron.WeightParams["v"][k] / (1 - MathF.Pow(beta2, curEpochs));
      curNeuron.Weights[k] -= learningRate * mHat / (MathF.Sqrt(vHat) + MinDelta);
      curNeuron.WeightParams["Delta"][k] = 0;
      }
      }
      #endregion
      }
      }

      d. 損失函數(shù)

      損失函數(shù)用來(lái)衡量輸出與正確值之間的差距,這里實(shí)現(xiàn)的是最常用的兩個(gè)損失函數(shù):

      • 均方差函數(shù):簡(jiǎn)單實(shí)用,形式如下:


      • 交叉熵函數(shù):主要用在多分類問題上,配合Softmax使用:


                                                                 using System;

      namespaceJufGame.AI.ANN
      {
      publicstaticclassLossFunc
      {
      privateconstfloat MinDelta = 1e-7f;
      publicenum Type
      {
      MeanSqurad, CrossEntropy,
      }
      public static float Calc(Type type, float[] targetOut, Layer outLayer)
      {
      return type switch
      {
      Type.MeanSqurad => MeanSquradErr_Calc(targetOut, outLayer),
      _ => CrossEntropy_Calc(targetOut, outLayer),
      };
      }
      public static void Diff(Type type, float[] targetOut, Layer outLayer)
      {
      switch(type)
      {
      case Type.MeanSqurad:
      MeanSquradErr_Diff(targetOut, outLayer);
      break;
      case Type.CrossEntropy:
      CrossEntropy_Diff(targetOut, outLayer);
      break;
      };
      }

      private static float MeanSquradErr_Calc(float[] targetOut, Layer outLayer)
      {
      var errSum = 0.0f;
      for(int i = 0; i < targetOut.Length; ++i)
      {
      errSum += MathF.Pow(outLayer.Output[i] - targetOut[i], 2);
      }
      return errSum / (2 * targetOut.Length);
      }
      private static void MeanSquradErr_Diff(float[] targetOut, Layer outLayer)
      {
      for(int i = 0; i < targetOut.Length; ++i)
      {
      var curNeuron = outLayer.Neurons[i];
      curNeuron.Params["Error"] = outLayer.Output[i] - targetOut[i];
      }
      }

      private static float CrossEntropy_Calc(float[] targetOut, Layer outLayer)
      {
      var errSum = 0.0f;
      for(int i = 0; i < targetOut.Length; ++i)
      {
      //加上一個(gè)極小值再取log,放置出現(xiàn)log(0)報(bào)錯(cuò)
      errSum -= targetOut[i] * MathF.Log(outLayer.Output[i] + MinDelta);
      }
      return errSum;
      }
      private static void CrossEntropy_Diff(float[] targetOut, Layer outLayer)
      {
      for(int i = 0; i < targetOut.Length; ++i)
      {
      var curNeuron = outLayer.Neurons[i];
      //用Output[i]的前提:神經(jīng)網(wǎng)絡(luò)的輸出經(jīng)過了softmax處理
      curNeuron.Params["Error"] = outLayer.Output[i] - targetOut[i];
      }
      }
      }
      }

      2. 感知機(jī)(神經(jīng)元)


      簡(jiǎn)單地對(duì)神經(jīng)元結(jié)構(gòu)進(jìn)行實(shí)現(xiàn),只是神經(jīng)元在訓(xùn)練時(shí),需要為自身或者自身的權(quán)重記錄一些額外信息,所以多了WeightParams和Params備以記錄。

      • 為什么不記錄激活函數(shù)的計(jì)算結(jié)果out?

      因?yàn)樵谟?xùn)練過程中常常要以層為單位統(tǒng)一處理激活函數(shù)的計(jì)算結(jié)果,故而將out都記錄在層中了。(實(shí)際上Python中許多深度學(xué)習(xí)框架庫(kù)都是以層為最小單位構(gòu)建神經(jīng)網(wǎng)絡(luò)的,這有利于進(jìn)行矩陣運(yùn)算,但在我們的實(shí)現(xiàn)中,一來(lái)沒用到矩陣運(yùn)算,二來(lái)是希望能讓大家更直接地看到神經(jīng)網(wǎng)絡(luò)訓(xùn)練、計(jì)算的細(xì)節(jié),所以我們以單個(gè)神經(jīng)元作為最小的單位。)

      • 為什么沒有激活函數(shù)f(x)?

      因?yàn)槲覀冎罢f(shuō)過,神經(jīng)網(wǎng)絡(luò)的隱藏層都是使用同一種激活函數(shù),頂多輸出層用的不太一樣。也就是說(shuō)我們只需要記錄兩個(gè)函數(shù)的類型,所以讓后續(xù)實(shí)現(xiàn)的神經(jīng)網(wǎng)絡(luò)類記下就行了,沒必要每個(gè)神經(jīng)元都記錄,浪費(fèi)空間。

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


      namespaceJufGame.AI.ANN
      {
      [Serializable] // 方便在編輯器頁(yè)面查看
      publicclassNeuron
      {
      //神經(jīng)元權(quán)重列表,末位放置偏置b
      publicfloat[] Weights => weights;
      //加權(quán)和
      publicfloat Sum => sum;
      //為各個(gè)權(quán)重分配的額外參數(shù)
      public Dictionary WeightParams{ get; privateset; }
      //為神經(jīng)元本身分配的額外參數(shù)
      public Dictionary Params{ get; privateset; }
      [SerializeField]privatefloat[] weights;
      privatefloat sum;
      public Neuron(int weightCount)
      {
      weights = newfloat[weightCount + 1];//末尾放偏置
      }
      ///
      /// 初始化訓(xùn)練所需參數(shù)列表,僅在訓(xùn)練時(shí)調(diào)用
      ///
      public void InitCache()
      {
      Params = new Dictionary
      {
      ["Error"] = 0,//該值用來(lái)記錄,每次更新時(shí)的累計(jì)損失
      };
      WeightParams = new Dictionary
      {
      //記錄權(quán)重待變化值
      ["Delta"] = newfloat[weights.Length],
      //Momentum和Adam中,用于記錄權(quán)重變化的「動(dòng)量」
      ["m"] = newfloat[weights.Length],
      //AdaGrad和Adam中,用于記錄權(quán)重獨(dú)立學(xué)習(xí)率
      ["v"] = newfloat[weights.Length],
      };
      }
      //計(jì)算Sum
      public float CalcSum(float[] input)
      {
      int i;
      sum = 0;
      for(i = 0; i < input.Length; ++i)
      {
      sum += weights[i] * input[i];//加權(quán)和
      }
      sum += weights[i];//加上權(quán)重
      return Sum;
      }
      }
      }

      3. 層

      沒有太多必要說(shuō)的,就是嵌套調(diào)用了包含的各神經(jīng)元的函數(shù),比如層的計(jì)算就是各個(gè)神經(jīng)元的計(jì)算,其它同理。

                                                                 using System;
      using UnityEngine;


      namespaceJufGame.AI.ANN
      {
      [Serializable]
      publicclassLayer
      {
      public Neuron[] Neurons => neurons;//存儲(chǔ)神經(jīng)元
      publicfloat[] Output => output;//存儲(chǔ)各神經(jīng)元激活函數(shù)輸出
      [SerializeField] private Neuron[] neurons;
      [SerializeField] privatefloat[] output;
      public Layer(int neuronCount)
      {
      output = newfloat[neuronCount];
      neurons = new Neuron[neuronCount];
      }
      //對(duì)層中的每個(gè)神經(jīng)元的權(quán)重進(jìn)行初始化
      public void InitWeights(int weightCount, InitWFunc.Type initType)
      {
      for(int i = 0; i < neurons.Length; ++i)
      {
      neurons[i] = new Neuron(weightCount);
      InitWFunc.InitWeights(initType, neurons[i]);
      }
      }
      //初始化層中每個(gè)神經(jīng)元的額外參數(shù)
      public void InitCache()
      {
      for(int i = 0; i < neurons.Length; ++i)
      {
      neurons[i].InitCache();
      }
      }
      //計(jì)算該層,實(shí)際上就是計(jì)算所有神經(jīng)元的加權(quán)和,并求出激活函數(shù)的輸出
      public float[] CalcLayer(float[] inputData, ActivationFunc.Type acFuc)
      {
      for(int i = 0; i < neurons.Length; ++i)
      {
      neurons[i].CalcSum(inputData);
      }
      ActivationFunc.Calc(acFuc, this);
      return output;
      }
      }
      }

      4. 多層感知機(jī)(神經(jīng)網(wǎng)絡(luò))

      神經(jīng)網(wǎng)絡(luò)也一樣,是對(duì)層的各個(gè)功能的再度包裝,只是多了些超參數(shù)成員變量。

      • 為什么沒有輸入層?

      因?yàn)檩斎雽悠鋵?shí)就只是輸入的數(shù)值,沒必要單獨(dú)設(shè)置一個(gè)層(Python中許多深度學(xué)習(xí)框架也是這樣的)。

      • 怎么讀取輸出?

      直接讀取輸出層的輸出列表即可。

                                                                 using System;
      using UnityEngine;

      namespaceJufGame.AI.ANN
      {
      [Serializable]
      publicclassNeuralNet
      {
      publicfloat TargetError = 0.0001f;//預(yù)期誤差,當(dāng)損失函數(shù)的結(jié)果小于它時(shí),就停止訓(xùn)練
      publicfloat LearningRate = 0.01f;//學(xué)習(xí)率
      publicint CurEpochs;//記錄當(dāng)前迭代的次數(shù)
      public ActivationFunc.Type hdnAcFunc;//隱藏層激活函數(shù)類型
      public ActivationFunc.Type outAcFunc;//輸出層激活函數(shù)類型
      public Layer[] HdnLayers => hdnLayers;//隱藏層
      public Layer OutLayer => outLayer;//輸出層
      [SerializeField] private Layer[] hdnLayers;
      [SerializeField] private Layer outLayer;

      public NeuralNet(int hdnLayerCount, int[] neuronsOfLayers, int outCount,
      ActivationFunc.Type hdnAcFnc, ActivationFunc.Type outAcFnc,
      float targetError = 0.0001f, float learningRate = 0.01f)
      {
      outLayer = new Layer(outCount);
      hdnLayers = new Layer[hdnLayerCount];
      for(int i = 0, j = 0; i < hdnLayerCount; ++i)
      {
      hdnLayers[i] = new Layer(neuronsOfLayers[j]);
      }
      hdnAcFunc = hdnAcFnc;
      outAcFunc = outAcFnc;
      TargetError = targetError;
      LearningRate = learningRate;
      }
      //初始化各神經(jīng)元權(quán)重
      public void InitWeights(int inputDataCount, InitWFunc.Type initType)
      {
      int neuronNum = inputDataCount;
      for(int i = 0; i < HdnLayers.Length; ++i)
      {
      hdnLayers[i].InitWeights(neuronNum, initType);
      neuronNum = HdnLayers[i].Neurons.Length;
      }
      outLayer.InitWeights(neuronNum, initType);
      }
      //初始化各神經(jīng)元額外參數(shù)列表
      public void InitCache()
      {
      for(int i = 0; i < HdnLayers.Length; ++i)
      {
      hdnLayers[i].InitCache();
      }
      outLayer.InitCache();
      }
      //計(jì)算神經(jīng)網(wǎng)絡(luò)
      public float[] CalcNet(float[] inputData)
      {
      var curInput = inputData;
      for(int j = 0; j < hdnLayers.Length; ++j)
      {
      curInput = hdnLayers[j].CalcLayer(curInput, hdnAcFunc);
      }
      return outLayer.CalcLayer(curInput, outAcFunc);
      }
      }
      }

      至此,神經(jīng)網(wǎng)絡(luò)就搭建完成了,并沒有想象的那么復(fù)雜。

      5. 訓(xùn)練器

      先實(shí)現(xiàn)一個(gè)訓(xùn)練器的基類……等等,明明就一種神經(jīng)網(wǎng)絡(luò),為什么還要有基類,直接寫不好嗎?

      其實(shí)最初是打算實(shí)現(xiàn)多種神經(jīng)網(wǎng)絡(luò)的,但后來(lái)考試臨近,不得不轉(zhuǎn)移重心,最終只實(shí)現(xiàn)了最簡(jiǎn)單的MLP。Unity本身也已經(jīng)可以導(dǎo)入ONNX模型,如果要在游戲里想實(shí)現(xiàn)圖像識(shí)別這類復(fù)雜功能的話,導(dǎo)入模型似乎更方便,所以實(shí)現(xiàn)更多神經(jīng)網(wǎng)絡(luò)的必要性就值得考慮了。當(dāng)然,這些文末會(huì)再進(jìn)行討論。

      先來(lái)看看這個(gè)基類有哪些東西:

                                                                 using UnityEngine;

      namespaceJufGame.AI.ANN
      {
      publicabstractclassTraining
      {
      public NeuralNet TrainingNet;//需要訓(xùn)練的神經(jīng)網(wǎng)絡(luò)
      publicfloat[][] InputSet => inputSet;//訓(xùn)練輸入集

      /*沒有「訓(xùn)練輸出集」是因?yàn)椴⒎撬蓄愋偷纳窠?jīng)網(wǎng)絡(luò)都需要「訓(xùn)練輸出」
      所以它不是基類必需的,當(dāng)然,這些就是題外話了*/

      protectedfloat[][] inputSet;
      [SerializeField] protectedint maxEpochs;//最大迭代次數(shù)

      public Training(NeuralNet initedNet, int maxEpochs)
      {
      this.maxEpochs = maxEpochs;
      TrainingNet = initedNet;
      }

      public void SetInput(float[][] inputSet)//設(shè)置訓(xùn)練輸入集
      {
      this.inputSet = inputSet;
      }
      public abstract bool IsTrainEnd();//是否訓(xùn)練完成
      public abstract void Train(); //不斷訓(xùn)練神經(jīng)網(wǎng)絡(luò)
      public abstract void Train_OneTime();//訓(xùn)練(迭代)一次神經(jīng)網(wǎng)絡(luò)

      //打印神經(jīng)網(wǎng)絡(luò)輸出的結(jié)果,調(diào)試用的
      public static void DebugNetRes(NeuralNet net, float[][] testInput)
      {
      for(int i = 0; i < testInput.GetLength(0); ++i)
      {
      var res = net.CalcNet(testInput[i]);
      for(int j = 0; j < res.Length; ++j)
      {
      Debug.Log("檢驗(yàn)結(jié)果 " + i + " = " + res[j]);
      }
      }
      }
      }
      }

      最后,就是真正用來(lái)訓(xùn)練的類了,我們將采用最常見梯度下降法進(jìn)行訓(xùn)練。其中涉及前向傳播和反向傳播,我稍作解釋:

      • 前向傳播(Forward Propagation):傳入訓(xùn)練輸入樣本計(jì)算出當(dāng)前神經(jīng)網(wǎng)絡(luò)模型的輸出,并進(jìn)一步計(jì)算損失(損失函數(shù)的計(jì)算結(jié)果其實(shí)在反向傳播中并沒有用,只是給開發(fā)者看的,用來(lái)判斷當(dāng)前訓(xùn)練情況)。

      • 反向傳播(Backward Propagation):從損失函數(shù)開始,用鏈?zhǔn)角髮?dǎo)法則,反向(輸出層?隱藏層?輸入層)計(jì)算每個(gè)神經(jīng)元的損失(下圖中的δ、代碼中的Params["Error"])。通過神經(jīng)元的損失,可以計(jì)算出神經(jīng)元的每個(gè)參數(shù)(權(quán)重、偏置)的損失貢獻(xiàn)(權(quán)重更新函數(shù)代碼中的WeightParams["Delta"])并一直累加,直到訓(xùn)練集被讀取完。這時(shí),我們就說(shuō)完成了一次訓(xùn)練迭代


      完成一次迭代后(不是訓(xùn)練完一個(gè)樣本后),將累加的損失除以訓(xùn)練樣本數(shù)取得均值,再用權(quán)重更新函數(shù)對(duì)各參數(shù)進(jìn)行更新。

      這一迭代過程反復(fù)進(jìn)行,直到損失函數(shù)計(jì)算的誤差達(dá)到可容許范圍(也就是小于預(yù)期損失)或達(dá)到最大訓(xùn)練次數(shù),詳情可看《解讀反向傳播算法(圖與公式結(jié)合)》[1](但注意!該文章為了方便講解,訓(xùn)練完一個(gè)樣本就開始更新權(quán)重了),實(shí)現(xiàn)如下:

                                                                 using UnityEngine;


      namespaceJufGame.AI.ANN
      {
      [System.Serializable]
      publicclassBPNN : Training
      {
      publicfloat[][] OutputSet => outputSet;
      [SerializeField] privatefloat meanError = float.MaxValue;
      [SerializeField] private LossFunc.Type errorFunc;
      [SerializeField] private UpdateWFunc.Type updateWFunc;
      privatefloat[][] outputSet;
      public BPNN(NeuralNet initedNet, LossFunc.Type errorFunc, UpdateWFunc.Type updateWFunc, int maxEpochs): base(initedNet,maxEpochs)
      {
      this.errorFunc = errorFunc;
      this.updateWFunc = updateWFunc;
      }
      public void SetOutput(float[][] outputSet)
      {
      this.outputSet = outputSet;
      }
      public override bool IsTrainEnd()//判斷是否訓(xùn)練完成
      {
      return meanError < TrainingNet.TargetError
      || maxEpochs < TrainingNet.CurEpochs;
      }
      public override void Train()
      {
      meanError = float.MaxValue;
      while(!IsTrainEnd())
      {
      Train_OneTime();
      }
      }
      public override void Train_OneTime()
      {
      int samplesCount = inputSet.GetLength(0);//記下樣本數(shù)量
      ++TrainingNet.CurEpochs;//更新迭代次數(shù)
      meanError = 0;
      for(int i = 0; i < samplesCount; ++i)
      {
      ForWard(i);
      Backpropagation();
      UpdateWFunc.CalcDelta(TrainingNet, inputSet[i]);
      }
      UpdateWFunc.UpdateNetWeights( updateWFunc, TrainingNet, samplesCount);
      meanError /= samplesCount;//取樣本誤差均值作為本次迭代的誤差
      #if UNITY_EDITOR
      Debug.Log($"誤差:{meanError}");//調(diào)試時(shí)用的
      #endif
      }
      private void ForWard(int trainIndex)
      {
      var outLayer = TrainingNet.OutLayer;
      TrainingNet.CalcNet(inputSet[trainIndex]);
      meanError = LossFunc.Calc(errorFunc, outputSet[trainIndex], outLayer);
      /*這里圖省事,將反向傳播的第一步一并計(jì)算了*/
      LossFunc.Diff(errorFunc, outputSet[trainIndex], outLayer);//損失函數(shù)求導(dǎo)
      for(int i = 0; i < outLayer.Neurons.Length; ++i)//輸出層激活函數(shù)求導(dǎo)
      {
      outLayer.Neurons[i].Params["Error"] *= ActivationFunc.Diff(TrainingNet.outAcFunc, outLayer, i);
      }
      }
      private void Backpropagation()
      {
      var lastLayer = TrainingNet.OutLayer;
      for(int i = TrainingNet.HdnLayers.Length - 1; i > -1; --i)
      {
      var curLayer = TrainingNet.HdnLayers[i];
      for(int j = 0; j < curLayer.Neurons.Length; ++j)
      {
      var curNeuron = curLayer.Neurons[j];
      //每次計(jì)算損失時(shí)要清零,避免上次迭代結(jié)果產(chǎn)生的干擾
      curNeuron.Params["Error"] = 0;
      for(int k = 0; k < lastLayer.Neurons.Length; ++k)
      {
      var lastNeuron = lastLayer.Neurons[k];
      curNeuron.Params["Error"] += lastNeuron.Params["Error"] * lastNeuron.Weights[j];
      }
      curNeuron.Params["Error"] *= ActivationFunc.Diff(TrainingNet.hdnAcFunc, curLayer, j);
      }
      lastLayer = curLayer;
      }
      }
      }
      }

      四、使用教程

      一切都準(zhǔn)備就緒了,那要怎么運(yùn)轉(zhuǎn)這個(gè)神經(jīng)網(wǎng)絡(luò)呢?我們創(chuàng)建一個(gè)繼承了MonoBehavior的腳本,并聲明下面三個(gè)公開的字段:

                                                                 using UnityEngine;
      using JufGame.AI.ANN;


      public class TrainANN : MonoBehaviour
      {
      public int inputCount;
      public BPNN bp;
      public InitWFunc.Type initW;
      }

      將它掛載在場(chǎng)景的任一物體上,不出意外的話,你可以在編輯器看到神經(jīng)網(wǎng)絡(luò)類的許多關(guān)鍵變量都可以顯示出來(lái)(如果你的沒有,就要注意是否遺漏[System.Serializable]或設(shè)置成了私有類):


      我們?cè)偻晟葡履_本,設(shè)置好訓(xùn)練輸入和輸出(以「異或」運(yùn)算為例),使得神經(jīng)網(wǎng)絡(luò)能在Unity運(yùn)行時(shí)逐幀訓(xùn)練:

                                                                 public classTrainANN : MonoBehaviour
      {
      publicint inputCount;
      public BPNN bp;
      public InitWFunc.Type initW;

      privatefloat[][] inSet = //異或運(yùn)算的輸入
      {
      newfloat[]{1, 0},
      newfloat[]{1, 1},
      newfloat[]{0, 0},
      newfloat[]{0, 1},
      };
      privatefloat[][] outSet = //異或運(yùn)算的輸出
      {
      newfloat[]{1},
      newfloat[]{0},
      newfloat[]{0},
      newfloat[]{1},
      };

      private void Awake()
      {
      ...

      特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺(tái)“網(wǎng)易號(hào)”用戶上傳并發(fā)布,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。

      Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

      相關(guān)推薦
      熱點(diǎn)推薦
      闞清子生子引發(fā)爭(zhēng)議,爆料人稱寶寶出生多處畸形,已救治無(wú)效去世

      闞清子生子引發(fā)爭(zhēng)議,爆料人稱寶寶出生多處畸形,已救治無(wú)效去世

      有范又有料
      2025-12-24 09:35:27
      “成人奧斯卡”,丁度·巴拉斯實(shí)至名歸,細(xì)膩情感藝術(shù),絕了

      “成人奧斯卡”,丁度·巴拉斯實(shí)至名歸,細(xì)膩情感藝術(shù),絕了

      棱鏡電影
      2025-12-17 20:45:26
      2026至2035年經(jīng)濟(jì)增速要超過3%,須重構(gòu)收入與資源分配制度

      2026至2035年經(jīng)濟(jì)增速要超過3%,須重構(gòu)收入與資源分配制度

      火星宏觀
      2025-12-24 15:38:24
      鄭麗文韓國(guó)瑜聯(lián)手清黨渣,侯友宜盧秀燕罕見求和,國(guó)民黨或?qū)⒎P

      鄭麗文韓國(guó)瑜聯(lián)手清黨渣,侯友宜盧秀燕罕見求和,國(guó)民黨或?qū)⒎P

      書紀(jì)文譚
      2025-12-24 15:34:03
      大難已過!2026年未來(lái)2個(gè)月,這3生肖賺得第一桶金,事業(yè)亨通

      大難已過!2026年未來(lái)2個(gè)月,這3生肖賺得第一桶金,事業(yè)亨通

      人閒情事
      2025-12-24 11:24:29
      導(dǎo)演翟俊杰去世

      導(dǎo)演翟俊杰去世

      新京報(bào)
      2025-12-24 16:04:03
      AI龍頭,年內(nèi)32次歷史新高!英偉達(dá)核心供應(yīng)商曝光

      AI龍頭,年內(nèi)32次歷史新高!英偉達(dá)核心供應(yīng)商曝光

      數(shù)據(jù)寶
      2025-12-24 12:26:34
      美團(tuán)拼好飯與漢堡王合作定制早餐,價(jià)格戰(zhàn)之后加碼改造供應(yīng)鏈

      美團(tuán)拼好飯與漢堡王合作定制早餐,價(jià)格戰(zhàn)之后加碼改造供應(yīng)鏈

      南方都市報(bào)
      2025-12-24 22:49:10
      廣東女護(hù)士林楚欣,因淤青確診癌癥,年僅18歲,兩個(gè)月共花費(fèi)13萬(wàn)

      廣東女護(hù)士林楚欣,因淤青確診癌癥,年僅18歲,兩個(gè)月共花費(fèi)13萬(wàn)

      溫辭韞
      2025-12-23 10:42:08
      我們已經(jīng)沒有退路了,如果中國(guó)再次衰落,歐美絕不會(huì)再給崛起機(jī)會(huì)

      我們已經(jīng)沒有退路了,如果中國(guó)再次衰落,歐美絕不會(huì)再給崛起機(jī)會(huì)

      扶蘇聊歷史
      2025-11-14 15:33:48
      造出EUV光刻機(jī)?中國(guó)如何突破

      造出EUV光刻機(jī)?中國(guó)如何突破

      南風(fēng)窗
      2025-12-24 13:29:46
      懵了!羅永浩還沒公布錄音,華與華要把公司賣了

      懵了!羅永浩還沒公布錄音,華與華要把公司賣了

      說(shuō)財(cái)貓
      2025-12-24 21:07:22
      “搶劫殺害發(fā)小一家三口”案兇手獲死刑 兇手父親:把他埋到地下便不能作惡

      “搶劫殺害發(fā)小一家三口”案兇手獲死刑 兇手父親:把他埋到地下便不能作惡

      上游新聞
      2025-12-23 21:58:08
      冷空氣“攜風(fēng)帶雨”!12小時(shí)內(nèi)“殺”到深圳!濕冷即將“到貨”!

      冷空氣“攜風(fēng)帶雨”!12小時(shí)內(nèi)“殺”到深圳!濕冷即將“到貨”!

      深圳本地寶
      2025-12-24 22:37:03
      有一種“報(bào)復(fù)”,叫22年后,在張國(guó)立面前領(lǐng)獎(jiǎng)

      有一種“報(bào)復(fù)”,叫22年后,在張國(guó)立面前領(lǐng)獎(jiǎng)

      娛小北
      2025-12-23 18:52:31
      最新 | 天津市委、市政府決定!名單發(fā)布!

      最新 | 天津市委、市政府決定!名單發(fā)布!

      天津廣播
      2025-12-24 09:54:15
      紅軍長(zhǎng)征路上吃什么?并非相傳的草根樹皮,其實(shí)非常“豐富”

      紅軍長(zhǎng)征路上吃什么?并非相傳的草根樹皮,其實(shí)非常“豐富”

      鶴羽說(shuō)個(gè)事
      2025-12-23 11:38:31
      12月25日精選熱點(diǎn):中芯國(guó)際漲價(jià)開啟,這些供應(yīng)商受益最大

      12月25日精選熱點(diǎn):中芯國(guó)際漲價(jià)開啟,這些供應(yīng)商受益最大

      元芳說(shuō)投資
      2025-12-24 21:04:47
      為何很多女性如此渴望性生活?無(wú)非是這4個(gè)原因,男性也無(wú)需害怕

      為何很多女性如此渴望性生活?無(wú)非是這4個(gè)原因,男性也無(wú)需害怕

      特約前排觀眾
      2025-07-02 07:18:22
      德拉富恩特:西班牙有很多優(yōu)秀的門將,現(xiàn)在無(wú)法預(yù)料會(huì)征召誰(shuí)

      德拉富恩特:西班牙有很多優(yōu)秀的門將,現(xiàn)在無(wú)法預(yù)料會(huì)征召誰(shuí)

      懂球帝
      2025-12-24 04:54:50
      2025-12-25 01:32:49
      侑虎科技UWA incentive-icons
      侑虎科技UWA
      游戲/VR性能優(yōu)化平臺(tái)
      1532文章數(shù) 986關(guān)注度
      往期回顧 全部

      科技要聞

      智譜和MiniMax拿出了“血淋淋”的賬本

      頭條要聞

      15歲女孩遭同班14歲男生殺害:對(duì)方曾拍攝其胸部等照片

      頭條要聞

      15歲女孩遭同班14歲男生殺害:對(duì)方曾拍攝其胸部等照片

      體育要聞

      26歲廣西球王,在質(zhì)疑聲中成為本土得分王

      娛樂要聞

      懷孕增重30斤!闞清子驚傳誕一女夭折?

      財(cái)經(jīng)要聞

      北京進(jìn)一步放松限購(gòu) 滬深是否會(huì)跟進(jìn)?

      汽車要聞

      “運(yùn)動(dòng)版庫(kù)里南”一月份亮相???或命名極氪9S

      態(tài)度原創(chuàng)

      游戲
      旅游
      教育
      時(shí)尚
      本地

      JUG與TE溯共寫“無(wú)畏之約”,ANTGAMER冠軍訓(xùn)練營(yíng)圓滿收官

      旅游要聞

      寶山文旅精彩亮相2025中國(guó)旅交會(huì)!

      教育要聞

      英語(yǔ)口語(yǔ)邪修方法!

      對(duì)不起周柯宇,是陳靖可先來(lái)的

      本地新聞

      云游安徽|一川江水潤(rùn)安慶,一塔一戲一城史

      無(wú)障礙瀏覽 進(jìn)入關(guān)懷版 主站蜘蛛池模板: 国产麻花豆剧传媒精品mv在线| 麻豆色漫| 本溪市| 大地资源网中文第五页| 无套内射蜜桃小视频| 免费观看日本污污ww网站69| 性中国熟妇| 欧美freesex黑人又粗又大 | 天天爱天天躁XXXXAAAA| 免费观看添你到高潮视频| 久久综合给合久久狠狠97色 | 日本一卡2卡3卡4卡无卡免费| 亚洲多毛视频| 苍井空浴缸大战猛男120分钟| 免费人成视频19674不收费| 国产黄色一区二区三区四区 | 国产综合久久久久鬼色| 欧美一区二区日韩国产| 亚洲国产精品综合久久20| 国产拳交视频| 国产在线精品一区二区三区| 日本熟妇色xxxxx欧美老妇| 国产成人女人在线观看| 永德县| 国产成人精品日本亚洲| 消息称老熟妇乱视频一区二区| 91在线视频播放| 山东省| 亚洲国产精品自产在线播放| 亚洲AV无码久久久久网站蜜桃| 99中文在线精品| 亚洲无码200p| 婷婷四房播播| 国产网友自拍| 亚洲日产专区| 免费拍拍拍网站| 国产精品原创不卡在线| 迅雷AV| 中文字幕无码人妻| 天天干-天天日| 人妻久久Aⅴ|