TensorFlow學習筆記

 TensorFlow

代碼庫地址:https://github.com/caicloud/tensorflow-tutorialhtml

TensorFlow遊樂場:http://playground.tensorflow.org node

安裝ipynb:pip install ipythonpython

打開ipynb:ipython notebookgit

介紹

計算機發明之初,人們就但願它可以幫助甚至替代人類完成重複性勞做。利用巨大的存儲空間和超高的運算速度,計算機已經能夠很是輕易地完成一些對於人類很是困難,但對於計算機相對簡單的問題。github

好比,統計一本書中不一樣單詞出現的次數,存儲一個圖書館中全部的藏書,或是計算很是複雜的數學公式,均可以輕鬆經過計算機解決。可是一些人類經過直覺能夠很好解決的問題,目前卻很難經過計算機解決。好比天然語言理解、圖像識別、語音識別等等。正則表達式

這就是人工智能須要解決的問題。算法

定義

機器學習的定義爲:若是一個程序能夠在任務T上,隨着經驗E的增長,效果P也能夠隨之增長,則稱這個程序能夠從經驗中學習。數據庫

例如垃圾郵件分類問題中,一個程序指須要用到機器學習算法,好比邏輯迴歸。任務T指區分垃圾郵件的任務,經驗E指已經區分過是否爲垃圾郵件的歷史郵件,在監督式機器學習問題中,這也被稱之爲訓練數據;效果P指機器學習算法在區分是否爲垃圾郵件任務上的正確率。在使用邏輯迴歸算法時,會從每一封郵件中提取對分類結果可能有影響的因素,好比:郵件地址、標題、收件人長度。編程

每個因素被稱之爲一個特徵(feature),邏輯迴歸算法能夠從訓練數據中計算出每一個特徵和預測結果的相關度。好比,在垃圾郵件分類中,若是一個郵件的收件人越多,那麼郵件爲垃圾郵件的機率也就越高。在對一個位置的郵件作判斷時,會根據從這封郵件中抽取獲得的每個特徵以及這些特徵和垃圾郵件的相關度來判斷這封郵件是否爲垃圾郵件。json

大部分狀況下,在訓練數據達到必定數量以前,越多的訓練數據能夠使邏輯迴歸算法對未知郵件做出的判斷越精準。邏輯迴歸算法能夠根據訓練數據(經驗E)提升在垃圾郵件分類問題(任務T)上的正確率(效果P)

邏輯迴歸算法的效果除了依賴於訓練數據,也依賴於從數據中提取的特徵。假設郵件中只有發送時間,那麼再多的數據也沒有用途。

如何從實體中提取特徵,對於不少傳統機器學習算法的性能有巨大影響。

一旦解決了數據表達和特徵提取,不少人工智能的任務也就解決了90%。 

深度學習解決的核心問題之一就是自動地將簡單的特徵組合成更加複雜的特徵,並使用這些組合特徵解決問題。

深度學習是機器學習的一個分支,除了能夠學習特徵和任務之間的關聯以外,還能自動從簡單特徵中提取更加複雜的特徵。

現代的深度學習已經超越了神經科學觀點,它能夠更普遍的適用於各類並非由神經網絡啓發而來的機器學習框架。

有一個領域的研究者試圖從算法層理解大腦的工做機制,它不一樣於深度學習的領域,被稱爲「計算神經學」(computational neuroscience)

深度學習領域主要關注如何搭建智能的計算機系統,解決人工智能中遇到的問題。計算神經學則主要關注如何創建更準確的模型來模擬人類大腦的工做。

人工智能是一類很是普遍的問題,機器學習是解決這類問題的一個重要的手段。深度學習則是機器學習的一個分支。

深度學習發展史 

深度學習基本上是深層神級網絡的一個代名詞,而神經網絡技術能夠追溯到1943年。神經網絡的發展史大體能夠分爲三個階段。

第一波

神經網絡模型相似於仿生機器學習,視圖模仿大腦的學習機理。最先的神經網絡數學模型是1943年提出的。兩位教授模擬人類大腦神經元的結構提出了McCulloch-PittsNeuron的計算結構。

McCulloch-Pitts Neuron結構大體模擬了人類神經元的工做原理,都有一些輸入,而後將輸入進行變化獲得輸出結果。雖然人類神經元處理輸入信號的原理目前還對咱們來講不徹底清晰,但此結構使用了簡單的線性加權和的方式來模擬這個變換。將n個輸入值提供給結構後,會經過n個權重w1,w2,L wn,來計算這n個輸入的加權和,而後用這個加權和通過一個閾(yu)值函數獲得一個0或1的輸出。爲了讓此結構更加精準,須要對權重進行特殊的設置。經過手動設置方式既複雜又很難達到最優,爲了讓計算機可以更加自動且更加合理地設置權重大小。

Frank Rosenblatt教授與1958年提出了感知機模型(perceptron),感知機是首個能夠根據樣例數據來學習特徵權重的模型。

1969年Marvin Minsky教授和Seymour Papert教授證實了感知機模型只能解決線性可分問題,並明確指明感知機沒法解決異或問題。

這致使了神經網絡的第一次重大低潮期,在以後的十多年內,基於神經網絡的研究幾乎處於停滯狀態。

第二波

20世紀80年代末,第二波神經網絡研究因分佈式知識表達(distributed representation)和神經網絡反向傳播算法的提出而興起。

分佈式的知識表達的核心思想是現實世界中的知識和概念,應該經過多個神經元來表達,而模型中的每個神經元也應該參與表達多個概念。

例如:設計一個系統來識別不一樣顏色不一樣型號的汽車,有兩種方法,

第一種:設計一個模型使得模型中每個神經元對應一種顏色和汽車型號的組合。

好比「白色小轎車」若是有n種顏色,m種型號,那麼這樣的表達方式須要n*m個神經元。

第二種:使用一些神經元專門表達顏色,好比「白色」,另一些神經元專門表達汽車型號,好比「小轎車」

這樣「白色小轎車」的概念能夠經過這兩個神經元的組合來表達。這種方式只須要n*m個神經元就能夠表達全部概念

即便在訓練數據中沒有出現概念「紅色皮卡」只要模型可以習得紅色和皮卡的概念,也能夠推廣到概念紅色皮卡

分佈式只是表達大大增強了模型的表達能力,讓神經網絡從寬度的方向走向了深度的方向。

第三波

2010年左右,計算量已經再也不是阻礙神經網絡發展的問題,隨着互聯網+的發展,獲取海量數據也再也不困難。

這讓神經網絡所面臨的幾個最大問題都獲得解決,因而神經網絡的發展也迎來了新的高潮。

2016年時,深度學習已經成了谷歌上最熱門的搜索詞,2013年深度學習被麻省理工(MIT)評爲了年度十大科技突破之一

現在,深度學習已經從最初的圖像識別領域擴展到了機器學習的各個領域。

TensorFlow環境搭建 

TensorFlow依賴的兩個主要的工具包-Protocol Buffer和Bazel。

雖然TensorFlow以來的工具包不只限於此節中列出來的兩個,可是這兩個是相對比較重要的。

Protocol Buffer

谷歌開發的處理結構化數據的工具。例如:要記錄一些用戶信息,每一個用戶的信息包括用戶的名字、ID、Email地址。

那麼一個用戶的信息能夠表示爲如下形式

name:張三
id:12345
email:zhangsan@abc.com

上面的用戶信息,就是一個結構化數據。 指擁有多種屬性的數據。好比上面有三個屬性,那麼他就是一個結構化數據。

當要將這些結構化的用戶信息持久化或者進行網絡傳輸,就須要先將他們序列化。

序列化指將結構化的數據變成數據流的格式,簡單的來講就是變成字符串。

如何將結構化的數據序列化,並從序列化以後的數據流中還原出原來的結構化數據,統稱爲處理結構化數據

這就是Protocol Buffer解決的主要問題。

除了Protocol Buffer以外,XML和JSON是兩種比較經常使用的結構化數據處理工具。可是與Protocol Buffer格式的數據有很大區別。

首先,Protocol Buffer序列化以後獲得的數據不是可讀字符串,而是二進制流。

其次,XML或JSON格式的數據信息都包含在序列化以後的數據中,不須要任何其餘信息就能還原序列化以後的數據。但使用Protocol Buffer時須要先定義數據格式

還原一個序列化以後的數據將須要使用到這個定義好的數據格式。Protocol Buffer序列化的數據要比XML小3到10倍,解析時間要快20到100倍

Protocol Buffer定義數據格式的文件通常保存在.proto文件中。每個message表明了一類結構化的數據,好比這裏的用戶信息。

message user{
    optional string name = 1;
    required int32 id = 2;
    repeated string email = 3;  
}

message裏面定義了每個屬性的類型和名字,屬性的類型能夠是布爾、整數、實數、字符串這樣的基本類型

也能夠是另外一個message。這樣大大增長了Protocol Buffer的靈活性 。

Protocol Buffer也定義了一個屬性是必須的(required)、可選的(optional)、可重複的(repeated)

若是一個屬性是必須的(required),那麼全部這個message的實例都須要有這個屬性;若是是可選的,那麼這個屬性的取值能夠爲空

若是是可重複的,那麼這個屬性的取值能夠是一個列表。

Bazel

谷歌開源的自動化構建工具,谷歌內部絕大部分的應用都是經過它來編譯的。

相比傳統的Makefile、Ant或者Maven。Bazel在速度、可伸縮性、靈活性以及對不一樣程序語言和平臺的支持上都要更加出色。

TensorFlow自己以及谷歌給出的不少官方樣例都是經過Bazel來編譯的。

項目空間(workspace)是Bazel的一個基本概念。一個項目空間能夠簡單地理解爲一個文件夾,在這個文件夾中包含了編譯一個軟件所須要的源代碼以及輸出編譯結果的軟鏈接地址。

一個項目空間能夠只包含一個應用(好比TensorFlow)也能夠包含多個應用。一個項目空間對應的文件夾是這個項目的根目錄。

根目錄須要有一個workspace文件,此文件定義了對外部資源的依賴關係。空文件也是一個合法的workspace文件。

在一個項目空間內,Bazel經過BUILD文件來找到須要編譯的目標。文件採用一種相似於Python的語法來指定每個編譯目標的輸入、輸出以及編譯方式。

Bazel對Python支持的編譯方式只有三種:py_binary、py_library、py_test。其中py_binary將Python程序編譯爲可執行文件,py_test編譯python測試程序

py_library將python程序編譯成庫函數供其餘py_binary或py_test調用。

使用pip安裝

pip3 install --upgrade tensorflow
pip3 install --upgrade tensorflow-gpu

測試TensorFlow

首先引入tensorflow,簡寫爲tf。而後定義a\b兩個常量,將兩個向量加起來。

生成一個session會話,經過這個會話來計算結果。

import tensorflow as tf

a = tf.constant([1.0,2.0],name="a")
b = tf.constant([2.0,3.0],name="b")
result = a+b
sess = tf.Session()
print(sess.run(result))

TensorFlow計算模型-計算圖

計算圖是TensorFlow中最基本的一個概念,它的全部計算都會被轉化爲計算圖上的節點。

TensorFlow的名字已經說明了它最重要的概念-Tensor和Flow。

Tensor就是張量,張量這個概念在數學或者物理學中能夠有不一樣的解釋。TensorFlow中張量能夠被簡單地理解爲多維數組。

若是說TensorFlow的第一個詞Tensor代表了它的數據結構,那麼Flow則體現了它的計算模型。它直觀地表達了張量之間經過計算相互轉化的過程。

TensorFlow是一個經過計算圖的形式來表述計算的編程系統。它的每個計算都是計算圖上的一個節點,而節點之間的邊描述了計算之間的依賴關係

計算圖的使用

第一個階段須要定義計算圖中全部的計算。第二個階段爲執行計算。如下代碼給出了第一階段-計算定義的樣例

import tensorflow as tf

a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = a + b

TensorFlow會自動將定義的計算轉化爲計算圖上的節點。

在TensorFlow程序中,系統會自動維護一個默認的計算圖,經過tf.get_default_grapth函數能夠獲取當前默認的計算圖。

import tensorflow as tf

a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = a + b
# 經過a.graph能夠查看張量所屬的計算圖。由於沒有特地指定,因此這個計算圖應該等於當前默認的計算圖。
# 因此下面這個操做輸出的值爲True
print("a", a.graph is tf.get_default_graph())
print("b", b.graph is tf.get_default_graph())

除了使用默認的計算圖,TensorFlow支持經過tf.Graph函數來生成新的計算圖。不一樣計算圖上的張量和運算都不會共享

如下代碼示意瞭如何在不一樣計算圖上定義和使用變量

import tensorflow as tf

g1 = tf.Graph()
with g1.as_default():
    # 計算圖g1中定義變量v,並設置初始值爲0
    v = tf.get_variable("v", initializer=tf.zeros_initializer(), shape=[1])

g2 = tf.Graph()
with g2.as_default():
    # 計算圖g2中定義變量v,並設置初始值爲1
    v = tf.get_variable("v", initializer=tf.ones_initializer(), shape=[1])

# 計算圖g1中讀取變量v的取值
with tf.Session(graph=g1) as sess:
    tf.initialize_all_variables().run()
    with tf.variable_scope("", reuse=True):
        # 計算圖g1中,變量v的取值應該爲0,因此下面這行會輸出[0.]
        print(sess.run(tf.get_variable("v")))

# 計算圖g2中讀取變量v的取值
with tf.Session(graph=g2) as sess:
    tf.initialize_all_variables().run()
    with tf.variable_scope("", reuse=True):
        # 計算圖g2中,變量v的取值應該爲1,因此下面這行會輸出[1.]
        print(sess.run(tf.get_variable("v")))

上面代碼產生了兩個計算圖,每一個計算圖中定義了一個名字爲v的變量。

在計算圖g1中將v初始化爲0,在計算圖g2中,將v初始化爲1。能夠看到當運行不一樣計算圖時,v的值也是不同的。

計算圖不只僅能夠用來隔離張量和計算,還提供了管理張量和計算的機制。計算圖能夠經過tf.Graph.device函數來指定運行計算的設備。

爲TensorFlow使用GPU提供了機制。

有效的整理TensorFlow程序中的資源也是計算圖的一個重要功能。在一個計算圖中,能夠經過集合來管理不一樣類別的資源。

好比經過tf.add_to_collection函數能夠將資源加入一個或多個集合中,而後經過tf.get_collection獲取一個集合裏面的全部資源。

資源能夠是張量、變量、運行TensorFlow程序所須要的隊列資源等等。爲了方便使用,TensorFlow自動管理了一些最經常使用的集合

 

集合名稱:集合內容:使用場景

tf.GraphKeys.VARIABLES全部變量 :持久化TensorFlow模型

tf.GraphKeys.TRAINABLE_VARIABLES可學習的變量(通常指神經網絡中的參數):模型訓練、生成模型可視化內容

tf.GraphKeys.SUMMARIES  :日誌生成相關的張量:TensorFlow計算可視化

tf.GraphKeys.QUEUE_RUNNERS  :處理輸入的QueueRunner:輸入處理

 tf.GraphKeys.MOVING_AVERAGE_VARIABLES全部計算了滑動平均值的變量:計算變量的滑動平均值

張量

在TensorFlow程序中,全部的數據都經過張量的形式來表示。從功能的角度上看,張量能夠被簡單理解爲多維數組。

其中零階張量表示標量(scalar),也就是一個數。

第一階張量爲向量(vector),也就是一個一維數組。

第n階張量能夠理解爲一個n維數組。

但張量在TensorFlow中的實現並非直接採用數組的形式,只是對TensorFlow中運算結果的引用。

在張量中並無真正保存數字,它保存的是如何獲得這些數字的計算過程。

以加法爲例,當運行下面代碼時,並不會獲得加法的結果,而是獲得對結果的一個引用

import tensorflow as tf

# tf.constant是一個計算,這個計算的結果爲一個張量,保存在變量a中。
a = tf.constant([1.0, 2.0], name="a")
b = tf.constant([2.0, 3.0], name="b")
result = tf.add(a, b, name="add")
print(result)  # Tensor("add:0", shape=(2,), dtype=float32)

從上面代碼能夠看出TensorFlow中的張量和NumPy中的數組不一樣,TensorFlow計算的結果不是一個具體的數組,並且一個張量的結構。

從上面代碼的運行結果能夠看出,一個張量中主要保存了三個屬性:名字(name)、維度(shape)、類型(type) 

張量的第一個屬性,不只是標識符,也給出了這個張量是如何計算出來的。

張量和計算圖上節點所表明的計算結果是對應的。這樣張量的命名就能夠經過node:src_output的形式來給出。

其中node爲節點的名稱,src_output表示當前張量來自節點的第幾個輸出。好比:add:0,就說明這個張量是計算節點add輸出的第一個結果

張量的第二個屬性是張量的維度(shape),描述了一個張量的維度信息。好比:shape=(2.)就說明result是一個一維數組長度爲2

維度是張量一個很重要的屬性,圍繞張量的維度TensorFlow也給出了不少有用的運算

張量的第三個屬性是類型(type)每個張量會有一個惟一的類型。TensorFlow會對參與運算的全部張量進行類型的檢查,當發現類型不匹配時會報錯

若是不指定類型,會給出默認的類型。由於使用默認類型有可能會致使潛在的類型不匹配問題,因此通常建議經過指定dtype來明確指出變量或常量類型

a = tf.constant([1, 2], name="a", dtype=tf.float32)

TensorFlow支持14種不一樣的類型包括:實數(tf.float32\float64)、整數(tf.int8\int16\int32\int64\uint8)、布爾(tf.bool)、複數(tf.complex64\complex128)

張量的使用 

張量使用主要是能夠總結爲兩大類

第一類用途是對中間計算結果的引用。當一個計算包含不少中間結果時,使用張量能夠大大提升代碼的可讀性。

第二類用途是當計算圖構造完成以後,張量能夠用來得到計算結果,也就是真實的數字。雖然張量自己沒有存儲具體的數字,可是經過會話能夠獲得具體數字

能夠使用tf.Session().run(result)語句來獲得計算結果

會話

會話擁有並管理TensorFlow程序運行時的全部資源。當全部計算完成以後須要關閉會話來幫助系統回收資源,不然就可能出現資源泄露問題。

TensorFlow中使用會話的模式通常有兩種

第一種須要明確調用生成和關閉會話函數

import tensorflow as tf

# 建立會話
sess = tf.Session()
# 關閉會話
sess.close()

使用這種模式後,再全部計算完成以後須要明確調用Session.close函數來關閉會話並釋放資源。

然而,當程序由於異常而退出時,關閉會話的函數可能就不會被執行從而致使資源泄漏。

第二種使用Python的上下文管理器

import tensorflow as tf

# 建立會話,並經過Python上下文管理器來管理
with tf.Session() as sess:
    sess.run()

經過上下文管理器的機制,只要將全部計算放在with內部就能夠。當上下文管理器退出時候會自動釋放全部資源。

TensorFlow不會自動生成默認的會話,須要手動指定。當默認會話被指定以後能夠經過tf.Tensor.eval函數來計算一個張量的取值。

下面代碼展現了經過設定默認會話計算張量的取值。分隔符下方代碼也能夠完成相同的功能。

import tensorflow as tf

sess = tf.Session()
with sess.as_default():
    print(result.eval())
#########################################
sess = tf.Session()
print(sess.run(result))
print(result.eval(session=sess))

在交互式環境下,經過設置默認會話的方式來獲取張量的取值更加方便。

因此TensorFlow提供了一種在交互環境下直接構建默認會話的函數。這個函數就是tf.InteractiveSession。

使用這個函數會自動將生成的會話註冊爲默認會話。如下代碼展現了tf.InteractiveSession函數的用法。

import tensorflow as tf

sess = tf.InteractiveSession()
print(result.eval())
sess.close()

經過tf.InteractiveSession函數能夠省去將產生的會話註冊爲默認會話的過程。不管使用哪一種方式均可以經過ConfigProto來配置須要生成的會話

下面代碼就是經過ConfigProto配置會話的方法:

import tensorflow as tf

config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)
sess1 = tf.InteractiveSession(config=config)
sess2 = tf.Session(config=config)

經過ConfigProto能夠配置相似並行的線程數、GPU分配策略、運算超時時間等參數。

最常使用的有兩個。

第一個是allow_soft_placement布爾型參數,當他爲True時如下任意一個條件成立時,GPU上的運算能夠放到CPU上進行

1. 運算沒法再GPU上執行

2. 沒有GPU資源

3. 運算輸入包含對CPU計算結果的引用

參數的默認值爲False,可是爲了使得代碼的可移植性更強,在有GPU的環境下這個參數通常會被設置爲True

不一樣的GPU驅動版本可能對計算的支持有略微的區別。經過設置參數爲True,能夠自動調整到CPU上,而且可讓程序擁有不一樣數量的GPU機器上運行

第二個是log_device_placement布爾型參數,當他爲True時日誌中將會記錄每一個節點被安排在哪一個設備上以方便調試

而在生產環境中將這個參數設置爲False能夠減小日誌量。

實現神經網絡 

TensorFlow遊樂場:http://playground.tensorflow.org 

是一個經過網頁瀏覽器就能夠訓練的簡單神經網絡,並實現了可視化訓練過程的工具。

TensorFlow遊樂場左側提供了4個不一樣的數據集來測試神經網絡。

默認的數據爲左上角被框出來的那個。被選中的數據也會顯示在最右邊的OUTPUT欄目下。

這個數據中能夠看到一個二維平面上有藍色或者橘色的點,每個小點表明了一個樣例,而點的顏色表明了樣例的標籤

點的顏色只有兩種,因此這是一個二分類的問題。這裏舉一個例子來講明這個數據能夠表明的實際問題。

假設須要判斷某工廠生產的零件是否合格,那麼橘色的點能夠表示全部合格的零件而藍色的點表示不合格零件。

這樣判斷一個零件是否合格就變成了區分點的顏色。

爲了將一個實際問題對應到平面上不一樣顏色點的劃分,還須要將實際問題中的實體,好比上述的零件變成平面上的點。

這就是特徵提取解決的問題。以零件爲例,能夠用零件的長度和質量來大體描述一個零件。

這樣物理意義上的零件就能夠被轉化成長度和質量兩個數字。

機器學習中,全部用於描述實體的數字的組合就是一個實體的特徵向量(feature vector)

特徵向量的提取對機器學習的效果相當重要,經過特徵提取就能夠將實際問題中的實體轉化爲空間中的點。

假設使用長度和質量做爲一個零件的特徵向量,那麼每一個零件就是二維平面上的一個點。

TensorFlow遊樂場中FEATURES一欄對應了特徵向量,在上圖樣例中,能夠認爲x1表明一個零件的長度而x2表明零件的質量

特徵向量是神經網絡的輸入,神經網絡的主體結構顯示在上圖中間位置。目前主流的神經網絡都是分層的結構

第一層是輸入層,表明特徵向量中每個特徵的取值。好比,若是一個零件的長度是0.5,那麼x1的值就是0.5

同一層的節點不會相互鏈接,並且每一層和下一層的連接,直到最後一層做爲輸出層才能獲得計算的結果。

在二分類問題中,好比判斷零件是否合格,神經網絡的輸出層每每只包含一個節點,而這個節點會輸出一個實數值。

經過這個輸出值和一個事先設定的閾值,就能夠獲得最後的分類結果。

以判斷零件合格爲例,能夠認爲當輸出的數值大於0時,給出的判斷結果是零件合格,反之則零件不合格。

通常能夠認爲當輸出值離閾值越遠時獲得的答案越可靠。

在輸入和輸出層之間的神經網絡叫作隱藏層,通常一個神經網絡的隱藏層越多,這個神經網絡越深。

而所謂深度學習中的這個深度和神經網絡的層數也是密切相關的。能夠增長/減小隱藏層的數量,控制神經網絡的深度

使用神經網絡解決分類問題主要能夠分爲如下4個步驟:

1. 提取問題中實體的特徵向量做爲神經網絡的輸入。不一樣的實體能夠提取不一樣的特徵向量。

2. 定義神經網絡的結構,並定義如何從神經網絡的輸入獲得輸出,這個過程就是神經網絡的前向傳播算法

3. 經過訓練數據來調整神經網絡中參數的取值,這就是訓練神經網絡的過程。

4. 使用訓練好的神經網絡來預測未知的數據

前向傳播算法簡介

爲了介紹神經網絡的前向傳播算法,須要先了解神經元的結構。

神經元是構成一個神經網絡的最小單元,一個神經元有多個輸入和一個輸出。

每一個神經元的輸入既能夠是其餘神經元的輸出,也能夠是整個神經網絡的輸入。

所謂神經網絡的結構就是指不一樣神經元之間的連接結構。

最簡單的神經元結構的輸出就是全部輸入的加權和,而不一樣的輸入的權重就是神經元的參數。

神經網絡的優化過程就是優化神經元中參數取值的過程。

計算神經網絡的前向傳播結果須要三部分信息

 

第一部分:神經網絡的輸入,這個輸入就是從實體中提取的特徵向量。好比上圖中有兩個輸入。一個是零件的長度x1,一個是零件的質量x2。

第二部分:神經網絡的連接結構,神經網絡是由神經元構成的,神經網絡的結構給出不一樣神經元之間輸入輸出的鏈接關係。神經元也稱之爲節點。

圖中a11節點有兩個輸入,分別是x1,x2的輸出。而a11的輸出則是節點y的輸入。最後一個部分是每一個神經元中的參數。

第三部分:每一個神經元中的參數。用W來表示神經元中的參數,W上標代表了神經網絡的層數,下標代表了鏈接編號。

W用來表示神經元中的參數,W上標表示神經網絡的層數,W1表示第一層節點的參數。W2表示第二層節點的參數。

W的下標代表了鏈接節點編號,好比W(1)1,2 表示鏈接x1和a12節點的邊上的權重。

提供神經網絡的輸入,神經網絡的結構以及邊上權重,就能夠經過前向傳播算法來計算出神經網絡的輸出。

 

從輸入層開始一層一層地使用向前傳播算法,首先隱藏層有三個節點,每個節點的取值都是輸入層取值的加權和。

下面就是a11取值的詳細計算過程:

 

在獲得第一層節點的取值以後,能夠進一步推到獲得輸出層的取值。相似輸出值就是第一層的加權和。

由於輸出值大於閾值0,因此這個樣例最後的答案是合格。

這就是前向傳播算法,能夠表示爲矩陣乘法,將輸入x1,x2組成一個1*2的矩陣X=[x1,x2],而W(1)組成一個2*3的矩陣

 

這樣經過矩陣乘法能夠獲得隱藏層的三個節點所組成的向量取值。

 

這樣前向傳播算法就是兩個矩陣乘積一致了。使用tf.matmul(x,w1) 能夠計算矩陣乘積

矩陣乘積:只有在第一矩陣列數和第二矩陣行數一致時纔有用(中間兩位必須一致),會生成第一矩陣行數、第二矩陣列數的另外一個矩陣(生成兩端規格的矩陣)。

神經網絡參數與TensorFlow變量

神經網絡中的參數是神經網絡實現分類或者回歸問題中重要的部分。

在TensorFlow中變量(tf.Variable)的做用就是保存和更新神經網絡中的參數。變量也須要指定初始值。

由於在神經網絡中,給參數賦值隨機初始值最爲常見,通常也使用隨機數給變量初始化。

例如聲明一個2*3的矩陣變量的方法:

a = tf.Variable(tf.random_normal([2, 3], stddev=2))

調用了變量聲明函數tf.Variable,在變量聲明函數中給出了初始化這個變量的方法。

變量的初始值能夠設置成隨機數、常數或者經過其餘變量的初始值計算獲得的。

tf.random_normal會產生一個2*3的矩陣,矩陣中的元素是均值爲0,標差爲2的隨機數。

該函數能夠經過mean來指定平均值,在沒有指定時默認爲0。經過知足正態分佈的隨機數來初始化神經網絡的參數。

目前支持的隨機數生成函數以下

函數名稱 隨機數分佈 主要參數
tf.random_normal 正態分佈 平均值、標準差、取值類型
tf.truncated_normal 正態分佈 同上
tf.random_uniform 均勻分佈 最小、最大取值、取值類型
tf.random_gamma Gamma分佈 形狀參數、alpha尺度參數、beta取值類型

 

 

 

 

 

TensorFlow也支持經過常數來初始化一個變量,下面給出了TensorFlow中經常使用的常量聲明方法

函數名稱 功能 樣例
tf.zeros 全0的數組 tf.zeros([2,3],int32)
tf.ones 全1的數組 tf.ones([2,3],int32)
tf.fill 所有爲指定數字的數組 tf.fill([2,3],9)
tf.constant 給定值的常量 tf.constant([2,3])

 

 

 

 

 

神經網絡中,偏置項一般會使用常數來設置初始值。例如

biases = tf.Variable(tf.zeros([3]))

這段代碼將會生成一個初始值所有爲0且長度爲3的變量。

偏置項:指望輸出與真實標記的差,若是此偏離程度太小,則會致使過擬合的發生,由於可能將訓練集中的噪聲也學習了。

除了使用隨機數或者常數,TensorFlow也支持經過其餘變量的初始值來初始化新的變量。

weights = tf.Variable(tf.random_normal([2, 3], stddev=2))
w2 = tf.Variable(weights.initial_value())
w3 = tf.Variable(weights.initial_value() * 2.0)

上面代碼中,w2的初始值被設置成了與weights變量相同。w3的初始值則是weights初始值的兩倍。

TensorFlow中,一個變量的值在被使用以前,這個變量的初始化過程須要被明確地調用。

下面代碼介紹如何經過變量實現神經網絡的參數並實現前向傳播的過程

import tensorflow as tf

# 聲明w一、w2兩個變量。這裏還經過seed參數設定了隨機種子
# 這樣能夠確保每次運行獲得的結果是同樣的
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))

# 將輸入的特徵向量定義爲一個常量
x = tf.constant([[0.7, 0.9]])

# 前向傳播算法得到神經網絡的輸出
# 定義矩陣乘積a,x * w1
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)

# 建立會話
with tf.Session() as sess:
    # 由於w1和w2都尚未運行初始化過程,下面的兩行分別初始化了w1和w2兩個變量
    sess.run(w1.initializer)  # 初始化w1
    sess.run(w2.initializer)  # 初始化w2
    print("y=", sess.run(y))  # y= [[ 3.95757794]]

當聲明變量w一、w2以後,能夠經過w1\2來定義神經網絡的前向傳播過程,並獲得中間結果a和最後答案z

TensorFlow提供了一種更加便捷的方式來完成變量初始化的過程, 

# 建立會話
with tf.Session() as sess:
    # 由於w1和w2都尚未運行初始化過程,下面的兩行分別初始化了w1和w2兩個變量
    sess.run(tf.initialize_all_variables())
    print("y=", sess.run(y))  # y= [[ 3.95757794]]

經過initialize_all_variables就能夠初始化全部變量,也會自動處理變量之間的依賴關係

可是在推薦使用tf.global_variables_initializer()來代替initialize_all_variables。

在TensorFlow中,變量的聲明函數tf.Variable是一個運算,這個運算輸出的結果就是一個張量,這個張量也就是變量。因此變量只是一種特殊的張量。

全部的變量都會被自動的加入GraphKeys.VARIABLES這個集合。經過tf.all_variables函數能夠拿到當前計算圖上全部變量。

拿到計算圖上全部的變量有助於持久化整個計算圖的運行狀態。

當構建機器學習模型時,能夠經過變量聲明函數中的trainable參數來區分須要優化的參數(神經網絡中的參數)和其餘參數(迭代的輪數)。

若是聲明變量時參數trainable爲True,那麼這個變量將會被加入GraphKeys.TRAINABLE_VARIABLES集合。

在TensorFlow中能夠經過tf.trainable_variables函數獲得全部須要優化的參數。

TensorFlow的神經網絡優化算法會將GraphKeys.TRAINABLE_VARIABLES集合中的變量做爲默認優化對象。

相似張量、維度和類型也是變量最重要的兩個屬性。變量的類型不可改變。

維度是變量另外一個重要的屬性,維度在程序運行中是有可能改變的,可是須要經過設置參數validate_shape=False

w1 = tf.Variable(tf.random_normal([2, 3], stddev=1), name="w1")
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1), name="w2")
tf.assign(w1, w2, validate_shape=False)

訓練神經網絡模型 

使用監督學習的方式設置神經網絡參數須要一個標註好的訓練數據集。以判斷零件是否合格爲例

這個標註好的訓練數據集就是收集的一批合格零件和一批不合格零件

監督學習最重要的思想就是,在已知答案的標註數據上,模型給出的預測結果要儘可能接近真實的答案。

經過調整神經網絡中的參數對訓練數據進行擬合。能夠使得模型對未知的樣本提供預測的能力。

在神經網絡優化算法中,最經常使用的方法是反向傳播算法。

 

反向傳播算法實現了一個迭代的過程,在每次迭代的開始,首先須要選取一小部分訓練數據,這一小部分數據叫作batch。

而後batch的樣例會經過前向傳播算法獲得神經網絡模型的預測結果。由於訓練數據都是有正確答案標註的。

因此能夠計算出當前神經網絡模型的預測答案與正確答案之間的差距

最後基於這預測值和真實值之間的差距,反向傳播算法會相應更新神經網絡參數的取值

使得在這個batch上神經網絡模型的預測結果和真實答案更加接近。

第一步是使用TensorFlow表達一個batch的數據。可是若是使用常量表達式,在每輪迭代中選取的數據都要經過常量來表達。

那麼TensorFlow的算圖將會太大,由於每生成一個常量,都會在計算圖中增長一個節點。

通常來講一個神經網絡的訓練過程當中須要通過幾百萬輪甚至幾億輪的迭代。這樣計算圖就會很是龐大,並且利用率很低。

placeholder

爲了不這個問題,TensorFlow提供了placeholder機制用於提供輸入數據。placeholder至關於定義了一個位置,這個位置中的數據在程序運行時再指定。

這樣在程序中就不須要生成大量常量來提供輸入數據,而只須要將數據經過placeholder傳入計算圖便可。

在定義時,這個位置上的數據類型是須要指定的。是不可改變的,維度能夠根據數據推導得出,不必定要給出。

import tensorflow as tf

w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, dtype=tf.float32))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, dtype=tf.float32))

# 定義placeholder做爲存放輸入數據的地方。維度不必定要定義
x = tf.placeholder(tf.float32, shape=(1, 2), name="input")
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)

with tf.Session() as sess:
    sess.run(tf.initialize_all_variables())
    print(sess.run(y, feed_dict={x: [[0.7, 0.9]]})) # [[ 3.65784049]]

這段程序中替換了原來經過常量定義的x。在新的程序中計算前向傳播結果時,須要提供一個feed_dict來指定x的取值。

feed_dict是一個字段,字段中須要給出每一個用到placeholder的取值,若是某個須要的placeholder沒有被指定取值,那麼會報錯。

在訓練神經網絡時須要每次提供一個batch的訓練樣例。須要在將1*2矩陣改成n*2的矩陣,那麼久能夠獲得n個樣例的前向傳播結果了。

其中n*2矩陣的每一行爲一個樣例數據。前向傳播的結果爲n*1的矩陣,每一行就表明了一個樣例的前向傳播結果

import tensorflow as tf

w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, dtype=tf.float32))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, dtype=tf.float32))

# 定義placeholder做爲存放輸入數據的地方。維度不必定要定義
x = tf.placeholder(tf.float32, shape=(3, 2), name="input")
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)

with tf.Session() as sess:
    sess.run(tf.initialize_all_variables())
    print(sess.run(y, feed_dict={x: [[0.7, 0.9], [0.1, 0.4], [0.5, 0.8]]}))
    # [[ 0.69916689]
    # [ 0.07952753]
    # [ 0.48762131]]

樣例展現了一次性計算多個樣例的前向傳播結果。運行時須要將3個樣例組成一個3*2的矩陣傳入placehodler。

計算的到的結果爲3*1的矩陣,其中每一個結果對應每一個樣例的前向傳播結果。例如[0.69]對應[0.7,0.9]的前向傳播結果

在獲得一個batch的前向傳播結果以後,須要定義一個損失函數來刻畫當前的預測值和真實答案之間的差距。

而後經過反向傳播算法來調整神經網絡參數的取值使得差距能夠被縮小。 下面代碼定義了一個簡單的損失函數,並定義了反向傳播算法

# 定義損失函數和反向傳播算法
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
train_step = tf.train.AdamOptimizer(0.001).minimize(cross_entropy)

cross_entropy定義了真實值和預測值之間的交叉熵,這是分類問題中一個經常使用的損失函數。

第二行train_step定義了反向傳播的優化方式。目前支持7中不一樣的優化器,能夠根據具體的應用選擇不一樣的優化器

經常使用的優化方法有三種:

1. tf.train.GradientDescentOptimizer

2. tf.train.AdamOptimizer

3. tf.train.MomentumOptimizer

定義了反向傳播算法以後,經過運行sess.run(train_step)就能夠對全部變量進行優化。使得在當前batch下損失函數更小

完整示例 

import tensorflow as tf
# NumPy 是一個科學計算的工具包,用來生成模擬數據集
from numpy.random import RandomState

# 定義訓練數據集batch的大小
batch_size = 8
# 定義神經網絡的參數
w1 = tf.Variable(tf.random_normal([2, 3], stddev=1, seed=1))
w2 = tf.Variable(tf.random_normal([3, 1], stddev=1, seed=1))

# 在shape的一個維度上使用None能夠方便使用不一樣大小的batch,訓練時須要把數據分紅比較小的batch
# 可是在測試時能夠一次性使用所有的數據,當數據集比較小時這樣比較方便測試。但數據集比較大時,大量數據放在batch可能會溢出
x = tf.placeholder(tf.float32, shape=(None, 2), name="x-input")
y_ = tf.placeholder(tf.float32, shape=(None, 1), name="y-input")

# 定義神經網絡前向傳播的過程
a = tf.matmul(x, w1)
y = tf.matmul(a, w2)

# 定義損失函數和反向傳播算法
cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))
train_step = tf.train.AdamOptimizer(0.001).minimize(cross_entropy)

# 經過隨機數生成一個模擬數據集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2)
# 定義規則來給出樣本的標籤,這裏全部x1+x2<1的樣例都被認爲是正樣本(合格)
# 而其餘爲負樣本(不合格),和TensorFlow遊樂場中表示法不大同樣的地方是
# 這裏使用0來表示負樣本,1來表示正樣本。大部分解決分類問題的神經網絡都會採用採用0和1的表示方法
Y = [[int(x1 + x2 < 1)] for (x1, x2) in X]

# 建立一個會話來運行程序
with tf.Session() as sess:
    sess.run(tf.initialize_all_variables())
    print("\n")
    print("w1:", sess.run(w1))
    print("w2:", sess.run(w2))
    # 訓練以前的數據
    # w1: [[-0.81131822  1.48459876  0.06532937]
    #      [-2.4427042   0.0992484   0.59122431]]
    # w2: [[-0.81131822]
    #      [1.48459876]
    #      [0.06532937]]

    # 設定訓練的輪數
    STEPS = 5000
    for i in range(STEPS):
        # 每次選取batch_size個樣本進行訓練。
        start = (i * batch_size) % dataset_size
        end = (i * batch_size) % dataset_size + batch_size
        # 經過選取的樣本訓練神經網絡並更新參數
        sess.run(train_step, feed_dict={x: X[start:end], y_: Y[start:end]})

        # 每隔一段時間計算在全部數據上的交叉熵並輸出
        if i % 1000 == 0:
            total_cross_entropy = sess.run(cross_entropy, feed_dict={x: X, y_: Y})
            print("After %d training step(s), cross entropy on all data is %g" % (i, total_cross_entropy))

    # 輸出訓練後的參數取值。
    print("\n")
    print("w1:", sess.run(w1))
    print("w2:", sess.run(w2))
# w1: [[-1.9618274   2.58235407  1.68203771]
#  [-3.46817183  1.06982327  2.11788988]]
# w2: [[-1.8247149 ]
#  [ 2.68546653]
#  [ 1.41819501]] 

上面的程序實現了訓練神經網絡的所有過程,從這段程序能夠總結出訓練神經網絡的過程能夠分爲三步

1. 定義神經網絡的結構和前向傳播輸出的結果。

2. 定義損失函數以及選擇反向傳播優化的算法

3. 生成會話,而且在訓練數據上反覆運行反向傳播優化算法。

不管神經網絡的結構如何變化,這三個步驟是不變的。 

隨機種子(題外話RandomState構造參數就是隨機種子):

1.計算機的僞隨機數是由隨機種子根據必定的計算方法計算出來的數值。因此,只要計算方法必定,隨機種子必定,那麼產生的隨機數就是固定的。
2.只要用戶或第三方不設置隨機種子,那麼在默認狀況下隨機種子來自系統時鐘。

深層神經網絡與深度學習

深度學習的精肯定義爲:一類經過多層非線性變換對高複雜性數據建模算法的合集

由於深層神經網絡是實現「多層非線性變換」最經常使用的一種方法,因此在實際中基本上能夠認爲深度學習就是深層神經網絡的代名詞。

深度學習有兩個很是重要的特性-多層和非線性

線性模型的侷限性

∑ 西格瑪,總和符號。例如∑Pi 其中i=1,2, 那麼就是求P1+P2的總和。∑下面的數字表示從幾開始求和,上面的數字表示求和到幾截止。


在線性模型中,模型的輸出爲輸入的加權和。假設一個模型的輸出y和輸入xi知足y=∑iwixi+b,那麼就是線性模型

其中wi,b € R 爲模型的參數。當模型的輸入只有一個時,x和y造成了二維座標系上的一條直線。

當模型有n個輸入時,x和y造成了n+1維空間中的一個平面。而一個線性模型中經過輸入獲得輸出的函數爲線性變換。

最大特色是任意線性模型的組合仍然仍是線性模型。前向傳播算法實現的就是一個線性模型。

公式爲:a(1) = xW(1), y = a(1)W(2)

x爲輸入,W爲參數。經過上面公式獲得整個模型的輸出爲: y=(xW(1))W(2) 根據矩陣乘法的結合律: y=x(W(1)W(2)) = xW`

W(1)W(2)其實能夠被表示爲一個新的參數W`。這樣輸入和輸出的關係就能夠表示爲

y=xW` = [x1 x2][W1` W2`]=[W1`x1 + W2`x2]

其中W`是新的參數,這個前向傳播的算法徹底符合線性模型的定義。雖然神經網絡有兩層,可是它和單層的神經網絡沒有區別

只經過線性變換,任意層的全鏈接神經網絡和單層神經網絡的表達能力沒有任何區別,並且都是線性模型

線性模型只能解決線性可分問題,因此在深度學習的定義中特地強調它的目的爲解決更加複雜的問題

激活函數實現去線性化

若是將每一條神經元的輸出經過一個非線性函數,那麼整個神經網絡的模型也就再也不是線性的了。

這個非線性函數就是激活函數。

偏置項能夠被表達爲一個輸出永遠爲1的節點,是神經網絡中很是經常使用的一種結構。

目前TensorFlow提供了7種不一樣的非線性激活函數,tf.nn.relu、tf.sigmoid和tf.tanh是比較經常使用的幾個。

也支持自定義激活函數,下面代碼實現了神經網絡的前向傳播算法。

a = tf.nn.relu(tf.matmul(x, w1) + biases1)

多層網絡解決異或運算

神經網絡的發展史上,一個很重要的問題就是異或問題。神經網絡的理論模型由Warren McCulloch和Water Pitts在1943年首次提出

並在1958年由Frank Rosenblatt提出了感知機(perceptron)模型,從數學上完成了對神經網絡的精確建模。

感知機能夠簡單地理解爲單層的神經網絡,感知機會先將輸入進行加權和,而後經過激活函數最後獲得輸出。

這就是沒有隱藏層的神經網絡。在1960年代,神經網絡做爲對人類大腦的模擬算法受到了不少關注。

然而到了1969年Marvin Minsky和Seymour Papert在書中提出感知機是沒法模擬異或運算的。

異或運算直觀來講就是若是兩個輸入的符合相同時,則輸出爲0,不然輸出爲1.

當加入隱藏層以後,異或問題就能夠獲得很好的解決。深層神經網絡實際上有組合特徵提取的功能,對於解決不易提取特徵向量的問題有很大幫助。

損失函數定義

神經網絡模型的效果以及優化的目標是經過損失函數(loss function)來定義的。

分類問題和迴歸問題是監督學習的兩大種類。

分類問題但願解決的是將不一樣的樣本分到事先定義好的類別中。好比零件是否合格,就是一個二分類。

在解決判斷零件是否合格的二分類問題時,定義過一個有單個輸出節點的神經網絡。

當節點的輸出越接近0時,這個樣本越有可能不合格,反之若是輸出越接近1,則這個樣本越有可能合格。

然而這種作法並不容易直接推廣到多分類的問題,雖然多個閾值理論上是能夠的,可是通常不這麼處理。

經過神經網絡解決多分類的問題最經常使用的就是設置n個輸出節點,其中n爲類別的個數。

對於每個樣例,神經網絡能夠獲得一個n維數組做爲輸出結果,數組中的每個維度對應一個類別。

在理想狀況下,若是一個樣本屬於類別k,那麼這個類別對應的輸出節點的輸出值應該爲1,而其餘節點的輸出都爲0.

以識別數字1爲例,神經網絡模型的輸出結果越接近[0,1,0,0,0,0,0,0,0]越好。

那麼如何判斷一個輸出向量和指望的向量有多接近呢?

交叉熵(cross entropy)是經常使用的評判方法之一,交叉熵刻畫了兩個機率分佈之間的距離,是分類問題中使用比較廣的一種損失函數。

交叉熵是一個信息論的概念,它本來是用來估算平均編碼的長度。

給定兩個機率分佈p和q,經過q表示p的交叉熵爲:H(p,q) = -∑xp(x)log q(x)

注意交叉熵刻畫的是兩個機率分佈之間的距離,然而神經網絡的輸出卻不必定是一個機率分佈。

機率分佈刻畫了不一樣事件發生的機率,當事件總數是有限的狀況下,機率分佈函數p(X = x) 知足:∑ p(X = x) = 1

任意事件發生的機率都是在0和1之間,且總有某一個事件發生機率的和爲1.

若是將分類問題中「一個樣例屬於某一個分類」當作一個機率事件,那麼訓練數據的正確答案就符合一個機率分佈。

由於事件「一個樣例屬於不正確的類別」的機率爲0,而「一個樣例屬於正確的類別」的機率爲1.

如何將神經網絡前向傳播獲得的結果也變成機率分佈呢?Softmax迴歸就是一個很是經常使用的方法。

Softmax迴歸自己能夠做爲一個學習算法來優化分類結果,可是在TensorFlow中,Softmax迴歸的參數被去掉了

它只是一層額外的處理層,將神經網絡的輸出變成了一個機率分佈。

 

假設原始的神經網絡輸出爲y1,y2,那麼通過Softmax迴歸處理以後的輸出爲:

 

原始神經網絡的輸出被用做置信度來生成新的輸出,而新的輸出知足機率分佈的全部要求。

這個新的輸出能夠理解爲通過神經網絡的推導,一個樣例爲不一樣類別的機率分別是多大。

這樣就把神經網絡的輸出也變成了一個機率分佈,從而能夠經過交叉熵來計算預測的機率分佈和真實答案的機率分佈之間的距離了。

交叉熵函數不是對稱的,它刻畫的是經過幾率分佈q來表達機率分佈p的困難程度。

由於正確答案是但願獲得的結果,因此當交叉熵做爲神經網絡的損失函數時,p表明的是正確答案,q表明的是預測值。

交叉熵刻畫的是兩個是兩個機率分佈的距離,也就說交叉熵值越小,兩個機率分佈越接近。

假設有一個三分類問題,某個樣例的正確答案是(1,0,0),某個模型通過Softmax迴歸以後的預測答案是(0.5,0.4,0.1)

那麼這個預測和正確的答案之間的交叉熵爲:H((1,0,0),(0.5,0.4,0.1)) = -(1*log0.5+0*log0.4+0*log0.1) ≈ 0.3

log對數:指10的平方次數。 例如:103=1000,log(1000)=3。log(4) = 0.602 = 100.602

若是另外一個模型的預測是0.8,0.1,0.1,那麼這個預測值和真實值之間的交叉熵爲:H(p,q) = -∑xp(x)log q(x)

H((1,0,0),(0.8,0.1,0.1)) = -(1*log0.8+0*log0.1+0*log0.1) ≈ 0.1

很容易的知道第二個預測答案要優於第一個。經過交叉熵計算獲得的結果也是一致的。實現代碼爲:

cross_entropy = -tf.reduce_mean(y_ * tf.log(tf.clip_by_value(y, 1e-10, 1.0)))

1e-10:就是"aeb"的形式表示1*10-10,e表明10,前面能夠是小數,後面必須是整數。2e10就是2*1010

其中y_表明正確結果,y表明預測結果。這一行代碼包含了四個不一樣的TensorFlow運算。

第一個運算經過tf.clip_by_value函數能夠將一個張量中的數值限制在一個範圍以內,這樣能夠避免一些運算錯誤。

下面給出tf.clip_by_value的簡單樣例

v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
result = tf.clip_by_value(v, 2.5, 4.5)
with tf.Session() as sess:
    print(sess.run(result))
# [[ 2.5  2.5  3. ]
#  [ 4.   4.5  4.5]]

小於2.5的數都被換成了2.5,大於4.5的數都被換成了4.5.這樣就能夠保證在進行log運算時,不會出現log(0)這樣的錯誤或者大於1的機率。

第二個運算是tf.log函數,這個函數完成了對張量中全部元素的依次求天然對數的功能,示例代碼

天然對數:咱們稱以10爲底的對數叫作經常使用對數。以無理數e(e=2.71828...)爲底的對數稱爲天然對數,並記爲ln。

v = tf.constant([1.0, 2.0, 3.0])
result = tf.log(v)
with tf.Session() as sess:
    print(sess.run(result))
    # [ 0.  0.69314718  1.09861231]

第三個運算是乘法,在交叉熵的代碼中直接將兩個矩陣經過*操做相乘。這個操做不是矩陣乘法,而是元素之間直接相乘。

import tensorflow as tf

v1 = tf.constant([[1.0, 2.0], [3.0, 4.0]])
v2 = tf.constant([[5.0, 6.0], [7.0, 8.0]])
with tf.Session() as sess:
    print("元素相乘", sess.run(v1 * v2))
    # [[5.  12.]
    #  [21.  32.]]
    print("矩陣乘積", sess.run(tf.matmul(v1, v2)))
    # [[19.  22.]
    #  [43.  50.]]

經過上面這三個運算完成了對於每個樣例中的每個類別交叉熵p(x) log q(x) 的計算。這三步計算獲得一個n*m二維矩陣的結果。

其中n爲一個batch中樣例的數量,m爲分類的類別數量。

最後一步根據交叉熵公式,應該將每行中的m個結果相加獲得全部樣例的交叉熵。

而後再對這個n行取平均獲得一個batch的平均交叉熵。但由於分類問題的類別數量是不變的,因此能夠直接對整個矩陣作平均而並不改變計算結果的意義。

import tensorflow as tf

v = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
result = tf.reduce_mean(v)
with tf.Session() as sess:
    print(sess.run(result))  #3.5

由於交叉熵通常會與softmax迴歸一塊兒使用,因此TensorFlow對這兩個功能進行了統一封裝,並提供了tf.nn.softmax_cross_entropy_with_iogits函數

好比能夠直接經過下面的代碼來實現使用softmax迴歸以後的交叉熵損失函數:

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(labels=y_, logits=y)

其中y表明了原始神經網絡的輸出結果,而y_給出了標準答案。這樣經過一個命令就能夠獲得使用了Softmax迴歸以後的交叉熵。

只有一個正確答案的分類問題中,TensorFlow提供了tf.nn.sparse_softmax_cross_entropy_with_logits函數來進一步加速計算過程。

labels 表示正確數據、logits表示未通過softmax迴歸的預測數

均方偏差

與分類問題不一樣,迴歸問題解決的是對具體數值的預測。好比房價預測、銷量預測這些都是迴歸問題。

迴歸問題須要預測的不是一個事先定義好的類別,而是任意實數。

解決迴歸問題的神經網絡通常只有一個輸出節點,這個節點的輸出值就是預測值。

對於迴歸問題,最經常使用的損失函數是均方偏差(MSE,mean squared error)

 

其中yi爲一個batch中第i個數據的正確答案,而y`i爲神經網絡給出的預測值。下面是均方偏差損失函數的例子:

mse = tf.reduce_mean(tf.square(y_ - y ))

其中y表明了神經網絡的輸出答案,y_表明了標準答案。這裏的減法也是兩個矩陣的對應元素減法。

自定義損失函數

預測商品銷量時,若是預測多了(預測值比真實銷量大),商家損失的是生產商品的成本;而若是預測少了(預測值比真實銷量小)損失的則是商品的利潤。

由於通常商品的成本和商品的利潤不會嚴格相等,因此均方偏差損失函數就不可以很好的最大化銷售利潤。

好比一個商品的成本是1元,可是利潤是10元,那麼少預測一個就少賺10元;多預測一個才少賺1元。

若是神經網絡模型最小化的是均方偏差,那麼頗有可能此模型就沒法最大化預期的利潤。

爲了最大化預期利潤,須要將損失函數和利潤直接聯繫起來。注意損失函數定義的是損失,因此要將利潤最大化。

定義的損失函數應該刻畫成本或者代價。下面的公式給出了一個當預測多餘真實值和預測少於真實值時有不一樣損失係數的損失函數

 

yi爲一個batch中第i個數據的正確答案,y`i爲神經網絡獲得的預測值,a和b是常量。

a等於10(實際值多於預測值的代價),而b等於1(實際值少於預測值的代價)

經過對這個自定義損失函數的優化,模型提供的預測值更有可能最大化收益。

在TensorFlow中,能夠經過如下代碼來實現這個損失函數

loss = tf.reduce_sum(tf.where(tf.greater(v1, v2), (v1 - v2) * a, (v2 - v1) * b))

上面的代碼用到了tf.greater和tf.where來實現選擇操做。tf.greater的輸入是兩個張量,此函數會比較兩個輸入張量中每個元素的大小,返回比較結果。

當tf.greater的輸入張量維度不同時,TensorFlow會進行相似NumPy廣播操做(broadcasting)的處理。

tf.where函數有三個參數,第一個爲選擇條件根據,當選擇條件爲True時,tf.where會選擇第二個參數中的值,不然使用第三個的值。

注意tf.select函數判斷和選擇都是在元素級別進行,如下代碼展現了tf.select函數和tf.greater函數的用法。

v1 = tf.constant([[5.0, 2.0], [3.0, 4.0]])
v2 = tf.constant([[4.0, 3.0], [2.0, 1.0]])

with tf.Session() as sess:
    print(sess.run(tf.greater(v1, v2)))
    # [[True False]
    # [True  True]]
    print(sess.run(tf.where(tf.greater(v1, v2), v1, v2)))
    # [[5.  3.]
    # [3.  4.]]

定義了損失函數以後,下面將經過一個簡單的神經網絡程序來說解損失函數對模型訓練結果的影響。

下面程序,實現了一個擁有兩個輸入節點、一個輸出節點,沒有隱藏層的神經網絡。

import tensorflow as tf
from numpy.random import RandomState

batch_size = 8
# 兩個輸入節點
x = tf.placeholder(tf.float32, shape=(None, 2), name='x-input')
# 迴歸問題通常只有一個輸出節點
y_ = tf.placeholder(tf.float32, shape=(None, 1), name='y-input')

# 定義了一個單層的神經網絡前向傳播的過程,這裏就是加權和
w1 = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w1)

# 定義預測多了和預測少了的成本
loss_less = 10
loss_more = 1
loss = tf.reduce_sum(tf.where(tf.greater(y, y_), (y - y_) * loss_more, (y_ - y) * loss_less))
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)

# 經過隨機數生成一個模擬數據集
rdm = RandomState(1)
dataset_size = 128
X = rdm.rand(dataset_size, 2)

# 設置迴歸的正確值爲兩個輸入的和加上一個隨機量,之因此要加上隨機量是爲了加入不可預測的噪音,
# 不然不一樣損失函數的意義就不大了,由於不一樣損失函數都會在能徹底預測正確的時候最低。
# 通常來講噪音爲一個均值爲0的小量,因此這裏的噪音設置爲-0.05 ~ 0.05的隨機數
Y = [[x1 + x2 + rdm.rand() / 10.0 - 0.05] for (x1, x2) in X]

# 訓練神經網絡
with tf.Session() as sess:
    sess.run(tf.initialize_all_variables())
    STEPS = 5000
    for i in range(STEPS):
        start = (i * batch_size) % dataset_size
        end = min(start + batch_size, dataset_size)
        sess.run(train_step, feed_dict={x: X[start:end], y_: Y[start:end]})
        if i % 5 == 0:
            print("w1:", sess.run(w1))

運行上面的代碼會的到w1的值[[ 1.01934695][ 1.04280889]],也就是說獲得的預測函數是1.02x1+1.04x2,這要比x1+x2大,

由於在損失函數中指定預測少了的損失更大(loss_less>loss_more)。

使用這個損失函數會盡可能讓預測值離標準答案更近,經過這個樣例能夠感受到,對於相同的神經網絡,不一樣的損失函數會對訓練獲得的模型產生重要影響。

神經網絡優化算法

假設用Θ表示神經網絡中的參數,J(Θ)表示在給定的參數取值下,訓練數據集上損失函數的大小,那麼整個優化過程能夠抽象爲尋找參數Θ,使得j(Θ)最小。

由於目前沒有一個通用的方法能夠對任意損失函數直接求解最佳的參數取值,在實踐中,梯度降低算法是最經常使用的神經網絡優化方法。

梯度降低算法會迭代式更新參數Θ,不斷沿着梯度的反方向讓參數朝着總損失更小的方向更新。

x軸表示參數Θ取不一樣值時,對應損失函數J(Θ)的大小。假設當前的參數和損失之對應圖中小圓點位置,那麼梯度降低算法會將參數向x軸左側移動,從而使得小圓點朝着箭頭的方向移動。

參數的梯度能夠經過求偏導的方式計算,對於參數Θ,其梯度爲有了梯度,還須要定義一個學習率 η 來定義每次參數更新的幅度。

從直觀上理解能夠認爲學習率定義的就是每次參數移動的幅度。經過參數的梯度和學習率,參數更新的公式爲:

下面給出了一個具體的例子來講明梯度降低算法是如何工做的。假設要經過梯度降低算法來優化參數x,使得損失函數J(x)=x2的值儘可能小。梯度降低算法的第一步須要隨機產生一個參數x的初始值,而後再經過梯度和學習率來更新參數x的取值。在這個樣例中,參數x的梯度爲那麼使用梯度降低算法每次對參數x的更新公式爲假設參數的初始值爲5,學習率爲0.3,那麼這個優化過程能夠總結爲下表

輪數 當前輪參數值 梯度*學習率  更新後參數值
1 5 2*5*0.3=3 5-3=2
2 2 2*2*0.3=1.2 2-1.2=0.8
3 0.8 2*0.8*0.3=0.48 0.8-0.48=0.32
4 0.32 2*0.32*0.3=0.192 0.32-0.192=0.128
5 0.128 2*0.128*0.3=0.0768 0.128-0.0768=0.0512

 

 

 

 

 

 

從表中能夠看出,通過5此迭代後,參數x的值變成了0.0512,這個和參數最優值0已經比較接近了。

神經網絡的優化過程能夠分爲兩個階段,第一個階段先經過前向傳播算法計算獲得預測值,並將預測值和真實值作比對獲得二者之間的差距。而後第二個階段經過反向傳播算法計算損失函數對每個參數的梯度,再根據梯度和學習率使用梯度降低算法更新每個參數。

須要注意的是,梯度降低算法並不能保證被優化的函數達到全局最優解。例以下圖

圖中給出的函數就有可能只能獲得局部最優解而不是全局最優解。在黑點處,損失函數的偏導爲0,因而參數就不會再進一步更新。這個樣例中,若是參數的x初始值落在右側紅色的區間中,那麼經過梯度降低獲得的結果會落到小黑點表明的局部最優解。只有當x的初始值落在左側綠色的區間時梯度降低才能給出全局最優解答。

因而可知在訓練神經網絡時,參數的初始值會很大程度影響最後獲得的結果。只有當損失函數爲凸函數時,梯度降低算法才能保證達到全局最優解。

凸函數:就是一個定義在某個向量空間的凸子集C(區間)上的實值函數

梯度降低算法的另外一個問題就是計算時間長,由於要所有訓練數據上最小化損失,因此損失函數是在全部訓練數據上的損失和。爲了加速訓練過程,能夠使用隨機梯度降低算法。

這個算法優化的不是在所有訓練數據上的損失函數,而是在每一輪迭代中,隨機優化某一條訓練數據上的損失函數。

這樣每一輪參數更新的速度就大大加快了,由於隨機梯度降低算法每次優化的只是某一條數據上的損失函數,因此使用隨機梯度降低優化獲得的神經網絡甚至可能沒法達到局部最優。

爲了綜合梯度降低算法和隨機梯度降低算法的優缺點,在實際應用中通常採用這兩個算法的這種。每次計算一小部分訓練數據的損失函數,這一小部分稱爲batch。經過矩陣運算,每次在一個batch上優化神經網絡的參數並不會比單個數據慢太多。

另外一方面,每次使用一個batch能夠大大減少收斂所須要的迭代次數,同時能夠使收斂到的結果更加接近梯度降低的效果。

如下代碼給出了在TensorFlow中如何實現神經網絡的訓練過程。訓練都大體遵循如下過程

import tensorflow as tf

batch_size = n
# 每次讀取一小部分數據做爲當前的訓練數據來執行反向傳播算法
x = tf.placeholder(tf.float32, shape=(batch_size, 2), name="x-input")
y_ = tf.placeholder(tf.float32, shape=(batch_size, 1), name="y-input")

# 定義神經網絡結構和優化算法
loss = ...
train_step = tf.train.AdamOptimizer(0.001).minimize(loss)

# 訓練神經網絡
with tf.Session() as sess:
    # 參數初始化
    ...

# 迭代的更新參數
for i in range(STEPS):
    # 準備batch_size個訓練數據。通常講全部訓練數據隨機打亂以後在選取能夠獲得更好的優化效果
    current_X, current_Y = ...
    sess.run(train_step, feed_dict={x: current_X, y_: current_Y})

神經網絡進一步優化

學習率的設置

學習率決定了參數每次更新的幅度,若是幅度過大,那麼可能致使參數在極優值的兩次來回移動。

好比上面介紹過的j(x)=x2函數的樣例。若是在優化中使用的學習率爲1,那麼整個優化過程將會以下表

輪數  當前輪參數值  梯度*學習率  更新後參數值
1 5 2*5*1=10 5-10=-5
2 -5 2*-5*1=-10 -5-(-10)=5
3 5 2*5*1=10 5-10=-5

 

 

 

 

上面的樣例能夠看出,不管進行多少輪迭代,參數將在5和-5之間搖擺,而不會收斂到一個極小值。相反當學習率太小時,雖然能保證收斂性,可是會大大下降優化速度。會須要更多輪的迭代才能達到理想的效果。

爲了解決設定學習率的問題,TensorFlow提供了一種更加靈活的學習率設置方法--指數衰減法

tf.train.exponential_decay函數實現了指數衰減學習率。經過這個函數,能夠先使用較大的學習率來快速獲得一個比較優的解。而後隨着迭代的繼續逐步減少學習率,使得模型在訓練後期更加穩定。

exponential_decay函數會指數級地減少學習率,它實現瞭如下代碼的功能

decayed_learning_rate = learning_rate * decay_rate ^ (global_step / decay_steps)

其中decayed_learning_rate爲每一輪優化時使用的學習率,learning_rate爲事先設定的初始學習率,decay_rate爲衰減係數,decay_steps爲衰減速度。

上圖中顯示了隨着迭代輪數增長,學習率逐步下降的過程。tf.train.exponential_decay函數能夠經過設置參數staircase選擇不一樣的衰減方式。staicrcase的默認值爲False,這時學習率隨迭代輪數變化的趨勢如圖中下方灰色曲線。

當staircase的值被設置爲True時,global_step/decay_steps會被轉化成整數。使得學習率成爲一個階梯函數。圖中上方黑色曲線顯示了階梯狀的學習率。

這樣的設置下,decay_steps一般表明了完整的使用一遍訓練數據所須要的迭代輪數。這個迭代輪數也就是總訓練樣本數除以每個batch中的訓練樣本數。

這種設置的經常使用場景是每完整地過一遍訓練數據,學習率就減小一次。使得訓練數據集中的全部數據對模型訓練有相等的做用。當使用連續的指數衰減學習率時,不一樣的訓練數據有不一樣的學習率,當學習率減小時,對應的訓練數據對模型訓練結果的影響也就少了。  

下面代碼來示範如何在TensorFlow中使用tf.train.exponential_decay函數

import tensorflow as tf

global_step = tf.Variable(0)

# 經過exponential_decay函數生成學習率
learning_rate = tf.train.exponential_decay(0.1, global_step, 100, 0.96, staircase=True)

# 使用指數衰減的學習率。在minimize函數中傳入global_step將自動更新global_step參數
# 從而使得學習率也獲得相應更新
learning_step = tf.train.GradientDescentOptimizer(learning_rate).minimize("... my loss ...", global_step=global_step)

上面代碼中設定了初始學習率爲0.1,由於制定了staircase=True,因此每訓練100輪後學習率乘以0.96.

通常來講初始學習率、衰減係數和衰減速度都是根據經驗設置的。並且損失函數降低的速度和迭代結束以後總損失的大小沒有必然的聯繫,也就是說不能經過前幾輪損失函數降低的速度來比較不一樣神經網絡的效果。

過擬合問題 

模型在訓練數據上的表現不必定表明了它在未知數據上的表現。過擬合問題就是能夠致使這個差距的一個很重要的因素。

所謂過擬合,指的是當一個模型過爲複雜以後,它能夠很好的記憶每個訓練數據中隨機噪音的部分而忘記了要去學習訓練數據中通用的趨勢。

若是一個模型中的參數比訓練數據的總數還多,那麼只要訓練數據不衝突,這個模型徹底能夠記住全部訓練數據的結果從而使得損失函數爲0。

能夠直觀地想像一個包含n各變量和n個等式的方程組,當方程不衝突時,這個方程組是能夠經過數學的方法來求解的。

然而過分擬合訓練數據中的隨機噪音雖然能夠獲得很是小的損失函數,可是對於未知數據可能沒法作出可靠的判斷。

上圖顯示了模型訓練的三種不一樣狀況。

第一種:模型過於簡單,沒法刻畫問題的趨勢。

第二種:模型比較合理的,既不會過於關注訓練數據中的噪音,又能比較好的刻畫問題的總體趨勢

第三種:模型就是過擬合了,雖然完美的劃分了不一樣形狀,可是這樣的劃分並不能很好地對未知數據作出判斷,由於它過分擬合了訓練數據中的噪音而忽視了問題的總體規律。

爲了不過擬合問題,一個很是經常使用的方法是正則化。正則化的思想就是在損失函數中加入刻畫模型複雜程度的指標。假設用於刻畫模型在訓練數據上表現的損失函數爲J(Θ)。

那麼在優化時不是直接優化J(Θ),而是優化其中R(w)刻畫的是模型的複雜程度,而λ表示模型複雜損失在總損失中的比例。注意這裏Θ表示的是一個神經網絡中全部的參數,包括邊上的權重w和偏置項b。

通常來講模型複雜度只由權重w決定。經常使用的刻畫模型複雜度的函數R(w)有兩種

一種是L1正則化,計算公式是:

另外一種是L2正則化,計算公式是:

不管是哪種正則化方式,基本的思想都是但願經過限制權重的大小,使得模型不能任意擬合訓練數據中的隨機噪音。

但這兩種正則化的方法也有很大的區別。首先L1會讓參數變得稀疏,會有更多的參數變爲0,這樣能夠達到相似特徵選取的功能。其次,L1正則化的計算公式不可導,在優化時須要計算損失函數的偏導數。

TensorFlow能夠優化任意形狀的損失函數,天然也能夠優化帶正則化的損失函數。

w = tf.Variable(tf.random_normal([2, 1], stddev=1, seed=1))
y = tf.matmul(x, w)

loss = tf.reduce_mean(tf.square(y_ - y)) + tf.contrib.layers.l2_regularizer(lambda)(w)

上面的程序中,loss爲定義的損失函數,有兩部分組成。

第一部分是均方偏差損失函數,刻畫了模型在訓練數據上的表現

第二部分是正則化,防止模型過分模擬訓練數據中的隨機噪音。lamdba參數表示了正則化項的權重,也就是λ

w爲須要計算正則化損失的參數。TensorFlow提供了這個函數,能夠返回一個函數,能夠計算一個給定參數的L2正則化項的值。

weights = tf.constant([[1.0, -2.0], [-3.0, 4.0]])
with tf.Session() as sess:
    print(sess.run(tf.contrib.layers.l1_regularizer(.5)(weights)))
    # 輸出爲(1+2+3+4)*0.5 = 5。0.5爲正則化項的權重
    print(sess.run(tf.contrib.layers.l2_regularizer(0.5)(weights)))
    # 輸出爲(12+2*2+3*2+42)/2*0.5=7.5

在簡單的神經網絡中,這樣能夠很好的計算帶正則化的損失函數。可是當神經網絡參數增多後,這樣會致使損失函數loss的定義很長,可讀性差且容易出錯。當網絡結構複雜以後定義網絡結構的部分和計算損失函數的部分可能不不在同一個函數中,這樣經過變量這種方式計算損失函數就不方便了。

TensorFlow提供的集合概念能夠解決這個問題。能夠在一個計算圖中保存一組實體。下面代碼是經過集合計算一個5層神經網絡帶L2正則化的損失函數的計算方法。

import tensorflow as tf


# 獲取一層神經網絡邊上的權重,並將這個權重的L2正則化損失加入名稱爲losses的集合中
def get_weight(shape, func):
    # 生成一個變量
    var = tf.Variable(tf.random_normal(shape), dtype=tf.float32)
    # add_to_collection函數將這個新生成變量的L2正則化損失項加入集合。
    # 這個函數的第一個參數losses是集合的名字,第二個參數是要加入這個集合的內容。
    tf.add_to_collection('losses', tf.contrib.layers.l2_regularizer(func)(var))
    # 返回生成的變量
    return var


x = tf.placeholder(tf.float32, shape=(None, 2))
y_ = tf.placeholder(tf.float32, shape=(None, 2))
batch_size = 8
# 定義了每一層網絡節點的個數
layer_dimension = [2, 10, 10, 10, 1]
# 神經網絡的層數
n_layers = len(layer_dimension)
# 這個變量維護前向傳播時最深層的節點,開始的時候就是輸入層
cur_layer = x
# 當前層的節點個數
in_dimension = layer_dimension[0]
# 經過一個循環來生成5層全鏈接的神經網絡結構
for i in range(1, n_layers):
    # layer_dimension[i]爲下一層的節點個數
    out_dimension = layer_dimension[i]
    # 生成當前層中權重的變量,並將這個變量的L2正則化損失加入計算圖上的集合
    weight = get_weight([in_dimension, out_dimension], 0.001)
    bias = tf.Variable(tf.constant(0.1, shape=[out_dimension]))
    # 使用ReLU激活函數
    cur_layer = tf.nn.relu(tf.matmul(cur_layer, weight) + bias)
    # 進入下一層以前將下一層的節點個數更新爲當前層節點個數
    in_dimension = layer_dimension[i]

# 在定義神經網絡前向傳播的同時已經將全部的L2正則化損失加入了圖上的集合,
# 這裏只須要計算刻畫模型在訓練數據上表現的損失函數
mse_loss = tf.reduce_mean(tf.square(y_ - cur_layer))

# 將均方偏差損失函數加入損失集合。
tf.add_to_collection('losses', mse_loss)

# get_collection返回一個列表,這個列表是全部這個集合的元素。在這個樣例中
# 這些元素就是損失函數的不一樣部分,將它們加起來就能夠獲得最終的損失函數
loss = tf.add_n(tf.get_collection('losses'))

從上面代碼能夠看出經過使用集合的方法在網絡結構比較複雜的狀況下能夠使代碼的可讀性更高。

上面代碼是隻有5層的全鏈接網絡,更加複雜的網絡結構中,使用這樣的方式來計算損失函數將大大加強代碼的可讀性。

滑動平均模型

在採用隨機梯度降低算法訓練神經網絡時,使用滑動平均模型在不少應用中均可以在必定程度上提升最終模型在測試數據上的表現。

在TensorFlow中提供了tf.train.ExponentialMovingAverage來實現滑動平均模型。在初始化時須要提供一個衰減率(decacy)。這個衰減率將用於控制模型更新的速度。對每個變量會維護一個影子變量(shadow variable)。

這個影子變量的初始值就是相應變量的初始值,而每次運動變量更新時,影子變量的值會更新爲:

shadow_variable = decay * shadow_variable + (1-decay) * variable

其中shadow_variable爲影子變量,variable爲待更新的變量,decay爲衰減率。

從公式中能夠看到decay決定了模型更新的速度,decay越大模型越趨於穩定。在實際用用中,decay通常會設定很是接近1的數(好比0.999)。爲了使得模型在訓練前期能夠更新的更快,ExponentialMovingAverage還提供了num_updates參數來動態設置decay的大小。若是在初始化時提供了num_updates參數,那麼每次使用的衰減率將是

下面是代碼是解釋ExponentialMovingAverage是如何被使用的

import tensorflow as tf

# 定義一個變量用於計算滑動平均,這個變量的初始化爲0.
# 這裏手動設置指定了變量類型,由於全部須要計算滑動平均的變量必須是實數形
v1 = tf.Variable(0, dtype=tf.float32)
# 這裏step變量模擬神經網絡中迭代的輪數,能夠用於動態控制衰減率
step = tf.Variable(0, trainable=False)
# 定義一個滑動平均的類,初始化時給定了衰減率和控制衰減率的變量step
ema = tf.train.ExponentialMovingAverage(0.999, step)
# 定義一個更新滑動平均的操做。這裏須要給定一個列表,每次執行這個操做時
# 這個列表中的變量都會被更新
maintain_averages_op = ema.apply([v1])
with tf.Session() as sess:
    # 初始化全部變量
    sess.run(tf.initialize_all_variables())
    # 經過ema.average(v1)獲取滑動平均以後的變量的取值。
    # 初始化以後變量v1的值和v2的滑動平均都爲0
    print(sess.run([v1, ema.average(v1)]))  # [0.0, 0.0]
    # 更新變量v1的值到5
    sess.run(tf.assign(v1, 5))
    # 更新v1的滑動平均值。衰減率爲min{0.999,(1+step)/(10+step)=0.1}=0.1
    # 因此v1的滑動平均會被更新爲0.1*0+0.9*5=4.5
    sess.run(maintain_averages_op)
    print(sess.run([v1, ema.average(v1)]))  # [5.0, 4.5]
    # 更新step的值爲1000
    sess.run(tf.assign(step, 10000))
    # 更新v1的值爲10
    sess.run(tf.assign(v1, 10))
    # 更新v1的滑動平均值。衰減率爲min{0.99,(1+step)/(10+step)0.999}=0.99
    # 因此v1的滑動平均會被更新爲0.99*4.5+0.01*10=4.555
    sess.run(maintain_averages_op)
    print(sess.run([v1, ema.average(v1)]))  # [10.0, 4.5055]
    # 再次更新滑動平均值,獲得新滑動平均值爲0.99*4.555+0.01*10=4.60945
    sess.run(maintain_averages_op)
    print(sess.run([v1, ema.average(v1)]))  # [10.0, 4.5109944]

MNIST數字識別問題

MNIST是一個很是有名的手寫體數字識別數據集,被用做深度學習的入門樣例。

TensorFlow的封裝讓使用MNIST數據集變得更加方便,MNIST數據集是NIST數據集的一個本身,包含了6萬張圖片做爲訓練數據,1萬張圖片做爲測試數據。在MNIST數據集中每一張圖片都表明了0~9的一個數字。圖片大小都爲28*28,且數字都會出如今圖片的正中間。下圖展現了一張數字圖片和它對應的像素矩陣。

左側顯示了一張數字1,右側顯示了對應的像素矩陣。

MNIST數據集提供了4個下載文件,以下所示

訓練數據圖片:http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz

訓練數據答案:http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz

測試數據圖片:http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz

測試數據答案:http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz

雖然這個數據集只提供了訓練和測試數據,可是爲了驗證模型訓練的效果,通常會從訓練數據中劃分出一部分數據做爲驗證數據。爲了方便使用,TensorFlow提供了一個類來處理MNIST數據。這個類會自動下載並轉化MNIST數據的格式,將數據從原始的數據包中解析成訓練和測試神經網絡時使用的格式。代碼以下

from tensorflow.examples.tutorials.mnist import input_data

# 載入MNIST數據集,若是沒有下載好數據,則會自動下載
mnist = input_data.read_data_sets("/MNIST_data/", one_hot=True)
print("Training data size:", mnist.train.num_examples) # 55000
print("Validating data size", mnist.validation.num_examples) # 5000
print("Testing data size", mnist.test.num_examples) # 10000

print("Example training data:", mnist.train.images[0])
print("Example training data label", mnist.train.labels[0])

經過input_data.read_data_sets函數生成的類會自動將MNIST數據集劃分爲train、validation和test三個數據集,其中train這個集合內有55000張圖片,validation集合內有5000張圖片,這兩個集合組成了MNIST自己提供的訓練數據集。test集合內有10000張圖片,這些圖片都來自與MNIST提供的測試數據集。處理後的每一張圖片是一個長度爲784的一維數組,這個數組中的元素對應了圖片像素矩陣中的每個數字(28*28=784)

由於神經網絡的輸入是一個特徵向量,因此在此把一張二維圖像的像素矩陣放到一個一維數組中能夠方便TensorFlow將圖片的像素矩陣提供給神經網絡的輸入層。像素矩陣中的元素的取值範圍爲[0,1],它表明了顏色的深淺。其中0表示白色、1表示黑色。爲了方便使用隨機梯度降低,input_data.read_data_sets函數生成的類還提供了mnist.train.next_batch函數,能夠從全部的訓練數據中讀取一小部分做爲一個訓練batch。

batch_size = 100
xs, ys = mnist.train.next_batch(batch_size)
# 從train集合中選取batch_size個訓練數據
print("x shape:", xs.shape)  # x shape: (100, 784)
print("y shape", ys.shape)  # y shape (100, 10)

TensorFlow訓練神經網絡

神經網絡的結構上,深度學習一方面須要使用激活函數實現神經網絡模型的去線性化,另外一方面須要使用一個或多個隱藏層使得神經網絡的結構更深,已解決複雜問題。

在訓練神經網絡時,使用帶指數衰減的學習率設置、使用正則化來避免過分擬合,以及使用滑動平均模型來使得最終模型更加健壯。下面代碼給出了在MNIST數據集上實現這些功能的完整程序

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# MNIST數據集相關的常數
INPUT_NODE = 784  # 輸入層的節點數。對於MNIST數據集,這個就等於圖片的像素
OUTPUT_NODE = 10  # 輸出層的節點數,等於類別的數目
# 隱藏層節點數。使用只有一個隱藏層的網絡結構做爲樣例,有500個節點
LAYER1_NODE = 500
# 訓練batch中的個數,數字越小越接近隨機梯度降低,越大越接近梯度降低
BATCH_SIZE = 100
# 基礎的學習率
LEARNING_RATE_BASE = 0.8
# 學習率的衰減率
LEARNING_RATE_DECAY = 0.99
# 模型複雜度的正則化在損失函數中的係數
REGULARIZATION_RATE = 0.0001
# 訓練輪數
TRAINING_STEPS = 50000
# 滑動平均衰減率
MOVING_AVERAGE_DECAY = 0.9


# 輔助函數,給定神經網絡的輸入和全部參數,計算神經網絡的前向傳播結果。
# 定義一個使用ReLU激活函數的三層全鏈接神經網絡。經過加入隱藏層實現了多層網絡結構。
# 經過ReLU激活函數實現了去線性化。支持傳入用於計算參數平均值的類,方便使用滑動平均模型
def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):
    # 當沒有提供滑動平均類時,直接使用參數當前的取值
    if avg_class is None:
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights1) + biases1)
        # 計算輸出層的前向傳播結果。計算損失函數時會一併計算softmax函數
        # 這裏不須要加入激活函數,不加入softmax不會影響預測結果,
        # 由於預測時使用的是不一樣類別對應節點輸出值的相對大小,沒有softmax對最後分類的結果計算沒有影響
        # 在計算整個神經網絡的前向傳播時能夠不加入最後的softmax層
        return tf.matmul(layer1, weights2) + biases2
    else:
        # 首先使用avg_class.average函數來計算得出變量的滑動平均值
        # 而後計算相應的神經網絡前向傳播結果
        layer1 = tf.nn.relu(tf.matmul(input_tensor, avg_class.average(weights1)) + avg_class.average(biases1))
        return tf.matmul(layer1, avg_class.average(weights2)) + avg_class.average(biases2)


# 訓練模型的過程
def train(mnist):
    x = tf.placeholder(tf.float32, [None, INPUT_NODE], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, OUTPUT_NODE], name='y-input')
    # 生成隱藏層的參數
    weights1 = tf.Variable(tf.truncated_normal([INPUT_NODE, LAYER1_NODE], stddev=0.1))
    biases1 = tf.Variable(tf.constant(0.1, shape=[LAYER1_NODE]))
    # 生成輸出層的參數
    weights2 = tf.Variable(tf.truncated_normal([LAYER1_NODE, OUTPUT_NODE], stddev=0.1))
    biases2 = tf.Variable(tf.constant(0.1, shape=[OUTPUT_NODE]))
    # 計算當前參數下神經網絡前向傳播的結果。給出的用於計算滑動平均的類爲None
    # 因此函數不會使用參數的滑動平均值
    y = inference(x, None, weights1, biases1, weights2, biases2)
    # 定義存儲訓練輪數的變量,這個變量不須要計算滑動平均值,因此這裏指定這個變量爲不可訓練的變量。
    # 在使用TensorFlow訓練神經網絡時,通常會將表明訓練輪數的變量指定爲不可訓練的參數
    global_step = tf.Variable(0, trainable=False)
    # 給定滑動平均衰減率和訓練輪數的變量,初始化滑動平均類。
    # 給定訓練輪數的變量能夠加快訓練早期變量的更新速度
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    # 全部表明神經網絡參數的變量上使用滑動平均。其餘輔助變量就不須要了。
    # tr.trainable_variables返回的就是圖上集合
    # GraphKeys.TRAINABLE_VARIABLES中的元素。這個元素就是全部沒有指定trainble=False的參數。
    variables_averages_op = variable_averages.apply(tf.trainable_variables())
    # 計算使用滑動平均以後的前向傳播結果,滑動平均不會改變變量自己的取值,
    # 而是會維護一個影子變量來記錄其滑動平均值。當須要使用這個滑動平均值時,須要明確調用average函數
    average_y = inference(x, variable_averages, weights1, biases1, weights2, biases2)
    # 計算交叉熵做爲刻畫預測值和真實值之間差距的損失函數。使用sparse_softmax_cross_entropy_with_logits函數來計算交叉熵
    # 當分類問題只有一個正確答案時,能夠使用這個函數來加速交叉熵的計算。
    # MNIST問題的圖片中,只包含0~9中的一個數字,因此能夠使用這個函數來計算交叉熵損失。
    # 這個函數的第一個參數是神經網絡不包括softmax層的前向傳播結果,第二個是訓練數據的正確答案。
    # 由於標準答案是一個長度爲10的一維數組,而該函數須要提供的是一個正確答案的數字,
    # 因此須要使用tf.argmax函數來獲得正確答案對應的類別編號。
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
    # 計算在當前batch中全部樣例的交叉熵平均值。
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    # 計算L2正則化損失函數。
    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    # 計算模型的正則化損失。只計算神經網絡邊上權重的正則化損失,而不使用偏置量
    regularization = regularizer(weights1) + regularizer(weights2)
    # 總損失等於交叉熵損失和正則化損失的和
    loss = cross_entropy_mean + regularization
    # 設置指數衰減的學習率
    learning_rate = tf.train.exponential_decay(LEARNING_RATE_BASE,  # 基礎的學習率,更新時使用的學習率在這個基礎上遞減
                                               global_step,  # 當前迭代的輪數
                                               mnist.train.num_examples / BATCH_SIZE,  # 過完全部的訓練數據須要的迭代次數
                                               LEARNING_RATE_DECAY)  # 學習率衰減速度
    # 使用tf.train.GradientDescentOptimizer優化算法來優化損失函數。
    # 這裏損失函數包含了交叉熵損失和L2正則化損失。
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
    # 訓練神經網絡時,每過一遍數據即須要經過反向傳播更新網絡中的參數,又要更新參數的滑動平均值
    # 爲了一次完成多個操做,TensorFlow提供了tf.control_dependencies和tf.group兩種機制
    # 下面兩行程序是等價的
    # train_op = tf.group(train_step, variables_averages_op)
    with tf.control_dependencies([train_step, variables_averages_op]):
        train_op = tf.no_op(name='train')
    # 檢驗使用了滑動平均模型的神經網絡的前向傳播結果是否正確。tf.argmax(average_y,1)
    # 計算每個樣例的預測答案。其中average_y是一個batch_size*10的二維數組,每一行表示一個樣例的前向傳播結果。
    # tf.argmax的第二個參數"1"表示選取最大值的操做僅在第一個維度中進行,也就是說,只在每一行選取最大值對應的下標。
    # 因而獲得的結果是一個長度爲batch的一維數組,這個一維數組中的值就表示了每個樣例對應的數字識別結果。
    # 判斷兩個張量的每一維是否相等,若是相等返回True,不然返回False
    correct_prediction = tf.equal(tf.argmax(average_y, 1), tf.argmax(y_, 1))
    # 這個運算首先將一個布爾型的數值轉換爲實數形,而後計算平均值。這個平均值就是模型在一組數據上的正確率
    accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
    # 初始化會話並開始訓練過程
    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())
        # 準備驗證數據,通常在神經網絡的訓練過程當中經過驗證數據來大體判斷中止的條件和評判訓練的效果
        validate_feed = {x: mnist.validation.images, y_: mnist.validation.labels}
        # 準備測試數據,真實的應用中這部分數據在訓練時是不可見的,這個數據只是做爲模型優劣的最後評價標準
        test_feed = {x: mnist.test.images, y_: mnist.test.labels}
        # 迭代地訓練神經網絡
        for i in range(TRAINING_STEPS):
            # 每1000輪輸出一次在驗證數據集上的測試結果
            if i % 1000 == 0:
                # 計算滑動平均模型在驗證數據上的結果。由於MNIST數據集比較小,因此一次能夠處理全部的驗證數據
                # 爲了計算方便,本楊麗沒有將驗證數據劃分爲更小的batch,
                # 當神經網絡模型比較複雜或驗證數據比較大時,太大的batch會致使計算時間過長甚至發生內存溢出的錯誤
                validate_acc = sess.run(accuracy, feed_dict=validate_feed)
                print("After %d training step(s),validation accuracy using average model is %g" % (i, validate_acc))
                # 產生這一輪使用的一個batch的訓練數據,並運行訓練過程
                xs, ys = mnist.train.next_batch(BATCH_SIZE)
                sess.run(train_op, feed_dict={x: xs, y_: ys})
                # 在訓練結束以後,在測試數據上檢測神經網絡模型的最終正確率
                test_acc = sess.run(accuracy, feed_dict=test_feed)
                print("After %d training step(s),test accuracy using average model is %g" % (TRAINING_STEPS, test_acc))


# 主程序入口
def main(argv=None):
    # 聲明處理MNIST數據集的類,初始化時會自動下載數據
    mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
    train(mnist)


# 命令行過濾,tf.app.run會調用上面定義的main函數
if __name__ == '__main__':
    tf.app.run()

從上面結果看,訓練初期隨着訓練的進行,模型在驗證數據集上的表現愈來愈好。從第4000輪開始,模型在驗證數據集上的表現開始波動,這說明已經接近極小值了,因此迭代也就能夠結束了。

使用驗證數據判斷模型效果

這個程序的開始設置了初始學習率、學習率衰減率、隱藏層節點數量、迭代輪數等7種不一樣的參數。大部分狀況下,配置神經網絡的參數都是須要經過實驗來調整的。使用測試數據來選取參數可能會致使神經網絡模型過分擬合測試數據,從而失去對未知數據的預判能力。爲了評測神經網絡模型在不一樣參數下的效果,通常會從訓練數據中抽取一部分做爲驗證數據,使用驗證數據就能夠評判不一樣參數取值下的模型的表現。除了使用驗證數據集,還能夠採用交叉驗證(cross validation)的方式來驗證模型效果。但由於神經網絡訓練時間自己就比較長,採用cross validation會話費大量時間。因此在海量數據的狀況下,通常會更多地採用驗證數據集的形式來評測模型的效果。

經過上面代碼獲得的每1000輪滑動平均模型在不一樣數據集上的正確率曲線。灰色表示隨着迭代輪數的增長,模型在驗證數據上的正確率。黑色的曲線表示在測試數據上的正確率。這兩條曲線不會徹底重合,可是這兩條曲線的趨勢基本同樣,並且他們的相關係數大於0.9999。意味着在MNIST問題上,徹底能夠經過模型在驗證數據上的表現來判斷一個模型的優劣。

不一樣模型效果比較

在前幾章節提到了設計神經網絡時的5種優化方法。在神經網絡結構的設計上,須要使用激活函數和多層隱藏層。在神經網絡優化時,能夠使用指數衰減的學習率、加入正則化的損失函數以及滑動平均模型。

下圖中給出了相同神經網絡參數下,使用不一樣優化方法,通過3萬輪訓練迭代後,獲得的最終模型的正確率。

 

能夠看出,調整神經網絡的結構對最終的正確率有很是大的影響。沒有隱藏層或者沒有激活函數時,模型的正確率只有92.6%。遠遠小於使用了隱藏層和激活函數時能夠達到的大約98.4%的正確率。這說明神經網絡的結構對最終模型的效果有本質性的影響。

使用滑動平均模型、指數衰減的學習率和使用正則化帶來的正確率的提高並非特別明顯。其中使用了全部優化算法的模型和不使用滑動平均的模型以及不使用指數衰減的學習率的模型均可以達到98.4%的正確率。由於滑動平均模型和指數衰減的學習率在必定程度上都是限制神經網絡中參數更新的速度,然而在MNIST數據上,由於模型收斂的速度很快,因此這兩種優化對最終模型的影響不大。

 

當模型迭代到4000輪時正確率就已經接近最終的正確率了。在迭代的早期,是否使用滑動平均模型或者指數衰減的學習率對訓練結果的影響相對較小。

前4000輪迭代對模型的改變是最大的。在4000輪以後,由於梯度自己比較小,因此參數的改變也就比較緩慢了。滑動平均模型或者指數衰減的學習率的做用也就沒有那麼突出了。學習率曲線呈現出階梯狀衰減,前4000輪時,衰減以後的學習率和最初的學習率差距並不大。

當問題更加複雜時,迭代不會這麼快接近收斂,這時滑動平均模型和指數衰減的學習率能夠發揮更大的做用。

相比滑動平均模型和指數衰減學習率,使用加入正則化的損失函數給模型效果帶來的提高要相對顯著。使用正則化損失函數的神經網絡模型能夠下降大約6%的錯誤率。

變量管理 

上方代碼中將計算神經網絡前向傳播結果的過程抽象成了一個函數。經過這種方式在訓練和測試的過程當中能夠統一調用同一個函數來獲得模型的前向傳播結果。

def inference(input_tensor, avg_class, weights1, biases1, weights2, biases2):

函數的參數包括了神經網絡中的全部參數。然而當神經網絡的結構更加複雜、參數更多時,就須要一個更好的方式來傳遞和管理神經網絡中的參數了。

TensorFlow提供了經過變量名稱來建立或者獲取一個變量的機制,經過這個機制,在不一樣的函數中能夠直接經過變量的名字來使用變量。

經過變量名稱獲取變量的機制主要是經過tf.get_variable和tf.variable_scope函數來實現的。

當tf.get_variable用於建立變量時,它和tf.Variable的功能是基本等價的。

v = tf.get_variable("v", shape=[1], initializer=tf.constant_initializer(1.0))
v = tf.Variable(tf.constant(1.0, shape=[1]), name="v")

經過tf.Variable和tf.get_variable函數建立變量的過程基本上是同樣的。tf.get_variable函數調用時提供的維度信息以及初始化方法的參數和tf.Variable函數調用時提供的初始化過程當中的參數也相似。 

上面例子中使用常數初始化函數tf.constant_initializer和常數生成函數tf.constant功能上就是一致的。TensorFlow提供了7種不一樣的初始化函數。以下表

初始化函數 功能  主要參數
tf.constant_initializer  將變量初始化爲給定常量 常量的取值
tf.random_normal_initializer  將變量初始化爲知足正態分佈的隨機值   正態分佈的均值和標準差
tf.truncated_normal_initializer

 將變量初始化爲知足正態分佈的隨機值

但若是隨機出來的值偏離平均值超過2個

標準差,那麼這個數將會被從新隨機

 正態分佈的均值和標準差
tf.random_uniform_initializer  將變量初始化爲知足平均分佈的隨機值  最大、最小值
tf.uniform_unit_scaling_initializer

 將變量初始化爲知足平均分佈但不影響

輸出數量級的隨機值

 factor(產生隨機值時乘以

的係數)

tf.zeros_initializer    將變量設置爲全0  變量維度
tf.ones_initializer  將變量設置爲全1  變量維度

tf.get_variable函數與tf.Variable函數最大的區別在於指定變量名稱的參數。對於tf.Variable函數,變量名稱是一個可選的參數,經過name="v"的形式給出。可是對於tf.get_variale函數,變量名稱是一個必填的參數。tf.get_variable會根據這個名字去建立或者獲取變量。

上面的樣例中,tf.get_variable會建立一個名字爲v的參數,若是建立失敗這個程序就會報錯。若是須要經過tf.get_variable獲取一個已經建立的變量,須要經過tf.variable_scope函數來生成一個上下文管理器,並明確指定在這個上下文管理器中,tf.get_variable將直接獲取已經生成的變量。

# 在名字爲foo的命名空間內建立名字爲v的變量
with tf.variable_scope("foo"):
    v = tf.get_variable("v", shape=[2, 1], initializer=tf.zeros_initializer())

# 在生成上下文管理器時,將參數reuse設置爲True。會直接獲取
with tf.Session() as sess:
    sess.run(tf.initialize_all_variables())
    with tf.variable_scope("foo", reuse=True):
        print(sess.run(tf.get_variable("v", [2, 1])))

tf.variable_scope函數是能夠嵌套的

with tf.variable_scope("root"):
    # 能夠經過下面函數來獲取當前上下文管理器中reuse參數的值
    print(tf.get_variable_scope().reuse)  # False
    with tf.variable_scope("foo", reuse=True):
        print(tf.get_variable_scope().reuse)  # True
        with tf.variable_scope("bar"):
            # 若是不指定reuse,取值回合外面一層保持一致
            print(tf.get_variable_scope().reuse)  # True

tf.variable_scope函數生成的上下文管理器也會建立一個命名空間,在命名空間內建立的變量名稱都會帶上這個命名空間做爲前綴。因此tf.variable_scope函數除了能夠控制tf.get_variable執行的功能以外,這個函數也提供了一個管理變量命名空間的方式。

v1 = tf.get_variable("v", [1])
print(v1.name)  # v:0,v爲變量名稱,0表示生成變量運算的第一個結果
with tf.variable_scope("foo"):
    v2 = tf.get_variable("v", [1])
    print(v2.name)  # foo/v:0
with tf.variable_scope("foo"):
    with tf.variable_scope("bar"):
        v3 = tf.get_variable("v", [1])
        print(v3.name)  # foo/bar/v:0
        v4 = tf.get_variable("v1", [1])
        print(v4.name)  # foo/bar/v1:0
        
with tf.variable_scope("", reuse=True):
    v5 = tf.get_variable("foo/bar/v", [1])
    v6 = tf.get_variable("foo/bar/v1", [1])
    with tf.Session() as sess:
        sess.run(tf.initialize_all_variables())
        print(sess.run(v5))
        print(sess.run(v6))

經過tf.variable_scope和tf.get_variable函數,能夠對前面的計算前向傳播結果的函數作了一些改進

def inference(input_tensor, reuse=False):
    # 定義第一層神經網絡的變量和前向傳播過程
    with tf.variable_scope("layer1", reuse=reuse):
        # 根據傳進來的reuse來判斷是建立變量仍是使用已經建立好的。
        # 在第一次構建網絡時須要建立新的變量,之後每次調用這個函數都直接使用reuse=True就不須要每次講變量傳進來了。
        weights = tf.get_variable("weights", [INPUT_NODE, LAYER1_NODE],
                                  initializer=tf.truncated_normal_initializer(stddev=0.1))
        biases = tf.get_variable("biases", [LAYER1_NODE], initializer=tf.constant_initializer(0.0))
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)
    # 相似地定義第二層神經網絡的變量和前向傳播過程
    with tf.variable_scope("layer2", reuse=reuse):
        weights = tf.get_variable("weights", [LAYER1_NODE, OUTPUT_NODE],
                                  initializer=tf.truncated_normal_initializer(stddev=0.1))
        biases = tf.get_variable("biases", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))
        layer2 = tf.matmul(layer1, weights) + biases
        return layer2

使用上面這段代碼所示的方式,就不在須要將全部變量都做爲參數傳遞到不一樣的函數中了。

當神經網絡結構更加複雜、參數更多時,使用這種變量管理的方式將大大提升程序的可讀性。

TensorFlow模型持久化 

爲了讓訓練結果能夠複用,須要將訓練獲得的神經網絡持久化。

TensorFlow提供了一個很是簡單的API來保存和還原一個神經網絡模型。這個API就是tf.train.Saver類

# 聲明兩個變量並計算它們的和
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2
init_op = tf.global_variables_initializer()

# 聲明tf.train.Saver類用於保存模型
saver = tf.train.Saver()
with tf.Session() as sess:
    sess.run(init_op)
    # 將模型保存到文件中
    saver.save(sess, "Saved_model/model.ckpt")

實現了持久化一個簡單的TensorFlow模型的功能。這段代碼中,經過saver.save函數將TensorFlow模型保存到文件中。通常會存在後綴爲.ckpt的文件中。雖然只指定了一個文件路徑,可是目錄下會出現三個文件。由於TensorFlow會將計算圖的結構和圖上參數取值分開保存。

上面代碼會生成一個model.ckpt.meta,保存了計算圖的結構。第二個文件model.ckpt.index,保存了程序中每個變量的取值。最後一個文件checkpoint文件,保存了目錄下全部的模型文件列表。

下面代碼是加載這個已經保存的模型方法

# 使用和保存模型代碼中同樣的方式來聲明變量
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2
saver = tf.train.Saver()
with tf.Session() as sess:
    # 加載已經保存過的模型,並經過模型中變量的值來計算加法
    saver.restore(sess, "Saved_model/model.ckpt")
    print(sess.run(result))

這段加載模型的代碼基本上和保存模型的代碼是同樣的。在加載模型的程序中也是先定義了計算圖上的全部運算,並聲明瞭一個tf.train.Saver類。兩段代碼惟一不一樣的是,在加載模型的代碼中沒有運行變量的初始化過程,而是將變量的值經過已經保存的模型加載進來。若是不但願重複定義圖上的運算,也能夠直接加載已經持久化的圖。

# 直接加載持久化的圖
saver = tf.train.import_meta_graph("Saved_model/model.ckpt.meta")
with tf.Session() as sess:
    saver.restore(sess, "Saved_model/model.ckpt")
    print(sess.run(tf.get_default_graph().get_tensor_by_name("add:0"))) # [3.]

默認保存和加載了計算圖上定義的所有變量。但有時可能只須要保存或者加載部分變量。好比,可能有一個以前訓練好的五層神經網絡模型,但如今想嘗試一個六層的神經網絡,那麼能夠將前面五層神經網絡中的參數直接加載到新的模型,而僅僅將最後一層神經網絡從新訓練。

爲了保存或加載部分變量,聲明Saver類時能夠提供一個列表來指定須要保存或者加載的變量。好比使用

saver = tf.train.Saver([v1]) 命令來構建tf.train.Saver類,那麼只有變量v1會被加載進來。若是運行修改後只加載了v1的代碼會獲得變量未初始化的錯誤

由於v2沒有被加載,因此v2在運行初始化以前是沒有值的。除了能夠選取須要被加載的變量,tf.train.Saver類也支持在保存或者加載時給變量重命名。

# 這裏聲明的變量名稱和已經保存的模型中變量的名稱不一樣
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name='other-v1')
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name='other-v2')

# 若是直接使用tf.train.Saver來加載模型會報變量找不到錯誤
# 使用一個字典來重命名變量就能夠加載原來的模型了。
# 原來名稱爲v1的變量如今加載到變量v1中,名稱爲v2的變量加載到變量v2中
saver = tf.train.Saver({"v1": v1, "v2": v2})

對變量v1和v2的名稱進行了修改。若是直接經過tf.train.Saver默認的構造函數來加載保存的模型,那麼程序會報變量找不到的錯誤。由於保存時候變量的名稱和加載時變量的名稱不一致。爲了解決這個問題,能夠經過字典將模型保存時的變量名和須要加載的變量聯繫起來。

這樣作是方便使用變量的滑動平均值,使用變量的滑動平均值可讓神經網絡模型更加健壯。每個變量的滑動平均值是經過影子變量維護的,因此要獲取變量的滑動平均值實際上就是獲取這個影子變量的取值。若是在加載模型時直接將影子變量映射到變量自身,那麼在使用訓練好的模型時就不須要再調用函數來獲取變量的滑動平均值了。這樣大大方便了滑動平均模型的使用。

如下代碼給出了一個保存滑動平均模型的樣例

v = tf.Variable(0, dtype=tf.float32, name="v")
# 沒有申明滑動平均模型時只有一個變量v
for variables in tf.all_variables():
    print(variables.name)  # v:0
ema = tf.train.ExponentialMovingAverage(0.99)
maintain_averages_op = ema.apply(tf.all_variables())
# 聲明滑動平均模型以後,會自動生成一個影子變量
for variables in tf.all_variables():
    print(variables.name)  # v:0 | v/ExponentialMovingAverage:0
saver = tf.train.Saver()
with tf.Session() as sess:
    init_op = tf.initialize_all_variables()
    sess.run(init_op)
    sess.run(tf.assign(v, 10))
    sess.run(maintain_averages_op)
    # 保存時會將兩個變量都存下來
    saver.save(sess, "Saved_model/model.ckpt")
    print(sess.run([v, ema.average(v)]))  # [10.0, 0.099999905]

如下代碼給出瞭如何經過變量重命名直接讀取變量的滑動平均值。下面程序的輸出能夠看出,讀取的變量v的值其實是上面代碼中變量v的滑動平均值。經過這個方法,就能夠使用徹底同樣的代碼來計算滑動平均模型前向傳播的結果。

v = tf.Variable(0, dtype=tf.float32, name="v")
# 經過變量重命名將原來變量v的滑動平均值直接賦值給v
saver = tf.train.Saver({"v/ExponentialMovingAverage": v})
with tf.Session() as sess:
    saver.restore(sess, "Saved_model/model.ckpt")
    print(sess.run(v))  # 0.099999905,這個值就是原來模型中變量v的滑動平均值

爲了方便加載時重命名滑動平均變量,tf.train.ExponentialMovingAverage類提供了variables_to_restore函數來生成tf.train.Saver類所須要的變量重命名字典。下面代碼給出了函數使用樣例

v = tf.Variable(0, dtype=tf.float32, name="v")
ema = tf.train.ExponentialMovingAverage(0.99)
# 經過使用variables_to_restore函數能夠直接生成上面代碼提供的字典
print(ema.variables_to_restore())
# {'v/ExponentialMovingAverage': <tf.Variable 'v:0' shape=() dtype=float32_ref>}
saver = tf.train.Saver(ema.variables_to_restore())
with tf.Session() as sess:
    saver.restore(sess, "Saved_model/model.ckpt")
    print(sess.run(v))  # 0.099999905

使用tf.train.Saver會保存運行程序所須要的所有信息,然而有時並不須要某些信息。並且將變量取值和計算圖結構分紅不一樣文件粗糙年初有時候也不方便,因而TensorFlow提供了convert_variables_to_constants函數,經過這個函數能夠將計算圖中的變量及其取值經過常量的方式保存。這樣整個計算圖能夠統一存放在一個文件中

import tensorflow as tf
from tensorflow.python.framework import graph_util

v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result = v1 + v2
init_op = tf.global_variables_initializer()
with tf.Session() as sess:
    sess.run(init_op)
    # 導出當前計算圖額GraphDef部分,只須要這一部分就能夠完成從輸入層到輸出層的計算
    graph_def = tf.get_default_graph().as_graph_def()
    # 將圖中的變量及其取值轉化爲常量,同時將圖中沒必要要的節點去掉
    # 到一些系統運算也會被轉化爲計算圖中的節點。若是隻關心程序中定義的某些計算時,
    # 這些計算無關的節點就沒有必要導出並保存了。
    # 在下面一行代碼中,最後一個參數['add']給出了須要保存的節點名稱。
    # add節點是上面定義的兩個變量相加的操做。
    output_graph_def = graph_util.convert_variables_to_constants(sess, graph_def, ['add'])
    # 將導出的模型存入文件
    with tf.gfile.GFile("Saved_model/combined_model.pb", "wb") as f:
        f.write(output_graph_def.SerializeToString())

經過下面的程序能夠直接計算定義的加法運算的結果。只須要獲得計算圖中某個節點的取值時,提供了一個更加方便的方法。

import tensorflow as tf
from tensorflow.python.platform import gfile

with tf.Session() as sess:
    model_filename = "Saved_model/combined_model.pb"
    # 讀取保存的模型文件,並將文件解析成對應的GraphDef Protocol Buffer
    with gfile.FastGFile(model_filename, "rb") as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
        # 將graph_def中保存的圖加載到當前的圖中。
        # return_elements=["add:0"]給出了返回的張量的名稱。
        # 在保存的時候給出的是計算節點的名稱"add"。
        # 在加載的時候給出的是張量的名稱,因此是add:0
        result = tf.import_graph_def(graph_def, return_elements=["add:0"])
        print(sess.run(result)) # [array([3.], dtype=float32)]

持久化原理及數據格式 

TensorFlow經過元圖(MetaGraph)來記錄計算圖中節點的信息以及運行計算圖中節點所須要的元數據。元圖是由MetaGraphDef Protocol Buffer定義的。MetaGraphDef中的內容就構成了TensorFlow持久化時第一個文件。下面diamante給出了MetaGraphDef類型的定義

message MetaGraphDef{
    MetaInfoDef meta_info_def = 1;
    GraphDef graph_def = 2;
    SaverDef saver_def = 3;
    map>string,CollectionDef>collection_def = 4;
    map>string,SignatureDef>signature_def = 5;
} 

從上面的代碼能夠看到,元圖主要記錄了5類信息。保存MetaGraphDef信息的文件默認爲.meta爲後綴名。

export_meta_graph函數,能夠以json格式導出MetaGraphDef Protocol Buffer。

下面代碼展現瞭如何使用

# 定義變量相加的計算
v1 = tf.Variable(tf.constant(1.0, shape=[1]), name="v1")
v2 = tf.Variable(tf.constant(2.0, shape=[1]), name="v2")
result1 = v1 + v2

saver = tf.train.Saver()
saver.export_meta_graph("Saved_model/model.ckpt.meda.json", as_text=True)

meta_info_def屬性 

經過MetaInfoDef定義的,記錄了計算圖中的元數據以及程序中全部使用到的運算方法的信息。

graph_def屬性

記錄TensorFlow計算圖上的節點信息

saver_def屬性 

記錄了持久化模型時須要用到的一些參數

collection_def屬性 

維護不一樣集合 

checkpoint

這個文件是Saver類自動生成且自動維護的。維護了持久化的全部模型文件的文件名

TensorFlow最佳實踐樣例程序 

介紹一個TensorFlow訓練神經網絡模型的最佳實踐。將訓練和測試分紅兩個獨立的程序,使得每個組件更加靈活。好比訓練神經網絡的程序能夠持續輸出訓練好的模型,而測試程序能夠每隔一段時間檢驗最新模型的正確率,若是模型效果更好,則將這個模型提供給產品使用。除了將不一樣功能模塊分開,本節還將前向傳播的過程抽象成一個單獨的庫函數。

提供重構以後的程序來解決MNIST問題,重構以後的代碼將會被拆成3個程序,

第一個是mnist_inference.py,定義了前向傳播的過程以及神經網絡中的參數。

第二個是mnist_ train.py,定義了神經網絡的訓練過程。

第三個是mnist_eval.py,定義了測試過程

代碼爲mnist_inference.py

# 定義前向傳播參數以及神經網絡的參數
# -*- coding:utf8 -*-
import tensorflow as tf

# 定義神經網絡結構相關的參數
INPUT_NODE = 784
OUTPUT_NODE = 10
LAYER1_NODE = 500


# 經過tf.get_variable函數來獲取變量。在訓練神經網絡時會建立這些變量;
# 在測試時會經過保存的模型加載這些變量的取值。
# 並且更加方便的是,由於能夠在變量加載時將滑動平均變量重命名,
# 因此能夠直接經過一樣的名字在訓練時使用變量自身,
# 而在測試時使用變量的滑動平均值。這個函數也會將變量的正則化損失加入損失集合
def get_weight_variable(shape, regularizer):
    weights = tf.get_variable("weights", shape, initializer=tf.truncated_normal_initializer(stddev=0.1))
    # 當給出正則化生成函數時,將當前變量的正則化損失加入名字爲losses的集合,
    # 在這裏使用add_to_collection函數將一個張量加入一個集合,而這個集合的名稱爲losses。
    # 這是自定義的集合,不在自動管理的集合列表中
    if regularizer != None: tf.add_to_collection('losses', regularizer(weights))
    return weights


# 定義神經網絡的前向傳播過程
def inference(input_tensor, regularizer):
    # 聲明第一層神經網絡的變量並完成前向傳播過程
    with tf.variable_scope("layer1"):
        # 這裏經過tf.get_variable或tf.Variable沒有本質區別,
        # 由於在訓練或是測試中沒有在同一個程序中屢次調用這個函數。
        # 若是在同一個程序中屢次調用,在第一次調用以後須要將reuse參數設置爲True
        weights = get_weight_variable([INPUT_NODE, LAYER1_NODE], regularizer)
        biases = tf.get_variable("biases", [LAYER1_NODE], initializer=tf.constant_initializer(0.0))
        layer1 = tf.nn.relu(tf.matmul(input_tensor, weights) + biases)

    # 相似的聲明第二層神經網絡的變量並完成前向傳播過程
    with tf.variable_scope("layer2"):
        weights = get_weight_variable([LAYER1_NODE, OUTPUT_NODE], regularizer)
        biases = tf.get_variable("biases", [OUTPUT_NODE], initializer=tf.constant_initializer(0.0))
        layer2 = tf.matmul(layer1, weights) + biases
        return layer2

這段代碼中定義了神經網絡的前向傳播算法。不管是訓練時仍是測試時,均可以直接調用inference這個函數,而不用關心具體的神經網絡結構。使用定義好的前向傳播過程,下面代碼是神經網絡的訓練程序mnist_train.py

# -*- coding:utf-8 -*-
import os
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 加載mnist_inference中定義的常量和前向傳播的函數
import mnist_inference

# 配置神經網絡的參數
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001
TRAINING_STEPS = 30000
MOVING_AVERAGE_DECAY = 0.99

# 模型保存的路徑和文件名
MODEL_SAVE_PATH = "Saved_model/"
MODEL_NAME = "model.ckpt"


def train(mnist):
    # 定義輸入輸出出placeholder
    x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name="x-input")
    y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    # 直接使用mnist_inference.py中定義的前向傳播過程
    y = mnist_inference.inference(x, regularizer)
    global_step = tf.Variable(0, trainable=False)
    # 定義損失函數、學習率、滑動平均操做以及訓練過程
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(LEARNING_RATE_BASE, global_step, mnist.train.num_examples / BATCH_SIZE,
                                               LEARNING_RATE_DECAY, staircase=True)
    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
    with tf.control_dependencies([train_step, variables_averages_op]):
        train_op = tf.no_op(name='train')

    # 初始化TensorFlow持久化類
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        # 訓練過程再也不測試模擬在驗證數據上的表現,驗證和測試的過程會有獨立的程序
        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})
            # 每1000輪保存一次模型
            if i % 1000 == 0:
                # 輸出當前的訓練狀況,只輸出了模型在當前訓練batch上的損失函數的大小。
                # 經過損失函數能夠大概瞭解訓練的狀況。
                print("After %d training step(s), loss on training batch is %g." % (step, loss_value))
                saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME), global_step=global_step)


def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
    train(mnist)


if __name__ == "__main__":
    tf.app.run()

新的訓練代碼中,再也不將訓練和測試跑在一塊兒。訓練過程當中,每1000輪輸出一次在當前訓練batch上損失函數的大小來大體估計訓練的效果。每1000輪保存一次訓練好的模型,這樣能夠經過一個單獨的測試程序,更加方便的在滑動平均模型上作測試。下面代碼是測試程序mnist_eval.py

# -*- coding:utf-8 -*-
import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 加載mnist_inference.py和mnist_train.py中定義的常量和函數
import mnist_inference
import mnist_train

# 每10秒加載一次最新的模型,並在測試數據上測試最新模型的正確率
EVAL_INTERVAL_SECS = 10


def evaluate(mnist):
    with tf.Graph().as_default() as g:
        # 定義輸入輸出的格式
        x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')
        y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
        validate_feed = {x: mnist.validation.images, y_: mnist.validation.labels}
        # 直接經過調用封裝好的函數來計算前向傳播的結果。由於測試時不關注正則化損失的值
        # 因此這裏用於計算正則化損失的函數被設置爲None
        y = mnist_inference.inference(x, None)
        # 使用前向傳播的結果計算正確率。若是須要對未知的樣例進行分類,
        # 那麼使用tf.argmax(y,1)就能夠獲得輸入樣例的預測類別了。
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        # 經過變量重命名的方式來加載模型,這樣在前向傳播的過程當中就不須要調用求滑動平均的函數來獲取平均值了。
        # 這樣就能夠徹底公用mnist_inference.py中定義的前向傳播過程
        variable_averages = tf.train.ExponentialMovingAverage(mnist_train.MOVING_AVERAGE_DECAY)
        variables_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variables_to_restore)
        # 每隔EVAL_INTERVAL_SECS秒調用一次計算正確率的過程以檢測訓練過程當中正確率的變化
        while True:
            with tf.Session() as sess:
                # tf.train.get_checkpoint_state函數經過checkpoint文件自動找到目錄中最新模型的文件名
                ckpt = tf.train.get_checkpoint_state(mnist_train.MODEL_SAVE_PATH)
                if ckpt and ckpt.model_checkpoint_path:
                    # 加載模型
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    # 經過文件名獲得模型保存時迭代的輪數
                    global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[1]
                    accuracy_score = sess.run(accuracy, feed_dict=validate_feed)
                    print("After %s trainning step(s),validation accuracy = %g" % (global_step, accuracy_score))
                else:
                    print("No checkpoint file found")
                    return
            time.sleep(EVAL_INTERVAL_SECS)


def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
    evaluate(mnist)


if __name__ == '__main__':
    tf.app.run()

上面給出的mnist_eval.py程序會每隔10秒運行一次,每次運行都是讀取最新保存的模型,並在MNIST驗證數據集上計算模型的正確率。若是須要離線預測未知數據的類別,只須要將計算正確率的部分改成答案輸出便可。

print("After %s training step(s),validation accuracy = %g" % (global_step, accuracy_score))
print("實際值:%s" % (sess.run(tf.argmax(y_, 1), feed_dict=validate_feed)[0:20]))
print("預測值:%s" % (sess.run(tf.argmax(y, 1), feed_dict=validate_feed)[0:20]))

圖像識別和卷積神經網絡 

神經網絡的結構會對神經網絡的準確率產生巨大的影響。卷積神經網絡(Convolutional Neural Network,CNN)

卷積神經網絡的應用很是普遍,在天然語言處理、醫藥發現、災難氣候發現甚至圍棋人工智能程序中都有應用。

圖像識別問題簡潔及經典數據集

視覺是人類認識時間很是重要的一種知覺。對於人類來講,經過視覺來識別手寫數字、識別圖片中的物體或者找出圖片中人臉的輪廓都是很是簡單的任務。然而對於計算機而言,讓計算機識別圖中的內容就不是一件容易的事情了。圖像識別問題但願藉助計算機程序來處理、分析和理解圖片中的內容,使得計算機能夠從圖片中自動識別各類不一樣模式的目標和對象。

圖像識別問題做爲人工智能的一個重要領域,在最近幾年已經取得了不少突破性的進展。下圖中顯示了圖像識別的主流技術在MNIST數據集上的錯誤率隨着年份的發展趨勢圖。

最下方的淺藍色線表示人工標註的錯誤率,其餘不一樣的線段表示了不一樣的算法的錯誤率。相比其餘算法,卷積神經網絡能夠獲得更低的錯誤率。並且經過卷積神經網絡達到的錯誤率已經很是接近人工標註的錯誤率了。在MNIST數據集的一萬個測試數據上,最好的深度學習算法只會比人工識別多錯一張圖片。

MNIST手寫體識別數據集是一個相對簡單的數據集,在其餘更加複雜的圖像識別數據集上,卷積神經網絡有更加突出的表現。Cifar數據集就是一個影響力很大的圖像分類數據集。Cifar數據集分爲了Cifar-10和Cifar-100兩個問題,都是圖像詞典項目(Visual Dictionary)中800萬張圖片的一個子集。Cifar數據集中的圖片爲32*32的彩色圖片。

Cifar-10問題收集了10個不一樣種類的60000張圖片。圖片的像素僅爲32*32,放大以後是比較模糊的,可是能夠看出輪廓

下載地址:https://www.cs.toronto.edu/~kriz/cifar.html

OneDrive有python的

和MNIST數據集相似,Cifar-10中的圖片大小都是固定的且每一張圖片中僅包含一個種類的實體。但和MNIST相比,Cifar數據集最大的區別在於圖片由黑白變成了彩色,且分類的難度也相對更高。在Cifar-10數據集上,人工標註的正確率大概爲94%,比MNIST數據集上的人工表現要低不少。

不管是MNIST數據集仍是Cifar數據集,相比真實環境下的圖像識別問題,有2個最大的問題。

第一,現實生活中的圖片分辨率要遠高於32*32,並且圖像的分辨率也不會是固定的。

第二,現實生活中的物體類別不少,不管是10種仍是100種都遠遠不夠,並且一張圖片中不會只出現一個種類的物體。

爲了更加貼近真實環境下的圖像識別問題,斯坦福大學的李飛飛教授帶頭整理的ImageNet很大程度地解決了這兩個問題。

ImageNet是一個基於WordNet的大型圖像數據庫,在ImageNet中,將近1500萬圖片唄關聯到WordNet的大約20000個名詞同義詞集上。目前每個與ImageNet相關的WordNet同義詞集都表明了現實世界中的一個實體,能夠被認爲是分類問題中的一個類別。ImageNet圖片都是互聯網上爬取下來的,而且經過亞馬遜的人工標註服務將圖片分了到WordNet的同義詞集上。在ImageNe的圖片中,一張圖片可能出現多個同義詞集所表明的實體。

上圖中用幾個矩形框出了不一樣實體的輪廓。在物體識別問題中,通常講用於框出不一樣實體的輪廓。在物體識別問題中,通常講用於框出實體的矩形稱爲bounding box。在圖中能夠找到四個實體,兩把椅子、一我的和一條狗。

ImageNet每一年都舉辦圖像識別相關的競賽(ImageNet LargeScale Visual Recognition Challengs,ILSVRC),並且每一年的競賽都會有一些不一樣的問題,這些問題基本涵蓋了圖像識別的主要研究方向。

ImageNet官網:http://www.image-net.org/(目前被牆的很嚴重)

ILSVRC2012圖像分類數據集的任務和Cifar數據集是基本一致的,也是識別圖像中的主要物體。ILSVRC2012圖像分類數據包含了來自1000個類別的120萬張圖片,其中每張圖片屬於且只屬於一個類別。由於ILSVRC2012圖像分類數據集中的圖片是直接從互聯網上爬取獲得的,因此圖片的大小從幾千字節到幾百萬字節不等。

上圖給出了不一樣算法在ImageNet圖像分類數據集上的top-5正確率。top-N正確率指的是圖像識別算法給出前N個答案中有一個是正確的機率。在圖片分類問題上,不少學術論文都將前N個答案中有一個是正確的機率。在圖像分類問題上,不少學術論文都將前N個答案的正確率做爲比較的方法,其中N的取值通常爲3或5。在更加複雜的ImageNet問題上,基於卷積神經網絡的圖像識別算法能夠遠遠超過人類的表現。左側對比了傳統算法與深度學習算法的正確率。深度學習,特別是卷積神經網絡,給圖像識別問題帶來了質的飛躍。2013年以後,基本上全部的研究都集中到了深度學習算法上。

卷積神經網絡簡介

前面所介紹的神經網絡每兩層之間的全部結點都是由邊相連的,因此本書稱這種網絡結構爲全鏈接層網絡結構。

爲了將只包含全鏈接層的神經網絡與卷積神經網絡、循環神經網絡區分開,將只包含全鏈接層的神經網絡稱之爲全鏈接神經網絡

上圖顯示了全鏈接神經網絡與卷積神經網絡的結構對比圖。

雖然圖中顯示的全鏈接神經網絡結構和卷積神經網絡的結構直觀上差別比較大,但實際上它們的總體架構是很是類似的。卷積神經網絡也是經過一層一層的節點組織起來的。和全鏈接神經網絡同樣,卷積神經網絡中的每個節點都是一個神經元。在全鏈接神經網絡中,每相鄰兩層之間的節點都有邊相連,因而通常會將每一層全鏈接層中的節點組織成一列,這樣方便顯示鏈接結構。而對於卷積神經網絡,相鄰兩層之間只有部分節點相連,爲了展現每一層神經元的維度,通常會將每一層卷積層的節點組成一個三維矩陣。

 

除告終構類似,卷積神經網絡的輸入輸出以及訓練流程與全鏈接神經網絡也基本一致。以圖像分類爲例,卷積神經網絡的輸入層就是圖像的原始像素,而輸出層的每個節點表明了不一樣類別的可信度。這和全鏈接神經網絡的輸入輸出是一致的。相似損失函數以及參數的優化過程也都適用於卷積神經網絡。

卷積神經網絡的流程和訓練一個全鏈接神經網絡沒有任何區別。惟一區別就在於神經網絡相鄰兩層的鏈接方式。進一步介紹卷積神經網絡的連接結構以前,先說一下爲何全鏈接神經網絡沒法很好的處理圖像數據。

使用全鏈接神經網絡處理圖像的最大問題在於全鏈接層的參數太多。對於MNIST數據,每一張圖片的大小是28*28*1,其中28*28爲圖片的大小,*1表示圖像是黑白的,只有一個色彩通道。

假設第一層隱藏層的節點數爲500,那麼一個全連接層的神經網絡將有28*28*500+500=392500個參數。當圖片更大時,好比在Cifar-10數據集中,圖片的大小爲32*32*3,其中32*32表示圖片的大小,*3表示圖片是經過紅綠藍三個色彩通道表示的。這樣輸入層就有3072個節點,若是隱藏層仍然是500個節點,那麼全連接神經網絡將有3072*500+500*150萬個參數。參數增多除了致使計算速度減慢,還很容易致使過擬合問題。因此須要一個更合理的神經網絡結構,來有效地減小神經網絡中參數個數。卷積神經網絡就能夠達到這個目的。

在卷積神經網絡的前幾層中,每一層的節點都被組織成一個三維矩陣。好比處理Cifar-10數據集中的圖片時,能夠將輸入層組織成一個32*32*3的三維矩陣。虛線部分展現了卷積神經網絡的一個鏈接示意圖,圖中能夠看出卷積神經網絡中前幾層中每個節點只和上一層中部分的節點相連。

一個卷積神經網絡主要由如下5種結構組成:

1. 輸入層是整個神經網絡的輸入,在處理圖像的卷積神經網絡中,它通常表明了一張圖片的像素矩陣。最左側的三維矩陣就能夠表明一張圖片。其中三維矩陣的長和寬表明了圖像的大小,而三維矩陣的深度表明了圖像的色彩通道。好比黑白圖片的深度爲1,而在RGB色彩模式下,圖像的深度爲3。從輸入層開始,卷積神經網絡經過不一樣的神經網絡將上一層的三維矩陣轉化爲下一層的三維矩陣,直到最後的全鏈接層。

2. 卷積層是一個卷積神經網絡中最爲重要的部分。和傳統全鏈接層不一樣,卷積層中每個節點的輸入只是上一層神經網絡的一小塊,這個小塊經常使用的大小有3*3或者5*5。卷積層試圖將神經網絡中的每一小塊進行更加深刻地分析從而獲得抽象程度更高的特徵。經過卷積層處理過的節點矩陣會變得更深。

3.池化層神經網絡不會改變三維矩陣的深度,可是它能夠縮小矩陣的大小。池化操做能夠認爲是將一張分辨率較高的圖片轉化爲分辨率較低的圖片。經過池化層,能夠進一步縮小最後全鏈接層中節點的個數,從而達到減小整個神經網絡中參數的目的。

4.全鏈接層,在通過多輪卷積層和池化層處理以後,在卷積神經網絡的最後通常會是由1到2個全鏈接層來給出最後的分類結果。通過幾輪卷積層和池化層的處理以後,能夠認爲圖像中的信息已經被抽象成了信息含量更高的特徵。咱們能夠將卷積層和池化層當作自動圖像特徵提取的過程。特徵提取完成以後,仍然須要使用全鏈接層來完成分類任務。

5.Softmax層主要用於分類問題。經過Softmax層,能夠獲得當前樣例屬於不一樣種類的機率分佈狀況。

在卷積神經網絡中使用到的輸入層、全鏈接層和Softmax層都有過詳細介紹,再也不闡述。下面詳細介紹卷積神經網絡中特殊的兩個網絡結構--卷積層和池化層

卷積神經網絡經常使用結構

卷積層

上圖顯示了卷積層神經網絡結構中最重要的部分。這個部分被稱之爲過濾器(filter)或者內核(kernel),這裏統稱爲過濾器。過濾器能夠將當前層神經網絡上的一個子節點矩陣轉化爲下一層神經網絡上的一個單位節點矩陣。

單位節點矩陣指的是一個長和寬都爲1,但深度不限的節點矩陣。

在一個卷積層中,過濾器所處理的節點矩陣的長和寬都是由人工指定的,這個節點矩陣的尺寸也被稱之爲過濾器的尺寸。經常使用的過濾器尺寸有3*3或者5*5,由於過濾器處理的矩陣深度和當前層神經網絡節點矩陣的深度是一致的,因此雖然節點矩陣是三維的,但過濾器的尺寸只須要指定兩個維度。過濾器中另一個須要人工指定的設置是處理獲得的單位節點矩陣的深度,這個設置稱爲過濾器的深度。注意過濾器的尺寸指的是一個過濾器輸入節點矩陣的大小,而深度指的是輸出單位節點矩陣的深度。 

如上圖所示,左側小矩陣的尺寸爲過濾器的尺寸,而右側單位矩陣的深度爲過濾器的深度。經過一些經典卷積神經網絡結構來了解如何設置每一層卷積層過濾器的尺寸和深度。

過濾器的前向傳播過程就是經過左側小矩陣中的節點計算出右側單位矩陣中節點的過程。爲了直觀地解釋過濾器的前向傳播過程,下面給出一個具體的樣例。

這個樣例中將展現如何經過過濾器將一個2*2*3的節點矩陣變化爲一個1*1*5的單位節點矩陣。

一個過濾器的前向傳播過程和全鏈接層類似,總共須要2*2*3*5+5=65個參數,其中最後的+5爲偏置項參數的個數。假設使用Wix,y,z來表示對於輸出單位節點矩陣中的第i個節點,過濾器輸入節點(x,y,z)的權重,使用bi表示第i個輸出節點對應的偏置項參數,那麼單位矩陣中的第i個節點的取值g(i)爲:

其中ax,y,z爲過濾中節點(x,y,z)的取值,f爲激活函數。

上圖展現了在給定a,w0和b0的狀況下,使用ReLU做爲激活函數時g(0)的計算過程。在左側給出了a和w0的取值,這裏經過3個二維矩陣來表示一個三維矩陣的取值,其中每個二維矩陣表示三維矩陣在某一個深度上的取值。

• 符號表示點積,也就是矩陣中對應元素乘積的和。右側顯示了g(0)的計算過程。若是給出w1到w4和b1到b4,那麼能夠相似地計算出g(1)到g(4)的取值。若是將a和wi組織成兩個向量,那麼一個過濾器的計算過程徹底能夠經過向量乘法來完成。

上面介紹了在卷積層中計算一個過濾器的前向傳播過程。卷積層結構的前向傳播過程就是經過將一個過濾器從神經網絡當前層的左上角移動到右下角,而且在移動中計算每個對應的單位矩陣獲得的。

上圖展現了卷積層結構前向傳播的過程。爲了更好地可視化過濾器的移動過程,使用的節點矩陣深度都爲1。左側展現了3*3矩陣上使用2*2過濾器的卷積前向傳播過程。首先將這個過濾器用於左上角子矩陣,而後移動到右上角矩陣,再到左下角矩陣,最後到右下角矩陣。過濾器每移動一次,能夠計算獲得一個值(當深度爲k時會計算出k個值)。將這些數值拼接成一個新的矩陣,就完成了卷積層前向傳播的過程。右側顯示了過濾器在移動過程當中計算獲得的結果與新矩陣中節點的對應關係。

當過濾器的大小不爲1*1時,卷積層前向傳播獲得的矩陣的尺寸要小於當前層矩陣的尺寸。當前層矩陣的大小爲3*3,經過卷積層前向傳播算法以後,獲得的矩陣大小爲2*2。爲了不尺寸的變化,能夠在當前層矩陣的邊界上加入全0填充。這樣能夠使得卷積層前向傳播結果矩陣的大小和當前層矩陣保持一致。

顯示了使用全0填充後卷積層前向傳播過程示意圖,圖中能夠看出,加入一層全0填充後,獲得的結構矩陣大小就爲3*3了。

除了使用全0填充,還能夠經過設置過濾器移動的步長來調整結果矩陣的大小。在上圖中,過濾器每次都只移動一格

上圖顯示了當移動步長爲2且使用全0填充時,卷積層前向傳播的過程。

能夠看出當長和寬的步長均爲2時,過濾器每隔2步計算一次結果,獲得的結果矩陣的長和寬也就都只有原來的一半。下面公式給出了在同時使用全0填充時結果矩陣的大小

其中outlength表示輸出層矩陣的長度,等於輸入層矩陣長度除以長度方向上的步長的向上取整值。相似的,outwidth表示輸出層矩陣的寬度,等於輸入層矩陣寬度除以寬度方向上的步長的向上取整值。若是不使用全0填充,下面的公式給出告終果矩陣的大小。

卷積神經網絡中,每個卷積層中使用的過濾器中的參數都是同樣的。這是卷積神經網絡的一個很是重要的性質。從直觀上理解,共享過濾器的參數能夠使得圖像上的內容不受位置的影響。以MNIST手寫體數字識別爲例,不管數字"1"出如今左上角仍是右下角,圖片的種類都是不變的。由於在左上角和右下角使用的過濾器參數相同,因此經過卷積層以後不管數字在圖像上的那個位置,獲得的結果都是同樣的。

共享每個卷積層中過濾器中的參數能夠巨幅減小神經網絡上的參數。以Cifar-10問題爲例,輸入層矩陣的維度是32*32*3.假設第一層卷積層使用尺寸爲5*5,深度爲16的過濾器。那麼這個卷積層的參數個數爲5*5*3*16+16=1216個。相比之下,卷積層的參數個數要遠遠小於全鏈接層。並且卷積層的參數個數和圖片的大小無關,只和過濾器的尺寸、深度以及當前層節點矩陣的深度有關。使得卷積神經網絡能夠很好地擴展到更大的圖像數據上。

結合過濾器的使用方法和參數共享的機制,下圖給出了使用全0填充、步長爲2的卷積層前向傳播的計算流程

圖中給出了過濾器上權重的取值以及偏置量的取值,經過計算能夠獲得每個格子的具體取值。下面給出了左上角格子取值的計算方法,其餘格子能夠依次類推

左上:ReLU(0*1+0*-1+0*0+1*2+1)=ReLU(3)=3

右上:ReLU(0*1+0*-1+-1*0+0*2+1)=ReLU(1)=1

左下:ReLU(0*1+-1*-1+0*0+0*2+1)=ReLU(2)=2

右下:ReLU(2*1+1*-1+2*0+-2*2+1)=ReLU(0)=0

TensorFlow對卷積神經網絡提供了很是好的支持,下面的程序實現了一個卷積層的前向傳播過程。如下代碼能夠看出,經過TensorFlow實現卷積層是很是方便的。

import tensorflow as tf

# 經過tf.get_variable的方式建立過濾器的權重變量和偏執變量。
# 卷積層的參數個數只和過濾器的尺寸、深度以及當前參數層節點矩陣的深度有關,
# 因此這裏聲明的參數變量是一個四維矩陣,
# 前面兩個維度表明了過濾器的尺寸,
# 第三個維度表明了當前層的深度,
# 第四個維度表示過濾器的深度。
filter_weight = tf.get_variable("weights", [5, 5, 3, 16], initializer=tf.truncated_normal_initializer(stddev=0.1))
# 和卷積層的權重相似,當前層矩陣上不一樣位置的偏置量也是共享的,
# 因此總共有下一層深度個不一樣的偏置量。
# 代碼中16爲過濾器的深度,也是神經網絡下一層節點矩陣的深度
biases = tf.get_variable('biases', [16], initializer=tf.constant_initializer(0.1))

# tf.nn.conv2d提供了一個很是方便的函數來實現卷積層前向傳播的算法。
# 這個函數的第一個輸入爲當前層的節點矩陣。
# 注意這個矩陣是一個四維矩陣,後面三個維度對應一個節點矩陣,
# 第一維對應一個輸入batch。
# 好比在輸入層,input[0,:,:,:]表示第一張圖片,input[1,:,:,:]表示第二張圖片,以此類推。
# tf.nn.conv2d第二個參數提供了卷積層的權重,
# 第三個參數爲不一樣維度上的步長。
# 雖然第三個參數提供的是一個長度爲4的數組,可是第一維和最後一維的數字要求必定是1.
# 這是由於卷積層的步長只對矩陣的長和寬有效。
# 最後一個參數是填充的方法,TensorFlow提供SAME或是VALID兩種選擇。其中SAME表示添加全0填充,VALID表示不添加
conv = tf.nn.conv2d(input, filter_weight, strides=[1, 1, 1, 1], padding="SAME")

# tf.nn.bias_add提供了一個方便的函數給每個節點加上偏置項。這裏不能直接使用加法,
# 由於矩陣上不一樣位置上的節點都須要加上一樣的偏置項。
# 雖然下一層神經網絡的大小爲2*2,可是偏置項只有一個數(由於深度是1),
# 而2*2矩陣中的每個值都須要加上這個偏置項。
bias = tf.nn.bias_add(conv, biases)

# 將計算結果經過ReLU激活函數完成去線性化
actived_conv = tf.nn.relu(bias)

池化層

池化層能夠很是有效地縮小矩陣的尺寸,從而減小最後全鏈接層中的參數。使用池化層既能夠加快計算速度,也有防止過擬合問題的做用。

池化層前向傳播的過程也是經過移動一格相似過濾器的結構完成的。不過池化層過濾器中的計算不是節點的加權和,而是採用更加簡單的最大值或者平均值運算。使用最大值操做的池化層被稱之爲最大池化層(max pooling),這是被使用得最多的池化層結構。使用平均值操做的池化層被稱之爲平均池化層(average pooling)。其餘池化層在實踐中使用的比較少

與卷積層的過濾器相似,池化層的過濾器須要人工設定過濾器的尺寸、是否使用全0填充以及過濾器移動的步長等設置,並且這些設置的意義也是同樣的。卷積層和池化層中過濾器移動的方式是類似的,惟一的區別在於卷積層使用的過濾器是橫跨整個深度的。而池化層使用的過濾器隻影響一個深度上的節點。因此池化層的過濾器除了在長和寬兩個維度移動以外,還須要在深度這個維度移動。下圖展現了一個最大池化層前向傳播計算過程。

不一樣顏色或者不一樣線段(虛線或者實現)表明了不一樣的池化層過濾器。能夠看出,池化層的過濾器除了在長和寬的維度上移動,還須要在深度的維度上移動。下面的TensorFlow程序實現了最大池化層的前向傳播算法。

# tf.nn.max_pool實現了最大池化層的前向傳播過程,參數和tf.nn.conv2d函數相似。
# ksize提供了過濾器的尺寸,strides提供了步長信息,padding提供了是否全0填充。
pool = tf.nn.max_pool(actived_conv, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')

對比池化層和卷積層前向傳播在TensorFlow中的實現,能夠發現函數的參數形式是類似的。在tf.nn.max_pool函數中,首先須要傳入當前層的節點矩陣,這個矩陣是一個四維矩陣,格式和tf.nn.conv2d函數中的第一個參數一致。第二個參數爲過濾器的尺寸。雖然給出的是一個長度爲4的一維數組,可是這個數組的第一個和最後一個數必須爲1。這意味着池化層的過濾器是不能夠跨不一樣輸入樣例或者節點矩陣深度的。在實際應用中使用得最多的池化層過濾器尺寸爲[1,2,2,1]或者[1,3,3,1]

tf.nn.max_pool函數的第三個參數爲步長,和tf.nn.conv2d函數中步長的意義是同樣的,並且第一維和最後一維也只能爲1。這意味着在TensorFlow中,池化層不能減小節點矩陣的深度或者輸入樣例的個數。tf.nn.max_pool函數的最後一個參數指定了是否使用全0填充。 這個參數也只有兩種取值--VALID或者SAME,其中VALID表示不使用全0填充,SAME表示使用全0填充。TensorFlow還提供了tf.nn.avg_pool來實現平均池化層。tf.nn.avg_pool函數的調用格式和tf.nn.max_pool函數是一致的。

經典卷積網絡模型

經過這些網絡結構任意組合獲得的神經網絡有無限多種,怎麼樣的神經網絡更有可能解決真實的圖像處理問題呢?

LeNet-5模型

LetNet-5模型是Yann LeCun教授與1998年在論文中提出的,它是第一個成功應用於數字識別問題的卷積神經網絡。在MNIST數據集上,LetNet-5模型能夠達到大約99.2%正確率。LeNet-5模型總共有7層,下圖展現了LeNet-5模型的架構。

下面詳細介紹LeNet-5模型每一層的結構

第一層,卷積層

第一層的輸入就是原始的圖像像素,LeNet-5模型接受的輸入層大小爲32*32*1,。第一個卷積層過濾器的尺寸爲5*5,深度爲6,不使用全0填充,步長爲1。由於沒有使用全0填充,因此這一層的輸出的尺寸爲32-5+1=28,深度爲6.這一個卷積層總共有5*5*1*6+6=156個參數,其中6個爲偏置項參數。由於下一層的節點矩陣有28*28*6=4704個節點,每一個節點和5*5=25個當前層節點相連,因此本層卷積層總共有4704*(25+1)=122304個鏈接。

第二層,池化層

這一層的輸入爲第一層的輸出,是一個28*28*6的節點矩陣。本層採用的過濾器大小爲2*2,長和寬的步長均爲2,因此本層的輸出矩陣大小爲14*14*6。原始的LeNet-5模型中使用的過濾器有細微差異。

第三層,卷積層

本層的輸入矩陣大小爲14*14*6,使用的過濾器大小爲5*5,深度爲16。本層不使用全0填充,步長爲1。本層的輸出矩陣大小爲10*10*16.按照標準的卷積層,本層應該有5*5*6*16+16=2416個參數,10*10*16*(25+1)=41600個鏈接。

第四層,池化層

本層的輸入矩陣大小爲10*10*16,採用的過濾器大小爲2*2,步長爲2。本層的輸出矩陣大小爲5*5*16

第五層,全鏈接層

本層的輸入矩陣大小爲5*5*16,在LeNet-5模型的論文中將這一層稱之爲卷積層,可是由於過時率的大小就是5*5,因此和全鏈接層沒有區別,在以後的TensorFlow程序實現中也會將這一層當作全鏈接層。若是將5*5*16矩陣中的節點拉成一個向量,那麼這一層和全鏈接層輸入就同樣了。本層的輸出節點個數爲120,總共有5*5*16*120+120=48120個參數

第六層,全鏈接層

本層的輸入節點個數爲120個,輸出節點個數爲84個,總共參數爲120*84+84=10164個

第七層,全鏈接層

本層的輸入節點個數爲84個,輸出節點個數爲10個,總共參數爲84*10+10=850個

上面介紹了LeNet-5模型每一層結構和設置,下面給出一個TensorFlow程序來實現一個相似LeNet-5模型的卷積神經網絡來解決MNIST數字識別問題。經過TensorFlow訓練卷積神經網絡的過程和訓練全鏈接神經網絡是同樣的。損失函數的計算、反向傳播過程的實現均可以複用mnist_train.py程序。惟一的區別在於卷積神經網絡的輸入層爲一個三維矩陣,因此須要調整一個輸入數據的格式:

tf.variable_scope可讓變量有相同的命名,包括tf.get_variable獲得的變量,還有tf.Variable的變量。

tf.name_scope可讓變量有相同的命名,只是限於tf.Variable的變量。若是上下文管理器中有重名的get_variable會報錯。

LeNet5_train.py

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import LeNet5_infernece
import os
import numpy as np

# 定義神經網絡相關參數
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.01
LEARNING_RATE_DECAY = 0.99
REGULARIZATION_RATE = 0.0001
TRAINING_STEPS = 6000
MOVING_AVERAGE_DECAY = 0.99

# 模型保存的路徑和文件名
MODEL_SAVE_PATH = "Saved_model/"
MODEL_NAME = "model.ckpt"

# 定義訓練過程
def train(mnist):
    # 定義輸出爲4維矩陣的placeholder
    x = tf.placeholder(tf.float32, [BATCH_SIZE, LeNet5_infernece.IMAGE_SIZE, LeNet5_infernece.IMAGE_SIZE,
                                    LeNet5_infernece.NUM_CHANNELS], name='x-input')
    y_ = tf.placeholder(tf.float32, [None, LeNet5_infernece.OUTPUT_NODE], name='y-input')

    regularizer = tf.contrib.layers.l2_regularizer(REGULARIZATION_RATE)
    y = LeNet5_infernece.inference(x, False, regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 定義損失函數、學習率、滑動平均操做以及訓練過程。
    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(LEARNING_RATE_BASE, global_step, mnist.train.num_examples / BATCH_SIZE,
                                               LEARNING_RATE_DECAY, staircase=True)

    train_step = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)
    with tf.control_dependencies([train_step, variables_averages_op]):
        train_op = tf.no_op(name='train')

    # 初始化TensorFlow持久化類。
    saver = tf.train.Saver()
    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        for i in range(TRAINING_STEPS):
            xs, ys = mnist.train.next_batch(BATCH_SIZE)

            reshaped_xs = np.reshape(xs, (
                BATCH_SIZE, LeNet5_infernece.IMAGE_SIZE, LeNet5_infernece.IMAGE_SIZE, LeNet5_infernece.NUM_CHANNELS))
            _, loss_value, step = sess.run([train_op, loss, global_step], feed_dict={x: reshaped_xs, y_: ys})

            if i % 1000 == 0:
                print("After %d training step(s), loss on training batch is %g." % (step, loss_value))
                saver.save(sess, os.path.join(MODEL_SAVE_PATH, MODEL_NAME), global_step=global_step)


def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
    train(mnist)


if __name__ == "__main__":
    tf.app.run()

LeNet5_inference.py 

import tensorflow as tf

# 設定神經網絡參數
INPUT_NODE = 784
OUTPUT_NODE = 10

IMAGE_SIZE = 28
NUM_CHANNELS = 1
NUM_LABELS = 10

CONV1_DEEP = 32
CONV1_SIZE = 5

CONV2_DEEP = 64
CONV2_SIZE = 5

FC_SIZE = 512


# 定義前向傳播參數
def inference(input_tensor, train, regularizer):
    with tf.variable_scope('layer1-conv1'):
        conv1_weights = tf.get_variable("weight", [CONV1_SIZE, CONV1_SIZE, NUM_CHANNELS, CONV1_DEEP],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv1_biases = tf.get_variable("bias", [CONV1_DEEP], initializer=tf.constant_initializer(0.0))
        conv1 = tf.nn.conv2d(input_tensor, conv1_weights, strides=[1, 1, 1, 1], padding='SAME')
        relu1 = tf.nn.relu(tf.nn.bias_add(conv1, conv1_biases))

    with tf.name_scope("layer2-pool1"):
        pool1 = tf.nn.max_pool(relu1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding="SAME")

    with tf.variable_scope("layer3-conv2"):
        conv2_weights = tf.get_variable("weight", [CONV2_SIZE, CONV2_SIZE, CONV1_DEEP, CONV2_DEEP],
                                        initializer=tf.truncated_normal_initializer(stddev=0.1))
        conv2_biases = tf.get_variable("bias", [CONV2_DEEP], initializer=tf.constant_initializer(0.0))
        conv2 = tf.nn.conv2d(pool1, conv2_weights, strides=[1, 1, 1, 1], padding='SAME')
        relu2 = tf.nn.relu(tf.nn.bias_add(conv2, conv2_biases))

    with tf.name_scope("layer4-pool2"):
        pool2 = tf.nn.max_pool(relu2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
        pool_shape = pool2.get_shape().as_list()
        nodes = pool_shape[1] * pool_shape[2] * pool_shape[3]
        reshaped = tf.reshape(pool2, [pool_shape[0], nodes])

    with tf.variable_scope('layer5-fc1'):
        fc1_weights = tf.get_variable("weight", [nodes, FC_SIZE],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        if regularizer is not None:
            tf.add_to_collection('losses', regularizer(fc1_weights))
        fc1_biases = tf.get_variable("bias", [FC_SIZE], initializer=tf.constant_initializer(0.1))

        fc1 = tf.nn.relu(tf.matmul(reshaped, fc1_weights) + fc1_biases)
        if train:
            fc1 = tf.nn.dropout(fc1, 0.5)

    with tf.variable_scope('layer6-fc2'):
        fc2_weights = tf.get_variable("weight", [FC_SIZE, NUM_LABELS],
                                      initializer=tf.truncated_normal_initializer(stddev=0.1))
        if regularizer is not None:
            tf.add_to_collection('losses', regularizer(fc2_weights))
        fc2_biases = tf.get_variable("bias", [NUM_LABELS], initializer=tf.constant_initializer(0.1))
        logit = tf.matmul(fc1, fc2_weights) + fc2_biases

    return logit

LeNet5_eval.py

# -*- coding:utf-8 -*-
import time
import numpy
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

# 加載mnist_inference.py和mnist_train.py中定義的常量和函數
import LeNet5_infernece
import LeNet5_train

# 每10秒加載一次最新的模型,並在測試數據上測試最新模型的正確率
EVAL_INTERVAL_SECS = 10


def evaluate(mnist):
    with tf.Graph().as_default() as g:
        # 定義輸入輸出的格式
        x = tf.placeholder(tf.float32,
                           [mnist.validation.num_examples, LeNet5_infernece.IMAGE_SIZE, LeNet5_infernece.IMAGE_SIZE,
                            LeNet5_infernece.NUM_CHANNELS], name="x-input")

        y_ = tf.placeholder(tf.float32, [None, LeNet5_infernece.OUTPUT_NODE], name='y-input')

        xs, ys = mnist.validation.images, mnist.validation.labels
        reshaped_xs = numpy.reshape(xs, (
            mnist.validation.num_examples, LeNet5_infernece.IMAGE_SIZE, LeNet5_infernece.IMAGE_SIZE,
            LeNet5_infernece.NUM_CHANNELS))
        validate_feed = {x: reshaped_xs, y_: ys}

        # 直接經過調用封裝好的函數來計算前向傳播的結果。由於測試時不關注正則化損失的值
        # 因此這裏用於計算正則化損失的函數被設置爲None
        y = LeNet5_infernece.inference(x, False, None)
        # 使用前向傳播的結果計算正確率。若是須要對未知的樣例進行分類,
        # 那麼使用tf.argmax(y,1)就能夠獲得輸入樣例的預測類別了。
        correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(y_, 1))
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
        # 經過變量重命名的方式來加載模型,這樣在前向傳播的過程當中就不須要調用求滑動平均的函數來獲取平均值了。
        # 這樣就能夠徹底公用mnist_inference.py中定義的前向傳播過程
        variable_averages = tf.train.ExponentialMovingAverage(LeNet5_train.MOVING_AVERAGE_DECAY)
        variables_to_restore = variable_averages.variables_to_restore()
        saver = tf.train.Saver(variables_to_restore)
        # 每隔EVAL_INTERVAL_SECS秒調用一次計算正確率的過程以檢測訓練過程當中正確率的變化
        while True:
            with tf.Session() as sess:
                # tf.train.get_checkpoint_state函數經過checkpoint文件自動找到目錄中最新模型的文件名
                ckpt = tf.train.get_checkpoint_state(LeNet5_train.MODEL_SAVE_PATH)
                if ckpt and ckpt.model_checkpoint_path:
                    # 加載模型
                    saver.restore(sess, ckpt.model_checkpoint_path)
                    # 經過文件名獲得模型保存時迭代的輪數
                    global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[1]
                    accuracy_score = sess.run(accuracy, feed_dict=validate_feed)
                    print("After %s training step(s),validation accuracy = %g" % (global_step, accuracy_score))
                    print("實際值:%s" % (sess.run(tf.argmax(y_, 1), feed_dict=validate_feed)[0:50]))
                    print("預測值:%s" % (sess.run(tf.argmax(y, 1), feed_dict=validate_feed)[0:50]))
                else:
                    print("No checkpoint file found")
            time.sleep(EVAL_INTERVAL_SECS)


def main(argv=None):
    mnist = input_data.read_data_sets("MNIST_data", one_hot=True)
    evaluate(mnist)


if __name__ == '__main__':
    tf.app.run()

在MNIST測試數據上,卷積神經網絡能夠達到大約99.4%的正確率。而全鏈接最高能夠達到98.4%的正確率。卷積神經網絡能夠巨幅提升神經網絡在MNIST數據集上的正確率。

然而LeNet-5模型就沒法很好地處理相似ImageNet這樣比較大的圖像數據集。

下面的正則表達式公式總結了一些經典的用於圖片分類問題的卷積神經網絡架構:

 

上面的公式中,卷積層+,表示一層或者多層卷積層,大部分卷積神經網絡中通常最多連續使用三層卷積層。

池化層?,表示沒有或者一層池化層。池化層雖然能夠起到減小參數防止過擬合問題,可是也能夠直接經過調整卷積層步長來完成。全部有些卷積層神經網絡中沒有池化層。

多輪卷積層和池化層以後,卷積神經網絡在輸出以前通常會通過1~2個全鏈接層。好比LeNet-5模型就能夠表示爲下面的結構

除了LeNet-5模型,2012年ImageNet ILSVRC圖像分類挑戰的第一名AlexNet模型、2013年ILSVRC第一名ZF Net模型以及2014年第二名VGGNet中做者嘗試過不一樣的卷積神經網絡架構。

從下表中能夠看出這些卷積神經網絡架構都知足介紹的正則表達式。

其中conv*表示卷積層,maxpool表示池化層,FC-*表示全鏈接層,soft-max爲softmax結構

有了卷積神經網絡的架構,那麼每一層卷積層或者池化層中的配置須要如何設置呢?

convX-Y表示過濾器的邊長爲X,深度爲Y。好比conv3-64表示過濾器的長和寬都爲3,深度爲64。VGG Net中過濾器邊長通常爲3或者1。在LeNet-5模型中,也使用了邊長爲5的過濾器。通常卷積層的過濾器邊長不會超過5,可是有些卷積神經網絡結構中,處理輸入的卷積層中使用了邊長爲7甚至是11的過濾器。

過濾器的深度上,大部分卷積神經網絡都採用逐層遞增的方式。好比上表中,每通過一次池化層以後,卷積過濾器的深度會乘以2。雖然不一樣的模型會選擇使用不一樣的具體數字,可是逐層遞增是比較廣泛的模型。卷積層的步長通常爲1,可是在有些模型中也會使用2或者3做爲步長。池化層的配置相對簡單,用的最多的是最大池化層。池化層的過濾器邊長通常爲2或者3,步長也通常爲2或者3。

Inception-v3模型

Inception結構是一種和LeNet-5結構徹底不一樣的卷積神經網絡結構。在LeNet-5模型中,不一樣卷積層經過串聯的方式鏈接在一塊兒,而Inception-v3模型中的Inception結構是將不一樣的卷積層經過並聯的方式結合在一塊兒。

一個卷積層能夠使用邊長爲一、3或者5的過濾器,那麼如何在這些邊長中選擇呢。Inception模塊給出了一個方案,那就是同時使用全部不一樣尺寸的過濾器,而後再將獲得的矩陣拼接起來。

 

從上圖中能夠看出,Inception模塊會首先使用不一樣尺寸的過濾器處理輸入矩陣。最上方矩陣爲使用邊長爲1的過濾器的卷積層前向傳播的結果。相似的,中間矩陣使用過濾器邊長爲3,下方矩陣使用的過濾器邊長爲5。不一樣的矩陣表明了Inception模塊中的一條計算路徑。雖然過濾器的大小不一樣,但若是全部的過濾器都使用全0填充且步長爲1,那麼前向傳播獲得的結果矩陣的長和寬都與輸入矩陣一致。這樣通過不一樣過濾器處理的結果矩陣能夠拼接成一個更深的矩陣。

Inception模塊獲得的結果矩陣的長和寬都與輸入矩陣一致。這樣通過不一樣過濾器處理的結果矩陣能夠拼成一個更深的矩陣。能夠將他們在深度這個維度上組合起來。

上圖所示的Inception模塊獲得的結果矩陣的長和寬與輸入同樣,深度爲紅黃藍三個矩陣深度的和。上圖展現的是Inception模塊的核心思想,真正的Inception-v3模塊中使用的Inception模塊要更加複雜且多樣。

Inception-v3模塊總共有46層,由11個Inception模塊組成。方框標註出來的結構就是一個Inception模塊。在Inception-v3模塊中有96個卷積層。

爲了更好的實現相似Inception-v3模型這樣的複雜卷積神經網絡,在下面將先介紹TensorFlow-Slim工具來更加簡潔地實現一個卷積層。

如下代碼對比了直接使用TensorFlow實現一個卷積層和使用TensorFlow-Slim實現一樣結構的神經網絡的代碼量

import tensorflow as tf

# 直接使用TensorFlow原始API實現卷積層
with tf.variable_scope(scope_name):
    weights = tf.get_variable("weight",...)
    biases = tf.get_variable("bias",...)
    conv = tf.nn.conv2d(...)
relu = tf.nn.relu(tf.nn.bias_add(conv,biases))

# 使用TensorFlow-Slim實現卷積層。能夠在一行中實現一個卷積層的前向傳播算法。
# 三個參數是必填的,第一個參數爲輸入節點矩陣,第二個是當前卷積層過濾器深度。
# 第三個是過濾器尺寸。可選參數有過濾器步長、是否全0填充、激活函數以及變量命名空間
net = slim.conv2d(input,32,[3,3])

遷移學習介紹 

隨着模型層數及複雜度的增長,模型在ImageNet上的錯誤率也隨之下降。然而,訓練複雜的卷積神經網絡須要很是多的標註數據。ImageNet圖像分類數據集中有120萬標註圖片,因此才能將152層的ResNet的模型訓練到大約96.5%的正確率。在真是用的應用中,很難收集到如此多的標註數據。即便能夠收集到,也須要大量人力物力。並且即便有海量的訓練數據,要訓練一個複雜的卷積神經網絡也須要幾天甚至幾周的時間。爲了解決標註數據和訓練時間的問題,能夠使用遷移學習。

所謂遷移學習,就是將一個問題上訓練好的模型經過簡單的調整使其適用於一個新的問題。能夠保留訓練好的Inception-v3模型中全部卷積層的參數,只是替換最後一層全鏈接層。在最後這一層全鏈接層以前的網絡層稱之爲瓶頸層。

將新的圖像經過訓練好的卷積神經網絡直到瓶頸層的過程能夠當作是對圖像進行特徵提取的過程。在訓練好的Inception-v3模型中,由於將瓶頸層的輸出再經過一個單層的全鏈接層神經網絡能夠很好地區分1000中類別的圖像,因此有理由認爲瓶頸層輸出的節點向量能夠被做爲任何圖像的一個更加精簡且表達能力更強的特徵向量。在新數據集上,能夠直接利用這個訓練好的神經網絡對圖像進行特徵提取,而後再將提取獲得的特徵向量做爲輸入來訓練一個新的單層全鏈接神經網絡處理新的分類問題。

通常來講,在數據量足夠的狀況下,遷移學習的效果不如徹底從新訓練。可是遷移學習所須要的訓練時間和訓練樣本數要遠遠小於訓練完整的模型。

TensorFlow實現遷移學習

LeNet-5模型和Inception-v3模型,對比着兩個模型能夠發現,卷積神經網絡模型的層數和複雜度都發生了巨大的變化。

隨着模型層數即複雜度的增長,模型的錯誤率也隨之下降。然而訓練複雜的卷積神經網絡須要很是多的標註數據。

例如ImageNet圖像分類數據集中有120萬標註圖片,能將152層的ResNet的模型訓練到大約96.5%的正確率。在真實的應用中很難收集到如此多的標註數據。並且即便有海量的訓練數據,要訓練一個複雜的卷積神經網絡也須要幾天甚至幾周的時間。爲了解決標註數據和訓練時間的問題,能夠使用遷移學習。

所謂遷移學習,就是將一個問題上訓練好的模型經過簡單的調整使其適用於一個新的問題。本節介紹如何利用ImageNet數據集上訓練好的Inception-v3模型來解決一個新的圖像分類問題。

能夠保留訓練好的Inception-v3模型中全部卷積層的參數,只是替換最後一層全鏈接層。在最後這一層全鏈接層以前的網絡層稱之爲瓶頸層。

新的圖像經過訓練好的卷積神經網絡直到瓶頸層的過程是對圖像進行特徵提取的過程。在Inception-v3模型中,由於瓶頸層的輸出再經過一個單層的全鏈接層神經網絡能夠很好地區分1000種類別的圖像,因此有理由認爲瓶頸層輸出的節點向量能夠被做爲任何圖像的一個精簡且表達能力更強的特徵向量。在新數據集上,能夠直接利用這個訓練好的神經網絡對圖像進行特徵提取,而後將提取的特徵向量做爲輸入來訓練一個新的單層全鏈接神經網絡處理新的分類問題。

遷移學習的效果不如徹底從新訓練。可是遷移學習所須要的訓練時間和訓練樣本數遠遠小於訓練完成的模型。

實現遷移學習

flower_photos.tgz   OneDrive

解壓以後的文件夾包含5個子文件夾,每個表明一種花的名稱,表明了不一樣的類別。一種花有734張圖片,每一張圖片都是RGB色彩模型的,大小也不相同。和以前的樣例不一樣,這一小節中給出的程序將直接處理沒有整理過的圖像數據。

同時經過下面的命名能夠下載谷歌提供的訓練好的Inception-v3模型。

inception_dec_2015.zip   OneDrive

新的數據集和已經訓練好的模型都準備好以後,能夠經過下面代碼完成遷移學習

import glob
import os.path
import random
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile

# Inception-v3模型瓶頸層的節點個數
BOTTLENECK_TENSOR_SIZE = 2048
# Inception-v3模型中表明瓶頸層結果的張量名稱。谷歌提供的Inception-v3模型中,張量名稱就是'pool_3/_reshape:0'
# 在訓練的模型時,能夠經過tensor.name來獲取張量的名稱
BOTTLENECK_TENSOR_NAME = 'pool_3/_reshape:0'
# 圖像輸入張量所對應的名稱
JPEG_DATA_TENSOR_NAME = 'DecodeJpeg/contents:0'
# 下載的Inception-v3模型文件目錄和文件名
MODEL_DIR = 'Saved_model'
MODEL_FILE = 'tensorflow_inception_graph.pb'
# 一個訓練數據會被使用屢次,能夠將原始圖像經過Inception-v3模型計算獲得的特徵向量保存在文件中,免去重複的計算。
# 下面的變量定義了這些文件的存放地址
CACHE_DIR = 'bottleneck'
# 圖片數據文件夾。每個子文件夾表示一個須要區分的類別,每一個子文件夾中存放了對應類別的圖片
INPUT_DATA = 'flower_photos'
# 驗證的數據百分比
VALIDATION_PERCENTAGE = 10
# 測試的數據百分比
TEST_PERCENTAGE = 10
# 定義神經網絡的設置
LEARNING_RATE = 0.01
STEPS = 4000
BATCH = 100


# 這個函數從數據文件夾中讀取全部的圖片列表並按訓練、驗證、測試數據分開
# testing_percentage和validation_percentage參數指定了測試數據集和驗證數據集的大小。
def create_image_lists(testing_percentage, validation_percentage):
    # 獲得的全部圖片都存在result這個字典裏。這個字典的key爲類別的名稱,value也是一個字典,存儲了全部圖片名稱
    result = {}
    # 獲取當前目錄下全部的子目錄
    sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]
    # 獲得的第一個目錄是當前目錄,不須要考慮
    is_root_dir = True
    for sub_dir in sub_dirs:
        if is_root_dir:
            is_root_dir = False
            continue
        # 獲取當前目錄下全部的有效圖片文件
        extensions = ['jpg', 'jpeg', 'JPG', 'JPEG']
        file_list = []
        dir_name = os.path.basename(sub_dir)
        for extension in extensions:
            file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + extension)
            file_list.extend(glob.glob(file_glob))
        if not file_list: continue
        # 經過目錄名稱獲取類別名稱
        label_name = dir_name.lower()
        # 初始化當前類別的訓練數據集、測試數據集和驗證數據集
        training_images = []
        testing_images = []
        validation_images = []
        for file_name in file_list:
            base_name = os.path.basename(file_name)
            # 隨機將數據分到訓練數據集、測試數據集和驗證數據集
            chance = np.random.randint(100)
            if chance < validation_percentage:
                validation_images.append(base_name)
            elif chance < (testing_percentage + validation_percentage):
                testing_images.append(base_name)
            else:
                training_images.append(base_name)
        # 將當前類別的數據放入結果字典
        result[label_name] = {'dir': dir_name, 'training': training_images, 'testing': testing_images,
                              'validation': validation_images, }
    return result


# 函數經過類別名稱、所屬數據集和圖片編號獲取一張圖片地址
# image_lists參數給出了全部圖片信息
# image_dir參數給出了根目錄。存放圖片數據的根目錄和存放圖片特徵向量的根目錄地址不一樣
# label_name參數給定了類別的名稱
# index參數給定了須要獲取的圖片的編號
# category參數指定了須要獲取的圖片是在訓練數據集、測試數據集仍是驗證數據集
def get_image_path(image_lists, image_dir, label_name, index, category):
    # 獲取給定類別中全部圖片的信息
    label_lists = image_lists[label_name]
    # 根據所屬數據集的名稱獲取集合中的所有圖片信息
    category_list = label_lists[category]
    mod_index = index % len(category_list)
    # 獲取圖片的文件名
    base_name = category_list[mod_index]
    sub_dir = label_lists['dir']
    # 最終的地址爲數據根目錄的地址加上類別的文件夾加上圖片的名稱
    full_path = os.path.join(image_dir, sub_dir, base_name)
    return full_path


# 這個函數經過類別名稱、所屬數據集和圖片編號獲取通過Inception-v3模型處理以後的特徵向量
# 文件地址
def get_bottleneck_path(image_lists, label_name, index, category):
    return get_image_path(image_lists, CACHE_DIR, label_name, index, category) + '.txt'


# 這個函數使用加載的訓練好的Inception-v3模型處理一張圖片,獲得這個圖片的特徵向量
def run_bottleneck_on_image(sess, image_data, image_data_tensor, bottleneck_tensor):
    # 這個過程實際上就是將當前圖片做爲輸入計算瓶頸張量的值。
    # 這個瓶頸張量的值就是這張圖片的新的特徵向量
    bottleneck_values = sess.run(bottleneck_tensor, {image_data_tensor: image_data})
    bottleneck_values = np.squeeze(bottleneck_values)
    return bottleneck_values


# 這個函數獲取一張圖片通過Inception-v3模型處理以後的特徵向量。
# 這個函數會試圖找尋已經計算且保存下來的特徵向量,若是找不到則先計算這個特徵向量,而後保存到文件
def get_or_create_bottleneck(sess, image_lists, label_name, index, category, jpeg_data_tensor, bottleneck_tensor):
    # 獲取一張圖片對應的特徵向量文件的路徑
    label_lists = image_lists[label_name]
    sub_dir = label_lists['dir']
    sub_dir_path = os.path.join(CACHE_DIR, sub_dir)
    if not os.path.exists(sub_dir_path): os.makedirs(sub_dir_path)
    bottleneck_path = get_bottleneck_path(image_lists, label_name, index, category)
    # 若是這個特徵向量文件不存在,則經過Inception-v3模型來計算特徵向量,並將計算的結果存入文件
    if not os.path.exists(bottleneck_path):
        # 獲取原始的圖片路徑
        image_path = get_image_path(image_lists, INPUT_DATA, label_name, index, category)
        # 獲取圖片內容
        image_data = gfile.FastGFile(image_path, 'rb').read()
        # 經過Inception-v3模型計算特徵向量
        bottleneck_values = run_bottleneck_on_image(sess, image_data, jpeg_data_tensor, bottleneck_tensor)
        # 將計算獲得的特徵向量存入文件
        bottleneck_string = ','.join(str(x) for x in bottleneck_values)
        with open(bottleneck_path, 'w') as bottleneck_file:
            bottleneck_file.write(bottleneck_string)
    else:
        # 直接從文件中獲取圖片相應的特徵向量
        with open(bottleneck_path, 'r') as bottleneck_file:
            bottleneck_string = bottleneck_file.read()
        bottleneck_values = [float(x) for x in bottleneck_string.split(',')]
    # 返回獲得的特徵向量
    return bottleneck_values


# 這個函數隨機獲取一個batch的圖片做爲訓練數據
def get_random_cached_bottlenecks(sess, n_classes, image_lists, how_many, category, jpeg_data_tensor,
                                  bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    for _ in range(how_many):
        label_index = random.randrange(n_classes)
        label_name = list(image_lists.keys())[label_index]
        image_index = random.randrange(65536)
        bottleneck = get_or_create_bottleneck(sess, image_lists, label_name, image_index, category, jpeg_data_tensor,
                                              bottleneck_tensor)
        ground_truth = np.zeros(n_classes, dtype=np.float32)
        ground_truth[label_index] = 1.0
        bottlenecks.append(bottleneck)
        ground_truths.append(ground_truth)

    return bottlenecks, ground_truths


# 獲取所有的測試數據。最終測試的時候須要在全部的測試數據上計算正確率
def get_test_bottlenecks(sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    label_name_list = list(image_lists.keys())
    # 枚舉全部的類別和每一個類別中的測試圖片
    for label_index, label_name in enumerate(label_name_list):
        category = 'testing'
        for index, unused_base_name in enumerate(image_lists[label_name][category]):
            # 經過Inception-v3模型計算圖片對應的特徵向量,並將其加入最終數據的列表
            bottleneck = get_or_create_bottleneck(sess, image_lists, label_name, index, category, jpeg_data_tensor,
                                                  bottleneck_tensor)
            ground_truth = np.zeros(n_classes, dtype=np.float32)
            ground_truth[label_index] = 1.0
            bottlenecks.append(bottleneck)
            ground_truths.append(ground_truth)
    return bottlenecks, ground_truths


def main():
    # 讀取全部圖片
    image_lists = create_image_lists(TEST_PERCENTAGE, VALIDATION_PERCENTAGE)
    n_classes = len(image_lists.keys())
    # 讀取已經訓練好的Inception-v3模型。
    with gfile.FastGFile(os.path.join(MODEL_DIR, MODEL_FILE), 'rb') as f:
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
    # 加載讀取的Inception-v3模型,並返回數據輸入所對應的張量以及計算瓶頸層結果所對應的張量
    bottleneck_tensor, jpeg_data_tensor = tf.import_graph_def(graph_def, return_elements=[BOTTLENECK_TENSOR_NAME,
                                                                                          JPEG_DATA_TENSOR_NAME])

    # 定義新的神經網絡輸入,這個輸入就是新的圖片通過Inception-v3模型前向傳播到達瓶頸層的節點取值。
    # 能夠將這個過程理解爲一種特徵提取。
    bottleneck_input = tf.placeholder(tf.float32, [None, BOTTLENECK_TENSOR_SIZE], name='BottleneckInputPlaceholder')
    ground_truth_input = tf.placeholder(tf.float32, [None, n_classes], name='GroundTruthInput')

    # 定義一層全鏈接層來解決新的圖片分類問題。
    # 由於訓練好的Inception-v3模型已經將原始的圖片抽象爲更加容易分類的特徵向量,
    # 因此不須要再訓練那麼複雜的神經網絡來完成這個新的分類任務
    with tf.name_scope('final_training_ops'):
        weights = tf.Variable(tf.truncated_normal([BOTTLENECK_TENSOR_SIZE, n_classes], stddev=0.001))
        biases = tf.Variable(tf.zeros([n_classes]))
        logits = tf.matmul(bottleneck_input, weights) + biases
        final_tensor = tf.nn.softmax(logits)

    # 定義交叉熵損失函數。
    cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=ground_truth_input)
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    train_step = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(cross_entropy_mean)

    # 計算正確率。
    with tf.name_scope('evaluation'):
        correct_prediction = tf.equal(tf.argmax(final_tensor, 1), tf.argmax(ground_truth_input, 1))
        evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

    with tf.Session() as sess:
        init = tf.global_variables_initializer()
        sess.run(init)
        # 訓練過程。
        for i in range(STEPS):
            # 每次獲取一個batch的訓練數據
            train_bottlenecks, train_ground_truth = get_random_cached_bottlenecks(sess, n_classes, image_lists, BATCH,
                                                                                  'training', jpeg_data_tensor,
                                                                                  bottleneck_tensor)
            sess.run(train_step,
                     feed_dict={bottleneck_input: train_bottlenecks, ground_truth_input: train_ground_truth})
            # 在實驗數據上測試正確率
            if i % 100 == 0 or i + 1 == STEPS:
                validation_bottlenecks, validation_ground_truth = get_random_cached_bottlenecks(sess, n_classes,
                                                                                                image_lists, BATCH,
                                                                                                'validation',
                                                                                                jpeg_data_tensor,
                                                                                                bottleneck_tensor)
                validation_accuracy = sess.run(evaluation_step, feed_dict={bottleneck_input: validation_bottlenecks,
                                                                           ground_truth_input: validation_ground_truth})
                print('Step %d: Validation accuracy on random sampled %d examples = %.1f%%' % (
                    i, BATCH, validation_accuracy * 100))

        # 在最後的測試數據上測試正確率。
        test_bottlenecks, test_ground_truth = get_test_bottlenecks(sess, image_lists, n_classes, jpeg_data_tensor,
                                                                   bottleneck_tensor)
        test_accuracy = sess.run(evaluation_step,
                                 feed_dict={bottleneck_input: test_bottlenecks, ground_truth_input: test_ground_truth})
        print('Final test accuracy = %.1f%%' % (test_accuracy * 100))


if __name__ == '__main__':
    main()

運行上面的程序須要大約40分鐘(數據處理3五、訓練5分鐘)

圖像數據處理

圖像的亮度和對比度等屬性對圖像的影響很是大,相同物體在不一樣亮度、對比度下差異很是大。然而在不少圖像識別問題中,這些因素都不該該影響最後的識別結果。介紹如何對圖像數據進行預處理使訓練獲得的神經網絡儘量小地被無關因素所影響。複雜的預處理過程可能致使訓練效率的降低。爲了減少預處理對於訓練速度的影響,本節介紹TensorFlow中多線程處理輸入數據的解決方案。

TFRecord輸入數據格式

TensorFlow提供了一種統一的格式來存儲數據,這個格式就是TFRecord。上面處理花朵分類的數據,就是使用了一個類別名稱到全部數據列表的詞典來維護圖像和類別的關係。

可是這種方式擴展性很是差。因而TensorFlow提供了TFRecord的格式來統一存儲數據。

TFRecord格式介紹

TFRecord文件中的數據都是經過tf.train.Example Protocol Buffer的格式存儲的。下面是tf.train.Example的定義

message Example{
    Features features = 1;
};

message Features{
    map<string,Feature> feature = 1;
};

message Feature{
    oneof kind{
        BytesList bytes_list = 1;
        FloatList float_list = 2;
        Int64List int64_list = 3;
    }
};

數據結構是比較簡潔的。包含了一個從屬性名稱到取值的字典。其中屬性名稱爲一個字符串,屬性的取值能夠爲字符串(BytesList),實數列表(FloatList),整數列表(Int64List)。好比將一張解碼前的圖像存爲一個字符串,圖像所對應的類別編號存爲整數列表。

TFRecord樣例程序 

下面程序給出瞭如何將MNIST輸入數據轉換爲TFRecord的格式

import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
import numpy as np


# 生成整數型的屬性
def _int64_feature(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))


# 生成字符串型的屬性
def _bytes_feature(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))


mnist = input_data.read_data_sets('MNIST_data', dtype=tf.uint8, one_hot=True)
images = mnist.train.images

# 訓練數據所對應的正確答案,能夠做爲一個屬性保存在TFRecord中
labels = mnist.train.labels
# 訓練數據的圖像分辨率,能夠做爲Example中的一個屬性
pixels = images.shape[1]
num_examples = mnist.train.num_examples

# 輸出TFRecord文件的地址
filename = 'MNIST_data/output.tfrecords'
# 建立一個writer來寫TFRecord文件
write = tf.python_io.TFRecordWriter(filename)
for index in range(num_examples):
    # 將圖像矩陣轉化爲一個字符串
    image_raw = images[index].tostring()
    # 將一個樣例轉化爲Example Protocol Buffer,並將全部的信息寫入這個數據結構
    example = tf.train.Example(features=tf.train.Features(
        feature={'pixels': _int64_feature(pixels), 'label': _int64_feature(np.argmax(labels[index])),
                 'image_raw': _bytes_feature(image_raw)}))
    # 將一個Example寫入TFRecord文件
    write.write(example.SerializeToString())
    write.close()

上面的程序能夠將MNIST數據集中全部的訓練數據存儲到一個TFRecord文件中。當數據量較大時,也能夠將數據寫入多個TFRecord文件。

下面程序給出瞭如何讀取TFRRecord文件中的數據

import tensorflow as tf

# 建立一個reader來讀取TFRecord文件中的樣例
reader = tf.TFRecordReader()
# 建立一個隊列來維護輸入文件列表
filename_queue = tf.train.string_input_producer(['MNIST_data/output.tfrecords'])
# 從文件中讀取一個樣例,也能夠使用read_up_to函數一次性讀取多個樣例
_, serialized_example = reader.read(filename_queue)
# 解析讀入的一個樣例。若是須要解析多個樣例,能夠用parse_example函數
features = tf.parse_single_example(serialized_example,
       features={
           # TensorFlow提供兩種不一樣的屬性解析方法,一種是tf.FixedLenFeature,這種方法解析的結果爲一個Tensor
           # 另外一種方法是tf.VarLenFeature,這種方法獲得的解析結果爲SparseTensor,用於處理稀疏數據
           # 這裏解析數據的格式須要和上面程序寫入數據的格式一致
           'image_raw': tf.FixedLenFeature([], tf.string),
           'pixels': tf.FixedLenFeature([], tf.int64),
           'label': tf.FixedLenFeature([], tf.int64)})
# tf.decode_raw能夠將字符串解析成圖像對應的像素數組
images = tf.decode_raw(features['image_raw'], tf.uint8)
labels = tf.cast(features['label'], tf.int32)
pixels = tf.cast(features['pixels'], tf.int32)
sess = tf.Session()
# 啓動多線程處理輸入數據
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess=sess, coord=coord)
# 每次運行能夠讀取TFRecord文件中的一個樣例,當全部樣例讀取完後,此樣例中程序會重頭讀取
for i in range(10):
    image, label, pixel = sess.run([images, labels, pixels])
    print('image %s label %s pixel %s' % (image.shape, label, pixel))

圖像數據處理 

TensorFlow提供了幾類圖像處理函數

圖像編碼處理

一張RGB色彩模型的圖像能夠當作一個三維矩陣,矩陣中的每個數都表示了圖像上不一樣位置,不一樣顏色的亮度。

然而圖像在存儲時並非直接記錄這些矩陣中的數字,而是記錄通過壓縮編碼以後的結果。將一張圖像還原成一個三維矩陣,須要解碼的過程。TensorFlow提供了對jpeg和png格式圖像的編碼/解碼函數。

import tensorflow as tf
import matplotlib.pyplot as plt

# 讀取圖像的原始數據
image_raw_data = tf.gfile.FastGFile('MNIST_data/cat.jpg', 'rb').read()
with tf.Session() as sess:
    # 將圖像使用jpeg的格式解碼從而獲得圖像對應的三維矩陣
    # TensorFlow提供了tf.image.decode_png函數對png格式的圖片進行解碼
    img_data = tf.image.decode_jpeg(image_raw_data)
    print(img_data.eval())
    plt.imshow(img_data.eval())
    plt.show()

    # 將數據的類型轉換爲實數方便進行處理
    img_data = tf.image.convert_image_dtype(img_data, dtype=tf.uint8)
    # 將表示一張圖像的三維矩陣從新按照jpeg格式進行編碼存入
    encode_image = tf.image.encode_jpeg(img_data)
    with tf.gfile.GFile('MNIST_data/cat2.jpg', 'wb') as f:
        f.write(encode_image.eval())

圖像大小調整 

網絡上獲取的圖像大小是不固定的,但神經網絡輸入節點的個數是固定的。因此在將圖像的像素做爲輸入提供給神經網絡以前,須要先將圖像的大小統一。這就是圖像大小調整須要完成的任務。圖像大小調整有兩種方式,第一種是經過算法使得新的圖像儘可能保存原始圖像上的全部消息。TensorFlow提供了四種不一樣的方法,而且將它們封裝到tf.image.resize_images函數。

# 讀取圖像的原始數據
image_raw_data = tf.gfile.FastGFile('MNIST_data/cat.jpg', 'rb').read()
with tf.Session() as sess:
    img_data = tf.image.decode_jpeg(image_raw_data)
    resized = tf.image.resize_images(img_data, [300, 300], method=0)
    # TensorFlow的函數處理圖片後存儲的數據是float32格式的,須要轉換成uint8才能正確打印圖片。
    cat = np.asarray(resized.eval(), dtype='uint8')
    plt.imshow(cat)
    plt.show()
    encode_image = tf.image.encode_jpeg(cat)
    with tf.gfile.GFile('MNIST_data/cat2.jpg', 'wb') as f:
        f.write(encode_image.eval())

Method取值,圖像大小調整算法

0:雙線性插值法(Bilinear interpolation)

1:最近鄰居法(Nearest neighbor interpolation)

2:雙三次插值法(Bicubic interpolation)

3:面積插值法(Area interpolation)

不一樣算法調整出來的結果會有細微差異,但不會相差太遠。除了將整張圖像信息完整保存,還提供了API對圖像進行裁剪或者填充。

# 讀取圖像的原始數據
image_raw_data = tf.gfile.FastGFile('MNIST_data/cat.jpg', 'rb').read()
with tf.Session() as sess:
    img_data = tf.image.decode_jpeg(image_raw_data)
    # 經過函數調整圖像的大小。第一個參數爲原始圖像,後面是調整大小
    # 若是原始圖像尺寸大於目標圖像,那麼會自動截取原始圖像居中的部分。
    # 若是原始圖像尺寸小於目標圖像,那麼會在四周填充全0背景
    croped = tf.image.resize_image_with_crop_or_pad(img_data, 1000, 1000)
    padded = tf.image.resize_image_with_crop_or_pad(img_data, 3000, 3000)
    plt.subplot(121)
    plt.imshow(croped.eval())
    plt.subplot(122)
    plt.imshow(padded.eval())
    plt.show()

TensorFlow還支持經過比例調整圖像大小

# 經過tf.image.central_crop函數能夠按比例裁剪圖像。
central_cropped = tf.image.central_crop(img_data, 0.5)

TensorFlow也提供了tf.image.crop_to_bounding_box函數和tf.image.pad_to_bounding_box函數來剪裁或者填充給定區域的圖像。好比在使用tf.image.crop_to_bounding_box函數時,要求提供圖像的尺寸要大於目標尺寸,也就是要求原始圖像可以裁剪出目標圖像的大小

圖像翻轉

TensorFlow提供了一些函數來支持對圖像的翻轉。

with tf.Session() as sess: 
    # 上下翻轉
    flipped1 = tf.image.flip_up_down(img_data)
    # 左右翻轉
    flipped2 = tf.image.flip_left_right(img_data)
    #對角線翻轉
    transposed = tf.image.transpose_image(img_data)
    # 以必定機率上下翻轉圖片。
    flipped = tf.image.random_flip_up_down(img_data)
    # 以必定機率左右翻轉圖片。
    flipped = tf.image.random_flip_left_right(img_data)

在不少圖像識別問題中,圖像的翻轉不會影響識別的結果。因而在訓練圖像識別的神經網絡模型時,能夠隨機地翻轉訓練圖像,這樣訓練獲得的模型能夠識別不一樣角度的實體。

好比假設在訓練數據中全部的貓頭都是向右,那麼訓練出來的模型就沒法很好的識別貓頭向左的貓。

隨機翻轉訓練圖像是一種很經常使用的圖像預處理方式。TensorFlow提供了方便API完成隨機圖像翻轉的過程

# 以必定機率上下翻轉圖像
filpped = tf.image.random_flip_up_down(img_data)
# 以必定機率左右翻轉圖像
filpped = tf.image.random_flip_left_right(img_data)

圖像色彩調整

和圖像翻轉相似,調整圖像的亮度和對比度、飽和度、色相在不少圖像識別應用中都不會影響識別結果。因此在訓練神經網絡模型時,能夠隨機調整訓練圖像的這些屬性,從而使得訓練獲得的模型儘量小地受到無關因素的影響。

亮度:亮度指照射在景物或圖像上光線的明暗程度。圖像亮度增長時,就會顯得耀眼或刺眼,亮度越小時,圖像就會顯得灰暗。

對比度:對比度指不一樣顏色之間的差異。對比度越大,不一樣顏色之間的反差越大,即所謂黑白分明,對比度過大,圖像就會顯得很刺眼。對比度越小,不一樣顏色之間的反差就越小。

色相:色相就是顏色,調整色相就是調整景物的顏色,例如,彩虹由紅、橙、黃、綠、青、藍、紫七色組成,那麼它就有七種色相。顧名思義即各種色彩的相貌稱謂,如大紅、普藍、檸檬黃等。色相是色彩的首要特徵,是區別各類不一樣色彩的最準確的標準。事實上任何黑白灰之外的顏色都有色相的屬性,而色相也就是由原色、間色和複色來構成的

飽和度:飽和度是指圖像顏色的濃度。飽和度越高,顏色越飽滿,即所謂的青翠欲滴的感受。飽和度越低,顏色就會顯得越陳舊、慘淡,飽和度爲0時,圖像就爲灰度圖像。能夠經過調整電視機的飽和度來進一步理解飽和度的概念。

# 將圖像的亮度-0.5,正數爲+0.5
    adjusted = tf.image.adjust_brightness(img_data, -0.5)
    # 在[-max_delta,max_delta]的範圍隨機調整圖像的亮度
    adjusted = tf.image.random_brightness(img_data, max_delta=2)
    # 將圖像的對比度-5,
    adjusted = tf.image.adjust_contrast(img_data, -5)
    # 在[lower,upper]的範圍隨機調整圖的對比度
    adjusted = tf.image.random_contrast(image=img_data, lower=1, upper=10)
    # 將色相增長1.0
    adjusted = tf.image.adjust_hue(img_data, 0.1)
    # 在[-max_delta,max_delta]的範圍隨機調整圖像的色相
    # max_delta的取值在[0,0.5]之間
    adjusted = tf.image.random_hue(img_data, 0.5)
    # 將圖像的飽和度-5
    adjusted = tf.image.adjust_saturation(img_data, -5)
    # 在[lower,upper]的範圍隨機調整圖的飽和度
    adjusted = tf.image.random_saturation(img_data, 1, 10)
    # 將表明一張圖像的三維矩陣中的數字均值變爲0,方差變爲1
    adjusted = tf.image.per_image_standardization(img_data

處理標註框 

在不少圖像識別的數據中,圖像中須要關注的物體一般會被標註框圈出來。TensorFlow提供了一些工具來處理標註框。

# 將圖像縮小一些,可視化能讓標註框更加清楚
    img_data = tf.image.resize_images(img_data, [100, 100])
    # tf.image.draw_bounding_boxes函數要求圖像矩陣中的數字爲實數,因此須要將圖像矩陣轉化爲實數類型。
    # 函數圖像的輸入是一個bacth的數據,也就是多張圖像組成的四維矩陣,須要將解碼以後的圖像矩陣加一維。
    batched = tf.expand_dims(tf.image.convert_image_dtype(img_data, tf.float32), 0)
    # 給出每一張圖像的全部標註框,一個標註框有四個數字
    boxes = tf.constant([[[0.05, 0.05, 0.9, 0.7], [0.35, 0.47, 0.5, 0.56]]])
    # 顯示加入標註框的圖像
    result = tf.image.draw_bounding_boxes(batched, boxes)
    plt.imshow(np.asarray(img_data.eval(), dtype='uint8'))
    plt.show()

隨機截取圖像上有信息含量的部分也是提升模型健壯性的一種方式。這樣能夠使訓練獲得的模型不受被識別物體大小的影響

boxes = tf.constant([[[0.05, 0.05, 0.9, 0.7], [0.35, 0.47, 0.5, 0.56]]])
    # 能夠經過提供標註框的方式來告訴隨機截取圖像的算法
    begin, size, bbox_for_draw = tf.image
    .sample_distorted_bounding_box(tf.shape(img_data), bounding_boxes=boxes) # 經過標註框可視化隨機截取獲得的圖像 batched = tf.expand_dims(tf.image.convert_image_dtype(img_data, tf.float32), 0) image_with_box = tf.image.draw_bounding_boxes(batched, bbox_for_draw) plt.imshow(img_data.eval()) # 截取隨機出來的圖像 distorted_image = tf.slice(img_data, begin, size) plt.imshow(distorted_image.eval()) plt.show()

圖像預處理完整樣例

在解決真實的圖像識別問題時,通常會同時使用多種處理方式。下面的程序完成了從圖像片斷截取到圖像大小調整再到圖像翻轉及色彩調整的整個圖像預處理過程

import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt


# 給定一張圖像,隨機調整圖像的色彩。由於調整亮度、對比度、飽和度、色相的順序會影響最後獲得的結果
# 因此能夠定義多種不一樣的順序,具體使用哪種能夠在預處理時隨機選擇。能夠進一步下降無關因素對模型的影響
def distort_color(image, color_ordering=0):
    if color_ordering == 0:
        image = tf.image.random_brightness(image, max_delta=32. / 255.)
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
        image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
    else:
        image = tf.image.random_saturation(image, lower=0.5, upper=1.5)
        image = tf.image.random_brightness(image, max_delta=32. / 255.)
        image = tf.image.random_contrast(image, lower=0.5, upper=1.5)
        image = tf.image.random_hue(image, max_delta=0.2)
    return tf.clip_by_value(image, 0.0, 1.0)


# 給定一張解碼後的圖像、目標圖像的尺寸以及圖像上的標註框,此函數能夠對給出的圖像進行預處理。
# 這個函數的輸入圖像是圖像識別問題中原始的訓練圖像,而輸出則是神經網絡模型的輸入層
# 注意這裏只處理模型的訓練數據,對於預測的數據,不須要使用隨機變化的步驟
def preprocess_for_train(image, height, width, bbox):
    # 若是沒有提供標註框,則認爲整個圖像就是須要關注的部分
    if bbox is None:
        bbox = tf.constant([0.0, 0.0, 1.0, 1.0], dtype=tf.float32, shape=[1, 1, 4])
    # 轉換圖像張量的類型
    if image.dtype != tf.float32:
        image = tf.image.convert_image_dtype(image, dtype=tf.float32)

    # 隨機截取圖像,減小須要關注的物體大小對圖像識別算法的影響
    bbox_begin, bbox_size, _ = tf.image.sample_distorted_bounding_box(tf.shape(image), bounding_boxes=bbox)
    distorted_image = tf.slice(image, bbox_begin, bbox_size)
    # 隨機截取的圖像調整爲神經網絡輸入層的大小。大小調整的算法是隨機選擇的
    distorted_image = tf.image.resize_images(distorted_image, [height, width], method=np.random.randint(4))
    # 隨機左右翻轉圖像
    distorted_image = tf.image.random_flip_left_right(distorted_image)
    # 使用一種隨機的順序調整圖像色彩
    distorted_image = distort_color(distorted_image, np.random.randint(2))
    return distorted_image


image_raw_dat = tf.gfile.FastGFile('MNIST_data/cat.jpg', 'rb').read()
with tf.Session() as sess:
    img_data = tf.image.decode_jpeg(image_raw_dat)
    boxes = tf.constant([[[0.05, 0.05, 0.9, 0.7], [0.35, 0.47, 0.5, 0.56]]])
    # 運行6次得到6中不一樣的圖像
    for i in range(6):
        # 將圖像的尺寸調整爲299*299
        result = preprocess_for_train(img_data, 299, 299, boxes)
        plt.imshow(result.eval())
        plt.show()

這樣就能夠經過一張訓練圖像衍生出不少訓練樣本。經過將訓練圖像進行預處理,訓練獲得的神經網絡模型能夠識別不一樣大小,方位,色彩等方面的實體

多線程輸入數據處理框架

上面對圖像數據進行預處理的方法。雖然使用這些圖像數據預處理的方法能夠減小無關因素對圖像識別模型效果的影響,但這些複雜的預處理過程也會減慢整個訓練過程。爲了不圖像預處理稱爲神經網絡模型訓練效率的瓶頸,TensorFlow提供了一套多線程處理輸入數據的框架。

隊列與多線程

隊列和變量類型,都是計算圖上有狀態的節點。其餘的計算節點能夠修改它們的狀態。對於變量,能夠經過賦值操做修改變量的取值。

對於隊列,修改隊列狀態的操做主要有Enqueue、EnqueueMany、Dequeue。如下程序展現瞭如何使用這些函數來操做一個隊列

import tensorflow as tf

# 建立一個先進先出的隊列,指定隊列中最多能夠保存兩個元素
q = tf.FIFOQueue(2, 'int32')
# 使用enqueue_many函數來初始化隊列中的元素,和變量初始化相似,使用隊列以前須要明確調用初始化過程
init = q.enqueue_many(([0, 10],))
# 使用Dequeue函數將隊列中的第一個元素出隊列。這個元素的值將被存在變量x中
x = q.dequeue()
y = x + 1
q_inc = q.enqueue([y])

with tf.Session() as sess:
    # 運行初始化隊列的操做
    init.run()
    for _ in range(5):
        # 運行q_inc將執行數據出隊列、出隊的元素+1,從新加入隊列的整個過程
        v, _ = sess.run([x, q_inc])
        print(v)  # 輸出內容 0, 10, 1, 11, 2
# 隊列開始有[0,10]兩個元素,第一個出隊的爲0,加1以後再次入隊獲得的隊列爲[10,1]
# 第二次出隊的爲10,加1以後入隊的爲11,獲得的隊列爲[1,11],以此類推

TensorFlow中提供了FIFOQueue和RandomShuffleQueue兩種隊列。上面展現的FIFOQueue,實現的是一個先進先出隊列。RandomShuffleQueue會將隊列中的元素打亂,每次出隊列操做獲得的是從當前隊列全部元素中隨機選擇的一個。在訓練神經網絡時但願每次使用的訓練數據儘可能隨機,RandomShuffleQueue就提供了這樣的功能。

隊列不只僅是一種數據結構,仍是異步計算張量取值的一個重要機制。好比多個線程能夠同時向一個隊列中寫元素,或者同時讀取一個隊列中的元素。

TensorFlow提供了tf.Coordinator和tf.QueueRunner兩個類來完成多線程協同的功能。tf.Coordinator主要用於協同多個線程一塊兒中止,並提供了should_stop、request_stop和join三個函數。在啓動線程以前,須要先聲明一個tf.Coordinator類,並將這個類傳入每個建立的線程中。啓動的線程須要一直查詢tf.Coordinator類中提供的should_stop函數,當這個函數的返回值爲True時,則當前線程也須要退出。

每個啓動的線程均可以經過調用request_stop函數來通知其餘線程退出。當某一個線程調用request_stop函數以後,should_stop函數的返回值將被設置爲True,這樣其餘的線程就能夠同時終止了。

import tensorflow as tf
import numpy as np
import threading
import time


# 線程中運行的程序,這個程序每隔1秒判斷是否須要中止並打印ID
def MyLoop(coord, worker_id):
    # 使用tf.Coordinator類提供的協同工具判斷當前線程是否須要中止
    while not coord.should_stop():
        # 隨機中止全部的線程
        if np.random.rand() < 0.1:
            print('Stoping from id: %d' % worker_id)
            coord.request_stop()
        else:
            print('Working on id %d' % worker_id)
        time.sleep(1)


# 聲明一個tf.train.Coordinator類來協同多個線程
coord = tf.train.Coordinator()
# 聲明建立5個線程
threads = [threading.Thread(target=MyLoop, args=(coord, i,)) for i in range(5)]
# 啓動全部線程
for t in threads:
    t.start()
# 等待全部線程退出
coord.join(threads)

當全部線程啓動以後,每一個線程會打印各自的ID,因而前面4行打印出了它們的ID。而後在暫停1秒以後,全部線程又開始第二遍打印ID。這個時候有一個線程退出的條件達到,因而調用了coord.request_stop函數來中止全部其餘的線程。然而在打印Stoping from id:4以後,就能夠看到有線程仍然在輸出。這是由於這些線程已經執行完coord.should_stop的判斷,因而仍然會繼續輸出本身的ID。

tf.QueueRunner主要用於啓動多個線程來操做同一個隊列,啓動的這些線程能夠經過上面介紹的tf.Coordinator類來統一管理。下面代碼展現瞭如何使用tf.QueueRunner和tf.Coordinator來管理多線程隊列操做

import tensorflow as tf

# 聲明一個先進先出的隊列,隊列中最多100個元素,類型爲實數
queue = tf.FIFOQueue(100, 'float')
# 定義隊列的入隊操做
enqueue_op = queue.enqueue([tf.random_normal([1])])
# 使用tf.train.QueueRunner來建立多個線程運行隊列的入隊操做
# QueueRunner的第一個參數給出了被操做的隊列
# [enqueue_op]*5,表示了須要啓動5個線程,每一個線程中運行的是enqueue_op操做
qr = tf.train.QueueRunner(queue, [enqueue_op] * 5)
# 將定義過的QueueRunner加入TensorFlow計算圖上指定的集合
# tf.train.add_queue_runner函數沒有指定集合則加入默認集合tf.GraphKeys.QUEUE_RUNNERS
# 下面的函數就是將剛剛定義的qr加入默認的tf.GraphKeys.QUEUE_RUNNERS集合
tf.train.add_queue_runner(qr)
# 定義出隊操做
out_tensor = queue.dequeue()
with tf.Session() as sess:
    # 使用tf.train.Coordinator來協同啓動的線程
    coord = tf.train.Coordinator()
    # 使用tf.train.QueueRunner時,須要明確調用tf.train.start_queue_runners
    # 來啓動全部線程,不然由於沒有線程運行入隊操做,當調用出隊操做時,程序會一直等待入隊操做被運行
    # start_queue_runners函數會默認啓動,QUEUE_RUNNERS集合中全部的QueueRunner。
    # 由於這個函數只支持啓動指定集合中的QueueRunner,
    # 通常來講add_queue_runner函數和start_queue_runners函數會指定同一個集合
    threads = tf.train.start_queue_runners(sess=sess, coord=coord)
    # 獲取隊列中的取值
    for _ in range(3):
        print(sess.run(out_tensor[0]))
    # 中止全部線程
    coord.request_stop()
    coord.join(threads)

上面程序將啓動五個線程來執行隊列入隊的操做,其中每個線程都是將隨機數寫入隊列。因而在每次運行出隊操做時,能夠獲得一個隨機數。

輸入文件隊列 

一個TFRecord文件能夠存儲多個訓練樣例,可是當訓練數據量較大時,能夠將數據分紅多個TFRecord文件來提升處理效率。TensorFlow提供了tf.train.match_filenames_once函數來獲取符合一個正則表達式的全部文件,獲得的文件列表能夠經過tf.train.string_input_producer函數進行有效的管理。

string_input_producer函數會使用初始化時提供的文件列表建立一個輸入隊列,輸入隊列中原始的元素爲文件列表中的全部文件。建立好的輸入隊列能夠做爲文件讀取函數的參數,每次調用文件讀取函數時,該函數會先判斷當前是否已有打開的文件刻度,若是沒有貨打開的文件已經讀完,這個函數就會從輸入列表中出隊一個文件並從這個文件中讀取數據

經過設置shuffle參數,string_input_producer函數支持隨機打亂文件列表中文件出隊的順序。當shuffle參數爲True時,文件在加入隊列之間會被打亂順序,出隊的順序也是隨機的。隨機打亂文件順序以及加入輸入隊列的過程會跑在一個單獨的線程上,不會影響獲取文件的速度。

string_input_producer生成的輸入隊列能夠同時被多個文件讀取線程操做,並且輸入隊列會將隊列中的文件均勻地分給不一樣的線程,不出現有些文件被處理過屢次而有些文件尚未被處理過的狀況。

當一個輸入隊列中的全部文件都被處理完後,會將初始化時提供的文件列表中的文件所有從新加入隊列。tf.train.string_input_producer函數能夠設置num_epochs參數來限制加載初始文件列表的最大輪數。當全部文件都已經被使用了設定的輪數後,若是繼續嘗試讀取新的文件,輸入隊列會報OutOfRange的錯誤。在測試神經網絡時,由於全部測試數據只須要使用一次,能夠將num_epochs參數設置爲1。這樣在計算完一輪以後程序將自動中止。

import tensorflow as tf

# 建立TFRecord文件的幫助函數
def _int64_feature(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))

# 模擬海量數據狀況下將數據寫入不一樣的文件。num_shards定義了總共寫入多少文件
# instances_per_shard定義了每一個文件中有多少個數據
num_shards = 2
instances_per_shard = 2
for i in range(num_shards):
    # 將數據分爲多個文件時,能夠將不一樣文件以相似0000n-of-0000m的後綴區分。
    # 其中m表示了數據總共被存在了多少個文件中,n表示當前文件的編號
    filename = ('MNIST_data/data.tfrecords-%.5d-of-%.5d' % (i, num_shards))
    writer = tf.python_io.TFRecordWriter(filename)
    # 將數據封裝成Example結構並寫入TFRecord文件
    for j in range(instances_per_shard):
        # Example結構僅包含當前樣例屬於第幾個文件以及是當前文件的第幾個樣本。
        example = tf.train.Example(features=tf.train.Features(feature={
            'i': _int64_feature(i), 'j': _int64_feature(j)}))
        writer.write(example.SerializeToString())
    writer.close()

程序運行以後,將生成兩個文件。每個文件中存儲了兩個樣例。生成了樣例數據以後,展現match_filenames_once函數和string_input_producer函數的使用方法

import tensorflow as tf

# 使用match_filenames_once函數獲取文件列表
files = tf.train.match_filenames_once('MNIST_data/data.tfrecords-*')
# 經過string_input_producer函數建立輸入隊列,輸入隊列中的文件列表爲match_filenames_once函數獲取的文件列表
# 這裏將shuffle參數設爲False,來避免隨機打亂讀文件的順序。但通常在解決真實問題時,會將shuffle參數設置爲True
filename_queue = tf.train.string_input_producer(files, shuffle=False)
reader = tf.TFRecordReader()
_, serialized_example = reader.read(filename_queue)
features = tf.parse_single_example(
      serialized_example,
      features={
          'i': tf.FixedLenFeature([], tf.int64),
          'j': tf.FixedLenFeature([], tf.int64),
      })
with tf.Session() as sess:
    # 使用tf.train.match_filenames_once函數須要初始化變量
    sess.run([tf.global_variables_initializer(), tf.local_variables_initializer()])
    print(sess.run(files))
    # 聲明tf.train.Coordinator類來協同不一樣線程,並啓動線程
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(sess=sess, coord=coord)
    # 屢次執行獲取數據的操做
    for i in range(6):
        print(sess.run([features['i'], features['j']]))
    coord.request_stop()
    coord.join(threads)

在不打亂文件列表的狀況下,會依次讀出樣例數據的每個樣例,並且當全部樣例都被讀完以後,程序會自動從頭開始

組合訓練數據(batching)

將多個輸入樣例組成一個batch能夠提升模型訓練的效率。在獲得單個樣例的預處理結果以後,還須要將它們組織成batch,而後再提供給神經網絡的輸入層。

TensorFlow提供了tf.train.batch和tf.train.shuffle_batch函數來將單個的樣例組織成batch的形式輸出。這兩個函數都會生成一個隊列,隊列的入隊操做是生成單個樣例的方法,而每次出隊獲得的是一個batch的樣例。它們惟一的區別在因而否會將數據順序打亂

# 這裏假設Example結構中i表示一個樣例的特徵向量
# 好比一張圖像的像素矩陣,而j表示該樣例對應的標籤
example, label = features['i'], features['j']
# 一個batch中樣例的個數
batch_size = 3
# 組合樣例的隊列中最多能夠存儲的樣例個數。
# 這個隊列若是太大,那麼須要佔用不少內存資源
# 若是過小,那麼出隊操做可能會由於沒有數據而被阻礙,從而致使訓練效率下降
# 通常來講這個隊列的大小會和每個batch的大小相關,下面設置隊列大小
capacity = 1000 + 3 * batch_size
# 使用tf.train.batch函數來組合樣例
example_batch, label_batch = tf.train.batch([example, label], batch_size=batch_size, capacity=capacity)
with tf.Session() as sess:
    tf.global_variables_initializer().run()
    tf.local_variables_initializer().run()
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(sess=sess, coord=coord)
    # 獲取並打印組合以後的樣例,通常會做爲神經網絡的輸入
    for i in range(3):
        cur_example_batch, cur_label_batch = sess.run([example_batch, label_batch])
        print(cur_example_batch, cur_label_batch)
    coord.request_stop()
    coord.join(threads)

循環神經網絡

循環神經網絡(recurrent neural network,RNN)源自於1982年由Saratha Sathasivam提出的霍普菲爾德網絡。由於實現困難,在其提出時並無被合適地應用。該網絡結構也於1986年後被全鏈接神經網絡以及一些傳統的機器學習算法所取代。然而,傳統的機器學習算法很是依賴於人工提取的特徵,使得基於傳統機器學習的圖像識別、語音識別、以及天然語言處理等問題存在特徵提取的瓶頸。

而基於全鏈接神經網絡的方法也存在參數太多,沒法利用數據中時間序列信息等問題。隨着更加有效的循環神經網絡結構被不斷提出,循環神經網絡挖掘數據中的時序信息以及語義信息的深度表達能力被充分利用,並在語音識別、語言模型、機器翻譯以及時序分析等方面實現了突破。

循環神經網絡的主要用途是處理和預測序列數據。全鏈接神經網絡或者卷積神經網絡模型中,網絡結構都是從輸入層到隱藏層再到輸出層,層與層之間是全鏈接或部分鏈接的,但每層之間的節點是無鏈接的。

考慮一下,若是要預測句子的下一個單詞是什麼,通常須要用到當前單詞以及前面的單詞,由於句子中先後單詞並非獨立的。好比,當前單詞是很,前一個單詞是天空,那麼下一個單詞很大機率是藍。循環神經網絡的來源就是爲了刻畫一個序列當前的輸出與以前信息的關係。

從網絡結構上,循環神經網絡會記憶以前的信息,並利用以前的信息影響後面結點的輸出。循環神經網絡的隱藏層之間的節點是有鏈接的,隱藏層的輸入不只包括輸入層的輸出,還包括上一時刻隱藏層的輸出

上圖展現了一個典型的循環神經網絡,一個很是重要的概念就是時刻。循環神經網絡會對於每個時刻的輸入結合當前模型的狀態給出一個輸出。循環神經網絡的主體結構A的輸入除了來自輸入層Xt,還有一個循環的邊來提供當前時刻的狀態。每個時刻,循環神經網絡的模塊A會讀取t時刻的輸入Xt,並輸出一個值ht。同時A的狀態會從當前步傳遞到下一步。所以循環神經網絡理論上能夠被看做是同一神經網絡結構被無限複製的結果。但出於優化的考慮,目前循環神經網絡沒法作到真正的無限循環,現實中通常會將循環體展開

循環神經網絡在每個時刻會有一個輸入Xt,而後根據循環神經網絡當前的狀態At提供一個輸出ht。而循環神經網絡當前的狀態At是根據上一時刻的狀態At-1和當前的輸入Xt共同決定的。從循環神經網絡的結構特徵能夠很容易得出它最擅長解決的問題是與時間序列相關的。循環神經網絡也是處理這類問題時最天然的神經網絡結構。

對於一個序列數據,能夠將這個序列上不一樣時刻的數據依次傳入循環神經網絡的輸入層,而輸出能夠是對序列中下一個時刻的預測,也能夠是對當前時刻信息的處理結果。循環神經網絡要求每個時刻都有一個輸入,可是不必定每一個時刻都須要有輸出。循環神經網絡已經被普遍地應用在語音識別、語音模型、機器翻譯以及時序分析等問題上,取得巨大成功。

以機器翻譯爲例來介紹循環神經網絡是如何解決實際問題的。循環神經網絡中每個時刻的輸入爲須要翻譯的句子中的單詞。須要翻譯的句子爲ABCD,那麼循環神經網絡第一段每個時刻的輸入就分別是A、B、C、D,而後用"_"做爲待翻譯句子的結束符。在第一段中,循環神經網絡沒有輸出。從結束符"_"開始,循環神經網絡進入翻譯階段。階段中每個時刻的輸入是上一個時刻的輸出,而最終獲得的輸出就是句子ABCD翻譯的結果。

上圖能夠看到句子ABCD對應的翻譯結果就是XYZ,而Q是表明翻譯結束的結束符。

循環神經網絡能夠被看作是同一神經網絡結構在時間序列上被複制屢次的結果,這個被複制屢次的結構被稱之爲循環體。如何設計循環體的網絡結構是循環神經網絡解決實際問題的關鍵。和卷積神經網絡過濾器中參數是共享的相似。在循環神經網絡中,循環體網絡結構中的參數在不一樣時刻也是共享的。

上圖展現了一個使用最簡單的循環體結構的循環神經網絡,這個循環體中只使用了一個相似全鏈接層的神經網絡結構。循環神經網絡中的狀態是經過一個向量來表示的,這個向量的維度也稱爲循環神經網絡隱藏層的大小,假設其爲h。循環體中的神經網絡的輸入有兩部分,一部分爲上一時刻的狀態,另外一部分爲當前時刻的輸入樣本。對於時間序列數據來講,每一時刻的輸入樣例能夠是當前時刻的數值(好比銷售值),對於語言模型來講,輸入樣例能夠是當前單詞對應的單詞向量(word embedding)

假設輸入向量的維度爲x,那麼上圖中循環體的全鏈接神經網絡的輸入大小爲h+x。也就是將上一時刻的狀態與當前時刻的輸入拼接成一個大的向量做爲循環體中神經網絡的輸入。由於該神經網絡的輸出爲當前時刻的狀態,因而輸出層的節點個數也爲h,循環體中的參數個數爲(h+x)*h+h個。循環體重的神經網絡輸出不但提供給了下一時刻做爲狀態,同時也會提供給當前時刻的輸出。爲了將當前時刻的狀態轉化爲最終的輸出,循環神經網絡還須要另一個全鏈接神經網絡來完成這個過程。

這和卷積神經網絡中最後的全鏈接層的意義是同樣的。相似的,不一樣時刻用於輸出的全鏈接神經網絡中的參數也是一直的。

下圖展現了一個循環神經網絡前向傳播的具體計算過程

假設狀態的維度爲2,輸入、輸出的維度都爲1,並且循環體中的全鏈接層中權重爲

偏置項的大小爲brnn = [0.1,-0.1],用於輸出的全鏈接層權重爲

偏置項大小爲boutput=0.1。在時刻t0,由於沒有上一時刻,因此將狀態初始化爲[0,0],而當前的輸入爲1,因此拼接獲得的向量爲[0,0,1],經過循環體重的全鏈接層神經網絡獲得的結果爲:

這個結果將做爲下一時刻的輸入狀態,同時循環神經網絡也會使用該狀態生成輸出。將該向量做爲輸入提供給用於輸出的全鏈接神經網絡能夠獲得t0時刻的最終輸出

使用t0時刻的狀態能夠相似地推導得出t1時刻的狀態爲[0.860,0.884],而t1時刻的輸出爲2.73。在獲得循環神經網絡的前向傳播結果以後,能夠和其餘神經網絡相似地定義損失函數。循環神經網絡惟一的區別在於由於它每一個時刻都有一個輸出,因此循環神經網絡的總損失爲全部時刻(或者部分時刻)上的損失函數的總和。

下面代碼實現了這個假單的循環神經網絡前向傳播的過程

import numpy as np

X = [1, 2]
state = [0.0, 0.0]
# 分開定義不一樣輸入部分的權重以方便操做
w_cell_state = np.asarray([[0.1, 0.2], [0.3, 0.4]])
w_cell_input = np.asarray([0.5, 0.6])
b_cell = np.asarray([0.1, -0.1])

# 定義用於輸出的全鏈接層參數
w_output = np.asarray([[1.0], [2.0]])
b_output = 0.1
# 按照時間順序執行循環神經網絡的前向傳播過程
for i in range(len(X)):
    # 計算循環體中的全鏈接層神經網絡
    before_activation = np.dot(state, w_cell_state) + X[i] * w_cell_input + b_cell
    state = np.tanh(before_activation)
    # 根據當前時刻狀態計算最終輸出
    final_output = np.dot(state, w_output) + b_output
    # 輸出每一個時刻的信息
    print('before activation:', before_activation)
    print('state:', state)
    print('output:', final_output)

和其餘神經網絡相似,在定義完損失函數以後,TensorFlow就能夠自動完成模型訓練的過程。

理論上循環神經網絡能夠支持任意長度的序列,然而在實際中,若是序列過長會致使優化時出現梯度消散的問題(the vanishing gradient problem),因此實際中通常會規定一個最大長度,當序列長度超過規定長度以後會對序列進行截取

長短時記憶網絡(LSTM)結構

循環神經網絡工做的關鍵點就是使用歷史的信息來幫助當前的決策。例如使用以前出現的單詞來增強對當前文字的理解。循環神經網絡能夠更好地利用傳統神經網絡結構所不能建模的信息,但同時,這也帶來了更大的技術挑戰--長期依賴(long-term dependencies)問題

模型僅僅須要短時間內的信息來執行當前的任務,好比預測短語‘大海的顏色是藍色’中的最後一個單詞「藍色」時,模型並不須要記憶這個短語以前更長的上下文信息,由於這一句話已經包含了足夠的信息來預測最後一個詞。這樣的場景中,相關的信息和待預測的詞的位置之間的間隔很小,循環神經網絡能夠比較容易地利用先前信息。

一樣也會有一些上下文場景更加複雜的狀況。好比當模型試着去預測段落「某地開設了大量工廠,空氣污染十分嚴重...這裏的天空都是灰色的」的最後一個單詞時,僅僅根據短時間依賴就沒法很好地解決這種問題。由於只根據最後一段,最後一個詞能夠是「藍色的」或者「灰色的」。但若是模型須要預測清楚具體是什麼顏色,就須要考慮先前提到的但離當前位置較遠的上下文信息。所以,當前預測位置和相關信息之間的文本間隔就有可能變得很大。當這個間隔不斷增大時,給出的簡單循環神經網絡有可能會喪失學習到距離如此遠的信息的能力。或者在複雜語言場景中,有用信息的間隔有大有小,長短不一,循環神經網絡的性能也會受到限制。

長短時記憶網絡(long short term memory,LSTM)的設計就是爲了解決這個問題,而循環神經網絡被成功應用的關鍵就是LSTM。在不少的任務上,採用LSTM結構的循環神經網絡比標準的循環神經網絡表現更好。LSTM結構是由Sepp Hochreiter和Jurgen Schmidhuber與1997年提出的,是一種特殊的循環體結構,與單一tanh循環體結構不一樣,LSTM是一種擁有三個「門」結構的特殊網絡結構

LSTM靠近一些「門」的結構讓信息有選擇性地影響循環神經網絡中每一個時刻的狀態。所謂「門」的結構就是一個使用sigmoid神經網絡和一個按位作乘法的操做。這兩個操做合在一塊兒就是一個「門」的結構。之因此該結構叫作「門」是由於使用sigmoid做爲激活函數的全鏈接神經網絡層會輸出一個0到1之間的數值,描述當前輸入有多少信息量能夠經過這個結構。因而這個結構的功能就相似於一扇門,當門打開時(sigmoid神經網絡層輸出爲1時)所有信息均可以經過。當門關上時(sigmoid神經網絡層輸出爲0時),任何信息都沒法經過。

爲了使循環神經網絡更有效的保存長期記憶,遺忘門和輸入門相當重要,它們是LSTM結構的核心。遺忘門的做用是讓循環神經網絡忘記以前沒有用的信息。好比文章中先介紹了某地原來是綠水藍天,但後來被污染了。因而在看到被污染了以後,循環神經網絡應該「忘記」以前綠水藍天的狀態。這個工做是經過「遺忘門」來完成的。遺忘門會根據當前的輸入xt,上一時刻狀態ct-1和上一時刻輸出ht-1共同決定那一部分記憶須要被遺忘。在循環神經網絡「忘記」了部分以前的狀態後,還須要從當前的輸入補充最新的記憶。這個過程就是「輸入門」完成的。輸入門會根據xt、ct-1和ht-1決定哪些部分將進入當前時刻的狀態ct。好比當看到文件中提到環境被污染以後,模型須要將這個信息寫入新的狀態。經過「遺忘門」和「輸入門」,LSTM結構能夠更加有效的決定哪些信息應該被遺忘,哪些信息應該獲得保留。

LSTM結構在計算獲得新的狀態ct後須要產生當前時刻的輸出,這個過程是經過輸出門完成的。輸出門會根據最新的狀態ct、上一時刻的輸出ht-1和當前的輸入xt來決定該時刻的輸出ht。好比當前的狀態爲被污染,那麼「天空的顏色」後面的單詞極可能就是「灰色的」

使用LSTM解構的循環神經網絡的前向傳播是一個相對比較複雜的過程。

下面代碼在TensorFlow中實現使用LSTM結構的循環神經網絡的前向傳播過程

import numpy as np
import tensorflow as tf
from tensorflow.python.ops import rnn_cell

# 定義一個LSTM結構。在TensorFlow中經過一句簡單的命令就能夠實現一個完整LSTM結構
# LSTM中使用的變量也會在該函數中自動被聲明
lstm = rnn_cell.BasicLSTMCell(lstm_hidden_size)
# 將LSTM中的狀態初始化爲全0數組。和其餘神經網絡相似,在優化循環神經網絡時,
# 每次也會使用一個batch的訓練樣本。如下代碼中,batch_size給出了一個batch的大小
# BasicLSTMCell類提供了zero_state函數來生成全領的初始狀態
state = lstm.zero_state(batch_size, tf.float32)
# 定義損失函數
loss = 0.0
# 避免梯度消散問題,規定一個最大的序列長度
for i in range(num_steps):
    # 第一個時刻聲明LSTM結構中使用的變量,在以後的時刻都須要複用以前定義好的變量
    if i > 0 :
        tf.get_variable_scope().reuse_variables()
    # 每一步處理時間序列中的一個時刻。將當前輸入(current_input)和前一刻狀態(state)傳入定義的LSTM結構
    # 能夠獲得當前LSTM結構的輸出lstm_output和更新後的狀態state
    lstm_output,state = lstm(current_input,state)
    # 將當前LSTM結構的輸出傳入一個全鏈接層獲得最後的輸出
    final_output = fully_connected(lstm_output)
    # 計算當前時刻輸出的損失
    loss += calc_loss(final_output,expected_output)

經過TensorFlow能夠很是方便地實現使用LSTM結構的循環神經網絡,並且並不須要用戶對LSTM內部結構有深刻的瞭解

循環神經網絡的變種

雙向循環神經網絡和深層循環神經網絡

在經典的循環神經網絡中,狀態的傳輸是從前日後單向的。然而在有些問題中,當前時刻的輸出不只和以前的狀態有關係,也和以後的狀態相關。這時就須要使用雙向循環神經網絡(bidirectional RNN)來解決這類問題。

例如預測一個詞語中缺失的單詞不只須要根據前文來判斷,也須要根據後面的內容,這時雙向循環網絡就能夠發揮它的做用。雙向循環神經網絡是由兩個循環神經網絡上下疊加在一塊兒組成的。輸出由這兩個循環神經網絡的狀態共同決定。

雙向循環神經網絡的主體結構就是兩個單項循環神經網絡的結合。每一個時刻t,輸入法會同時提供給這兩個方向相反的循環神經網絡,而輸出則是由這兩個單向循環神經網絡共同決定。雙向循環神經網絡的前向傳播過程和單向的循環神經網絡十分相似。

深層循環神經網絡(deep RNN)是循環神經網絡的另一種變種。爲了加強模型的表達能力,能夠將每個時刻上的循環體重複屢次。

 

上體給出了深層循環神經網絡的結構示意圖。相比循環神經網絡,深層循環神經網絡在每個時刻上將循環體結構複製了屢次。和卷積神經網絡相似,每一層的循環體中參數是一致的,而不一樣層中的參數能夠不一樣。爲了更好地支持深層循環神經網絡,TensorFlow提供了MultiRNNCell類來實現深層循環神經網絡的前向傳播過程

import tensorflow as tf
from tensorflow.python.ops import rnn_cell

# 定義一個基本的LSTM結構做爲循環體的基礎結構。深層循環神經網絡也支持使用其餘的循環體結
lstm = rnn_cell.BasicLSTMCell(lstm_size)
# 經過MultiRNNCell類實現深層循環神經網絡中每個時刻的前向傳播過程。
# 其中number_of_layers表示了有多少層,也就是從xt到ht須要通過多少個LSTM結構
stacked_lstm = rnn_cell.MultiRNNCell([lstm] * number_of_layers)
# 和經典循環神經網絡同樣,能夠經過zero_state函數來獲取初始狀態
state = stacked_lstm.zero_state(batch_size, tf.float32)
# 計算每一時刻的前向傳播結果
for i in range(len(num_steps)):
    if i > 0:
        tf.get_variable_scope().reuse_variables()
    stacked_lstm_output, state = stacked_lstm(current_input, state)
    final_output = fully_connected(stacked_lstm_output)
    loss += calc_loss(final_output, expected_output)

TensorFlow中只須要BasicLSTMCell的基礎上再封裝一層MultiRNNCell就能夠很是容易地實現深層循環神經網絡了

循環神經網絡的dropout 

經過dropout可讓卷積神經網絡更加健壯。相似的在循環神經網絡中使用dropout也有一樣的功能。並且相似卷積神經網絡只在最後的全鏈接層中使用dropout,循環神經網絡通常只在不一樣層循環體結構之間使用dropout,而不在同一層的循環體結構之間使用。從時刻t-1傳遞到時刻t時,循環神經網絡不會進行狀態的dropout。而在同一個時刻t中,不一樣層循環體之間會使用dropout

上圖展現了神經網絡使用dropout示意圖。假設要從t-2時刻的輸入xt-2傳遞到t+1時刻的輸出yt+1,那麼xt-2將首先傳入第一層循環體結構,這個時候會使用dropout。但從t-2時刻的第一層循環體結構傳遞到第一層的t-一、t、t+1時刻不會使用dropout。在t+1時刻的第一層循環體結構傳遞到同一時刻內更高層的循環體結構時,會再次使用dropout。

在TensorFlow中,使用tf.nn.rnn_cell.DropoutWrapper類能夠很容易實現dropout功能。

import tensorflow as tf
from tensorflow.python.ops import rnn_cell

# 定義一個基本的LSTM結構
lstm = rnn_cell.BasicLSTMCell(lstm_size)
# 使用DropoutWrapper類來實現dropout功能。經過兩個參數來控制
# 第一個參數input_keep_prob,用來控制輸入的dropout機率
# 另外一個參數output_keep_prob,用來控制輸出的dropout機率
dropout_lstm = tf.nn.rnn_cell.DropoutWrapper(lstm, output_keep_prob=0.5)
# 使用了dropout的基礎上定義
stacked_lstm = rnn_cell.MultiRNNCell([dropout_lstm] * number_of_layers)

 

循環神經網絡樣例應用 

天然語言建模

簡單來講,語言模型的目的是爲了計算一個句子的出現機率。在這裏把句子當作是單詞的序列,因而語言模型須要計算的就是p(w1,w2,w3,...,wm)。利用語言模型,能夠肯定哪一個單詞序列出現的可能性更大,或者給定若干單詞,能夠預測下一個最可能出現的詞語。

舉個音字轉換的例子,假設輸入的拼音串爲「xianzaiquna」,它的輸出能夠是「西安在去哪」或「如今去哪」。根據語言常識能夠知道,轉換成第二個機率更高。語言模型就能夠獲得後者的機率大於前者,所以在大多數狀況下轉換成後者比較合理。

如何計算一個句子的機率呢?首先一個句子可被當作是一個單詞序列

S = (w1,w2,w3,w4,w5,...,wm)

其中m爲句子的長度。那麼它的機率能夠表示爲

要計算一個句子出現的機率,就須要知道上面公式中等式右邊中每一項的取值。等式右邊的每一項都是語言模型中的一個參數。通常來講,任何一門語言的詞彙量都很大,詞彙的組合更不可勝數。爲了估計這些參數的取值,常見的方法有n-gram方法、決策樹、最大熵模型、條件隨機場、神經網絡語言模型,等等。

本節使用最簡單的n-gram模型來介紹語言模型問題以及評價模型優劣的標準。n-gram模型有一個有限歷史假設:當前單詞的出現機率僅僅與前面的n-1個單詞相關。所以上面公式能夠近似爲:

n-gram模型裏的n指的是當前單詞依賴它前面的單詞的個數。一般n能夠取1,2,3。這時n-gram模型分別稱爲unigram、bigram、trigram語言模型。n-gram模型中須要估計的參數爲條件機率

假設某種語言的單詞表大小爲k,那麼n-gram模型須要估計的不一樣參數數量爲kn。當n越大時,n-gram模型理論上越準確,但也越複雜,須要的計算量和訓練語料數據量也就越大。所以最經常使用的是bigram,其次是unigram和trigram。n取>=4的狀況很是少。n-gram模型的參數通常採用最大似然估計(maximum likelihood estimation,MLE)的方法計算

其中C(X)表示單詞序列X在訓練語料中出現的次數。訓練語料的規模越大,參數估計的結果越可靠。但即便訓練數據的規模很是大時,仍是會有不少單詞序列在訓練語料中沒有出現過,這就會致使不少參數爲0。

舉個例子,IBM使用了366M英語語料訓練trigram,發現14.7%的trigram和2.2%的bigram在訓練中沒有出現。爲了不由於乘以0而致使整個機率爲0,使用最大似然估計方法時都須要加入平滑避免參數取值爲0。使用n-gram創建語言模型的細節再也不贅述。

語言模型效果好壞的經常使用評價指標是複雜度(perplexity)。簡單來講perplexity值刻畫的就是經過某一個語言模型估計的一句話出現的機率。好比當已經知道(w1,w2,w3,...,wm) 這句話出如今語料庫之中,那麼經過語言模型計算獲得的這句話的機率越高越好,也就是perplexity值越小越好。計算perplexity值的公式以下

複雜度perplexity表示的概念實際上是平均分支系數(average branch factor)即模型預測下一個詞時的平都可選擇數量。考慮一個由0~9這10個數字隨機組成的長度爲m的序列。因爲這10個數字出現的機率是隨機的,因此每一個數字出現的機率是1/10。所以在任意時刻,模型都有10個等機率的候選答案能夠選擇,因而perplexity就是10(有10個合理的答案)

perplexity的計算過程以下

所以,若是一個語言模型的perplexity是89,就表示,平均狀況下,模型預測下一個詞時,有89個詞等可能地能夠做爲下一個詞的合理選擇。另外一種經常使用的perplexity表達形式以下

相比乘積開根號的方式,使用加法的形式能夠加速計算,這也有效避免了機率爲0時致使整個計算結果爲0的問題

除了n-gram模型,循環神經網絡也能夠用來對天然語言建模。每一個時刻的輸入爲一個句子中的單詞,而每一個時刻的輸出爲一個機率分佈,表示句子中下一個位置爲不一樣單詞的機率。經過這種方式,對於給定的句子,就能夠經過循環神經網絡的前向傳播過程計算出。在第一個時刻輸入的單詞爲「大海」,而輸出爲p(x|"大海")。

也就是知道第一個詞爲「大海」後,其餘不一樣單詞出如今下一個位置的機率。下圖中能夠看出,p(「的」|「大海」)=0.8,也就是說「大海」以後的單詞爲「的」的機率爲0.8

相似的,經過循環神經網絡能夠求得機率p(x|「大海」,「的」)、p(x|「大海」,「的」,「顏色」)、p(x|「大海」,「的」,「顏色」,「是」)。因而也能夠求得整個句子「大海的顏色是藍色」的機率,從而計算出這句話的perplexity值

PTB文本數據集介紹

PTB(Penn Treebank Dataset)文本數據集是語言模型學習中目前最普遍使用的數據集。本節將在PTB數據上使用循環神經網絡實現語言模型。給出語言模型代碼以前將先簡單介紹PTB數據集的格式以及TensorFlow對於PTB數據集的支持。

首先須要下載PTB數據,simple-examples.tgz,OneDrive中有

data文件夾下總共有7個文件,只會用到三個文件

ptb.test.txt # 測試集數據文件

ptb.train.txt # 訓練集數據文件

ptb.valid.txt # 驗證集數據文件

三個數據文件中的數據已經通過了預處理,包含了10000個不一樣的詞語和語句結束標記符(換行符)以及標記稀有詞語的特殊符號<unk>。

爲了讓使用PTB數據集更加方便,TensorFlow提供了兩個函數來幫助實現數據的預處理。首先TensorFlow提供了ptb_raw_data函數來讀取PTB的原始數據,並將原始數據中的單詞轉化爲單詞ID。

首先須要加載model,來引入PTB

git clone --recurse-submodules https://github.com/tensorflow/models

而後引入

from models.tutorials.rnn.ptb import reader

若是報錯

ModuleNotFoundError: No module named 'util'

則刪除掉__init__.py中的import引用

import tensorflow as tf
from models.tutorials.rnn.ptb import reader

# 存放原始數據的路徑
DATA_PATH = 'simple-examples/data'
train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH)

# 將訓練數據組成batch大小爲4,截斷長度爲5的數據組
result = reader.ptb_producer(train_data, 4, 5)
# 讀取第一個batch中的數據,其中包括每一個時刻的輸入和對應的正確輸出
with tf.Session() as sess:
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(sess=sess, coord=coord)
    for i in range(3):
        x, y = sess.run(result)
        print('X%d' % i, x)
        print('Y%d' % i, y)
    coord.request_stop()
    coord.join(threads)

 

上圖展現了ptb_producer函數實現的功能。會將一個長序列劃分紅batch_size段,其中batch_size爲一個batch的大小。每次調用ptb_producer時,該函數會從每段中讀取長度爲num_step的子序列,其中num_step爲截斷的長度。

上面代碼輸出以下

X0 [[9970 9971 9972 9974 9975]
[ 332 7147 328 1452 8595]
[1969 0 98 89 2254]
[ 3 3 2 14 24]]
Y0 [[9971 9972 9974 9975 9976]
[7147 328 1452 8595 59]
[ 0 98 89 2254 0]
[ 3 2 14 24 198]]

在第一個batch的第一行中,前面5個單詞的ID和整個訓練數據中前5個單詞的ID是對應的。在生成batch時能夠會自動生成每一個batch對應的正確答案,這個對於每個單詞,對應的正確答案就是該單詞的後面一個單詞

使用循環神經網絡實現語言模型

下面代碼是經過循環神經網絡實現語言模型

# -*- coding:utf-8 -*-
import numpy as np
import tensorflow as tf
from models.tutorials.rnn.ptb import reader

DATA_PATH = 'simple-examples/data'
HIDDEN_SIZE = 200  # 隱藏層規模
NUM_LAYERS = 2  # 深層循環神經網絡中LSTM結構的層數
VOCAB_SIZE = 10000  # 詞典規模加上語句結束標識符和稀有單詞標識符總共一萬個單詞
LEARNING_RATE = 1.0  # 學習速率
TRAIN_BATCH_SIZE = 20  # 訓練數據batch的大小
TRAIN_NUM_STEP = 35  # 訓練數據截斷長度

# 測試時不須要使用截斷,能夠將測試數據當作一個超長的序列
EVAL_BATCH_SIZE = 1  # 測試數據batch的大小
EVAL_NUM_STEP = 1  # 測試數據截斷長度
NUM_EPOCH = 2  # 使用訓練數據的輪數
KEEP_PROB = 0.5  # 節點不被dropout的機率
MAX_GRAD_NORM = 5  # 用於控制梯度膨脹的參數


# 經過一個PTBModel類來描述模型,這樣方便維護循環神經網絡中的狀態
class PTBModel(object):
    def __init__(self, is_training, batch_size, num_steps):
        # 記錄使用的batch大小和截斷長度
        self.batch_size = batch_size
        self.num_steps = num_steps
        # 定義輸入層,能夠看到輸入層的維度爲batch_size * num_steps,
        # 和ptb_producer函數輸出的訓練數據batch是一致的
        self.input_data = tf.placeholder(tf.int32, [batch_size, num_steps])
        # 定義預期輸出,維度和ptb_producer函數輸出的正確答案維度同樣的
        self.targets = tf.placeholder(tf.int32, [batch_size, num_steps])
        # 定義LSTM結構爲循環體結構且使用dropout的深層循環神經網絡
        lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        if is_training:
            lstm_cell = tf.nn.rnn_cell.DropoutWrapper(lstm_cell, output_keep_prob=KEEP_PROB)
        cell = tf.nn.rnn_cell.MultiRNNCell([lstm_cell] * NUM_LAYERS)
        # 初始化最初的狀態,也就是全零的向量
        self.initial_state = cell.zero_state(batch_size, tf.float32)
        # 將單詞ID轉換成單詞向量,總共有VOCAB_SIZE個單詞,每一個單詞向量的維度爲HIDDEN_SIZE
        # 因此embedding參數的維度爲VOCAB_SIZE*HIDDEN_SIZE
        embedding = tf.get_variable('embedding', [VOCAB_SIZE, HIDDEN_SIZE])
        # 將本來batch_size*num_steps個單詞ID轉化爲單詞向量,轉化後的輸出層維度
        # 爲batch_size*num_steps*HIDDEN_SIZE
        inputs = tf.nn.embedding_lookup(embedding, self.input_data)
        # 只在訓練時使用dropout
        if is_training:
            inputs = tf.nn.dropout(inputs, KEEP_PROB)
        # 定義輸出列表,這裏先將不一樣時刻LSTM結構的輸出收集起來,再經過一個全鏈接層獲得最終輸出
        outputs = []
        # state存儲不一樣batch中LSTM的狀態,將其初始化爲0
        state = self.initial_state
        with tf.variable_scope('RNN'):
            for time_step in range(num_steps):
                if time_step > 0:
                    tf.get_variable_scope().reuse_variables()
                cell_output, state = cell(inputs[:, time_step, :], state)
                # 將當前輸出加入輸出隊列
                outputs.append(cell_output)
        # 輸出隊列展開成[batch,hidden_size*num_steps]的形狀
        # 再reshape成[batch*numsteps,hidden_size]的形狀
        output = tf.reshape(tf.concat(outputs, 1), [-1, HIDDEN_SIZE])
        # 將LSTM中獲得的輸出再通過一個全鏈接層獲得最後的預測結果
        # 最終的預測結果在每個時刻上都是一個長度爲VOCAB_SIZE的數組,
        # 通過softmax層以後表示下一個位置是不一樣單詞的機率
        weight = tf.get_variable('weight', [HIDDEN_SIZE, VOCAB_SIZE])
        bias = tf.get_variable('bias', [VOCAB_SIZE])
        logits = tf.matmul(output, weight) + bias
        # 定義交叉熵損失函數。sequence_loss_by_example函數來計算一個序列的交叉熵的和
        loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example(
            [logits],
            [tf.reshape(self.targets, [-1])],
            [tf.ones([batch_size * num_steps], dtype=tf.float32)])
        # 計算獲得的每一個batch的平均損失
        self.cost = tf.reduce_sum(loss) / batch_size
        self.final_state = state
        # 只在訓練模型時定義反轉傳播操做
        if not is_training:
            return
        trainable_variables = tf.trainable_variables()
        # 經過clip_by_global_norm函數控制梯度的大小,避免梯度膨脹的問題
        grads, _ = tf.clip_by_global_norm(
            tf.gradients(self.cost, trainable_variables), MAX_GRAD_NORM)
        # 定義優化方式
        optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE)
        # 定義訓練步驟
        self.train_op = optimizer.apply_gradients(zip(grads, trainable_variables))


# 使用給定的模型model在數據data上運行train_op並返回在所有數據上的perplexity值
def run_epoch(session, model, data, train_op, output_log, epoch_size):
    # 計算perplexity的輔助變量
    total_costs = 0.0
    iters = 0
    state = session.run(model.initial_state)
    # 使用當前數據訓練或者測試模型
    for step in range(epoch_size):
        x, y = session.run(data)
        # 當前batch上運行train_op並計算損失值。交叉熵損失函數計算的就是下一個單詞爲給定單詞的機率
        cost, state, _ = session.run(
            [model.cost, model.final_state, train_op],
            {model.input_data: x, model.targets: y, model.initial_state: state})
        # 將不一樣時刻、不一樣batch的機率加起來就能夠獲得第二個perplexity公式等號右邊的部分
        # 再將這個和作指數運算就能夠獲得perplexity值
        total_costs += cost
        iters += model.num_steps
        # 只有在訓練時輸出日誌
        if output_log and step % 100 == 0:
            print('after %d steps,perplexity is %.3f' % (step, np.exp(total_costs / iters)))
    # 返回給定模型在給定數據上的perplexity值
    return np.exp(total_costs / iters)


def main(args=None):
    # 獲取原始數據
    train_data, valid_data, test_data, _ = reader.ptb_raw_data(DATA_PATH)
    # 計算一個epoch須要訓練的次數
    train_data_len = len(train_data)
    train_batch_len = train_data_len // TRAIN_BATCH_SIZE
    train_epoch_size = (train_batch_len - 1) // TRAIN_NUM_STEP

    valid_data_len = len(valid_data)
    valid_batch_len = valid_data_len // EVAL_BATCH_SIZE
    valid_epoch_size = (valid_batch_len - 1) // EVAL_NUM_STEP

    test_data_len = len(test_data)
    test_batch_len = test_data_len // EVAL_BATCH_SIZE
    test_epoch_size = (test_batch_len - 1) // EVAL_NUM_STEP

    # 定義初始化函數
    initializer = tf.random_uniform_initializer(-0.05, 0.05)
    # 定義訓練用的循環神經網絡模型
    with tf.variable_scope("language_model", reuse=None, initializer=initializer):
        train_model = PTBModel(True, TRAIN_BATCH_SIZE, TRAIN_NUM_STEP)
    # 定義評測用的循環神經網絡模型
    with tf.variable_scope('language_model', reuse=True, initializer=initializer):
        eval_model = PTBModel(False, EVAL_BATCH_SIZE, EVAL_NUM_STEP)

    with tf.Session() as sess:
        tf.global_variables_initializer().run()
        train_queue = reader.ptb_producer(train_data, train_model.batch_size, train_model.num_steps)
        eval_queue = reader.ptb_producer(valid_data, eval_model.batch_size, eval_model.num_steps)
        test_queue = reader.ptb_producer(test_data, eval_model.batch_size, eval_model.num_steps)

        coord = tf.train.Coordinator()
        threads = tf.train.start_queue_runners(sess=sess, coord=coord)

        # 使用訓練數據訓練模型
        for i in range(NUM_EPOCH):
            print('In iteration: %d' % (i + 1))
            # 在全部訓練數據上訓練循環神經網絡模型
            run_epoch(sess, train_model, train_queue, train_model.train_op, True, train_epoch_size)
            # 使用驗證數據測評模型效果
            valid_perplexity = run_epoch(sess, eval_model, eval_queue, tf.no_op(), False, valid_epoch_size)
            print('Epoch: %d Validation Perplexity:%.3f' % (i + 1, valid_perplexity))
        # 最後使用測試數據測試模型效果
        test_perplexity = run_epoch(sess, eval_model, test_queue, tf.no_op(), False, test_epoch_size)
        print('Test Perplexity:%.3f' % test_perplexity)
        coord.request_stop()
        coord.join(threads)


if __name__ == '__main__':
    tf.app.run()

迭代開始時perplexity值爲10003.783,這基基本至關於從一萬個單詞中隨機選擇下一個單詞。而在訓練結束後,在訓練數據集上的perplexity值下降到了179.420。這代表經過訓練過程,將選擇下一個單詞的範圍從一萬個減少到了大約180個,經過調整LSTM隱藏層的節點個數和大小以及訓練迭代的輪數還能夠將perplexity值降到更低。

時間序列預測

本節介紹如何利用循環神經網絡來預測正弦(sin)函數,下圖給出了sin函數的函數圖像。

TFLearn和TensorFlow-Slim相似,經過使用TFLearn可讓TensorFlow代碼效率進一步提升

使用TFLearn自定義模型

TensorFlow另一個高層封裝TFLearn(繼承在tf.contrib.learn裏)對訓練TensorFlow模型進行了一些封裝,使其更便於使用。本節經過iris數據集簡單介紹如何使用TFLearn實現分類問題。

iris數據集須要經過4個特徵(feature)來分辨三種類型的植物。iris數據集中總共包含了150個樣本。如下程序介紹瞭如何經過TFLearn快速的解決iris分類問題

# -*- coding:utf-8 -*-
# 爲了方便數據處理,本程序使用sklearn工具包
from sklearn import datasets, model_selection, metrics
import tensorflow as tf
import numpy as np

# 導入TFLearn
from tensorflow.contrib.learn import SKCompat

learn = tf.contrib.learn


# 自定義模型,對於給定的輸入數據(features)以及其對應的正確答案(target)
# 返回在這些輸入上的預測值、損失值以及訓練步驟
def my_model(features, target):
    # 將預測的目標轉換爲one-hot編碼的形式,由於共有三個類別,因此向量長度爲3
    # 通過轉化後,第一個類別表示爲(1,0,0)第二個爲(0,1,0)第三個爲(0,0,1)
    target = tf.one_hot(target, 3, 1, 0)
    # 定義模型以及其在給定數據上的損失函數。
    # TFLearn經過logistic_regression封裝了一個單層全鏈接神經網絡
    logits, loss = learn.models.logistic_regression(features, target)
    # 建立模型的優化器,並獲得優化步驟
    train_op = tf.contrib.layers.optimize_loss(
        loss, tf.contrib.framework.get_global_step(),
        optimizer='Adagrad',learning_rate=0.1)
    return tf.arg_max(logits, 1), loss, train_op


# 加載iris數據集,並劃分爲訓練集合和測試集合
iris = datasets.load_iris()
x_train, x_test, y_train, y_test = model_selection.train_test_split(
    iris.data, iris.target, test_size=0.2,random_state=0)
x_train, x_test = map(np.float32, [x_train, x_test])
# 對自定義的模型進行封裝
classifier = SKCompat(learn.Estimator(model_fn=my_model, model_dir="Saved_model/model_1"))
classifier.fit(x_train, y_train, steps=800)

y_predicted = [i for i in classifier.predict(x_test)]
score = metrics.accuracy_score(y_test, y_predicted)
print('Accuracy: %.2f%%' % (score * 100))

經過上面程序能夠看出TFLearn既封裝了一些經常使用的神經網絡結構,又省去了模型訓練的部分,讓TensorFlow的程序變得更加簡短

TensorBoard可視化

在將這些神經網絡用於實際問題以前,須要先優化神經網絡中的參數。這就是訓練神經網絡的過程。訓練神經網絡十分複雜,有時須要幾天甚至幾周的時間。爲了更好的管理、調試、優化神經網絡的訓練過程,TensorFlow提供了一個可視化工具TensorBoard。能夠有效展現運行過程當中的計算圖、各類指標隨着時間的變化趨勢以及訓練中使用到的圖像等信息。

TensorBoard簡介

TensorBoard能夠經過TensorFlow程序運行過程當中輸出的日誌文件可視化程序的運行過程。兩個程序跑在不一樣的進程中,TensorBoard會自動讀取最新的TensorFlow日誌文件,並呈現當前TensorFlow程序運行的最新狀態

TensorFlow計算加速

 一個很是大的問題在於訓練深度學習模型須要的計算量太大,好比Inception-v3模型在單機上訓練到78%的正確率須要近半年的時間,這樣的訓練速度是徹底沒法應用到實際生產中的。爲了加速訓練過程,本節介紹利用GPU或分佈式計算進行模型訓練

TensorFlow使用GPU

TensorFlow程序能夠經過tf.device函數來指定運行每個操做的設備,這個設備能夠是本地的CPU或者GPU,也能夠是某一臺遠程的服務器。

TensorFlow會給每個可用的設備一個名稱,tf.device函數能夠經過設備的名稱來指定執行運算的設備。好比CPU在TensorFlow中的名稱爲/cpu:0。默認狀況下,即便機器有多個CPU,TensorFlow也不會區分它們,全部的CPU都使用/cpu:0做爲名稱。而一臺機器上不一樣GPU的名稱是不一樣的,第n個GPU在TensorFlow中的名稱爲/gpu:n。好比第一個GPU的名稱爲/gpu:0,第二個GPU名稱爲/gpu:1,以此類推

TensorFlow提供了一個快捷的方式來查看運行每個運算的設備。生成會話時,能夠經過設置log_device_placement參數來打印運行每個運算的設備。下面展現如何使用log_device_placement

import tensorflow as tf

a = tf.constant([1.0, 2.0, 3.0], shape=[3], name='a')
b = tf.constant([1.0, 2.0, 3.0], shape=[3], name='b')
c = a + b

# 經過log_device_placement參數來輸出運行每個運算設備
with tf.Session(config=tf.ConfigProto(log_device_placement=True)) as sess:
    print(sess.run(c))

 

在沒有GPU的機器上運行以上代碼獲得以下輸出

Device mapping: no known devices.

add: (Add): /job:localhost/replica:0/task:0/device:CPU:0
a: (Const): /job:localhost/replica:0/task:0/device:CPU:0
b: (Const): /job:localhost/replica:0/task:0/device:CPU:0

TensorFlow程序生成會話時加入了參數log_device_placement=True,因此程序會將運行每個操做的設備輸出到屏幕。因而除了能夠看到最後的計算結果以外,還能夠看到相似add: (Add): /job:localhost/replica:0/task:0/device:CPU:0這樣的輸出。

這些輸出顯示了執行每個運算的設備。好比加法操做add是經過CPU來運行的,由於它的設備名稱中包含了/cpu:0

配置好GPU環境的TensorFlow中,若是操做沒有明確地指定運行設備,那麼TensorFlow會優先選擇GPU。好比以上代碼在亞馬遜雲服務器上運行會獲得

add: (Add): /job:localhost/replica:0/task:0/device:CPU:0

在配置好GPU環境的TensorFlow中,TensorFlow會自動優先將運算放置在GPU上。默認狀況下,TensorFlow只會將運算優先放到/gpu:0上。若是須要將某些運算放在不一樣的GPU或CPU上,就須要經過tf.device來手動指定

import tensorflow as tf

with tf.device('/cpu:0'):
    a = tf.constant([1.0, 2.0, 3.0], shape=[3], name='a')
    b = tf.constant([1.0, 2.0, 3.0], shape=[3], name='b')
with tf.device('/gpu:1'):
    c = a + b

# 經過log_device_placement參數來輸出運行每個運算設備
with tf.Session(config=tf.ConfigProto(log_device_placement=True)) as sess:
    print(sess.run(c))

不一樣版本的TensorFlow對GPU的支持不同,若是程序中所有使用強制指定設備的方式會下降程序的可移植性。在kernel中定義了那些操做能夠跑在GPU上。

雖然GPU能夠加速TensorFlow的計算,但通常來講不會吧全部的操做所有放在GPU上。一個比較好的實踐是將計算密集型的運算放在GPU上,而把其餘操做放在CPU上。GPU是機器中相對獨立的資源,將計算放入或者轉出GPU都須要額外的事件。並且GPU須要將計算時用到的數據從內存複製到GPU設備上,也須要額外的時間。TensorFlow能夠自動完成這些操做而不須要用戶特別處理,但爲了提升程序運行的速度,用戶也須要儘可能將相關的運算放在同一個設備上。

深度學習訓練並行模式 

TensorFlow能夠很容易利用單個GPU加速深度學習模型的訓練過程,但要利用更多的GPU或者機器,須要瞭解如何並行化地訓練深度學習模型。經常使用的並行化深度學習模型訓練方式有兩種,同步模式和異步模式

先回顧一下如何訓練深度學習模型

深度學習模型的訓練是一個迭代的過程。每一輪迭代中,前向傳播算法會根據當前參數的取值計算出在一小部分訓練數據上的預測值,而後反向傳播算法再根據損失函數計算參數的梯度並更新參數。在並行化地訓練深度學習模型時,不一樣設備能夠在不一樣訓練數據上運行這個迭代過程,而不一樣並行模式的區別在於不一樣的參數更新方式。

上圖展現了一步模型的訓練流程圖。每一輪迭代時,不一樣設備會讀取參數最新的取值,由於不一樣設備讀取參數取值的時間不同,因此獲得的值也有可能不同。根據當前參數的取值和隨機獲取的一部分訓練數據,不一樣設備各自運行反向傳播的過程並獨立地更新參數。能夠簡單地認爲異步模式就是單機模式複製了多份,每一份使用不一樣的訓練數據進行訓練。異步模式下,不一樣設備之間是徹底獨立的。

然而使用異步模式訓練的深度學習模型有可能沒法達到較優的訓練結果。

上圖給出了一個具體的樣例來講明異步模式的問題。其中黑色曲線展現了模型的損失函數,黑色小球表示了在t0時刻參數所對應的損失函數的大小。假設兩個設備d0和d1在時間t0同時讀取了參數的取值,那麼設備d0和d1計算出來的梯度都會將小黑球向左移動。假設在時間t1設備d0已經完成了反向傳播的計算並更新了參數,修改後的參數處於小灰球的位置。然而這時的設備d1並不知道參數已經被更新了,在時間t2時,設備d1會繼續將小球向左移動,使得小球的位置達到小白球的地方。當參數被調整到小白球的位置時,將沒法達到最優勢。

爲了不更新不一樣步的問題,能夠使用同步模式。在同步模式下,全部的設備同時讀取參數的取值,而且當反向傳播算法完成以後同步更新參數的取值。單個設備不會單獨對參數進行更新,而會等待全部設備都完成反向傳播以後再統一更新參數。

上圖展現了同步模式的訓練過程。每一輪迭代時,不一樣設備首先統一讀取當前參數的取值,並隨機獲取一小部分數據。而後在不一樣設備上運行反向傳播過程獲得在各自訓練數據上參數的梯度。雖然全部設備使用的參數是一致的,可是由於訓練數據不一樣,所得獲得參數的梯度就可能不同。當全部設備完成反向傳播的計算以後,須要計算出不一樣設備上參數梯度的平均值,最後再根據平均值對參數進行更新。

同步模式解決了異步模式中存在的參數更新問題,然而同步模式的效率卻低於異步模式。在同步模式下,,每一輪迭代都須要設備統一開始、統一結束。若是設備的運行速度不一致,那麼每一輪訓練都須要等待最慢的設備結束才能開始更新參數,因而不少時間將被花在等待上。雖然理論上異步模式存在缺陷,但由於訓練深度學習模型時使用的隨機梯度降低自己就是梯度降低的一個近似解法,並且即便是梯度降低也沒法保證達到全局最優值,因此在實際應用中,在相同時間內,使用異步模型訓練的模型不必定比同步模式差。

多GPU並行

這節給出具體TensorFlow代碼,在一臺機器的多個GPU上並行訓練深度學習模型。通常來講一臺機器上的多個GPU性能類似,因此在這種設置下會更多地蠶蛹同步模式訓練深度學習模型。

下面代碼給出在多GPU上訓練深度學習模型解決MNIST問題。使用mnist_inference.py程序來完成神經網絡的前向傳播過程。

# coding=utf-8
from datetime import datetime
import os
import time

import tensorflow as tf
import mnist_inference

# 定義訓練神經網絡時須要用到的參數。
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.001
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 1000
MOVING_AVERAGE_DECAY = 0.99
N_GPU = 4

# 定義日誌和模型輸出的路徑。
MODEL_SAVE_PATH = "logs_and_models/"
MODEL_NAME = "model.ckpt"
DATA_PATH = "output.tfrecords"


# 定義輸入隊列獲得訓練數據,具體細節能夠參考第七章。
def get_input():
    filename_queue = tf.train.string_input_producer([DATA_PATH])
    reader = tf.TFRecordReader()
    _, serialized_example = reader.read(filename_queue)

    # 定義數據解析格式。
    features = tf.parse_single_example(serialized_example, features={'image_raw': tf.FixedLenFeature([], tf.string),
                                                                     'pixels': tf.FixedLenFeature([], tf.int64),
                                                                     'label': tf.FixedLenFeature([], tf.int64), })

    # 解析圖片和標籤信息。
    decoded_image = tf.decode_raw(features['image_raw'], tf.uint8)
    reshaped_image = tf.reshape(decoded_image, [784])
    retyped_image = tf.cast(reshaped_image, tf.float32)
    label = tf.cast(features['label'], tf.int32)

    # 定義輸入隊列並返回。
    min_after_dequeue = 10000
    capacity = min_after_dequeue + 3 * BATCH_SIZE
    return tf.train.shuffle_batch([retyped_image, label], batch_size=BATCH_SIZE, capacity=capacity,
                                  min_after_dequeue=min_after_dequeue)


# 定義損失函數。
def get_loss(x, y_, regularizer, scope, reuse_variables=None):
    with tf.variable_scope(tf.get_variable_scope(), reuse=reuse_variables):
        y = mnist_inference.inference(x, regularizer)
    cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(y, y_))
    regularization_loss = tf.add_n(tf.get_collection('losses', scope))
    loss = cross_entropy + regularization_loss
    return loss


# 計算每個變量梯度的平均值。
def average_gradients(tower_grads):
    average_grads = []

    # 枚舉全部的變量和變量在不一樣GPU上計算得出的梯度。
    for grad_and_vars in zip(*tower_grads):
        # 計算全部GPU上的梯度平均值。
        grads = []
        for g, _ in grad_and_vars:
            expanded_g = tf.expand_dims(g, 0)
            grads.append(expanded_g)
        grad = tf.concat(grads, 0)
        grad = tf.reduce_mean(grad, 0)

        v = grad_and_vars[0][1]
        grad_and_var = (grad, v)
        # 將變量和它的平均梯度對應起來。
        average_grads.append(grad_and_var)
    # 返回全部變量的平均梯度,這個將被用於變量的更新。
    return average_grads


# 主訓練過程。
def main(argv=None):
    # 將簡單的運算放在CPU上,只有神經網絡的訓練過程放在GPU上。
    with tf.Graph().as_default(), tf.device('/cpu:0'):
        # 定義基本的訓練過程
        x, y_ = get_input()
        regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)

        global_step = tf.get_variable('global_step', [], initializer=tf.constant_initializer(0), trainable=False)
        learning_rate = tf.train.exponential_decay(LEARNING_RATE_BASE, global_step, 60000 / BATCH_SIZE,
                                                   LEARNING_RATE_DECAY)

        opt = tf.train.GradientDescentOptimizer(learning_rate)

        tower_grads = []
        reuse_variables = False  # 將神經網絡的優化過程跑在不一樣的GPU上。
        for i in range(N_GPU):
            # 將優化過程指定在一個GPU上。
            with tf.device('/gpu:%d' % i):
                with tf.name_scope('GPU_%d' % i) as scope:
                    cur_loss = get_loss(x, y_, regularizer, scope, reuse_variables)
                    reuse_variables = True
                    grads = opt.compute_gradients(cur_loss)
                    tower_grads.append(grads)
        # 計算變量的平均梯度。
        grads = average_gradients(tower_grads)
        for grad, var in grads:
            if grad is not None:
                tf.histogram_summary('gradients_on_average/%s' % var.op.name, grad)

        # 使用平均梯度更新參數。
        apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)
        for var in tf.trainable_variables():
            tf.histogram_summary(var.op.name, var)

        # 計算變量的滑動平均值。
        variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
        variables_to_average = (tf.trainable_variables() + tf.moving_average_variables())
        variables_averages_op = variable_averages.apply(variables_to_average)
        # 每一輪迭代須要更新變量的取值並更新變量的滑動平均值。
        train_op = tf.group(apply_gradient_op, variables_averages_op)
        saver = tf.train.Saver(tf.all_variables())
        summary_op = tf.merge_all_summaries()
        init = tf.initialize_all_variables()

        with tf.Session(config=tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)) as sess:
            # 初始化全部變量並啓動隊列。
            init.run()
            coord = tf.train.Coordinator()
            threads = tf.train.start_queue_runners(sess=sess, coord=coord)
            summary_writer = tf.train.SummaryWriter(MODEL_SAVE_PATH, sess.graph)
            for step in range(TRAINING_STEPS):
                # 執行神經網絡訓練操做,並記錄訓練操做的運行時間。
                start_time = time.time()
                _, loss_value = sess.run([train_op, cur_loss])
                duration = time.time() - start_time
                # 每隔一段時間數據當前的訓練進度,並統計訓練速度。
                if step != 0 and step % 10 == 0:
                    # 計算使用過的訓練數據個數。
                    num_examples_per_step = BATCH_SIZE * N_GPU
                    examples_per_sec = num_examples_per_step / duration
                    sec_per_batch = duration / N_GPU

                    # 輸出訓練信息。
                    format_str = ('%s: step %d, loss = %.2f (%.1f examples/sec; %.3f sec/batch)')
                    print(format_str % (datetime.now(), step, loss_value, examples_per_sec, sec_per_batch))

                    # 經過TensorBoard可視化訓練過程。
                    summary = sess.run(summary_op)
                    summary_writer.add_summary(summary, step)
                # 每隔一段時間保存當前的模型。
                if step % 1000 == 0 or (step + 1) == TRAINING_STEPS:
                    checkpoint_path = os.path.join(MODEL_SAVE_PATH, MODEL_NAME)
                    saver.save(sess, checkpoint_path, global_step=step)

            coord.request_stop()
            coord.join(threads)


if __name__ == '__main__':
    tf.app.run()

分佈式TensorFlow

經過多GPU並行的方式能夠達到很好的加速效果。然而一臺機器上可以安裝的GPU有限,要進一步提高深度學習模型的訓練速度,就須要將TensorFlow分佈式運行在多臺機器上。

分佈式TensorFlow原理

下面代碼展現瞭如何建立一個最簡單的TensorFlow集羣

import tensorflow as tf

c = tf.constant('Hello,distributed TensorFlow')
# 建立一個本地TensorFlow集羣
server = tf.train.Server.create_local_server()
# 在集羣上建立一個會話
sess = tf.Session(server.target)
# 輸出
print(sess.run(c))

首先經過create_local_server函數在本地創建了一個只有一臺機器的TensorFlow集羣。而後在該集羣上生成了一個會話,並經過生成的會話將運算運行在建立的TensorFlow集羣上。雖然只是一個單機集羣,但它大體反應了TensorFlow集羣的工做流程。TensorFlow集羣經過一系列任務(tasks)來執行計算圖中的運算。

通常來講,不一樣任務跑在不一樣機器上。最主要的例外是使用GPU時,不一樣任務能夠使用同一臺機器上的不一樣GPU。TensorFlow集羣中的任務也會被聚合成工做(jobs),每一個工做能夠包含一個或多個任務。好比在訓練深度學習模型時,一臺運行反向傳播的機器是一個任務,而全部運行反向傳播機器的集合是一種工做。

上面的樣例代碼是隻有一個任務的集羣。當一個TensorFlow集羣由多個任務時,須要使用tf.train.ClusterSpec來指定運行每個任務的機器。下面代碼展現了本地運行有兩個任務的TensorFlow集羣。

第一個任務

import tensorflow as tf

c = tf.constant('Hello,from server1!')
# 生成一個有兩個任務的集羣,一個任務跑在本地2222端口,另一個跑在本地2223端口
cluster = tf.train.ClusterSpec({'local': ['localhost:2222', 'localhost:2223']})
# 經過上面生成的集羣配置生成server,並經過job_name和task_index指定當前所啓動的任務
# 由於該任務是第一個任務,因此task_index爲0
server = tf.train.Server(cluster, job_name='local', task_index=0)
# 經過server.target生成會話來使用TensorFlow集羣中的資源
# 經過設置log_device_placement能夠看到執行每個操做的任務
sess = tf.Session(server.target, config=tf.ConfigProto(log_device_placement=True))
print(sess.run(c))
server.join()

第二個任務

import tensorflow as tf

c = tf.constant('Hello,from server2!')
# 集羣中的每個任務都須要採用相同配置
cluster = tf.train.ClusterSpec({'local': ['localhost:2222', 'localhost:2223']})
# 指定task_index爲1,因此這個程序將在localhost:2223啓動服務
server = tf.train.Server(cluster, job_name='local', task_index=1)
# 經過server.target生成會話來使用TensorFlow集羣中的資源
# 經過設置log_device_placement能夠看到執行每個操做的任務
sess = tf.Session(server.target, config=tf.ConfigProto(log_device_placement=True))
print(sess.run(c))
server.join()

當只啓動第一個任務時,程序會停下來等待第二個任務啓動,當第二個任務啓動後,會輸出結果

和使用多個GPU相似,TensorFlow支持經過tf.device來指定操做運行在哪一個任務上。

with tf.device('/job:local/task:1'):
    c = tf.constant('Hello from server2!')

上面的樣例只定義了一個工做local,但通常在訓練深度學習模型時,會定義兩個工做。

一個專門負責存儲、獲取以及更新變量的取值,這個工做所包含的任務統稱爲參數服務器(parameter server,ps)

另一個工做負責運行反向傳播算法來獲取參數梯度,這個工做所包含的任務統稱爲計算服務器(worker)

tf.train.ClusterSpec({
    'worker':[
        'tf-worker0:2222',
        'tf-worker1:2222',
        'tf-worker2:2222'
    ],
    'ps':[
        'tf-ps0:2222',
        'tf-ps1:2222'
    ]
})

上面是比較常見的用於訓練深度學習模型的TensorFlow集羣配置方法

使用分佈式TensorFlow訓練深度學習模型通常有兩種方式。一種方式叫作計算圖內分佈式(in-graph replication)。使用這種分佈式訓練方式時,全部的任務都會使用一個TensorFlow計算圖中的變量(也就是深度學習模型中的參數),而只是將計算部分發布到不一樣的計算服務器上。多GPU樣例程序將計算複製了多份,每一份放到一個GPU上進行運算。但不一樣的GPU使用的參數都是在一個TensorFlow計算圖中的。由於參數都是存在同一個計算圖中,因此同步更新參數比較容易控制。然而由於計算圖內分佈式須要一箇中心節點來生成這個計算圖並分配計算任務,因此當數據量太大時,這個中心節點容易形成性能瓶頸。

另一個分佈式TensorFlow訓練深度學習的方法叫計算圖之間分佈式(between-graph replication)使用這種分佈式時,在每一個計算服務器上都會建立一個獨立的TensorFlow計算圖,但不一樣計算圖中的相同參數須要以一種固定的方式放在同一個參數服務器上。TensorFlow提供了tf.train.replica_device_setter函數來幫助完成這一過程。由於每一個計算服務器的TensorFlow計算圖是獨立的,因此這種方式的並行度要更高。但在計算圖之間分佈式下進行參數的同步更新比較困難。爲了解決這個問題TensorFlow提供了tf.trainSyncReplicasOptimizer函數來幫助實現參數的同步更新。這讓計算圖之間的分佈式方式被更加普遍地使用。

分佈式TensorFlow模型訓練

本節給出兩個樣例程序分別實現使用計算圖之間分佈式完成分佈式深度學習模型訓練的異步更新和同步更新。第一部分將給出使用計算圖之間分佈式實現異步更新的TensorFlow程序。這一部分也會給出具體的命令行將該程序分佈式的運行在一個參數服務器和兩個計算服務器上,並經過TensorBoard可視化在第一個計算服務器上的TensorFlow計算圖。第二部分將給出計算圖之間分佈式實現同步參數更新的TensorFlow程序。

異步模型樣例程序

下面代碼實現了異步模式的分佈式神經網絡訓練過程

# coding=utf-8
import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

import mnist_inference

# 配置神經網絡的參數。
BATCH_SIZE = 100
LEARNING_RATE_BASE = 0.01
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 1000
MOVING_AVERAGE_DECAY = 0.99

# 模型保存的路徑和文件名。
MODEL_SAVE_PATH = "logs/log_async"
DATA_PATH = "../../datasets/MNIST_data"

FLAGS = tf.app.flags.FLAGS

# 指定當前程序是參數服務器仍是計算服務器。
tf.app.flags.DEFINE_string('job_name', 'worker', ' "ps" or "worker" ')
# 指定集羣中的參數服務器地址。
tf.app.flags.DEFINE_string(
    'ps_hosts', ' tf-ps0:2222,tf-ps1:1111',
    'Comma-separated list of hostname:port for the parameter server jobs. e.g. "tf-ps0:2222,tf-ps1:1111" ')
# 指定集羣中的計算服務器地址。
tf.app.flags.DEFINE_string(
    'worker_hosts', ' tf-worker0:2222,tf-worker1:1111',
    'Comma-separated list of hostname:port for the worker jobs. e.g. "tf-worker0:2222,tf-worker1:1111" ')
# 指定當前程序的任務ID。
tf.app.flags.DEFINE_integer('task_id', 0, 'Task ID of the worker/replica running the training.')

# 定義TensorFlow的計算圖,並返回每一輪迭代時須要運行的操做。
def build_model(x, y_, is_chief):
    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    # 經過和5.5節給出的mnist_inference.py代碼計算神經網絡前向傳播的結果。
    y = mnist_inference.inference(x, regularizer)
    global_step = tf.Variable(0, trainable=False)

    # 計算損失函數並定義反向傳播過程。
    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE, global_step, 60000 / BATCH_SIZE, LEARNING_RATE_DECAY)
    train_op = tf.train.GradientDescentOptimizer(learning_rate).minimize(loss, global_step=global_step)

    # 定義每一輪迭代須要運行的操做。
    if is_chief:
        # 計算變量的滑動平均值。   
        variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
        variables_averages_op = variable_averages.apply(tf.trainable_variables())
        with tf.control_dependencies([variables_averages_op, train_op]):
            train_op = tf.no_op()
    return global_step, loss, train_op


def main(argv=None):
    # 解析flags並經過tf.train.ClusterSpec配置TensorFlow集羣。
    ps_hosts = FLAGS.ps_hosts.split(',')
    worker_hosts = FLAGS.worker_hosts.split(',')
    cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})
    # 經過tf.train.ClusterSpec以及當前任務建立tf.train.Server。
    server = tf.train.Server(cluster, job_name = FLAGS.job_name, task_index=FLAGS.task_id)

    # 參數服務器只須要管理TensorFlow中的變量,不須要執行訓練的過程。server.join()會
    # 一致停在這條語句上。
    if FLAGS.job_name == 'ps':
        with tf.device("/cpu:0"):
            server.join()

    # 定義計算服務器須要運行的操做。
    is_chief = (FLAGS.task_id == 0)
    mnist = input_data.read_data_sets(DATA_PATH, one_hot=True)

    device_setter = tf.train.replica_device_setter(worker_device="/job:worker/task:%d" % FLAGS.task_id, cluster=cluster)
    with tf.device(device_setter):

        # 定義輸入並獲得每一輪迭代須要運行的操做。
        x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')
        y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
        global_step, loss, train_op = build_model(x, y_, is_chief)

        # 定義用於保存模型的saver。
        saver = tf.train.Saver()
        # 定義日誌輸出操做。
        summary_op = tf.summary.merge_all()
        # 定義變量初始化操做。
        init_op = tf.global_variables_initializer()
        # 經過tf.train.Supervisor管理訓練深度學習模型時的通用功能。
        sv = tf.train.Supervisor(
            is_chief=is_chief,
            logdir=MODEL_SAVE_PATH,
            init_op=init_op,
            summary_op=summary_op,
            saver=saver,
            global_step=global_step,
            save_model_secs=60,
            save_summaries_secs=60)

        sess_config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=False)
        # 經過tf.train.Supervisor生成會話。
        sess = sv.prepare_or_wait_for_session(server.target, config=sess_config)

        step = 0
        start_time = time.time()

        # 執行迭代過程。
        while not sv.should_stop():
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            _, loss_value, global_step_value = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})
            if global_step_value >= TRAINING_STEPS: break

           # 每隔一段時間輸出訓練信息。
            if step > 0 and step % 100 == 0:
                duration = time.time() - start_time
                sec_per_batch = duration / global_step_value
                format_str = "After %d training steps (%d global steps), loss on training batch is %g.  (%.3f sec/batch)"
                print format_str % (step, global_step_value, loss_value, sec_per_batch)
            step += 1
    sv.stop()

if __name__ == "__main__":
    tf.app.run()

同步模式樣例程序

和異步模式相似

# coding=utf-8
import time
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data

import mnist_inference

# 配置神經網絡的參數。
BATCH_SIZE = 100 
LEARNING_RATE_BASE = 0.8
LEARNING_RATE_DECAY = 0.99
REGULARAZTION_RATE = 0.0001
TRAINING_STEPS = 10000
MOVING_AVERAGE_DECAY = 0.99 
MODEL_SAVE_PATH = "logs/log_sync"
DATA_PATH = "../../datasets/MNIST_data"


# 和異步模式相似的設置flags。
FLAGS = tf.app.flags.FLAGS

tf.app.flags.DEFINE_string('job_name', 'worker', ' "ps" or "worker" ')
tf.app.flags.DEFINE_string(
    'ps_hosts', ' tf-ps0:2222,tf-ps1:1111',
    'Comma-separated list of hostname:port for the parameter server jobs. e.g. "tf-ps0:2222,tf-ps1:1111" ')
tf.app.flags.DEFINE_string(
    'worker_hosts', ' tf-worker0:2222,tf-worker1:1111',
'Comma-separated list of hostname:port for the worker jobs. e.g. "tf-worker0:2222,tf-worker1:1111" ')
tf.app.flags.DEFINE_integer('task_id', 0, 'Task ID of the worker/replica running the training.')

# 和異步模式相似的定義TensorFlow的計算圖。惟一的區別在於使用
# tf.train.SyncReplicasOptimizer函數處理同步更新。
def build_model(x, y_, n_workers, is_chief):
    regularizer = tf.contrib.layers.l2_regularizer(REGULARAZTION_RATE)
    y = mnist_inference.inference(x, regularizer)
    global_step = tf.Variable(0, trainable=False)

    variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())

    cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=y, labels=tf.argmax(y_, 1))
    cross_entropy_mean = tf.reduce_mean(cross_entropy)
    loss = cross_entropy_mean + tf.add_n(tf.get_collection('losses'))
    learning_rate = tf.train.exponential_decay(
        LEARNING_RATE_BASE, global_step, 60000 / BATCH_SIZE, LEARNING_RATE_DECAY)
   
    # 經過tf.train.SyncReplicasOptimizer函數實現同步更新。
    opt = tf.train.SyncReplicasOptimizer(
        tf.train.GradientDescentOptimizer(learning_rate),
        replicas_to_aggregate=n_workers,
        total_num_replicas=n_workers)

    train_op = opt.minimize(loss, global_step=global_step)     
    if is_chief:
        variable_averages = tf.train.ExponentialMovingAverage(MOVING_AVERAGE_DECAY, global_step)
        variables_averages_op = variable_averages.apply(tf.trainable_variables())
        with tf.control_dependencies([variables_averages_op, train_op]):
            train_op = tf.no_op()

    return global_step, loss, train_op, opt

def main(argv=None): 
    # 和異步模式相似的建立TensorFlow集羣。
    ps_hosts = FLAGS.ps_hosts.split(',')
    worker_hosts = FLAGS.worker_hosts.split(',')
    print ('PS hosts are: %s' % ps_hosts)
    print ('Worker hosts are: %s' % worker_hosts)
    n_workers = len(worker_hosts)

    cluster = tf.train.ClusterSpec({"ps": ps_hosts, "worker": worker_hosts})
    server = tf.train.Server(
        cluster, job_name = FLAGS.job_name, task_index=FLAGS.task_id)

    if FLAGS.job_name == 'ps':
        with tf.device("/cpu:0"):
            server.join()

    is_chief = (FLAGS.task_id == 0)
    mnist = input_data.read_data_sets(DATA_PATH, one_hot=True)
   
    with tf.device(tf.train.replica_device_setter(
            worker_device="/job:worker/task:%d" % FLAGS.task_id, cluster=cluster)):
        x = tf.placeholder(tf.float32, [None, mnist_inference.INPUT_NODE], name='x-input')
        y_ = tf.placeholder(tf.float32, [None, mnist_inference.OUTPUT_NODE], name='y-input')
        global_step, loss, train_op, opt = build_model(x, y_, n_workers, is_chief)
        # 和異步模式相似的聲明一些輔助函數。
        saver = tf.train.Saver()
        summary_op = tf.summary.merge_all()
        init_op = tf.global_variables_initializer()

        # 在同步模式下,主計算服務器須要協調不一樣計算服務器計算獲得的參數梯度並最終更新參數。
        # 這須要主計算服務器完成一些額外的初始化工做。
        if is_chief:
            # 獲取協調不一樣計算服務器的隊列。在更新參數以前,主計算服務器須要先啓動這些隊列。
            chief_queue_runner = opt.get_chief_queue_runner()
            # 初始化同步更新隊列的操做。
            init_tokens_op = opt.get_init_tokens_op(0)
     
        # 和異步模式相似的聲明tf.train.Supervisor。
        sv = tf.train.Supervisor(is_chief=is_chief,
                                logdir=MODEL_SAVE_PATH,
                                init_op=init_op,
                                summary_op=summary_op,
                                saver = saver,
                                global_step=global_step,
                                save_model_secs=60,
                                save_summaries_secs=60)
        sess_config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=False)
        sess = sv.prepare_or_wait_for_session(server.target, config=sess_config)        

        # 在主計算服務器上啓動協調同步更新的隊列並執行初始化操做。
        if is_chief:
            sv.start_queue_runners(sess, [chief_queue_runner])
            sess.run(init_tokens_op)
     
        # 和異步模式相似的運行迭代的訓練過程。
        step = 0
        start_time = time.time()
        while not sv.should_stop():
            xs, ys = mnist.train.next_batch(BATCH_SIZE)
            _, loss_value, global_step_value = sess.run([train_op, loss, global_step], feed_dict={x: xs, y_: ys})
            if global_step_value >= TRAINING_STEPS: break

            if step > 0 and step % 100 == 0:
                duration = time.time() - start_time
                sec_per_batch = duration / (global_step_value * n_workers)
                format_str = "After %d training steps (%d global steps), loss on training batch is %g.  (%.3f sec/batch)"
                print format_str % (step, global_step_value, loss_value, sec_per_batch)
            step += 1
    sv.stop()
       
if __name__ == "__main__":
    tf.app.run()

和異步模式相似,在不一樣機器上運行以上代碼就能夠啓動TensorFlow集羣。但和異步模式不一樣的是,當第一臺計算服務器初始化完畢以後,並不能直接更新參數。由於在程序中要求每一次參數更新都須要來自兩個計算服務器的梯度。

使用Caicloud運行分佈式TensorFlow 

 

相關文章
相關標籤/搜索