信息技術是人類歷史上的第三次工業革命,計算機、互聯網、智能家居等技術的普及
極大地方便了人們的平常生活。經過編程的方式,人類能夠將提早設計好的交互邏輯重複
且快速地執行,從而將人類從簡單枯燥的重複勞動任務中解脫出來。可是對於須要較高智
能的任務,如人臉識別,聊天機器人,自動駕駛等任務,很難設計明確的邏輯規則,傳統
的編程方式顯得力不從心,而人工智能技術是有望解決此問題的關鍵技術。html
隨着深度學習算法的崛起,人工智能在部分任務上取得了類人甚至超人的水平,如圍
棋上 AlphaGo 智能程序已經擊敗人類最強圍棋專家柯潔,在 Dota2 遊戲上 OpenAI Five 智
能程序擊敗冠軍隊伍 OG,同時人臉識別,智能語音,機器翻譯等一項項實用的技術已經
進入到人們的平常生活中。如今咱們的生活到處被人工智能環繞,儘管目前達到的智能水
平離通用人工智能(Artificial General Intelligence,簡稱 AGI)還有一段距離,咱們仍堅決相
信人工智能時代即未來臨。
接下來咱們將介紹人工智能,機器學習,深度學習的概念以及它們之間的聯繫與區
別。python
人工智能是指讓機器得到像人類同樣的智能機制的技術,這一律念最先出如今 1956 年
召開的達特茅斯會議上。這是一項極具挑戰性的任務,人類目前尚沒法對人腦的工做機制
有全面科學的認知,但願能製造達到人腦水平的智能機器無疑是難於上青天。即便如此,
在某個方面呈現出相似、接近甚至超越人類智能水平的機器被證實是可行的。web
怎麼實現人工智能是一個很是廣袤的問題。人工智能的發展主要經歷過 3 種階段,每
個階段都表明了人類從不一樣的角度嘗試實現人工智能的探索足跡。最先期人類試圖經過總
結、概括出一些邏輯規則,並將邏輯規則以計算機程序的方式來開發智能系統。可是這種
顯式的規則每每過於簡單,很難表達複雜、抽象的規則。這一階段被稱爲推理期。算法
1970 年代,科學家們嘗試經過知識庫+推理的方式解決人工智能,經過構建龐大複雜
的專家系統來模擬人類專家的智能水平。這些明確指定規則的方式存在一個最大的難題,
就是不少複雜,抽象的概念沒法用具體的代碼實現。好比人類對圖片的識別,對語言的理
解過程,根本沒法經過既定規則模擬。爲了解決這類問題,一門經過讓機器自動從數據中
學習規則的研究學科誕生了,稱爲機器學習,並在 1980 年代成爲人工智能中的熱門學科。編程
在機器學習中,有一門經過神經網絡來學習複雜、抽象邏輯的方向,稱爲神經網絡。
神經網絡方向的研究經歷了 2 起 2 落,並從 2012 年開始,因爲效果極爲顯著,應用深層神
經網絡技術在計算機視覺、天然語言處理、機器人等領域取得了重大突破,部分任務上甚
至超越了人類智能水平,開啓了以深層神經網絡爲表明的人工智能的第 3 次復興。深層神
經網絡有了一個新名字,叫作深度學習,通常來說,神經網絡和深度學習的本質區別並不
大,深度學習特指基於深層神經網絡實現的模型或算法。人工智能,機器學習,神經網
絡,深度學習的相互之間的關係如圖所示。
數組
機器學習能夠分爲有監督學習(Supervised Learning)、無監督學習(Unsupervised
Learning)和強化學習(Reinforcement Learning),如圖 所示:
有監督學習 有監督學習的數據集包含了樣本𝒙與樣本的標籤𝒚,算法模型須要學習到映射
𝑓𝜃: 𝒙 → 𝒚,其中𝑓𝜃表明模型函數,𝜃爲模型的參數。在訓練時,經過計算模型的預測值
𝑓𝜃(𝒙)與真實標籤𝒚之間的偏差來優化網絡參數𝜃,使得網絡下一次可以預測更精準。常見的
有監督學習有線性迴歸,邏輯迴歸,支持向量機,隨機森林等。瀏覽器
無監督學習 收集帶標籤的數據每每代價較爲昂貴,對於只有樣本𝒙的數據集,算法須要自
行發現數據的模態,這種方式叫作無監督學習。無監督學習中有一類算法將自身做爲監督
信號,即模型須要學習的映射爲𝑓𝜃: 𝒙 → 𝒙,稱爲自監督學習(Self-supervised Learning)。在
訓練時,經過計算模型的預測值𝑓𝜃(𝒙)與自身𝒙之間的偏差來優化網絡參數𝜃。常見的無監督
學習算法有自編碼器,生成對抗網絡等。緩存
強化學習 也稱爲加強學習,經過與環境進行交互來學習解決問題的策略的一類算法。與有監督、無監督學習不一樣,強化學習問題並無明確的「正確的」動做監督信號,算法須要與環境進行交互,獲取環境反饋的滯後的獎勵信號,所以並不能經過計算動做與「正確動做」之間的偏差來優化網絡。常見的強化學習算法有 DQN,PPO 等。安全
神經網絡算法是一類經過神經網絡從數據中學習的算法,它仍然屬於機器學習的範疇。受限於計算能力和數據量,早期的神經網絡層數較淺,通常在 1~4 層左右,網絡表達能力有限。隨着計算能力的提高和大數據時代的到來,高度並行化的 GPU 和海量數據讓大規模神經網絡的訓練成爲可能。網絡
2006 年,Geoffrey Hinton(傑弗裏 希爾頓) 首次提出深度學習的概念,2012 年,8 層的深層神經網絡 AlexNet 發佈,並在圖片識別競賽中取得了巨大的性能提高,此後數十層,數百層,甚至上千層的神經網絡模型相繼提出,展示出深層神經網絡強大的學習能力。咱們通常將利用深層神經網絡實現的算法或模型稱做深度學習,本質上神經網絡和深度學習是相同的。
咱們來比較一下深度學習算法與其餘算法,如圖 1.3 所示。基於規則的系統通常會編寫顯示的規則邏輯,這些邏輯通常是針對特定的任務設計的,並不適合其餘任務。傳統的機器學習算法通常會人爲設計具備必定通用性的特徵檢測方法,如 SIFT,HOG 特徵,這些特徵可以適合某一類的任務,具備必定的通用性,可是如何設計特徵方法,特徵方法的好壞是問題的關鍵。神經網絡的出現,使得人爲設計特徵這一部分工做能夠經過神經網絡讓機器自動學習,不須要人類干預。可是淺層的神經網絡的特徵提取能力較爲有限,而深層的神經網絡擅長提取深層,抽象的高層特徵,所以具備更好的性能表現。
咱們將神經網絡的發展歷程大體分爲淺層神經網絡階段和深度學習階段,以 2006 年爲分割點。2006 年之前,深度學習以神經網絡和鏈接主義名義發展,歷經了 2 次興盛和 2 次寒冬;在 2006 年,Geoffrey Hinton 首次將深層神經網絡命名爲深度學習,開啓了深度學習
的第 3 次復興之路。
1943 年,心理學家 Warren McCulloch 和邏輯學家 Walter Pitts 根據生物神經元(Neuron)結構,提出了最先的神經元數學模型,稱爲 MP 神經元模型。該模型的輸出𝑓(𝒙) =ℎ(𝑔(𝒙)),其中𝑔(𝒙) = ∑𝑖 𝑥𝑖, 𝑥𝑖 ∈ {0,1},模型經過𝑔(𝒙)的值來完成輸出值的預測,若是𝑔(𝒙) ≥ 𝟎,輸出爲 1;若是𝑔(𝒙) < 𝟎,輸出爲 0。能夠看到,MP 神經元模型並無學習能力,只能完成固定邏輯的斷定。
1958 年,美國心理學家 Frank Rosenblatt 提出了第一個能夠自動學習權重的神經元模型,稱爲感知機(Perceptron),如圖 1.5 所示,輸出值與真實值之間的偏差用於調整神經元的權重參數{
)。感知機隨後基於「Mark 1 感知機」硬件實現,如圖 1.6 、1.7 所示,輸入爲 400 個單元的圖像傳感器,輸出爲 8 個節點端子,能夠成功識別一些英文字母。咱們通常認爲 1943 年~1969 年爲人工智能發展的第一次興盛期。
1969 年,美國科學家 Marvin Minsky 等人在出版的《Perceptrons》一書中指出了感知機等線性模型的主要缺陷,即沒法處理簡單的異或 XOR 等線性不可分問題。這直接致使了以感知機爲表明的神經網絡相關研究進入了低谷期,通常認爲 1969 年~1982 年爲人工智能發展的第一次寒冬。
儘管處於 AI 發展的低谷期,仍然有不少意義重大的研究相繼發表,這其中最重要的成果就是反向傳播算法(Backpropagation,簡稱 BP 算法)的提出,它依舊是現代深度學習的核心理論基礎。實際上,反向傳播的數學思想早在 1960 年代就已經被推導出了,可是並無應用在神經網絡上。直到 1974 年,美國科學家 Paul Werbos 在他的博士論文中第一次提出能夠將 BP 算法應用到神經網絡上,遺憾的是,這一成果並無得到足夠重視。直至1986 年,David Rumelhart 等人在 Nature 上發表了經過 BP 算法來表徵學習的論文,BP 算法纔得到了普遍的關注。
1982 年 John Hopfild 的循環鏈接的 Hopfield 網絡的提出,開啓了 1982 年~1995 年的第二次人工智能復興的大潮,這段期間相繼提出了卷積神經網絡,循環神經網絡,反向傳播算法等算法模型。1986 年,David Rumelhart 和 Geoffrey Hinton 等人將 BP 算法應用在多層感知機上;1989 年Yann LeCun 等人將 BP 算法應用在手寫數字圖片識別上,取得了巨大成功,這套系統成功商用在郵政編碼識別、銀行支票識別等系統上;1997 年,應用最爲普遍的循環神經網絡變種之一 LSTM 被 Jürgen Schmidhuber 提出;同年雙向循環神經網絡也被提出。
遺憾的是,神經網絡的研究隨着以支持向量機(Support Vector Machine,簡稱 SVM)爲
表明的傳統機器學習算法興起而逐漸進入低谷,稱爲人工智能的第二次寒冬。支持向量機
擁有嚴格的理論基礎,須要的樣本數量較少,同時也具備良好的泛化能力,相比之下,神
經網絡理論基礎欠缺,可解釋性差,很難訓練深層網絡,性能也通常。圖 1.8 畫出了 1943
年~2006 年之間的重大時間節點。
2006 年,Geoffrey Hinton 等人發現經過逐層預訓練的方式能夠較好地訓練多層神經網絡,並在 MNIST 手寫數字圖片數據集上取得了優於 SVM 的錯誤率,開啓了第 3 次人工智能的復興。在論文中,Geoffrey Hinton 首次提出了 Deep Learning 的概念,這也是(深層)神經網絡被叫作深度學習的由來。2011 年,Xavier Glorot 提出了線性整流單元(Rectified Linear Unit, ReLU)激活函數,這是如今使用最爲普遍的激活函數之一。2012 年,Alex Krizhevsky 提出了 8 層的深層神經網絡 AlexNet,它採用了 ReLU 激活函數,並使用 Dropout 技術防止過擬合,同時拋棄了逐層預訓練的方式,直接在 2 塊 GTX580 GPU 上訓練網絡。AlexNet 在 ILSVRC-2012 圖片識別比賽中得到了第一名,比第二名在 Top-5 錯誤率上下降了驚人的 10.9%。
自 AlexNet 模型提出後,各類各樣的算法模型相繼被髮表,其中有 VGG 系列,GoogleNet,ResNet 系列,DenseNet 系列等等,其中 ResNet 系列網絡實現簡單,效果顯著,很快將網絡的層數提高至數百層,甚至上千層,同時保持性能不變甚至更好。
除了有監督學習領域取得了驚人的成果,在無監督學習和強化學習領域也取得了巨大的成績。2014 年,Ian Goodfellow 提出了生成對抗網絡,經過對抗訓練的方式學習樣本的真實分佈,從而生成逼近度較高的圖片。此後,大量的生成對抗網絡模型被提出,最新的圖片生成效果已經達到了肉眼難辨真僞的逼真度。2016 年,DeepMind 公司應用深度神經網絡到強化學習領域,提出了 DQN 算法,在 Atari 遊戲平臺中的 49 個遊戲取得了人類至關甚至超越人類的水平;在圍棋領域,DeepMind 提出的 AlphaGo 和 AlphaGo Zero 智能程序相繼戰勝人類頂級圍棋專家李世石、柯潔等;在多智能體協做的 Dota2 遊戲平臺,OpenAI 開發的 OpenAI Five 智能程序在受限遊戲環境中戰勝了 TI8 冠軍 OG 隊,展示出了大量專業級的高層智能的操做。圖 1.9 列出了 2006 年~2019 年之間重大的時間節點。
與傳統的機器學習算法、淺層神經網絡相比,現代的深度學習算法一般具備以下特色。
早期的機器學習算法比較簡單,容易快速訓練,須要的數據集規模也比較小,如 1936年由英國統計學家 Ronald Fisher 收集整理的鳶尾花卉數據集 Iris 共包含 3 個類別花卉,每一個類別 50 個樣本。隨着計算機技術的發展,設計的算法愈來愈複雜,對數據量的需求也隨之增大。1998 年由 Yann LeCun 收集整理的 MNIST 手寫數字圖片數據集共包含 0~9 共 10類數字,每一個類別多達 7000 張圖片。隨着神經網絡的興起,尤爲是深度學習,網絡層數較深,模型的參數量成百上千萬個,爲了防止過擬合,須要的數據集的規模一般也是巨大的。現代社交媒體的流行也讓收集海量數據成爲可能,如 2010 年的ImageNet 數據集收錄了 14,197,122 張圖片,整個數據集的壓縮文件大小就有 154GB。
儘管深度學習對數據集需求較高,收集數據,尤爲是收集帶標籤的數據,每每是代價昂貴的。數據集的造成一般須要手動採集、爬取原始數據,並清洗掉無效樣本,再經過人類智能去標註數據樣本,所以不可避免地引入主觀誤差和隨機偏差。所以研究數據量需求較少的算法模型是很是有用的一個方向。
計算能力的提高是第三次人工智能復興的一個重要因素。實際上,目前深度學習的基礎理論在 1980 年代就已經被提出,但直到 2012 年基於 2 塊 GTX580 GPU 訓練的 AlexNet發佈後,深度學習的真正潛力才得以發揮。傳統的機器學習算法並不像神經網絡這樣對數據量和計算能力有嚴苛的要求,一般在 CPU 上串行訓練便可獲得滿意結果。可是深度學習很是依賴並行加速計算設備,目前的大部分神經網絡均使用 NVIDIA GPU 和 Google TPU或其餘神經網絡並行加速芯片訓練模型參數。如圍棋程序 AlphaGo Zero 在 64 塊 GPU 上從零開始訓練了 40 天才得以超越全部的 AlphaGo 歷史版本;自動網絡結構搜索算法使用了800 塊 GPU 同時訓練才能優化出較好的網絡結構。
目前普通消費者可以使用的深度學習加速硬件設備主要來自 NVIDIA 的 GPU 顯卡,圖 1.12 例舉了從 2008 年到 2017 年 NVIDIA GPU 和 x86 CPU 的每秒 10 億次的浮點運算數(GFLOPS)的指標變換曲線。能夠看到,x86 CPU 的曲線變化相對緩慢,而 NVIDIA GPU的浮點計算能力指數式增加,這主要是由日益增加的遊戲計算量和深度學習計算量等驅動的。
早期的感知機模型和多層神經網絡層數只有 1 層或者 2~4 層,網絡參數量也在數萬左右。隨着深度學習的興起和計算能力的提高,AlexNet(8 層),VGG16(16 層),GoogLeNet(22 層),ResNet50(50 層),DenseNet121(121 層)等模型相繼被提出,同時輸入圖片的大小也從 28x28 逐漸增大,變成 224x224,299x299 等,這些使得網絡的總參數量可達到千萬級別,如圖 1.13 所示。
網絡規模的增大,使得神經網絡的容量相應增大,從而可以學習到複雜的數據模態,模型的性能也會隨之提高;另外一方面,網絡規模的增大,意味着更容易出現過擬合現象,訓練須要的數據集和計算代價也會變大。
在過去,爲了提高某項任務上的算法性能,每每須要手動設計相應的特徵和先驗設定,以幫助算法更好地收斂到最優解。這類特徵或者先驗每每是與具體任務場景強相關的,一旦場景發生了變更,這些依靠人工設計的特徵或先驗沒法自適應新場景,每每須要從新設計算法模型,模型的通用性不強。
設計一種像人腦同樣能夠自動學習、自我調整的通用智能機制一直是人類的共同願景。深度學習從目前來看,是最接近通用智能的算法之一。在計算機視覺領域,過去須要針對具體的任務設計徵、添加先驗的作法,已經被深度學習徹底拋棄了,目前在圖片識別、目標檢測、語義分割等方向,幾乎全是基於深度學習端到端地訓練,得到的模型性能好,適應性強;在 Atria 遊戲平臺上,DeepMind 設計的 DQN 算法模型能夠在相同的算法、模型結構和超參數的設定下,在 49 個遊戲上得到人類至關的遊戲水平,呈現出必定程度的通用智能。圖 1.14 是 DQN 算法的網絡結構,它並非針對於某個遊戲而設計的,而是能夠運行在全部的 Atria 遊戲平臺上的 49 個遊戲。
深度學習算法已經普遍應用到人們生活的角角落落,例如手機中的語音助手,汽車上的智能輔助駕駛,人臉支付等等。咱們將從計算機視覺、天然語言處理和強化學習 3 個領域入手,爲你們介紹深度學習的一些主流應用。
圖片識別(Image Classification) 是常見的分類問題。神經網絡的輸入爲圖片數據,輸出值爲當前樣本屬於每一個類別的機率,一般選取機率值最大的類別做爲樣本的預測類別。圖片識別是最先成功應用深度學習的任務之一,經典的網絡模型有 VGG 系列、Inception 系列、ResNet 系列等。
目標檢測(Object Detection) 是指經過算法自動檢測出圖片中常見物體的大體位置,一般用邊界框(Bounding box)表示,並分類出邊界框中物體的類別信息,如圖 1.15 所示。常見的目標檢測算法有 RCNN,Fast RCNN,Faster RCNN,Mask RCNN,SSD,YOLO 系列等。
語義分割(Semantic Segmentation) 是經過算法自動分割並識別出圖片中的內容,能夠將語義分割理解爲每一個像素點的分類問題,分析每一個像素點屬於物體的類別,如圖 1.16 所示。常見的語義分割模型有 FCN,U-net,SegNet,DeepLab 系列等。
視頻理解(Video Understanding) 隨着深度學習在 2D 圖片的相關任務上取得較好的效果,具備時間維度信息的 3D 視頻理解任務受到愈來愈多的關注。常見的視頻理解任務有視頻分類,行爲檢測,視頻主體抽取等。經常使用的模型有 C3D,TSN,DOVF,TS_LSTM等。
圖片生成(Image Generation) 經過學習真實圖片的分佈,並從學習到的分佈中採樣而得到逼真度較高的生成圖片。目前主要的生成模型有 VAE 系列,GAN 系列等。其中 GAN 系列算法近年來取得了巨大的進展,最新 GAN 模型產生的圖片樣本達到了肉眼難辨真僞的效果,如圖 1.17 爲 GAN 模型的生成圖片。
除了上述應用,深度學習還在其餘方向上取得了不俗的效果,好比藝術風格遷移(圖1.18),超分辨率,圖片去燥/去霧,灰度圖片着色等等一系列很是實用酷炫的任務,限於篇幅,再也不敖述。
機器翻譯(Machine Translation) 過去的機器翻譯算法一般是基於統計機器翻譯模型,這也是 2016 年前 Google 翻譯系統採用的技術。2016 年 11 月,Google 基於 Seq2Seq 模型上線了 Google 神經機器翻譯系統(GNMT),首次實現了源語言到目標語言的直譯技術,在多項任務上實現了 50~90%的效果提高。經常使用的機器翻譯模型有 Seq2Seq,BERT,GPT,GPT-2 等,其中 OpenAI 提出的 GPT-2 模型參數量高達 15 億個,甚至發佈之初以技術安全考慮爲由拒絕開源 GPT-2 模型。
聊天機器人(Chatbot) 聊天機器人也是天然語言處理的一項主流任務,經過機器自動與人類對話,對於人類的簡單訴求提供滿意的自動回覆,提升客戶的服務效率和服務質量。常應用在諮詢系統、娛樂系統,智能家居等中。
虛擬遊戲 相對於真實環境,虛擬遊戲平臺既能夠訓練、測試強化學習算法,有能夠避免無關干擾,同時也能將實驗代價降到最低。目前經常使用的虛擬遊戲平臺有 OpenAI Gym,OpenAI Universe,OpenAI Roboschool,DeepMind OpenSpiel,MuJoCo 等,經常使用的強化學習算法有 DQN,A3C,A2C,PPO 等。在圍棋領域,DeepMind AlaphGo 程序已經超越人類圍棋專家;在 Dota2 和星際爭霸遊戲上,OpenAI 和 DeepMind 開發的智能程序也在限制規則下打敗了職業隊伍。
機器人(Robotics) 在真實環境中,機器人的控制也取得了必定的進展。如 UC Berkeley在機器人的 Imitation Learning,Meta Learning,Few-shot Learning 等方向取得了很多進展。美國波士頓動力公司在人工智能應用中取得喜人的成就,其製造的機器人在複雜地形行走,多智能體協做等任務上表現良好(圖 1.19)。
自動駕駛(Autonomous Driving) 被認爲是強化學習短時間內能技術落地的一個應用方向,不少公司投入大量資源在自動駕駛上,如百度、Uber,Google 無人車等,其中百度的無人巴士「阿波龍」已經在北京、雄安、武漢等地展開試運營,圖 1.20 爲百度的自動駕駛汽車。
工欲善其事,必先利其器。在瞭解了深度學習及其發展簡史後,咱們來挑選一下深度學習要使用的工具吧。
❑ Theano 是最先的深度學習框架之一,由 Yoshua Bengio 和 Ian Goodfellow 等人開發,是一個基於 Python 語言、定位底層運算的計算庫,Theano 同時支持 GPU 和 CPU 運算。因爲 Theano 開發效率較低,模型編譯時間較長,同時開發人員轉投 TensorFlow等緣由,Theano 目前已經中止維護。
❑ Scikit-learn 是一個完整的面向機器學習算法的計算庫,內建了常見的傳統機器學習算法支持,文檔和案例也較爲豐富,可是 Scikit-learn 並非專門面向神經網絡而設計的,不支持 GPU 加速,對神經網絡相關層實現也較欠缺。
❑ Caffe 由華人博士賈揚清在 2013 年開發,主要面向使用卷積神經網絡的應用場合,並不適合其餘類型的神經網絡的應用。Caffe 的主要開發語言是 C++,也提供 Python 語言等接口,支持 GPU 和 CPU。因爲開發時間較早,在業界的知名度較高,2017 年Facebook 推出了 Caffe 的升級版本 Cafffe2,Caffe2 目前已經融入到 PyTorch 庫中。
❑ Torch 是一個很是優秀的科學計算庫,基於較冷門的編程語言 Lua 開發。Torch 靈活性較高,容易實現自定義網絡層,這也是 PyTorch 繼承得到的優良基因。可是因爲 Lua語言使用人羣較小,Torch 一直未能得到主流應用。
❑ MXNET 由華人博士陳天奇和李沐等人開發,已是亞馬遜公司的官方深度學習框架。採用了命令式編程和符號式編程混合方式,靈活性高,運行速度快,文檔和案例也較爲豐富。
❑ PyTorch 是 Facebook 基於原有的 Torch 框架推出的採用 Python 做爲主要開發語言的深度學習框架。PyTorch 借鑑了 Chainer 的設計風格,採用命令式編程,使得搭建網絡和調試網絡很是方便。儘管 PyTorch 在 2017 年才發佈,可是因爲精良緊湊的接口設計,PyTorch 在學術界得到了普遍好評。在 PyTorch 1.0 版本後,原來的 PyTorch 與 Caffe2進行了合併,彌補了 PyTorch 在工業部署方面的不足。總的來講,PyTorch 是一個很是優秀的深度學習框架。
❑ Keras 是一個基於 Theano 和 TensorFlow 等框架提供的底層運算而實現的高層框架,提供了大量方便快速訓練,測試的高層接口,對於常見應用來講,使用 Keras 開發效率很是高。可是因爲沒有底層實現,須要對底層框架進行抽象,運行效率不高,靈活性通常。
❑ TensorFlow 是 Google 於 2015 年發佈的深度學習框架,最第一版本只支持符號式編程。得益於發佈時間較早,以及 Google 在深度學習領域的影響力,TensorFlow 很快成爲最流行的深度學習框架。可是因爲 TensorFlow 接口設計頻繁變更,功能設計重複冗餘,符號式編程開發和調試很是困難等問題,TensorFlow 1.x 版本一度被業界詬病。2019年,Google 推出 TensorFlow 2 正式版本,將以動態圖優先模式運行,從而可以避免TensorFlow 1.x 版本的諸多缺陷,已得到業界的普遍承認。
目前來看,TensorFlow 和 PyTorch 框架是業界使用最爲普遍的兩個深度學習框架,TensorFlow 在工業界擁有完備的解決方案和用戶基礎,PyTorch 得益於其精簡靈活的接口設計,能夠快速設計調試網絡模型,在學術界得到好評如潮。TensorFlow 2 發佈後,彌補了 TensorFlow 在上手難度方面的不足,使得用戶能夠既能輕鬆上手 TensorFlow 框架,又能無縫部署網絡模型至工業系統。本書以 TensorFlow 2.0 版本做爲主要框架,實戰各類深度學習算法。
咱們這裏特別介紹 TensorFlow 與 Keras 之間的聯繫與區別。Keras 能夠理解爲一套高層 API 的設計規範,Keras 自己對這套規範有官方的實現,在 TensorFlow 中也實現了這套規範,稱爲 tf.keras 模塊,而且 tf.keras 將做爲 TensorFlow 2 版本的惟一高層接口,避免出現接口重複冗餘的問題。如無特別說明,本書中 Keras 均指代 tf.keras。
TensorFlow 2 是一個與 TensorFlow 1.x 使用體驗徹底不一樣的框架,TensorFlow 2 不兼容TensorFlow 1.x 的代碼,同時在編程風格、函數接口設計等上也截然不同,TensorFlow 1.x的代碼須要依賴人工的方式遷移,自動化遷移方式並不靠譜。Google 即將中止支持TensorFlow 1.x,不建議學習 TensorFlow 1.x 版本。
TensorFlow 2 支持動態圖優先模式,在計算時能夠同時得到計算圖與數值結果,能夠代碼中調試實時打印數據,搭建網絡也像搭積木同樣,層層堆疊,很是符合軟件開發思惟。
以簡單的2.0 + 4.0的相加運算爲例,在 TensorFlow 1.x 中,首先建立計算圖:
import tensorflow as tf # 1.建立計算圖階段 # 2.建立2個輸入端子,指定類型和名字 a_ph=tf.placeholder(tf.float32,name='variable_a'); b_ph=tf.placeholder(tf.float32,name='variable_b'); # 建立輸出端子的運算操做,並命名 c_op=tf.add(a_ph,b_ph,name='variable_a')
建立計算圖的過程就類比經過符號創建公式𝑐 = 𝑎 + 𝑏的過程,僅僅是記錄了公式的計算步驟,並無實際計算公式的數值結果,須要經過運行公式的輸出端子𝑐,並賦值𝑎 =2.0, 𝑏 = 4.0才能得到𝑐的數值結果:
#2. 運行計算圖階段 # 建立運行環境 sess=tf.InteractiveSession() # 初始化步驟也須要爲操做運行 init=tf.global_variables_initializer() sess.run(init) #運行初始化操做,完成初始化 #運行端輸出端子,須要給輸入端子賦值 c_numpy=sess.run(c_op,feed_dict={a_ph:2.,b_ph:4.}); #運算完輸出端子才能獲得數值類型的c_numpy print('a+b=',c_numpy)
能夠看到,在 TensorFlow 中完成簡單的2.0 + 4.0尚且如此繁瑣,更別說建立複雜的神經網絡算法有多艱難,這種先建立計算圖後運行的編程方式叫作符號式編程。接下來咱們使用 TensorFlow 2 來完成2.0 + 4.0運算:
# 1.建立輸入張量 a=tf.constant(2.) b=tf.constant(4.) # 2.直接計算並打印 print('a+b=',a+b)
這種運算時同時建立計算圖𝑎 + 𝑏和計算數值結果2.0 + 4.0的方式叫作命令式編程,也稱爲動態圖優先模式。TensorFlow 2 和 PyTorch 都是採用動態圖(優先)模式開發,調試方便,所見即所得。通常來講,動態圖模型開發效率高,可是運行效率可能不如靜態圖模式,TensorFlow 2 也支持經過 tf.function 將動態圖優先模式的代碼轉化爲靜態圖模式,實現開發和運行效率的共贏。
深度學習的核心是算法的設計思想,深度學習框架只是咱們實現算法的工具。下面咱們將演示 TensorFlow 深度學習框架的 3 大核心功能,從而幫助咱們理解框架在算法設計中扮演的角色。
a) 加速計算
神經網絡本質上由大量的矩陣相乘,矩陣相加等基本數學運算構成,TensorFlow 的重要功能就是利用 GPU 方便地實現並行計算加速功能。爲了演示 GPU 的加速效果,咱們經過完成屢次矩陣 A 和矩陣 B 的矩陣相乘運算的平均運算時間來驗證。其中矩陣 A 的 shape爲[1,𝑛],矩陣 B 的 shape 爲[𝑛, 1],經過調節 n 便可控制矩陣的大小。
首先咱們分別建立使用 CPU 和 GPU 運算的 2 個矩陣:
# 建立在CPU上運行的2個矩陣 with tf.device('/cpu:0'): cpu_a=tf.random.random_normal([1,n]) cpu_b=tf.random.random.normal([n,1]) print(cpu_a.device,cpu_b.device) # 建立使用GPU運算的2個矩陣 with tf.device('./gpu:0'): gpu_a=tf.random.normal([1,n]) gpu_b=tf.random.normal([n,1]) print(gpu_a.device,gpu_b.device)
並經過 timeit.timeit()函數來測量 2 個矩陣的運時間:
def cpu_run(): with tf.device('./cpu:0'): c=tf.matmul(cpu_a,cpu_b) return c def gpu_run(): with tf.device('./gpu:0'): c=tf.matmul(gpu_a,gpu_b) return c import timeit # 第一次計算須要熱身,避免將初始化階段時間結算在內 cpu_time=timeit.timeit(cpu_run(),number=10) gpu_time=timeit.timeit(gpu_run(),number=10) print('Warmup:',cpu_time,gpu_time) # 正式計算10次,取平均時間 cpu_time=timeit.timeit(cpu_run(),number=10) gpu_time=timeit.timeit(gpu_run(),number=10) print('rum time:',cpu_time,gpu_time)
咱們將不一樣大小的 n 下的 CPU 和 GPU 的運算時間繪製爲曲線,如圖 1.21 所示。能夠看到,在矩陣 A 和 B 較小時,CPU 和 GPU 時間幾乎一致,並不能體現出 GPU 並行計算的優點;在矩陣較大時,CPU 的計算時間明顯上升,而 GPU 充分發揮並行計算優點,運算時間幾乎不變。
b) 自動梯度
在使用 TensorFlow 構建前向計算過程的時候,除了可以得到數值結果,TensorFlow 還會自動構建計算圖,經過 TensorFlow 提供的自動求導的功能,能夠不須要手動推導,便可計算出輸出對網絡的偏導數。
考慮在(a,b,c,w)=(1,2,3,4)處的導數,
經過Tensorflow實現以下
import tensorflow as tf # 建立4個張量 a=tf.constant(1.) b=tf.constant(2.) c=tf.constant(3.) w=tf.constant(4.) with tf.GradientTape() as tape: #構建梯度環境 tape.watch([w]) # 將w加入梯度跟蹤列表 # 構建計算過程 y=a*w**2+b*w+c # 求導 [dy_dw]=tape.gradient(y,[w]) print(dy_dw) # 打印出導數 tf.Tensor(10.0, shape=(), dtype=float32)
c) 經常使用神經網絡接口
TensorFlow 除了提供底層的矩陣相乘,相加等運算函數,還內建了經常使用網絡運算函數,經常使用網絡層,網絡訓練,網絡保存與加載,網絡部署等一系列深度學習系統的便捷功能。使用 TensorFlow 開發網絡,能夠方便地利用這些功能完成經常使用業務流程,高效穩定。
在領略完深度學習框架所帶來的的便利後,咱們來着手在本地計算機環境安裝TensorFlow 最新版框架。TensorFlow 框架支持多種常見的操做系統,如 Windows 10,Ubuntu 18.04, Mac OS 等等,同時也支持運行在 NVIDIA 顯卡上的 GPU 版本和僅適用 CPU完成計算的 CPU 版本。咱們以最爲常見的 Windows 10 系統,NVIDIA GPU,Python 語言環境爲例,介紹如何安裝 TensorFlow 框架及其餘開發軟件等。
通常來講,開發環境安裝分爲 4 大步驟:安裝 Python 解釋器 Anaconda,安裝 CUDA加速庫,安裝 TensorFlow 框架,安裝經常使用編輯器。
Python 解釋器是讓 Python 語言編寫的代碼可以被 CPU 執行的橋樑,是 Python 語言的核心。用戶能夠從 官網下載最新版本(Python 3.8)的解釋器,像普通的應用軟件同樣安裝完成後,就能夠調用 python.exe 程序執行 Python 語言編寫的源代碼文件(*.py)。
咱們這裏選擇安裝集成了 Python 解釋器和虛擬環境等一系列輔助功能的 Anaconda 軟件,經過安裝 Anaconda 軟件,能夠同時得到 Python 解釋器,包管理,虛擬環境等一系列便捷功能,何樂而不爲呢。咱們從 網址進入 Anaconda 下載頁面,選擇 Python 最新版本的下載連接便可下載,下載完成後安裝便可進入安裝程序。如圖 1.22 所示,勾選」Add Anaconda to my PATH environmentvariable」一項,這樣能夠經過命令行方式調用 Anaconda 的程序。如圖 1.23 所示,安裝程序詢問是否連帶安裝 VS Code軟件,選擇skip便可。整個安裝流程持續5~10分鐘,具體時間需依據計算機性能而定。
安裝完成後,怎麼驗證 Anaconda 是否安裝成功呢?經過鍵盤上的 Windows 鍵+R 鍵,便可調出運行程序對話框,輸入 cmd 回車即打開 Windows 自帶的命令行程序 cmd.exe,或者點擊開始菜單,輸入 cmd 也可搜索到 cmd.exe 程序,打開便可。輸入 conda list 命令便可查看Python 環境已安裝的庫,若是是新安裝的 Python 環境,則列出的庫都是 Anaconda 自帶已默認安裝的軟件庫,如圖 1.24 所示。若是 conda list 可以正常彈出一系列的庫列表信息,說明 Anaconda 軟件安裝成功,若是 conda 命名不能被識別,則說明安裝失敗,須要從新安裝。
目前的深度學習框架大都基於 NVIDIA 的 GPU 顯卡進行加速運算,所以須要安裝NVIDIA 提供的 GPU 加速庫 CUDA 程序。在安裝 CUDA 以前,請確認本地計算機具備支持 CUDA 程序的 NVIDIA 顯卡設備,若是計算機沒有 NVIDIA 顯卡,如部分計算機顯卡生產商爲 AMD,以及部分 MacBook 筆記本電腦,則沒法安裝 CUDA 程序,所以能夠跳過這一步,直接進入 TensorFlow 安裝。CUDA 的安裝分爲 CUDA 軟件的安裝、cuDNN 深度神經網絡加速庫的安裝和環境變量配置三個步驟,安裝稍微繁瑣,請讀者在操做時思考每一個步驟的緣由,避免死記硬背流程。
CUDA 軟件安裝 打開 CUDA 程序的下載官網:https://developer.nvidia.com/cuda-10.0-download-archive,這裏咱們使用 CUDA 10.0 版本,依次選擇 Windows 平臺,x86_64 架構,10 系統,exe(local)本地安裝包,再選擇 Download 便可下載 CUDA 安裝軟件。下載完成後,打開安裝軟件。如圖 1.25 所示,選擇」Custom」選項,點擊 NEXT 按鈕進入圖 1.26安裝程序選擇列表,在這裏選擇須要安裝和取消不須要安裝的程序。在 CUDA 節點下,取消」Visual Studio Integration」一項;在「Driver components」節點下,比對目前計算機已經安裝的顯卡驅動「Display Driver」的版本號「Current Version」和 CUDA 自帶的顯卡驅動版本號「New Version」,若是「Current Version」大於「New Version」,則須要取消「Display Driver」的勾,若是小於或等於,則默認勾選便可。設置完成後便可正常安裝完成。
安裝完成後,咱們來測試 CUDA 軟件是否安裝成功。打開 cmd 命令行,輸入「nvcc -V」,便可打印當前 CUDA 的版本信息,如圖 1.29 所示,若是命令沒法識別,則說明安裝失敗。同時咱們也可從 CUDA 的安裝路徑「C:\Program Files\NVIDIA GPU ComputingToolkit\CUDA\v10.0\bin」下找到「nvcc.exe」程序,如圖 1.28 所示。
cuDNN 神經網絡加速庫安裝 CUDA 並非針對於神經網絡設計的 GPU 加速庫,它面向各類須要並行計算的應用設計。若是但願針對於神經網絡應用加速,須要額外安裝cuDNN 庫。須要注意的是,cuDNN 庫並非運行程序,只須要下載解壓 cuDNN 文件,並配置 Path 環境變量便可。
打開網址 https://developer.nvidia.com/cudnn,選擇「Download cuDNN」,因爲 NVIDIA公司的規定,下載 cuDNN 須要先登陸,所以用戶須要登陸或建立新用戶後才能繼續下載。登陸後,進入 cuDNN 下載界面,勾選「I Agree To the Terms of the cuDNN SoftwareLicense Agreement」,便可彈出 cuDNN 版本下載選項。咱們選擇 CUDA 10.0 匹配的 cuDNN版本,並點擊「cuDNN Library for Windows 10」連接便可下載 cuDNN 文件。須要注意的是,cuDNN 自己具備一個版本號,同時它還須要和 CUDA 的版本號對應上,不能下錯不匹配 CUDA 版本號的 cuDNN 文件。
下載完成 cuDNN 文件後,解壓並進入文件夾,咱們將名爲「cuda」的文件夾重命名爲「cudnn765」,並複製此文件夾。進入 CUDA 的安裝路徑 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.0,粘貼「cudnn765」文件夾便可,此處可能會彈出須要管理員權限的對話框,選擇繼續便可粘貼,如圖 1.31 所示。
環境變量 Path 配置 上述 cudnn 文件夾的複製即已完成 cuDNN 的安裝,但爲了讓系統可以感知到 cuDNN 文件的位置,咱們須要額外配置 Path 環境變量。打開文件瀏覽器,在「個人電腦」上右擊,選擇「屬性」,選擇「高級系統屬性」,選擇「環境變量」,如圖1.32。在「系統變量」一欄中選中「Path」環境變量,選擇「編輯」,如圖 1.33 所示。選擇「新建」,輸入咱們 cuDNN 的安裝路徑「C:\Program Files\NVIDIA GPU ComputingToolkit\CUDA\v10.0\cudnn765\bin」,並經過「向上移動」按鈕將這一項上移置頂。
CUDA 安裝完成後,環境變量中應該包含「C:\Program Files\NVIDIA GPU ComputingToolkit\CUDA\v10.0\bin」,「C:\Program Files\NVIDIA GPU ComputingToolkit\CUDA\v10.0\libnvvp」和「C:\Program Files\NVIDIA GPU ComputingToolkit\CUDA\v10.0\cudnn765\bin」三項,具體的路徑可能依據實際路徑略有出入,如圖測
1.34 所示,確認無誤後依次點擊肯定,關閉全部對話框。
TensorFlow 和其餘的 Python 庫同樣,使用Python 包管理工具 pip install 命令便可安裝。安裝 TensorFlow 時,須要根據電腦是否 NVIDIAGPU 顯卡來肯定是安裝性能更強的GPU 版本仍是性能通常的 CPU 版本。
國內使用 pip 命令安裝時,可能會出現下載速度緩慢甚至鏈接斷開的狀況,須要配置國內的 pip 源,只須要在 pip install 命令後面帶上「-i 源地址」便可,例如使用清華源安裝numpy 包,首先打開 cmd 命令行程序,輸入:
# 使用國內清華源安裝 numpy pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple
便可自動下載並按着 numpy 庫,配置上國內源的 pip 下載速度會提高顯著。如今咱們來 TensorFlow GPU 最新版本:
# 使用清華源安裝 TensorFlow GPU 版本 pip install -U tensorflow-gpu -i https://pypi.tuna.tsinghua.edu.cn/simple
上述命令自動下載 TensorFlow GPU 版本並安裝,目前是 TensorFlow 2.0.0 正式版,「-U」參數指定若是已安裝此包,則執行升級命令。
如今咱們來測試 GPU 版本的 TensorFlow 是否安裝成功。在 cmd 命令行輸入 ipython 進入 ipython 交互式終端,輸入「import tensorflow as tf」
命令,若是沒有錯誤產生,繼續輸入「tf.test.is_gpu_available()」
測試 GPU 是否可用,此命令會打印出一系列以「I」開頭的信息(Information),其中包含了可用的 GPU 顯卡設備信息,最後會返回「True」或者「False」,表明了 GPU 設備是否可用,如圖 1.35 所示。若是爲 True,則 TensorFlow GPU版本安裝成功;若是爲 False,則安裝失敗,須要再次檢測 CUDA,cuDNN,環境變量等步驟,或者複製錯誤,從搜索引擎中尋求幫助。
若是不能安裝 TensorFlow GPU 版本,則能夠安裝 CPU 版本暫時用做學習。CPU 版本沒法利用 GPU 加速運算,計算速度相對緩慢,可是做爲學習介紹的算法模型通常不大,使用 CPU 版本也能勉強應付,待往後對深度學習有了必定了解再升級 NVIDIA GPU 設備也何嘗不可。亦或者,安裝 TensorFlow GPU 版本可能容易出現安裝失敗的狀況,不少讀者朋友動手能力通常,若是折騰了好久還不能搞定,能夠直接安裝 CPU 版本先使用着。
安裝 CPU 版本的命令爲:
# 使用國內清華源安裝 TensorFlow CPU 版本 pip install -U tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple
安裝完後,在 ipython 中輸入「import tensorflow as tf」命令便可驗證 CPU 版本是否安裝成功。
TensorFlow GPU/CPU 版本安裝完成後,能夠經過「tf.version」查看本地安裝的TensorFlow 版本號,如圖 1.36所示
經常使用的 python 庫也能夠順帶安裝:
# 使用清華源安裝經常使用 python 庫 pip install -U numpy matplotlib pillow pandas -i https://pypi.tuna.tsinghua.edu.cn/simple
使用 Python 語言編寫程序的方式很是多,能夠使用 ipython 或者 ipython notebook 方式交互式編寫代碼,也能夠利用 Sublime Text,PyCharm 和 VS Code 等綜合 IDE 開發中大型項目。本書推薦使用 PyCharm 編寫和調試,使用 VS Code 交互式開發,這二者均可以避免費使用,用戶自行下載安裝,並配置 Python 解釋器,限於篇幅,再也不敖述。接下來,讓咱們開啓深度學習之旅吧!
有些人擔憂人工智能會讓人類以爲自卑,可是實際上,即便是看到一朵花,咱們也應該或多或少感到一些自愧不如。−艾倫·凱
成年人大腦中包含了約 1000 億個神經元,每一個神經元經過樹突獲取輸入信號,經過軸突傳遞輸出信號,神經元之間相互鏈接構成了巨大的神經網絡,從而造成了人腦的感知和意識基礎,圖 2.1 是一種典型的生物神經元結構。1943 年,心理學家沃倫·麥卡洛克(Warren McCulloch)和數理邏輯學家沃爾特·皮茨(Walter Pitts)經過對生物神經元的研究,提出了模擬生物神經元機制的人工神經網絡的數學模型 (McCulloch & Pitts, 1943),這一成果被美國神經學家弗蘭克·羅森布拉特(Frank Rosenblatt)進一步發展成感知機(Perceptron)模型,這也是現代深度學習的基石。
咱們將從生物神經元的結構出發,重溫科學先驅們的探索之路,逐步揭開自動學習機器的神祕面紗。
首先,咱們把生物神經元(Neuron)的模型抽象爲如圖 2.2(a)所示的數學結構:神經元輸入向量𝒙 = [𝑥1, 𝑥2, 𝑥3, … , 𝑥𝑛]T,通過函數映射:𝑓𝜃: 𝒙 → 𝑦後獲得輸出𝑦,其中𝜃爲函數𝑓自身的參數。考慮一種簡化的狀況,即線性變換:𝑓(𝒙) = 𝒘T𝒙 + 𝑏,展開爲標量形式:
𝑓(𝒙) = 𝑤1𝑥1 + 𝑤2𝑥2 + 𝑤3𝑥3 + ⋯ + 𝑤𝑛𝑥𝑛 + 𝑏
上述計算邏輯能夠經過圖 2.2(b)直觀地展示
參數𝜃 ∶= {𝑤1, 𝑤2, 𝑤3, . . . , 𝑤𝑛,𝑏}肯定了神經元的狀態,經過固定𝜃參數便可肯定此神經元的處理邏輯。當神經元輸入節點數𝑛 = 1(單輸入)時,神經元數學模型可進一步簡化爲:𝑦 = 𝑤𝑥 + 𝑏
此時咱們能夠繪製出神經元的輸出𝑦和輸入𝑥的變化趨勢,如圖 2.3 所示,隨着輸入信號𝑥的增長,輸出電平𝑦也隨之線性增長,其中𝑤參數能夠理解爲直線的斜率(Slope),b 參數爲直線的偏置(Bias)。
對於某個神經元來講,𝑥和𝑦的映射關係
是未知但肯定的。兩點便可肯定一條直線,爲了估計𝑤和𝑏的值,咱們只需從圖 2.3 中直線上採樣任意 2 個數據點:
(𝑥^{1}, 𝑦(1)), (𝑥(2), 𝑦(2))便可,其中上標表示數據點編號:
𝑦(1) = 𝑤𝑥(1) + 𝑏
𝑦(2) = 𝑤𝑥(2) + 𝑏
當(𝑥(1),𝑦(1)) ≠ (𝑥(2),𝑦(2))時,經過求解上式即可計算出𝑤和𝑏的值。考慮某個具體的例子:𝑥(1) = 1, 𝑦
(1) = 1.56 , 𝑥(2) = 2, 𝑦(2) = 3. 3, 代入上式中可得:
1.56 = 𝑤 ∙ 1 + 𝑏
3. 3 = 𝑤 ∙ 2 + 𝑏
這就是咱們初中時代學習過的二元一次方程組,經過消元法能夠輕鬆計算出𝑤和𝑏的解析解:𝑤 = 1. 477, 𝑏 = 0.089 。
能夠看到,只須要觀測兩個不一樣數據點,就可完美求解單輸入線性神經元模型的參數,對於𝑁輸入的現象神經元模型,只須要採樣𝑁 + 1組不一樣數據點便可,彷佛線性神經元模型能夠獲得完美解決。那麼上述方法存在什麼問題呢?考慮對於任何採樣點,都有可能存在觀測偏差,咱們假設觀測偏差變量𝜖屬於均值爲𝜇,方差爲𝜎2的正態分佈(NormalDistribution,或高斯分佈,Gaussian Distribution):𝒩(𝜇, 𝜎
2),則採樣到的樣本符合:
一旦引入觀測偏差後,即便簡單如線性模型,若是僅採樣兩個數據點,可能會帶來較大估計誤差。如圖 2.4 所示,圖中的數據點均帶有觀測偏差,若是基於藍色矩形塊的兩個數據點進行估計,則計算出的藍色虛線與真實橙色直線存在較大誤差。爲了減小觀測偏差引入的估計誤差,能夠經過採樣多組數據樣本集合𝔻 =
,而後找出一條「最好」的直線,使得它儘量地讓全部採樣點到該直線的偏差(Error,或損失 Loss)之和最小。
也就是說,因爲觀測偏差𝜖的存在,當咱們採集了多個數據點𝔻時,可能不存在一條直線完美的穿過全部採樣點。退而求其次,咱們但願能找到一條比較「好」的位於採樣點中間的直線。那麼怎麼衡量「好」與「很差」呢?一個很天然的想法就是,求出當前模型的全部採樣點上的預測值𝑤𝑥(𝑖) + 𝑏與真實值𝑦(𝑖)之間的差的平方和做爲總偏差ℒ:
而後搜索一組參數𝑤∗, 𝑏∗使得ℒ最小,對應的直線就是咱們要尋找的最優直線:
其中𝑛表示採樣點的個數。這種偏差計算方法稱爲均方偏差(Mean Squared Error,簡稱
MSE)。
如今來小結一下上述方案:咱們須要找出最優參數(Optimal Parameter)𝑤∗和𝑏∗,使得輸入和輸出知足線性關係𝑦(𝑖) = 𝑤𝑥(𝑖) + 𝑏, 𝑖 ∈ [1, 𝑛]。可是因爲觀測偏差𝜖的存在,須要經過採樣足夠多組的數據樣本組成的數據集(Dataset):𝔻 ={(𝑥(1),𝑦(1)), (𝑥(2),𝑦(2)),… , (𝑥(𝑛), 𝑦(𝑛))},找到一組最優的參數 𝑤∗, 𝑏∗使得均方差ℒ =1𝑛 (𝑤𝑥(𝑖) + 𝑏 − 𝑦(𝑖))2 𝑛𝑖=1 最小。
對於單輸入的神經元模型,只須要兩個樣本,能經過消元法求出方程組的精確解,這種經過嚴格的公式推導出的精確解稱爲解析解(Closed-form Solution)。可是對於多個數據點(𝑛 ≫ 2)的狀況,這時頗有可能不存在解析解,咱們只能藉助數值方法去優化(Optimize)出一個近似的數值解(Numerical Solution)。爲何叫做優化?這是由於計算機的計算速度很是快,咱們能夠藉助強大的計算能力去屢次「搜索」和「試錯」,從而一步步下降偏差ℒ。最簡單的優化方法就是暴力搜索或隨機試驗,好比要找出最合適的w∗和𝑏∗,咱們就能夠從(部分)實數空間中隨機採樣任意的𝑤和𝑏,並計算出對應模型的偏差值ℒ,而後從測試過的{ℒ}中挑出最好的ℒ∗,它所對應的𝑤和𝑏就能夠做爲咱們要找的最優w∗和𝑏∗。
這種算法當然簡單直接,可是面對大規模、高維度數據的優化問題時計算效率極低,基本不可行。梯度降低算法(Gradient Descent)是神經網絡訓練中最經常使用的優化算法,配合強大的圖形處理芯片 GPU(Graphics Processing Unit)的並行加速能力,很是適合優化海量數據的神經網絡模型,天然也適合優化咱們這裏的神經元線性模型。這裏先簡單地應用梯度降低算法,用於解決神經元模型預測的問題。因爲梯度降低算法是深度學習的核心算法,咱們將在第 7 章很是詳盡地推導梯度降低算法在神經網絡中的應用,這裏先給讀者第一印象。
咱們在高中時代學過導數(Derivative)的概念,若是要求解一個函數的極大、極小值,能夠簡單地令導數函數爲 0,求出對應的自變量點(稱爲駐點),再檢驗駐點類型便可。以函數𝑓(𝑥) =
∙ 𝑠𝑖𝑛 (𝑥)爲例,咱們繪製出函數及其導數在𝑥 ∈ [−1 ,1 ]區間曲線,其中藍色實線爲𝑓(𝑥),黃色虛線爲
,如圖 2.5 所示。能夠看出,函數導數(虛線)爲 0 的點即爲𝑓(𝑥)的駐點,函數的極大值和極小值點均出如今駐點中。
函數的梯度(Gradient)定義爲函數對各個自變量的偏導數(Partial Derivative)組成的向量。考慮 3 維函數𝑧 = 𝑓(𝑥, 𝑦),函數對自變量𝑥的偏導數記爲
,函數對自變量y的偏導數記爲
,則梯度∇𝑓爲向量
。咱們經過一個具體的函數來感覺梯度的性質,如圖 2.6所示,
,圖中𝑥𝑦平面的紅色箭頭的長度表示梯度向量的模,箭頭的方向表示梯度向量的方向。能夠看到,箭頭的方向老是指向當前位置函數值增速最大的方向,函數曲面越陡峭,箭頭的長度也就越長,梯度的模也越大。
經過上面的例子,咱們能直觀地感覺到,函數在各處的梯度方向∇𝑓老是指向函數值增大的方向,那麼梯度的反方向−∇𝑓應指向函數值減小的方向。利用這一性質,咱們只須要按照
來迭代更新𝒙′,就能得到愈來愈小的函數值,其中𝜂用來縮放梯度向量,通常設置爲某較小的值,如 0.01,0.001 等。特別地,對於一維函數,上述向量形式能夠退化成標量形式:
經過上式迭代更新𝑥′若干次,這樣獲得的𝑥′處的函數值𝑦′,老是更有可能比在𝑥處的函數值𝑦小。
經過上面公式優化參數的方法稱爲梯度降低算法,它經過循環計算函數的梯度∇𝑓並更新待優化參數𝜃,從而獲得函數𝑓得到極小值時參數𝜃的最優數值解。須要注意的是,在深度學習中,通常𝒙表示模型輸入,模型的待優化參數通常用𝜃、𝑤、𝑏等符號表示。
如今咱們將應用速學的梯度降低算法來求解𝑤∗和𝑏∗參數。這裏要最小化的是均方差偏差函數ℒ:
須要優化的模型參數是𝑤和𝑏,所以咱們按照
方式循環更新參數。
在介紹了用於優化𝑤和𝑏的梯度降低算法後,咱們來實戰訓練單輸入神經元線性模型。首先咱們須要採樣自真實模型的多組數據,對於已知真實模型的玩具樣例(Toy Example),咱們直接從指定的𝑤 = 1.477 , 𝑏 = 0.089 的真實模型中直接採樣:
1. 採樣數據
爲了可以很好地模擬真實樣本的觀測偏差,咱們給模型添加偏差自變量𝜖,它採樣自均值爲 0,方差爲 0.01 的高斯分佈:
經過隨機採樣𝑛 = 100 次,咱們得到𝑛個樣本的訓練數據集𝔻train:
import numpy as np data=[] #保存樣本集的列表 for i in range(100): #循環採樣100個點 x=np.random.uniform(-10.,10.) #隨機採樣輸入x # 採樣高斯噪聲 eps=np.random.normal(0.,0.1) # 均值和方差 # 獲得模型的輸出 y=1.477*x+0.089+eps data.append([x,y]) #保存樣本點 data=np.array(data)# 轉換爲2D Numpy數組 print(data)
循環進行 100 次採樣,每次從區間[-10, 10]的均勻分佈U( ,1)中隨機採樣一個數據𝑥,同時從均值爲 0,方差爲 的高斯分佈𝒩( 0, )中隨機採樣噪聲𝜖,根據真實模型生成𝑦的數據,並保存爲 Numpy數組。
2. 計算偏差
循環計算在每一個點(𝑥(𝑖), 𝑦(𝑖))處的預測值與真實值之間差的平方並累加,從而得到訓練集上的均方差損失值.
def mse(b,w,points): totalError=0 # 根據當前的w,b參數計算均方差損失 for i in range(0,len(points)): # 循環迭代全部點 x=points[i,0] #得到i號點的輸入x y=points[i,1] #得到i號點的輸出y # 計算差的平方,並累加 totalError+=(y-(w*x+b))**2 # 將累加的偏差求平均,獲得均方偏差 return totalError/float(len(points))
最後的偏差和除以數據樣本總數,從而獲得每一個樣本上的平均偏差。
3. 計算梯度
根據以前介紹的梯度降低算法,咱們須要計算出函數在每個點上的梯度信息:
。咱們來推導一下梯度的表達式,首先考慮
,將均方差函數展開:
考慮到
所以
若是難以理解上述推導,能夠複習數學中函數的梯度相關課程,同時在本書第 7 章也會詳細介紹,咱們能夠記住
的最終表達式便可。用一樣的方法,咱們能夠推導偏導數
的表達式:
根據上面偏導數的表達式,咱們只須要計算在每個點上面的(𝑤𝑥(𝑖) + 𝑏 − 𝑦(𝑖)) ∙𝑥(𝑖)和(𝑤𝑥(𝑖) + 𝑏 − 𝑦(𝑖)
)值,平均後便可獲得偏導數
和
。實現以下:
def step_gradient(b_current,w_current,points,lr): # 計算偏差函數在全部點上的異數,並更新w,b b_gradirnt=0 w_gradient=0 M=float(len(points))# 整體樣本 for i in range(0,len(points)): x=points[i,0] y=points[i,1] # 偏差函數對b的導數;grad_b=2(wx+b-y) b_gradirnt+=(2/M) *((w_current*x+b_current)-y) # 偏差函數對w的求導:grad_w=2(wx+b-y)*x w_gradient=w_gradient+(2/M)*x*((w_current*x+b_current)-y) # 根據梯度降低算法更新的 w',b',其中lr爲學習率 new_b=b_current-(lr*b_gradirnt) new_w=w_current-(lr*w_gradient) return [new_b,new_w]
4. 梯度更新
在計算出偏差函數在𝑤和𝑏處的梯度後,咱們能夠根據公式來更新𝑤和𝑏的值。咱們把對數據集的全部樣本訓練一次稱爲一個 Epoch,共循環迭代 num_iterations 個 Epoch。實現以下:
def gradient_descent(points,starting_b,starting_w,lr,num_iterations): # 循環更新w,b屢次 b=starting_b #b的初始值 w=starting_w #w的初始值 #根據梯度降低算法更新屢次 for step in range(num_iterations): # 計算梯度並跟新一次 b,w=step_gradient(b,w,np.array(points),lr) loss=mse(b,w,points) #計算當前的均方偏差,用於監控訓練進度 if step%50==0: #打印偏差和實時的w,b值 print("iteration:{},loss:{},w:{},b:{}".format(step,loss,w,b)) return [b,w] #返回最後一次的w,b
主訓練函數實現以下:
def main(): # 加載訓練數據集,這些數據是經過真實模型添加觀測偏差採集的到的 lr=0.01 # 學習率 initial_b=0 # 初始化b爲0 initial_w=0 # 初始化w爲0 num_iteration=1000 # 訓練優化1000次,返回最優 w*,b*和訓練Loss的降低過程 [b,w],losses=gradient_descent(data,initial_b,initial_w,lr,num_iteration) loss=mse(b,w,data)# 計算最優數值w,b的均方偏差 print('Final loss:{},w:{},b:{}'.format(loss,w,b))
通過 1000 的迭代更新後,保存最後的𝑤和𝑏值,此時的𝑤和𝑏的值就是咱們要找的w∗和𝑏∗數值解。運行結果以下:
iteration:0, loss:11.437586448749, w:0.88955725981925, b:0.02661765516748428
iteration:50, loss:0.111323083882350, w:1.48132089048970, b:0.58389075913875
iteration:100, loss:0.02436449474995, w:1.479296279074, b:0.78524532356388
…
iteration:950, loss:0.01097700897880, w:1.478131231919, b:0.901113267769968
Final loss:0.010977008978805611, w:1.4781312318924746, b:0.901113270434582
能夠看到,第 100 次迭代時,𝑤和𝑏的值就已經比較接近真實模型了,更新 1000 次後獲得的𝑤∗和𝑏∗數值解與真實模型的很是接近,訓練過程的均方差變化曲線如圖 2.7 所示。
上述例子比較好地展現了梯度降低算法在求解模型參數上的強大之處。須要注意的是,對於複雜的非線性模型,經過梯度降低算法求解到的𝑤和𝑏多是局部極小值而非全局最小值解,這是由模型函數的非凸性決定的。可是咱們在實踐中發現,經過梯度降低算法求得的數值解,它的性能每每都能優化得很好,能夠直接使用求解到的數值解𝑤和𝑏來近似做爲最優解。
簡單回顧一下咱們的探索之路:首先假設𝑛個輸入的生物神經元的數學模型爲線性模型以後,只採樣𝑛 + 1個數據點就能夠估計線性模型的參數𝒘和𝑏。引入觀測偏差後,經過梯度降低算法,咱們能夠採樣多組數據點循環優化獲得𝒘和𝑏的數值解。
若是咱們換一個角度來看待這個問題,它其實能夠理解爲一組連續值(向量)的預測問題。給定數據集𝔻,咱們須要從𝔻中學習到數據的真實模型,從而預測未見過的樣本的輸出值。在假定模型的類型後,學習過程就變成了搜索模型參數的問題,好比咱們假設神經元爲線性模型,那麼訓練過程即爲搜索線性模型的𝒘和𝑏參數的過程。訓練完成後,利用學到的模型,對於任意的新輸入𝒙,咱們就能夠使用學習模型輸出值做爲真實值的近似。從這個角度來看,它就是一個連續值的預測問題。
在現實生活中,連續值預測問題是很是常見的,好比股價的走勢預測、天氣預報中溫度和溼度等的預測、年齡的預測、交通流量的預測等。對於預測值是連續的實數範圍,或者屬於某一段連續的實數區間,咱們把這種問題稱爲迴歸(Regression)問題。特別地,若是使用線性模型去逼近真實模型,那麼咱們把這一類方法叫作線性迴歸(Linear Regression,簡稱 LR),線性迴歸是迴歸問題中的一種具體的實現。
除了連續值預測問題之外,是否是還有離散值預測問題呢?好比說硬幣正反面的預測,它的預測值𝑦只可能有正面或反面兩種可能;再好比說給定一張圖片,這張圖片中物體的類別也只多是像貓、狗、天空之類的離散類別值。對於這一類問題,咱們把它稱爲分類(Classification)問題。接下來咱們來挑戰分類問題吧!
在人工智能上花一年時間,這足以讓人相信上帝的存在。−艾倫·佩利
前面已經介紹了用於連續值預測的線性迴歸模型,如今咱們來挑戰分類問題。分類問題的一個典型應用就是教會機器如何去自動識別圖片中物體的種類。考慮圖片分類中最簡單的任務之一:0~9 數字圖片識別,它相對簡單,並且也具備很是普遍的應用價值,好比郵政編碼、快遞單號、手機號碼等都屬於數字圖片識別範疇。咱們將以數字圖片識別爲例,探索如何用機器學習的方法去解決這個問題。
機器學習須要從數據中間學習,首先咱們須要採集大量的真實樣本數據。以手寫的數字圖片識別爲例,如圖 3.1 所示,咱們須要收集大量的由真人書寫的 0~9的數字圖片,爲了便於存儲和計算,通常把收集的原始圖片縮放到某個固定的大小(Size 或 Shape),好比224 個像素的行和 224 個像素的列(224 × 224),或者 96 個像素的行和 96 個像素的列(96 × 96),這張圖片將做爲輸入數據 x。同時,咱們須要給每一張圖片標註一個標籤(Label),它將做爲圖片的真實值𝑦,這個標籤代表這張圖片屬於哪個具體的類別,通常經過映射方式將類別名一一對應到從 0 開始編號的數字,好比說硬幣的正反面,咱們能夠用0 來表示硬幣的反面,用 1 來表示硬幣的正面,固然也能夠反過來 1 表示硬幣的反面,這種編碼方式叫做數字編碼(Number Encoding)。對於手寫數字圖片識別問題,編碼更爲直觀,咱們用數字的 0~9 來表示類別名字爲 0~9 的圖片。
若是但願模型可以在新樣本上也能具備良好的表現,即模型泛化能力(GeneralizationAbility)較好,那麼咱們應該儘量多地增長數據集的規模和多樣性(Variance),使得咱們用於學習的訓練數據集與真實的手寫數字圖片的分佈(Ground-truth Distribution)儘量的逼近,這樣在訓練數據集上面學到了模型可以很好的用於未見過的手寫數字圖片的預測。
爲了方便業界統一測試和評估算法, (Lecun, Bottou, Bengio, & Haffner, 1998)發佈了手寫數字圖片數據集,命名爲 MNIST,它包含了 0~9 共 10 種數字的手寫圖片,每種數字一共有 7000 張圖片,採集自不一樣書寫風格的真實手寫圖片,一共 70000 張圖片。其中 60000張圖片做爲訓練𝔻train(Training Set),用來訓練模型,剩下 10000 張圖片做爲測試集𝔻test(Test Set),用來預測或者測試,訓練集和測試集共同組成了整個 MNIST 數據集。
考慮到手寫數字圖片包含的信息比較簡單,每張圖片均被縮放到28 × 28的大小,同時只保留了灰度信息,如圖 3.2 所示。這些圖片由真人書寫,包含了如字體大小、書寫風格、粗細等豐富的樣式,確保這些圖片的分佈與真實的手寫數字圖片的分佈儘量的接近,從而保證了模型的泛化能力。
如今咱們來看下圖片的表示方法。一張圖片包含了ℎ行(Height/Row),𝑤列(Width/Column),每一個位置保存了像素(Pixel)值,像素值通常使用 0~255 的整形數值來表達顏色強度信息,例如 0 表示強度最低,255 表示強度最高。若是是彩色圖片,則每一個像素點包含了 R、G、B 三個通道的強度信息,分別表明紅色通道、綠色通道、藍色通道的顏色強度,因此與灰度圖片不一樣,它的每一個像素點使用一個 1 維、長度爲 3 的向量(Vector)來表示,向量的 3 個元素依次表明了當前像素點上面的 R、G、B 顏色強值,所以彩色圖片須要保存爲形狀是[ℎ, 𝑤, 3]的張量(Tensor,能夠通俗地理解爲 3 維數組)。若是是灰度圖片,則使用一個數值來表示灰度強度,例如 0 表示純黑,255 表示純白,所以它只須要一個形爲[ℎ, 𝑤]的二維矩陣(Matrix)來表示一張圖片信息(也能夠保存爲[ℎ, 𝑤, 1]形狀的張量)。圖 3.3 演示了內容爲 8 的數字圖片的矩陣內容,能夠看到,圖片中黑色的像素用 0 表示,灰度信息用 0~255 表示,圖片中灰度越白的像素點,對應矩陣位置中數值也就越大。
目前經常使用的深度學習框架,如 TensorFlow,PyTorch 等,均可以很是方便的經過數行代碼自動下載、管理和加載 MNIST 數據集,不須要咱們額外編寫代碼,使用起來很是方便。咱們這裏利用TensorFlow 自動在線下載 MNIST 數據集,並轉換爲 Numpy 數組格式:
import os import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers,optimizers,datasets (x,y),(x_val,y_val)=datasets.mnist.load_data()#60000訓練集/10000測試集 x=2*tf.convert_to_tensor(x,dtype=tf.float32)/255-1 #轉換爲張量,縮放到-1~1 60000*28*28 y=tf.one_hot(y,depth=10)#one-hot編碼 60000,10 print(x.shape,y.shape) train_dataset=tf.data.Dataset.from_tensor_slices((x,y))#構建數據集對象 train_dataset=train_dataset.batch(512)#批量訓練
load_data()函數返回兩個元組(tuple)對象,第一個是訓練集,第二個是測試集,每一個 tuple的第一個元素是多個訓練圖片數據X,第二個元素是訓練圖片對應的類別數字Y。其中訓練集X的大小爲(60000,28,28),表明了 60000 個樣本,每一個樣本由 28 行、28 列構成,因爲是灰度圖片,故沒有 RGB 通道;訓練集Y的大小爲(60000, ),表明了這 60000 個樣本的標籤數字,每一個樣本標籤用一個 0~9 的數字表示。測試集 X 的大小爲(10000,28,28),表明了10000 張測試圖片,Y 的大小爲(10000, )。
從 TensorFlow 中加載的 MNIST 數據圖片,數值的範圍在[0,255]之間。在機器學習中間,通常但願數據的範圍在 0 周圍小範圍內分佈。經過預處理步驟,咱們把[0,255]像素範圍歸一化(Normalize)到[0,1.]區間,再縮放到[−1,1]區間,從而有利於模型的訓練。
每一張圖片的計算流程是通用的,咱們在計算的過程當中能夠一次進行多張圖片的計算,充分利用 CPU 或 GPU 的並行計算能力。一張圖片咱們用 shape 爲[h, w]的矩陣來表示,對於多張圖片來講,咱們在前面添加一個數量維度(Dimension),使用 shape 爲[𝑏, ℎ, 𝑤]的張量來表示,其中的𝑏表明了 batch size(批量);多張彩色圖片能夠使用 shape 爲[𝑏, ℎ, 𝑤, 𝑐]的張量來表示,其中的𝑐表示通道數量(Channel),彩色圖片𝑐 = 3。經過 TensorFlow 的Dataset 對象能夠方便完成模型的批量訓練,只須要調用 batch()函數便可構建帶 batch 功能的數據集對象。
回顧咱們在迴歸問題討論的生物神經元結構。咱們把一組長度爲
的輸入向量𝒙 =[𝑥1, 𝑥2, … , 𝑥𝑑𝑖𝑛]𝑇簡化爲單輸入標量 x,模型能夠表達成𝑦 = 𝑥 ∗ 𝑤 + 𝑏。若是是多輸入、單輸出的模型結構的話,咱們須要藉助於向量形式:
更通常地,經過組合多個多輸入、單輸出的神經元模型,能夠拼成一個多輸入、多輸出的模型:
其中,
對於多輸出節點、批量訓練方式,咱們將模型寫成張量形式:
其中
,
表示輸入節點數,
表示輸出節點數;X shape 爲[𝑏,
],表示𝑏個樣本的輸入數據,每一個樣本的特徵長度爲𝑑𝑖𝑛;W的 shape 爲[
,
],共包含了
*
個網絡參數;偏置向量𝒃 shape 爲
,每一個輸出節點上均添加一個偏置值;@符號表示矩陣相乘(Matrix Multiplication,matmul)。
考慮 2 個樣本,輸入特徵長度
= 3,輸出特徵長度
= 2的模型,公式展開爲
其中
,
等符號的上標表示樣本索引號,下標表示樣本向量的元素。對應模型結構圖爲
能夠看到,經過張量形式表達網絡結構,更加簡潔清晰,同時也可充分利用張量計算的並行加速能力。那麼怎麼將圖片識別任務的輸入和輸出轉變爲知足格式要求的張量形式呢
考慮輸入格式,一張圖片𝒙使用矩陣方式存儲,shape 爲:[ℎ, 𝑤],𝑏張圖片使用 shape爲[𝑏, ℎ, 𝑤]的張量 X 存儲。而咱們模型只能接受向量形式的輸入特徵向量,所以須要將[ℎ, 𝑤]的矩陣形式圖片特徵平鋪成[ℎ ∗ 𝑤]長度的向量,如圖 3.5 所示,其中輸入特徵的長度
= ℎ ∗ 𝑤。
對於輸出標籤,前面咱們已經介紹了數字編碼,它能夠用一個數字來表示便籤信息,例如數字 1 表示貓,數字 3 表示魚等。可是數字編碼一個最大的問題是,數字之間存在自然的大小關係,好比1 < 2 < 3,若是 一、二、3 分別對應的標籤是貓、狗、魚,他們之間並無大小關係,因此採用數字編碼的時候會迫使模型去學習到這種沒必要要的約束。
那麼怎麼解決這個問題呢?能夠將輸出設置爲
個輸出節點的向量,
與類別數相同,讓第𝑖 ∈ [1,
]個輸出值表示當前樣本屬於類別𝑖的機率𝑃(𝑥屬於類別𝑖|𝑥)。咱們只考慮輸入圖片只輸入一個類別的狀況,此時輸入圖片的真實的標註已經明確:若是物體屬於第𝑖類的話,那麼索引爲𝑖的位置上設置爲 1,其餘位置設置爲 0,咱們把這種編碼方式叫作 one-hot 編碼(獨熱編碼)。以圖 3.6 中的「貓狗魚鳥」識別系統爲例,全部的樣本只屬於「貓狗魚鳥」4 個類別中其一,咱們將第1,2,3,4號索引位置分別表示貓狗魚鳥的類別,對於全部貓的圖片,它的數字編碼爲 0,One-hot 編碼爲[1,0,0,0];對於全部狗的圖片,它的數字編碼爲 1,One-hot 編碼爲[0,1,0,0],以此類推。
手寫數字圖片的總類別數有 10 種,即輸出節點數
= 10,那麼對於某個樣本,假設它屬於類別𝑖,即圖片的中數字爲𝑖,只須要一個長度爲 10 的向量𝐲,向量𝐲的索引號爲𝑖的元素設置爲 1,其餘位爲 0。好比圖片 0 的 One-hot 編碼爲[1,0,0,… ,0],圖片 2 的 Onehot 編碼爲[0,0,1,… ,0],圖片 9 的One-hot 編碼爲[0,0,0, … ,1]。One-hot 編碼是很是稀疏(Sparse)的,相對於數字編碼來講,佔用較多的存儲空間,因此通常在存儲時仍是採用數字編碼,在計算時,根據須要來把數字編碼轉換成 One-hot 編碼,經過 tf.one_hot
便可實現
y=tf.constant([0,1,2,3])#數字編碼 y=tf.one_hot(y,depth=10)#one-hot編碼 print(y) tf.Tensor( [[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 1. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]], shape=(4, 10), dtype=float32)
如今咱們回到手寫數字圖片識別任務,輸入是一張打平後的圖片向量𝒙 ∈ R
,輸出
是一個長度爲 10 的向量 ∈
,圖片的真實標籤 y 通過 one-hot 編碼後變成長度爲 10 的非 0 即 1 的稀疏向量
。預測模型採用多輸入、多輸出的線性模型 = 𝑊𝑻𝒙 + 𝒃,其中模型的輸出記爲輸入的預測值 ,咱們但願 越接近真實標籤𝒚越好。咱們通常把輸入通過一次(線性)變換叫作一層網絡。
對於分類問題來講,咱們的目標是最大化某個性能指標,好比準確度 acc,可是把準確度當作損失函數去優化時,會發現
是不可導的,沒法利用梯度降低算法優化網絡參數𝜃。通常的作法是,設立一個平滑可導的代理目標函數,好比優化模型的輸出 與 Onehot 編碼後的真實標籤𝒚之間的距離(Distance),經過優化代理目標函數獲得的模型,通常在測試性能上也能有良好的表現。所以,相對迴歸問題而言,分類問題的優化目標函數和評價目標函數是不一致的。模型的訓練目標是經過優化損失函數ℒ來找到最優數值解W∗, 𝒃∗:
對於分類問題的偏差計算來講,更常見的是採用****交叉熵(Cross entropy)損失函數,而不是採用迴歸問題中介紹的均方差損失函數。咱們將在後續章節介紹交叉熵損失函數,這裏仍是採用 MSE 損失函數來求解手寫數字識別問題。對於𝑁個樣本的均方差損失函數能夠表達爲:
如今咱們只須要採用梯度降低算法來優化損失函數獲得W, 𝒃的最優解,利用求得的模型去預測未知的手寫數字圖片𝒙 ∈ 𝔻𝑡𝑒𝑠𝑡。
按照上面的方案,手寫數字圖片識別問題真的獲得了完美的解決嗎?目前來看,至少存在兩大問題:
❑ 線性模型 線性模型是機器學習中間最簡單的數學模型之一,參數量少,計算簡單,可是隻能表達線性關係。即便是簡單如數字圖片識別任務,它也是屬於圖片識別的範疇,人類目前對於複雜大腦的感知和決策的研究尚處於初步探索階段,若是隻使用一個簡單的線性模型去逼近複雜的人腦圖片識別模型,很顯然不能勝任
❑ 表達能力 上面的解決方案只使用了少許神經元組成的一層網絡模型,相對於人腦中千億級別的神經元互聯結構,它的表達能力明顯偏弱,其中表達能力體現爲逼近複雜分佈的能力
模型的表達能力與數據模態之間的示意圖如圖 3.7 所示,圖中繪製了帶觀測偏差的採樣點的分佈,人爲推測數據的真實分佈多是某 2 次拋物線模型。如圖 3.7(a)所示,若是使用表達能力偏弱的線性模型去學習,很難學習到比較好的模型;若是使用合適的多項式函數模型去學習,則能學到比較合適的模型,如圖 3.7(b);但模型過於複雜,表達能力過強時,則頗有可能會過擬合,傷害模型的泛化能力,如圖 3.7©。
目前咱們所採用的多神經元模型還是線性模型,表達能力偏弱,接下來咱們嘗試解決這 2個問題。
既然線性模型不可行,咱們能夠給線性模型嵌套一個非線性函數,便可將其轉換爲非線性模型。我們把這個非線性函數稱爲激活函數(Activation function),用𝜎表示:
這裏的𝜎表明了某個具體的非線性激活函數,好比 Sigmoid 函數(圖 3.8(a)),ReLU 函數(圖3.8(b))。
ReLU 函數很是簡單,僅僅是在𝑦 = 𝑥在基礎上面截去了𝑥 < 0的部分,能夠直觀地理解爲 ReLU 函數僅僅保留正的輸入部份,清零負的輸入。雖然簡單,ReLU 函數卻有優良的非線性特性,並且梯度計算簡單,訓練穩定,是深度學習模型使用最普遍的激活函數之一。咱們這裏經過嵌套 ReLU 函數將模型轉換爲非線性模型:
針對於模型的表達能力偏弱的問題,能夠經過重複堆疊屢次變換來增長其表達能力:
𝒉𝟏 = 𝑅𝑒𝐿𝑈(𝑾𝟏𝒙 + 𝒃𝟏)
𝒉𝟐 = 𝑅𝑒𝐿𝑈(𝑾𝟐𝒉𝟏 + 𝒃𝟐)
= 𝑾𝟑𝒉𝟐 + 𝒃𝟑
把第一層神經元的輸出值𝒉𝟏做爲第二層神經元模型的輸入,把第二層神經元的輸出𝒉𝟐做爲第三層神經元的輸入,最後一層神經元的輸出做爲模型的輸出 。
從網絡結構上看,如圖 3.9 所示,函數的嵌套表現爲網絡層的先後相連,每堆疊一個(非)線性環節,網絡層數增長一層。咱們把數據節點所在的層叫作輸入層,每個非線性模塊的輸出𝒉𝒊連同它的網絡層參數𝑾𝒊和𝒃𝒊稱爲一層網絡層,特別地,對於網絡中間的層,叫作隱藏層,最後一層叫作輸出層。這種由大量神經元模型鏈接造成的網絡結構稱爲(前饋)神經網絡(Neural Network)。
如今咱們的網絡模型已經升級爲爲 3 層的神經網絡,具備較好的非線性表達能力,接下來咱們討論怎麼優化網絡。
對於僅一層的網絡模型,如線性迴歸的模型,咱們能夠直接推導出 和 的表達式,而後直接計算每一步的梯度,根據梯度更新法則循環更新𝑤, 𝑏參數便可。可是,當網絡層數增長、數據特徵長度增大、添加複雜的非線性函數以後,模型的表達式將變得很是複雜,很難手動推導出梯度的計算公式;並且一旦網絡結構發生變更,網絡的函數模型也隨之發生改變,依賴人工去計算梯度的方式顯然不可行。
這個時候就是深度學習框架發明的意義所在,藉助於自動求導(Autograd
)技術,深度學習框架在計算函數的損失函數的過程當中,會記錄模型的計算圖模型,並自動完成任意參數𝜃的偏導分
的計算,用戶只須要搭建出網絡結構,梯度將自動完成計算和更新,使用起來很是便捷高效。
本節咱們將在未介紹 TensorFlow 的狀況下,先帶你們體驗一下神經網絡的樂趣。本節的主要目的並非教會每一個細節,而是讓讀者對神經網絡算法有全面、直觀的感覺,爲接下來介紹 TensorFlow 基礎和深度學習理論打下基礎。讓咱們開始體驗神奇的圖片識別算法吧!
網絡搭建 對於第一層模型來講,他接受的輸入𝒙 ∈ ,輸出𝒉𝟏 ∈ 設計爲長度爲 256的向量,咱們不須要顯式地編寫𝒉𝟏 = 𝑅𝑒𝐿𝑈(𝑾𝟏𝒙 + 𝒃𝟏)的計算邏輯,在 TensorFlow 中經過一行代碼便可實現:
layers.Dense(256, activation='relu')
使用 TensorFlow 的 Sequential
容器能夠很是方便地搭建多層的網絡。對於 3 層網絡,咱們能夠經過
keras.sequential([ layers.Dense(256,activation='relu'), layers.Dense(128,activation='relu'), layers.Dense(10)])
快速完成 3 層網絡的搭建,第 1 層的輸出節點數設計爲 256,第 2 層設計爲 128,輸出層節點數設計爲 10。直接調用這個模型對象 model(x)就能夠返回模型最後一層的輸出 。
模型訓練 獲得模型輸出 後,經過 MSE 損失函數計算當前的偏差ℒ:
with tf.GradientTape() as tape:#構建梯度記錄環境 #打平,[b,28,28] =>[b,784] x=tf.reshape(x,(-1,28*28)) #step1. 獲得模型輸出 output # [b,784] =>[b,10] out=model(x)
再利用 TensorFlow 提供的自動求導函數 tape.gradient(loss, model.trainable_variables)
求出模型中全部的梯度信息
, 𝜃 ∈ {𝑊1, 𝒃𝟏,𝑊2, 𝒃𝟐,𝑊3, 𝒃𝟑}:
# Step3. 計算參數的梯度 w1, w2, w3, b1, b2, b3 grads = tape.gradient(loss, model.trainable_variables)
計算得到的梯度結果使用 grads 變量保存。再使用 optimizers
對象自動按着梯度更新法則
去更新模型的參數𝜃。
grads = tape.gradient(loss, model.trainable_variables) # w' = w - lr * grad,更新網絡參數 optimizer.apply_gradients(zip(grads, model.trainable_variables))
循環迭代屢次後,就能夠利用學好的模型𝑓𝜃去預測未知的圖片的類別機率分佈。模型的測試部分暫不討論。
手寫數字圖片 MNIST 數據集的訓練偏差曲線如圖 3.10 所示,因爲 3 層的神經網絡表達能力較強,手寫數字圖片識別任務簡單,偏差值能夠較快速、穩定地降低,其中對數據集的全部圖片迭代一遍叫作一個 Epoch,咱們能夠在間隔數個 Epoch 後測試模型的準確率等指標,方便監控模型的訓練效果。
本章咱們經過將一層的線性迴歸模型類推到分類問題,提出了表達能力更強的三層非線性神經網絡,去解決手寫數字圖片識別的問題。本章的內容以感覺爲主,學習完你們其實已經瞭解了(淺層)的神經網絡算法,接下來咱們將學習 TensorFlow 的一些基礎知識,爲後續正式學習、實現深度學習算法打下夯實的基石。
TensorFlow 是一個面向於深度學習算法的科學計算庫,內部數據保存在張量(Tensor)對象上,全部的運算操做(Operation, OP)也都是基於張量對象進行。複雜的神經網絡算法本質上就是各類張量相乘、相加等基本運算操做的組合,在深刻學習深度學習算法以前,熟練掌握 TensorFlow 張量的基礎操做方法十分重要。
首先咱們來介紹 TensorFlow 中的基本數據類型,它包含了數值型、字符串型和布爾型。
數值類型的張量是 TensorFlow 的主要數據載體,分爲:
❑ 標量(Scalar) 單個的實數,如 1.2, 3.4 等,維度數(Dimension,也叫秩)爲 0,shape 爲[]
❑ 向量(Vector) n 個實數的有序集合,經過中括號包裹,如[1.2],[1.2,3.4]等,維度數爲1,長度不定,shape 爲[𝑛]
❑ 矩陣(Matrix) n 行 m 列實數的有序集合,如[[1,2],[3,4]],也能夠寫成
維度數爲 2,每一個維度上的長度不定,shape 爲[𝑛, 𝑚]
❑ 張量(Tensor) 全部維度數dim > 2的數組統稱爲張量。張量的每一個維度也作軸(Axis),通常維度表明了具體的物理含義,好比 Shape 爲[2,32,32,3]的張量共有 4 維,若是表示圖片數據的話,每一個維度/軸表明的含義分別是:圖片數量、圖片高度、圖片寬度、圖片通道數,其中 2 表明了 2 張圖片,32 表明了高寬均爲 32,3 表明了 RGB 3 個通道。張量的維度數以及每一個維度所表明的具體物理含義須要由用戶自行定義
在 TensorFlow 中間,爲了表達方便,通常把標量、向量、矩陣也統稱爲張量,不做區分,須要根據張量的維度數和形狀自行判斷。
首先來看標量在 TensorFlow 是如何建立的:
a=1.2 aa=tf.constant(1.2)# 建立標量 print(type(a)) print(type(aa)) print(tf.is_tensor(aa)) output: <class 'float'> <class 'tensorflow.python.framework.ops.EagerTensor'> True
必須經過 TensorFlow 規定的方式去建立張量,而不能使用 Python 語言的標準變量建立方式。
經過 print(x)或 x 能夠打印出張量 x 的相關信息:
x = tf.constant([1,2.,3.3]) <tf.Tensor: id=165, shape=(3,), dtype=float32, numpy=array([1. , 2. , 3.3],dtype=float32)>
其中 id 是 TensorFlow 中內部索引對象的編號,shape 表示張量的形狀,dtype 表示張量的數值精度,張量 numpy()方法能夠返回 Numpy.array 類型的數據,方便導出數據到系統的其餘模塊:
Ix.numpy() array([1. , 2. , 3.3], dtype=float32)
與標量不一樣,向量的定義須經過 List 類型傳給 tf.constant()
。建立一個元素的向量
a = tf.constant([1.2]) a, a.shape (<tf.Tensor: id=8, shape=(1,), dtype=float32, numpy=array([1.2],dtype=float32)>,TensorShape([1]))
建立 2 個元素的向量:
a = tf.constant([1,2, 3.]) a, a.shape (<tf.Tensor: id=11, shape=(3,), dtype=float32, numpy=array([1., 2., 3.],dtype=float32)>,TensorShape([3]))
一樣的方法定義矩陣:
a = tf.constant([[1,2],[3,4]]) a, a.shape (<tf.Tensor: id=13, shape=(2, 2), dtype=int32, numpy=array([[1, 2],[3, 4]])>, TensorShape([2, 2]))
3 維張量能夠定義爲:
a = tf.constant([ [ [1,2],[3,4]],[[5,6],[7,8] ] ]) <tf.Tensor: id=15, shape=(2, 2, 2), dtype=int32, numpy=array([[[1, 2],[3, 4]],[[5, 6],[7, 8]]])>
除了豐富的數值類型外,TensorFlow 還支持字符串(String)類型的數據,例如在表示圖片數據時,能夠先記錄圖片的路徑,再經過預處理函數根據路徑讀取圖片張量。經過傳入字符串對象便可建立字符串類型的張量:
a = tf.constant('Hello, Deep Learning.') <tf.Tensor: id=17, shape=(), dtype=string, numpy=b'Hello, Deep Learning.'>
在 tf.strings
模塊中,提供了常見的字符串型的工具函數,如拼接 join(),長度 length(),切分 split()等等:
tf.strings.lower(a) <tf.Tensor: id=19, shape=(), dtype=string, numpy=b'hello, deep learning.'>
深度學習算法主要仍是以數值類型張量運算爲主,字符串類型的數據使用頻率較低,咱們不作過多闡述。
爲了方便表達比較運算操做的結果,TensorFlow 還支持布爾類型(Boolean
, bool)的張量。布爾類型的張量只須要傳入 Python 語言的布爾類型數據,轉換成 TensorFlow 內部布爾型便可:
a = tf.constant(True) <tf.Tensor: id=22, shape=(), dtype=bool, numpy=True>
傳入布爾類型的向量:
a = tf.constant([True, False]) <tf.Tensor: id=25, shape=(2,), dtype=bool, numpy=array([ True, False])>
須要注意的是,TensorFlow 的布爾類型和 Python 語言的布爾類型並不對等,不能通用:
a = tf.constant(True) # 建立布爾張量 a == True False
對於數值類型的張量,能夠保持爲不一樣字節長度的精度,如浮點數 3.14 既能夠保存爲16-bit 長度,也能夠保存爲 32-bit 甚至 64-bit 的精度。Bit 位越長,精度越高,同時佔用的內存空間也就越大。經常使用的精度類型有 tf.int16, tf.int32, tf.int64, tf.float16, tf.float32,tf.float64
,其中 tf.float64 即爲 tf.double。在建立張量時,能夠指定張量的保存精度:
tf.constant(123456789, dtype=tf.int16) tf.constant(123456789, dtype=tf.int32) <tf.Tensor: id=33, shape=(), dtype=int16, numpy=-13035> <tf.Tensor: id=35, shape=(), dtype=int32, numpy=123456789>
能夠看到,保存精度太低時,數據 123456789 發生了溢出,獲得了錯誤的結果,通常使用tf.int32, tf.int64 精度。對於浮點數,高精度的張量能夠表示更精準的數據,例如採用tf.float32 精度保存𝜋時:
import numpy as np np.pi tf.constant(np.pi, dtype=tf.float32) <tf.Tensor: id=29, shape=(), dtype=float32, numpy=3.1415927>
若是採用 tf.float32 精度保存𝜋,則能得到更高的精度:
tf.constant(np.pi, dtype=tf.float64) <tf.Tensor: id=31, shape=(), dtype=float64, numpy=3.141592653589793>
對於大部分深度學習算法,通常使用 tf.int32, tf.float32 可知足運算精度要求,部分對精度要求較高的算法,如強化學習,能夠選擇使用 tf.int64, tf.float64 精度保存張量。
經過訪問張量的 dtype 成員屬性能夠判斷張量的保存精度:
print('before:',a.dtype) if a.dtype != tf.float32: a = tf.cast(a,tf.float32) # 轉換精度 print('after :',a.dtype) before: <dtype: 'float16'> after : <dtype: 'float32'>
對於某些只能處理指定精度類型的運算操做,須要提早檢驗輸入張量的精度類型,並將不符合要求的張量進行類型轉換。
系統的每一個模塊使用的數據類型、數值精度可能各不相同,對於不符合要求的張量的類型及精度,須要經過 tf.cast
函數進行轉換:
a = tf.constant(np.pi, dtype=tf.float16) tf.cast(a, tf.double) <tf.Tensor: id=44, shape=(), dtype=float64, numpy=3.140625>
進行類型轉換時,須要保證轉換操做的合法性,例如將高精度的張量轉換爲低精度的張量
時,可能發生數據溢出隱患:
a = tf.constant(123456789, dtype=tf.int32) tf.cast(a, tf.int16) <tf.Tensor: id=38, shape=(), dtype=int16, numpy=-13035>
布爾型與整形之間相互轉換也是合法的,是比較常見的操做:
a = tf.constant([True, False]) tf.cast(a, tf.int32) <tf.Tensor: id=48, shape=(2,), dtype=int32, numpy=array([1, 0])>
通常默認 0 表示 False,1 表示 True,在 TensorFlow 中,將非 0 數字都視爲 True:
a = tf.constant([-1, 0, 1, 2]) tf.cast(a, tf.bool) <tf.Tensor: id=51, shape=(4,), dtype=bool, numpy=array([ True, False, True,True])>
爲了區分須要計算梯度信息的張量與不須要計算梯度信息的張量,TensorFlow 增長了一種專門的數據類型來支持梯度信息的記錄:tf.Variable
。tf.Variable
類型在普通的張量類型基礎上添加了 name
,trainable
等屬性來支持計算圖的構建。因爲梯度運算會消耗大量的計算資源,並且會自動更新相關參數,對於不須要的優化的張量,如神經網絡的輸入 X,不須要經過 tf.Variable 封裝;相反,對於須要計算梯度並優化的張量,如神經網絡層的W和𝒃,須要經過 tf.Variable 包裹以便 TensorFlow 跟蹤相關梯度信息。
經過 tf.Variable()函數能夠將普通張量轉換爲待優化張量:
a = tf.constant([-1, 0, 1, 2]) aa = tf.Variable(a) aa.name, aa.trainable ('Variable:0', True)
其中張量的 name 和 trainable 屬性是 Variable 特有的屬性,name 屬性用於命名計算圖中的變量,這套命名體系是 TensorFlow 內部維護的,通常不須要用戶關注 name 屬性;trainable表徵當前張量是否須要被優化,建立 Variable 對象是默認啓用優化標誌,能夠設置trainable=False 來設置張量不須要優化。
除了經過普通張量方式建立 Variable,也能夠直接建立:
a = tf.Variable([[1,2],[3,4]]) <tf.Variable 'Variable:0' shape=(2, 2) dtype=int32, numpy=array([[1, 2],[3, 4]])>
待優化張量可看作普通張量的特殊類型,普通張量也能夠經過 GradientTape.watch()
方法臨時加入跟蹤梯度信息的列表。
在 TensorFlow 中,能夠經過多種方式建立張量,如從 Python List 對象建立,從Numpy 數組建立,或者建立採樣自某種已知分佈的張量等。
Numpy Array 數組和 Python List 是 Python 程序中間很是重要的數據載體容器,不少數據都是經過 Python 語言將數據加載至 Array 或者 List 容器,再轉換到 Tensor 類型,經過TensorFlow 運算處理後導出到 Array 或者 List 容器,方便其餘模塊調用。
經過 tf.convert_to_tensor
能夠建立新 Tensor,並將保存在 Python List 對象或者 Numpy Array 對象中的數據導入到新 Tensor 中:
tf.convert_to_tensor([1,2.]) <tf.Tensor: id=86, shape=(2,), dtype=float32, numpy=array([1., 2.],dtype=float32)> tf.convert_to_tensor(np.array([[1,2.],[3,4]])) <tf.Tensor: id=88, shape=(2, 2), dtype=float64, numpy=array([[1., 2.], [3., 4.]])>
須要注意的是,Numpy 中浮點數數組默認使用 64-Bit 精度保存數據,轉換到 Tensor 類型時精度爲 tf.float64,能夠在須要的時候轉換爲 tf.float32 類型。
實際上,tf.constant()
和 tf.convert_to_tensor()
都可以自動的把 Numpy 數組或者 PythonList 數據類型轉化爲 Tensor 類型,這兩個 API 命名來自 TensorFlow 1.x 的命名習慣,在TensorFlow 2 中函數的名字並非很貼切,使用其一便可.
將張量建立爲全 0 或者全 1 數據是很是常見的張量初始化手段。考慮線性變換𝒚 = 𝑊𝒙 + 𝒃,將權值矩陣 W 初始化爲全 1 矩陣,偏置 b 初始化爲全 0 向量,此時線性變化層輸出𝒚 = 𝒙,是一種比較好的層初始化狀態。
經過 tf.zeros()
和 tf.ones()
便可建立任意形狀全 0 或全 1 的張量。例如,建立爲 0 和爲 1 的標量張量:
tf.zeros([]),tf.ones([]) (<tf.Tensor: id=90, shape=(), dtype=float32, numpy=0.0>, <tf.Tensor: id=91, shape=(), dtype=float32, numpy=1.0>)
建立全 0 和全 1 的向量:
tf.zeros([1]),tf.ones([1]) (<tf.Tensor: id=96, shape=(1,), dtype=float32, numpy=array([0.],dtype=float32)>, <tf.Tensor: id=99, shape=(1,), dtype=float32, numpy=array([1.],dtype=float32)>)
建立全 0 的矩陣:
tf.zeros([2,2]) <tf.Tensor: id=104, shape=(2, 2), dtype=float32, numpy=array([[0., 0.],[0., 0.]],dtype=float32)>
建立全 1 的矩陣:
tf.ones([3,2]) <tf.Tensor: id=108, shape=(3, 2), dtype=float32, numpy=array([[1., 1.],[1., 1.],[1., 1.]], dtype=float32)>
經過 tf.zeros_like
, tf.ones_like
能夠方便地新建與某個張量 shape 一致,內容全 0 或全 1
的張量。例如,建立與張量 a 形狀同樣的全 0 張量:
a = tf.ones([2,3]) tf.zeros_like(a) <tf.Tensor: id=113, shape=(2, 3), dtype=float32, numpy=array([[0., 0., 0.],[0., 0., 0.]], dtype=float32)>
建立與張量 a 形狀同樣的全 1 張量:
a = tf.zeros([3,2]) tf.ones_like(a) <tf.Tensor: id=120, shape=(3, 2), dtype=float32, numpy=array([[1., 1.],[1., 1.],[1., 1.]], dtype=float32)>
tf.*_like 是一個便捷函數,能夠經過 tf.zeros(a.shape)等方式實現。
除了初始化爲全 0,或全 1 的張量以外,有時也須要所有初始化爲某個自定義數值的張量,好比將張量的數值所有初始化爲-1 等。
經過tf.fill(shape, value)
能夠建立全爲自定義數值 value 的張量。例如,建立元素爲-1
的標量:
tf.fill([], -1) <tf.Tensor: id=124, shape=(), dtype=int32, numpy=-1>
建立全部元素爲-1 的向量:
tf.fill([1], -1) <tf.Tensor: id=128, shape=(1,), dtype=int32, numpy=array([-1])>
建立全部元素爲 99 的矩陣:
tf.fill([2,2], 99) <tf.Tensor: id=136, shape=(2, 2), dtype=int32, numpy=array([[99, 99],[99, 99]])>
正態分佈(Normal Distribution,或 Gaussian Distribution)和均勻分佈(UniformDistribution)是最多見的分佈之一,建立採樣自這 2 種分佈的張量很是有用,好比在卷積神經網絡中,卷積核張量 W 初始化爲正態分佈有利於網絡的訓練;在對抗生成網絡中,隱藏變量 z 通常採樣自均勻分佈。
經過 tf.random.normal(shape, mean=0.0, stddev=1.0)
能夠建立形狀爲 shape,均值爲mean,標準差爲 stddev 的正態分佈𝒩(𝑚𝑒𝑎𝑛, 𝑠𝑡𝑑𝑑𝑒𝑣2)。例如,建立均值爲 0,標準差爲 1的正太分佈:
tf.random.normal([2,2]) <tf.Tensor: id=143, shape=(2, 2), dtype=float32, numpy=array([[-0.4307344 , 0.44147003],[-0.6563149 , -0.30100572]], dtype=float32)>
建立均值爲 1,標準差爲 2 的正太分佈:
tf.random.normal([2,2], mean=1,stddev=2) <tf.Tensor: id=150, shape=(2, 2), dtype=float32, numpy=array([[-2.2687864, -0.7248812],[ 1.2752185, 2.8625617]], dtype=float32)>
經過 tf.random.uniform(shape, minval=0, maxval=None, dtype=tf.float32)
能夠建立採樣自
[𝑚𝑖𝑛𝑣𝑎𝑙, 𝑚𝑎𝑥𝑣𝑎𝑙]區間的均勻分佈的張量。例如建立採樣自區間[0,1],shape 爲[2,2]的矩
陣:
tf.random.uniform([2,2]) <tf.Tensor: id=158, shape=(2, 2), dtype=float32, numpy=array([[0.65483284, 0.63064325],[0.008816 , 0.81437767]], dtype=float32)>
建立採樣自區間[0,10],shape 爲[2,2]的矩陣:
tf.random.uniform([2,2],maxval=10) <tf.Tensor: id=166, shape=(2, 2), dtype=float32, numpy=array([[4.541913 , 0.26521802],[2.578913 , 5.126876 ]], dtype=float32)>
若是須要均勻採樣整形類型的數據,必須指定採樣區間的最大值 maxval 參數,同時制定數據類型爲 tf.int*型:
tf.random.uniform([2,2],maxval=100,dtype=tf.int32) <tf.Tensor: id=171, shape=(2, 2), dtype=int32, numpy=array([[61, 21],[95, 75]])>
在循環計算或者對張量進行索引時,常常須要建立一段連續的整形序列,能夠經過tf.range()
函數實現。tf.range(limit, delta=1)
能夠建立[0,𝑙𝑖𝑚𝑖𝑡)之間,步長爲 delta 的整形序列,不包含 limit 自己。例如,建立 0~9,步長爲 1 的整形序列:
tf.range(10) <tf.Tensor: id=180, shape=(10,), dtype=int32, numpy=array([0, 1, 2, 3, 4, 5,6, 7, 8, 9])>
建立 0~9,步長爲 2 的整形序列:
tf.range(10,delta=2) <tf.Tensor: id=185, shape=(5,), dtype=int32, numpy=array([0, 2, 4, 6, 8])>
經過 tf.range(start, limit, delta=1)
能夠建立[𝑠𝑡𝑎𝑟𝑡, 𝑙𝑖𝑚𝑖𝑡),步長爲 delta 的序列,不包含 limit
自己:
tf.range(1,10,delta=2) <tf.Tensor: id=190, shape=(5,), dtype=int32, numpy=array([1, 3, 5, 7, 9])>
在介紹完張量的相關屬性和建立方式後,咱們將介紹每種維度下張量的典型應用,讓讀者在看到每種張量時,可以直觀地聯想到它主要的物理意義和用途,對後續張量的維度變換等一系列抽象操做的學習打下基礎。本節在介紹典型應用時不可避免地會說起後續將要學習的網絡模型或算法,學習時不須要徹底理解,有初步印象便可。
在 TensorFlow 中,標量最容易理解,它就是一個簡單的數字,維度數爲 0,shape 爲[]。標量的典型用途之一是偏差值的表示、各類測量指標的表示,好比準確度(Accuracy,acc),精度(Precision)和召回率(Recall)等。
考慮某個模型的訓練曲線,如圖 4.1 所示,橫座標爲訓練 Batch 步數 Step,縱座標分別爲偏差變化趨勢(圖 4.1(a))和準確度變化趨勢曲線(圖 4.1(b)),其中損失值 loss 和準確度均由張量計算產生,類型爲標量。
以均方差誤函數爲例,通過 tf.keras.losses.mse
(或 tf.keras.losses.MSE
)返回每一個樣本上的偏差值,最後取偏差的均值做爲當前 batch 的偏差,它是一個標量:
out = tf.random.uniform([4,10]) #隨機模擬網絡輸出 y = tf.constant([2,3,2,0]) # 隨機構造樣本真實標籤 y = tf.one_hot(y, depth=10) # one-hot 編碼 loss = tf.keras.losses.mse(y, out) # 計算每一個樣本的 MSE loss = tf.reduce_mean(loss) # 平均 MSE print(loss) tf.Tensor(0.19950335, shape=(), dtype=float32)
向量是一種很是常見的數據載體,如在全鏈接層和卷積神經網絡層中,偏置張量𝒃就使用向量來表示。如圖 4.2 所示,每一個全鏈接層的輸出節點都添加了一個偏置值,把全部輸出節點的偏置表示成向量形式:𝒃 = [𝑏1, 𝑏2]
考慮 2 個輸出節點的網絡層,咱們建立長度爲 2 的偏置向量𝒃,並累加在每一個輸出節點上:
In [42]:
# z=wx,模擬得到激活函數的輸入 z z = tf.random.normal([4,2]) b = tf.zeros([2]) # 模擬偏置向量 z = z + b # 累加偏置 <tf.Tensor: id=245, shape=(4, 2), dtype=float32, numpy= array([[ 0.6941646 , 0.4764454 ], [-0.34862405, -0.26460952], [ 1.5081744 , -0.6493869 ], [-0.26224667, -0.78742725]], dtype=float32)>
注意到這裏 shape 爲[4,2]的𝒛和 shape 爲[2]的𝒃張量能夠直接相加,這是爲何呢?讓咱們在 Broadcasting 一節爲你們揭祕。
經過高層接口類 Dense()
方式建立的網絡層,張量 W 和𝒃存儲在類的內部,由類自動建立並管理。能夠經過全鏈接層的 bias 成員變量查看偏置變量𝒃,例如建立輸入節點數爲 4,輸出節點數爲 3 的線性層網絡,那麼它的偏置向量 b 的長度應爲 3:
fc = layers.Dense(3) # 建立一層 Wx+b,輸出節點爲 3 # 經過 build 函數建立 W,b 張量,輸入節點爲 4 fc.build(input_shape=(2,4)) fc.bias # 查看偏置 <tf.Variable 'bias:0' shape=(3,) dtype=float32, numpy=array([0., 0., 0.], dtype=float32)>
能夠看到,類的偏置成員 bias 初始化爲全 0,這也是偏置𝒃的默認初始化方案。
矩陣也是很是常見的張量類型,好比全鏈接層的批量輸入𝑋 = [𝑏, ],其中𝑏表示輸入樣本的個數,即 batch size, 表示輸入特徵的長度。好比特徵長度爲 4,一共包含 2 個樣本的輸入能夠表示爲矩陣:
x = tf.random.normal([2,4])
令全鏈接層的輸出節點數爲 3,則它的權值張量 W 的 shape 爲[4,3]:
w=tf.ones([4,3]) b=tf.zeros([3]) o =tf.matmul(x, w)+b print(o) <tf.Tensor: id=291, shape=(2, 3), dtype=float32, numpy= array([[ 2.3506963, 2.3506963, 2.3506963], [-1.1724043, -1.1724043, -1.1724043]], dtype=float32)>
其中 X,W 張量均是矩陣。x*w+b 網絡層稱爲線性層,在 TensorFlow 中能夠經過 Dense類直接實現,Dense 層也稱爲全鏈接層。咱們經過 Dense 類建立輸入 4 個節點,輸出 3 個節點的網絡層,能夠經過全鏈接層的 kernel 成員名查看其權值矩陣 W:
fc = layers.Dense(3) # 定義全鏈接層的輸出節點爲 3 fc.build(input_shape=(2,4)) # 定義全鏈接層的輸入節點爲 4 fc.kernel <tf.Variable 'kernel:0' shape=(4, 3) dtype=float32, numpy= array([[ 0.06468129, -0.5146048 , -0.12036425], [ 0.71618867, -0.01442951, -0.5891943 ], [-0.03011459, 0.578704 , 0.7245046 ], [ 0.73894167, -0.21171576, 0.4820758 ]], dtype=float32)>
三維的張量一個典型應用是表示序列信號,它的格式是
𝑋 = [𝑏, 𝑠𝑒𝑞𝑢𝑒𝑛𝑐𝑒 𝑙𝑒𝑛, 𝑓𝑒𝑎𝑡𝑢𝑟𝑒 𝑙𝑒𝑛]
其中𝑏表示序列信號的數量,sequence len 表示序列信號在時間維度上的採樣點數,featurelen 表示每一個點的特徵長度。
考慮天然語言處理中句子的表示,如評價句子的是否爲正面情緒的情感分類任務網絡,如圖 4.3 所示。爲了可以方便字符串被神經網絡處理,通常將單詞經過嵌入層(Embedding Layer)編碼爲固定長度的向量,好比「a」編碼爲某個長度 3 的向量,那麼 2 個等長(單詞數爲 5)的句子序列能夠表示爲 shape 爲[2,5,3]的 3 維張量,其中 2 表示句子個數,5 表示單詞數量,3 表示單詞向量的長度:
# 自動加載 IMDB 電影評價數據集 (x_train,y_train),(x_test,y_test)=keras.datasets.imdb.load_data(num_words=10000) # 將句子填充、截斷爲等長 80 個單詞的句子 x_train = keras.preprocessing.sequence.pad_sequences(x_train,maxlen=80) x_train.shape
能夠看到 x_train 張量的 shape 爲[25000,80],其中 25000 表示句子個數,80 表示每一個句子共 80 個單詞,每一個單詞使用數字編碼方式。咱們經過 layers.Embedding
層將數字編碼的單詞轉換爲長度爲 100 個詞向量:
# 建立詞向量 Embedding 層類 embedding=layers.Embedding(10000, 100) # 將數字編碼的單詞轉換爲詞向量 out = embedding(x_train) out.shape TensorShape([25000, 80, 100])
能夠看到,通過 Embedding 層編碼後,句子張量的 shape 變爲[25000,80,100],其中 100 表示每一個單詞編碼爲長度 100 的向量。
對於特徵長度爲 1 的序列信號,好比商品價格在 60 天內的變化曲線,只須要一個標量便可表示商品的價格,所以 2 件商品的價格變化趨勢能夠使用 shape 爲[2,60]的張量表示。爲了方便統一格式,也將價格變化趨勢表達爲 shape 爲 [2,60,1]的張量,其中的 1 表示特徵長度爲 1。
咱們這裏只討論 3/4 維張量,大於 4 維的張量通常應用的比較少,如在元學習(metalearning)中會採用 5 維的張量表示方法,理解方法與 3/4 維張量相似。
4維張量在卷積神經網絡中應用的很是普遍,它用於保存特徵圖(Feature maps)數據,格式通常定義爲
其中𝑏表示輸入的數量,h/w分佈表示特徵圖的高寬,𝑐表示特徵圖的通道數,部分深度學習框架也會使用[𝑏, 𝑐, ℎ, ]格式的特徵圖張量,例如 PyTorch。圖片數據是特徵圖的一種,對於含有 RGB 3 個通道的彩色圖片,每張圖片包含了 h 行 w 列像素點,每一個點須要 3 個數值表示 RGB 通道的顏色強度,所以一張圖片能夠表示爲[h,w, 3]。如圖 4.4 所示,最上層的圖片表示原圖,它包含了下面 3 個通道的強度信息。
神經網絡中通常並行計算多個輸入以提升計算效率,故𝑏張圖片的張量可表示爲[𝑏, ℎ, w, 3]。
# 建立 32x32 的彩色圖片輸入,個數爲 4 x = tf.random.normal([4,32,32,3]) # 建立卷積神經網絡 layer = layers.Conv2D(16,kernel_size=3) out = layer(x) # 前向計算 out.shape # 輸出大小 TensorShape([4, 30, 30, 16])
其中卷積核張量也是 4 維張量,能夠經過 kernel 成員變量訪問:
layer.kernel.shape Out[49]: TensorShape([3, 3, 3, 16])
經過索引與切片操做能夠提取張量的部分數據,使用頻率很是高
在 TensorFlow 中,支持基本的[𝑖][𝑗]…標準索引方式,也支持經過逗號分隔索引號的索引方式。考慮輸入 X 爲 4 張 32x32 大小的彩色圖片(爲了方便演示,大部分張量都使用隨機分佈模擬產生,後文同),shape 爲[4,32,32,3],首先建立張量:
x = tf.random.normal([4,32,32,3])
接下來咱們使用索引方式讀取張量的部分數據。
❑ 取第 1 張圖片的數據:
x[0] <tf.Tensor: id=379, shape=(32, 32, 3), dtype=float32, numpy= array([[[ 1.3005302 , 1.5301839 , -0.32005513], [-1.3020388 , 1.7837263 , -1.0747638 ], ... [-1.1092019 , -1.045254 , -0.4980363 ], [-0.9099222 , 0.3947732 , -0.10433522]]], dtype=float32)>
❑ 取第 1 張圖片的第 2 行:
x[0][1] <tf.Tensor: id=388, shape=(32, 3), dtype=float32, numpy= array([[ 4.2904025e-01, 1.0574218e+00, 3.1540772e-01], [ 1.5800388e+00, -8.1637271e-02, 6.3147342e-01], ..., [ 2.8893018e-01, 5.8003378e-01, -1.1444757e+00], [ 9.6100050e-01, -1.0985689e+00, 1.0827581e+00]], dtype=float32)>
❑ 取第 1 張圖片,第 2 行,第 3 列的像素:
In [53]: x[0][1][2] Out[53]: <tf.Tensor: id=401, shape=(3,), dtype=float32, numpy=array([-0.55954427, 0.14497331, 0.46424514], dtype=float32)>
❑ 取第 3 張圖片,第 2 行,第 1 列的像素,B 通道(第 2 個通道)顏色強度值:
x[2][1][0][1] Out[54]: <tf.Tensor: id=418, shape=(), dtype=float32, numpy=-0.84922135>
當張量的維度數較高時,使用[𝑖][𝑗]. . .[𝑘]的方式書寫不方便,能夠採用[𝑖,𝑗, … , 𝑘]的方式索引,它們是等價的。
❑ 取第 2 張圖片,第 10 行,第 3 列:
x[1,9,2] <tf.Tensor: id=436, shape=(3,), dtype=float32, numpy=array([ 1.7487534 , - 0.41491988, -0.2944692 ], dtype=float32)>
經過𝑠𝑡𝑎𝑟𝑡: 𝑒𝑛𝑑: 𝑠𝑡𝑒𝑝
切片方式能夠方便地提取一段數據,其中 start 爲開始讀取位置的索引,end 爲結束讀取位置的索引(不包含 end 位),step 爲讀取步長。
以 shape 爲[4,32,32,3]的圖片張量爲例:
❑ 讀取第 2和第3 張圖片:
x[1:3] <tf.Tensor: id=441, shape=(2, 32, 32, 3), dtype=float32, numpy= array([[[[ 0.6920027 , 0.18658352, 0.0568333 ], [ 0.31422952, 0.75933754, 0.26853144], [ 2.7898 , -0.4284912 , -0.26247284],...
start: end: step切片方式有不少簡寫方式,其中 start、end、step 3 個參數能夠根據須要選擇性地省略,所有省略時即::,表示從最開始讀取到最末尾,步長爲 1,即不跳過任何元素。如 x[0,::]表示讀取第 1 張圖片的全部行,其中::表示在行維度上讀取全部行,它等於x[0]的寫法:
x[0,::] <tf.Tensor: id=446, shape=(32, 32, 3), dtype=float32, numpy= array([[[ 1.3005302 , 1.5301839 , -0.32005513], [-1.3020388 , 1.7837263 , -1.0747638 ], [-1.1230233 , -0.35004002, 0.01514002],
爲了更加簡潔,::能夠簡寫爲單個冒號:,如
x[:,0:28:2,0:28:2,:] <tf.Tensor: id=451, shape=(4, 14, 14, 3), dtype=float32, numpy= array([[[[ 1.3005302 , 1.5301839 , -0.32005513], [-1.1230233 , -0.35004002, 0.01514002], [ 1.3474811 , 0.639334 , -1.0826371 ],
表示取全部圖片,隔行採樣,隔列採樣,全部通道信息,至關於在圖片的高寬各縮放至原來的 50%。
咱們來總結start: end: step切片的簡寫方式,其中從第一個元素讀取時 start 能夠省略,即 start=0 是能夠省略,取到最後一個元素時 end 能夠省略,步長爲 1 時 step 能夠省略,簡寫方式總結如表格 4.1:
特別地,step 能夠爲負數,考慮最特殊的一種例子,step = −1時,start: end: −1表示從 start 開始,逆序讀取至 end 結束(不包含 end),索引號𝑒𝑛𝑑 ≤ 𝑠𝑡𝑎𝑟𝑡。考慮一 0~9 簡單序列,逆序取到第 1 號元素,不包含第 1 號:
x = tf.range(9) x[8:0:-1] <tf.Tensor: id=466, shape=(8,), dtype=int32, numpy=array([8, 7, 6, 5, 4, 3,2, 1])>
逆序取所有元素:
x[::-1] <tf.Tensor: id=471, shape=(9,), dtype=int32, numpy=array([8, 7, 6, 5, 4, 3,2, 1, 0])>
逆序間隔採樣:
x[::-2] <tf.Tensor: id=476, shape=(5,), dtype=int32, numpy=array([8, 6, 4, 2, 0])>
讀取每張圖片的全部通道,其中行按着逆序隔行採樣,列按着逆序隔行採樣:
x = tf.random.normal([4,32,32,3]) x[0,::-2,::-2] <tf.Tensor: id=487, shape=(16, 16, 3), dtype=float32, numpy= array([[[ 0.63320625, 0.0655185 , 0.19056146], [-1.0078577 , -0.61400175, 0.61183935], [ 0.9230892 , -0.6860094 , -0.01580668], …
當張量的維度數量較多時,不須要採樣的維度通常用單冒號:表示採樣全部元素,此時有可能出現大量的:出現。繼續考慮[4,32,32,3]的圖片張量,當須要讀取 G 通道上的數據時,前面全部維度所有提取,此時須要寫爲:
x[:,:,:,1] <tf.Tensor: id=492, shape=(4, 32, 32), dtype=float32, numpy= array([[[ 0.575703 , 0.11028383, -0.9950867 , ..., 0.38083118, -0.11705163, -0.13746642], ...
爲了不出現像𝑥[: , : , : ,1]這樣出現過多冒號的狀況,能夠使用⋯符號表示取多個維度上全部的數據,其中維度的數量需根據規則自動推斷:當切片方式出現⋯符號時,⋯符號左邊的維度將自動對齊到最左邊,⋯符號右邊的維度將自動對齊到最右邊,此時系統再自動推斷⋯符號表明的維度數量,它的切片方式總結如表格 4.2:
考慮以下例子:
❑ 讀取第 1-2 張圖片的 G/B 通道數據:
In [64]: x[0:2,...,1:] Out[64]: <tf.Tensor: id=497, shape=(2, 32, 32, 2), dtype=float32, numpy= array([[[[ 0.575703 , 0.8872789 ], [ 0.11028383, -0.27128693], [-0.9950867 , -1.7737272 ], ...
❑ 讀取最後 2 張圖片:
x[2:,...] <tf.Tensor: id=502, shape=(2, 32, 32, 3), dtype=float32, numpy= array([[[[-8.10753584e-01, 1.10984087e+00, 2.71821529e-01], [-6.10031188e-01, -6.47952318e-01, -4.07003373e-01], [ 4.62206364e-01, -1.03655539e-01, -1.18086267e+00], ...
❑ 讀取 R/G 通道數據:
x[...,:2] <tf.Tensor: id=507, shape=(4, 32, 32, 2), dtype=float32, numpy= array([[[[-1.26881 , 0.575703 ], [ 0.98697686, 0.11028383], [-0.66420585, -0.9950867 ], ...
張量的索引與切片方式多種多樣,尤爲是切片操做,初學者容易犯迷糊。但其實本質上切片操做只有𝑠𝑡𝑎𝑟𝑡: 𝑒𝑛𝑑: 𝑠𝑡𝑒𝑝這一種基本形式,經過這種基本形式有目的地省略掉默認參數,從而衍生出多種簡寫方法,這也是很好理解的。它衍生的簡寫形式熟練後一看就能推測出省略掉的信息,書寫起來也更方便快捷。因爲深度學習通常處理的維度數在 4 維之內,⋯操做符徹底能夠用:符號代替,所以理解了這些就會發現張量切片操做並不複雜.
在神經網絡運算過程當中,維度變換是最核心的張量操做,經過維度變換能夠將數據任意地切換形式,知足不一樣場合的運算需求。
那麼爲何須要維度變換呢?考慮線性層的批量形式:
其中 X 包含了 2 個樣本,每一個樣本的特徵長度爲 4,X 的 shape 爲[2,4]。線性層的輸出爲 3個節點,即 W 的 shape 定義爲[4,3],偏置𝒃的 shape 定義爲[3]。那麼X@W的運算張量shape 爲[2,3],須要疊加上 shape 爲[3]的偏置𝒃。不一樣 shape 的 2 個張量怎麼直接相加呢?
回到咱們設計偏置的初衷,咱們給每一個層的每一個輸出節點添加一個偏置,這個偏置數據是對全部的樣本都是共享的,換言之,每一個樣本都應該累加上一樣的偏置向量𝒃,如圖4.5 所示:
所以,對於 2 個樣本的輸入 X,咱們須要將 shape 爲[3]的偏置𝒃
按樣本數量複製 1 份,變成矩陣形式𝐵
經過與X′ = X@W
相加,此時X′與𝐵 shape 相同,知足矩陣相加的數學條件:
經過這種方式,既知足了數學上矩陣相加須要 shape 一致的條件,又達到了給每一個輸入樣本的輸出節共享偏置的邏輯。爲了實現這種運算方式,咱們將𝒃插入一個新的維度,並把它定義爲 batch 維度,而後在 batch 維度將數據複製 1 份,獲得變換後的B′,新的 shape 爲[2,3]。
算法的每一個模塊對於數據張量的格式有不一樣的邏輯要求,當現有的數據格式不知足算法要求時,須要經過維度變換將數據調整爲正確的格式。這就是維度變換的功能。
基本的維度變換包含了改變視圖 reshape
,插入新維度 expand_dims
,刪除維度squeeze
,交換維度 transpose
,複製數據 tile
等
在介紹改變視圖操做以前,咱們先來認識一下張量的存儲和視圖(View)的概念。張量的視圖就是咱們理解張量的方式,好比 shape 爲[2,4,4,3]的張量 A,咱們從邏輯上能夠理解爲 2 張圖片,每張圖片 4 行 4 列,每一個位置有 RGB 3 個通道的數據;張量的存儲體如今張量在內存上保存爲一段連續的內存區域,對於一樣的存儲,咱們能夠有不一樣的理解方式,好比上述 A,咱們能夠在不改變張量的存儲下,將張量 A 理解爲 2 個樣本,每一個樣本的特徵爲長度 48 的向量。這就是存儲與視圖的關係。
咱們經過 tf.range()模擬生成 x 的數據:
x=tf.range(96) x=tf.reshape(x,[2,4,4,3]) <tf.Tensor: id=11, shape=(2, 4, 4, 3), dtype=int32, numpy= array([[[[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]],…
在存儲數據時,內存並不支持這個維度層級概念,只能以平鋪方式按序寫入內存,所以這種層級關係須要人爲管理,也就是說,每一個張量的存儲順序須要人爲跟蹤。爲了方便表達,咱們把張量 shape 中相對靠左側的維度叫作大維度,shape 中相對靠右側的維度叫作小維度,好比[2,4,4,3]的張量中,圖片數量維度與通道數量相比,圖片數量叫作大維度,通道數叫作小維度。在優先寫入小維度的設定下,上述張量的內存佈局爲
數據在建立時按着初始的維度順序寫入,改變張量的視圖僅僅是改變了張量的理解方式,並不會改變張量的存儲順序,這在必定程度上是從計算效率考慮的,大量數據的寫入操做會消耗較多的計算資源。改變視圖操做在提供便捷性的同時,也會帶來不少邏輯隱患,這主要的緣由是張量的視圖與存儲不一樣步形成的。咱們先介紹合法的視圖變換操做,再介紹不合法的視圖變換。
好比張量按着初始視圖[𝑏, ℎ, w, 𝑐]寫入的內存佈局,咱們改變初始視圖[𝑏, ℎ, w, 𝑐]的理解方式,它能夠有多種合法理解方式:
❑ [𝑏, ℎ ∗w , 𝑐] 張量理解爲 b 張圖片,hw 個像素點,c 個通道
❑ [𝑏, ℎ, w∗ 𝑐] 張量理解爲 b 張圖片,h 行,每行的特徵長度爲 wc
❑ [𝑏, ℎ ∗ w∗ 𝑐] 張量理解爲 b 張圖片,每張圖片的特徵長度爲 hwc
從語法上來講,視圖變換隻須要知足新視圖的元素總量與內存區域大小相等便可,即新視圖的元素數量等於
正是因爲視圖的設計約束不多,徹底由用戶定義,使得在改變視圖時容易出現邏輯隱患。
如今咱們來考慮不合法的視圖變換。例如,若是定義新視圖爲[𝑏,w , ℎ, 𝑐],[𝑏, 𝑐, ℎ ∗w ]或者[𝑏, 𝑐, ℎ, w]等時,與張量的存儲順序相悖,若是不一樣步更新張量的存儲順序,那麼恢復出的數據將與新視圖不一致,從而致使數據錯亂。
爲了可以正確恢復出數據,必須保證張量的存儲順序與新視圖的維度順序一致,例如根據圖片數量-行-列-通道初始視圖保存的張量,按照圖片數量-行-列-通道(𝑏 − ℎ −w − 𝑐)的順序能夠得到合法數據。若是按着圖片數量-像素-通道( b− h ∗ w − c)的方式恢復視圖,也能獲得合法的數據。可是若是按着圖片數量-通道-像素( b− c − h ∗ w)的方式恢復數據,因爲內存佈局是按着圖片數量-行-列-通道的順序,視圖維度與存儲維度順序相悖,提取的數據將是錯亂的。
改變視圖是神經網絡中很是常見的操做,能夠經過串聯多個 Reshape 操做來實現複雜邏輯,可是在經過 Reshape 改變視圖時,必須始終記住張量的存儲順序,新視圖的維度順序不能與存儲順序相悖,不然須要經過交換維度操做將存儲順序同步過來。
舉個例子,對於 shape 爲[4,32,32,3]的圖片數據,經過 Reshape 操做將 shape 調整爲[4,1024,3],此時視圖的維度順序爲𝑏 − 𝑝𝑖𝑥𝑒𝑙 − 𝑐,張量的存儲順序爲[𝑏, ℎ, w, 𝑐]。能夠將[4,1024,3]恢復爲
❑ [𝑏, ℎ, w, 𝑐] = [4,32,32,3]時,新視圖的維度順序與存儲順序無衝突,能夠恢復出無邏輯問題的數據
❑ [𝑏, w, ℎ, 𝑐] = [4,32,32,3]時,新視圖的維度順序與存儲順序衝突
❑ [ℎ ∗w ∗ 𝑐, 𝑏] = [3072,4]時,新視圖的維度順序與存儲順序衝突
在 TensorFlow 中,能夠經過張量的 ndim
和 shape
成員屬性得到張量的維度數和形狀:
x.ndim,x.shape (4, TensorShape([2, 4, 4, 3]))
經過 tf.reshape(x, new_shape)
,能夠將張量的視圖任意的合法改變:
tf.reshape(x,[2,-1]) <tf.Tensor: id=520, shape=(2, 48), dtype=int32, numpy= array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,… 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95]])>
其中的參數-1 表示當前軸上長度須要根據視圖總元素不變的法則自動推導,從而方便用戶書寫。好比,上面的-1 能夠推導爲
再次改變數據的視圖爲[2,4,12]:
tf.reshape(x,[2,4,12]) <tf.Tensor: id=523, shape=(2, 4, 12), dtype=int32, numpy= array([[[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],… [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]], [[48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59], … [84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95]]])>
再次改變數據的視圖爲[2,16,3]:
tf.reshape(x,[2,-1,3]) <tf.Tensor: id=526, shape=(2, 16, 3), dtype=int32, numpy= array([[[ 0, 1, 2], … [45, 46, 47]], [[48, 49, 50],… [93, 94, 95]]])>
經過上述的一系列連續變換視圖操做時須要意識到,張量的存儲順序始終沒有改變,數據在內存中仍然是按着初始寫入的順序0,1,2, … ,95保存的。
增長維度 增長一個長度爲 1 的維度至關於給原有的數據增長一個新維度的概念,維度長度爲 1,故數據並不須要改變,僅僅是改變數據的理解方式,所以它其實能夠理解爲改變視圖的一種特殊方式。
考慮一個具體例子,一張 28x28 灰度圖片的數據保存爲 shape 爲[28,28]的張量,在末尾給張量增長一新維度,定義爲爲通道數維度,此時張量的 shape 變爲[28,28,1]:
x = tf.random.uniform([28,28],maxval=10,dtype=tf.int32) <tf.Tensor: id=552, shape=(28, 28), dtype=int32, numpy= array([[4, 5, 7, 6, 3, 0, 3, 1, 1, 9, 7, 7, 3, 1, 2, 4, 1, 1, 9, 8, 6, 6, 4, 9, 9, 4, 6, 0],…
經過 tf.expand_dims(x, axis)
可在指定的 axis 軸前能夠插入一個新的維度:
x = tf.expand_dims(x,axis=2) <tf.Tensor: id=555, shape=(28, 28, 1), dtype=int32, numpy= array([[[4], [5], [7], [6], [3],…
能夠看到,插入一個新維度後,數據的存儲順序並無改變,依然按着 4,5,7,6,3,0,…的順序保存,僅僅是在插入一個新的維度後,改變了數據的視圖。
一樣的方法,咱們能夠在最前面插入一個新的維度,並命名爲圖片數量維度,長度爲1,此時張量的 shape 變爲[1,28,28,1]。
x = tf.expand_dims(x,axis=0) <tf.Tensor: id=558, shape=(1, 28, 28), dtype=int32, numpy= array([[[4, 5, 7, 6, 3, 0, 3, 1, 1, 9, 7, 7, 3, 1, 2, 4, 1, 1, 9, 8, 6, 6, 4, 9, 9, 4, 6, 0], [5, 8, 6, 3, 6, 4, 3, 0, 5, 9, 0, 5, 4, 6, 4, 9, 4, 4, 3, 0, 6, 9, 3, 7, 4, 2, 8, 9],…
須要注意的是,tf.expand_dims
的 axis 爲正時,表示在當前維度以前插入一個新維度;爲負時,表示當前維度以後插入一個新的維度。以[𝑏, ℎ, w, 𝑐]張量爲例,不一樣 axis 參數的實際插入位置以下圖 4.6 所示:
刪除維度 是增長維度的逆操做,與增長維度同樣,刪除維度只能刪除長度爲 1 的維度,也不會改變張量的存儲。繼續考慮增長維度後 shape 爲[1,28,28,1]的例子,若是但願將圖片數量維度刪除,能夠經過 tf.squeeze(x, axis)
函數,axis 參數爲待刪除的維度的索引號,圖片數量的維度軸 axis=0:
x = tf.squeeze(x, axis=0) <tf.Tensor: id=586, shape=(28, 28, 1), dtype=int32, numpy= array([[[8], [2], [2], [0],…
繼續刪除通道數維度,因爲已經刪除了圖片數量維度,此時的 x 的 shape 爲[28,28,1],所以刪除通道數維度時指定 axis=2:
x = tf.squeeze(x, axis=2) <tf.Tensor: id=588, shape=(28, 28), dtype=int32, numpy= array([[8, 2, 2, 0, 7, 0, 1, 4, 9, 1, 7, 4, 8, 2, 7, 4, 8, 2, 9, 8, 8, 0, 9, 9, 7, 5, 9, 7], [3, 4, 9, 9, 0, 6, 5, 7, 1, 9, 9, 1, 2, 7, 2, 7, 5, 3, 3, 7, 2, 4, 5, 2, 7, 3, 8, 0],…
若是不指定維度參數 axis,即 ·tf.squeeze(x)·,那麼他會默認刪除全部長度爲 1 的維度:
x = tf.random.uniform([1,28,28,1],maxval=10,dtype=tf.int32) tf.squeeze(x) <tf.Tensor: id=594, shape=(28, 28), dtype=int32, numpy= array([[9, 1, 4, 6, 4, 9, 0, 0, 1, 4, 0, 8, 5, 2, 5, 0, 0, 8, 9, 4, 5, 0, 1, 1, 4, 3, 9, 9],…
改變視圖、增刪維度都不會影響張量的存儲。在實現算法邏輯時,在保持維度順序不變的條件下,僅僅改變張量的理解方式是不夠的,有時須要直接調整的存儲順序,即交換維度(Transpose)。經過交換維度,改變了張量的存儲順序,同時也改變了張量的視圖。
交換維度操做是很是常見的,好比在 TensorFlow 中,圖片張量的默認存儲格式是通道後行格式:[𝑏, ℎ, w, 𝑐],可是部分庫的圖片格式是通道先行:[𝑏, 𝑐, ℎ, w],所以須要完成[𝑏, ℎ, w, 𝑐]到[𝑏, 𝑐, ℎ,w ]維度交換運算。
咱們以[𝑏, ℎ, w, 𝑐]轉換到[𝑏, 𝑐, ℎ,w ]爲例,介紹如何使用 tf.transpose(x, perm)
函數完成維度交換操做,其中 perm
表示新維度的順序 List。考慮圖片張量 shape 爲[2,32,32,3],圖片數量、行、列、通道數的維度索引分別爲 0,1,2,3,若是須要交換爲[𝑏, 𝑐, ℎ, w]格式,則新維度的排序爲圖片數量、通道數、行、列,對應的索引號爲[0,3,1,2],實現以下:
x = tf.random.normal([2,32,32,3]) tf.transpose(x,perm=[0,3,1,2]) <tf.Tensor: id=603, shape=(2, 3, 32, 32), dtype=float32, numpy= array([[[[-1.93072677e+00, -4.80163872e-01, -8.85614634e-01, ..., 1.49124235e-01, 1.16427064e+00, -1.47740364e+00], [-1.94761145e+00, 7.26879001e-01, -4.41877693e-01, ...
若是但願將[𝑏, ℎ, w, 𝑐]交換爲[𝑏, w, ℎ, 𝑐],即將行列維度互換,則新維度索引爲[0,2,1,3]:
x = tf.random.normal([2,32,32,3]) tf.transpose(x,perm=[0,2,1,3]) <tf.Tensor: id=612, shape=(2, 32, 32, 3), dtype=float32, numpy= array([[[[ 2.1266546 , -0.64206547, 0.01311932], [ 0.918484 , 0.9528751 , 1.1346699 ], ...,
須要注意的是,經過 tf.transpose
完成維度交換後,張量的存儲順序已經改變,視圖也隨之改變,後續的全部操做必須基於新的存續順序進行。
當經過增長維度操做插入新維度後,可能但願在新的維度上面複製若干份數據,知足後續算法的格式要求。
考慮𝑌 = 𝑋@𝑊 + 𝒃的例子,偏置𝒃插入新維度後,須要在新維度上覆制 batch size 份數據,將 shape 變爲與𝑋@𝑊一致後,才能完成張量相加運算。能夠經過tf.tile(x, multiples)
函數完成數據在指定維度上的複製操做,multiples 分別指定了每一個維度上面的複製倍數,對應位置爲 1 代表不復制,爲 2 代表新長度爲原來的長度的 2 倍,即數據複製一份,以此類推。
以輸入爲[2,4],輸出爲 3 個節點線性變換層爲例,偏置𝒃定義爲:
經過 tf.expand_dims(b,axis=0)
插入新維度:樣本數量維度
此時𝒃的 shape 變爲[1,3],咱們須要在 axis=0 圖片數量維度上根據輸入樣本的數量複製若干次,這裏的 batch size 爲 2,𝒃變爲矩陣 B:
經過 tf.tile(b, multiples=[2,1])
便可在 axis=0 維度複製 1 次,在 axis=1 維度不復制。首先插入新的維度:
b = tf.constant([1,2]) b = tf.expand_dims(b, axis=0) b <tf.Tensor: id=645, shape=(1, 2), dtype=int32, numpy=array([[1, 2]])>
在 batch 維度上覆制數據 1 份:
b = tf.tile(b, multiples=[2,1]) <tf.Tensor: id=648, shape=(2, 2), dtype=int32, numpy= array([[1, 2], [1, 2]])>
此時 B 的 shape 變爲[2,3],能夠直接與X@W進行相加運算。
考慮另外一個例子,輸入 x 爲 2 行 2 列的矩陣:
x = tf.range(4) x=tf.reshape(x,[2,2]) <tf.Tensor: id=655, shape=(2, 2), dtype=int32, numpy= array([[0, 1], [2, 3]])>
首先在列維度複製 1 份數據:
x = tf.tile(x,multiples=[1,2]) <tf.Tensor: id=658, shape=(2, 4), dtype=int32, numpy= array([[0, 1, 0, 1], [2, 3, 2, 3]])>
而後在行維度複製 1 份數據:
x = tf.tile(x,multiples=[2,1]) <tf.Tensor: id=672, shape=(4, 4), dtype=int32, numpy= array([[0, 1, 0, 1], [2, 3, 2, 3], [0, 1, 0, 1], [2, 3, 2, 3]])>
通過 2 個維度上的複製運算後,能夠看到數據的變化過程,shape 也變爲原來的 2 倍。
須要注意的是,tf.tile
會建立一個新的張量來保存複製後的張量,因爲複製操做涉及到大量數據的讀寫 IO 運算,計算代價相對較高。神經網絡中不一樣 shape 之間的運算操做十分頻繁,那麼有沒有輕量級的複製操做呢?這就是接下來要介紹的 Broadcasting 操做。
Broadcasting
也叫廣播機制(自動擴展也許更合適),它是一種輕量級張量複製的手段,在邏輯上擴展張量數據的形狀,可是隻要在須要時纔會執行實際存儲複製操做。對於大部分場景,Broadcasting 機制都能經過優化手段避免實際複製數據而完成邏輯運算,從而相對於 tf.tile
函數,減小了大量計算代價。
對於全部長度爲 1 的維度,Broadcasting 的效果和 tf.tile 同樣,都能在此維度上邏輯複製數據若干份,區別在於 tf.tile 會建立一個新的張量,執行復制 IO 操做,並保存複製後的張量數據,Broadcasting 並不會當即複製數據,它會邏輯上改變張量的形狀,使得視圖上變成了複製後的形狀。
Broadcasting 會經過深度學習框架的優化手段避免實際複製數據而完成邏輯運算,至於怎麼實現的用戶沒必要關係,對於用戶來講,Broadcasting 和 tf.tile 複製的最終效果是同樣的,操做對用戶透明,可是 Broadcasting 機制節省了大量計算資源,建議在運算過程當中儘量地利用 Broadcasting 提升計算效率。
繼續考慮上述的Y = X@W + 𝒃
的例子,X@W的 shape 爲[2,3],𝒃的 shape 爲[3],咱們能夠經過結合 tf.expand_dims
和 tf.tile
完成實際複製數據運算,將𝒃變換爲[2,3],而後與X@W完成相加。但實際上,咱們直接將 shape 爲[2,3]與[3]的𝒃相加:
x = tf.random.normal([2,4]) w = tf.random.normal([4,3]) b = tf.random.normal([3]) y = x@w+b
上述加法並無發生邏輯錯誤,那麼它是怎麼實現的呢?這是由於它自動調用 Broadcasting函數 tf.broadcast_to(x, new_shape)
,將 2 者 shape 擴張爲相同的[2,3],即上式能夠等效爲:
y = x@w + tf.broadcast_to(b,[2,3])
也就是說,操做符+在遇到 shape 不一致的 2 個張量時,會自動考慮將 2 個張量Broadcasting 到一致的 shape,而後再調用 tf.add
完成張量相加運算,這也就解釋了咱們以前一直存在的困惑。經過自動調用 tf.broadcast_to(b, [2,3])
的 Broadcasting 機制,既實現了增長維度、複製數據的目的,又避免實際複製數據的昂貴計算代價,同時書寫更加簡潔高效。
那麼有了Broadcasting 機制後,全部 shape 不一致的張量是否是均可以直接完成運算?很明顯,全部的運算都須要在正確邏輯下進行,Broadcasting 機制並不會擾亂正常的計算邏輯,它只會針對於最多見的場景自動完成增長維度並複製數據的功能,提升開發效率和運行效率。這種最多見的場景是什麼呢?這就要說到 Broadcasting 設計的核心思想。
Broadcasting 機制的核心思想是普適性,即同一份數據能廣泛適合於其餘位置。在驗證普適性以前,須要將張量 shape 靠右對齊,而後進行普適性判斷:對於長度爲 1 的維度,默認這個數據廣泛適合於當前維度的其餘位置;對於不存在的維度,則在增長新維度後默認當前數據也是普適性於新維度的,從而能夠擴展爲更多維度數、其餘長度的張量形狀。
考慮 shape 爲[ , 1]的張量 A,須要擴展爲 shape:[𝑏, ℎ, w, 𝑐],如圖 4.7 所示,上行爲欲擴展的 shape,下面爲現有 shape:
首先將 2 個 shape 靠右對齊,對於通道維度 c,張量的現長度爲 1,則默認此數據一樣適合當前維度的其餘位置,將數據邏輯上覆制𝑐 − 1份,長度變爲 c;對於不存在的 b 和 h 維度,則自動插入新維度,新維度長度爲 1,同時默認當前的數據普適於新維度的其餘位置,即對於其它的圖片、其餘的行來講,與當前的這一行的數據徹底一致。這樣將數據b,h 維度的長度自動擴展爲 b,h,如圖 4.8 所示:
經過 tf.broadcast_to(x, new_shape)
能夠顯式將現有 shape 擴張爲 new_shape:
A = tf.random.normal([32,1]) tf.broadcast_to(A, [2,32,32,3]) <tf.Tensor: id=13, shape=(2, 32, 32, 3), dtype=float32, numpy= array([[[[-1.7571245 , -1.7571245 , -1.7571245 ], [ 1.580159 , 1.580159 , 1.580159 ], [-1.5324328 , -1.5324328 , -1.5324328 ],...
能夠看到,在普適性原則的指導下,Broadcasting 機制變得直觀好理解,它的設計是很是符合人的思惟模式。
咱們來考慮不知足普適性原則的例子,以下圖 4.9 所示:
在 c 維度上,張量已經有 2 個特徵數據,新 shape 對應維度長度爲 c(𝑐 ≠ 2,好比 c=3),那麼當前維度上的這 2 個特徵沒法普適到其餘長度,故不知足普適性原則,沒法應用Broadcasting 機制,將會觸發錯誤:
A = tf.random.normal([32,2]) tf.broadcast_to(A, [2,32,32,4]) InvalidArgumentError: Incompatible shapes: [32,2] vs. [2,32,32,4] [Op:BroadcastTo]
在進行張量運算時,有些運算能夠在處理不一樣 shape 的張量時,會隱式自動調用Broadcasting 機制,如+,-,*,/等運算等,將參與運算的張量 Broadcasting 成一個公共shape,再進行相應的計算,如圖 4.10 所示,演示了 3 種不一樣 shape 下的張量 A,B 相加的例子
簡單測試一下基本運算符的自動 Broadcasting 機制:
a = tf.random.normal([2,32,32,1]) b = tf.random.normal([32,32]) a+b,a-b,a*b,a/b
這些運算都能 Broadcasting 成[2,32,32,32]的公共 shape,再進行運算。熟練掌握並運用Broadcasting 機制可讓代碼更簡潔,計算效率更高。
前面的章節咱們已經使用了基本的加減乘除等數學運算函數,本節咱們將系統地介紹TensorFlow 中常見的數學運算函數。
加減乘除是最基本的數學運算,分別經過 tf.add
, tf.subtract
, tf.multiply
, tf.divide
函數實現,TensorFlow 已經重載了+ −∗/運算符,通常推薦直接使用運算符來完成加減乘除運算。
整除和餘除也是常見的運算之一,分別經過//和%運算符實現。咱們來演示整除運算:
a = tf.range(5) b = tf.constant(2) a//b <tf.Tensor: id=115, shape=(5,), dtype=int32, numpy=array([0, 0, 1, 1, 2])>
餘除運算:
a%b <tf.Tensor: id=117, shape=(5,), dtype=int32, numpy=array([0, 1, 0, 1, 0])>
經過 tf.pow(x, a)
能夠方便地完成𝑦 =
乘方運算,也能夠經過運算符**實現𝑥 ∗∗ 𝑎運算,實現以下:
x = tf.range(4) tf.pow(x,3) <tf.Tensor: id=124, shape=(4,), dtype=int32, numpy=array([ 0, 1, 8, 27])> x**2 <tf.Tensor: id=127, shape=(4,), dtype=int32, numpy=array([0, 1, 4, 9])>
設置指數爲 形式便可實現根號運算: :
x=tf.constant([1.,4.,9.]) x**(0.5) <tf.Tensor: id=139, shape=(3,), dtype=float32, numpy=array([1., 2., 3.], dtype=float32)>
特別地,對於常見的平方和平方根運算,能夠使用 tf.square(x)
和 tf.sqrt(x)
實現。平方運算實現以下:
x = tf.range(5) x = tf.cast(x, dtype=tf.float32) x = tf.square(x) <tf.Tensor: id=159, shape=(5,), dtype=float32, numpy=array([ 0., 1., 4., 9., 16.], dtype=float32)>
平方根運算實現以下:
tf.sqrt(x) <tf.Tensor: id=161, shape=(5,), dtype=float32, numpy=array([0., 1., 2., 3., 4.], dtype=float32)>
經過 tf.pow(a, x)
或者**運算符能夠方便實現指數運算
:
x = tf.constant([1.,2.,3.]) 2**x <tf.Tensor: id=179, shape=(3,), dtype=float32, numpy=array([2., 4., 8.], dtype=float32)>
特別地,對於天然指數
,能夠經過 tf.exp(x)
實現:
tf.exp(1.) <tf.Tensor: id=182, shape=(), dtype=float32, numpy=2.7182817>
在 TensorFlow 中,天然對數
能夠經過 tf.math.log(x)
實現:
x=tf.exp(3.) tf.math.log(x) <tf.Tensor: id=186, shape=(), dtype=float32, numpy=3.0>
若是但願計算其餘底數的對數,能夠根據對數的換底公式:
間接的經過 tf.math.log(x)
實現。如計算
能夠經過
實現以下:
x = tf.constant([1.,2.]) x = 10**x tf.math.log(x)/tf.math.log(10.) <tf.Tensor: id=222, shape=(2,), dtype=float32, numpy=array([0. , 2.3025851], dtype=float32)>
實現起來相對繁瑣,也許 TensorFlow 之後會推出任意底數的 log 函數.
神經網絡中間包含了大量的矩陣相乘運算,前面咱們已經介紹了經過@運算符能夠方便的實現矩陣相乘,還能夠經過 tf.matmul(a, b)
實現。須要注意的是,TensorFlow 中的矩陣相乘能夠使用批量方式,也就是張量 a,b 的維度數能夠大於 2。當張量 a,b 維度數大於 2時,TensorFlow 會選擇 a,b 的最後兩個維度進行矩陣相乘,前面全部的維度都視做 Batch 維度。
根據矩陣相乘的定義,a 和 b 可以矩陣相乘的條件是,a 的倒數第一個維度長度(列)和b 的倒數第二個維度長度(行)必須相等。好比張量 a shape:[4,3,28,32]能夠與張量 bshape:[4,3,32,2]進行矩陣相乘:
a = tf.random.normal([4,3,23,32]) b = tf.random.normal([4,3,32,2]) a@b <tf.Tensor: id=236, shape=(4, 3, 28, 2), dtype=float32, numpy= array([[[[-1.66706240e+00, -8.32602978e+00], [ 9.83304405e+00, 8.15909767e+00], [ 6.31014729e+00, 9.26124632e-01],…
獲得 shape 爲[4,3,28,2]的結果。
矩陣相乘函數支持自動 Broadcasting
機制:
a = tf.random.normal([4,28,32]) b = tf.random.normal([32,16]) tf.matmul(a,b) <tf.Tensor: id=264, shape=(4, 28, 16), dtype=float32, numpy= array([[[-1.11323869e+00, -9.48194981e+00, 6.48123884e+00, ..., 6.53280640e+00, -3.10894990e+00, 1.53050375e+00], [ 4.35898495e+00, -1.03704405e+01, 8.90656471e+00, ...,
到如今爲止,咱們已經介紹瞭如何建立張量,對張量進行索引切片,維度變換和常見的數學運算等操做。本節咱們將利用咱們已經學到的知識去完成三層神經網絡的實現:
咱們採用的數據集是 MNIST 手寫數字圖片集,輸入節點數爲 784,第一層的輸出節點數是256,第二層的輸出節點數是 128,第三層的輸出節點是 10,也就是當前樣本屬於 10 類別的機率。
首先建立每一個非線性函數的 w,b 參數張量:
w1 = tf.Variable(tf.random.truncated_normal([784, 256], stddev=0.1)) b1 = tf.Variable(tf.zeros([256])) w2 = tf.Variable(tf.random.truncated_normal([256, 128], stddev=0.1)) b2 = tf.Variable(tf.zeros([128])) w3 = tf.Variable(tf.random.truncated_normal([128, 10], stddev=0.1)) b3 = tf.Variable(tf.zeros([10]))
在前向計算時,首先將 shape 爲[𝑏, 28,28]的輸入數據 Reshape 爲[𝑏, 784]:
# [b, 28, 28] => [b, 28*28] x = tf.reshape(x, [-1, 28*28])
完成第一個非線性函數的計算,咱們這裏顯示地進行 Broadcasting:
# [b, 784]@[784, 256] + [256] => [b, 256] + [256] => [b, 256] +[b, 256] h1 = x@w1 + tf.broadcast_to(b1, [x.shape[0], 256]) h1 = tf.nn.relu(h1)
一樣的方法完成第二個和第三個非線性函數的前向計算,輸出層能夠不使用 ReLU 激活函數:
# [b, 256] => [b, 128] h2 = h1@w2 + b2 h2 = tf.nn.relu(h2) # [b, 128] => [b, 10] out = h2@w3 + b3
將真實的標註張量 y 轉變爲 one-hot 編碼,並計算與 out 的均方差:
# mse = mean(sum(y-out)^2) # [b, 10] loss = tf.square(y_onehot - out) # mean: scalar loss = tf.reduce_mean(loss)
上述的前向計算過程都須要包裹在 with tf.GradientTape() as tape
上下文中,使得前向計算時可以保存計算圖信息,方便反向求導運算。
經過 tape.gradient()
函數求得網絡參數到梯度信息:
# compute gradients grads = tape.gradient(loss, [w1, b1, w2, b2, w3, b3])
並按照
來更新網絡參數:
# w1 = w1 - lr * w1_grad w1.assign_sub(lr * grads[0]) b1.assign_sub(lr * grads[1]) w2.assign_sub(lr * grads[2]) b2.assign_sub(lr * grads[3]) w3.assign_sub(lr * grads[4]) b3.assign_sub(lr * grads[5])
其中 assign_sub()
將原地(In-place)減去給定的參數值,實現參數的自我更新操做。網絡訓練
偏差值的變化曲線如圖 4.11 所示。
在介紹完張量的基本操做後,咱們來進一步學習張量的進階操做,如張量的合併與分割,範數統計,張量填充,限幅等,並用過 MNIST 數據集的測試實戰加深讀者對TensorFlow 張量操做的當即。
合併是指將多個張量在某個維度上合併爲一個張量。以某學校班級成績冊數據爲例,設張量 A 保存了某學校 1-4 號班級的成績冊,每一個班級 35 個學生,共 8 門科目,則張量 A的 shape 爲:[4,35,8];一樣的方式,張量 B 保存了剩下的 6 個班級的成績冊,shape 爲[6,35,8]。經過合併 2 個成績冊,即可獲得學校全部班級的成績冊張量 C,shape 應爲[10,35,8]。這就是張量合併的意義所在。張量的合併能夠使用拼接(Concatenate
)和堆疊(Stack
)操做實現,拼接並不會產生新的維度,而堆疊會建立新維度。選擇使用拼接仍是堆疊操做來合併張量,取決於具體的場景是否須要建立新維度。
拼接 在 TensorFlow 中,能夠經過 tf.concat(tensors, axis)
,其中 tensors 保存了全部須要合併的張量 List,axis 指定須要合併的維度。回到上面的例子,這裏班級維度索引號爲 0,即 axis=0,合併張量 A,B 以下:
a = tf.random.normal([4,35,8]) # 模擬成績冊 A b = tf.random.normal([6,35,8]) # 模擬成績冊 B tf.concat([a,b],axis=0) # 合併成績冊 <tf.Tensor: id=13, shape=(10, 35, 8), dtype=float32, numpy= array([[[ 1.95299834e-01, 6.87859178e-01, -5.80048323e-01, ..., 1.29430830e+00, 2.56610274e-01, -1.27798581e+00], [ 4.29753691e-01, 9.11329567e-01, -4.47975427e-01, ...,
除了能夠在班級維度上進行合併,還能夠在其餘維度上合併張量。考慮張量 A 保存了全部班級全部學生的前 4 門科目成績,shape 爲[10,35,4],張量 B 保存了剩下的 4 門科目成績,shape 爲[10,35,4],則能夠合併 shape 爲[10,35,8]的總成績冊張量:
a = tf.random.normal([10,35,4]) b = tf.random.normal([10,35,4]) tf.concat([a,b],axis=2) # 在科目維度拼接 <tf.Tensor: id=28, shape=(10, 35, 8), dtype=float32, numpy= array([[[-5.13509691e-01, -1.79707789e+00, 6.50747120e-01, ..., 2.58447856e-01, 8.47878829e-02, 4.13468748e-01], [-1.17108583e+00, 1.93961406e+00, 1.27830813e-02, ...,
合併操做能夠在任意的維度上進行,惟一的約束是非合併維度的長度必須一致。好比 shape爲[4,32,8]和 shape 爲[6,35,8]的張量則不能直接在班級維度上進行合併,由於學生數維度的長度並不一致,一個爲 32,另外一個爲 35:
a = tf.random.normal([4,32,8]) b = tf.random.normal([6,35,8]) tf.concat([a,b],axis=0) # 非法拼接 InvalidArgumentError: ConcatOp : Dimensions of inputs should match: shape[0] = [4,32,8] vs. shape[1] = [6,35,8] [Op:ConcatV2] name: concat
堆疊 tf.concat
直接在現有維度上面合併數據,並不會建立新的維度。若是在合併數據時,但願建立一個新的維度,則須要使用 tf.stack
操做。考慮張量 A 保存了某個班級的成績冊,shape 爲[35,8],張量 B 保存了另外一個班級的成績冊,shape 爲[35,8]。合併這 2 個班級的數據時,須要建立一個新維度,定義爲班級維度,新維度能夠選擇放置在任意位置,通常根據大小維度的經驗法則,將較大概念的班級維度放置在學生維度以前,則合併後的張量的新 shape 應爲[2,35,8]。
使用 tf.stack(tensors, axis)
能夠合併多個張量 tensors,其中 axis 指定插入新維度的位置,axis 的用法與 tf.expand_dims
的一致,當axis ≥ 0時,在 axis 以前插入;當axis < 0時,在 axis 以後插入新維度例如 shape 爲[𝑏, 𝑐, ℎ, 𝑤]的張量,在不一樣位置經過 stack 操做插入新維度,axis 參數對應的插入位置設置如圖 5.1 所示:
堆疊方式合併這 2 個班級成績冊以下:
a = tf.random.normal([35,8]) b = tf.random.normal([35,8]) tf.stack([a,b],axis=0) # 堆疊合併爲 2 個班級 <tf.Tensor: id=55, shape=(2, 35, 8), dtype=float32, numpy= array([[[ 3.68728966e-01, -8.54765773e-01, -4.77824420e-01, -3.83714020e-01, -1.73216307e+00, 2.03872994e-02, 2.63810277e+00, -1.12998331e+00],…
一樣能夠選擇在其餘位置插入新維度,如在最末尾插入:
a = tf.random.normal([35,8]) b = tf.random.normal([35,8]) tf.stack([a,b],axis=-1) # 在末尾插入班級維度 <tf.Tensor: id=69, shape=(35, 8, 2), dtype=float32, numpy= array([[[ 0.3456724 , -1.7037214 ], [ 0.41140947, -1.1554345 ], [ 1.8998919 , 0.56994915],…
此時班級的維度在 axis=2 軸上面,理解時也須要按着最新的維度順序去理解數據。若選擇使用 tf.concat
上述成績單,則能夠合併爲:
a = tf.random.normal([35,8]) b = tf.random.normal([35,8]) tf.concat([a,b],axis=0) # 拼接方式合併,沒有 2 個班級的概念 <tf.Tensor: id=108, shape=(70, 8), dtype=float32, numpy= array([[-0.5516891 , -1.5031327 , -0.35369992, 0.31304857, 0.13965549, 0.6696881 , -0.50115544, 0.15550546], [ 0.8622069 , 1.0188094 , 0.18977325, 0.6353301 , 0.05809061,…
tf.concat
也能夠順利合併數據,可是在理解時,須要按着前 35 個學生來自第一個班級,後35 個學生來自第二個班級的方式。在這裏,明顯經過 tf.stack
方式建立新維度的方式更合理,獲得的 shape 爲[2,35,8]的張量也更容易理解。
tf.stack
也須要知足張量堆疊合並條件,它須要全部合併的張量 shape 徹底一致纔可合併。咱們來看張量 shape 不一致時進行堆疊合並會發生的錯誤:
a = tf.random.normal([35,4]) b = tf.random.normal([35,8]) tf.stack([a,b],axis=-1) # 非法堆疊操做 InvalidArgumentError: Shapes of all inputs must match: values[0].shape = [35,4] != values[1].shape = [35,8] [Op:Pack] name: stack
上述操做嘗試合併 shape 爲[35,4]和[35,8]的 2 個張量,因爲 2 者形狀不一致,沒法完成合並操做。
合併操做的逆過程就是分割,將一個張量分拆爲多個張量。繼續考慮成績冊的例子,咱們獲得整個學校的成績冊張量,shape 爲[10,35,8],如今須要將數據在班級維度切割爲10 個張量,每一個張量保存了對應班級的成績冊。
經過 tf.split(x, axis, num_or_size_splits)
能夠完成張量的分割操做,其中
❑ x:待分割張量
❑ axis:分割的維度索引號
❑ num_or_size_splits:切割方案。當 num_or_size_splits 爲單個數值時,如 10,表示切割爲 10 份;當 num_or_size_splits 爲 List 時,每一個元素表示每份的長度,如[2,4,2,2]表示切割爲 4 份,每份的長度分別爲 2,4,2,2
如今咱們將總成績冊張量切割爲 10 份:
x = tf.random.normal([10,35,8]) # 等長切割 result = tf.split(x,axis=0,num_or_size_splits=10) len(result) #[1,35,8] 10
能夠查看切割後的某個張量的形狀,它應是某個班級的全部成績冊數據,shape 爲[35,8]之類:
result[0] Out[9]: <tf.Tensor: id=136, shape=(1, 35, 8), dtype=float32, numpy= array([[[-1.7786729 , 0.2970506 , 0.02983334, 1.3970423 , 1.315918 , -0.79110134, -0.8501629 , -1.5549672 ], [ 0.5398711 , 0.21478991, -0.08685189, 0.7730989 ,…
能夠看到,切割後的班級 shape 爲[1,35,8],保留了班級維度,這一點須要注意。咱們進行不等長的切割:將數據切割爲 4份,每份長度分別爲[4,2,2,2]:
x = tf.random.normal([10,35,8]) # 自定義長度的切割 result = tf.split(x,axis=0,num_or_size_splits=[4,2,2,2]) len(result) 4
查看第一個張量的 shape,根據咱們的切割方案,它應該包含了 4 個班級的成績冊:
result[0] <tf.Tensor: id=155, shape=(4, 35, 8), dtype=float32, numpy= array([[[-6.95693314e-01, 3.01393479e-01, 1.33964568e-01, ...,
特別地,若是但願在某個維度上所有按長度爲 1 的方式分割,還能夠直接使用 tf.unstack(x,axis)
。這種方式是 tf.split
的一種特殊狀況,切割長度固定爲 1,只須要指定切割維度便可。例如,將總成績冊張量在班級維度進行 unstack:
x = tf.random.normal([10,35,8]) result = tf.unstack(x,axis=0) # Unstack 爲長度爲 1 len(result) 10
查看切割後的張量的形狀:
result[0] <tf.Tensor: id=166, shape=(35, 8), dtype=float32, numpy= array([[-0.2034383 , 1.1851563 , 0.25327438, -0.10160723, 2.094969 , -0.8571669 , -0.48985648, 0.55798006],…
能夠看到,經過 tf.unstack 切割後,shape 變爲[35,8],即班級維度消失了,這也是與 tf.split
區別之處。
在神經網絡的計算過程當中,常常須要統計數據的各類屬性,如最大值,均值,範數等等。因爲張量一般 shape 較大,直接觀察數據很難得到有用信息,經過觀察這些張量統計信息能夠較輕鬆地推測張量數值的分佈。
向量範數(Vector norm)是表徵向量「長度」的一種度量方法,在神經網絡中,經常使用來表示張量的權值大小,梯度大小等。經常使用的向量範數有:
❑ L1 範數,定義爲向量𝒙的全部元素絕對值之和
❑ L2 範數,定義爲向量𝒙的全部元素的平方和,再開根號
❑ ∞ −範數,定義爲向量𝒙的全部元素絕對值的最大值:
對於矩陣、張量,一樣能夠利用向量範數的計算公式,等價於將矩陣、張量打平成向量後計算。
在 TensorFlow 中,能夠經過 tf.norm(x, ord)
求解張量的 L1, L2, ∞等範數,其中參數 ord指定爲 1,2 時計算 L1, L2 範數,指定爲 np.inf 時計算∞ −範數:
x = tf.ones([2,2]) tf.norm(x,ord=1) # 計算 L1 範數 <tf.Tensor: id=183, shape=(), dtype=float32, numpy=4.0> tf.norm(x,ord=2) # 計算 L2 範數 Out[14]: <tf.Tensor: id=189, shape=(), dtype=float32, numpy=2.0> import numpy as np tf.norm(x,ord=np.inf) # 計算∞範數 Out[15]: <tf.Tensor: id=194, shape=(), dtype=float32, numpy=1.0>
經過 tf.reduce_max
, tf.reduce_min
, tf.reduce_mean
, tf.reduce_sum
能夠求解張量在某個維度上的最大、最小、均值、和,也能夠求全局最大、最小、均值、和信息。
考慮 shape 爲[4,10]的張量,其中第一個維度表明樣本數量,第二個維度表明了當前樣本分別屬於 10 個類別的機率,須要求出每一個樣本的機率最大值爲:
x = tf.random.normal([4,10]) tf.reduce_max(x,axis=1) # 統計機率維度上的最大值 Out[16]:<tf.Tensor: id=203, shape=(4,), dtype=float32, numpy=array([1.2410722 , 0.88495886, 1.4170984 , 0.9550192 ], dtype=float32)>
一樣求出每一個樣本機率的最小值:
tf.reduce_min(x,axis=1) # 統計機率維度上的最小值 Out[17]:<tf.Tensor: id=206, shape=(4,), dtype=float32, numpy=array([- 0.27862206, -2.4480672 , -1.9983795 , -1.5287997 ], dtype=float32)>
求出每一個樣本的機率的均值:
tf.reduce_mean(x,axis=1) # 統計機率維度上的均值 Out[18]:<tf.Tensor: id=209, shape=(4,), dtype=float32, numpy=array([ 0.39526337, -0.17684573, -0.148988 , -0.43544054], dtype=float32)>
當不指定 axis 參數時,tf.reduce_*
函數會求解出全局元素的最大、最小、均值、和:
x = tf.random.normal([4,10]) # 統計全局的最大、最小、均值、和 tf.reduce_max(x),tf.reduce_min(x),tf.reduce_mean(x) Out [19]: (<tf.Tensor: id=218, shape=(), dtype=float32, numpy=1.8653786>, <tf.Tensor: id=220, shape=(), dtype=float32, numpy=-1.9751656>, <tf.Tensor: id=222, shape=(), dtype=float32, numpy=0.014772797>)
在求解偏差函數時,經過 TensorFlow 的 MSE 偏差函數能夠求得每一個樣本的偏差,須要計算樣本的平均偏差,此時能夠經過 tf.reduce_mean
在樣本數維度上計算均值:
out = tf.random.normal([4,10]) # 網絡預測輸出 y = tf.constant([1,2,2,0]) # 真實標籤 y = tf.one_hot(y,depth=10) # one-hot 編碼 loss = keras.losses.mse(y,out) # 計算每一個樣本的偏差 loss = tf.reduce_mean(loss) # 平均偏差 loss <tf.Tensor: id=241, shape=(), dtype=float32, numpy=1.1921183>
與均值函數類似的是求和函數 tf.reduce_sum(x,axis)
,它能夠求解張量在 axis 軸上全部特徵的和:
out = tf.random.normal([4,10]) tf.reduce_sum(out,axis=-1) # 求和 Out[21]:<tf.Tensor: id=303, shape=(4,), dtype=float32, numpy=array([- 0.588144 , 2.2382064, 2.1582587, 4.962141 ], dtype=float32)>
除了但願獲取張量的最值信息,還但願得到最值所在的索引號,例如分類任務的標籤預測。
考慮 10 分類問題,咱們獲得神經網絡的輸出張量 out,shape 爲[2,10],表明了 2 個樣本屬於 10 個類別的機率,因爲元素的位置索引表明了當前樣本屬於此類別的機率,預測時每每會選擇機率值最大的元素所在的索引號做爲樣本類別的預測值:
out = tf.random.normal([2,10]) out = tf.nn.softmax(out, axis=1) # 經過 softmax 轉換爲機率值 out Out[22]:<tf.Tensor: id=257, shape=(2, 10), dtype=float32, numpy= array([ [0.18773547, 0.1510464 , 0.09431915, 0.13652141, 0.06579739,0.02033597, 0.06067333, 0.0666793 , 0.14594753, 0.07094406], [0.5092072 , 0.03887136, 0.0390687 , 0.01911005, 0.03850609,0.03442522, 0.08060656, 0.10171875, 0.08244187, 0.05604421]], dtype=float32)>
以第一個樣本爲例,能夠看到,它機率最大的索引爲𝑖 = 0,最大機率值爲 0.1877。因爲每一個索引號上的機率值表明了樣本屬於此索引號的類別的機率,所以第一個樣本屬於 0 類的機率最大,在預測時考慮第一個樣本應該最有可能屬於類別 0。這就是須要求解最大值的索引號的一個典型應用。經過 tf.argmax(x, axis)
,tf.argmin(x, axis)
能夠求解在 axis 軸上,x 的最大值、最小值所在的索引號:
pred = tf.argmax(out, axis=1) # 選取機率最大的位置 pred Out[23]:<tf.Tensor: id=262, shape=(2,), dtype=int64, numpy=array([0, 0], dtype=int64)>
能夠看到,這 2 個樣本機率最大值都出如今索引 0 上,所以最有可能都是類別 0,咱們將類別 0 做爲這 2 個樣本的預測類別。
爲了計算分類任務的準確率等指標,通常須要將預測結果和真實標籤比較,統計比較結果中正確的數量來就是計算準確率。考慮 100 個樣本的預測結果:
out = tf.random.normal([100,10]) out = tf.nn.softmax(out, axis=1) # 輸出轉換爲機率 pred = tf.argmax(out, axis=1) # 選取預測值 Out[24]:<tf.Tensor: id=272, shape=(100,), dtype=int64, numpy= array([0, 6, 4, 3, 6, 8, 6, 3, 7, 9, 5, 7, 3, 7, 1, 5, 6, 1, 2, 9, 0, 6, 5, 4, 9, 5, 6, 4, 6, 0, 8, 4, 7, 3, 4, 7, 4, 1, 2, 4, 9, 4,…
能夠看到咱們模擬的 100 個樣本的預測值,咱們與這 100 樣本的真實值比較:
# 真實標籤 y = tf.random.uniform([100],dtype=tf.int64,maxval=10) Out[25]:<tf.Tensor: id=281, shape=(100,), dtype=int64, numpy= array([0, 9, 8, 4, 9, 7, 2, 7, 6, 7, 3, 4, 2, 6, 5, 0, 9, 4, 5, 8, 4, 2, 5, 5, 5, 3, 8, 5, 2, 0, 3, 6, 0, 7, 1, 1, 7, 0, 6, 1, 2, 1, 3, …
便可得到每一個樣本是否預測正確。經過 tf.equal(a, b)
(或 tf.math.equal(a, b))
函數能夠比較這 2個張量是否相等:
out = tf.equal(pred,y) # 預測值與真實值比較 Out[26]:<tf.Tensor: id=288, shape=(100,), dtype=bool, numpy= array([False, False, False, False, True, False, False, False, False, False, False, False, False, False, True, False, False, True,…
tf.equal()
函數返回布爾型的張量比較結果,只須要統計張量中 True 元素的個數,便可知道預測正確的個數。爲了達到這個目的,咱們先將布爾型轉換爲整形張量,再求和其中 1 的個數,能夠獲得比較結果中 True 元素的個數:
out = tf.cast(out, dtype=tf.float32) # 布爾型轉 int 型 correct = tf.reduce_sum(out) # 統計 True 的個數 Out[27]:<tf.Tensor: id=293, shape=(), dtype=float32, numpy=12.0>
能夠看到,咱們隨機產生的預測數據的準確度是
這也是隨機預測模型的正常水平。
除了比較相等的 tf.equal(a, b)函數,其餘的比較函數用法相似,如表格 5.1 所示:
對於圖片數據的高和寬、序列信號的長度,維度長度可能各不相同。爲了方便網絡的並行計算,須要將不一樣長度的數據擴張爲相同長度,以前咱們介紹了經過複製的方式能夠增長數據的長度,可是重複複製數據會破壞原有的數據結構,並不適合於此處。一般的作法是,在須要補充長度的信號開始或結束處填充足夠數量的特定數值,如 0,使得填充後的長度知足系統要求。那麼這種操做就叫作填充(Padding)。
考慮 2 個句子張量,每一個單詞使用數字編碼的方式,如 1 表明 I,2 表明 like 等。第一個句子爲:
「I like the weather today.」
咱們假設句子編碼爲:[1,2,3,4,5,6],第二個句子爲:
「So do I.」
它的編碼爲:[7,8,1,6]。爲了可以保存在同一個張量中,咱們須要將這兩個句子的長度保持一致,也就是說,須要將第二個句子的長度擴充爲 6。常見的填充方案是在句子末尾填充若干數量的 0,變成:
[7,8,1,6,0,0]
此時這兩個句子堆疊合並 shape 爲[2,6]的張量。
填充操做能夠經過 tf.pad(x, paddings)
函數實現,paddings 是包含了多個[𝐿𝑒𝑓𝑡 𝑃𝑎𝑑𝑑𝑖𝑛𝑔, 𝑅𝑖𝑔ℎ𝑡 𝑃𝑎𝑑𝑑𝑖𝑛𝑔]的嵌套方案 List.
如[[0,0],[2,1],[1,2]]表示第一個維度不填充,第二個維度左邊(起始處)填充兩個單元,右邊(結束處)填充一個單元,第三個維度左邊填充一個單元,右邊填充兩個單元。
考慮上述 2 個句子的例子,須要在第二個句子的第一個維度的右邊填充 2 個單元,則 paddings 方案爲[[0,2]]:
a = tf.constant([1,2,3,4,5,6]) b = tf.constant([7,8,1,6]) b = tf.pad(b, [[0,2]]) # 填充 b Out[28]:<tf.Tensor: id=3, shape=(6,), dtype=int32, numpy=array([7, 8, 1, 6, 0, 0])>
填充後句子張量形狀一致,再將這 2 句子 Stack 在一塊兒:
tf.stack([a,b],axis=0) # 合併 Out[29]:<tf.Tensor: id=5, shape=(2, 6), dtype=int32, numpy= array([[1, 2, 3, 4, 5, 6], [7, 8, 1, 6, 0, 0]])>
在天然語言處理中,須要加載不一樣句子長度的數據集,有些句子長度較小,如 10 個單詞左右,部份句子長度較長,如超過 100 個單詞。爲了可以保存在同一張量中,通常會選取可以覆蓋大部分句子長度的閾值,如 80 個單詞:對於小於 80 個單詞的句子,在末尾填充相應數量的 0;對大於 80 個單詞的句子,截斷超過規定長度的部分單詞。以 IMDB 數據集的加載爲例:
total_words = 10000 # 設定詞彙量大小 max_review_len = 80 # 最大句子長度 embedding_len = 100 # 詞向量長度 # 加載 IMDB 數據集 (x_train, y_train), (x_test, y_test) =keras.datasets.imdb.load_data(num_words=total_words) # 將句子填充或截斷到相同長度,設置爲末尾填充和末尾截斷方式 x_train = keras.preprocessing.sequence.pad_sequences( x_train,maxlen=max_review_len,truncating='post',padding='post') x_test = keras.preprocessing.sequence.pad_sequences( x_test,maxlen=max_review_len,truncating='post',padding='post') print(x_train.shape, x_test.shape) Out[30]: (25000, 80) (25000, 80)
上述代碼中,咱們將句子的最大長度 max_review_len
設置爲 80 個單詞,經過keras.preprocessing.sequence.pad_sequences
能夠快速完成句子的填充和截斷工做,以其中某個句子爲例:
[ 1 778 128 74 12 630 163 15 4 1766 7982 1051 2 32 85 156 45 40 148 139 121 664 665 10 10 1361 173 4 749 2 16 3804 8 4 226 65 12 43 127 24 2 10 10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
能夠看到在句子末尾填充了若干數量的 0,使得句子的長度恰好 80。實際上,也能夠選擇句子長度不夠時,在句子前面填充 0;句子長度過長時,截斷句首的單詞。通過處理後,全部的句子長度都變爲 80,從而訓練集能夠保存 shape 爲[25000,80]的張量,測試集能夠保存 shape 爲[25000,80]的張量。
咱們介紹對多個維度進行填充的例子。考慮對圖片的高寬維度進行填充。以 28x28 大小的圖片數據爲例,若是網絡層所接受的數據高寬爲 32x32,則必須將 28x28 大小填充到32x32,能夠在上、下、左、右方向各填充 2 個單元,以下圖 5.2 所示:
上述填充方案能夠表達爲[[0,0],[2,2],[2,2],[0,0]],實現爲:
x = tf.random.normal([4,28,28,1]) # 圖片上下、左右各填充 2 個單元 tf.pad(x,[[0,0],[2,2],[2,2],[0,0]]) <tf.Tensor: id=16, shape=(4, 32, 32, 1), dtype=float32, numpy= array([[[[ 0. ], [ 0. ], [ 0. ],…
經過填充操做後,圖片的大小變爲 32x32,知足神經網絡的輸入要求。
在維度變換一節,咱們就介紹了經過 tf.tile()
函數實現長度爲 1 的維度複製的功能。tf.tile
函數除了能夠對長度爲 1 的維度進行復制若干份,還能夠對任意長度的維度進行復制若干份,進行復制時會根據原來的數據次序重複複製。因爲已經介紹過,此處僅做簡單回顧。
經過 tf.tile
函數能夠在任意維度將數據重複複製多份,如 shape 爲[4,32,32,3]的數據,複製方案 multiples=[2,3,3,1],即通道數據不復制,高寬方向分別複製 2 份,圖片數再複製1 份:
x = tf.random.normal([4,32,32,3]) tf.tile(x,[2,3,3,1]) # 數據複製 Out[32]:<tf.Tensor: id=25, shape=(8, 96, 96, 3), dtype=float32, numpy= array([[[[ 1.20957184e+00, 2.82766962e+00, 1.65782201e+00], [ 3.85402292e-01, 2.00732923e+00, -2.79068202e-01], [-2.52583921e-01, 7.82584965e-01, 7.56870627e-01],...
考慮怎麼實現非線性激活函數 ReLU 的問題。它其實能夠經過簡單的數據限幅運算實現,限制數據的範圍𝑥 ∈ [0, +∞)便可。
在 TensorFlow 中,能夠經過 tf.maximum(x, a)
實現數據的下限幅:𝑥 ∈ [𝑎, +∞);能夠經過 tf.minimum(x, a)
實現數據的上限幅:𝑥 ∈ (−∞,𝑎],舉例以下:
x = tf.range(9) tf.maximum(x,2) # 下限幅 2 Out[33]:<tf.Tensor: id=48, shape=(9,), dtype=int32, numpy=array([2, 2, 2, 3, 4, 5, 6, 7, 8])> In [34]:tf.minimum(x,7) # 上限幅 7 Out[34]:<tf.Tensor: id=41, shape=(9,), dtype=int32, numpy=array([0, 1, 2, 3, 4, 5, 6, 7, 7])>
那麼 ReLU 函數能夠實現爲:
def relu(x): return tf.minimum(x,0.) # 下限幅爲 0 便可
經過組合 tf.maximum(x, a)
和 tf.minimum(x, b)
能夠實現同時對數據的上下邊界限幅:𝑥 ∈ [𝑎, 𝑏]:
x = tf.range(9) tf.minimum(tf.maximum(x,2),7) # 限幅爲 2~7 Out[35]:<tf.Tensor: id=57, shape=(9,), dtype=int32, numpy=array([2, 2, 2, 3, 4, 5, 6, 7, 7])>
更方便地,咱們能夠使用 tf.clip_by_value
實現上下限幅:
x = tf.range(9) tf.clip_by_value(x,2,7) # 限幅爲 2~7 Out[36]:<tf.Tensor: id=66, shape=(9,), dtype=int32, numpy=array([2, 2, 2, 3, 4, 5, 6, 7, 7])>
上述介紹的操做函數大部分都是常有而且容易理解的,接下來咱們將介紹部分經常使用,可是稍複雜的功能函數。
tf.gather
能夠實現根據索引號收集數據的目的。考慮班級成績冊的例子,共有 4 個班級,每一個班級 35 個學生,8 門科目,保存成績冊的張量 shape 爲[4,35,8]。
x = tf.random.uniform([4,35,8],maxval=100,dtype=tf.int32)
如今須要收集第 1-2 個班級的成績冊,能夠給定須要收集班級的索引號:[0,1],班級的維度 axis=0:
tf.gather(x,[0,1],axis=0) # 在班級維度收集第 1-2 號班級成績冊 Out[38]:<tf.Tensor: id=83, shape=(2, 35, 8), dtype=int32, numpy= array([[[43, 10, 93, 85, 75, 87, 28, 19], [52, 17, 44, 88, 82, 54, 16, 65], [98, 26, 1, 47, 59, 3, 59, 70],…
實際上,對於上述需求,經過切片𝑥[: 2]能夠更加方便地實現。可是對於不規則的索引方式,好比,須要抽查全部班級的第 1,4,9,12,13,27 號同窗的成績,則切片方式實現起來很是麻煩,而 tf.gather
則是針對於此需求設計的,使用起來很是方便:
# 收集第 1,4,9,12,13,27 號同窗成績 tf.gather(x,[0,3,8,11,12,26],axis=1) Out[39]:<tf.Tensor: id=87, shape=(4, 6, 8), dtype=int32, numpy= array([[[43, 10, 93, 85, 75, 87, 28, 19], [74, 11, 25, 64, 84, 89, 79, 85],…
若是須要收集全部同窗的第 3,5 等科目的成績,則能夠:
tf.gather(x,[2,4],axis=2) # 第 3,5 科目的成績 Out[40]:<tf.Tensor: id=91, shape=(4, 35, 2), dtype=int32, numpy= array([[[93, 75], [44, 82], [ 1, 59],…
能夠看到,tf.gather
很是適合索引沒有規則的場合,其中索引號能夠亂序排列,此時收集的數據也是對應順序:
a=tf.range(8) a=tf.reshape(a,[4,2]) # 生成張量 a Out[41]:<tf.Tensor: id=115, shape=(4, 2), dtype=int32, numpy= array([[0, 1], [2, 3], [4, 5], [6, 7]])> tf.gather(a,[3,1,0,2],axis=0) # 收集第 4,2,1,3 號元素 Out[42]:<tf.Tensor: id=119, shape=(4, 2), dtype=int32, numpy= array([[6, 7], [2, 3], [0, 1], [4, 5]])>
咱們將問題變得複雜一點:若是但願抽查第[2,3]班級的第[3,4,6,27]號同窗的科目成績,則能夠經過組合多個 tf.gather
實現。首先抽出第[2,3]班級:
students=tf.gather(x,[1,2],axis=0) # 收集第 2,3 號班級 <tf.Tensor: id=227, shape=(2, 35, 8), dtype=int32, numpy= array([[[ 0, 62, 99, 7, 66, 56, 95, 98],…
再從這 2 個班級的同窗中提取對應學生成績:
tf.gather(students,[2,3,5,26],axis=1) # 收集第 3,4,6,27 號同窗 Out[44]:<tf.Tensor: id=231, shape=(2, 4, 8), dtype=int32, numpy= array([[[69, 67, 93, 2, 31, 5, 66, 65], …
此時獲得這 2 個班級 4 個同窗的成績,shape 爲[2,4,8]。
咱們再將問題進一步複雜:此次咱們但願抽查第 2 個班級的第 2 個同窗的全部科目,第 3 個班級的第 3 個同窗的全部科目,第 4 個班級的第 4 個同窗的全部科目。那麼怎麼實現呢?
能夠經過笨方式一個一個的手動提取:首先提取第一個採樣點的數據:𝑥[1,1],可獲得8 門科目的數據向量:
x[1,1] # 收集第 2 個班級的第 2 個同窗 Out[45]:<tf.Tensor: id=236, shape=(8,), dtype=int32, numpy=array([45, 34, 99, 17, 3, 1, 43, 86])>
再串行提取第二個採樣點的數據:𝑥[2,2],和第三個採樣點的數據𝑥[3,3],最後經過 stack
方式合併採樣結果:
In [46]: tf.stack([x[1,1],x[2,2],x[3,3]],axis=0) Out[46]:<tf.Tensor: id=250, shape=(3, 8), dtype=int32, numpy= array([[45, 34, 99, 17, 3, 1, 43, 86], [11, 25, 84, 95, 97, 95, 69, 69], [ 0, 89, 52, 29, 76, 7, 2, 98]])>
這種方法也能正確的獲得 shape 爲[3,8]的結果,其中 3 表示採樣點的個數,4 表示每一個採樣點的數據。可是它最大的問題在於手動串行方式執行採樣,計算效率極低。有沒有更好的方式實現呢?這就是下一節要介紹的 tf.gather_nd 的功能。
經過 tf.gather_nd
,能夠經過指定每次採樣的座標來實現採樣多個點的目的。
回到上面的挑戰,咱們但願抽查第 2 個班級的第 2 個同窗的全部科目,第 3 個班級的第 3 個同窗的全部科目,第 4 個班級的第 4 個同窗的全部科目。
那麼這 3 個採樣點的索引座標能夠記爲:[1,1],[2,2],[3,3],咱們將這個採樣方案合併爲一個 List 參數:[[1,1],[2,2],[3,3]],經過tf.gather_nd 實現以下:
# 根據多維度座標收集數據 tf.gather_nd(x,[[1,1],[2,2],[3,3]]) Out[47]:<tf.Tensor: id=256, shape=(3, 8), dtype=int32, numpy= array([[45, 34, 99, 17, 3, 1, 43, 86], [11, 25, 84, 95, 97, 95, 69, 69], [ 0, 89, 52, 29, 76, 7, 2, 98]])>
能夠看到,結果與串行採樣徹底一致,實現更加簡潔,計算效率大大提高。
通常地,在使用 tf.gather_nd 採樣多個樣本時,若是但願採樣第 i 號班級,第 j 個學生,第 k 門科目的成績,則能夠表達爲[. . . ,[𝑖,𝑗, 𝑘], . . .],外層的括號長度爲採樣樣本的個數,內層列表包含了每一個採樣點的索引座標:
# 根據多維度座標收集數據 tf.gather_nd(x,[[1,1,2],[2,2,3],[3,3,4]]) Out[48]:<tf.Tensor: id=259, shape=(3,), dtype=int32, numpy=array([99, 95, 76])>
上述代碼中,咱們抽出了班級 1,學生 1 的科目 2;班級 2,學生 2 的科目 3;班級 3,學生 3 的科目 4 的成績,共有 3 個成績數據,結果彙總爲一個 shape 爲[3]的張量。
除了能夠經過給定索引號的方式採樣,還能夠經過給定掩碼(mask)的方式採樣。繼續以 shape 爲[4,35,8]的成績冊爲例,此次咱們以掩碼方式進行數據提取。考慮在班級維度上進行採樣,對這 4 個班級的採樣方案的掩碼爲
𝑚𝑎𝑠𝑘 = [𝑇𝑟𝑢𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝑇𝑟𝑢𝑒]
即採樣第 1 和第 4 個班級,經過 tf.boolean_mask(x, mask, axis)
能夠在 axis 軸上根據 mask 方案進行採樣,實現爲:
# 根據掩碼方式採樣班級 tf.boolean_mask(x,mask=[True, False,False,True],axis=0) <tf.Tensor: id=288, shape=(2, 35, 8), dtype=int32, numpy= array([[[43, 10, 93, 85, 75, 87, 28, 19],…
注意掩碼的長度必須與對應維度的長度一致,如在班級維度上採樣,則必須對這 4 個班級是否採樣的掩碼所有指定,掩碼長度爲 4。
若是對 8 門科目進行掩碼採樣,設掩碼採樣方案爲:
𝑚𝑎𝑠𝑘 = [𝑇𝑟𝑢𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝑇𝑟𝑢𝑒, 𝑇𝑟𝑢𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝐹𝑎𝑙𝑠𝑒, 𝑇𝑟𝑢𝑒]
則能夠實現爲:
# 根據掩碼方式採樣科目 tf.boolean_mask(x,mask=[True,False,False,True,True,False,False,True],axis=2) <tf.Tensor: id=318, shape=(4, 35, 4), dtype=int32, numpy= array([[[43, 85, 75, 19],…
不難發現,這裏的 tf.boolean_mask
的用法其實與 tf.gather
很是相似,只不過一個經過掩碼方式採樣,一個直接給出索引號採樣。
如今咱們來考慮與 tf.gather_nd
相似方式的多維掩碼採樣方式。爲了方便演示,咱們將班級數量減小到 2 個,學生的數量減小到 3 個,即一個班級只有 3 個學生,shape 爲[2,3,8]。
若是但願採樣第 1 個班級的第 1-2 號學生,第 2 個班級的第 2-3 號學生,經過tf.gather_nd
能夠實現爲:
x = tf.random.uniform([2,3,8],maxval=100,dtype=tf.int32) tf.gather_nd(x,[[0,0],[0,1],[1,1],[1,2]]) # 多維座標採集 <tf.Tensor: id=325, shape=(4, 8), dtype=int32, numpy= array([[52, 81, 78, 21, 50, 6, 68, 19], [53, 70, 62, 12, 7, 68, 36, 84], [62, 30, 52, 60, 10, 93, 33, 6], [97, 92, 59, 87, 86, 49, 47, 11]])>
共採樣 4 個學生的成績,shape 爲[4,8]。
若是用掩碼方式,怎麼表達呢?以下表格 5.2 所示,行爲每一個班級,列爲對應學生,表中數據表達了對應位置的採樣狀況:
所以,經過這張表,就能很好地表徵利用掩碼方式的採樣方案:
# 多維掩碼採樣 tf.boolean_mask(x,[[True,True,False],[False,True,True]]) <tf.Tensor: id=354, shape=(4, 8), dtype=int32, numpy= array([[52, 81, 78, 21, 50, 6, 68, 19], [53, 70, 62, 12, 7, 68, 36, 84], [62, 30, 52, 60, 10, 93, 33, 6], [97, 92, 59, 87, 86, 49, 47, 11]])>
採樣結果與 tf.gather_nd
徹底一致。可見 tf.boolean_mask
既能夠實現了tf.gather
方式的一維掩碼採樣,又能夠實現 tf.gather_nd
方式的多維掩碼採樣。
上面的 3 個操做比較經常使用,尤爲是 tf.gather
和 tf.gather_nd
出現的頻率較高,必須掌握。下面再補充 3 個高階操做。
經過 tf.where(cond, a, b)
操做能夠根據 cond 條件的真假從 a 或 b 中讀取數據,條件斷定規則以下:
其中 i 爲張量的索引,返回張量大小與 a,b 張量一致,當對應位置中
爲 True,
位置從
中複製數據;當對應位置中
爲 False,
位置從
中複製數據。
考慮從 2 個全 一、全 0 的 3x3 大小的張量 a,b 中提取數據,其中 cond 爲 True 的位置從 a 中對應位置提取,cond 爲 False 的位置從 b 對應位置提取:
a = tf.ones([3,3]) # 構造 a 爲全 1 b = tf.zeros([3,3]) # 構造 b 爲全 0 # 構造採樣條件 cond =tf.constant([[True,False,False],[False,True,False],[True,True,False]]) tf.where(cond,a,b) # 根據條件從 a,b 中採樣 Out[53]:<tf.Tensor: id=384, shape=(3, 3), dtype=float32, numpy= array([[1., 0., 0.], [0., 1., 0.], [1., 1., 0.]], dtype=float32)>
能夠看到,返回的張量中爲 1 的位置來自張量 a,返回的張量中爲 0 的位置來自張量 b。當 a=b=None 即 a,b 參數不指定時,tf.where
會返回 cond 張量中全部 True 的元素的索引座標。考慮以下 cond 張量:
cond # 構造 cond Out[54]:<tf.Tensor: id=383, shape=(3, 3), dtype=bool, numpy= array([[ True, False, False], [False, True, False], [ True, True, False]])>
其中 True 共出現 4 次,每一個 True 位置處的索引分佈爲[0,0],[1,1],[2,0],[2,1],能夠直接經過 tf.where(cond)
來得到這些索引座標:
tf.where(cond) # 獲取 cond 中爲 True 的元素索引 Out[55]:<tf.Tensor: id=387, shape=(4, 2), dtype=int64, numpy= array([[0, 0], [1, 1], [2, 0], [2, 1]], dtype=int64)>
那麼這有什麼用途呢?考慮一個例子,咱們須要提取張量中全部正數的數據和索引。首先構造張量 a,並經過比較運算獲得全部正數的位置掩碼:
x = tf.random.normal([3,3]) # 構造 a Out[56]:<tf.Tensor: id=403, shape=(3, 3), dtype=float32, numpy= array([[-2.2946844 , 0.6708417 , -0.5222212 ], [-0.6919401 , -1.9418817 , 0.3559235 ], [-0.8005251 , 1.0603906 , -0.68819374]], dtype=float32)>
經過比較運算,獲得正數的掩碼:
mask=x>0 # 比較操做,等同於 tf.equal() mask Out[57]:<tf.Tensor: id=405, shape=(3, 3), dtype=bool, numpy= array([[False, True, False], [False, False, True], [False, True, False]])>
經過 tf.where
提取此掩碼處 True 元素的索引:
indices=tf.where(mask) # 提取全部大於 0 的元素索引 Out[58]:<tf.Tensor: id=407, shape=(3, 2), dtype=int64, numpy= array([[0, 1], [1, 2], [2, 1]], dtype=int64)>
拿到索引後,經過 tf.gather_nd 便可恢復出全部正數的元素:
tf.gather_nd(x,indices) # 提取正數的元素值 Out[59]:<tf.Tensor: id=410, shape=(3,), dtype=float32, numpy=array([0.6708417, 0.3559235, 1.0603906], dtype=float32)>
實際上,當咱們獲得掩碼 mask 以後,也能夠直接經過 tf.boolean_mask
獲取對於元素:
tf.boolean_mask(x,mask) # 經過掩碼提取正數的元素值 <tf.Tensor: id=439, shape=(3,), dtype=float32, numpy=array([0.6708417, 0.3559235, 1.0603906], dtype=float32)>
結果也是一致的。
經過 tf.scatter_nd(indices, updates, shape)
能夠高效地刷新張量的部分數據,可是只能在全 0 張量的白板上面刷新,所以可能須要結合其餘操做來實現現有張量的數據刷新功能。
以下圖 5.3 所示,演示了一維張量白板的刷新運算,白板的形狀表示爲 shape 參數,須要刷新的數據索引爲 indices,新數據爲 updates,其中每一個須要刷新的數據對應在白板中的位置,根據 indices 給出的索引位置將 updates 中新的數據依次寫入白板中,並返回更新後的白板張量。
咱們實現一個圖 5.3 中向量的刷新實例:
# 構造須要刷新數據的位置 indices = tf.constant([[4], [3], [1], [7]]) # 構造須要寫入的數據 updates = tf.constant([4.4, 3.3, 1.1, 7.7]) # 在長度爲 8 的全 0 向量上根據 indices 寫入 updates tf.scatter_nd(indices, updates, [8]) Out[61]:<tf.Tensor: id=467, shape=(8,), dtype=float32, numpy=array([0. , 1.1, 0. , 3.3, 4.4, 0. , 0. , 7.7], dtype=float32)>
能夠看到,在長度爲 8 的白板上,寫入了對應位置的數據,一個 4 個新數據被刷新。考慮 3 維張量的刷新例子,以下圖 5.4 所示,白板 shape 爲[4,4,4],共有 4 個通道的特徵圖,現有需 2 個通道的新數據 updates:[2,4,4],須要寫入索引爲[1,3]的通道上:
咱們將新的特徵圖寫入現有白板張量,實現以下:
# 構造寫入位置 indices = tf.constant([[1],[3]]) updates = tf.constant([# 構造寫入數據 [[5,5,5,5],[6,6,6,6],[7,7,7,7],[8,8,8,8]], [[1,1,1,1],[2,2,2,2],[3,3,3,3],[4,4,4,4]] ]) # 在 shape 爲[4,4,4]白板上根據 indices 寫入 updates tf.scatter_nd(indices,updates,[4,4,4]) Out[62]:<tf.Tensor: id=477, shape=(4, 4, 4), dtype=int32, numpy= array([[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[5, 5, 5, 5], # 寫入的新數據 1 [6, 6, 6, 6], [7, 7, 7, 7], [8, 8, 8, 8]], [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]], [[1, 1, 1, 1], # 寫入的新數據 2 [2, 2, 2, 2], [3, 3, 3, 3], [4, 4, 4,