《Python數據分析與機器學習實戰-唐宇迪》讀書筆記第20章--神經網絡項目實戰——影評情感分析

python數據分析個人學習讀書筆記-目錄索引

第20章神經網絡項目實戰——影評情感分析

   之前講解神經網絡時,都是以圖像數據爲例,訓練過程中,數據樣本之間是相互獨立的。但是在自然語言處理中就有些區別,例如,一句話中各個詞之間有明確的先後順序,或者一篇文章的上下文之間肯定有聯繫,但是,傳統神經網絡卻無法處理這種關係。遞歸神經網絡(Recurrent Neural Network,RNN)就是專門解決這類問題的,本章就遞歸神經網絡結構展開分析,並將其應用在真實的影評數據集中進行分類任務。

20.1遞歸神經網絡

   遞歸神經網絡與卷積神經網絡並稱深度學習中兩大傑出代表,分別應用於計算機視覺與自然語言處理中,本節介紹遞歸神經網絡的基本原理。

20.1.1RNN網絡架構

   RNN網絡的應用十分廣泛,任何與自然語言處理能掛鉤的任務基本都有它的影子,先來看一下它的整體架構,如圖20-1所示。

   邀月工作室

  圖20-1 RNN網絡整體架構

  其實只要大家熟悉了基本的神經網絡結構,再來分析遞歸神經網絡就容易多了,它只比傳統網絡多做了一件事——保留各個輸入的中間信息。例如,有一個時間序列數據 [X0,X1,X2,...,Xt],如果直接用神經網絡去做,網絡會依次輸入各個數據,不會考慮它們之間的聯繫。

  在RNN網絡中,一個序列的輸入數據來了,不僅要計算最終結果,還要保存中間結果,例如,整體操作需要2個全連接層得到最後的結果,現在把經過第一個全連接層得到的特徵單獨保存下來。

  在計算下一個輸入樣本時,輸入數就不僅是當前的輸入樣本,還包括前一步得到的中間特徵,相當於綜合考慮本輪的輸入和上一輪的中間結果。通過這種方法,可以把時間序列的關係加入網絡結構中。

  例如,可以把輸入數據想象成一段話,X0,X1,…,Xi就是其中每一個詞語,此時要做一個分類任務,看一看這句話的情感是積極的還是消極的。首先將X0最先輸入網絡,不僅得到當前輸出結果h0,還有其中間輸出特徵。接下來將X1輸入網絡,和它一起進來的還有前一輪X0的中間特徵,以此類推,最終這段話最後一個詞語Xt輸入進來,依舊會結合前一輪的中間特徵(此時前一輪不僅指Xt-1,因爲Xt-1也會帶有Xt-2的特徵,以依類推,就好比綜合了前面全部的信息),得到最終的結果ht就是想要的分類結果。

  可以看到,在遞歸神經網絡中,只要沒到最後一步,就會把每一步的中間結果全部保存下來,以供後續過程使用。每一步也都會得到相應的輸出,只不過中間階段的輸出用處並不大,因爲還沒有把所有內容都加載加來,通常都是把最後一步的輸出結果當作整個模型的最終輸出,因爲它把前面所有的信息都考慮進來。

  例如,當前的輸入是一句影評數據,如圖20-2所示。

 邀月工作室

  圖20-2 影評輸入數據

  網絡結構展開如圖20-3所示。

 邀月工作室

  圖20-3 RNN網絡展開

  每一輪輸出結果爲:

 邀月工作室

  這就是遞歸神經網絡的整體架構,原理和計算方法都與神經網絡類似(全連接),只不過要考慮前一輪的結果。這就使得RNN網絡更適用於時間序列相關數據,它與語言和文字的表達十分相似,所以更適合自然語言處理任務。

20.1.2LSTM網絡

   RNN網絡看起來十分強大,那麼它有沒有問題呢?如果一句話過長,也就是輸入X序列過多的時候,最後一個輸入會把前面所有的中間特徵都考慮進來。此時可以想象一下,通常情況下,語言或者文字都是離着越近,相關性越高,例如:“今天我白天在家玩了一天,主要在玩遊戲,晚上照樣沒事幹,準備出去打球。”最後的詞語“打球”應該會和晚上沒事幹比較相關,而和前面的玩遊戲沒有多大關係,但是,RNN網絡會把很多無關信息全部考慮進來。實際的自然語言處理任務也會有相似的問題,越相關的應當前後越緊密,如果中間東西記得太多,就會使得整體網絡模型效果有所下降。

  所以最好的辦法就是讓網絡有選擇地記憶或遺忘一些內容,重要的東西需要記得更深刻,價值不大的信息可以遺忘掉,這就用到當下最流行的Long Short Term Memory Units,簡稱LSTM,它在RNN網絡的基礎上加入控制單元,以有選擇地保留或遺忘部分中間結果,現在來看一下它的整體架構,如圖20-4所示。

   邀月工作室

  圖20-4 LSTM整體架構

  它的主要組成部分有輸入門、輸出門、遺忘門和一個記憶控制器C,簡單概述,就是通過一個持續維護並進行更新的Ct來控制每次迭代需要記住或忘掉哪些信息,如果一個序列很長,相關的內容會選擇記憶下來,一些沒用的描述忘掉就好。

  LSTM網絡在處理問題時,整體流程還是與RNN網絡類似,只不過每一步增加了選擇記憶的細節,這裏只向大家進行了簡單介紹,瞭解其基本原理即可,如圖20-5所示。隨着技術的升級,RNN網絡中各種新產品也是層出不窮。

   邀月工作室

  圖20-5 LSTM網絡展開

 

20.2影評數據特徵工程

   現在要對電影評論數據集進行分類任務(二分類),建立一個LSTM網絡模型,以識別哪些評論是積極肯定的情感、哪些是消極批判的情感。下面先來看看數據(見圖20-6)。

One of the very best Three Stooges shorts ever. A spooky house full of evil guys and “The Goon” challenge the Alert Detective Agency’s best men. Shemp is in top form in the famous in-the-dark scene. Emil Sitka provides excellent support in his Mr. Goodrich role,as the target of a murder plot. Before it’s over,Shemp’s “trusty little shovel” is employed to great effect. This 16 minute gem moves about as fast as any Stooge’s short and packs twice the wallop. Highly recommended.

   邀月工作室

  圖20-6 影評數據分類任務

  這就是其中一條影評數據,由於英文數據本身以空格爲分隔符,所以直接處理詞語即可。但是這裏有一個問題——如何構建文本特徵呢?如果直接利用詞袋模型或者TF-IDF方法計算整個文本向量,很難得到比較好的效果,因爲一篇文章實在太長。

  另一個問題就是RNN網絡的輸入要求是什麼?在原理講解中已經指出,需要把整個句子分解成一個個詞語,因此每一個詞就是一個輸入,即x0,x1,…,xt。所以需要考慮每一個詞的特徵表示。

  在數據處理階段,一定要弄清楚最終網絡需要的輸入是什麼,按照這個方向去處理數據。

20.2.1詞向量

   特徵一直是機器學習中的難點,爲了使得整個模型效果更好,必須要把詞的特徵表示做好,也就是詞向量。

  如圖20-7所示,每一個詞都需要轉換成相應的特徵向量,而且維度必須一致,關於詞向量的組成,可不是簡單的詞頻統計,而是需要有實際的含義。

   邀月工作室

  圖20-7 詞向量的組成

  如果基於統計的方法來製作向量,love和adore是兩個完全不同的向量,因爲統計的方法很難考慮詞語本身以及上下文的含義,如圖20-8所示。如果用詞向量模型(word2vec)來製作,結果就大不相同。

  圖20-9爲詞向量的特徵空間意義。相似的詞語在向量空間上也會非常類似,這纔是希望得到的結果。所以,當拿到文本數據之後,第一步要對語料庫進行詞向量建模,以得到每一個詞的向量。

   邀月工作室

  圖20-8 詞向量的意義

   邀月工作室

  圖20-9 詞向量的特徵空間意義

由於訓練詞向量的工作量很大,在很多通用任務中,例如常見的新聞數據、影評數據等,都可以直接使用前人用大規模語料庫訓練之後的結果。因爲此時希望得到每一個詞的向量,肯定是預料越豐富,得到的結果越好(見圖20-10)。

 邀月工作室

圖20-10 詞向量製作

  詞語能通用的原因在於,語言本身就是可以跨內容使用的,這篇文章中使用的每一個詞語的含義換到下一篇文章中基本不會發生變化。但是,如果你的任務是專門針對某一領域,例如醫學實驗,這裏面肯定會有大量的專有名詞,此時就需要單獨訓練詞向量模型來解決專門問題。

  接下來簡單介紹一下詞向量的基本原理,也就是Word2Vec模型,在自然語言處理中經常用到這個模型,其目的就是得到各個詞的向量表示。

  Word2Vec模型如圖20-11所示,整體的結構還是神經網絡,只不過此時要訓練的不僅是網絡的權重參數,還有輸入數據。首先對每個詞進行向量初始化,例如隨機創建一個300維的向量。在訓練過程中,既可以根

  據上下文預測某一箇中間詞,例如文本是:今天天氣不錯,上下文就是今天不錯,預測結果爲:天氣,如圖20-11(a)所示;也可以由一個詞去預測其上下文結果。最終通過神經網絡不斷迭代,以訓練出每一個詞向量結果。

   邀月工作室

  圖20-11 Word2Vec模型

  在word2vec模型中,每一次迭代更新,輸入的詞向量都會發生變化,相當於既更新網絡權重參數,也更新輸入數據。

  關於詞向量的建模方法,Gensim工具包中已經給出了非常不錯的文檔教程,如果要親自動手創建一份詞向量,可以參考其使用方法,只需先將數據進行分詞,然後把分詞後的語料庫傳給Word2Vec函數即可,方法還是非常簡單的。

1 from gensim.models.word2vec import Word2Vec
2 model=Word2Vec(sentences_list,works=num_workers,size=num_features,min_count=min_word_count,window=context)

  使用時,需要指定好每一個參數值。

  • sentences:分好詞的語料庫,可以是一個list。
  • sg:用於設置訓練算法,默認爲0,對應CBOW算法;sg=1則採用skip-gram算法。
  • size:是指特徵向量的維度,默認爲100。大的size需要更多的訓練數據,但是效果會更好,推薦值爲幾十到幾百。
  • window:表示當前詞與預測詞在一個句子中的最大距離是多少。
  • alpha:是學習速率。
  • seed:用於隨機數發生器,與初始化詞向量有關。
  • min_count:可以對字典做截斷,詞頻少於min_count次數的單詞會被丟棄掉,默認值爲5。
  • max_vocab_size:設置詞向量構建期間的RAM限制。如果所有獨立單詞個數超過這個,則就消除掉其中最不頻繁的一個。每1000萬個單詞需要大約1GB的RAM。設置成None,則沒有限制。
  • workers:控制訓練的並行數。
  • hs:如果爲1,則會採用hierarchica softmax技巧。如果設置爲0(defaut),則negative sampling會被使用。
  • negative:如果>0,則會採用negative samping,用於設置多少個noise words。
  • iter:迭代次數,默認爲5。

  訓練完成後得到的詞向量如圖20-12所示,基本上都是較小的數值,其含義如同降維得到的結果,還是很難進行解釋。

 邀月工作室

  圖20-12 詞向量結果

  製作好詞向量之後,還可以動手試試其效果,看一下到底有沒有空間中的實際含義:

 邀月工作室

邀月工作室

  通過實驗結果可以看出,使用語料庫訓練得到的詞向量確實有着實際含義,並且具有相同含義的詞在特徵空間中是非常接近的。關於詞向量的維度,通常情況下,50~300維比較常見,谷歌官方給出的word2vec模型的詞向量是300維,能解決絕大多數任務。

20.2.2數據特徵製作

   影評數據集中涉及的詞語都是常見詞,所以完全可以利用前人訓練好的詞向量模型,英文數據集中有很多訓練好的結果,最常用的就是谷歌官方給出的詞向量結果,但是,它的詞向量是300維度,也就是說,在RNN模型中,每一次輸入的數據都是300維的,如果大家用筆記本電腦來跑程序會比較慢,所以這裏選擇另外一份詞向量結果,每個詞只有50維特徵,一共包含40萬個常用詞。

 1 import numpy as np
 2 #讀取詞數據集
 3 wordsList = np.load('./training_data/wordsList.npy')
 4 print('Loaded the word list!')
 5 #已經訓練好的詞向量模型
 6 wordsList = wordsList.tolist() 
 7 #給定相應格式
 8 wordsList = [word.decode('UTF-8') for word in wordsList] 
 9 #讀取詞向量數據集
10 wordVectors = np.load('./training_data/wordVectors.npy')
11 print ('Loaded the word vectors!')
12 
13 print(len(wordsList))
14 print(wordVectors.shape)
400000
(400000, 50)

  關於詞向量的製作,也可以自己用Gensim工具包訓練,如果大家想處理一份300維的特徵數據,不妨自己訓練一番,文本數據較少時,很快就能得到各個詞的向量表示。

如果大家想看看詞向量的模樣,可以實際傳入一些單詞試一試:

1 baseballIndex = wordsList.index('baseball')
2 wordVectors[baseballIndex]
array([-1.9327  ,  1.0421  , -0.78515 ,  0.91033 ,  0.22711 , -0.62158 ,
       -1.6493  ,  0.07686 , -0.5868  ,  0.058831,  0.35628 ,  0.68916 ,
       -0.50598 ,  0.70473 ,  1.2664  , -0.40031 , -0.020687,  0.80863 ,
       -0.90566 , -0.074054, -0.87675 , -0.6291  , -0.12685 ,  0.11524 ,
       -0.55685 , -1.6826  , -0.26291 ,  0.22632 ,  0.713   , -1.0828  ,
        2.1231  ,  0.49869 ,  0.066711, -0.48226 , -0.17897 ,  0.47699 ,
        0.16384 ,  0.16537 , -0.11506 , -0.15962 , -0.94926 , -0.42833 ,
       -0.59457 ,  1.3566  , -0.27506 ,  0.19918 , -0.36008 ,  0.55667 ,
       -0.70315 ,  0.17157 ], dtype=float32)

  上述代碼返回的結果就是一個50維的向量,其中每一個數值的含義根本理解不了,但是計算機卻能看懂它們的整體含義。

  現在已經有各個詞的向量,但是手裏拿到的是一篇文章,需要對應地找到其各個詞的向量,然後再組合在一起,先來整體看一下流程,如圖20-13所示。

   邀月工作室

  圖20-13 詞向量讀取

  由圖可見,先得到一句話,然後取其在詞庫中的對應索引位置,再對照詞向量錶轉換成相應的結果,例如輸入10個詞,最終得到的結果就是[10,50],表示每個詞都轉換成其對應的向量。

  Embedding Matrix表示整體的詞向量大表,要在其中尋找所需的結果,TensorFlow提供了一個非常便捷的函數tf.nn.embedding_lookup(),可以快速完成查找工作,如果任務與自然語言處理相關,那會經常用到這個函數。

  整體流程看起來有點麻煩,其實就是對照輸入中的每一個詞將其轉換成相應的詞向量即可,在數據量較少時,也可以用字典的方法查找替換,但是,當數據量與詞向量矩陣都較大時,最好使用embedding_lookup()函數,速度起碼快一個數量級。

  在將所有影評數據替換爲詞向量之前,需要考慮不同的影評數據長短不一所導致的問題,要不要規範它們?

 1 import tensorflow as tf
 2 #可以設置文章的最大詞數來限制
 3 maxSeqLength = 10 
 4 #每個單詞的最大維度
 5 numDimensions = 300 
 6 firstSentence = np.zeros((maxSeqLength), dtype='int32')
 7 firstSentence[0] = wordsList.index("i")
 8 firstSentence[1] = wordsList.index("thought")
 9 firstSentence[2] = wordsList.index("the")
10 firstSentence[3] = wordsList.index("movie")
11 firstSentence[4] = wordsList.index("was")
12 firstSentence[5] = wordsList.index("incredible")
13 firstSentence[6] = wordsList.index("and")
14 firstSentence[7] = wordsList.index("inspiring")
15 #如果長度沒達到設置的標準,用0來佔位
16 print(firstSentence.shape)
17 #結果
18 print(firstSentence) 
19 
20 with tf.Session() as sess:
21     print(tf.nn.embedding_lookup(wordVectors,firstSentence).eval().shape)

  修正代碼爲:

1 with tf.compat.v1.Session() as sess:
2     print(tf.nn.embedding_lookup(wordVectors,firstSentence).eval().shape)
3     
4 # AttributeError: module 'tensorflow' has no attribute 'Session'
5 # with tf.Session() as sess:
6 #     print(tf.nn.embedding_lookup(wordVectors,firstSentence).eval().shape)        
(10,)
[    41    804 201534   1005     15   7446      5  13767      0      0]
(10, 50)

  對一篇影評數據來說,首先找到其對應索引位置(之後要通過索引得到其對應的詞向量結果),再利用embedding_lookup()函數就能得到其詞向量結果,其中wordVectors是製作好的詞向量庫,firstSentence就是要尋找的詞向量的這句話。(10,50)表示將10個單詞轉換成對應的詞向量結果。

  這裏需要注意,之後設計的RNN網絡必須適用於所有文章。例如一篇文章的長度是200(x1,x2,…,x200),另一篇是300(x1,x2,…,x300),此時輸入數據大小不一致,這是根本不行的,在網絡訓練中,必須保證結構是一樣的(這是全連接操作的前提)。

  此時需要對文本數據進行預處理操作,基本思想就是選擇一個合適的值來限制文本的長度,例如選250(需要根據實際任務來選擇)。如果一篇影評數據中詞語數量比250多,那就從第250個詞開始截斷,後面的就不需要了;少於250個詞的,缺失部分全部用0來填充即可。

  影評數據一共包括25000篇評論,其中消極和積極的數據各佔一半,之前說到需要定義一個合適的篇幅長度來設計RNN網絡結構,這裏先來統計一下每篇文章的平均長度,由於數據存儲在不同文件夾中,所以需要分別讀取不同類別中的每一條影評數據(見圖20-14)。

 邀月工作室

  圖20-14 數據存儲格式

 1 from os import listdir
 2 from os.path import isfile, join
 3 #指定好數據集位置,由於提供的數據都一個個單獨的文件,所以還得一個個讀取
 4 positiveFiles = ['./training_data/positiveReviews/' + f for f in listdir('./training_data/positiveReviews/') if isfile(join('./training_data/positiveReviews/', f))]
 5 negativeFiles = ['./training_data/negativeReviews/' + f for f in listdir('./training_data/negativeReviews/') if isfile(join('./training_data/negativeReviews/', f))]
 6 numWords = []
 7 #分別統計積極和消極情感數據集
 8 for pf in positiveFiles:
 9     with open(pf, "r", encoding='utf-8') as f:
10         line=f.readline()
11         counter = len(line.split())
12         numWords.append(counter)       
13 print('情感積極數據集加載完畢')
14 
15 for nf in negativeFiles:
16     with open(nf, "r", encoding='utf-8') as f:
17         line=f.readline()
18         counter = len(line.split())
19         numWords.append(counter)  
20 print('情感消極數據集加載完畢')
21 
22 numFiles = len(numWords)
23 print('總共文件數量', numFiles)
24 print('全部詞語數量', sum(numWords))
25 print('平均每篇評論詞語數量', sum(numWords)/len(numWords)
情感積極數據集加載完畢
情感消極數據集加載完畢
總共文件數量 25000
全部詞語數量 5844680
平均每篇評論詞語數量 233.7872

  可以將平均長度233當作RNN中序列的長度,最好還是繪圖觀察其分佈情況:

1 import matplotlib.pyplot as plt
2 %matplotlib inline
3 plt.hist(numWords, 50)
4 plt.xlabel('Sequence Length')
5 plt.ylabel('Frequency')
6 plt.axis([0, 1200, 0, 8000])
7 plt.show()

   邀月工作室

  從整體上觀察,絕大多數評論的長度都在300以內,所以暫時設置RNN序列長度爲250沒有問題,這也可以當作是整體模型的一個參數,大家也可以用實驗來對比不同長度對結果的影響。

 

 1 maxSeqLength = 250
 2 
 3 #隨便哪一篇評論來看看結果
 4 fname = positiveFiles[3] 
 5 with open(fname) as f:
 6     for lines in f:
 7         print(lines)
 8         exit
 9 
10 # 刪除標點符號、括號、問號等,只留下字母數字字符
11 import re
12 strip_special_chars = re.compile("[^A-Za-z0-9 ]+")
13 
14 def cleanSentences(string):
15     string = string.lower().replace("<br />", " ")
16     return re.sub(strip_special_chars, "", string.lower())
17 
18 firstFile = np.zeros((maxSeqLength), dtype='int32')
19 with open(fname) as f:
20     indexCounter = 0
21     line=f.readline()
22     cleanedLine = cleanSentences(line)
23     split = cleanedLine.split()
24     for word in split:
25         try:
26             firstFile[indexCounter] = wordsList.index(word)
27         except ValueError:
28             firstFile[indexCounter] = 399999 #Vector for unknown words
29         indexCounter = indexCounter + 1
30 firstFile

邀月工作室

   上述輸出就是對文章截斷後的結果,長度不夠的時候,指定0進行填充。接下來是一個非常耗時間的過程,需要先把所有文章中的每一個詞轉換成對應的索引,然後再把這些矩陣的結果返回。

  如果大家的筆記本電腦性能一般,可能要等上大半天,這裏直接給出一份轉換結果,實驗的時候,可以直接讀取轉換好的矩陣:

# ids = np.zeros((numFiles, maxSeqLength), dtype='int32')
# fileCounter = 0
# for pf in positiveFiles:
#    with open(pf, "r") as f:
#        indexCounter = 0
#        line=f.readline()
#        cleanedLine = cleanSentences(line)
#        split = cleanedLine.split()
#        for word in split:
#            try:
#                ids[fileCounter][indexCounter] = wordsList.index(word)
#            except ValueError:
#                ids[fileCounter][indexCounter] = 399999 #Vector for unkown words
#            indexCounter = indexCounter + 1
#            if indexCounter >= maxSeqLength:
#                break
#        fileCounter = fileCounter + 1 

# for nf in negativeFiles:
#    with open(nf, "r") as f:
#        indexCounter = 0
#        line=f.readline()
#        cleanedLine = cleanSentences(line)
#        split = cleanedLine.split()
#        for word in split:
#            try:
#                ids[fileCounter][indexCounter] = wordsList.index(word)
#            except ValueError:
#                ids[fileCounter][indexCounter] = 399999 #Vector for unkown words
#            indexCounter = indexCounter + 1
#            if indexCounter >= maxSeqLength:
#                break
#        fileCounter = fileCounter + 1 
# #Pass into embedding function and see if it evaluates. 

# np.save('idsMatrix', ids)
1 ids = np.load('./training_data/idsMatrix.npy')

  在RNN網絡進行迭代的時候,需要指定每一次傳入的batch數據,這裏先做好數據的選擇方式,方便之後在網絡中傳入數據。

 1 from random import randint
 2 # 製作batch數據,通過數據集索引位置來設置訓練集和測試集
 3 #並且讓batch中正負樣本各佔一半,同時給定其當前標籤
 4 def getTrainBatch():
 5     labels = []
 6     arr = np.zeros([batchSize, maxSeqLength])
 7     for i in range(batchSize):
 8         if (i % 2 == 0): 
 9             num = randint(1,11499)
10             labels.append([1,0])
11         else:
12             num = randint(13499,24999)
13             labels.append([0,1])
14         arr[i] = ids[num-1:num]
15     return arr, labels
16 
17 def getTestBatch():
18     labels = []
19     arr = np.zeros([batchSize, maxSeqLength])
20     for i in range(batchSize):
21         num = randint(11499,13499)
22         if (num <= 12499):
23             labels.append([1,0])
24         else:
25             labels.append([0,1])
26         arr[i] = ids[num-1:num]
27     return arr, labels

  構造好batch數據後,數據和標籤就確定了。

  圖20-15所示爲數據最終預處理後的結果,構建RNN模型的時候,還需再將詞索引轉換成對應的向量。現在再向大家強調一下輸入數據的格式,傳入RNN網絡中的數據需是一個三維的形式,即[batchsize,文本長度,詞向量維度],例如一次迭代訓練10個樣本數據,每個樣本長度爲250,每個詞的向量維度爲50,輸入就是[10,250,50]。

   邀月工作室

  圖20-15 數據預處理結果

  在數據預處理時,最好的方法就是先倒着來思考,想一想最終網絡模型要求輸入什麼,然後對照目標進行預處理和特徵提取。

 

20.3構建RNN模型

      邀月:本篇以下示例全部基於tensorflow 1.15.2版本運行,最初原想用tensorflow tensorflow-2.1.0運行,折騰兩個晚上,放棄了。

https://github.com/tensorflow/community/blob/master/rfcs/20180907-contrib-sunset.md 看這裏,google的版本什麼時候都會給人驚喜和絕望。

/*
D:\tools\Python37>pip install tensorflow==1.15.2
Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple
Collecting tensorflow==1.15.2
........
Installing collected packages: tensorboard, tensorflow-estimator, tensorflow
  Attempting uninstall: tensorboard
    Found existing installation: tensorboard 2.1.1
    Uninstalling tensorboard-2.1.1:
      Successfully uninstalled tensorboard-2.1.1
  Attempting uninstall: tensorflow-estimator
    Found existing installation: tensorflow-estimator 2.1.0
    Uninstalling tensorflow-estimator-2.1.0:
      Successfully uninstalled tensorflow-estimator-2.1.0
Successfully installed tensorboard-1.15.0 tensorflow-1.15.2 tensorflow-estimator-1.15.1
*/

 

  首先需要設置模型所需參數,在RNN網絡中,其基本計算方式還是全連接,所以需要指定隱層神經元數量:

  • batchSize=24
  • lstmUnits=64
  • numClasses=2
  • iterations=50000

  其中,batchSize可以根據自己機器性能來選擇,如果覺得迭代過程有些慢,可以再降低一些;lstmUnits表示其中每一個隱層的神經元數量;numClasses就是最終要得到的輸出結果,也就是一個二分類問題;在迭代過程中,iterations就是最大迭代次數。

  網絡模型的搭建方法都是相同的,還是先指定輸入數據的格式,然後定義RNN網絡結構訓練迭代:

 1 batchSize = 24
 2 lstmUnits = 64
 3 numClasses = 2
 4 iterations = 50000

  

1 import tensorflow as tf
2 tf.reset_default_graph()
3 
4 labels = tf.placeholder(tf.float32, [batchSize, numClasses])
5 input_data = tf.placeholder(tf.int32, [batchSize, maxSeqLength])

  依舊用placeholder()進行佔位,此時只得到二維的結果,即[batchSize,maxSeqLength],還需將文本中每一個詞由其索引轉換成相應的詞向量。

1 data = tf.Variable(tf.zeros([batchSize, maxSeqLength, numDimensions]),dtype=tf.float32)
2 data = tf.nn.embedding_lookup(wordVectors,input_data)

 

  使用embedding_lookup函數完成最後的詞向量讀取轉換工作,就搞定了輸入數據,大家在建模時,一定要清楚[batchSize,maxSeqLength,numDimensions]這三個維度的含義,不能只會調用工具包函數,還需要理解其中細節。

  構建LSTM網絡模型,需要分幾步走:

1 lstmCell = tf.contrib.rnn.BasicLSTMCell(lstmUnits)
2 lstmCell = tf.contrib.rnn.DropoutWrapper(cell=lstmCell, output_keep_prob=0.75)
3 value, _ = tf.nn.dynamic_rnn(lstmCell, data, dtype=tf.float32)
WARNING:tensorflow:
The TensorFlow contrib module will not be included in TensorFlow 2.0.
For more information, please see:
  * https://github.com/tensorflow/community/blob/master/rfcs/20180907-contrib-sunset.md
  * https://github.com/tensorflow/addons
  * https://github.com/tensorflow/io (for I/O related ops)
If you depend on functionality not listed there, please file an issue.

WARNING:tensorflow:From <ipython-input-17-db6a6fc2c55e>:1: BasicLSTMCell.__init__ (from tensorflow.python.ops.rnn_cell_impl) is deprecated and will be removed in a future version.
Instructions for updating:
This class is equivalent as tf.keras.layers.LSTMCell, and will be replaced by that in Tensorflow 2.0.
WARNING:tensorflow:From <ipython-input-17-db6a6fc2c55e>:3: dynamic_rnn (from tensorflow.python.ops.rnn) is deprecated and will be removed in a future version.
Instructions for updating:
Please use `keras.layers.RNN(cell)`, which is equivalent to this API
WARNING:tensorflow:From d:\tools\python37\lib\site-packages\tensorflow_core\python\ops\rnn_cell_impl.py:735: Layer.add_variable (from tensorflow.python.keras.engine.base_layer) is deprecated and will be removed in a future version.
Instructions for updating:
Please use `layer.add_weight` method instead.
WARNING:tensorflow:From d:\tools\python37\lib\site-packages\tensorflow_core\python\ops\rnn_cell_impl.py:739: calling Zeros.__init__ (from tensorflow.python.ops.init_ops) with dtype is deprecated and will be removed in a future version.
Instructions for updating:
Call initializer instance with the dtype argument instead of passing it to the constructor

  首先創建基本的LSTM單元,也就是每一個輸入走的網絡結構都是相同的,再把這些基本單元和輸入的序列數據組合起來,還可以加入Dropout功能。關於RNN網絡,還有很多種創建方法,這些在TensorFlow官網中都有實例說明,用的時候最好先參考一下其API文檔。

1 #權重參數初始化
2 weight = tf.Variable(tf.truncated_normal([lstmUnits, numClasses]))
3 bias = tf.Variable(tf.constant(0.1, shape=[numClasses]))
4 value = tf.transpose(value, [1, 0, 2])
5 #取最終的結果值
6 last = tf.gather(value, int(value.get_shape()[0]) - 1)
7 prediction = (tf.matmul(last, weight) + bias)

  RNN網絡的權重參數初始化方法與傳統神經網絡一致,都是全連接的操作,需要注意網絡輸出會有多個結果,可以參考圖20-1,每一個輸入的詞向量都與當前輸出結果相對應,最終選擇最後一個詞所對應的結果,並且通過一層全連接操作轉換成對應的分類結果。

  網絡模型和輸入數據確定後,接下來與之前訓練方法一致,給定損失函數和優化器,然後迭代求解即可:

 1 correctPred = tf.equal(tf.argmax(prediction,1), tf.argmax(labels,1))
 2 accuracy = tf.reduce_mean(tf.cast(correctPred, tf.float32))
 3 
 4 
 5 loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=prediction, labels=labels))
 6 optimizer = tf.train.AdamOptimizer().minimize(loss)
 7 
 8 sess = tf.InteractiveSession()
 9 saver = tf.train.Saver()
10 sess.run(tf.global_variables_initializer())
11 
12 for i in range(iterations):
13     #之前已經定義好的拿到batch數據函數
14     nextBatch, nextBatchLabels = getTrainBatch();
15     sess.run(optimizer, {input_data: nextBatch, labels: nextBatchLabels}) 
16     #每隔1000次打印一下當前的結果
17     if (i % 1000 == 0 and i != 0):
18         loss_ = sess.run(loss, {input_data: nextBatch, labels: nextBatchLabels})
19         accuracy_ = sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels})
20         
21         print("iteration {}/{}...".format(i+1, iterations),
22               "loss {}...".format(loss_),
23               "accuracy {}...".format(accuracy_))    
24     #每個1W次保存一下當前模型
25     if (i % 10000 == 0 and i != 0):
26         save_path = saver.save(sess, "models/pretrained_lstm.ckpt", global_step=i)
27         print("saved to %s" % save_path)

  這裏不僅打印當前迭代結果,每隔1萬次還會保存當前的網絡模型。TensorFlow中保存模型最簡單的方法,就是用saver.save()函數指定保存的模型,以及保存的路徑。保存好訓練的權重參數,當預測任務來臨時,直接讀取模型即可。

  可能有同學會問,爲什麼不能只保存最後一次的結果?由於網絡在訓練過程中,其效果可能發生浮動變化,而且不一定迭代次數越多,效果就越好,可能第3萬次的效果要比第5萬次的還要強,因此需要保存中間結果。

  訓練網絡需要耐心,這份數據集中,由於給定的網絡結構和詞向量維度都比較小,所以訓練起來很快:

iteration 1001/50000... loss 0.5977783799171448... accuracy 0.5...
iteration 2001/50000... loss 0.6750070452690125... accuracy 0.5833333134651184...
iteration 3001/50000... loss 0.6415902972221375... accuracy 0.3333333432674408...
iteration 4001/50000... loss 0.6623604893684387... accuracy 0.5416666865348816...
iteration 5001/50000... loss 0.6792729496955872... accuracy 0.5833333134651184...
iteration 6001/50000... loss 0.6929864883422852... accuracy 0.5...
iteration 7001/50000... loss 0.6421437859535217... accuracy 0.5416666865348816...
iteration 8001/50000... loss 0.6045424938201904... accuracy 0.7083333134651184...
iteration 9001/50000... loss 0.49526092410087585... accuracy 0.875...
iteration 10001/50000... loss 0.23377956449985504... accuracy 0.9166666865348816...
saved to models/pretrained_lstm.ckpt-10000
iteration 11001/50000... loss 0.22206246852874756... accuracy 0.9166666865348816...
iteration 12001/50000... loss 0.49464142322540283... accuracy 0.7916666865348816...
iteration 13001/50000... loss 0.26161226630210876... accuracy 0.9166666865348816...
iteration 14001/50000... loss 0.3195655047893524... accuracy 0.9166666865348816...
iteration 15001/50000... loss 0.2544494867324829... accuracy 0.7916666865348816...
iteration 16001/50000... loss 0.4504941403865814... accuracy 0.7916666865348816...
iteration 17001/50000... loss 0.14206933975219727... accuracy 0.9583333134651184...
iteration 18001/50000... loss 0.28434768319129944... accuracy 0.875...
iteration 19001/50000... loss 0.2196163386106491... accuracy 0.875...
iteration 20001/50000... loss 0.1411515176296234... accuracy 0.9166666865348816...
saved to models/pretrained_lstm.ckpt-20000
iteration 21001/50000... loss 0.10852870345115662... accuracy 0.9166666865348816...
iteration 22001/50000... loss 0.17102549970149994... accuracy 0.875...
iteration 23001/50000... loss 0.2163759469985962... accuracy 0.9583333134651184...
iteration 24001/50000... loss 0.40744829177856445... accuracy 0.9583333134651184...
iteration 25001/50000... loss 0.12172506004571915... accuracy 0.875...
iteration 26001/50000... loss 0.22618673741817474... accuracy 0.9166666865348816...
iteration 27001/50000... loss 0.29675474762916565... accuracy 0.9583333134651184...
iteration 28001/50000... loss 0.028712689876556396... accuracy 1.0...
iteration 29001/50000... loss 0.06705771386623383... accuracy 0.9583333134651184...
iteration 30001/50000... loss 0.06020817533135414... accuracy 1.0...
saved to models/pretrained_lstm.ckpt-30000
iteration 31001/50000... loss 0.053414225578308105... accuracy 1.0...
iteration 32001/50000... loss 0.018836012110114098... accuracy 1.0...
iteration 33001/50000... loss 0.22417615354061127... accuracy 0.9583333134651184...
iteration 34001/50000... loss 0.11704185605049133... accuracy 0.9583333134651184...
iteration 35001/50000... loss 0.01906798779964447... accuracy 1.0...
iteration 36001/50000... loss 0.11806797981262207... accuracy 0.9166666865348816...
iteration 37001/50000... loss 0.03285054862499237... accuracy 1.0...
iteration 38001/50000... loss 0.10801851749420166... accuracy 0.9166666865348816...
iteration 39001/50000... loss 0.026212451979517937... accuracy 1.0...
iteration 40001/50000... loss 0.047293175011873245... accuracy 1.0...
saved to models/pretrained_lstm.ckpt-40000
iteration 41001/50000... loss 0.015445593744516373... accuracy 1.0...
iteration 42001/50000... loss 0.06675603985786438... accuracy 0.9583333134651184...
iteration 43001/50000... loss 0.026207834482192993... accuracy 1.0...
iteration 44001/50000... loss 0.012909072451293468... accuracy 1.0...
iteration 45001/50000... loss 0.019438063725829124... accuracy 1.0...
iteration 46001/50000... loss 0.01888447254896164... accuracy 1.0...
iteration 47001/50000... loss 0.010169420391321182... accuracy 1.0...
iteration 48001/50000... loss 0.04857032373547554... accuracy 0.9583333134651184...
iteration 49001/50000... loss 0.009694952517747879... accuracy 1.0...

 

  隨着網絡迭代的進行,模型也越來越收斂,基本上2萬次就能夠達到完美的效果,但是不要高興得太早,這只是訓練集的結果,還要看測試集上的效果。

  如圖20-16、圖20-17所示,雖然只用了非常簡單的LSTM結構,收斂效果還是不錯的,其實最終模型的效果在很大程度上還是與輸入數據有關,如果不使用詞向量模型,訓練的效果可能就要大打折扣。

 邀月工作室

  ▲圖20-16 訓練時準備率變化情況

 邀月工作室

  ▲圖20-17 訓練時損失變化情況

  接下來再看看測試的效果,這裏先給大家演示一下如何加載已經保存好的模型:

1 sess = tf.InteractiveSession()
2 saver = tf.train.Saver()
3 saver.restore(sess, tf.train.latest_checkpoint('models'))

  這裏加載的是最後保存的模型,當然也可以指定具體的名字來加載指定的模型文件,讀取的就是之前訓練網絡時候所得到的各個權重參數,接下來只需要在batch裏面傳入實際的測試數據集即可:

1 iterations = 10
2 for i in range(iterations):
3     nextBatch, nextBatchLabels = getTestBatch();
4     print("Accuracy for this batch:", (sess.run(accuracy, {input_data: nextBatch, labels: nextBatchLabels})) * 100)

 

Accuracy for this batch: 87.5
Accuracy for this batch: 79.16666865348816
Accuracy for this batch: 83.33333134651184
Accuracy for this batch: 83.33333134651184
Accuracy for this batch: 91.66666865348816
Accuracy for this batch: 83.33333134651184
Accuracy for this batch: 75.0
Accuracy for this batch: 87.5
Accuracy for this batch: 91.66666865348816
Accuracy for this batch: 91.66666865348816

  爲了使測試效果更穩定,選擇10個batch數據,在二分類任務中,得到的結果只能說整體還湊合,可以明顯發現網絡模型已經有些過擬合。在訓練數據集中,基本都是100%,然而實際測試時卻有所折扣。大家在實驗的時候,也可以嘗試改變其中的參數,以調節網絡模型,再對比最終的結果。

  在神經網絡訓練過程中,可以調節的細節比較多,通常都是先調整學習率,導致過擬合最可能的原因就是學習率過大。網絡結構與輸出數據也會對結果產生影響,這些都需要通過大量的實驗進行對比觀察。

 

項目小結:

   該章從整體上介紹了RNN網絡結構及其升級版本LSTM網絡,針對自然語言處理,其實很大程度上拼的是如何進行特徵構造,詞向量模型可以說是當下最好的解決方案之一,對詞的維度進行建模要比整體文章建模更實用。針對影評數據集,首先進行數據格式處理,這也是按照後續網絡模型的要求輸入的,TensorFlow當中有很多便捷的API可以完成處理任務,例如常用的embedding_lookup(),至於其具體用法,官網的解釋肯定是最好的,所以千萬不要忽視最直接的資源。在處理序列數據上,RNN網絡結構有着先天的優勢,所以,其在文本處理任務上,尤其涉及上下文和序列相關任務的時候,還是儘可能優先選擇深度學習算法,雖然速度要慢一些,但是整體效果還不錯。

  20章的機器學習算法與實戰的學習到這裏就結束了,其中經歷了數學推導的考驗與案例中反覆的實驗,相信大家已經掌握了機器學習的核心思想與實踐方法。算法本身並沒有高低之分,很多時候拼的是如何對數據進行合適的特徵提取,結合特徵工程,將最合適的算法應用到最適合的數據中才是上策。學習應當是反覆的過程,每一次都會有更深的理解,機器學習算法本身較爲複雜,時常複習也是必不可缺的。案例的利用也是如此,光看不練終歸不是自己的,舉一反三才能提升自己的實戰技能。在後續的學習和工作中,根據業務需求,還可以結合實際論文來探討解決方案,善用資料,加以理解,並應用到自己的任務中,纔是最佳的提升路線。

 

第20章完。全書完

python數據分析個人學習讀書筆記-目錄索引

 

該書資源下載,請至異步社區:https://www.epubit.com

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章