深度學習概述:從感知機到深度網絡

近些年來,人工智能領域又活躍起來,除了傳統了學術圈外,Google、Microsoft、facebook等工業界優秀企業也紛紛成立相關研究團隊,並取得了不少使人矚目的成果。這要歸功於社交網絡用戶產生的大量數據,這些數據大都是原始數據,須要被進一步分析處理;還要歸功於廉價而又強大的計算資源的出現,好比GPGPU的快速發展。算法

除去這些因素,AI尤爲是機器學習領域出現的一股新潮流很大程度上推進了此次復興——深度學習。本文中我將介紹深度學習背後的關鍵概念及算法,從最簡單的元素開始並以此爲基礎進行下一步構建。api

機器學習基礎數組

若是你不太熟悉相關知識,一般的機器學習過程以下:網絡

一、機器學習算法須要輸入少許標記好的樣本,好比10張小狗的照片,其中1張標記爲1(意爲狗)其它的標記爲0(意爲不是狗)——本文主要使用監督式、二叉分類。數據結構

二、這些算法「學習」怎麼樣正確將狗的圖片分類,而後再輸入一個新的圖片時,能夠指望算法輸出正確的圖片標記(如輸入一張小狗圖片,輸出1;不然輸出0)。架構

這一般是難以置信的:你的數據多是模糊的,標記也可能出錯;或者你的數據是手寫字母的圖片,用其實際表示的字母來標記它。框架

感知機機器學習

感知機是最先的監督式訓練算法,是神經網絡構建的基礎。分佈式

假如平面中存在 個點,並被分別標記爲「0」和「1」。此時加入一個新的點,若是咱們想知道這個點的標記是什麼(和以前提到的小狗圖片的辨別同理),咱們要怎麼作呢?函數

一種很簡單的方法是查找離這個點最近的點是什麼,而後返回和這個點同樣的標記。而一種稍微「智能」的辦法則是去找出平面上的一條線來將不一樣標記的數據點分開,並用這條線做爲「分類器」來區分新數據點的標記。

在本例中,每個輸入數據均可以表示爲一個向量 x = (x_1, x_2) ,而咱們的函數則是要實現「若是線如下,輸出0;線以上,輸出1」。

用數學方法表示,定義一個表示權重的向量 w 和一個垂直偏移量 b。而後,咱們將輸入、權重和偏移結合能夠獲得以下傳遞函數:

這個傳遞函數的結果將被輸入到一個激活函數中以產生標記。在上面的例子中,咱們的激活函數是一個門限截止函數(即大於某個閾值後輸出1):

訓練

感知機的訓練包括多訓練樣本的輸入及計算每一個樣本的輸出。在每一次計算之後,權重 w 都要調整以最小化輸出偏差,這個偏差由輸入樣本的標記值與實際計算得出值的差得出。還有其它的偏差計算方法,如均方差等,但基本的原則是同樣的。

缺陷

這種簡單的感知機有一個明顯缺陷:只能學習線性可分函數。這個缺陷重要嗎?好比 XOR,這麼簡單的函數,都不能被線性分類器分類(以下圖所示,分隔兩類點失敗):

爲了解決這個問題,咱們要使用一種多層感知機,也就是——前饋神經網絡:事實上,咱們將要組合一羣這樣的感知機來建立出一個更強大的學習機器。

前饋神經網絡

神經網絡實際上就是將大量以前講到的感知機進行組合,用不一樣的方法進行鏈接並做用在不一樣的激活函數上。

咱們簡單介紹下前向神經網絡,其具備如下屬性:

一個輸入層,一個輸出層,一個或多個隱含層。上圖所示的神經網絡中有一個三神經元的輸入層、一個四神經元的隱含層、一個二神經元的輸出層。

每個神經元都是一個上文提到的感知機。

輸入層的神經元做爲隱含層的輸入,同時隱含層的神經元也是輸出層神經元的輸入。

每條創建在神經元之間的鏈接都有一個權重 w (與感知機中提到的權重相似)。

在 t 層的每一個神經元一般與前一層( t - 1層)中的每一個神經元都有鏈接(但你能夠經過將這條鏈接的權重設爲0來斷開這條鏈接)。

爲了處理輸入數據,將輸入向量賦到輸入層中。在上例中,這個網絡能夠計算一個3維輸入向量(因爲只有3個輸入層神經元)。假如輸入向量是 [7, 1, 2],你將第一個輸入神經元輸入7,中間的輸入1,第三個輸入2。這些值將被傳播到隱含層,經過加權傳遞函數傳給每個隱含層神經元(這就是前向傳播),隱含層神經元再計算輸出(激活函數)。

輸出層和隱含層同樣進行計算,輸出層的計算結果就是整個神經網絡的輸出。

超線性

若是每個感知機都只能使用一個線性激活函數會怎麼樣?整個網絡的最終輸出也仍然是將輸入數據經過一些線性函數計算過一遍,只是用一些在網絡中收集的不一樣權值調整了一下。換名話說,再多線性函數的組合仍是線性函數。若是咱們限定只能使用線性激活函數的話,前饋神經網絡其實比一個感知機強大不到哪裏去,不管網絡有多少層。

正是這個緣由,大多數神經網絡都是使用的非線性激活函數,如對數函數、雙曲正切函數、階躍函數、整流函數等。不用這些非線性函數的神經網絡只能學習輸入數據的線性組合。

訓練

大多數常見的應用在多層感知機的監督式訓練的算法都是反向傳播算法。基本的流程以下:

一、將訓練樣本經過神經網絡進行前向傳播計算。

二、計算輸出偏差,經常使用均方差:

其中 t 是目標值, y 是實際的神經網絡計算輸出。其它的偏差計算方法也能夠,但MSE(均方差)一般是一種較好的選擇。

三、網絡偏差經過隨機梯度降低的方法來最小化。

梯度降低很經常使用,但在神經網絡中,輸入參數是一個訓練偏差的曲線。每一個權重的最佳值應該是偏差曲線中的全局最小值(上圖中的 global minimum)。在訓練過程當中,權重以很是小的步幅改變(在每一個樣本或每小組樣本訓練完成後)以找到全局最小值,但這可不容易,訓練一般會結束在局部最小值上(上圖中的local minima)。如例子中的,若是當前權重值爲0.6,那麼要向0.4方向移動。

這個圖表示的是最簡單的狀況,偏差只依賴於單個參數。可是,網絡偏差依賴於每個網絡權重,偏差函數很是、很是複雜。

好消息是反向傳播算法提供了一種經過利用輸出偏差來修正兩個神經元之間權重的方法。關係自己十分複雜,但對於一個給定結點的權重修正按以下方法(簡單):

其中 E 是輸出偏差, w_i 是輸入 i 的權重。

實質上這麼作的目的是利用權重 來修正梯度的方向。關鍵的地方在於偏差的導數的使用,這可不必定好計算:你怎麼樣能給一個大型網絡中隨機一個結點中的隨機一個權重求導數呢?

答案是:經過反向傳播。偏差的首次計算很簡單(只要對預期值和實際值作差便可),而後經過一種巧妙的方法反向傳回網絡,讓咱們有效的在訓練過程當中修正權重並(指望)達到一個最小值。

隱含層

隱含層十分有趣。根據普適逼近原理,一個具備有限數目神經元的隱含層能夠被訓練成可逼近任意隨機函數。換句話說,一層隱含層就強大到能夠學習任何函數了。這說明咱們在多隱含層(如深度網絡)的實踐中能夠獲得更好的結果。

隱含層存儲了訓練數據的內在抽象表示,和人類大腦(簡化的類比)保存有對真實世界的抽象同樣。接下來,咱們將用各類方法來搞一下這個隱含層。

一個網絡的例子

能夠看一下這個經過 testMLPSigmoidBP 方法用Java實現的簡單(4-2-3)前饋神經網絡,它將 IRIS 數據集進行了分類。這個數據集中包含了三類鳶尾屬植物,特徵包括花萼長度,花瓣長度等等。每一類提供50個樣本給這個神經網絡訓練。特徵被賦給輸入神經元,每個輸出神經元表明一類數據集(「1/0/0」 表示這個植物是Setosa,「0/1/0」表示 Versicolour,而「0/0/1」表示 Virginica)。分類的錯誤率是2/150(即每分類150個,錯2個)。

大規模網絡中的難題

神經網絡中能夠有多個隱含層:這樣,在更高的隱含層裏能夠對其以前的隱含層構建新的抽象。並且像以前也提到的,這樣能夠更好的學習大規模網絡。增長隱含層的層數一般會致使兩個問題:

一、梯度消失:隨着咱們添加愈來愈多的隱含層,反向傳播傳遞給較低層的信息會愈來愈少。實際上,因爲信息向前反饋,不一樣層次間的梯度開始消失,對網絡中權重的影響也會變小。

二、過分擬合:也許這是機器學習的核心難題。簡要來講,過分擬合指的是對訓練數據有着過於好的識別效果,這時導至模型很是複雜。這樣的結果會致使對訓練數據有很是好的識別較果,而對真實樣本的識別效果很是差。

下面咱們來看看一些深度學習的算法是如何面對這些難題的。

自編碼器

大多數的機器學習入門課程都會讓你放棄前饋神經網絡。可是實際上這裏面大有可爲——請接着看。

自編碼器就是一個典型的前饋神經網絡,它的目標就是學習一種對數據集的壓縮且分佈式的表示方法(編碼思想)。

從概念上講,神經網絡的目的是要訓練去「從新創建」輸入數據,好像輸入和目標輸出數據是同樣的。換句話說:你正在讓神經網絡的輸出與輸入是同同樣東西,只是通過了壓縮。這仍是很差理解,先來看一個例子。

壓縮輸入數據:灰度圖像

這裏有一個由28x28像素的灰度圖像組成的訓練集,且每個像素的值都做爲一個輸入層神經元的輸入(這時輸入層就會有784個神經元)。輸出層神經元要有相同的數目(784),且每個輸出神經元的輸出值和輸入圖像的對應像素灰度值相同。

在這樣的算法架構背後,神經網絡學習到的實際上並非一個訓練數據到標記的「映射」,而是去學習數據自己的內在結構和特徵(也正是由於這,隱含層也被稱做特徵探測器(feature detector))。一般隱含層中的神經元數目要比輸入/輸入層的少,這是爲了使神經網絡只去學習最重要的特徵並實現特徵的降維。

咱們想在中間層用不多的結點去在概念層上學習數據、產生一個緊緻的表示方法。

流行感冒

爲了更好的描述自編碼器,再看一個應用。

此次咱們使用一個簡單的數據集,其中包括一些感冒的症狀。若是感興趣,這個例子的源碼發佈在這裏。

數據結構以下:

輸入數據一共六個二進制位

前三位是病的證狀。例如,1 0 0 0 0 0 表明病人發燒;0 1 0 0 0 0 表明咳嗽;1 1 0 0 0 0 表明即咳嗽又發燒等等。

後三位表示抵抗能力,若是一個病人有這個,表明他/她不太可能患此病。例如,0 0 0 1 0 0 表明病人接種過流感疫苗。一個可能的組合是:0 1 0 1 0 0 ,這表明着一個接種過流感疫苗的咳嗽病人,等等。

當一個病人同時擁用前三位中的兩位時,咱們認爲他生病了;若是至少擁用後三位中的兩位,那麼他是健康的,如:

111000, 101000, 110000, 011000, 011100 = 生病

000111, 001110, 000101, 000011, 000110 = 健康

咱們來訓練一個自編碼器(使用反向傳播),六個輸入、六個輸出神經元,而只有兩個隱含神經元。

在通過幾百次迭代之後,咱們發現,每當一個「生病」的樣本輸入時,兩個隱含層神經元中的一個(對於生病的樣本老是這個)老是顯示出更高的激活值。而若是輸入一個「健康」樣本時,另外一個隱含層則會顯示更高的激活值。

再看學習

本質上來講,這兩個隱含神經元從數據集中學習到了流感症狀的一種緊緻表示方法。爲了檢驗它是否是真的實現了學習,咱們再看下過分擬合的問題。經過訓練咱們的神經網絡學習到的是一個緊緻的簡單的,而不是一個高度複雜且對數據集過分擬合的表示方法。

某種程度上來說,與其說在找一種簡單的表示方法,咱們更是在嘗試從「感受」上去學習數據。

受限波爾茲曼機

下一步來看下受限波爾茲曼機(Restricted Boltzmann machines RBM),一種能夠在輸入數據集上學習機率分佈的生成隨機神經網絡。

RBM由隱含層、可見層、偏置層組成。和前饋神經網絡不一樣,可見層和隱含層之間的鏈接是無方向性(值能夠從可見層->隱含層或隱含層->可見層任意傳輸)且全鏈接的(每個當前層的神經元與下一層的每一個神經元都有鏈接——若是容許任意層的任意神經元鏈接到任意層去,咱們就獲得了一個波爾茲曼機(非受限的))。

標準的RBM中,隱含和可見層的神經元都是二態的(即神經元的激活值只能是服從伯努力分佈的0或1),不過也存在其它非線性的變種。

雖然學者們已經研究RBM很長時間了,最近出現的對比差別無監督訓練算法使這個領域復興。

對比差別

單步對比差別算法原理:

一、正向過程:

輸入樣本 v 輸入至輸入層中。

v 經過一種與前饋網絡類似的方法傳播到隱含層中,隱含層的激活值爲 h

二、反向過程:

將 h 傳回可見層獲得 v’ (可見層和隱含層的鏈接是無方向的,能夠這樣傳)。

再將 v’ 傳到隱含層中,獲得 h’

三、權重更新:

其中 a 是學習速率, vv’hh’ 和 都是向量。

算法的思想就是在正向過程當中影響了網絡的內部對於真實數據的表示。同時,反向過程當中嘗試經過這個被影響過的表示方法重建數據。主要目的是可使生成的數據與原數據儘量類似,這個差別影響了權重更新。

換句話說,這樣的網絡具備了感知對輸入數據表示的程度的能力,並且嘗試經過這個感知能力重建數據。若是重建出來的數據與原數據差別很大,那麼進行調整並再次重建。

再看流行感冒的例子

爲了說明對比差別,咱們使用與上例相同的流感症狀的數據集。測試網絡是一個包含6個可見層神經元、2個隱含層神經元的RBM。咱們用對比差別的方法對網絡進行訓練,將症狀 v 賦到可見層中。在測試中,這些症狀值被從新傳到可見層;而後再被傳到隱含層。隱含層的神經元表示健康/生病的狀態,與自編碼器類似。

在進行過幾百次迭代後,咱們獲得了與自編碼器相同的結果:輸入一個生病樣本,其中一個隱含層神經元具備更高激活值;輸入健康的樣本,則另外一個神經元更興奮。

例子的代碼在這裏。

深度網絡

到如今爲止,咱們已經學習了隱含層中強大的特徵探測器——自編碼器和RBM,但如今尚未辦法有效的去利用這些功能。實際上,上面所用到的這些數據集都是特定的。而咱們要找到一些方法來間接的使用這些探測出的特徵。

好消息是,已經發現這些結構能夠經過棧式疊加來實現深度網絡。這些網絡能夠經過貪心法的思想訓練,每次訓練一層,以克服以前提到在反向傳播中梯度消失及過分擬合的問題。

這樣的算法架構十分強大,能夠產生很好的結果。如Google著名的「貓」識別,在實驗中經過使用特定的深度自編碼器,在無標記的圖片庫中學習到人和貓臉的識別。

下面咱們將更深刻。

棧式自編碼器

和名字同樣,這種網絡由多個棧式結合的自編碼器組成。

自編碼器的隱含層 t 會做爲 t + 1 層的輸入層。第一個輸入層就是整個網絡的輸入層。利用貪心法訓練每一層的步驟以下:

一、經過反向傳播的方法利用全部數據對第一層的自編碼器進行訓練(t=1,上圖中的紅色鏈接部分)。

二、訓練第二層的自編碼器 t=2 (綠色鏈接部分)。因爲 t=2 的輸入層是 t=1 的隱含層,咱們已經再也不關心 t=1 的輸入層,能夠從整個網絡中移除。整個訓練開始於將輸入樣本數據賦到 t=1 的輸入層,經過前向傳播至 t = 2 的輸出層。下面t = 2的權重(輸入->隱含和隱含->輸出)使用反向傳播的方法進行更新。t = 2的層和 t=1 的層同樣,都要經過全部樣本的訓練。

三、對全部層重複步驟1-2(即移除前面自編碼器的輸出層,用另外一個自編碼器替代,再用反向傳播進行訓練)。

四、步驟1-3被稱爲預訓練,這將網絡裏的權重值初始化至一個合適的位置。可是經過這個訓練並無獲得一個輸入數據到輸出標記的映射。例如,一個網絡的目標是被訓練用來識別手寫數字,通過這樣的訓練後還不能將最後的特徵探測器的輸出(即隱含層中最後的自編碼器)對應到圖片的標記上去。這樣,一個一般的辦法是在網絡的最後一層(即藍色鏈接部分)後面再加一個或多個全鏈接層。整個網絡能夠被看做是一個多層的感知機,並使用反向傳播的方法進行訓練(這步也被稱爲微調)。

棧式自編碼器,提供了一種有效的預訓練方法來初始化網絡的權重,這樣你獲得了一個能夠用來訓練的複雜、多層的感知機。

深度信度網絡

和自編碼器同樣,我也能夠將波爾茲曼機進行棧式疊加來構建深度信度網絡(DBN)。

在本例中,隱含層 RBM 能夠看做是 RBM t+1 的可見層。第一個RBM的輸入層便是整個網絡的輸入層,層間貪心式的預訓練的工做模式以下:

1. 經過對比差別法對全部訓練樣本訓練第一個RBM t=1

2. 訓練第二個RBM t=1。因爲 t=2 的可見層是 t=1 的隱含層,訓練開始於將數據賦至 t=1 的可見層,經過前向傳播的方法傳至 t=1 的隱含層。而後做爲 t=2 的對比差別訓練的初始數據。

3. 對全部層重複前面的過程。

4. 和棧式自編碼器同樣,經過預訓練後,網絡能夠經過鏈接到一個或多個層間全鏈接的 RBM 隱含層進行擴展。這構成了一個能夠經過反向傳僠進行微調的多層感知機。

本過程和棧式自編碼器很類似,只是用RBM將自編碼器進行替換,並用對比差別算法將反向傳播進行替換。

(注: 例中的源碼能夠從 此處得到.)

卷積網絡

這個是本文最後一個軟件架構——卷積網絡,一類特殊的對圖像識別很是有效的前饋網絡。

在咱們深刻看實際的卷積網絡之臆,咱們先定義一個圖像濾波器,或者稱爲一個賦有相關權重的方陣。一個濾波器能夠應用到整個圖片上,一般能夠應用多個濾波器。好比,你能夠應用四個6x6的濾波器在一張圖片上。而後,輸出中座標(1,1)的像素值就是輸入圖像左上角一個6x6區域的加權和,其它像素也是如此。

有了上面的基礎,咱們來介紹定義出卷積網絡的屬性:

卷積層 對輸入數據應用若干濾波器。好比圖像的第一卷積層使用4個6x6濾波器。對圖像應用一個濾波器以後的獲得的結果被稱爲特徵圖譜(feature map, FM),特徵圖譜的數目和濾波器的數目相等。若是前驅層也是一個卷積層,那麼濾波器應用在FM上,至關於輸入一個FM,輸出另一個FM。從直覺上來說,若是將一個權重分佈到整個圖像上後,那麼這個特徵就和位置無關了,同時多個濾波器能夠分別探測出不一樣的特徵。

下采樣層 縮減輸入數據的規模。例如輸入一個32x32的圖像,而且經過一個2x2的下采樣,那麼能夠獲得一個16x16的輸出圖像,這意味着原圖像上的四個像素合併成爲輸出圖像中的一個像素。實現下采樣的方法有不少種,最多見的是最大值合併、平均值合併以及隨機合併。

最後一個下采樣層(或卷積層)一般鏈接到一個或多個全連層,全連層的輸出就是最終的輸出。

訓練過程經過改進的反向傳播實現,將下采樣層做爲考慮的因素並基於全部值來更新卷積濾波器的權重。

能夠在這看幾個應用在 MNIST 數據集上的卷積網絡的例子,在這還有一個用JavaScript實現的一個可視的相似網絡。

實現

目前爲止,咱們已經學會了常見神經網絡中最主要的元素了,可是我只寫了不多的在實現過程當中所遇到的挑戰。

歸納來說,個人目標是實現一個深度學習的庫,即一個基於神經網絡且知足以下條件的框架:

一個能夠表示多種模型的通用架構(好比全部上文提到的神經網絡中的元素)

可使用多種訓練算法(反向傳播,對比差別等等)。

體面的性能

爲了知足這些要求,我在軟件的設計中使用了分層的思想。

結構

咱們從以下的基礎部分開始:

NeuralNetworkImpl 是全部神經網絡模型實現的基類。

每一個網絡都包含有一個 layer 的集合。

每一層中有一個 connections 的鏈表, connection 指的是兩個層之間的鏈接,將整個網絡構成一個有向無環圖。

這個結構對於經典的反饋網絡、RBM 及更復雜的如 ImageNet 都已經足夠靈活。

這個結構也容許一個 layer 成爲多個網絡的元素。好比,在 Deep Belief Network(深度信度網絡)中的layer也能夠用在其 RBM 中。

另外,經過這個架構能夠將DBN的預訓練階段顯示爲一個棧式RBM的列表,微調階段顯示爲一個前饋網絡,這些都很是直觀並且程序實現的很好。

數據流

下個部分介紹網絡中的數據流,一個兩步過程:

定義出層間的序列。例如,爲了獲得一個多層感知機的結果,輸入數據被賦到輸入層(所以,這也是首先被計算的層),而後再將數據經過不一樣的方法流向輸出層。爲了在反向傳播中更新權重,輸出的偏差經過廣度優先的方法從輸出層傳回每一層。這部分經過 LayerOrderStrategy 進行實現,應用到了網絡圖結構的優點,使用了不一樣的圖遍歷方法。其中一些樣例包含了 廣度優先策略 和 定位到一個指定的層。層的序列實際上由層間的鏈接進行決定,因此策略部分都是返回一個鏈接的有序列表。

計算激活值。每一層都有一個關聯的 ConnectionCalculator,包含有鏈接的列表(從上一步得來)和輸入值(從其它層獲得)並計算獲得結果的激活值。例如,在一個簡單的S形前饋網絡中,隱含層的 ConnectionCalculator 接受輸入層和偏置層的值(分別爲輸入值和一個值全爲1的數組)和神經元之間的權重值(若是是全鏈接層,權重值實際上以一個矩陣的形式存儲在一個 FullyConnected 結構中,計算加權和,而後將結果傳給S函數。ConnectionCalculator 中實現了一些轉移函數(如加權求和、卷積)和激活函數(如對應多層感知機的對數函數和雙曲正切函數,對應RBM的二態函數)。其中的大部分均可以經過 Aparapi 在GPU上進行計算,能夠利用迷你批次訓練。

經過 Aparapi 進行 GPU 計算

像我以前提到的,神經網絡在近些年復興的一個重要緣由是其訓練的方法能夠高度並行化,容許咱們經過GPGPU高效的加速訓練。本文中,我選擇 Aparapi 庫來進行GPU的支持。

Aparapi 在鏈接計算上強加了一些重要的限制:

只容許使用原始數據類型的一維數組(變量)。

在GPU上運行的程序只能調用 Aparapi Kernel 類自己的成員函數。

這樣,大部分的數據(權重、輸入和輸出數據)都要保存在 Matrix 實例裏面,其內部是一個一維浮點數組。全部Aparapi 鏈接計算都是使用 AparapiWeightedSum (應用在全鏈接層和加權求和函數上)、 AparapiSubsampling2D (應用在下采樣層)或 AparapiConv2D(應用在卷積層)。這些限制能夠經過 Heterogeneous System Architecture 裏介紹的內容解決一些。並且Aparapi 容許相同的代碼運行在CPU和GPU上。

訓練

training 的模塊實現了多種訓練算法。這個模塊依賴於上文提到的兩個模塊。好比,BackPropagationTrainer (全部的訓練算法都以 Trainer 爲基類)在前饋階段使用前饋層計算,在偏差傳播和權重更新時使用特殊的廣度優先層計算。

我最新的工做是在Java8環境下開發,其它一些更新的功能能夠在這個branch 下得到,這部分的工做很快會merge到主幹上。

結論

本文的目標是提供一個深度學習算法領域的一個簡明介紹,由最基本的組成元素開始(感知機)並逐漸深刻到多種當前流行且有效的架構上,好比受限波爾茲曼機。

神經網絡的思想已經出現了很長時間,可是今天,你若是身處機器學習領域而不知道深度學習或其它相關知識是不該該的。不該該過分宣傳,但不能否認隨着GPGPU提供的計算能力、包括Geoffrey Hinton, Yoshua Bengio, Yann LeCun and Andrew Ng在內的研究學者們提出的高效算法,這個領域已經表現出了很大的但願。如今正是最佳的時間深刻這些方面的學習。

相關文章
相關標籤/搜索