第2章
了解TensorFlow
在本章中,你将深入了解TensorFlow。這是一個開源分布式數值計算架構,它将成為我們實作所有練習的主要平台。
我們通過定義一個簡單的計算并用TensorFlow實作它來作為TensorFlow的入門。在成功完成此操作後,我們将探讨TensorFlow是如何執行這個計算的。這将有助于我們了解該架構如何建立計算圖來計算輸出/并通過稱為“會話”的方式執行此圖。然後,通過将TensorFlow執行操作的方式與餐廳的運作進行類比,我們深入了解TensorFlow架構。
在對TensorFlow的運作方式有了良好的概念性和技術上的了解之後,我們将介紹該架構提供的一些重要的計算操作。首先,我們将讨論如何在TensorFlow中定義各種資料結構,比如變量、占位符和張量,同時我們還将介紹如何讀取輸入。然後,我們将執行一些與神經網絡相關的操作(例如,卷積運算、定義損失函數和優化方法)。接下來,我們将學習如何使用作用域來重用和有效管理TensorFlow變量。最後,在練習中應用這些知識,實作一個可以識别手寫數字圖像的神經網絡。
2.1 TensorFlow是什麼
在第1章中,我們簡要讨論了TensorFlow是什麼。現在讓我們更深入地認識它。Tensor-
Flow是由Google釋出的開源分布式數值計算架構,主要用于減少在實作神經網絡的過程中那些令人感到痛苦的細節(例如,計算神經網絡權重的梯度)。TensorFlow使用計算統一裝置架構(CUDA)來進一步有效實作這種數值計算,CUDA是由NVIDIA引入的并行計算平台。在
https://www.tensorflow.org/api_docs/python/上有TensorFlow的應用程式程式設計接口(API),可以看到TensorFlow提供了數千種操作,這使我們的工作更輕松。
TensorFlow不是一夜之間開發出來的,它是有才華、善良的人們堅持不懈的成果。他們希望通過将深度學習帶給更廣泛的使用者來使我們的生活發生變化。如果你有興趣,可以通路
https://github.com/tensorflow/tensorflow檢視TensorFlow代碼。目前,TensorFlow擁有大約1000名貢獻者,并且擁有超過25000次成果送出,它每天都在變得越來越好。
2.1.1 TensorFlow入門
現在讓我們通過代碼示例了解TensorFlow架構中的一些基本元件,讓我們編寫一個示例來執行以下計算,這對于神經網絡非常常見:

這裡W和x是矩陣,b是向量。然後,
表示點積。sigmoid是一個非線性變換,由以下公式給出:
我們将逐漸驟讨論如何通過TensorFlow進行此計算。
首先,我們需要導入TensorFlow和NumPy。在Python中運作與TensorFlow或NumPy相關的任何類型的操作之前,必須先導入它們:
接下來,我們将定義一個圖對象,稍後我們将在這個對象上定義操作和變量:
圖形對象包括計算圖,計算圖可以連接配接我們在程式中定義的各種輸入和輸出,以獲得最終的所需輸出(即它定義了如何根據圖連接配接W,x和b來生成h)。例如,如果你将輸出視為蛋糕,那麼圖就是使用各種成分(即輸入)制作蛋糕的配方。此外,我們将定義一個會話對象,該對象将定義的圖作為輸入,以執行圖。我們将在下一節詳細讨論這些元素。
你可以用以下方式建立新的圖對象,就像我們在上一個的例子裡一樣:
或者你可以用以下方式擷取TensorFlow的預設計算圖:
這兩種方式都會在練習中使用。
現在我們定義一些張量,即x、W、b和h。張量在TensorFlow中基本上是n維數組。例如,一維向量或二維矩陣稱為張量。在TensorFlow中有幾種不同的方法可以定義張量,在這裡,我們會讨論三種不同的方法:
1.首先,x是占位符。顧名思義,占位符沒有初始化值,我們将在圖執行時臨時提供值。
2.接下來,我們有變量W和b。變量是可變的,這意味着它們的值可以随時間變化。
3.最後,我們有h,這是一個通過對x、W和b執行一些操作而産生的不可變張量:
另外,請注意,對于W和b,我們提供了一些重要的參數,如下所示:
它們稱為變量初始化器,是最初指派給W和b變量的張量。變量不能像占位符一樣在沒有初始值的情況下傳遞,并且我們需要一直為變量指定一些值。這裡,tf.random_uniform意味着我們在minval(-0.1)和maxval(0.1)之間均勻地采樣,以便将采樣值賦給張量,而tf.zeros則用零初始化張量。在定義張量時,定義張量的形狀也非常重要,shape屬性定義張量的每個次元的大小。例如,如果形狀是[10, 5],則意味着它将是一個二維結構,在第0維上有10個元素,在1維上有5個元素。
接下來,我們将運作初始化操作,初始化圖中的變量W和b:
現在,我們執行該圖,以獲得我們需要的最終輸出h。這是通過運作session.run(…)來完成的,我們提供占位符的值作為session.run()指令的參數:
最後,我們關閉會話,釋放會話對象占用的資源:
下面是這個TensorFlow例子的完整代碼。本章所有的示例代碼都可以在ch2檔案夾下的tensorf?low_introduction.ipynb中找到。
當你執行這段代碼的時候,可能會遇到下面這樣的警告:
不用擔心這個,這個警告說你使用了現成的TensorFlow預編譯版本,而沒有在你的計算機上編譯它,這完全沒問題。如果你在計算機上進行編譯,會獲得稍微好一點的性能,因為TensorFlow将針對特定硬體進行優化。
在後面的幾節中,我們将解釋TensorFlow如何執行此代碼,以生成最終輸出。另請注意,接下來的兩節可能有些複雜和偏技術。但是,即使你沒有完全了解所有内容,也不必擔心,因為在此之後,我們将通過一個完全是現實世界中的例子來進一步說明。我們會用在我們自己的餐廳Café Le TensorFlow裡訂單是如何完成的,來解釋之前的相同執行過程。
2.1.2 TensorFlow用戶端詳細介紹
前面的示例程式稱為TensorFlow用戶端。在使用TensorFlow編寫的任何用戶端中,都有兩種主要的對象類型:操作和張量。在前面的例子中,tf.nn.sigmoid是一個操作,h是張量。
然後我們有一個圖對象,它是存儲程式資料流的計算圖。當我們在代碼中依次添加x、W、b和h時,TensorFlow會自動将這些張量和任何操作(例如,tf.matmul())作為節點添加到圖中。該圖将存儲重要資訊,比如張量之間的依賴性以及在哪裡執行什麼運算。在我們的示例中,圖知道要計算h,需要張量x、W和b。是以,如果在運作時沒有正确初始化其中某一個,TensorFlow會指出需要修複的初始化錯誤。
接下來,會話扮演執行圖的角色,它将圖劃分為子圖,然後劃分為更精細的碎片,之後将這些碎片配置設定給執行任務的worker。這是通過session.run(…)函數完成的,我們很快就會談到它。為了之後引用友善,我們将這個例子稱為sigmoid示例。
2.1.3 TensorFlow架構:當你執行用戶端時發生了什麼
我們知道TensorFlow非常善于建立一個包含所有依賴關系和操作的計算圖,它可以确切地知道資料是如何及什麼時候在哪裡流轉。但是,應該有一個元素可以有效執行定義好的計算圖,使TensorFlow變得更好,這個元素就是會話。現在讓我們來看看會話的内部,了解圖的執行方式。
首先,TensorFlow用戶端包含圖和會話。建立會話時,它會将計算圖作為tf.GraphDef協定緩沖區發送到分布式主伺服器,tf.GraphDef是圖的标準化表示。分布式主伺服器檢視圖中的所有計算,并将計算切割後配置設定給不同的裝置(例如,不同的GPU和CPU)。我們的sigmoid示例中的圖如圖2.1所示,圖的單個元素稱為節點。
接下來,計算圖将由分布式主伺服器分解為子圖,并進一步分解為更小的任務。雖然在我們的例子中分解計算圖似乎很微不足道,但在實際應用中,有多層隐藏層的神經網絡解決方案的計算圖可能是指數級增長的。此外,将計算圖分解為多個部分來并行執行(例如,多個裝置)變得越來越重要。執行圖(或由這個圖劃分的子圖)稱為單個任務,任務會配置設定給單個TensorFlow伺服器。
但是,實際上,每個任務都會分解為兩個部分來執行,其中每個部分由一個worker執行:
- 一個worker使用參數的目前值執行TensorFlow操作(稱為操作執行器)
- 另一個worker存儲參數并在執行操作後更新它們的值(稱為參數伺服器)
TensorFlow用戶端的正常工作流程如圖2.2所示。
圖2.3展示了圖的分解過程。除了将圖分解以外,TensorFlow還插入發送和接收節點,以幫助參數伺服器和操作執行器互相通信。你可以把發送節點了解為一旦資料可用時發送資料,而接受節點在相應的發送節點發送資料時偵聽和捕獲資料。
最後,一旦計算完成,會話就會将更新的資料從參數伺服器帶回用戶端。TensorFlow的體系結構如圖2.4所示,這一解釋基于
https://www.tensorflow.org/extend/architecture上的官方TensorFlow文檔。
2.1.4 Cafe Le TensorFlow:使用類比了解TensorFlow
如果你對技術性說明中包含的資訊感到不堪重負,下面我們嘗試從不同的角度來介紹相關概念。假設有一家新咖啡館開業了,你一直想去那。然後你去了那家咖啡館,在靠窗的位置坐下。
接下來,服務員來請你下訂單,你點了一個有奶酪沒有蕃茄的雞肉漢堡。這裡,請将你自己看作用戶端,你的訂單就是定義的圖。該圖定義了你需要什麼以及相關資訊。服務員類似于會話,他的責任是将訂單帶到廚房,以便執行訂單。在接受訂單時,服務員使用特定格式來傳達你的訂單,例如,桌号、菜單項ID、數量和特殊要求。可以把服務員用的格式化訂單想象成GraphDef。然後,服務員把訂單帶到廚房,把它交給廚房經理。從這一刻開始,廚房經理負責執行訂單。到這裡,廚房經理代表分布式主伺服器。廚房經理做出決定,例如需要多少廚師來制作菜肴,以及哪些廚師是最适合此工作的人選。我們假設每位廚師都有一位助理,他的職責是為廚師提供合适的食材、裝置等。是以,廚房經理将訂單交給一位廚師和一位廚師助理(雖然漢堡沒有這麼難準備),并要求他們準備好菜肴。在這裡,廚師是操作執行器,助理是參數伺服器。
廚師檢視訂單,告訴助理需要什麼。是以,助理首先找到所需的材料(例如,面團、肉餅和洋蔥),并盡快将它們準備在一起以滿足廚師的要求。此外,廚師可能會要求暫時保留菜肴的中間結果(例如,切好的蔬菜),直到廚師再次需要它們。
漢堡準備好後,廚房經理會收到廚師和廚師助理做的漢堡,并通知服務員。此時,服務員從廚房經理那裡取出漢堡帶給你,你終于可以享用根據你的要求制作的美味漢堡,該過程如圖2.5所示。
2.2 輸入、變量、輸出和操作
在了解底層架構之後,我們将介紹構成TensorFlow用戶端的最常見元素。如果你閱讀過網上數百萬個TensorFlow用戶端中的任何一個,那麼它們(TensorFlow相關的代碼)都屬于下面這些類型之一:
- 輸入:用來訓練和測試算法的資料。
- 變量:可變的張量,大部分用于定義算法的參數。
- 輸出:不可變的張量,用于存儲中間和最終的輸出。
- 操作:對輸入做不同的變換以産生想要的輸出。
在之前的sigmoid示例中,我們可以找到所有這些類别的執行個體,表2.1中列出這些元素:
下面更詳細地解釋每個TensorFlow元素。
2.2.1 在TensorFlow中定義輸入
用戶端主要以三種方式接收資料:
- 使用Python代碼在算法的每個步驟中提供資料
- 将資料預加載并存儲為TensorFlow張量
- 搭建輸入管道
讓我們來看看這些方式。
2.2.1.1 使用Python代碼提供資料
在第一種方法中,可以使用傳統的Python代碼将資料饋送到TensorFlow用戶端。在之前的示例中,x是這種方法的一個例子。為了從外部資料結構(例如,numpy.ndarray)向用戶端提供資料,TensorFlow庫提供了一種優雅的符号資料結構,稱為占位符,它被定義為tf.placeholder(…)。顧名思義,占位符在圖的建構階段不需要實際資料,相反,僅在圖執行過程中通過session.run(…,feed_dict = {placeholder:value})将外部資料以Python字典的形式傳遞給feed_dict參數,其中鍵是tf .placeholder變量,相應的值是實際資料(例如,numpy.ndarray)。占位符定義采用以下形式:
參數如下:
- dtype:這是送入占位符的資料類型
- shape:這是占位符的形狀,以1維向量形式給出
- name:這是占位符的名稱,這對調試很重要
2.2.1.2 将資料預加載并存儲為張量
第二種方法類似于第一種方法,但可以少擔心一件事。由于資料已預先加載,是以我們無須在圖的執行期間提供資料。為了明白這一點,讓我們修改我們的sigmoid示例。請記住,我們之前将x定義為占位符:
現在,讓我們将其定義為包含特定值的張量:
以下是完整的代碼:
你會注意到,代碼與我們原來的sigmoid示例有兩個主要差別。首先,我們定義x的方式不同,我們現在直接指定一個特定的值并将x定義為張量,而不是使用占位符對象并在圖執行時輸入實際值。另外,正如你所看到的,我們沒有在session.run(…)中提供任何額外的參數。但缺點是,現在你無法在session.run(…)中向x提供不同的值,并看到輸出是如何改變的。
2.2.1.3 搭建輸入管道
輸入管道是專門為需要快速處理大量資料的更重型用戶端設計的。實際上會建立一個儲存資料的隊列,直到我們需要它為止。TensorFlow還提供各種預處理步驟(例如,用于調整圖像對比度/亮度,或進行标準化),這些步驟可在将資料送到算法之前執行。為了提高效率,可以讓多個線程并行讀取和處理資料。
一個典型的通道包含以下元素:
- 檔案名清單
- 檔案名隊列,用于為輸入(記錄)讀取器生成檔案名
- 記錄讀取器,用于讀取輸入(記錄)
- 解碼器,用于解碼讀取記錄(例如,JPEG圖像解碼)
- 預處理步驟(可選)
- 一個示例(即解碼輸入)隊列
讓我們使用TensorFlow編寫一個輸入管道的快速示例。在這個例子中,我們有三個CSV格式的文本檔案(text1.txt、text2.txt和text3.txt),每個檔案有五行,每行有10個用逗号分隔的數字(示例行:0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0)。我們需要從檔案一直到表示檔案中那些輸入的張量,搭建一個輸入管道,來分批讀取資料(多行資料向量),下面一步一步地介紹這個過程。
有關更多資訊,請參閱官方TensorFlow頁面上有關導入資料的内容:
https://www.tensorflow.org/programmers_guide/reading_data。
首先,像以前一樣導入一些重要庫:
接着,定義圖和會話對象:
然後,定義一個檔案名隊列,這是一個包含檔案名的隊列資料結構。它将作為參數傳遞給讀取器(很快将被定義)。隊列将根據讀取器的請求生成檔案名,以便讀取器可以用這些檔案名通路檔案以讀取資料:
這裡,capacity是給定時間隊列持有的資料量,shuffle告訴隊列是否應該在吐出資料之前将其打亂。
TensorFlow有幾種不同類型的讀取器(
https://www.tensorflow.org/api_guides/python/io_ops#Readers提供了可用讀取器清單)。由于我們有一些單獨的文本檔案,其中一行代表一個單獨的資料點,是以TextLineReader最适合我們:
定義讀取器後,我們可以使用read()函數從檔案中讀取資料,它的輸出是鍵值對,其中,鍵辨別檔案和檔案中正在讀取的記錄(即文本行),我們可以省略它,而值傳回讀取器讀取的行的實際值:
接下來,我們定義record_defaults,如果發現任何錯誤記錄,将輸出它:
現在我們将讀取到的文本行解碼為數字列(因為我們有CSV檔案),為此,我們使用decode_csv()方法。如果使用文本編輯器打開檔案(例如,test1.txt),你會看到每一行有10列:
然後,我們把這些列拼接起來,形成單個張量(稱為特征),這些張量被傳給另一個方法tf.train.shuff?le_batch(),該方法的輸入是前面定義的張量(特征),然後将張量進行打亂按批次輸出:
batch_size參數是在給定的步驟中對資料采樣的批次大小,capacity是資料隊列的容量(大隊列需要更多記憶體),min_after_dequeue表示出隊後留在隊列中的最小元素數量。最後,num_threads定義用于生成一批資料的線程數。如果管道中有大量的預處理,則可以增加此線程數。此外,如果需要在不打亂資料(使用tf.train.shuffle_batch)的情況下讀取資料,則可以使用tf.train.batch操作。然後,我們将通過調用以下代碼啟動此管道:
可以将類tf.train.Coordinator()視為線程管理器,它實作了各種管理線程的機制(例如,啟動線程并在任務完成後讓線程加入主線程)。我們需要tf.train.Coordinator()類,因為輸入管道會産生許多線程來執行隊列填充(即入隊)、隊列出隊和許多其他任務。接下來,我們将使用之前建立的線程管理器執行tf.train.start_queue_runners(…)。QueueRunner()儲存隊列的入隊操作,并在定義輸入管道時自動建立它們。是以,要填充已定義的隊列,我們需要使用tf.train.start_queue_runners函數啟動這些隊列運作程式。
接下來,在我們感興趣的任務完成之後,我們需要顯式地停止線程,并讓它們加入主線程,否則程式将無限期挂起,這是通過coord.request_stop()和coord.join(threads)來實作的。這種輸入管道可以與我們的sigmoid示例相合,以便它直接從檔案中讀取資料,如下所示:
2.2.2 在TensorFlow中定義變量
變量在TensorFlow中扮演重要角色。變量本質上是具有特定形狀的張量,而形狀定義了變量有多少次元以及每個次元的大小。然而,與正常張量不同,變量是可變的,這意味着變量的值在定義後可以改變。這對于需要改變模型參數(例如,神經網絡權重)的學習模型來說是理想特性,其權重在每個學習步驟之後會稍微變化。例如,如果使用x = tf.Variable(0, dtype = tf.int32)定義變量,則可以使用TensorFlow操作(比如tf.assign(x,x + 1))更改該變量的值。
但是,如果像x = tf.constant(0,dtype = tf.int32)這樣定義張量,則無法像對變量一樣更改張量的值,它會一直保持為0,直到程式執行結束。
變量建立非常簡單,在我們的例子中,我們已經建立了兩個變量W和b。在建立變量時,有一些事情非常重要,我們在這裡列出它們并在以下段落中詳細讨論:
- 變量形狀
- 資料類型
- 初始值
- 名稱(可選)
變量形狀是[x,y,z,…]格式的一維向量。清單中的每個值表示相應次元或軸的大小。例如,如果需要具有50行和10列的二維張量作為變量,則形狀是[50, 10]。
變量的維數(即形狀矢量的長度)在TensorFlow中被看作張量的秩,不要将它與矩陣的秩混淆。
TensorFlow中,張量的秩表示張量的維數,對于二維矩陣,秩= 2。
資料類型在決定變量大小方面起着重要作用。有許多不同的資料類型,包括常用的tf.bool、tf.uint8、tf.f?loat32和tf.int32。每種資料類型都需要一定的比特數來表示該類型的值。例如,tf.uint8需要8比特,而tf.f?loat32需要32比特。通常的做法是使用相同的資料類型進行計算,否則會導緻資料類型不比對。是以,如果你有兩個不同資料類型的張量,則需要對它們做資料類型轉換,因而必須使用tf.cast(…)操作将一個張量顯式轉換為另一個類型的張量。tf.cast(…)操作就是為了應對這種情況而設計的。例如,如果有一個tf.int32類型的x變量,需要将其轉換為tf.f?loat32,則可以通過tf.cast(x,dtype = tf.f?loat32)将x轉換為tf.f?loat32。
接下來,變量需要用初始值進行初始化。為友善起見,TensorFlow提供了幾種不同的初始化器,包括常數初始化器和正态分布初始化器。以下是一些可用于初始化變量的流行TensorFlow初始化器:
- tf.zeros
- tf.constant_initializer
- tf.random_uniform
- tf.truncated_normal
最後,我們會将變量的名稱用作ID在圖中辨別該變量。是以,如果你可視化計算圖,那麼變量将顯示為傳遞給name關鍵字的參數。如果未指定名稱,TensorFlow将使用預設命名方案。
請注意,計算圖并不知道被tf.Variable指派的Python變量,該變量不是TensorFlow變量命名的一部分。例如,如果定義如下TensorFlow變量:
則TensorFlow計算圖知道這個變量的名稱是b,而不是a。
2.2.3 定義TensorFlow輸出
TensorFlow輸出通常是張量,并且結果要麼轉換為輸入,要麼轉換為變量,或兩者都有。在我們的例子中,h是一個輸出,其中h = tf.nn.sigmoid(tf.matmul(x,W)+ b)。也可以将這些輸出提供給其他操作,形成一組鍊式操作,此外,它不一定必須是TensorFlow操作,也可以在TensorFlow中使用Python算術運算。這是一個例子:
2.2.4 定義TensorFlow操作
如果看一看
上的TensorFlow API,會看到TensorFlow有數量巨大的可用操作。
在這裡,我們選擇其中幾個進行介紹。
2.2.4.1 比較操作
比較操作對于比較兩個張量非常有用。以下代碼示例包含一些有用的比較操作。你可以在
https://www.tensorflow.org/api_guides/python/control_flow_ops的比較運算符部分中找到比較運算符的完整清單。此外,為了了解這些操作的工作原理,讓我們考慮兩個示例張量x和y:
2.2.4.2 數學運算
TensorFlow允許對從簡單到複雜的張量執行數學運算,我們将讨論TensorFlow提供的幾個數學運算,在
https://www.tensorflow.org/api_guides/python/math_ops可以看到完整的清單。
2.2.4.3 分散和聚合操作
分散和聚合操作在矩陣操作任務中起着至關重要的作用,因為這兩種操作的變體是在TensorFlow中索引張量的唯一方法(直到最近)。換句話說,你不能像在NumPy中那樣通路TensorFlow中的張量元素(例如,x [1, 0],其中x是2D numpy.ndarray)。分散操作允許你将值配置設定給給定張量的特定索引,而聚合操作允許你提取給定張量的切片(即個體元素)。以下代碼顯示分散和聚合操作的幾個變體:
2.2.4.4 神經網絡相關操作
現在讓我們看看幾個有用的神經網絡相關的操作,我們将在後面的章節中大量使用它們。在這裡讨論的操作涵蓋了從簡單的逐元素變換(即激活),到計算一組參數相對于另一個值的偏導數,我們還會實作一個簡單的神經網絡作為練習。
(1)神經網絡中使用的非線性激活
非線性激活使神經網絡能夠在許多任務中表現良好。通常,在神經網絡中的每個層輸出之後都會有非線性激活變換(即激活層)(除最後一層之外)。非線性變換有助于神經網絡學習資料中存在的各種非線性模式。這對于現實中複雜的問題非常有用,與線性模式相比,資料通常具有更複雜的非線性模式。如果層之間沒有非線性激活,深層神經網絡将是一堆互相堆疊的線性變換層。而且,一組線性層基本上可以壓縮成單個較大的線性層。總之,如果沒有非線性激活,我們就無法建立具有多層的神經網絡。
讓我們通過一個例子來觀察非線性激活的重要性。首先,回想一下我們在sigmoid示例中看到的神經網絡的計算。如果我們忽視b,它将是這樣的:
假設一個三層神經網絡(每層的權重為W1、W2和W3),每個層都執行上面的計算,完整的計算如下所示:
但是,如果去掉非線性激活函數(就是sigmoid),就會是這樣:
是以,在沒有非線性激活的情況下,可以将三層減少成單個線性層。
現在,我們将列出神經網絡中兩種常用的非線性激活,以及它們如何在TensorFlow中實作:
(2)卷積操作
卷積運算是一種廣泛使用的信号處理技術。對于圖像,使用卷積可以産生圖像的不同效果。使用卷積進行邊緣檢測的示例如圖2.6所示,其實作方法是在圖像頂部移動卷積濾波器,進而在每個位置産生不同的輸出(參見本節後面的圖2.7)。具體來說,在每個位置,對于與卷積濾波器重疊的圖像塊(與卷積濾波器大小相同),在卷積濾波器中對其元素進行逐元素相乘相加,并對結果求和:
以下是卷積操作的實作:
在這裡,過多的方括号可能會讓你認為去掉這些備援括号可以很容易地了解這個例子,不幸的是,事實并非如此。對于tf.conv2d(…)操作,TensorFlow要求input、filter和strides具有精确的格式。現在我們将更詳細地介紹tf.conv2d(input, filter, strides, padding)中的每個參數:
- input:這通常是4D張量,其次元應按[batch_size,height,width,channels]排序。
- batch_size:這是單批資料中的資料量(例如,如圖像和單詞的輸入)。我們通常批量處理資料,因為進行學習的資料集很大。在給定的訓練步驟,我們随機抽樣一小批資料,這些資料近似代表完整的資料集。通過許多次執行此操作,我們可以很好地逼近完整的資料集。這個batch_size參數與我們在TensorFlow輸入管道示例中讨論的參數相同。
- height和width:這是輸入的高度和寬度。
- channels:這是輸入的深度(例如,對于RGB圖像其值為3,表示有3個通道)。
- batch_size:這是單批資料中的資料量(例如,如圖像和單詞的輸入)。我們通常批量處理資料,因為進行學習的資料集很大。在給定的訓練步驟,我們随機抽樣一小批資料,這些資料近似代表完整的資料集。通過許多次執行此操作,我們可以很好地逼近完整的資料集。這個batch_size參數與我們在TensorFlow輸入管道示例中讨論的參數相同。
- filter:這是一個4D張量,表示卷積運算的卷積視窗,其次元應為[height,width,in_channels,out_channels]:
- height和width:這是卷積核的高度和寬度(通常小于輸入的高度和寬度)
- in_channels:這是該層的輸入的通道數
- out_channels:這是要在該層的輸出中生成的通道數
- strides:這是一個包含四個元素的清單,其中元素是[batch_stride,height_stride,width_stride,channels_stride]。strides參數表示卷積視窗在輸入上單次滑動期間要跳過的元素數。如果你不完全了解步長是什麼,則可以使用預設值1。
- padding:這可以是['SAME','VALID']之一,它決定如何處理輸入邊界附近的卷積運算。VALID操作在沒有填充的情況下執行卷積。如果我們用大小為h的卷積視窗卷積長度為n的輸入,則輸出大小為(n - h + 1 < n),輸出大小的減小會嚴重限制神經網絡的深度。SAME将用零填充邊界,使輸出具有與輸入相同的高度和寬度。要更好地了解卷積核大小、步長和填充是什麼,請參見圖2.7。
(3)池化操作
池化操作的行為與卷積操作類似,但最終輸出不同。池化操作取該位置的圖像塊的最大值,而不是輸出卷積核和圖像塊的逐元素相乘的總和(參見圖2.8)。
(4)定義損失
我們知道,為了讓神經網絡學習有用的東西,需要定義一個損失。在TensorFlow中有幾種可以自動計算損失的函數,其中兩種函數如下面的代碼所示。tf.nn.l2_loss函數是均方誤差損失,而tf.nn.softmax_cross_entropy_with_logits_v2是另一種類型的損失,在分類任務中它有更好的性能。這裡的logits指的是神經網絡的沒有歸一化的輸出(即神經網絡最後一層的線性輸出):
(5)優化神經網絡
在定義了神經網絡的損失之後,我們的目标是盡量減少這種損失,優化就是用于此的過程。換句話說,優化器的目标是找到對于所有輸入均給出最小損失的神經網絡參數(即權重和偏內插補點)。同樣,TensorFlow提供了幾種不同的優化器,是以,我們不必從頭開始實作它們。
圖2.9展示一個簡單的優化問題,以及優化是如何随時間進行的。曲線可以想象為損失曲線(對于高維,則是損失曲面),其中x可以被認為是神經網絡的參數(在這裡,是具有單個權重的神經網絡),而y可以被認為是損失。起點設為x = 2,從這一點開始,我們使用優化器來達到在x = 0時獲得的最小值y(即損失)。更具體地說,我們在給定點的與梯度相反的方向上移動一些小步長,并以這種方式繼續走幾個步長。然而,在實際問題中,損失曲面不會像圖中那樣好,它會更複雜:
在此示例中,我們使用GradientDescentOptimizer。learning_rate參數表示在最小化損失方向上的步長(兩個點之間的距離):
每次使用session.run(minimize_op)執行最小化損失運算時,都會接近給出最小值tf_y的tf_x值。
(6)控制流操作
控制流操作,顧名思義,控制圖中元素執行的順序。例如,假設我們需要按順序執行以下計算:
确切地說,如果x = 2,我們應該得到z = 14。讓我們首先嘗試以最簡單的方式實作這一點:
理想情況下,我們希望x = 7和z = 14,但是,TensorFlow産生x = 2和z = 4。這不是你期待的答案。這是因為除非你明确指定,否則TensorFlow不關心事物的執行順序。控制流操作就是使你能控制執行順序的操作。要修複上述代碼,我們執行以下操作:
現在,結果應該是x = 7和z = 14。tf.control_dependencies(…)操作確定在執行嵌套操作之前将執行作為參數傳遞給它的運算。
2.3 使用作用域重用變量
到目前為止,我們已經了解了TensorFlow的體系結構,以及實作基本TensorFlow用戶端所需的基本知識。然而,TensorFlow還有更多内容。正如我們已經看到的,TensorFlow的行為與典型的Python腳本完全不同。例如,你無法實時調試TensorFlow代碼(但可以用Python IDE執行簡單的Python腳本),因為在TensorFlow中計算不會實時發生(除非你使用的是Eager執行方法,它最近出現在TensorFlow1.7中:
https://research.googleblog.com/2017/10/eager-execution-imperative-def?ine-by.html)。換句話說,TensorFlow首先定義完整的計算圖,再在裝置上執行所有計算,最後得到結果。是以,調試TensorFlow用戶端可能會非常煩瑣和痛苦,這強化了在實作TensorFlow用戶端時注意細節的重要性。是以,建議遵循為TensorFlow引入的正确編碼規範。一種這樣的規範被稱為“作用域”,并允許更容易的變量重用。
重用TensorFlow變量是TensorFlow用戶端中經常出現的情況。要了解答案的價值,我們必須首先了解這個問題。此外,錯誤的代碼可以幫助我們更好了解這個問題。
假設我們想要一個執行某種計算的函數:給定w,需要計算x w + y * 2。讓我們編寫一個TensorFlow用戶端,它具有執行此操作的函數:
假設你想要在某一步計算它,然後,你可以調用session.run(very_simple_computation(2))
(當然,在調用tf.global_variables_initializer().run()之後),之後你會得到結果,并對編寫實際有效的代碼感覺良好。但是,情況可能相反,因為多次運作此函數會出現問題。每次調用此方法時,都會建立兩個TensorFlow變量。還記得我們讨論過TensorFlow與Python不同嗎?這就是一個這樣的例子。多次調用此方法時,圖中的x和y變量不會被替換。相反,将保留舊變量,并在圖中建立新變量,直到記憶體不足為止。但是,結果是正确的。要檢視此操作,請在for循環中運作session.run(very_simple_computation(2)),如果列印圖中變量的名稱,将看到兩個以上變量。
這是運作10次時的輸出:
每次運作該函數時,都會建立一對變量。讓我們明确一點:如果你運作這個函數100次,你的圖中将有198個過時變量(99個x變量和99個y變量)。
這是作用域可以解決的問題。作用域允許你重用變量,而不是每次調用函數時都建立一個變量。現在為我們的小例子添加可重用性,我們将代碼更改為以下内容:
在這個例子中,如果執行session.run([z1,z2,a1,a2,zz1,zz2]),應該看到z1、z2、a1、a2、zz1、zz2的值依次為9.0、90.0、9.0、90.0、9.0、90.0。現在,如果列印變量,你應該隻看到四個不同的變量:scopeA/x,scopeA/y,scopeB/x和scopeB/y。我們現在可以在循環中多次運作它,而不必擔心建立備援變量和記憶體不足。
現在,你可能想知道為什麼不能在代碼的開頭建立四個全局變量,并在之後的方法中使用它們。因為這會破壞代碼的封裝,這樣一來,代碼将明确依賴于該代碼塊之外的内容。
總之,作用域允許可重用性,同時保留代碼的封裝性。此外,作用域使代碼更直覺,并減少出錯的可能性,因為我們可以通過域和名稱顯式擷取變量,而不是使用被TensorFlow變量指派的Python變量。
2.4 實作我們的第一個神經網絡
在了解了TensorFlow的架構、基礎知識和作用域機制之後,我們現在應該實作比較複雜的東西:一個神經網絡。準确地說,我們将實作一個我們在第1章自然語言處理簡介中讨論過的全連接配接的神經網絡模型。
神經網絡能被引入的原因之一是能夠用它對數字進行分類。對于此任務,我們使用
http://yann.lecun.com/exdb/mnist/上提供的著名的MNIST資料集。你可能對我們使用計算機視覺任務而不是NLP任務感到有點疑惑,這是因為視覺任務可以通過較少的預處理來實作,并且易于了解。
由于這是我們第一次接觸神經網絡,我們将詳細介紹示例的主要部分。但請注意,我隻會介紹練習中的關鍵部分。要從頭到尾運作示例,可以在ch2檔案夾中的tensorf?low_introduction.ipynb檔案内找到完整練習。
2.4.1 準備資料
首先,我們需要使用maybe_download(…)函數下載下傳資料集,并使用read_mnist(…)函數對其進行預處理。這兩個函數在練習檔案中定義。read_mnist(…)函數主要執行兩個步驟:
- 讀取資料集的位元組流,并将其轉變為适當的numpy.ndarray對象
-
将圖像标準化為均值為0和方差為1(也稱為白化)
以下代碼顯示read_mnist(…)函數。
read_mnist(…)函數将包含圖像檔案的檔案名和包含标簽檔案的檔案名作為輸入,然後生成兩個包含所有圖像及其相應标簽的NumPy矩陣:
2.4.2 定義TensorFLow圖
要定義TensorFlow圖,我們首先要為輸入圖像(tf_inputs)和相應的标簽(tf_labels)定義占位符:
接下來,我們将編寫一個Python函數,它将首次建立變量。
請注意,我們使用作用域來確定可重用性,并確定正确命名變量:
接下來,我們定義神經網絡的推理過程。與使用沒有作用域的變量相比,請注意作用域是如何為函數中的代碼提供非常直覺的流程的。這個網絡有三層:
- 具有ReLU激活的全連接配接層(第一層)
- 具有ReLU激活的全連接配接層(第二層)
- 完全連接配接的softmax層(輸出)
借助于作用域,我們将每個層的變量(權重和偏差)命名為layer1/weights、layer1/bias、layer2/weights、layer2/bias、output/weights和output/bias。注意,在代碼中,它們都具有相同的名稱,但作用域不同:
現在,我們定義一個損失函數,然後定義最小化損失運算。最小化損失運算通過将網絡參數推向最小化損失的方向來最小化損失。TensorFlow中提供了多種優化器,在這裡,我們将使用MomentumOptimizer,它比GradientDescentOptimizer有更好的準确率和收斂性:
最後,我們定義一個運算來獲得給定的一批輸入的softmax預測機率,它可以用于計算神經網絡的準确率:
2.4.3 運作神經網絡
現在,我們有了運作神經網絡所需的所有必要操作,下面我們檢查它是否能夠成功學習對數字的分類:
在此代碼中,accuracy(test_predictions, test_labels)是一個函數,它接受預測的結果和标簽作為輸入,并提供準确率(與實際标簽比對的預測數量),它在練習檔案中定義。
如果運作成功,你應該能夠看到類似于圖2.10中所示的結果。50個疊代周期後,測試準确率應達到約98%:
2.5 總結
在本章中,你通過了解我們實作算法的主要的底層平台(TensorFlow),邁出了解決NLP任務的第一步。首先,我們讨論了TensorFlow架構的基本細節。接下來,我們讨論了一個有意義TensorFlow用戶端的基本要素。然後我們讨論了TensorFlow中廣泛使用的一般編碼規範,稱為作用域。後來,我們将所有這些元素組合在一起,實作了一個對MNIST資料集進行分類的神經網絡。
具體來說,我們讨論了TensorFlow架構,并使用TensorFlow用戶端示例進行說明。在TensorFlow用戶端中,我們定義了TensorFlow圖。然後,我們建立一個會話,它會檢視這個圖,建立一個表示圖的GraphDef對象,并将其發送給分布式主伺服器。分布式主伺服器檢視圖,确定用于計算的相關元件,并将其劃分為多個子圖以使計算速度更快。最後,worker執行子圖并通過會話傳回結果。
接下來,我們讨論了構成一個典型TensorFlow用戶端的各種元素:輸入、變量、輸出和操作。輸入是我們提供給算法的資料,用于訓練和測試。我們讨論了三種不同的輸入方式:使用占位符,将資料預加載并存儲為TensorFlow張量,以及使用輸入管道。然後我們讨論了TensorFlow變量,它們與其他張量如何差別,以及如何建立和初始化它們。在此之後,我們讨論了如何使用變量來建立中間及最終輸出。最後,我們讨論了幾個可用的TensorFlow操作,例如數學運算、矩陣運算、神經網絡相關運算和控制流運算,這些運算将在本書後面使用。
然後,我們讨論了在實作TensorFlow用戶端時如何使用作用域來避免某些陷阱。作用域使我們可以輕松使用變量,同時保持代碼的封裝性。
最後,我們使用所有之前學過的概念實作了一個神經網絡,我們使用三層神經網絡對MNIST數字資料集進行分類。
在下一章中,将介紹如何使用我們在本章中實作的全連接配接神經網絡來學習單詞的語義數值表示。