淺入淺出深度學習理論與實踐
前言
之前在知乎上看到這麼一個問題:在實際業務裡,在工作中有什麼用得到深度學習的例子麼?用到 GPU 了麼?,回頭看了一下自己寫了這麼多東西一直圍繞著traditional machine learning,所以就有了一個整理出深度學習在我熟悉的風控、推薦、CRM等等這些領域的用法的想法。
我想在這邊篇文章淺入淺出的談談這幾個方面,當然深度學習你所要了解必然不僅僅如此,後面如果有機會我會一篇篇的完善:
- CNN/RNN理解
- Attention理解
- 深度學習(CNN和RNN)傳統領域的簡單應用
- 關於深度學習的一些想法
大概會將全文分為以上幾塊,大家可以跳讀,因為本文理論上應該會冗長無比,肯定也包括資料塊+程式碼塊+解析塊,很多有基礎的同學沒有必要從頭在瞭解一遍。好了,讓我們正式開始。
CNN/RNN理解
CNN
CNN,卷積神經網路,讓我們先從一個簡單的網路結構來梳理一下:
輸入層
假如,我們有car,plane,desk,flower等等一共十類的圖片,需要讓電腦識別圖片是哪種的話,自然需要把圖片變成電腦理解的了的一種方式,比如:RxGxB(圖片的高度、寬度和深度)也就是上面輸入層32x32x3,至於什麼是RGB,請自行閱讀RGB百度百科。
卷積層一/卷積層二
卷積過程
這張圖片我覺得形象的不能再形象了,讓我們結合程式碼和圖形來理解這個卷積到底是什麼意思。tensorflow中卷積的整合程式碼如下:
filter_weights = tf.Variable(tf.truncated_normal([window_size, embed_dim, 1, filter_num], stddev=0.1),name="filter_weights")
conv_layer = tf.nn.conv2d(item_embed_layer_expand, filter_weights, [1, 1, 1, 1], padding="VALID",name="conv_layer")
filter_weights的shape是window_sizexembed_dimx1xfilter_num,window_sizexembed_dimgx1就是類似於gif圖中的黃色區域的大小,這邊就可以看作window_size=embed_dim=3,filter_weights第三維的1是指HRG中的第三維度:
filter_weights第三維如果是2的話:
想到於多了一維的並行處理,接下來看filter_weights的filter_num,最上方的gif的動圖解釋了卷積層的計算形式:
黑色字型的1/0矩陣是原來影象的畫素值,紅色的1/0是上面設定filter_weights值,他們的分別計算後的累計值即為一次掃描計算結果,比如黃色區域即為1x1+1x0+0x1+1x0+1x1+1x0+0x1+1x0+1x1.將所有的畫素值所在的位置都進行一次掃描後就可以得到:
當然,除了隨機生產filter_weights,影象操作中,指定不同的filter_weights會起到不同的作用:
但是這裡面存在兩個問題:
- 邊界掃描
- 掃描速度
邊界掃描(padding="VALID")
VALID模式如上圖所示,對原始影象進行卷積,卷積後的矩陣只有3×3階,比原來的圖片要小了。
SAME模式要求卷積後的feature map與輸入的矩陣大小相同,因此需要對輸入矩陣的外層包裹n層0,然後再按照VALID的卷積方法進行卷積。
n的求法如下式:
SAME:
edge_row = (kernel_row - 1) / 2
edge_cols = (kernel_cols - 1) / 2
VALID:edge_row = edge_cols = 0
其中,edge_row是包裹0的行數,edge_cols是包裹0的列數 , kernel_row就卷積核的行數。
掃描速度(tf.nn.conv2d中的[1,1,1,1])
這個概率也是最好理解的了,就是圖中的黃色方框位移的速度:
回到最上面filter_num,filter_num的值就是重複上述流程的次數,隨著次數的增加,會增加後面pooling層的基礎資料層數:
每次黃色箭頭後的pool層的層數就是filter_num。
到此為止,卷積層就簡單的梳理完了,主要是要清楚幾個概念:filter_weights的作用,filter_num的定義,padding的差異,還有掃描的速度。這些主要是圍繞著下面我要實際應用的場景梳理的卷積神經網路的知識點,如果要深刻透徹的瞭解還需要更多更深入的解讀。
池化層一/池化層二
先從數學角度來看,它的操作步驟:
這張圖看起來,和卷積層中的image --> Convolved feature非常類似,也是確定一個shape之後,對shape內的資料進行操作,但是差異就在:卷積層中是採取對image裡面的畫素點逐點計算後彙總,相當於加權了每個畫素點的作用;而池化層通常採用最大/最小/均值/求和等方式彙總Convolved feature。
羅列出來就是:
- 物件不同,一個是image,一個是Convolved feature
- 計算方式不同,一個matrix點對點乘積後求和,一個是直接求和(或者其他聚合操作)
池化層數學的操作比較簡單,在實際工程中的理解比較讓人困惑,其實,意義主要在三點:
- 其中一個顯而易見,就是減少引數。通過對 Feature Map(通過的手段是聚合計算) 降維,有效減少後續層需要的引數
- 一個是 Translation Invariance。它表示對於 Input,當其中畫素在鄰域發生微小位移時,Pooling Layer 的輸出是不變的。這就使網路的魯棒性增強了,有一定抗擾動的作用
- 另一個是以區塊的角度代替逐個點進行計算,降低每個點對最後結果的影響,避免了過擬合的現象
全連線層/輸出層
這個就比較簡單了,全連線層(FC)構造如下:
tf.layers.dense(brand_embed_layer, embed_dim, name="brand_fc_layer", activation=tf.nn.relu)
簡單的來說,通過activation增加了特徵的非線性的擬合能力;如果不設定activation的話,就增加了特徵的線性擬合能力。
但是,我們要知道,全連線層會有很多缺陷:
在一定程度上,可以通過增加全連線的層數提高train data的準確率,但是如果過分的增加,會造成過擬合,所以如果是自己寫的網路,一定程度上,如何控制還好全連線層的數量決定了valid data了準確率的波動。其實,完全可以通過pool層代替全連線層,17年年初很多論文指出:GAP(Global Average Pooling)的方法可以代替FC(全連線)。思想就是:用 feature map 直接表示屬於某個類的 confidence map,比如有10個類,就在最後輸出10個 feature map,每個feature map中的值加起來求平均值,然後把得到的這些平均值直接作為屬於某個類別的 confidence value,再輸入softmax中分類, 更重要的是實驗效果並不比用 FC 差,所以全連線層的分類器的作用就可以被pool層合理代替掉。
而且,全連線層引數冗餘(僅全連線層引數就可佔整個網路引數60%-80%),計算量會集中在這些引數的計算上,而且隨著你的層數的增加,你的計算成本越來越大,如果是非GPU的機器在計算的過程中會非常非常吃虧。
之所以,現在的很多很多流行網路還是以FC參與計算的原因:
- 簡單,很方便了解。而且當前的各個計算框架tensorflow,caffe等等對FC的封裝即成也是非常的完善
- 借鑑非常容易。舉個例子,如果已經有了一個通過輸出層產出10類的模型,我現在要做一個5類的模型的話,我只需要在最後一次FC層後面增加一個embed_dim=5的FC層即可。
上面就簡單了梳理了CNN裡面的簡單的網路結構,是不是真的就是這麼簡單呢?讓我們看看上面叫做相對成熟的網路:
影象中幾年前的技術,Alexnet,SSD,Yolo,還有去年的RCNN,fast-RCNN等等,網路結構都遠遠比我們想象中的要複雜,在對資料操作的行為中,無非也是上面這些操作的一些組合。
再次強調,本文重點不在介紹CNN,而是利用CNN作為傳統機器學習做協助,所以,如果想要深入瞭解CNN的同學建議從頭開始學習,不建議閱讀我這種跳講的內容作為入門。
RNN
相比CNN而言,RNN要簡單而又有趣一些:
幾乎所有的講RNN的技術文章都會有下面這張圖,無法免俗,因為確實囊括了RNN的核心:
不得不說,nlp是RNN非常優秀的應用場景,我們從nlp的角度去切入,觀察RNN在其中所起的作用也是非常好的一個方式: 假設有一句話:“今天天氣真的不是很好,讓我們去____吧。”
如果用樸素貝葉斯來解決這個填空問題,它的解決思路是:
- 先分詞,今天,天氣,真的,不是很好,讓,我們,去,吧
- 去除語氣詞等,剩餘天氣,不是很好,我們,去
- 再根據貝葉斯公式得到概率最大的值
如果用N-Grams來解決這個填空問題,它的解決思路是:
- 先分詞,今天,天氣,真的,不是很好,讓,我們,去,吧
- 去除語氣詞等,剩餘天氣,不是很好,我們,去
- 根據前缺失詞的前N個詞的條件概率來計算出具有最大概率的缺失值
樸素貝葉斯的方法只考慮每個詞出現的結果沒有考慮先後順序,可能導致由意外的非真實排序決定缺失值的問題;N-Grams的方法雖然考慮了每個詞出現的可能的同時,也考慮缺失值前N個詞的內容,但是由於計算能力的約束,並不能夠完整的保留全部的前置語句的資訊。
而RNN的出現,利用state層來儲存前面t-1刻的資訊,並迴圈傳遞在每次輸出計算中,解決ngram做不到的完整資訊儲存的問題,如下圖:
很明顯,在對Yt+2的結果預測的時候,考慮到了前面所有前置的資訊。
這張圖很好的解釋了RNN的傳遞邏輯,將所有前期的資訊以state的形式進行傳遞,在第t次的輸出結果計算的過程中,不僅僅考慮第t次的輸入值,同時考慮t-1次的state,也就是前t-1層的資訊的流動彙總結果。我們知道,最簡單最基礎的RNN裡面,可以通過tanh層來合併t-1時刻的state和t時刻的xt資訊的。雖然理論上來說,無論資訊是各多遠,RNN都能夠記得,但是!但是!實際上,我們發現,RNN隨著tanh的重複操作,是無法稍遠的資訊就無法合理的被記憶,幸運的是後面優化出來的LSTM和GRU就能一定程度上緩解這些的問題。
下面讓我們以GRU為例子,具體看看RNN是怎麼進行一次迴圈神經網路的計算的:
這邊大家需要注意,與LSTM不同,GRU將LSTM中的輸入門和遺忘門合併成了更新門。而且沒有建立中間過渡鍵memory cell,而是直接通過更新門和重置門來更新state。這樣做的好處就是大大的降低了計算的成本,加快了整個RNN訓練的速度。同時通過各種Gate將重要特徵保留,保證其在long-term傳播的時候也不會被丟失,也有利於BP的時候不容易造成梯度消失。
在RNN的官方論文中,我們看到了實測的效果如下:
很明顯的可以看到,1.雖然GRU減少了一個門的存在,但是效果與LSTM相當,但是幾乎每次測試的test效果都要優秀於傳統方法。2.GRU是真的肉眼可見的比LSTM快,證實了我們上述說的內容。也是因為這些原因,在後面為實際應用的過程中,我也是選擇了GRU來代替了LSTM做向量化及state層提取等等操作。
問題來了,雖然我知道LSTM和GRU在最後實測的效果上是比直接用tanh的簡單RNN效果要好的,但是我也無法解釋和理解為什麼這樣的構造就能夠有這樣的提升,這就比較尷尬了。
另外要提的一點就是,在GRU實際計算的過程中,採取了學習引數拼接的方式,比如上面的Wz,Wr等是通過拼接的方式儲存的,在需要的計算的時候再拆分開進行計算:
這也是讓我在學習GRU過程中眼前一亮的點,非常值得玩味的地方。
Attention理解
在篇幅如此冗長的情況下,我依然堅持要和大家討論一下關於Attention的一些看法和觀點,我覺得正是有attention的存在,才讓我們能夠想到如何更好的去擴充套件應用這些深度學習的方法。
我之前一直沒有找到很好的通俗易懂的解釋attention的文章,這邊我嘗試以業務的角度為大家分析一下,儘可能的拋開數學的角度讓大家淺入淺出一下。
假設存在使用者A,及他的各種行為A_actions,如果我不做任何操作簡單的把他的各種行為A_actions當成變數進行模型訓練可以得到模型AM。但是,如果我知道,他可能是一個2年前流失近期活躍的使用者,我選擇剔除他兩年前的A_actions,而只考慮他近期的行為,這樣的過程其實就是一個Attention的過程,因為我們要預測他近期可能買什麼,所以我們應該把關注點集中在了他近期的部分資訊而不是全部資訊上。
而在深度學習運用的過程中,我們也應該考慮attention的問題,比如使用者商品點選流為A-->B-->A-->C-->D,我們常規操作是什麼樣的?無非是:
- 生成4xembed_dim的embedding層
- 將ABCD四個商品編號為0123
- 找到對應商品在embedding層中的向量表示
- Encoder過程完成
- 通過RNN或者其他深度學習網路進行非線性Decoder輸出對應的可能結果
以上就是一個非常簡單的Encoder-Decoder過程。
仔細想想其實就會發現很多不合理的地方,比如我在B商品停留了2mins,而其他商品均只停留了不到10s;再比如,我有購買C商品的歷史,ABD商品均為第一次接觸等等。其實,對於ABCD而言,簡單的理解就是它們為不是等權的。而且我們發現,隨著你的資訊量的增加,也就是item點選流的長度增加,encoder的資訊丟失就會變得非常嚴重,decoder的難度會大大的提升。
回過頭來看上述的流程,如果變成:
- 生成4xembed_dim的embedding層
- 將ABCD四個商品編號為0123
- 找到對應商品在embedding層中的向量表示
- Xa,Xb,Xc,Xd = ∑(aiA,biB,ciC,diD)
- 通過RNN或者其他深度學習網路進行非線性Decoder輸出對應的可能結果
換句話說,就是在坑爹的Encoder到Decoder過程中,增加了緩衝計算Ci,通過構造Ci代替Encoder結果進行Decoder的過程,讓深度學習的過程更加的合理。
數學的形式就是:
y1=f1(C1)
y2=f1(C2,y1)
y3=f1(C3,y1,y2)
...
比如我在B商品停留了2mins,而其他商品均只停留了不到10s時,我們就可以構造緩衝C=g(0.1xf(A)+0.6xf(B)+0.1xf(C)+0.1xf(D)),這意味著A-->B-->A-->C-->D-->?,對於?的判斷,B起了比ACD都要重要的作用。
大名鼎鼎的RNN在attention的機制下就會變成:
那麼具體如何構造緩衝C呢,我們看下面這個流程: 首先,在RNN最初引數設定的時候,我們會確定init memory,不妨記為z0;hi為當前時刻輸出的隱層輸出向量,所以對每個商品ABCD都有一個z0與hi的相似度∂0i:
在每次迴圈之前,相當於考慮了當前所有的輸入(比如此刻的ABCD)與initmemory的匹配度,至於匹配度match在論文中的計算方式為:矩陣變換α=hTWz (Multiplicative attention,Luong et al., 2015) ,其實簡化為餘弦相似度也是可以的,只要能判斷兩者之間的相似程度都行。算出所有的∂0a,∂0b,∂0c,∂0d後歸一化後的值即可作為ABCD對應的隱層ha,hb,hc,hd的權重:
c0即可作為rnn的輸入,有c0和z0,我們非常容易可以算出z1,得到z1後,重複上述的過程可以得到c1...,如此迴圈,直到結束。論文中的計算方式如下:
和nlp中構造方式對比起來,還是有一定的差異,nlp的訓練集往往是確定的。比如:“我愛學習”翻譯為“i love studying”,我翻譯為i,所以我確定一定要對“i”進行翻譯的時候,需要提高對應i的權重。而我在商品點選流預測購買概率的時候,只能通過停留時長,歷史是否購買過來建立約等的關係,但是這個約等的關係是不存在強成立的前提的。
attention的機制最初理解起來有點繞,但是如果能夠搞懂並在我們做深度網路設計中應用起來,理論上收益還是非常之大的,建議大家把上述為貼的論文詳讀一邊,真的是寫的非常不錯的一篇文章。
深度學習傳統領域的應用
我們先來回想一下,我們做傳統有監督是怎麼做的,如果記不得了,可以回顧這篇文章:提升有監督學習效果的實戰解析,我認為有幾點傳統有監督學習不是很友好:
- 特徵工程
- 實效性
- 資料解析能力
特徵工程
想必有過特徵工程專案經驗的同學可能是對資料預處理及特徵篩選過程心有餘悸:
- 是不是使用者資訊,商品資訊,使用者歷史資訊,商品資訊統計屬性刻畫,使用者行為整合每一塊寫hive都要很久很久?跑資料的時間更久?
- 是不是資料好不容易跑出來了,各種垃圾資訊,各種格式問題,pandas,numpy來回折騰到幾百行的程式碼?
- 是不是好不容易資料處理完,一跑結果auc0.6?修改都不知道怎麼修改?
- 是不是四處看別人整理的調參心得,比如這個傢伙的Kaggle&TianChi分類問題相關純演算法理論剖析,然後發現優化後就提升了1個點?
- 是不是上線之後發現數據量一旦一大,你本地跑的指令碼全部都報出:
MemoryError
? - 是不是立項一週後產品經理過來問什麼時候上線的時候,你連資料還沒整理完?
諸如這樣坑爹的事情實在多的不能再多,相對而言,無論是是CNN還是RNN或者其他深度學習網路的input都是非常簡單很清晰的,我這邊給出一些簡單的例子:
你在構造卷積神經網路的時候,只需拿出商品的基礎屬性,然後用不同性質的向量化方法embedding成不同的向量物件進行channel疊加就行了:
你在構造迴圈神經網路的時候,只需拿出使用者商品的點選流,然後構造一個流通的點對點的迴圈網路即可:
卷積網路的原始資料只需要整理item與attribute對應關係,迴圈網路的原始資料只需要整理item與clickflow對應關係,相比複雜的傳統方法的各種技巧,特徵工程的提取整理的時間會大大減少,而且在線上資料處理過程中發生Memory Error
的可能也無限變小。
實效性
這個就比較好理解了,如果我們需要知道使用者在app上每一刻的下單概率分佈,如果用傳統方法實現難度比較大,比如彙總前若干長時間內的資訊再處理加工成模型需要的形式,再通過模型判斷概率,可能就不是實時概率預估了。
而如果按照上述深度學習特徵梳理方法,離線訓練訓練好使用者的點選資訊商品資訊,再利用訓練好的模型加使用者在app上的實時行為,去預測使用者在app上每一步操作對目標變數的影響,雖然我在離線訓練的時間會付出的更多,但是我在線上預測會更加快捷。
具體效果我們以訂單預估為例,深度學習預估方法下我們會很容易看到一個使用者從開始一個session到結束一個session的過程中,購買慾望的分佈:
在使用者購買慾望特別高漲的時候,通過相應的push或者文案提醒,促進使用者下單,提高成單率。
除此之外,我們還可以觀察到每一刻全平臺使用者的購買慾望分佈:
資料解析能力
在圍繞構造特徵的時候,我們在對過去的資料整理的過程中,其實構造的最多的就是“過去N天”,“歷史上”,“最後一次”,“第一次至今”,等等。其實,這些構造方法要麼是彙總整合一段時間的資訊,要麼是單點的考慮某個時刻的資訊量。但是,深度學習一定程度上會選擇的彙總過去的資訊的累積,根據實際對最終結果的影響,改變單次行為上的權重,避免單次行為對因變數的錯誤影響。比如RNN中的state,上面RNN中的文章我也介紹了,它的生成其實就是保留了前t-1次中的部分資訊。
知乎上有這麼一個問題RNN方法能夠捕捉到 傳統時間序列迴歸中的 trend ,seasonality麼?,其實我也很好奇,在引入深度學習的fc層到machine learing做stack的時候,確實絕大部分都能提高auc,但是是不是因為這些深度學習方法能捕捉到傳統的資料裡面的類似trend這些難以統計描述的性質?
案例
說了這麼多,我覺得還是以具體的例子來剖析比較有說服力,因為深度學習的模型相對比傳統的模型程式碼要長很多很多,我這邊只擷取我認為比較重要的地方解釋一下,想要看demo的去看我的GitHub吧。
https://link.jianshu.com/?t=https%3A%2F%2Fgithub.com%2Fsladesha%2Fdeep_learning
我給出的例子都是最簡單的網路設計,如果實際要應用大家可以按照自己業務的需求增加網路的深度,改變網路的結構,這邊只是給大家一個方向。此外,資料的處理也並沒有因為深度學習模型的出現而變得不重要,Garbage In, Garbage Out!!!
RNN方案的思路是來自於Domonkos Tikk和Alexandros Karatzoglou在《Session-based Recommendations with Recurrent Neural Networks》
它構造了embedding層來把原始的輸入item對映為一個長度定義好的向量:
embedding = tf.get_variable('embedding', [self.n_risks, self.rnn_size], initializer=initializer)
通過把user瀏覽或者點選過的item進行index編號X,然後根據編號去embedding層去找對應的vector,後續只要用使用者接觸到了該item就重複以上的embedding過程就行了。
inputs = tf.nn.embedding_lookup(embedding, self.X)
再構造了簡單的GRU層來學習每次使用者的點選先後順序之間的關係:
# 多層簡單GRU層定義cell = rnn_cell.GRUCell(self.rnn_size, activation=self.hidden_act)
drop_cell = rnn_cell.DropoutWrapper(cell, output_keep_prob=self.dropout_p_hidden)
stacked_cell = rnn_cell.MultiRNNCell([drop_cell] * self.layers)
它的網路結構一點也不復雜:
首先,需要把資料集構造成中間的uid+item+time的格式:
然後通過使用者自身點選item的順序,以時間靠前的item項預測時間靠後的item項,訓練完成後記錄每條資料對應的out和state。
但是我實測了多層GRU和單層GRU,因為我們需要進行stacking的過程,不建議做多層的GRU,層數越多每層的資訊量越稀薄,我通過sum,mean處理後仍不如單層:
CNN的方案通常可以採取以下通用的網路形式:
所以可優化的點我均在網路結構中標註了,但是我一直沒有找到CNN再傳統學習中比較好的應用方式,如果拿最後一個FC層的向量stacking實測效果並不理想,相關程式碼我也放在了GitHub中了,大家可以作為一個嘗試性的demo去看。
一樣的item的向量化處理方式:
cate_embed_matrix = tf.Variable(tf.random_uniform([cate_max, embed_dim], -1, 1),name="cate_embed_matrix")
cate_embed_layer = tf.nn.embedding_lookup(cate_embed_matrix, cate, name="cate_embed_layer")
本文來自 微信公眾號 datadw 【大資料探勘DT資料分析】
網路結構中主要是通過構造全連線層和卷積層:
# 全連線cate_fc_layer = tf.layers.dense(cate_embed_layer, embed_dim, name="cate_fc_layer", activation=tf.nn.relu)# 卷積層filter_weights = tf.Variable(tf.truncated_normal([window_size, embed_dim, 1, filter_num], stddev=0.1),name="filter_weights")
filter_bias = tf.Variable(tf.constant(0.1, shape=[filter_num]), name="filter_bias")
conv_layer = tf.nn.conv2d(item_embed_layer_expand, filter_weights, [1, 1, 1, 1], padding="VALID",name="conv_layer")
relu_layer = tf.nn.relu(tf.nn.bias_add(conv_layer, filter_bias), name="relu_layer")
maxpool_layer = tf.nn.max_pool(relu_layer, [1, sentences_size - window_size + 1, 1, 1], [1, 1, 1, 1],padding="VALID", name="maxpool_layer")
然後再把所有全連線完和卷積完的vector拼接:
# 第一層全連線cate_fc_layer = tf.layers.dense(cate_embed_layer, embed_dim, name="cate_fc_layer",activation=tf.nn.relu)
brand_fc_layer = tf.layers.dense(brand_embed_layer, embed_dim, name="brand_fc_layer",activation=tf.nn.relu)# 第二層全連線bc_combine_layer = tf.concat([cate_fc_layer, brand_fc_layer, dropout_layer], 2) bc_combine_layer = tf.contrib.layers.fully_connected(bc_combine_layer, 200, tf.tanh)
最後通過全連線輸出結果:
inference_layer = item_combine_layer_flat
inference = tf.layers.dense(inference_layer, 2, kernel_initializer=tf.truncated_normal_initializer(stddev=0.01),kernel_regularizer=tf.nn.l2_loss, name="inference")
關於深度學習一些想法
這篇文章終於要結束了,漫漫長文。影象、語音、自然語言處理這三個領域,深度學習的效能就是比傳統方法好得多,無可辯駁。但是傳統領域,比如點選率預估,風控概率預估,金融風險預估等等,我不贊成非得和深度學習扯上關係,我們應該想想:
- 我們有足夠大量的資料支撐計算麼?
- 我們的業務需求允許我們進行大量黑盒計算麼?
- 帶來的“提高”允許你所付出的成本麼?
- 使用者真的知道自己在做什麼麼?
最後,我以血和淚的教訓知道自己寫的網路對模型的效果提升是非常非常小的,建議大家先熟知現有的成熟的網路
https://www.jianshu.com/p/adff4c48c40c