讀懂操作系統之虛擬內存地址翻譯原理分析篇(二)_網頁設計公司

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

以設計的實用美學觀點,規劃出舒適、美觀的視覺畫面,有效提昇使用者的心理期待,營造出輕鬆、愉悅的網站瀏覽體驗。

前言

上一節我們整體概括通過MMU將虛擬地址翻譯為物理地址的轉換,這個過程都是按序就班的進行,一切都是基於已提前創建、分配虛擬頁、物理頁以及命中的前提,只是給和我一樣沒怎麼系統學習操作系統的童鞋首先在腦海里有個大概的印象,本節我們從源頭開始分析為程序創建進程到映射到主存上整個詳細過程,本文將通過大量圖解來分析原理,以便讓各位能夠完全理解地址翻譯原理。若有敘述不當之處,還請批評指正。 

虛擬內存原理分析

如果沒有系統學習現代操作系統,理論上我們會認為用戶程序會將內存視為單個連續的內存空間,實際上可以將用戶程序在內存中分佈可以分散在頁面的整個物理內存中。分頁是一種內存管理方案,它允許進程的物理地址空間不連續。

物理內存劃分:將物理內存劃分為稱為幀的固定大小的塊(大小為2的冪,介於512字節和16 MB之間,必須跟蹤所有空閑幀)

虛擬(邏輯)內存劃分:將邏輯內存分成大小相同的塊(稱為頁,每一塊也是分為相同大小的頁面)

若要運行大小為N頁的程序,需要找到N個空閑幀並加載程序

地址翻譯方案

通過常駐內存中的頁表將虛擬地址翻譯為物理地址, CPU生成的虛擬地址被劃分為虛擬頁號(用作頁表索引,該頁表包含物理內存中每個頁的基地址)和虛擬頁偏移量(與基址結合找到存儲單元的物理存儲地址)。對於給定的邏輯地址空間2m和頁面大小2n,如下:

分頁內存管理方案本質就是通過MMU將CPU產生的虛擬地址通過中間媒介(頁表)進行地址翻譯,如下為簡單翻譯版本,一目瞭然。

上述我們學習了將邏輯地址(虛擬地址)劃分為頁號(注意:頁號並不屬於頁表的一部分,頁號不儲存在主存)和頁偏移量,到底是怎樣藉助頁號和頁偏移量進行翻譯的呢?我們舉個例子:假設如下一個32字節的物理內存,邏輯地址空間為16字節,說明邏輯地址有4位,而頁幀偏移量為4個字節,因頁幀偏移量和虛擬頁偏移量相等,所以虛擬頁偏移量也為4個字節即2位,所以頁號為(4-2)= 2位即邏輯地址共有4頁,如此假設和實際理論計算對等。地址翻譯如下:

若CPU要找出邏輯地址為4的物理地址,通過上述我們知道邏輯地址為4在第1頁且偏移量為0,然後去查找頁表索引等於1的頁幀號為6,因為物理地址 = (frame * pageSize)+ offset,所以邏輯地址4的物理地址=(6 * 4 bytes)+ 0 byte offset = 24。同理,比如如上邏輯地址為7在第1頁,偏移量為3對應頁表上的幀6,所以其物理地址為:(6 * 4 bytes)+ 3 byte offset = 27,這裏需要注意的是物理地址的偏移量是相對這頁的起始位置偏移。通過上述圖解,我們反推根據邏輯地址和每頁字節大小計算出其所在頁和偏移量(下面根據虛擬地址計算虛擬頁號和偏移量會用到),比如邏輯地址為7,因每頁大小為4個字節,則所在頁為(7 / 4) = 1,偏移量(7 mod / 4) = 3。

擴展頁表條目(PTE)信息

現代計算機頁表上的條目除了包含將虛擬地址翻譯為虛擬地址的主要信息(有效位、頁號)外,其中還包含如下其他信息(下面講解頁面置換算法會用到):

保護位(Protection):控制對指定虛擬頁的訪問是否可讀、可寫、可執行

引用位(Refrence):為了近似實現LRU算法,幫助操作系統估算最近最少使用的頁,當一頁被訪問時該位將被置位,操作系統定期將引用位清零,然後重新記錄,這樣就可以判定在這段特定時間內哪些頁被訪問過,通過檢查引用位是否關閉,操作系統就可以從那些最近最少訪問的頁中選擇一頁

臟位(Modify):當某一頁被替換時,操作系統需要知道該頁是否需要被複制寫回,為了追蹤讀入主存中的頁是否被寫過,增加一個臟位,當頁中任何字被寫時就將這一位置位。如果操作系統選擇替換某一頁,臟位指明了把該頁所佔用的主存讓給另一頁之前,是否需要將該頁寫回磁盤,因此,一個被修改的頁通常被稱為臟頁。

TLB緩存頁表

上一節我們講過CPU產生邏輯地址后通過MMU轉換為物理地址時,每次都要訪問頁表,訪問緩存和主存的時間相差上百個時鐘周期,所以為了提高查找性能則使用TLB,我們可認為TLB是實現頁表最好的方式,本質上是緩存頁表。在沒有TLB作為緩存時,我們使用頁號(VPN)作為索引去頁表上查找物理頁號,引入TLB后,將頁號劃分為TLBT(TLB標記)和TLBI(TLB索引)只是做了一下轉換而已,TLBI佔2位,剩餘的位就是TLBT。下面會通過一個實際例子來講解如何結合TLBT和TLBI在TLB上查找。

 

TLB作為頁表的緩存,用於存放映射到頁幀中的那些項,TLB包含了頁表中虛擬頁到頁幀映射的一個子集,因為將其作為緩存,所以額外還存在如上一個標記區域(TLBT),換句話說頁表不同於TLB並不是作為緩存,所以並不需要標記區域,再加上如上額外的PTE擴展信息,所以TLB的存儲結構如下:

TLB缺失

接下來我們開始進入TLB缺失環節,我們假設虛擬地址有14位,物理地址有16位,每頁大小有64個字節,那麼虛擬地址空間和物理地址空間如下圖所示

因為每頁大小為64字節即(26),同時虛擬頁偏移量和頁幀偏移量相等,所以虛擬頁偏移量和頁幀偏移量都為6位,那麼將虛擬地址空間和物理地址空間劃分為對應的頁號和頁偏移量則如下圖所示:

接下來則是將虛擬頁號劃分為TLBT和TLBI,因為TLB包含16個條目且4路關聯,那麼說明有S =(16 / 4)= 4組,那麼TLBI佔位 =  log2S = 2,剩餘的則是TLBT = (8 – 2) = 6位,如下圖所示

現在我們對虛擬地址和物理地址都有了完整的劃分,現在假設TLB和頁表狀態存儲結構分別如下圖

 

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

台中景泰電動車行只是一個單純的理由,將來台灣的環境,出門可以自由放心的深呼吸,讓空氣回歸自然的乾淨,減少污染,留給我們下一代有好品質無空污的優質環境

假設現在CPU產生一個虛擬地址(0x0334),首先我們需要將其轉換為虛擬頁號(VPN),因每個頁面大小為64字節,所以計算方式如下代碼

var xvpn = Convert.ToInt32("0x334", 16);
var vpn = xvpn / 64; //vpn = 12
var vpo = xvpn % 64; //vpo = 52

上述計算出VPN等於12,然後將其對應虛擬地址上的VPN和VPO用二進製表示,分別如下圖所示

而存儲在TLB和頁表上的狀態都是16進制,所以上述VPN = 1210 = 0x0C16和VPO = 5210 = 0x3416,到此已經劃分完VPN和VPO,接下來則是將VPN劃分為TLBT和TLBI,由上述我們已經知道TLBT和TLBI在VPN中所佔位數,所以如下圖所示

由上我們可得出TLBT = 310 = 0x0316,而TLBI = 0,有了TLBT(0x03)和TLBI(0)再去查找TLB狀態表,如下紅色標記

由上圖我們發現此時標誌無效而且物理頁號也沒有,此時發生TLB缺失,於是通過MMU將虛擬地址得到的VPN去頁表中查找

此時我們看到在頁表中也缺失,所以這裏將發生缺頁異常。TLB缺失分為如下兩種情況

頁在主存(頁表)中,只需要創建缺失的TLB表項

頁不在主存(頁表)中,需要將控制權交給操作系統來解決缺頁 

TLB缺失既可以通過軟件處理也可以通過硬件處理,比如MIPS、Alpha通過軟件處理TLB缺失,x86、ARM通過硬件處理TLB缺失,兩種處理方式在性能差別上很小,無論哪一種方式需要執行的基本操作都是一樣的。理論上來講,在進程分配頁幀時會將對應頁幀更新到頁表上,但是上述情況並未在主存頁表中說明在頁幀列表中沒有空閑的頁幀,所以這是TLB缺失中真正的缺頁情況,此時將觸發缺頁異常,控制權交給操作系統內核中的缺頁異常處理程序,操作系統知道了引起缺頁的虛擬地址,操作系統必須完成以下3個步驟:【1】使用虛擬地址查找頁表項,並在磁盤上找到被訪問的頁的位置【2】選擇替換一個物理頁,如果該選中的頁被修改過,需要在把新的虛擬頁裝入之前將這個物理頁寫回磁盤,這一過程稱為頁面置換【3】啟動讀操作,將被訪問的頁從磁盤上取回到所選擇的物理頁的位置上【4】重新執行引發缺頁的那條指令。因為第3個步驟需要耗費數百萬個時鐘周期,如果第2個步驟中被替換的物理頁已被重寫過,那麼同樣也會花費這麼長時間,因此操作系統會選擇另外一個進程在處理器上執行直到磁盤訪問結束,所以前3個步驟執行所耗費的時間比較長,最後重新執行缺頁指令。若在頁表中找到了頁幀號(即頁在主存中),那說明TLB缺失只是一次轉換缺失,在這種情況下,CPU只需要將頁表項裝載到TLB並且重新訪問來進行缺失處理。

頁面置換算法

為了解決缺頁情況,所以必須實現頁面置換作為請求調頁的基礎,這裏我們介紹常見的幾種置換算法,分別是Optional or MIN algorithm、FIFO(First-In-First-Out)、Clock、LRU(Least Recently Used),針對各個算法,現假設有(1、2、3、4、1、2、5、1、2、3、4、5)12個引用串,4個空閑頁幀。

FIFO(先進先出)

該算法記錄了每個頁面記錄調到內存的時間,當必須置換頁面時,將選擇最舊的頁面,請注意,並不需要記錄調入頁面的確切時間,可以通過創建一個隊列實現此目的。具體過程太過簡單,這裏就不再細講,此時將發生10次缺頁錯誤,我們可計算出缺頁率為(10/12)= 83%。如下:

 

OPT or MIN(最優)

最優置換算法找出最長時間沒有使用的頁,具有最低缺頁率,可以用作離線分析方法,但是難以實現。此時將發生6次缺頁錯誤,我們可計算出缺頁率為(6/12)= 50%。如下:

 

LRU(最近最少使用)

FIFO算法使用的是頁面調入內存的時間,OPT算法使用的是頁面將來使用的時間,而LRU算法採用置換最長時間沒有的頁,該算法將每個頁面與它上次使用的時間關聯起來,當需要置換頁面時,LRU選擇最長時間沒有使用的頁面,該算法很難實現。此時將發生8次缺頁錯誤,我們可計算出缺頁率為(8/12)= 67%。如下:

啟動和切換進程

上述我們只是從已經將程序加載到內存中所創建的進程角度來分析如何將虛擬地址翻譯為物理地址,由於操作系統負責管理內存,因此必須了解物理內存的分配詳細信息,分配了哪些頁幀、每個頁幀分配個哪個進程的哪個頁面,哪些頁幀可用,總共有多少幀,對此我們還一無所知。將用戶程序加載到虛擬內存中的進程後為其劃分對應的虛擬頁,假設如下劃分了4個虛擬頁,操作系統在跟蹤的頁幀列表找出空閑(操作系統分配幀算法,這裏暫不討論)的頁幀分配給虛擬頁,然後操作系統再啟動進程。如下圖:

 

如上節所述頁表保存在主存中,當調度進程時通過頁表基址寄存器(PTBR)指向激活的指定進程頁表, 當然也會加載另外一個寄存器(程序計數器,PC),所以每個數據或指令訪問需要進行兩次主存訪問,一次是頁表,另一次則是用於數據或指令。

 

當進程希望以受限的方式共享信息時,操作系統必須對其進行協助,這是因為訪問另外一個進程的信息需要改變訪問進程的頁表,寫訪問位可以用來把共享限製為只讀,並且和頁表中其他位一樣,該位只能被操作系統所修改。為了允許另一進程,設為P1,去讀屬於進程P2的一頁,P2就要請求操作系統在P1地址空間中為一個虛擬頁生成頁表項,指向P2想要共享的物理頁。如果P2要求操作系統可以使用寫保護位以防止P1對數據進行改寫,由於只有TLB缺失才會訪問頁表,任何決定頁對的訪問權限不僅要包含在頁表中,還要包含在TLB中。當操作系統決定從進程P1切換到P2時,我們稱之為上下文切換,它必須保證P2不能P1的頁表,否則不利於數據保護,若沒有TLB,只需要把頁表基址寄存器指向P2的頁表而不是P1就夠了,如果有TLB,我們必須在其中清除屬於P1的表項,不僅僅是為了保護P1的數據,而且是為了迫使TLB裝入P2的表項。如果進程切換的頻率很高,那麼這一舉措效率將會很低。例如,在操作系統切回P1之前,P2可能只裝入了很少的TLB表項,但是,P1隨後發現它所有的表項都不見了,因此不得不通過TLB缺失來重新加載這些表項,產生這個問題的原因在於進程P1和P2使用同一虛擬地址空間,並且我們必須清除TLB以防止地址混淆。另一種常見的方法則是增加進程標識符和任務標識符來擴展虛擬地址空間,比如FastMATH就有8位地址空間標識域(ASID),這個標識域標識了當前正在運行的進程,當進程切換時,它保存在由操作系統裝入的寄存器中,進程標識符與TLB的標記部分相連接,因此只有在頁號和進程標識符相匹配時,TLB才會發生命中,如此一來,除非特殊情況,我們就不需要清除TLB。 說了怎麼多除了保護機制外,當我們切換進程時主要需要做哪些工作呢(即從一個進程控制塊(Process Control Block,PCB)切換到另一個進程塊,後續會深入講解操作系統線程和進程)?

切換頁表到當前PCB

頁表基址寄存器指向當前頁表

清除TLB,並將當前頁表項裝載到TLB(按需加載,進程訪問哪些頁才將對應頁表項加載到TLB)

留個作業

若TLB中的PTE條目達到上限即滿時,不難想象理論上會替換現有條目,那麼採取替換的策略或機制是怎樣的呢?

總結

基於上一節內容我們詳細講解了將虛擬地址翻譯為物理地址的具體過程、進程頁幀分配、頁面置換算法,在講解TLB缺失時並未涉及高速緩存,TLB和高速緩存將在下一節作為詳解。關於虛擬內存內容通過一兩篇文章根本講解不清楚,比如還有減少頁表容量方式、TLB和高速緩存關係、Intel和Linux虛擬內存系統等等。我盡量通過圖解方式來帶給大家較好的理解體驗,能夠更好的消化和吸收虛擬內存。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網站的第一印象網頁設計,決定了客戶是否繼續瀏覽的意願。台北網動廣告製作的RWD網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上它。

「MoreThanJava」一文了解二進制和CPU工作原理_如何寫文案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

  • 「MoreThanJava」 宣揚的是 「學習,不止 CODE」,本系列 Java 基礎教程是自己在結合各方面的知識之後,對 Java 基礎的一個總回顧,旨在 「幫助新朋友快速高質量的學習」
  • 當然 不論新老朋友 我相信您都可以 從中獲益。如果覺得 「不錯」 的朋友,歡迎 「關注 + 留言 + 分享」,文末有完整的獲取鏈接,您的支持是我前進的最大的動力!

Part 1. 原來,我們是這樣記數的

本節內容節選自下方 參考資料 1

在討論「二進制」和「CPU 如何工作」之前,我們先來討論一下我們生活中最稀疏平常的 数字,我們與之頻繁地打交道:一個約定的時間、一件商品的價格、一個人的身高….卻很少有人細細想過,這些数字是如何表達出來的?為什麼你理所當然地把 1024 理解為「一千零二十四」而不是別的含義?

也許你從未想過,在這簡單的記數中,沉澱着人類的大智慧。

一進制計數法

  • 圖片來源:https://www.goethe.de/ins/cn/cn/kul/mag/20629923.html

早在数字的概念產生之前,人類就學會了使用樹枝、石子、貝殼等自然界隨處可見的小物件表示獵物的、果實的、部落人口的數量。比如在某個角落堆上一堆石子,每打到 1 只獵物,就扔 1 顆石子進去,每吃掉 2 只獵物,就從中取走 2 顆石子。他們並不在意石子的總數,只是時不時地瞅一眼,心底大致有數。

其實這是一種最樸素的記數方式,數學家稱之為 一進制記數法(unary numeral system)。我們把它符號化一下,比如用斜杠 / 來表示:

  • 1 就是 /
  • 2 就是 //
  • 4 就是 ////

好像沒毛病,我們平時掰手指用的就是這種記數法,但数字一大,場面就要失控了。

符值相加記數法

為了解決記錄大數的問題,於是我們得發明一些其他符號來表示更大的數值,比如用橫杠 - 表示 10,用十字 + 表示 100。那麼:

  • 16 就是 -//////;
  • 32 就是 ---//;
  • 128 就是 +--////////;

漂亮….這種靠符號類型和符號數量表示数字的方法被稱為 符值相加記數法(sign-value notation),古埃及和古羅馬用的都是它,只不過符號各不相同。

古埃及的記數符號:

1 10 100 1000 10000 100000 1000000

1024 在古埃及就寫作:

你會發現,符值相加記數法的一大優點是,符號的順序可以任意打亂,数字含義不受影響。我國藏族曾用石子表示 1、木棍表示 10、果核表示 100、蠶豆表示 1000、瓦片表示 10000,那麼,當你把 1 顆蠶豆、2 根木棍和 4 顆石子胡亂地攥在手裡,別人依然知道它們是 1024

古羅馬的做法略有不同,他們對五進制情有獨鍾:

1 5 10 50 100 500 1000
I V X L C D M

這些符號沿用至今,想必大家(至少對前 3 個)都比較熟悉,許多鐘錶仍保留着使用羅馬数字的習慣,1~12 分別表示為:IIIIIIIVVVIVIIVIIIIXXXIXII。你會發現,羅馬記數法是符值相加記數法的變種,因為它不光「相加」,還「相減」。這種方式就不允許符號亂序了,IVVI 表示的是不同的数字。

那羅馬人何苦要使用這種更複雜的記數法呢?無非是為了讀寫方便。同樣表示 9IXVIIII 更簡潔。

其實有一種更好使的方法——用另外一些列符號來表示符號的數量。比如用 A 表示 1 個符號,用 B 表示 2 個符號,以此類推,用 I 表示 9 個符號。

如此,上文表示 256++-----////// 就可以寫作 B+E-F/。你一定感覺莫名其妙,這種寫法哪裡方便了。其實中文的數字錶示就是這種形式,只不過我們用得太習慣了,以至於沒有發現。

在中文中, 代替了 /-+,而 代替了 ABC256 就寫作 二百五十六個 比較累贅,我們通常把它省略了。

其實像日語、英語用的也同樣是這種記數法,簡潔、優雅。

美中不足的是,這種形式雖便於讀寫,卻不便於計算。中國古人為算籌和算盤這類經典算具搭建起廣闊的舞台,卻沒給筆算留出一席之地。想象一下,如果讓你把這些漢字寫在草稿紙上,列個豎式,你的內心一定非常彆扭。

位值制記數法

公元5世紀,印度數學家阿耶波多(Aryabhata 476–550)創立了現在廣泛使用的 位值制記數法(positional notation/place-value notation),該記數法使用的主要符號,是同為印度人發明的阿拉伯数字:0123456789

與符值相加記數法類比,位值制中的 123 代替的是 ABC,那 /-+ 呢?是 靠阿拉伯数字的位置來表示的。眾所周知,最右位相當於 /,次右位相當於 -靠每個位置上的數值來表示数字,故名位值制。

嚴謹的數學家用一種多項式高度概括了位值制記數法的本質,在十進制中,這個多項式是這樣的:

這是一個 n 位十進制數,ai 就是第 i 位上的數值。為便於直觀理解,舉個 1024 的例子吧:

由於我們熟悉了十進制,這樣費心費力的展開可能會讓你覺得好笑,但當我們把它推廣到其他進制時,這個多項式的價值就體現了出來。n 位 b 進制數的位值製表示:

1024 用二進制怎麼表示?

因此,1024 的二進制寫作 10000000000

除了最普遍的十進制和計算機中的二進制,常見的還有七進制(如 17 天)、十二進制(如 112 個月)、十六進制(如古代 116 兩)、六十進制(如六十甲子)等等,只要有意義,任何進制都可以為你所用。

非標準位值制

在上述的多項式中,如果 ai 或 b 的取值奇葩一點,就形成了 非標準位值制(non-standard positional numeral systems),這類記數法往往應用於專業領域,很難在日常生活中見到。比如標準位值制中的三進制 ai 的取值為 012,但在一種名為平衡三進制(balanced ternary)的非標準位值制中,ai-101,蘇聯曾使用這種進制研發电子計算機。

Part 2. 二進制簡介

  • 圖片來源:https://zhuanlan.zhihu.com/p/26743163

至此,你對「二進制」應該會感覺親切了些,它只是一種數制而已,本質上與我們熟悉的十進制沒有很大的差別,我們這一 Part 來稍微理解一下二進制。(至於電腦為什麼使用二進制我們在下一 Part 中介紹)

二進制的基本運算

十進制中的那些基本運算原則,二進制中同樣適用,只不過需要稍加變幻而已,下面我們分別就加、減、乘、除四則運算來介紹。

二級制加法

根據「逢二進一」規則,二進制數加法的法則為:

0+0=0
0+1=1+0=1
1+1=0 (進位為1) 
1+1+1=1 (進位為1)

例如:11011011 相加過程如下:

二進制數的減法

根據「借一有二」的規則,二進制數減法的法則為:

0-0=0
1-1=0
1-0=1
0-1=1 (借位為1)

例如:1101 減去 1011 的過程如下:

二進制的乘法

二進制數乘法過程可仿照十進制數乘法進行。但由於二進制數只有 01 兩種可能的乘數位,導致二進制乘法更為簡單。二進制數乘法的法則為:

0×0=0
0×1=1×0=0
1×1=1

例如:10011010 相乘的過程如下:

二進制的除法

二進制數除法與十進制數除法很類似。

例如:100110 ÷ 110 的過程如下:

二進製為什麼能表示所有的數據

因為編碼規定。

之前我們有說到,所有保存的程序和數據在計算機中都被描述為 文件,也就是說我們能夠知道當前的數據集合被期望的用途是什麼,也就能夠找到對應的 處理器 來正確處理當前的數據。

例如保存文字

拿文字舉例,為了讓一串 01 能夠代表特定的文字,人們規定使用一個字節中的七位來表達特定的文字, 這就是大名鼎鼎 ASCII (American Standard Code for Information Interchange) 碼,ASCll 碼能夠表達 27=128 種字符(編碼從 0~127),對於 26 個英文字母和一些常用的可打印字符,這完全足夠了:

可是,世界文化是多元的,面對類似漢字這樣的象形文字,ASCll碼錶用起來自然是捉襟見肘。

窮則思變,一個字節不行,那就兩個字節,這就是大名鼎鼎的 Unicode 碼,不難看出,Unicode 碼有 216=65536 種表示方式,這樣就足以表達一些常用的字符了,值得一提的是,Unicode 碼算是在 ASCll 碼上的一種擴充,其第 0~127 個編碼字符與 ASCll 碼錶一模一樣。

再比如圖片

這裏涉及一點點物理知識,話說很久以前,牛頓通過三稜鏡把白色的光分解成七種不同顏色的光,後來又通過各種實驗發現紅、綠、藍三種顏色的光是無法被分解的,因此我們就稱為紅藍綠為光的三原色。

至此人類已經知道了:可以通過組合不同比例的紅、綠、藍三種顏色來獲得各種各樣的顏色,那麼我們就可以在計算機上模擬了。現在的計算機,一般使用 32 位來表示顏色,32 位平分給四個分量,也就是每個分量 8 位。

為啥是四個顏色分量?

因為顏色模型中有一個 alpha 值,用來表示透明度,這一點我們先不考慮。總之三種顏色,每個使用 8 位來表示的話,我們就能夠表示 256 * 256 * 256 = 16777216 種顏色了,已經足夠基礎的使用了。

先來看一張圖片:

這張圖像的尺寸是 600px * 664px(px 是一種圖片單位,中文名稱為像素,你可以暫時理解為一個點)。我們把它放大一下,如下圖所示:

※教你寫出一流的銷售文案?

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

看見了嗎?實際上,大部分圖像(你拍攝的照片、你掃描的圖片、你使用 iPad 畫的圖片等等…)都是位圖文件,位圖就是由像素點構成的,它就像是一個網格一樣,每個格子裏面填一個顏色。(除了位圖外,還有一種圖是矢量圖,它描述的是形狀而非網格)

OK,我想你已經能理解圖像是由像素點組成的了(事實上我們的显示器也是),我們只需要在編碼中附帶上一些額外的信息,例如圖像有多大的尺寸、時間、作者、顏色深度、是否支持透明度之類的就能夠對圖像進行正確表示了。(視頻可以簡單理解成一張張連續不斷的圖片)

要讓显示器正確显示圖片或者視頻,只需要讓显示器上每個像素显示特定的顏色就好了。

  • 圖片來源:https://www.bbc.co.uk/bitesize/topics/zf2f9j6/articles/z2tgr82

Part 3. 為什麼是二進制?

  • 圖片來源:https://zhuanlan.zhihu.com/p/33439000

可為什麼一定是二進制呢?使用人類習慣的十進制不好嗎?

理由一:物理上易於實現

計算機依靠電力工作,這也就意味着需要將数字信號映射到電信號,實現這種映射最簡單的方法是:

  • 0 – 沒有電(0 V)
  • 1 – 有點(5 V)

二進制在技術上最容易實現。這是因為具有兩種 穩定狀態 的物理器件很多,如門電路的導通與截止、電壓的高與低等,而它們恰好可以對應表示 “1” 和 “0” 這兩個數碼。假如採用十進制,那麼就要製造具有 10穩定狀態 的物理電路,而這是非常困難的。

理由二:機器可靠性高

為什麼使用更複雜的数字系統是一個問題?

假設我們使用三元(3 位数字)数字系統涉及計算機,如果我們具有從 0 V5 V 的電壓,那麼我們可以進行以下的映射:

  • 0 – 0 V;
  • 1 – 2.5 V;
  • 2 – 5 V;
  • 圖片來源:https://pmihaylov.com/intro-binary-numbers/

看起來合理吧?但是,想象一下,我以 2.5 V 的電壓發送了一個数字。但是由於電路中的一些噪聲,我在輸出端得到 2.3 V 的電壓,因此將其視為 0。結果是?

有人給我發送了 1,但我將其視為 0。數據丟失可是一個非常嚴重的問題。

使用二進制則可靠得多,由於電壓的高和低、電流的有和無等都是一種 質的變化,兩種物理狀態穩定、分明,因此,二進制碼傳輸的抗干擾能力強,鑒別信息的可靠性高。

為什麼計算機系統必須有時鐘

  • 圖片來源:http://programmedlessons.org/Java9/chap02/ch02_11.html

建立数字系統的目的是 僅在某些時間點測試開/關(二進制)值,這使電線(或其他設備)有時間更換。這就是計算機系統有時鐘的原因。

時鐘會周期性地進行信號的測量,圖中所示的 T1 和 T2 就是可以測量信號的時間點。

時鐘利用所有這些時間點來保持同步。更快的時鐘意味着每秒可以對電線進行更多次測試,並且整個系統運行得更快。2 GHz 處理器每秒檢查二進制值 20 億次。在這些時間之間,允許值改變並穩定下來。處理器芯片速度越快,每秒可以測試的次數就越多,每秒可以做出的決策就越多。

理由三:運算規則簡單

數學推導已經證明,對 N 進制數進行算術求和或求積運算,其運算規則各有 N(N+1)/2 種。如採用十進制,則 N=10,就有 55 種求和或求積的運算規則;而採用二進制,則 N=2,僅有 3 種求和或求積的運算規則,以上面提到的加法為例:

0+0=0,0+1=1 (1+0=1),1+1=10

因而可以大大簡化運算器等物理器件的設計。

理由四:邏輯判斷方便

採用二進制后,僅有的兩個符號 “1” 和 “0” 正好可以與邏輯命題的兩個值 “真” 和 “假” 相對應,能夠方便地使用邏輯代數這一有力工具來分析和設計計算機的邏輯電路。

雖然在 1950 年代就造出了更加高效的三元計算機,但在效率和複雜度的取捨上,始終抵不過二進制。二進制仍然在當今世界中長期存在。

Part 4. CPU 的實際工作方式

上面我們了解到計算機以二進制的形式運行,它們只有兩種狀態:開(1)和關(0),為了執行二進制計算,我們需要採用一種特殊的电子元器件,稱為 「晶體管」。暫時我們把它理解為一種開關吧,通電就打開,沒電流通過就關閉。

利用”開關”搭建邏輯電路

我們知道,給電燈通上電,它就會亮:

於是,結合上開關,我們就能搭建出最基礎的 與門或門

與門

該電路的邏輯是:只有當 A 和 B 同時開啟時,LED 燈才會亮,也就是認為輸出 1,我們可以利用電信號來簡單模擬一下:

A B Y
0 0 0
1 0 0
0 1 0
1 1 1

或門

該電路的邏輯是:當 A 或者 B 開啟時,LED 燈就會亮,也就是認為輸出 1,我們可以利用電信號來簡單模擬一下:

A B Y
0 0 0
1 0 1
0 1 1
1 1 1

其他門

類似地,我們可以藉助更多的电子元器件來創造出基礎的 7 種邏輯門電路:

  • 圖片來源:https://www.zhihu.com/question/348237008/answer/843382847 | @Zign

這裏需要特別提一下 異或門,我們需要先知道有一種电子元器件可以利用電氣特性對 輸入取反,也就是說輸入 1 則輸出 0,輸入 0 則輸出 1,那麼我們就可以 簡單模擬 出異或門邏輯電路(實際會更複雜些,這裏僅展示出異或的意思):

A'B' 分別表示 AB 開關的反值,從圖中我們很容易知道只有當 AB 只存在一個輸入 1 時,整個電路才會輸出 1

利用邏輯門簡單計算加法

OK,上面我們了解到我們能夠利用 “開關” 來模擬邏輯的運算,我們接下來試着還原一個簡單的加法運算器是如何實現的:

僅需兩個門,就可以完成基本的二進制加法運算。上圖是利用 logic.ly 創建的半加法器,AB 相當於使我們計算的兩個數,最後一塊相當於是我們的數顯芯片,它的功能是根據輸入显示数字,從上到下的引腳(也就是圖中輸入的地方,通常我們這樣稱呼)分別對應了 20=121=222=423=8 的輸入,沒有任何輸入時显示為 0,如果 引腳 1(對應 20=1)像上圖一樣有輸入,則显示 0 + 1 = 1

我們來理解一下上方的電路:

  • 如果僅打開一個輸入,但不同時打開兩個輸入,則此處的 XOR 門(異或門)將打開,此時對應輸入 引腳 1,显示 数字 1(類似於 1 + 0 和 0 + 1);
  • 如果兩個輸入均打開,則 AND 門(與門)將打開,此時對應輸入 引腳 2,显示 数字 2(類似於 1 + 1);
  • 如果沒有輸入,則 AND 門和 XOR 門都保持關閉,此時显示 数字 0(類似於 0 + 0);

因此,如果兩個都打開,則 XOR 保持關閉,並且 AND 門打開,得出正確的答案為 2

但這隻是最基礎的半加法運算器,不是太有用,因為它只能解決最簡單的數學問題之一。但如果我們把它們兩個與另一個輸入連接,就會得到一個完整的加法器:

仔細思考幾遍,你就會得知這個三個輸入的加法器已經可以計算 3 個二進制数字的加法運算了,我們如法炮製,可以通過連接更多的”進位”來使這個加法器能夠運算更多的數,這當然也意味着這個計算鏈條更長。

大多數其他數學運算都可以加法完成。乘法只是重複加法,減法可以通過一些奇特的位反轉來完成,而除法只是重複減法。並且,儘管所有現代計算機都具有基於硬件的解決方案以加快更複雜的操作,但從技術上講,您可以使用完整的加法器來完成全部操作。

總線和內存

現在,我們的計算機只不過是一個計算器,它記不住任何內容也對輸出沒有任何操作,上述電路只是接了一個显示單元而已。

上面展示的是一個存儲單元。它使用了大量的 NAND 門,並且在實際生產中,根據存儲技術的不同,它們可能會大不相同,但其功能是相同的。

您給它一些輸入,並打開“寫”位(Write 輸入 1),它將把輸入存儲在單元內。這不僅是一個存儲單元,因為我們還需要一種從中讀取信息的方法。這是通過一個使能器完成的,該使能器是「存儲器」中每個位的“與”門的集合,所有位都與另一個輸入(即“讀取”位)綁定在一起。寫入和讀取位通常也稱為“設置”(set)和“啟用”(enable)。

上面整個存儲單元都包裹在所謂的寄存器中。這些寄存器連接到 總線,總線是圍繞整個系統運行的一束電線,並連接到每個組件。即使現代計算機也具有總線,儘管它們可能具有多個總線以提高多任務處理性能。

每個寄存器仍有一個讀寫位,但是在這種設置下,輸入和輸出是一樣的。這實際上很好。例如:如果要將 R1 的內容複製到 R2,則應打開 R1 的讀取位,這會將 R1 的內容壓入總線。當讀取位打開時,您將打開 R2 的寫入位,這會將總線內容複製到 R2 中。

寄存器也用於製作 RAM。RAM 通常布置在網格中,並且導線有兩個方向:

解碼器採用二進制輸入並打開相應的編號線。例如,11 在二進制數中是 3,即最高的 2 位数字,因此解碼器將打開最高的線路。每個路口都有一個寄存器。所有這些都連接到中央總線以及中央寫入和讀取輸入。只有跨寄存器的兩條導線也都打開時,讀和寫輸入才會打開,從而有效地允許您選擇要從中進行讀寫的寄存器。同樣,現代 RAM 要複雜得多,但是此設置仍然有效。

時鐘,步進器和解碼器

寄存器無處不在,是在 CPU 中移動數據並將信息存儲在 CPU 中的基本工具。那麼,是什麼告訴他們移動數據的呢?

時鐘是 CPU 核心中的第一個組件,它將按設置的時間間隔(以赫茲或每秒周期為單位)關閉和打開。這就是您看到的最直觀的 CPU 速度指標。

時鐘具有三種不同的狀態:基本時鐘,使能時鐘和設置時鐘。基本時鐘將打開半個周期,另一半關閉。使能時鐘用於打開寄存器,並且需要更長的時間才能確保數據被使能。設置時鐘必須始終與使能時鐘同時打開,否則可能會寫入錯誤的數據。

時鐘連接到步進器,步進器將從 1 到最大步數進行計數,並在完成后將自身重置為 1。時鐘還連接到 CPU 可以寫入的每個寄存器的 AND 門:

這些 “與” 門還連接到另一個組件的輸出,即指令解碼器。指令解碼器接受 SET R2 TO R1 之類的指令,並將其解碼為 CPU 可以理解的內容。它有自己的內部寄存器,稱為“指令寄存器”,該寄存器存儲了當前操作。它的精確程度取決於您正在運行的系統,但是一旦解碼,它將打開正確的設置並啟用正確寄存器的位,這些寄存器將根據時鐘觸發。

程序指令存儲在 RAM(或現代系統中的 L1 高速緩存,更靠近 CPU)中。由於程序數據與其他所有變量一樣都存儲在寄存器中,因此可以隨時對其進行操作以在程序中跳轉。這就是程序通過循環和 if 語句獲取結構的方式。跳轉指令將指令解碼器正在讀取的存儲器中的當前位置設置到其他位置。

一切如何配合

現在,我們對 CPU 工作原理的有了一些基本的了解。主總線跨越整個系統,並連接到所有寄存器。完整的加法器以及其他一系列運算都打包在算術邏輯單元或 ALU 中。該 ALU 將與總線建立連接,並且還將具有自己的寄存器來存儲正在操作的第二個数字。

為了執行計算,將程序數據從系統 RAM 加載到控制部分。控制部分從 RAM 中讀取兩個数字,將第一個数字加載到 ALU 的指令寄存器中,然後將第二個数字加載到總線上。同時,它向 ALU 發送指令代碼,告知其操作方法。然後,ALU 執行所有計算,並將結果存儲在另一個寄存器中,CPU 可以從該寄存器中讀取該值,然後繼續該過程。

參考資料

  1. 原來,我們是這樣記數的 – https://www.jianshu.com/p/58844323e4fb
  2. 二進制數的運算方法 – https://www.jianshu.com/p/560aba49c9a4
  3. 文字,圖片,視頻,音頻的二進製表示 – https://blog.csdn.net/c46550/article/details/91040925
  4. 知乎 – 計算機只認識0和1但是怎麼表示圖像和影視等等眾多應用的? | @kross – https://www.zhihu.com/question/36269548
  5. Introduction to binary numbers – https://pmihaylov.com/intro-binary-numbers/
  6. What is Binary, and Why Do Computers Use It? – https://www.howtogeek.com/367621/what-is-binary-and-why-do-computers-use-it/
  7. CPU 是怎樣認識代碼的? | 知乎 – https://www.zhihu.com/question/348237008/answer/843382847 | @Zign
  8. HTG Explains: How Does a CPU Actually Work? – https://www.howtogeek.com/367931/htg-explains-how-does-a-cpu-actually-work/

往期推薦

  1. 「MoreThanJava」當大學選擇了計算機之後應該知道的
  2. 「MoreThanJava」計算機發展史—從織布機到IBM
  3. 「MoreThanJava」計算機系統概述
  4. 媽媽再也不擔心我面試被Redis問得臉都綠了
  • 本文已收錄至我的 Github 程序員成長系列 【More Than Java】,學習,不止 Code,歡迎 star:https://github.com/wmyskxz/MoreThanJava
  • 個人公眾號 :wmyskxz,個人獨立域名博客:wmyskxz.com,堅持原創輸出,下方掃碼關注,2020,與您共同成長!

非常感謝各位人才能 看到這裏,如果覺得本篇文章寫得不錯,覺得 「我沒有三顆心臟」有點東西 的話,求點贊,求關注,求分享,求留言!

創作不易,各位的支持和認可,就是我創作的最大動力,我們下篇文章見!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※別再煩惱如何寫文案,掌握八大原則!

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)_網頁設計公司

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

透過資料庫的網站架設建置,建立公司的形象或購物系統,並提供最人性化的使用介面,讓使用者能即時接收到相關的資訊

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

從本篇就開始博客頁面的接口開發了,其實這些接口我是不想用文字來描述的,太枯燥太無趣了。全是CRUD,誰還不會啊,用得着我來講嗎?想想為了不半途而廢,為了之前立的Flag,還是咬牙堅持吧。

準備工作

現在博客數據庫中的數據是比較混亂的,為了看起來像那麼回事,顯得正式一點,我先手動搞點數據進去。

搞定了種子數據,就可以去愉快的寫接口了,我這裏將根據我現在的博客頁面去分析所需要接口,感興趣的去點點。

為了讓接口看起來清晰,一目瞭然,刪掉之前在IBlogService中添加的所有接口,將5個自定義倉儲全部添加至BlogService中,然後用partial修飾。

//IBlogService.cs
public partial interface IBlogService
{
}

//BlogService.cs
using Meowv.Blog.Application.Caching.Blog;
using Meowv.Blog.Domain.Blog.Repositories;

namespace Meowv.Blog.Application.Blog.Impl
{
    public partial class BlogService : ServiceBase, IBlogService
    {
        private readonly IBlogCacheService _blogCacheService;
        private readonly IPostRepository _postRepository;
        private readonly ICategoryRepository _categoryRepository;
        private readonly ITagRepository _tagRepository;
        private readonly IPostTagRepository _postTagRepository;
        private readonly IFriendLinkRepository _friendLinksRepository;

        public BlogService(IBlogCacheService blogCacheService,
                           IPostRepository postRepository,
                           ICategoryRepository categoryRepository,
                           ITagRepository tagRepository,
                           IPostTagRepository postTagRepository,
                           IFriendLinkRepository friendLinksRepository)
        {
            _blogCacheService = blogCacheService;
            _postRepository = postRepository;
            _categoryRepository = categoryRepository;
            _tagRepository = tagRepository;
            _postTagRepository = postTagRepository;
            _friendLinksRepository = friendLinksRepository;
        }
    }
}

在Blog文件夾下依次添加接口:IBlogService.Post.csIBlogService.Category.csIBlogService.Tag.csIBlogService.FriendLink.csIBlogService.Admin.cs

在Blog/Impl文件夾下添加實現類:IBlogService.Post.csBlogService.Category.csBlogService.Tag.csBlogService.FriendLink.csBlogService.Admin.cs

同上,.Application.Caching層也按照這個樣子添加。

注意都需要添加partial修飾為局部的接口和實現類,所有文章相關的接口放在IBlogService.Post.cs中,分類放在IBlogService.Category.cs,標籤放在IBlogService.Tag.cs,友鏈放在IBlogService.FriendLink.cs,後台增刪改所有接口放在IBlogService.Admin.cs,最終效果圖如下:

文章列表頁

分析:列錶帶分頁,以文章發表的年份分組,所需字段:標題、鏈接、時間、年份。

.Application.Contracts層Blog文件夾下添加返回的模型:QueryPostDto.cs

//QueryPostDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog
{
    public class QueryPostDto
    {
        /// <summary>
        /// 年份
        /// </summary>
        public int Year { get; set; }

        /// <summary>
        /// Posts
        /// </summary>
        public IEnumerable<PostBriefDto> Posts { get; set; }
    }
}

模型為一個年份和一個文章列表,文章列表模型:PostBriefDto.cs

//PostBriefDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class PostBriefDto
    {
        /// <summary>
        /// 標題
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// 鏈接
        /// </summary>
        public string Url { get; set; }

        /// <summary>
        /// 年份
        /// </summary>
        public int Year { get; set; }

        /// <summary>
        /// 創建時間
        /// </summary>
        public string CreationTime { get; set; }
    }
}

搞定,因為返回時間為英文格式,所以CreationTime 給了字符串類型。

IBlogService.Post.cs中添加接口分頁查詢文章列表QueryPostsAsync,肯定需要接受倆參數分頁頁碼和分頁數量。還是去添加一個公共模型PagingInput吧,在.Application.Contracts下面。

//PagingInput.cs
using System.ComponentModel.DataAnnotations;

namespace Meowv.Blog.Application.Contracts
{
    /// <summary>
    /// 分頁輸入參數
    /// </summary>
    public class PagingInput
    {
        /// <summary>
        /// 頁碼
        /// </summary>
        [Range(1, int.MaxValue)]
        public int Page { get; set; } = 1;

        /// <summary>
        /// 限制條數
        /// </summary>
        [Range(10, 30)]
        public int Limit { get; set; } = 10;
    }
}

Page設置默認值為1,Limit設置默認值為10,Range Attribute設置參數可輸入大小限制,於是這個分頁查詢文章列表的接口就是這個樣子的。

//IBlogService.Post.cs
public partial interface IBlogService
{
    /// <summary>
    /// 分頁查詢文章列表
    /// </summary>
    /// <param name="input"></param>
    /// <returns></returns>
    Task<ServiceResult<PagedList<QueryPostDto>>> QueryPostsAsync(PagingInput input);
}

ServiceResultPagedList是之前添加的統一返回模型,緊接着就去添加一個分頁查詢文章列表緩存接口,和上面是對應的。

//IBlogCacheService.Post.cs
using Meowv.Blog.Application.Contracts;
using Meowv.Blog.Application.Contracts.Blog;
using Meowv.Blog.ToolKits.Base;
using System;
using System.Threading.Tasks;

namespace Meowv.Blog.Application.Caching.Blog
{
    public partial interface IBlogCacheService
    {
        /// <summary>
        /// 分頁查詢文章列表
        /// </summary>
        /// <param name="input"></param>
        /// <param name="factory"></param>
        /// <returns></returns>
        Task<ServiceResult<PagedList<QueryPostDto>>> QueryPostsAsync(PagingInput input, Func<Task<ServiceResult<PagedList<QueryPostDto>>>> factory);
    }
}

分別實現這兩個接口。

//BlogCacheService.Post.cs
public partial class BlogCacheService
{
    private const string KEY_QueryPosts = "Blog:Post:QueryPosts-{0}-{1}";

    /// <summary>
    /// 分頁查詢文章列表
    /// </summary>
    /// <param name="input"></param>
    /// <param name="factory"></param>
    /// <returns></returns>
    public async Task<ServiceResult<PagedList<QueryPostDto>>> QueryPostsAsync(PagingInput input, Func<Task<ServiceResult<PagedList<QueryPostDto>>>> factory)
    {
        return await Cache.GetOrAddAsync(KEY_QueryPosts.FormatWith(input.Page, input.Limit), factory, CacheStrategy.ONE_DAY);
    }
}
//BlogService.Post.cs
/// <summary>
/// 分頁查詢文章列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<ServiceResult<PagedList<QueryPostDto>>> QueryPostsAsync(PagingInput input)
{
    return await _blogCacheService.QueryPostsAsync(input, async () =>
    {
        var result = new ServiceResult<PagedList<QueryPostDto>>();

        var count = await _postRepository.GetCountAsync();

        var list = _postRepository.OrderByDescending(x => x.CreationTime)
                                  .PageByIndex(input.Page, input.Limit)
                                  .Select(x => new PostBriefDto
                                  {
                                      Title = x.Title,
                                      Url = x.Url,
                                      Year = x.CreationTime.Year,
                                      CreationTime = x.CreationTime.TryToDateTime()
                                  }).GroupBy(x => x.Year)
                                  .Select(x => new QueryPostDto
                                  {
                                      Year = x.Key,
                                      Posts = x.ToList()
                                  }).ToList();

        result.IsSuccess(new PagedList<QueryPostDto>(count.TryToInt(), list));
        return result;
    });
}

PageByIndex(...)TryToDateTime().ToolKits層添加的擴展方法,先查詢總數,然後根據時間倒序,分頁,篩選出所需字段,根據年份分組,輸出,結束。

BlogController中添加API。

/// <summary>
/// 分頁查詢文章列表
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpGet]
[Route("posts")]
public async Task<ServiceResult<PagedList<QueryPostDto>>> QueryPostsAsync([FromQuery] PagingInput input)
{
    return await _blogService.QueryPostsAsync(input);
}

[FromQuery]設置input為從URL進行查詢參數,編譯運行看效果。

※想知道最厲害的網頁設計公司嚨底家"!

RWD(響應式網頁設計)是透過瀏覽器的解析度來判斷要給使用者看到的樣貌

已經可以查詢出數據,並且緩存至Redis中。

獲取文章詳情

分析:文章詳情頁,文章的標題、作者、發布時間、所屬分類、標籤列表、文章內容(HTML和MarkDown)、鏈接、上下篇的標題和鏈接。

創建返回模型:PostDetailDto.cs

//PostDetailDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.Application.Contracts.Blog
{
    public class PostDetailDto
    {
        /// <summary>
        /// 標題
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// 作者
        /// </summary>
        public string Author { get; set; }

        /// <summary>
        /// 鏈接
        /// </summary>
        public string Url { get; set; }

        /// <summary>
        /// HTML
        /// </summary>
        public string Html { get; set; }

        /// <summary>
        /// Markdown
        /// </summary>
        public string Markdown { get; set; }

        /// <summary>
        /// 創建時間
        /// </summary>
        public string CreationTime { get; set; }

        /// <summary>
        /// 分類
        /// </summary>
        public CategoryDto Category { get; set; }

        /// <summary>
        /// 標籤列表
        /// </summary>
        public IEnumerable<TagDto> Tags { get; set; }

        /// <summary>
        /// 上一篇
        /// </summary>
        public PostForPagedDto Previous { get; set; }

        /// <summary>
        /// 下一篇
        /// </summary>
        public PostForPagedDto Next { get; set; }
    }
}

同時添加CategoryDtoTagDtoPostForPagedDto

//CategoryDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class CategoryDto
    {
        /// <summary>
        /// 分類名稱
        /// </summary>
        public string CategoryName { get; set; }

        /// <summary>
        /// 展示名稱
        /// </summary>
        public string DisplayName { get; set; }
    }
}

//TagDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class TagDto
    {
        /// <summary>
        /// 標籤名稱
        /// </summary>
        public string TagName { get; set; }

        /// <summary>
        /// 展示名稱
        /// </summary>
        public string DisplayName { get; set; }
    }
}

//PostForPagedDto.cs
namespace Meowv.Blog.Application.Contracts.Blog
{
    public class PostForPagedDto
    {
        /// <summary>
        /// 標題
        /// </summary>
        public string Title { get; set; }

        /// <summary>
        /// 鏈接
        /// </summary>
        public string Url { get; set; }
    }
}

添加獲取文章詳情接口和緩存的接口。

//IBlogService.Post.cs
public partial interface IBlogService
{
    /// <summary>
    /// 根據URL獲取文章詳情
    /// </summary>
    /// <param name="url"></param>
    /// <returns></returns>
    Task<ServiceResult<PostDetailDto>> GetPostDetailAsync(string url);
}
//IBlogCacheService.Post.cs
public partial interface IBlogCacheService
{
    /// <summary>
    /// 根據URL獲取文章詳情
    /// </summary>
    /// <param name="url"></param>
    /// <returns></returns>
    Task<ServiceResult<PostDetailDto>> GetPostDetailAsync(string url, Func<Task<ServiceResult<PostDetailDto>>> factory);
}

分別實現這兩個接口。

//BlogCacheService.Post.cs
public partial class BlogCacheService
{
    private const string KEY_GetPostDetail = "Blog:Post:GetPostDetail-{0}";

    /// <summary>
    /// 根據URL獲取文章詳情
    /// </summary>
    /// <param name="url"></param>
    /// <param name="factory"></param>
    /// <returns></returns>
    public async Task<ServiceResult<PostDetailDto>> GetPostDetailAsync(string url, Func<Task<ServiceResult<PostDetailDto>>> factory)
    {
        return await Cache.GetOrAddAsync(KEY_GetPostDetail.FormatWith(url), factory, CacheStrategy.ONE_DAY);
    }
}
//BlogService.Post.cs
/// <summary>
/// 根據URL獲取文章詳情
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public async Task<ServiceResult<PostDetailDto>> GetPostDetailAsync(string url)
{
    return await _blogCacheService.GetPostDetailAsync(url, async () =>
    {
        var result = new ServiceResult<PostDetailDto>();

        var post = await _postRepository.FindAsync(x => x.Url.Equals(url));

        if (null == post)
        {
            result.IsFailed(ResponseText.WHAT_NOT_EXIST.FormatWith("URL", url));
            return result;
        }

        var category = await _categoryRepository.GetAsync(post.CategoryId);

        var tags = from post_tags in await _postTagRepository.GetListAsync()
                   join tag in await _tagRepository.GetListAsync()
                   on post_tags.TagId equals tag.Id
                   where post_tags.PostId.Equals(post.Id)
                   select new TagDto
                   {
                       TagName = tag.TagName,
                       DisplayName = tag.DisplayName
                   };

        var previous = _postRepository.Where(x => x.CreationTime > post.CreationTime).Take(1).FirstOrDefault();
        var next = _postRepository.Where(x => x.CreationTime < post.CreationTime).OrderByDescending(x => x.CreationTime).Take(1).FirstOrDefault();

        var postDetail = new PostDetailDto
        {
            Title = post.Title,
            Author = post.Author,
            Url = post.Url,
            Html = post.Html,
            Markdown = post.Markdown,
            CreationTime = post.CreationTime.TryToDateTime(),
            Category = new CategoryDto
            {
                CategoryName = category.CategoryName,
                DisplayName = category.DisplayName
            },
            Tags = tags,
            Previous = previous == null ? null : new PostForPagedDto
            {
                Title = previous.Title,
                Url = previous.Url
            },
            Next = next == null ? null : new PostForPagedDto
            {
                Title = next.Title,
                Url = next.Url
            }
        };

        result.IsSuccess(postDetail);
        return result;
    });
}

ResponseText.WHAT_NOT_EXIST是定義在MeowvBlogConsts.cs的常量。

TryToDateTime()和列表查詢中的擴展方法一樣,轉換時間為想要的格式。

簡單說一下查詢邏輯,先根據參數url,查詢是否存在數據,如果文章不存在則返回錯誤消息。

然後根據 post.CategoryId 就可以查詢到當前文章的分類名稱。

聯合查詢post_tags和tag兩張表,指定查詢條件post.Id,查詢當前文章的所有標籤。

最後上下篇的邏輯也很簡單,上一篇取大於當前文章發布時間的第一篇,下一篇取時間倒序排序並且小於當前文章發布時間的第一篇文章。

最後將所有查詢到的數據賦值給輸出對象,返回,結束。

BlogController中添加API。

 /// <summary>
 /// 根據URL獲取文章詳情
 /// </summary>
 /// <param name="url"></param>
 /// <returns></returns>
 [HttpGet]
 [Route("post")]
 public async Task<ServiceResult<PostDetailDto>> GetPostDetailAsync(string url)
 {
     return await _blogService.GetPostDetailAsync(url);
 }

編譯運行,然後輸入URL查詢一條文章詳情數據。

成功輸出預期內容,緩存同時也是ok的。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

搭配下方課程學習更佳 ↓ ↓ ↓

http://gk.link/a/10iQ7

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

當全世界的人們隨著網路時代而改變向上時您還停留在『網站美醜不重要』的舊有思維嗎?機會是留給努力改變現況的人們,別再浪費一分一秒可以接觸商機的寶貴時間!

HTTP 冷知識 | HTTP 請求中,空格應該被編碼為 %20 還是 + ?_網頁設計

台北網頁設計公司這麼多該如何選擇?

網動是一群專業、熱情、向前行的工作團隊,我們擁有靈活的組織與溝通的能力,能傾聽客戶聲音,激發創意的火花,呈現完美的作品

HTTP 請求中,空格應該被編碼為什麼?今天我們走進 RFC 文檔W3C 文檔,了解一下這個「史詩級」大坑。

1.%20 還是 +

開始講解前先看個小測試,在瀏覽器里輸入 blank testblanktest 間有個空格),我們看看瀏覽器如何處理的:

從動圖可以看出瀏覽器把空格解析為一個加號「+」。

是不是感覺有些奇怪?我們再做個測試,用瀏覽器提供的幾個函數試一下:

encodeURIComponent("blank test"// "blank%20test"
encodeURI("q=blank test")        // "q=blank%20test"
new URLSearchParams("q=blank test").toString() // "q=blank+test"

image-20200524184735653

代碼是不會說謊的,其實上面的結果都是正確的,encode 結果不一樣,是因為 URI 規範和 W3C 規範衝突了,才會搞出這種讓人疑惑的烏龍事件。

2.衝突的協議

我們首先看看 URI 中的保留字,這些保留字不參与編碼。保留字符一共有兩大類:

  • gen-delims:
    :
    /
    ?
    #
    [
    ]
    @
  • sub-delims:
    !
    $
    &
    '
    (
    )
    *
    +
    ,
    ;
    =

URI 的編碼規則也很簡單,先把非限定範圍的字符轉為 16 進制,然後前面加百分號。

空格這種不安全字符轉為十六進制就是 0x20,前面再加上百分號 % 就是 %20

image-20200524184601512

所以這時候再看 encodeURIComponentencodeURI 的編碼結果,就是完全正確的。

既然空格轉為%20 是正確的,那轉為 + 是怎麼回事?這時候我們就要了解一下 HTML form 表單的歷史。

早期的網頁沒有 AJAX 的時候,提交數據都是通過 HTML 的 form 表單。form 表單的提交方法可以用 GET 也可以用 POST,大家可以在 MDN form 詞條上測試:

網頁設計最專業,超強功能平台可客製化

窩窩以「數位行銷」「品牌經營」「網站與應用程式」「印刷品設計」等四大主軸,為每一位客戶客製建立行銷脈絡及洞燭市場先機。

經過測試我們可以看出表單提交的內容中,空格都是轉為加號的,這種編碼類型就是 application/x-www-form-urlencoded,在 WHATWG 規範里是這樣定義的:

image-20200524185912590

到這裏基本上就破案了,URLSearchParams 做 encode 的時候,就按這個規範來的。我找到了 URLSearchParams 的 Polyfill 代碼,裏面就做了 %20+ 的映射:

replace = {
    '!''%21',
    "'"'%27',
    '(''%28',
    ')''%29',
    '~''%7E',
    '%20''+'// <= 就是這個
    '%00''\x00'
}

規範里對這個編碼類型還有解釋說明:

The application/x-www-form-urlencoded format is in many ways an aberrant monstrosity, the result of many years of implementation accidents and compromises leading to a set of requirements necessary for interoperability, but in no way representing good design practices. In particular, readers are cautioned to pay close attention to the twisted details involving repeated (and in some cases nested) conversions between character encodings and byte sequences. Unfortunately the format is in widespread use due to the prevalence of HTML forms.

這種編碼方式就不是個好的設計,不幸的是隨着 HTML form 表單的普及,這種格式已經推廣開了

其實上面一大段句話就是一個意思:這玩意兒設計的就是 ,但積重難返,大家還是忍一下吧

3.一句話總結

  • URI 規範里,空格 encode 為 %20application/x-www-form-urlencoded 格式里,空格 encode 為 +

  • 實際業務開發時,最好使用業內成熟的 HTTP 請求庫封裝請求,這些雜活兒累活兒框架都幹了;

  • 如果非要使用原生 AJAX 提交 application/x-www-form-urlencoded 格式的數據,不要手動拼接參數,要用 URLSearchParams 處理數據,這樣可以避免各種噁心的編碼衝突。

4.文章推薦

本文選自我的長文HTTP 規範中的那些暗坑,想要了解更多可點擊原文查看。

更多推薦:

  • webpack 中最易混淆的 5 個知識點,掘金快 800 贊了
  • webpack dll 是個什麼東西,看完就能
    擺脫繁瑣的 dll 配置
  • React Native 性能優化指南從渲染層的角度分析了 RN 性能優化的 6 個點,並以圖文形式講解了 FlatList 的實現原理
  • Web Scraper——輕量數據爬取利器 介紹了一個小巧的瀏覽器爬蟲插件,可以實現簡單的數據爬取功能

最後推薦一下我的個人公眾號:「滷蛋實驗室」,平時會分享一些前端技術和數據分析的內容,大家感興趣的話可以關注一波:


本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※推薦評價好的iphone維修中心

擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢

Redis詳解(十二)—— 緩存穿透、緩存擊穿、緩存雪崩_貨運

※評比南投搬家公司費用收費行情懶人包大公開

搬家價格與搬家費用透明合理,不亂收費。本公司提供下列三種搬家計費方案,由資深專業組長到府估價,替客戶量身規劃選擇最經濟節省的計費方式

  本篇博客我們來介紹Redis使用過程中需要注意的三種問題:緩存穿透、緩存擊穿、緩存雪崩。

1、緩存穿透

一、概念

  緩存穿透:緩存和數據庫中都沒有的數據,可用戶還是源源不斷的發起請求,導致每次請求都會到數據庫,從而壓垮數據庫。

  如下圖紅色的流程:

  

 

   比如客戶查詢一個根本不存在的東西,首先從Redis中查不到,然後會去數據庫中查詢,數據庫中也查詢不到,那麼就不會將數據放入到緩存中,後面如果還有類似源源不斷的請求,最後都會壓到數據庫來處理,從而給數據庫造成巨大的壓力。

二、解決辦法

  ①、業務層校驗

  用戶發過來的請求,根據請求參數進行校驗,對於明顯錯誤的參數,直接攔截返回。

  比如,請求參數為主鍵自增id,那麼對於請求小於0的id參數,明顯不符合,可以直接返回錯誤請求。

  ②、不存在數據設置短過期時間

  對於某個查詢為空的數據,可以將這個空結果進行Redis緩存,但是設置很短的過期時間,比如30s,可以根據實際業務設定。注意一定不要影響正常業務。

  ③、布隆過濾器

  關於布隆過濾器,後面會詳細介紹。布隆過濾器是一種數據結構,利用極小的內存,可以判斷大量的數據“一定不存在或者可能存在”。

  對於緩存擊穿,我們可以將查詢的數據條件都哈希到一個足夠大的布隆過濾器中,用戶發送的請求會先被布隆過濾器攔截,一定不存在的數據就直接攔截返回了,從而避免下一步對數據庫的壓力。

2、緩存擊穿

一、概念

  緩存擊穿:Redis中一個熱點key在失效的同時,大量的請求過來,從而會全部到達數據庫,壓垮數據庫。

  

   這裏要注意的是這是某一個熱點key過期失效,和後面介紹緩存雪崩是有區別的。比如淘寶雙十一,對於某個特價熱門的商品信息,緩存在Redis中,剛好0點,這個商品信息在Redis中過期查不到了,這時候大量的用戶又同時正好訪問這個商品,就會造成大量的請求同時到達數據庫。

二、解決辦法

  ①、設置熱點數據永不過期

※智慧手機時代的來臨,RWD網頁設計為架站首選

網動結合了許多網際網路業界的菁英共同研發簡單易操作的架站工具,及時性的更新,為客戶創造出更多的網路商機。

  對於某個需要頻繁獲取的信息,緩存在Redis中,並設置其永不過期。當然這種方式比較粗暴,對於某些業務場景是不適合的。

  ②、定時更新

  比如這個熱點數據的過期時間是1h,那麼每到59minutes時,通過定時任務去更新這個熱點key,並重新設置其過期時間。

  ③、互斥鎖

  這是解決緩存穿透比較常用的方法。

  互斥鎖簡單來說就是在Redis中根據key獲得的value值為空時,先鎖上,然後從數據庫加載,加載完畢,釋放鎖。若其他線程也在請求該key時,發現獲取鎖失敗,則睡眠一段時間(比如100ms)后重試。

3、緩存雪崩

一、概念

  緩存雪崩:Redis中緩存的數據大面積同時失效,或者Redis宕機,從而會導致大量請求直接到數據庫,壓垮數據庫。

  

   對於一個業務系統,如果Redis宕機或大面積的key同時過期,會導致大量請求同時打到數據庫,這是災難性的問題。

二、解決辦法

  ①、設置有效期均勻分佈

  避免緩存設置相近的有效期,我們可以在設置有效期時增加隨機值;

  或者統一規劃有效期,使得過期時間均勻分佈。

  ②、數據預熱

  對於即將來臨的大量請求,我們可以提前走一遍系統,將數據提前緩存在Redis中,並設置不同的過期時間。

  ③、保證Redis服務高可用

  前面我們介紹過Redis的哨兵模式和集群模式,為防止Redis集群單節點故障,可以通過這兩種模式實現高可用。

  

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※回頭車貨運收費標準

宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念

談反應式編程在服務端中的應用,數據庫操作優化,萬條記錄從20秒到0.5秒_網頁設計公司

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

節能減碳愛地球是景泰電動車的理念,是創立景泰電動車行的初衷,滿意態度更是服務客戶的最高品質,我們的成長來自於你的推薦。

反應式編程在客戶端編程當中的應用相當廣泛,而當前在服務端中的應用相對被提及較少。本篇將介紹如何在服務端編程中應用響應時編程來改進數據庫操作的性能。

開篇就是結論

利用 System.Reactive 配合 TaskCompelteSource ,可以將分散的單次數據庫插入請求合併會一個批量插入的請求。在確保正確性的前提下,實現數據庫插入性能的優化。

如果讀者已經了解了如何操作,那麼剩下的內容就不需要再看了。

預設條件

現在,我們假設存在這樣一個 Repository 接口來表示一次數據庫的插入操作。

  csharp

namespace Newbe.RxWorld.DatabaseRepository { public interface IDatabaseRepository { /// <summary> /// Insert one item and return total count of data in database /// </summary> /// <param name="item"></param> /// <returns></returns> Task<int> InsertData(int item); } }

接下來,我們在不改變該接口簽名的前提下,體驗一下不同的實現帶來的性能區別。

基礎版本

首先是基礎版本,採用的是最為常規的單次數據庫INSERT操作來完成數據的插入。本示例採用的是SQLite作為演示數據庫,方便讀者自行實驗。

  csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl { public class NormalDatabaseRepository : IDatabaseRepository { private readonly IDatabase _database; public NormalDatabaseRepository( IDatabase database) { _database = database; } public Task<int> InsertData(int item) { return _database.InsertOne(item); } } }

常規操作。其中_database.InsertOne(item)的具體實現就是調用了一次INSERT

基礎版本在同時插入小於20次時基本上可以較快的完成。但是如果數量級增加,例如需要同時插入一萬條數據庫,將會花費約20秒鐘,存在很大的優化空間。

TaskCompelteSource

TaskCompelteSource 是 TPL 庫中一個可以生成一個可操作 Task 的類型。對於 TaskCompelteSource 不太熟悉的讀者可以通過該實例代碼了解。

此處也簡單解釋一下該對象的作用,以便讀者可以繼續閱讀。

對於熟悉 javascript 的朋友,可以認為 TaskCompelteSource 相當於 Promise 對象。也可以相當於 jQuery 當中的 $.Deferred 。

如果都不了解的朋友,可以聽一下筆者吃麻辣燙時想到的生活化例子。

吃麻辣燙 技術解釋
吃麻辣燙之前,需要先用盤子夾菜。 構造參數
夾好菜之後,拿到結賬處去結賬 調用方法
收銀員結賬完畢之後,會得到一個叫餐牌,會響鈴的那種 得到一個 Task 返回值
拿着菜牌找了一個位子坐下,玩手機等餐 正在 await 這個 Task ,CPU轉而處理其他事情
餐牌響了,去取餐,吃起來 Task 完成,await 節數,繼續執行下一行代碼

那麼 TaskCompelteSource 在哪兒呢?

首先,根據上面的例子,在餐牌響的時候,我們才會去取餐。那麼餐牌什麼時候才會響呢?當然是服務員手動按了一個在櫃檯的手動開關才觸發了這個響鈴。

那麼,櫃檯的這個開關,可以被技術解釋為 TaskCompelteSource 。

餐台開關可以控制餐牌的響鈴。同樣, TaskCompelteSource 就是一種可以控制 Task 的狀態的對象。

解決思路

有了前面對 TaskCompelteSource 的了解,那麼接下來就可以解決文章開頭的問題了。思路如下:

當調用 InsertData 時,可以創建一個 TaskCompelteSource 以及 item 的元組。為了方便說明,我們將這個元組命名為BatchItem

將 BatchItem 的 TaskCompelteSource 對應的 Task 返回出去。

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

搬家費用:依消費者運送距離、搬運樓層、有無電梯、步行距離、特殊地形、超重物品等計價因素後,評估每車次單

調用 InsertData 的代碼會 await 返回的 Task,因此只要不操作 TaskCompelteSource ,調用者就一會一直等待。

然後,另外啟動一個線程,定時將 BatchItem 隊列消費掉。

這樣就完成了單次插入變為批量插入的操作。

筆者可能解釋的不太清楚,不過以下所有版本的代碼均基於以上思路。讀者可以結合文字和代碼進行理解。

ConcurrentQueue 版本

基於以上的思路,我們採用 ConcurrentQueue 作為 BatchItem 隊列進行實現,代碼如下(代碼很多,不必糾結,因為下面還有更簡單的):

  csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl { public class ConcurrentQueueDatabaseRepository : IDatabaseRepository { private readonly ITestOutputHelper _testOutputHelper; private readonly IDatabase _database; private readonly ConcurrentQueue<BatchItem> _queue; // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable private readonly Task _batchInsertDataTask; public ConcurrentQueueDatabaseRepository( ITestOutputHelper testOutputHelper, IDatabase database) { _testOutputHelper = testOutputHelper; _database = database; _queue = new ConcurrentQueue<BatchItem>(); // 啟動一個 Task 消費隊列中的 BatchItem _batchInsertDataTask = Task.Factory.StartNew(RunBatchInsert, TaskCreationOptions.LongRunning); _batchInsertDataTask.ConfigureAwait(false); } public Task<int> InsertData(int item) { // 生成 BatchItem ,將對象放入隊列。返回 Task 出去 var taskCompletionSource = new TaskCompletionSource<int>(); _queue.Enqueue(new BatchItem { Item = item, TaskCompletionSource = taskCompletionSource }); return taskCompletionSource.Task; } // 從隊列中不斷獲取 BatchItem ,並且一批一批插入數據庫,更新 TaskCompletionSource 的狀態 private void RunBatchInsert() { foreach (var batchItems in GetBatches()) { try { BatchInsertData(batchItems).Wait(); } catch (Exception e) { _testOutputHelper.WriteLine($"there is an error : {e}"); } } IEnumerable<IList<BatchItem>> GetBatches() { var sleepTime = TimeSpan.FromMilliseconds(50); while (true) { const int maxCount = 100; var oneBatchItems = GetWaitingItems() .Take(maxCount) .ToList(); if (oneBatchItems.Any()) { yield return oneBatchItems; } else { Thread.Sleep(sleepTime); } } IEnumerable<BatchItem> GetWaitingItems() { while (_queue.TryDequeue(out var item)) { yield return item; } } } } private async Task BatchInsertData(IEnumerable<BatchItem> items) { var batchItems = items as BatchItem[] ?? items.ToArray(); try { // 調用數據庫的批量插入操作 var totalCount = await _database.InsertMany(batchItems.Select(x => x.Item)); foreach (var batchItem in batchItems) { batchItem.TaskCompletionSource.SetResult(totalCount); } } catch (Exception e) { foreach (var batchItem in batchItems) { batchItem.TaskCompletionSource.SetException(e); } throw; } } private struct BatchItem { public TaskCompletionSource<int> TaskCompletionSource { get; set; } public int Item { get; set; } } } }

以上代碼中使用了較多的 Local Function 和 IEnumerable 的特性,不了解的讀者可以點擊此處進行了解。

正片開始!

接下來我們使用 System.Reactive 來改造上面較為複雜的 ConcurrentQueue 版本。如下:

  csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl { public class AutoBatchDatabaseRepository : IDatabaseRepository { private readonly ITestOutputHelper _testOutputHelper; private readonly IDatabase _database; private readonly Subject<BatchItem> _subject; public AutoBatchDatabaseRepository( ITestOutputHelper testOutputHelper, IDatabase database) { _testOutputHelper = testOutputHelper; _database = database; _subject = new Subject<BatchItem>(); // 將請求進行分組,每50毫秒一組或者每100個一組 _subject.Buffer(TimeSpan.FromMilliseconds(50), 100) .Where(x => x.Count > 0) // 將每組數據調用批量插入,寫入數據庫 .Select(list => Observable.FromAsync(() => BatchInsertData(list))) .Concat() .Subscribe(); } // 這裏和前面對比沒有變化 public Task<int> InsertData(int item) { var taskCompletionSource = new TaskCompletionSource<int>(); _subject.OnNext(new BatchItem { Item = item, TaskCompletionSource = taskCompletionSource }); return taskCompletionSource.Task; } // 這段和前面也完全一樣,沒有變化 private async Task BatchInsertData(IEnumerable<BatchItem> items) { var batchItems = items as BatchItem[] ?? items.ToArray(); try { var totalCount = await _database.InsertMany(batchItems.Select(x => x.Item)); foreach (var batchItem in batchItems) { batchItem.TaskCompletionSource.SetResult(totalCount); } } catch (Exception e) { foreach (var batchItem in batchItems) { batchItem.TaskCompletionSource.SetException(e); } throw; } } private struct BatchItem { public TaskCompletionSource<int> TaskCompletionSource { get; set; } public int Item { get; set; } } } }

代碼減少了 50 行,主要原因就是使用 System.Reactive 中提供的很強力的 Buffer 方法實現了 ConcurrentQueue 版本中的複雜的邏輯實現。

老師,可以更給力一點嗎?

我們,可以“稍微”優化一下代碼,將 Buffer 以及相關的邏輯獨立於“數據庫插入”這個業務邏輯。那麼我們就會得到一個更加簡單的版本:

  csharp

namespace Newbe.RxWorld.DatabaseRepository.Impl { public class FinalDatabaseRepository : IDatabaseRepository { private readonly IBatchOperator<int, int> _batchOperator; public FinalDatabaseRepository( IDatabase database) { var options = new BatchOperatorOptions<int, int> { BufferTime = TimeSpan.FromMilliseconds(50), BufferCount = 100, DoManyFunc = database.InsertMany, }; _batchOperator = new BatchOperator<int, int>(options); } public Task<int> InsertData(int item) { return _batchOperator.CreateTask(item); } } }

其中 IBatchOperator 等代碼,讀者可以到代碼庫中進行查看,此處就不在陳列了。

性能測試

基本可以測定如下:

在 10 條數據併發操作時,原始版本和批量版本沒有多大區別。甚至批量版本在數量少時會更慢,畢竟其中存在一個最大 50 毫秒的等待時間。

但是,如果需要批量操作併發操作一萬條數據,那麼原始版本可能需要消耗20秒,而批量版本僅僅只需要0.5秒。

所有的示例代碼均可以在代碼庫中找到。如果 Github Clone 存在困難,也可以點擊此處從 Gitee 進行 Clone

最後但是最重要!

最近作者正在構建以反應式Actor模式事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出“分佈式”、“可水平擴展”、“可測試性高”的應用系統——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及項目。您的支持是促進項目成功的關鍵。

當前項目已經快要發布 0.1 alpha 版本,歡迎參与討論。

GitHub 項目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 項目地址:https://gitee.com/yks/Newbe.Claptrap

文章作者: newbe36524
本文章著作權歸作者所有,任何形式的轉載都請註明出處。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

透過選單樣式的調整、圖片的縮放比例、文字的放大及段落的排版對應來給使用者最佳的瀏覽體驗,所以不用擔心有手機版網站兩個後台的問題,而視覺效果也是透過我們前端設計師優秀的空間比例設計,不會因為畫面變大變小而影響到整體視覺的美感。

大學四年自學走來,這些私藏的實用工具/學習網站我貢獻出來了_租車

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

有別於一般網頁架設公司,除了模組化的架站軟體,我們的營業主軸還包含:資料庫程式開發、網站建置、網頁設計、電子商務專案開發、系統整合、APP設計建置、專業網路行銷。

點贊再看,養成習慣,微信搜索【敖丙】關注這個互聯網苟且偷生的工具人。

本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

##前言

在大學的時候我們有大量的業餘時間,我們可以拿出一部分時間去自學,也可以自學你感興趣的非你本專業的內容,就比如舞蹈,畫畫,等等,我就是在B站舞蹈區經常看欣小萌,咬人喵學習舞蹈,咳咳。

當然如果你成績不理想或者想更上一層,也可以去利用業餘事假給自己的當前的專業知識補補課。

我大學本身的專業是电子信息工程比較偏硬件的,後面通過在圖書館一年的自學,拿了不少獎,也在大二就收穫了某大廠的offer,現在自己走軟件這條路也是通過自學走過來的,如果大家對我當時軟件的學習方法和學習路線感情去,我後面可以單獨出一期,本期主要是說一些我覺得不錯的一些學習工具和網站。

正所謂工欲善其事必先利其器嘛,好的工具往往可以讓你事半功倍,首先我介紹的了呢,就是B站。

B站

網址:www.bilibili.com

這真的是一個學習網站,上面提到的咬人喵,欣小萌等等真的好看,咳咳 扯回正題,B站上面有很多優秀的up主,大家在這裏可以學到各行各業豐富的專業知識,在下海前,呸呸呸入海前,在這裏武裝自己也是極好的。

何同學大家都知道吧,我很早就關注了他,我最早是關注他學剪輯的你敢信?總之很多不錯的up大家真的值得關注像他們學習。

中國大學MOOC

網址:www.icourse163.org

這是我在大學自學c語言和計算機基礎的地方,學校有相關課程,但是我容易走神,所以我一般會選擇在這裏補課,是一個非常良心的免費學習網站,裏面主要是大學相關課程的視頻,什麼類型的都有。

現在我都還記得浙江大學的翁愷老師,當時我就在學校感慨,這個男人怎麼這麼有魅力。

IMOOC

網址:www.imooc.com

慕課網主要就是互聯網IT一類的課程,大部分都是付費的,號稱程序員的夢工廠。

我之前在裏面看過Linux相關的課程,整體的課程質量還是不錯的,如果你是個還在迷茫的學生,又或者是在考慮是否轉行的工作人員,請你耐心看完,相信會對你有所幫助!

極客時間

網址:https://time.geekbang.org

所有內容都是收費的,但是課程質量一樣很高,特別是我看過的丁奇的MySQL47講,裏面大部分的老師都是相關領域的技術專家。

之前我們公司的技術總監趙成花名謙益,就有在上面寫運維相關的專欄,因為他也是我的朋友,所以我也知道他寫一篇專欄需要耗費的時間和精力。

所以也是很值得推薦的一個學習網站。

極客學院

網址:www.jikexueyuan.com

有免費,有收費的課程,成為會員也比較簡單,讓朋友幫忙點擊鏈接就好了,課程質量也是不錯的。

網易雲課堂

網址:https://study.163.com

內容不局限於程序員,ui產品都有,大家比較迷茫可以可以看看。

百度/谷歌

網址:www.baidu.com www.google.com

搜索引擎嘛,想廣告少點就谷歌吧。

知乎

網址:www.zhihu.com

知乎,大家的刻板印象可能就是裝逼的地方,說實話我也經常在上面裝逼,但是如果你想在上面學習知識,這完全也是個很好的學習網站,同樣有很多不錯的博主會在上面分享自己的經驗經歷和相關的專業知識,你就去看贊高的就好了,我覺得也是很值得大家使用的網站。

GitHub

網址:https://github.com

程序員男生居多所以用戶也是男生居多,用戶來自全球,號稱gayhub全球最大的同性交友網站啊,很多開源網站,你要的畢設或許就可以在上面找到,搞不好還帶論文。

所有的開源項目,都在這裏託管,想看源碼,或者各種知識點的總結,這裏都有,我訂閱了官網的熱門,發現比較優秀的開源項目,都會搞下來看看到底有沒有東西。

也有很多博主比如我看一下我的GitHub,就收集了我寫的文章,還有我整理的學習資料等等。

我要自學網

網址:www.51zw.net

我要自學網是由來自電腦培訓學校和職業高校的老師聯手創立的一個視頻教學網,網站里的視頻教程均由經驗豐富的在職老師原創錄製,同時提供各類貼心服務,讓大家享受一站式的學習體驗。

我當時學了一些ps的東西,就是在這上面看的。

w3school、菜鳥教程

網址:www.w3school.com.cn www.runoob.com

領先的Web技術教程,在w3school,你可以找到你所需要的所有的網站建設教程,我主要當工具查詢網站了。

豆瓣、微信讀書、噹噹

網址:www.douban.com https://weread.qq.com http://book.dangdang.com

萬般皆下品惟有讀書高、書中自有黃金屋、黑髮不知勤學早,白首方悔讀書遲。

我想關於描寫讀書的諺語都不用我在過多的贅述了,讀書我想也只有好處,沒有壞處的,還是那句話,作者都是把自己幾年甚至幾十年的總結,都在寫在了書里,就算有錯誤的點,大的方向大家還是能收穫東西的。

了解一個新的知識點,技術棧,看書是不二之選,我個人也比較推薦看書,看書的時候你的心會沒那麼浮躁,其實看一本書你規定每天看十幾頁,一本300頁的書,也一個月不到就完了,很是很快的,根本不浪費大家多少時間。

那大家不知道書的好壞,買之前可以去噹噹或者京東看看對應的評價,或者去豆瓣看書評,我個人是豆瓣看得多點。

CSDN

www.csdn.net

這個也是我姐姐在推薦我看技術博客的時候,一起給我推薦的,不過在姐姐推薦之前,我其實就已經在使用他了,他有資源共享下載的地方,雖然要花C幣,我還記得大學為了幾C幣去各種求網友的場景。

他是1999年成立的,算是國內最老,最成熟的技術博客網站了,內容無比豐富,底蘊很深厚,註冊會員是千萬級別的,他的SEO也做的很好,所以百度你查資料基本上就是他和博客園置頂了。

掘金

https://juejin.im

是一個幫助開發者成長的社區,是一個面向互聯網技術人的內容分享平台,是的社區這兩個字很醒目,這也是我個人認為掘金的最大特點,15成立的他很年輕,我是在大學的時候我姐姐推薦給我的。

當然你現在也知道了我也經常在掘金髮文,從一個知識攝入者,變成了一個知識的分享者,他社區的特點一覽無遺,是一個很包容的平台,裏面前端到後端我覺得界限沒別的平台那麼明顯,說他是社區是因為,他有分享生活,分享技術的種種文章,沸點甚至有種看朋友圈的特點。

博客園

網址:www.cnblogs.com

昂,上面剛提到的博客園,04年的他,比起CSDN年輕了不少,是一個以.net為主的博客網站,這麼多年風格變化不是很大,至少在大學接觸他到現在這麼久了,都沒怎麼變。

移動端這麼重要的今天,他的移動端,還是那麼下飯,我不知道怎麼想的哈哈,不過問題不大,因為他復古的界面,以及他極度DIY的風格定義,導致他有一批死忠粉和老用戶,也是很活躍的博客網站。

思否(segmentfault)

網址:https://segmentfault.com

說到回答,這個跟我一樣是杭漂的網站就厲害了,他也是博客和問答的形式,是中國版的stackoverflow,不過因為他的年輕,可能沉澱的文章還不夠,不過我想會越來越好的。

stackoverflow

網址:https://stackoverflow.com

上面既然提到了中國版的,我就說一下這個世界級別的老大哥,這是我這裏面使用頻率很高的網站了,遇到問題不會就Google,然後基本上都往這裏面跳,你就知道他的厲害了。

是的你遇到的問題,外國的友人肯定也有遇到的,問答的方式,還有很多熱血仔在下面跟答的形式,我比較喜歡,而且國外大牛多嘛。

開源中國

網址:www.oschina.net

跟碼雲是一家的,作為博客網站來說,年輕了點,但是不影響他有很多優質的博主不斷給他輸出內容,基本上的領域都覆蓋了,經常也會舉辦各種活動,我也看好他的未來。

V2ex

網址:www.v2ex.com

v2ex,是一個彙集各類奇妙好玩的話題和流行動向的網站。V2EX 提供了特別有用的小工具 [ ZEN ],V2EX 中充滿了各種各樣精心雕琢的細節。

infoQ

網址:www.infoq.cn

※超省錢租車方案

商務出差、學生出遊、旅遊渡假、臨時用車!GO 神州租賃有限公司!合法經營、合法連鎖、合法租賃小客車!

InfoQ(Information Queue)是一個在線新聞/社區網站,旨在通過促進軟件開發領域知識與創新的傳播,為軟件開發者提供幫助,文章質量特別高,但是需要一定的專業基礎知識讀起來才有意思。

有道詞典

網址:www.youdao.com

查單詞,我英語是真的菜,基本上都是一直開着的。

印象筆記

網址:www.yinxiang.com

個人用的印象筆記,從大學到現在用了很多年了,還是很不錯的,也承載了我的很多記憶,基本上有我學硬件,到學軟件這一路的筆記了,也有一些婆娑的日記,我是話癆來的嘛,這個習慣我覺得還是挺不錯的,至少寫筆記這個習慣讓我現在寫文章也算是比較從容。

他的插件也很好用,可以直接保存網頁信息到筆記,不用怕到時候找不到,或者作者刪除了。

如果大家不想安裝,想直接使用雲的,他也有,或者你可以嘗試以下兩個。

有道雲、石墨文檔

網址:https://note.youdao.com https://shimo.im

這兩個是我使用比較頻繁的雲筆記工具,主要是在線嘛,方便。

ProcessOn 、xmind

網址:www.processon.com www.xmind.cn

這個是我工作以來,一直都很依賴的工作和學習的方式,就是做腦圖,工作中大家也會發現身邊的仔,基本上也都會或多或少的做一些腦圖,去輔助自己設計系統,或者去了解學習一些知識點什麼的。

他可以做詳細設計,做概要設計,當然也可以做我上面提到的時間規劃,以及知識點清單啥的。

鳩摩搜索

網址:www.jiumodiary.com

這個網址可以用來搜索一些pdf的書,有了這個網址,就不用百度全網搜索pdf的書了。雖然可能沒有像百度全網搜索那麼全,但大部分也都有了。注意每種搜索結果的來源,有些來源是百度雲盤,有些是微盤等,可以根據需要獲取。

腳本之家

網址:www.jb51.net/books

腳本之家資源還是挺多的,电子書其中之一,不過,也是有挺多电子書的,並且提供多種下載方式,如果你鳩摩搜書搜索不到,或許可以考慮在腳本之家搜索,或者腳本之家搜索不到就到鳩摩搜書搜索。

牛客網 校招

網址:www.nowcoder.com

我沒經歷過校招,但是我確實在上面刷過計算機相關的題目,也有各大互聯網公司的面試真題,還有一些學長在上面發麵經,內推信息等等,大家都可以提前去看看,聯繫自己想去公司的學長,讓他幫你內推

LeetCode、lintcode

網址:https://leetcode-cn.com www.lintcode.com

算算法題目的,我算法很差,後悔沒好好刷了,大家想去字節這樣的公司是一定要刷的。

數據結構模擬

網址:https://www.cs.usfca.edu

大家如果對一些數據結構感覺很抽象,那這個網站可以幫助你,比如二叉樹紅黑樹,在這裏都能模擬數據的插入刪除。

BOSS、拉鈎

網址:www.zhipin.com www.lagou.com

什麼都學好了,找工作當然也得需要一個投遞簡歷的好網站,我這裏就推薦boss和拉鈎,這兩個也是我出去找工作會經常使用的兩個網站,boss效率會高很多。

總結

文中提到的所有網址:

B站 網址:www.bilibili.com

中國大學MOOC 網址:www.icourse163.org

IMOOC 網址:www.imooc.com

極客時間 網址:https://time.geekbang.org

極客學院 網址:www.jikexueyuan.com

網易雲課堂 網址:https://study.163.com

百度/谷歌 網址:www.baidu.com www.google.com

知乎 網址:www.zhihu.com

GitHub 網址:https://github.com

我要自學網 網址:www.51zw.net

w3school、菜鳥教程 網址:www.w3school.com.cn www.runoob.com

豆瓣、微信讀書、噹噹 網址:www.douban.com https://weread.qq.com http://book.dangdang.com

CSDN 網址www.csdn.net

掘金 網址 https://juejin.im

博客園 網址:www.cnblogs.com

思否(segmentfault) 網址:https://segmentfault.com

stackoverflow 網址:https://stackoverflow.com

開源中國 網址:www.oschina.net

V2ex 網址:www.v2ex.com

infoQ 網址:www.infoq.cn

有道詞典 網址:www.youdao.com

印象筆記 網址:www.yinxiang.com

有道雲、石墨文檔 網址:https://note.youdao.com https://shimo.im

ProcessOn 、xmind 網址:www.processon.com www.xmind.cn

鳩摩搜索 網址:www.jiumodiary.com

腳本之家 網址:www.jb51.net/books

牛客網 校招 網址:www.nowcoder.com

LeetCode、lintcode 網址:https://leetcode-cn.com www.lintcode.com

數據結構模擬 網址:www.cs.usfca.edu

BOSS、拉鈎 網址:www.zhipin.com www.lagou.com

我相信上面的一下網站,總有一兩個是能幫助到大家的,學習是個時而欣喜若狂,時而鬱郁寡歡的道路,希望你能堅持下去,時間會回報你的。

我是敖丙,一個在互聯網苟且偷生的程序員。

你知道的越多,你不知道的越多人才們的 【三連】 就是丙丙創作的最大動力,我們下期見!

注:如果本篇博客有任何錯誤和建議,歡迎人才們留言!

文章持續更新,可以微信搜索「 敖丙 」第一時間閱讀,回復【資料】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。

你知道的越多,你不知道的越多

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

日本、大陸,發現這些先進的國家已經早就讓電動車優先上路,而且先進國家空氣品質相當好,電動車節能減碳可以減少空污

Golang基礎教程——map使用篇_包裝設計

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網動廣告出品的網頁設計,採用精簡與質感的CSS語法,提升企業的專業形象與簡約舒適的瀏覽體驗,讓瀏覽者第一眼就愛上她。

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天是golang專題的第7篇文章,我們來聊聊golang當中map的用法。

map這個數據結構我們經常使用,存儲的是key-value的鍵值對。在C++/java當中叫做map,在Python中叫做dict。這些數據結構的名稱雖然不經相同,背後的技術支撐也不一定一樣,比如說C++的map是紅黑樹實現的,Java中的hashmap則是通過hash表。但是使用起來的方法都差不多,除了Java是通過get方法獲取鍵值之外,C++、Python和golang都是通過方括號獲取的。

聲明與初始化

golang中的map聲明非常簡單,我們用map關鍵字表示聲明一個map,然後在方括號內填上key的類型,方括號外填上value的類型。

var m map[string] int

這樣我們就聲明好了一個map。

但是要注意,這樣聲明得到的是一個空的map,map的零值是nil,可以理解成空指針。所以我們不能直接去操作這個m,否則會得到一個panic。

panic: assignment to entry in nil map

panic在golang當中表示非常嚴重不可恢復的錯誤,可以恢復的錯誤有些類似於Java或者是其他語言當中的異常,當異常出現的時候,我們可以選擇handle住它們,讓程序不崩潰繼續運行。而那些非常嚴重,無法handle的異常在golang當中稱為panic。

golang當中的異常處理機制和其他語言相差很大,整體的邏輯和內核都不太一樣。當然這個是一個比較大的話題,我們這裏可以簡單將它理解成error就行了。

回到map上來,我們聲明了一個map之後,想要使用它還需要對它進行初始化。使用它的方法也很簡單,就是使用make方法創建出一個實例來。它的用法和之前通過make創建元組非常類似:

m = make(map[string] int)

// 我們還可以指定創建出來的map的存儲能力的大小
m = make(map[string] int, 100)

我們也可以在聲明的時候把初始化也寫上:

var m = map[string] int {"abc": 3, "ccd": 4}

當然也可以通過賦值運算符,直接make出一個空的map來:

m := make(map[string] int)

增刪改查

map創建好了當然是要用的,整體使用起來和Python當中的dict比較像,比較簡單直觀,沒有太多彎彎繞的東西。我們一個一個來看,首先是map的添加元素。map的添加元素直接用方括號賦值即可:

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

上新台中搬家公司提供您一套專業有效率且人性化的辦公室搬遷、公司行號搬家及工廠遷廠的搬家服務

m["abc"] = 4

同樣,我們需要保證這裏的m經過初始化,否則也會包nil的panic。如果key值在map當中已經存在,那麼會自動替換掉原本的key。也就是說map的更新和添加元素都是一樣的,都是通過這種方式。如果不存在就是添加,否則則是更新。

刪除元素也很簡單,和Python當中類似,通過delete關鍵字刪除

delete(m, "abc")

當我們刪除key的時候,如果是其他的語言,我們需要判斷這個key值是否存在,否則的話不能刪除,或者是會引起異常。在golang當中並不會,對這點做了優化。如果要刪除的key值原本就不在map當中,那麼當我們調用了delete之後,什麼也不會發生。但是有一點,必須要保證傳入的map不為nil,否則也會引起panic。

最後,我們看下元素的查找。對於Java和Python來說我們都是通過一些判斷語句來進行判斷的,比如java的話是containsKey,Python的話用in操作符。在golang當中我們則是直接通過方括號進行查詢,那麼這就有了一個問題,如果key不在其中怎麼辦?

如果是其他語言,我們直接訪問一個不存在的key是會拋出異常的,但是在golang當中不會觸發panic,因為它會額外返回一個bool類型的元素表示元素是否查找到。所以我們可以同時用兩個變量去接收,如果第二個變量為True的話,就說明查找成功了。

進一步,我們還可以將這個邏輯和if的初始化操作合在一起:

if val, ok := m["1234"]; ok {
    fmt.Println(val)
}

這裏的ok就表示查找是否成功,這也是golang當中map查找的慣用寫法。

最後, 我們看一個實際運用map的例子,通過map來生成統計字符串當中單詞數量的wordCount:

package main

import (
 "golang.org/x/tour/wc"
 "strings"
)

func WordCount(s string) map[string]int {
 cnt := make(map[string]int)
    // 通過Split方法拆分字符串
 for _, str:= range strings.Split(s){
        // 直接++即可,golang會自動填充
  cnt[str]++
 }
 return cnt
}

func main() {
 wc.Test(WordCount)
}

總結

到這裏,關於golang當中map的使用就算是介紹完了。我們可以發現,map一如既往地體現了golang語法精簡的特點。比如通過返回error的操作省略了判斷元素是否存在map當中的操作,剛開始的時候會覺得有些不太適應,但是接觸多了之後,會發現這些都是有套路的。golang的套路就是精簡,能省就省,能簡單絕不複雜。

這一點不僅在map上體現,在其他特性上也是一樣。在後續的內容當中,我們還會繼續感知這一點。

如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

※產品缺大量曝光嗎?你需要的是一流包裝設計!

窩窩觸角包含自媒體、自有平台及其他國家營銷業務等,多角化經營並具有國際觀的永續理念。

用TensorFlow搭建一個萬能的神經網絡框架(持續更新),tensorflow常用函數_台中搬家

台中搬家公司費用怎麼算?

擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司

博客作者:凌逆戰

博客地址:https://www.cnblogs.com/LXP-Never/p/12774058.html

文章代碼:https://github.com/LXP-Never/blog_data/tree/master/tensorflow_model

  我一直覺得TensorFlow的深度神經網絡代碼非常困難且繁瑣,對TensorFlow搭建模型也十分困惑,所以我近期閱讀了大量的神經網絡代碼,終於找到了搭建神經網絡的規律,各位要是覺得我的文章對你有幫助不妨點個,點個關注吧。

  我個人把深度學習分為以下步驟:數據處理 –> 模型搭建 –> 構建損失 –> 模型訓練 –> 模型評估

我先把代碼放出來,然後一點一點來講

# Author:凌逆戰
# -*- encoding:utf-8 -*-
# 修改時間:2020年5月31日
import time
from tensorflow.examples.tutorials.mnist import input_data
from nets.my_alex import alexNet
from ops import *

tf.flags.DEFINE_integer('batch_size', 50, 'batch size, default: 1')
tf.flags.DEFINE_integer('class_num', 10, 'batch size, default: 1')
tf.flags.DEFINE_integer('epochs', 10, 'batch size, default: 1')
tf.flags.DEFINE_float('learning_rate', 1e-4, '初始學習率, 默認: 0.0002')
tf.flags.DEFINE_string('checkpoints_dir', "checkpoints", '保存檢查點的地址')
FLAGS = tf.flags.FLAGS

# 從MNIST_data/中讀取MNIST數據。當數據不存在時,會自動執行下載
mnist = input_data.read_data_sets('./data', one_hot=True, reshape=False)
# reshape=False  (None, 28,28,1)    # 用於第一層是卷積層
# reshape=False  (None, 784)        # 用於第一層是全連接層

# 我們看一下數據的shape
print(mnist.train.images.shape)  # 訓練數據圖片(55000, 28, 28, 1)
print(mnist.train.labels.shape)  # 訓練數據標籤(55000, 10)
print(mnist.test.images.shape)  # 測試數據圖片(10000, 28, 28, 1)
print(mnist.test.labels.shape)  # 測試數據圖片(10000, 10)
print(mnist.validation.images.shape)  # 驗證數據圖片(5000, 28, 28, 1)
print(mnist.validation.labels.shape)  # 驗證數據圖片(5000, 784)


def train():
    batch_size = FLAGS.batch_size  # 一個batch訓練多少個樣本
    batch_nums = mnist.train.images.shape[0] // batch_size  # 一個epoch中應該包含多少batch數據
    class_num = FLAGS.class_num  # 分類類別數
    epochs = FLAGS.epochs  # 訓練周期數
    learning_rate = FLAGS.learning_rate  # 初始學習率

    ############    保存檢查點的地址   ############
    checkpoints_dir = FLAGS.checkpoints_dir  # checkpoints
    # 如果檢查點不存在,則創建
    if not os.path.exists(checkpoints_dir):
        os.makedirs(FLAGS.checkpoints_dir)

    ######################################################
    #                    創建圖                          #
    ######################################################
    graph = tf.Graph()  # 自定義圖
    # 在自己的圖中定義數據和操作
    with graph.as_default():
        inputs = tf.placeholder(dtype="float", shape=[None, 28, 28, 1], name='inputs')
        labels = tf.placeholder(dtype="float", shape=[None, class_num], name='labels')
        # 看個人喜歡,有的人在初始化定義中就定義了learning_rate,有的人喜歡通過feed傳learning_rate
        learning_rate = tf.placeholder("float", None, name='learning_rate')
        # 如果網絡結構有dropout層,需要定義keep_probn,如果沒有則不需要
        # 訓練的時候需要,測試的時候需要設置成1
        keep_prob = tf.placeholder(dtype="float", name='keep_prob')
        ############    搭建模型   ############
        logits = alexNet(inputs, class_num, keep_prob=keep_prob)  # 使用placeholder搭建模型
        ############    損失函數   ############
        loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=logits))
        tf.add_to_collection('losses', loss)
        total_loss = tf.add_n(tf.get_collection("losses"))  # total_loss=模型損失+權重正則化損失
        ############    模型精度   ############
        predict = tf.argmax(logits, 1)  # 模型預測結果
        accuracy = tf.reduce_mean(tf.cast(tf.equal(predict, tf.argmax(labels, 1)), tf.float32))
        ############    優化器   ############
        variable_to_train = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES)  # 可訓練變量列表
        # 創建優化器,更新網絡參數,最小化loss,
        global_step = tf.Variable(0, trainable=False)
        learning_rate = tf.train.exponential_decay(learning_rate=learning_rate,  # 初始學習率
                                                   global_step=global_step,
                                                   decay_steps=batch_nums,  # 多少步衰減一次
                                                   decay_rate=0.1,  # 衰減率
                                                   staircase=True)  # 以階梯的形式衰減
        # 移動平均值更新參數
        # train_op = moving_average(loss, learning_rate, global_step)
        # adam優化器,adam算法好像會自動衰減學習率,
        train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss=total_loss,
                                                                  global_step=global_step,
                                                                  var_list=variable_to_train)
        ############    TensorBoard可視化 summary  ############
        summary_writer = tf.summary.FileWriter(logdir="./logs", graph=graph)  # 創建事件文件
        tf.summary.scalar(name="losses", tensor=total_loss)  # 收集損失值變量
        tf.summary.scalar(name="acc", tensor=accuracy)  # 收集精度值變量
        tf.summary.scalar(name='learning_rate', tensor=learning_rate)
        merged_summary_op = tf.summary.merge_all()  # 將所有的summary合併為一個op
        ############    模型保存和恢復 Saver   ############
        saver = tf.train.Saver(max_to_keep=5)

    ######################################################
    #                   創建會話                          #
    ######################################################
    max_acc = 0.
    config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)
    with tf.Session(config=config, graph=graph) as sess:
        # 加載模型,如果模型存在返回 是否加載成功和訓練步數
        could_load, checkpoint_step = load_model(sess, saver, FLAGS.checkpoints_dir)
        if could_load:
            print(" [*] 模型加載成功")
        else:
            print(" [!] 模型加載失敗")
            try:
                tf.global_variables_initializer().run()
            except:
                tf.initialize_all_variables().run()

        for epoch in range(epochs):
            for i in range(batch_nums):
                start_time = time.time()
                # batch_images = data_X[i * batch_size:(i + 1) * batch_size]
                # batch_labels = data_y[i * batch_size:(i + 1) * batch_size]
                train_batch_x, train_batch_y = mnist.train.next_batch(batch_size)

                # 使用真實數據填充placeholder,運行訓練模型和合併變量操作
                _, summary, loss, step = sess.run([train_op, merged_summary_op, total_loss, global_step],
                                                  feed_dict={inputs: train_batch_x,
                                                             labels: train_batch_y,
                                                             keep_prob: 0.5})
                if step % 100 == 0:
                    summary_writer.add_summary(summary, step)  # 將每次迭代后的變量寫入事件文件
                    summary_writer.flush()  # 強制summary_writer將緩存中的數據寫入到日誌文件中(可選)

                    ############    可視化打印   ############
                    print("Epoch:[%2d] [%4d/%4d] time:%4.4f,loss:%.8f" % (
                        epoch, i, batch_nums, time.time() - start_time, loss))

                # 打印一些可視化的數據,損失...
                if step % 100 == 0:
                    acc = sess.run(accuracy, feed_dict={inputs: mnist.validation.images,
                                                        labels: mnist.validation.labels,
                                                        keep_prob: 1.0})
                    print("Epoch:[%2d] [%4d/%4d] accuracy:%.8f" % (epoch, i, batch_nums, acc))
                    ############    保存模型   ############
                    if acc > max_acc:
                        max_acc = acc
                        save_path = saver.save(sess,
                                               save_path=os.path.join(checkpoints_dir, "model.ckpt"),
                                               global_step=step)
                        tf.logging.info("模型保存在: %s" % save_path)
        print("優化完成!")


def main(argv=None):
    train()


if __name__ == '__main__':
    # logging.basicConfig(level=logging.INFO)
    tf.logging.set_verbosity(tf.logging.INFO)
    tf.app.run()

main(global_step)

# Author:凌逆戰
# -*- encoding:utf-8 -*-
# 修改時間:2020年5月31日
import time
from tensorflow.examples.tutorials.mnist import input_data
from nets.my_vgg import VGG16Net
from ops import *

tf.flags.DEFINE_integer('batch_size', 100, 'batch size, default: 1')
tf.flags.DEFINE_integer('class_num', 10, 'batch size, default: 1')
tf.flags.DEFINE_integer('epochs', 10, 'batch size, default: 1')
tf.flags.DEFINE_float('learning_rate', 2e-4, '初始學習率, 默認: 0.0001')
tf.flags.DEFINE_string('checkpoints_dir', "checkpoint", '保存檢查點的地址')
FLAGS = tf.flags.FLAGS

# 從MNIST_data/中讀取MNIST數據。當數據不存在時,會自動執行下載
mnist = input_data.read_data_sets('./MNIST_data', one_hot=True, reshape=False)
# reshape=False  (None, 28,28,1)    # 用於第一層是卷積層
# reshape=False  (None, 784)        # 用於第一層是全連接層

# 我們看一下數據的shape
print(mnist.train.images.shape)  # 訓練數據圖片(55000, 28, 28, 1)
print(mnist.train.labels.shape)  # 訓練數據標籤(55000, 10)
print(mnist.test.images.shape)  # 測試數據圖片(10000, 28, 28, 1)
print(mnist.test.labels.shape)  # 測試數據圖片(10000, 10)
print(mnist.validation.images.shape)  # 驗證數據圖片(5000, 28, 28, 1)
print(mnist.validation.labels.shape)  # 驗證數據圖片(5000, 784)


def train():
    batch_size = FLAGS.batch_size
    batch_nums = mnist.train.images.shape[0] // batch_size  # 一個epoch中應該包含多少batch數據
    class_num = FLAGS.class_num
    epochs = FLAGS.epochs
    learning_rate = FLAGS.learning_rate

    ############    保存檢查點的地址   ############
    checkpoints_dir = FLAGS.checkpoints_dir  # checkpoints
    # 如果檢查點不存在,則創建
    if not os.path.exists(checkpoints_dir):
        os.makedirs(FLAGS.checkpoints_dir)

    ######################################################
    #                    創建圖                          #
    ######################################################
    graph = tf.Graph()  # 自定義圖
    # 在自己的圖中定義數據和操作
    with graph.as_default():
        inputs = tf.placeholder(dtype="float", shape=[None, 28, 28, 1], name='inputs')
        labels = tf.placeholder(dtype="float", shape=[None, class_num], name='labels')
        ############    搭建模型   ############
        logits = VGG16Net(inputs, class_num)  # 使用placeholder搭建模型
        ############    損失函數   ############
        # 計算預測值和真實值之間的誤差
        loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=logits))
        tf.add_to_collection('losses', loss)
        total_loss = tf.add_n(tf.get_collection("losses"))  # total_loss=模型損失+權重正則化損失
        ############    模型精度   ############
        predict = tf.argmax(logits, axis=1)
        accuracy = tf.reduce_mean(tf.cast(tf.equal(predict, tf.argmax(labels, axis=1)), tf.float32))
        ############    優化器   ############
        variable_to_train = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES)  # 可訓練變量列表
        # 創建優化器,更新網絡參數,最小化loss,
        train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss=total_loss,
                                                                  var_list=variable_to_train)
        ############    TensorBoard可視化 summary  ############
        summary_writer = tf.summary.FileWriter("./logs", graph=graph)  # 創建事件文件
        tf.summary.scalar(name="loss", tensor=total_loss)  # 收集損失值變量
        tf.summary.scalar(name='accuracy', tensor=accuracy)  # 收集精度值變量
        tf.summary.scalar(name='learning_rate', tensor=learning_rate)
        merged_summary_op = tf.summary.merge_all()  # 將所有的summary合併為一個op
        ############    模型保存和恢復 Saver   ############
        saver = tf.train.Saver(max_to_keep=5)

    ######################################################
    #                   創建會話                          #
    ######################################################
    max_acc = 0.
    config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)
    with tf.Session(config=config, graph=graph) as sess:
        # 加載模型,如果模型存在返回 是否加載成功和訓練步數
        could_load, checkpoint_step = load_model(sess, saver, FLAGS.checkpoints_dir)
        if could_load:
            step = checkpoint_step
            print(" [*] 模型加載成功")
        else:
            print(" [!] 模型加載失敗")
            try:
                tf.global_variables_initializer().run()
            except:
                tf.initialize_all_variables().run()
            step = 0

        for epoch in range(epochs):
            for i in range(batch_nums):
                start_time = time.time()    # 記錄一下開始訓練的時間
                # batch_images = data_X[i * batch_size:(i + 1) * batch_size]
                # batch_labels = data_y[i * batch_size:(i + 1) * batch_size]
                train_batch_x, train_batch_y = mnist.train.next_batch(batch_size)

                # 使用真實數據填充placeholder,運行訓練模型和合併變量操作
                _, summary, loss = sess.run([train_op, merged_summary_op, total_loss],
                                            feed_dict={inputs: train_batch_x,
                                                       labels: train_batch_y})
                if step % 100 == 0:
                    summary_writer.add_summary(summary, step)  # 將每次迭代后的變量寫入事件文件
                    summary_writer.flush()  # 強制summary_writer將緩存中的數據寫入到日誌文件中(可選)

                ############    可視化打印   ############
                print("Epoch:[%2d] [%4d/%4d] time:%4.4f,loss:%.8f" % (
                    epoch, i, batch_nums, time.time() - start_time, loss))

                # 打印一些可視化的數據,損失...
                # if np.mod(step, 100) == 1
                if step % 100 == 0:
                    acc = sess.run(accuracy, {inputs: mnist.validation.images,
                                              labels: mnist.validation.labels})
                    print("Epoch:[%2d] [%4d/%4d],acc:%.8f" % (epoch, i, batch_nums, acc))
                    ############    保存模型   ############
                    if acc > max_acc:
                        max_acc = acc
                        save_path = saver.save(sess,
                                               save_path=os.path.join(checkpoints_dir, "model.ckpt"),
                                               global_step=step)
                        # logging.info("模型保存在: %s" % save_path)
                        tf.logging.info("模型保存在: %s" % save_path)
                step += 1
            print("優化完成!")


def main(argv=None):
    train()


if __name__ == '__main__':
    # logging.basicConfig(level=logging.INFO)
    tf.logging.set_verbosity(tf.logging.INFO)
    tf.app.run()

main(step)

數據處理

  數據處理因為每個專業領域的原因各不相同,而這不同點也是各位論文創新點的新方向。不同的我沒法講,但我總結了幾點相同的地方——batch數據生成。因為深度學習模型需要一個batch一個batch的喂數據進行訓練,所以我們的數據必須是batch的形式,這裏衍生了三點問題

  1. 通過代碼批量讀取數據,
  2. 如何生成batch數據:由於篇幅過長,實在有很多地方要介紹和詳述,我把這一塊內容移到了這篇文章《TensorFlow讀取數據的三種方法》中
  3. 數據的shape:我舉兩個例子讓大家理解:圖片數據為4維 (batch_size, height,width, channels),序列數據為3維 (batch_size, time_steps, input_size),
    • 不同的shape處理方法不同,選擇神經網絡模型單元也不同。我會在後面細講

模型搭建

  閱讀這一節我默認大家已經學會了數據的batch讀取了。

  模型搭建這一步很像我們小時候玩的搭積木,我這裏以經典神經網絡模型VGG、Alex、ResNet、Google Inception Net為例講解,大家看代碼看多了也會很簡單的就找到,當然我是有一點私心的,我想把這些經典的網絡在這篇文章做一個tensorflow實現匯總,我細講第一個,大家可能看一個例子就懂了,看懂了就直接往下看,看不懂就多看幾個。

LeNet5模型

論文:1998_LeNet_Gradient-Based Learning Applied to Document Recognition

  下面我們定義一個LeNet5模型,我們先定義需要用到的神經網絡單元,相同的代碼盡量封裝成函數的形式以節省代碼量和簡潔代碼

def conv(input, kernel_size, output_size, stride, init_bias=0.0, padding="SAME", name=None, wd=None):
    input_size = input.shape[-1]
    conv_weights = tf.get_variable(name='weights',
                                   shape=[kernel_size, kernel_size, input_size, output_size],
                                   initializer=tf.truncated_normal_initializer(stddev=0.1),
                                   dtype=tf.float32)
    conv_biases = tf.get_variable(name='biases',
                                  shape=[output_size],
                                  initializer=tf.constant_initializer(init_bias),
                                  dtype=tf.float32)

    if wd is not None:
        # wd 0.004
        # tf.nn.l2_loss(var)=sum(t**2)/2
        weight_decay = tf.multiply(tf.nn.l2_loss(conv_weights), wd, name='weight_loss')
        tf.add_to_collection('losses', weight_decay)

    conv_layer = tf.nn.conv2d(input, conv_weights, [1, stride, stride, 1], padding=padding, name=name)  # 卷積操作
    conv_layer = tf.nn.bias_add(conv_layer, conv_biases)  # 加上偏置項
    conv_layer = tf.nn.relu(conv_layer)  # relu激活函數

    return conv_layer


def fc(input, output_size, init_bias=0.0, activeation_func=True, wd=None):
    input_shape = input.get_shape().as_list()
    # 創建 全連接權重 變量
    fc_weights = tf.get_variable(name="weights",
                                 shape=[input_shape[-1], output_size],
                                 initializer=tf.truncated_normal_initializer(stddev=0.1),
                                 dtype=tf.float32)
    if wd is not None:
        # wd 0.004
        # tf.nn.l2_loss(var)=sum(t**2)/2
        weight_decay = tf.multiply(tf.nn.l2_loss(fc_weights), wd, name='weight_loss')
        tf.add_to_collection('losses', weight_decay)
    # 創建 全連接偏置 變量
    fc_biases = tf.get_variable(name="biases",
                                shape=[output_size],
                                initializer=tf.constant_initializer(init_bias),
                                dtype=tf.float32)

    fc_layer = tf.matmul(input, fc_weights)  # 全連接計算
    fc_layer = tf.nn.bias_add(fc_layer, fc_biases)  # 加上偏置項
    if activeation_func:
        fc_layer = tf.nn.relu(fc_layer)  # rele激活函數
    return fc_layer

View Code

  然後利用我們搭建的神經網絡單元,搭建LeNet5神經網絡模型

# 訓練時:keep_prob=0.5
# 測試時:keep_prob=1.0
def leNet(inputs, class_num, keep_prob=0.5):
    # 第一層 卷積層 conv1
    with tf.variable_scope('layer1-conv1'):
        conv1 = conv(input=inputs, kernel_size=5, output_size=32, stride=1, init_bias=0.0, name="layer1-conv1",
                     padding="SAME")
    # 第二層 池化層
    with tf.name_scope('layer2-pool1'):
        pool1 = tf.nn.max_pool(conv1, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
    # 第三層 卷積層 conv2
    with tf.variable_scope('layer3-conv2'):
        conv2 = conv(input=pool1, kernel_size=5, output_size=64, stride=1, init_bias=0.0, name="layer3-conv2",
                     padding="SAME")
    # 第四層 池化層
    with tf.name_scope('layer4-pool2'):
        pool2 = tf.nn.max_pool(conv2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')

    # 後面要做全連接,因此要把數據變成2維
    # pool_shape = pool2.get_shape().as_list()
    pool_shape = pool2.shape
    flatten = tf.reshape(pool2, [-1, pool_shape[1] * pool_shape[2] * pool_shape[3]])
    with tf.variable_scope('layer5-fcl'):
        fc1 = fc(input=flatten, output_size=512, init_bias=0.1, activeation_func=tf.nn.relu, wd=None)
        fc1 = tf.nn.dropout(fc1, keep_prob=keep_prob, name="dropout1")
    with tf.variable_scope('layer6-fc2'):
        logit = fc(input=fc1, output_size=class_num, init_bias=0.1, activeation_func=False, wd=None)
    return logit

Alex模型

論文:2012_Alex_ImageNet Classification with Deep Convolutional Neural Networks

  下面我們定義一個Alex模型,我們先定義需要用到的神經網絡單元,相同的代碼盡量封裝成函數的形式以節省代碼量和簡潔代碼

def conv(input, kernel_size, output_size, stride, init_bias=0.0, padding="SAME", name=None, wd=None):
    input_size = input.shape[-1]
    conv_weights = tf.get_variable(name='weights',
                                   shape=[kernel_size, kernel_size, input_size, output_size],
                                   initializer=tf.random_normal_initializer(mean=0, stddev=0.01),
                                   dtype=tf.float32)
    if wd is not None:
        # wd 0.004
        # tf.nn.l2_loss(var)=sum(t**2)/2
        weight_decay = tf.multiply(tf.nn.l2_loss(conv_weights), wd, name='weight_loss')
        tf.add_to_collection('losses', weight_decay)

    conv_biases = tf.get_variable(name='biases',
                                  shape=[output_size],
                                  initializer=tf.constant_initializer(init_bias),
                                  dtype=tf.float32)
    conv_layer = tf.nn.conv2d(input, conv_weights, [1, stride, stride, 1], padding=padding, name=name)  # 卷積操作
    conv_layer = tf.nn.bias_add(conv_layer, conv_biases)  # 加上偏置項
    conv_layer = tf.nn.relu(conv_layer)  # relu激活函數

    return conv_layer

conv函數

def fc(input, output_size, init_bias=0.0, activeation_func=True, wd=None):
    input_shape = input.get_shape().as_list()
    # 創建 全連接權重 變量
    fc_weights = tf.get_variable(name="weights",
                                 shape=[input_shape[-1], output_size],
                                 initializer=tf.random_normal_initializer(mean=0.0, stddev=0.01),
                                 dtype=tf.float32)
    if wd is not None:
        # wd 0.004
        # tf.nn.l2_loss(var)=sum(t**2)/2
        weight_decay = tf.multiply(tf.nn.l2_loss(fc_weights), wd, name='weight_loss')
        tf.add_to_collection('losses', weight_decay)

    # 創建 全連接偏置 變量
    fc_biases = tf.get_variable(name="biases",
                                shape=[output_size],
                                initializer=tf.constant_initializer(init_bias),
                                dtype=tf.float32)

    fc_layer = tf.matmul(input, fc_weights)  # 全連接計算
    fc_layer = tf.nn.bias_add(fc_layer, fc_biases)  # 加上偏置項
    if activeation_func:
        fc_layer = tf.nn.relu(fc_layer)  # rele激活函數
    return fc_layer

fc函數

def LRN(input, depth_radius=2, alpha=0.0001, beta=0.75, bias=1.0):
    """Local Response Normalization 局部響應歸一化"""
    return tf.nn.local_response_normalization(input, depth_radius=depth_radius, alpha=alpha,
                                              beta=beta, bias=bias)

LRN函數

  然後利用我們搭建的神經網絡單元,搭建Alex神經網絡模型

def alexNet(inputs, class_num, keep_prob=0.5):
    # 第一層卷積層 conv1
    with tf.variable_scope("conv1"):
        conv1 = conv(input=inputs, kernel_size=7, output_size=96, stride=3, init_bias=0.0, name="conv1", padding="SAME")
        conv1 = LRN(conv1)
        conv1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name="pool1")
    # 第二層卷積層 conv2
    with tf.variable_scope("conv2"):
        conv2 = conv(input=conv1, kernel_size=7, output_size=96, stride=3, init_bias=1.0, name="conv2", padding="SAME")
        conv2 = LRN(conv2)
        conv2 = tf.nn.max_pool(conv2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name="pool2")
    # 第三層卷積層 conv3
    with tf.variable_scope("conv3"):
        conv3 = conv(input=conv2, kernel_size=7, output_size=96, stride=3, init_bias=0.0, name="conv3", padding="SAME")
    # 第四層卷積層 conv4
    with tf.variable_scope("conv4"):
        conv4 = conv(input=conv3, kernel_size=7, output_size=96, stride=3, init_bias=1.0, name="conv4", padding="SAME")
    # 第五層卷積層 conv5
    with tf.variable_scope("conv5"):
        conv5 = conv(input=conv4, kernel_size=3, output_size=256, stride=1, init_bias=1.0, name="conv5")
        conv5 = tf.nn.max_pool(conv5, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='VALID', name="pool5")
    conv5_shape = conv5.shape  # 後面做全連接,所以要把shape改成2維
    # shape=[batch, dim]
    flatten = tf.reshape(conv5, [-1, conv5_shape[1] * conv5_shape[2] * conv5_shape[3]])
    # 第一層全連接層 fc1
    with tf.variable_scope("fc1"):
        fc1 = fc(input=flatten, output_size=4096, init_bias=1.0, activeation_func=tf.nn.relu, wd=None)
        fc1 = tf.nn.dropout(fc1, keep_prob=keep_prob, name="dropout1")
    # 第一層全連接層 fc2
    with tf.variable_scope("fc2"):
        fc2 = fc(input=fc1, output_size=4096, init_bias=1.0, activeation_func=tf.nn.relu, wd=None)
        fc2 = tf.nn.dropout(fc2, keep_prob=keep_prob, name="dropout1")
    # 第一層全連接層 fc3
    with tf.variable_scope("fc3"):
        logit = fc(input=fc2, output_size=class_num, init_bias=1.0, activeation_func=False, wd=None)

    return logit  # 模型輸出

VGG模型

論文:2014_VGG_Very Deep Convolutional Networks for Large-Scale Image Recognition

VGG有兩個比較有名的網絡:VGG16、VGG19,我在這裏搭建VGG16,有興趣的朋友可以按照上面的模型結構自己用TensorFlow搭建VGG19模型

  下面我們定義一個VGG16模型,和前面一樣,我們先定義需要用到的神經網絡單元,相同的代碼盡量封裝成函數的形式以節省代碼量和簡潔代碼

  因為模型中同一個變量域中包含多個卷積操作,因此在卷積函數中套一層變量域

def conv(inputs, scope_name, kernel_size, output_size, stride, init_bias=0.0, padding="SAME", wd=None):
    input_size = int(inputs.get_shape()[-1])
    with tf.variable_scope(scope_name):
        conv_weights = tf.get_variable(name='weights',
                                       shape=[kernel_size, kernel_size, input_size, output_size],
                                       dtype=tf.float32,
                                       initializer=tf.truncated_normal_initializer(mean=0.0, stddev=1e-1))
        if wd is not None:
            # tf.nn.l2_loss(var)=sum(t**2)/2
            weight_decay = tf.multiply(tf.nn.l2_loss(conv_weights), wd, name='weight_loss')
            tf.add_to_collection('losses', weight_decay)

        conv_biases = tf.get_variable(name='biases',
                                      shape=[output_size],
                                      dtype=tf.float32,
                                      initializer=tf.constant_initializer(init_bias))
        conv_layer = tf.nn.conv2d(inputs, conv_weights, [1, stride, stride, 1], padding=padding, name=scope_name)
        conv_layer = tf.nn.bias_add(conv_layer, conv_biases)
        conv_layer = tf.nn.relu(conv_layer)
    return conv_layer

conv函數

def fc(inputs, scope_name, output_size, init_bias=0.0, activeation_func=True, wd=None):
    input_shape = inputs.get_shape().as_list()
    with tf.variable_scope(scope_name):
        # 創建 全連接權重 變量
        fc_weights = tf.get_variable(name="weights",
                                     shape=[input_shape[-1], output_size],
                                     dtype=tf.float32,
                                     initializer=tf.truncated_normal_initializer(mean=0.0, stddev=1e-1))
        if wd is not None:
            # wd 0.004
            # tf.nn.l2_loss(var)=sum(t**2)/2
            weight_decay = tf.multiply(tf.nn.l2_loss(fc_weights), wd, name='weight_loss')
            tf.add_to_collection('losses', weight_decay)
        
        # 創建 全連接偏置 變量
        fc_biases = tf.get_variable(name="biases",
                                    shape=[output_size],
                                    dtype=tf.float32,
                                    initializer=tf.constant_initializer(init_bias),
                                    trainable=True)

        fc_layer = tf.matmul(inputs, fc_weights)  # 全連接計算
        fc_layer = tf.nn.bias_add(fc_layer, fc_biases)  # 加上偏置項
        if activeation_func:
            fc_layer = tf.nn.relu(fc_layer)  # rele激活函數
    return fc_layer

fc函數

  然後利用我們搭建的神經網絡單元,搭建VGG16神經網絡模型

def VGG16Net(inputs, class_num):
    with tf.variable_scope("conv1"):
        # conv1_1 [conv3_64]
        conv1_1 = conv(inputs=inputs, scope_name="conv1_1", kernel_size=3, output_size=64, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv1_2 [conv3_64]
        conv1_2 = conv(inputs=conv1_1, scope_name="conv1_2", kernel_size=3, output_size=64, stride=1,
                       init_bias=0.0, padding="SAME")
    pool1 = tf.nn.max_pool(conv1_2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool1')
    with tf.variable_scope("conv2"):
        # conv2_1
        conv2_1 = conv(inputs=pool1, scope_name="conv2_1", kernel_size=3, output_size=128, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv2_2
        conv2_2 = conv(inputs=conv2_1, scope_name="conv2_2", kernel_size=3, output_size=128, stride=1,
                       init_bias=0.0, padding="SAME")
    pool2 = tf.nn.max_pool(conv2_2, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool2')
    with tf.variable_scope("conv3"):
        # conv3_1
        conv3_1 = conv(inputs=pool2, scope_name="conv3_1", kernel_size=3, output_size=256, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv3_2
        conv3_2 = conv(inputs=conv3_1, scope_name="conv3_2", kernel_size=3, output_size=256, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv3_3
        conv3_3 = conv(inputs=conv3_2, scope_name="conv3_3", kernel_size=3, output_size=256, stride=1,
                       init_bias=0.0, padding="SAME")
    pool3 = tf.nn.max_pool(conv3_3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool3')
    with tf.variable_scope("conv4"):
        # conv4_1
        conv4_1 = conv(inputs=pool3, scope_name="conv4_1", kernel_size=3, output_size=512, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv4_2
        conv4_2 = conv(inputs=conv4_1, scope_name="conv4_2", kernel_size=3, output_size=512, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv4_3
        conv4_3 = conv(inputs=conv4_2, scope_name="conv4_3", kernel_size=3, output_size=512, stride=1,
                       init_bias=0.0, padding="SAME")
    pool4 = tf.nn.max_pool(conv4_3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool4')
    with tf.variable_scope("conv5"):
        # conv5_1
        conv5_1 = conv(inputs=pool4, scope_name="conv4_1", kernel_size=3, output_size=512, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv5_2
        conv5_2 = conv(inputs=conv5_1, scope_name="conv4_2", kernel_size=3, output_size=512, stride=1,
                       init_bias=0.0, padding="SAME")
        # conv5_3
        conv5_3 = conv(inputs=conv5_2, scope_name="conv4_3", kernel_size=3, output_size=512, stride=1,
                       init_bias=0.0, padding="SAME")
    pool5 = tf.nn.max_pool(conv5_3, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool4')
    input_shape = pool5.get_shape().as_list()  # 後面做全連接,所以要把shape改成2維
    # shape=[batch, dim]
    flatten = tf.reshape(pool5, [-1, input_shape[1] * input_shape[2] * input_shape[3]])
    fc1 = fc(inputs=flatten, scope_name="fc1", output_size=4096, init_bias=1.0, activeation_func=True)
    fc2 = fc(inputs=fc1, scope_name="fc2", output_size=4096, init_bias=1.0, activeation_func=True)
    fc3 = fc(inputs=fc2, scope_name="fc3", output_size=class_num, init_bias=1.0, activeation_func=True)

    return fc3

上圖中有一個softmax層,我們也可以定義出來

class_num = 1000
# placeholder 定義
inputs = tf.placeholder(dtype="float", shape=[None, 28, 28, 3], name='inputs')
labels = tf.placeholder(dtype="float", shape=[None, class_num], name='labels')
learning_rate = tf.placeholder("float", None, name='learning_rate')

logits = VGG16Net(inputs)
probs = tf.nn.softmax(logits)

ResNet模型

論文

  • 2016_ResNet_Deep Residual Learning for Image Recognition
  • 2016_ResNet_Identity Mappings in Deep Residual Networks

  ResNet的網絡結構如下圖所示

我們先定義需要用到的神經網絡單元

def batch_normalization(inputs, output_size):
    mean, variance = tf.nn.moments(inputs, axes=[0, 1, 2])  # 計算均值和方差
    beta = tf.get_variable('beta', output_size, tf.float32, initializer=tf.zeros_initializer)
    gamma = tf.get_variable('gamma', output_size, tf.float32, initializer=tf.ones_initializer)
    bn_layer = tf.nn.batch_normalization(inputs, mean, variance, beta, gamma, 0.001)

    return bn_layer

batch_normalization函數

def conv(input, kernel_size, output_size, stride, padding="SAME", wd=None):
    input_size = input.shape[-1]
    conv_weights = tf.get_variable(name='weights',
                                   shape=[kernel_size, kernel_size, input_size, output_size],
                                   dtype=tf.float32,
                                   initializer=tf.truncated_normal_initializer(mean=0.0, stddev=0.1),
                                   regularizer=tf.contrib.layers.l2_regularizer(0.00004)) # 正則損失衰減率0.000004

    conv_layer = tf.nn.conv2d(input, conv_weights, [1, stride, stride, 1], padding=padding)  # 卷積操作
    batch_norm = batch_normalization(conv_layer, output_size)
    conv_output = tf.nn.relu(batch_norm)  # relu激活函數
    return conv_output

conv函數

def fc(input, output_size, activeation_func=True):
    input_shape = input.shape[-1]
    # 創建 全連接權重 變量
    fc_weights = tf.get_variable(name="weights",
                                 shape=[input_shape, output_size],
                                 initializer=tf.truncated_normal_initializer(stddev=0.01),
                                 dtype=tf.float32,
                                 regularizer=tf.contrib.layers.l2_regularizer(0.01))
    # 創建 全連接偏置 變量
    fc_biases = tf.get_variable(name="biases",
                                shape=[output_size],
                                initializer=tf.zeros_initializer,
                                dtype=tf.float32)

    fc_layer = tf.matmul(input, fc_weights)  # 全連接計算
    fc_layer = tf.nn.bias_add(fc_layer, fc_biases)  # 加上偏置項
    if activeation_func:
        fc_layer = tf.nn.relu(fc_layer)  # rele激活函數
    return fc_layer

fc函數

def block(input, n, output_size, change_first_stride, bottleneck):
    if n == 0 and change_first_stride:
        stride = 2
    else:
        stride = 1
    if bottleneck:
        with tf.variable_scope('a'):
            conv_a = conv(input=input, kernel_size=1, output_size=output_size, stride=stride, padding="SAME")
            conv_a = batch_normalization(conv_a, output_size)
            conv_a = tf.nn.relu(conv_a)
        with tf.variable_scope('b'):
            conv_b = conv(input=conv_a, kernel_size=3, output_size=output_size, stride=1, padding="SAME")
            conv_b = batch_normalization(conv_b, output_size)
            conv_b = tf.nn.relu(conv_b)

        with tf.variable_scope('c'):
            conv_c = conv(input=conv_b, kernel_size=1, output_size=output_size * 4, stride=1, padding="SAME")
            output = batch_normalization(conv_c, output_size * 4)
    else:
        with tf.variable_scope('A'):
            conv_A = conv(input=input, kernel_size=3, output_size=output_size, stride=stride, padding="SAME")
            conv_A = batch_normalization(conv_A, output_size)
            conv_A = tf.nn.relu(conv_A)

        with tf.variable_scope('B'):
            conv_B = conv(input=conv_A, kernel_size=3, output_size=output_size, stride=1, padding="SAME")
            output = batch_normalization(conv_B, output_size)

    if input.shape == output.shape:
        with tf.variable_scope('shortcut'):
            shortcut = input  # shortcut
    else:
        with tf.variable_scope('shortcut'):
            shortcut = conv(input=input, kernel_size=1, output_size=output_size * 4, stride=1, padding="SAME")
            shortcut = batch_normalization(shortcut, output_size * 4)

    return tf.nn.relu(output + shortcut)

block函數

  然後我們定義神經網絡框架

def inference(inputs, class_num, num_blocks=[3, 4, 6, 3], bottleneck=True):
    # data[1, 224, 224, 3]

    # 我們嘗試搭建50層ResNet
    with tf.variable_scope('conv1'):
        conv1 = conv(input=inputs, kernel_size=7, output_size=64, stride=2, padding="SAME")
        conv1 = batch_normalization(inputs=conv1, output_size=64)
        conv1 = tf.nn.relu(conv1)

    with tf.variable_scope('conv2_x'):
        conv_output = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME')
        for n in range(num_blocks[0]):
            with tf.variable_scope('block%d' % (n + 1)):
                conv_output = block(conv_output, n, output_size=64, change_first_stride=False, bottleneck=bottleneck)

    with tf.variable_scope('conv3_x'):
        for n in range(num_blocks[1]):
            with tf.variable_scope('block%d' % (n + 1)):
                conv_output = block(conv_output, n, output_size=128, change_first_stride=True, bottleneck=bottleneck)

    with tf.variable_scope('conv4_x'):
        for n in range(num_blocks[2]):
            with tf.variable_scope('block%d' % (n + 1)):
                conv_output = block(conv_output, n, output_size=256, change_first_stride=True, bottleneck=bottleneck)

    with tf.variable_scope('conv5_x'):
        for n in range(num_blocks[3]):
            with tf.variable_scope('block%d' % (n + 1)):
                conv_output = block(conv_output, n, output_size=512, change_first_stride=True, bottleneck=bottleneck)

    output = tf.reduce_mean(conv_output, reduction_indices=[1, 2], name="avg_pool")
    with tf.variable_scope('fc'):
        output = fc(output, class_num, activeation_func=False)

    return output

Google Inception Net模型

  Inception Net模型 以後再更新吧,如果這篇文章對大家有用,歡迎大家催促我。

RNN模型

  Tensorflow中的CNN變數很少,而RNN卻豐富多彩,不僅在RNN Cell上有很多種、在實現上也有很多種,在用法上更是花樣百出。

五個基本的RNN CellRNNCellBasicRNNCellLSTMCellBasicLSTMCellGRUCell

RNN Cell的封裝和變形MultiRNNCell(多層RNN)、DropoutWrapperResidualWrapperDeviceWrapper

四種架構 (static+dynamic)*(單向+雙向)=4:static_rnn(靜態RNN)、dynamic_rnn(動態RNN)、static_bidirectional_rnn(靜態雙向RNN)、bidirectional_dynamic_rnn(動態雙向RNN)

五種手法 (one+many)*(one+many) +1=5:

  1. one to one(1 vs 1)輸入一個,輸出一個。其實和全連接神經網絡並沒有什麼區別,這一類別算不得是 RNN。
  2. one to many(1 vs N)輸入一個,輸出多個。圖像標註,輸入一個圖片,得到對圖片的語言描述
  3. many to one(N vs 1)輸入多個,輸出一個。序列分類,把序列壓縮成一個向量
  4. many to many(N vs N)輸入多個,輸出多個。兩者長度可以不一樣。翻譯任務
  5. many to many(N vs N)輸入多個,輸出多個。兩者長度一樣。char RNN

我們先定義需要用到的神經網絡單元

全連接層

def fc(input, output_size, activeation_func=tf.nn.relu):
    input_shape = input.shape[-1]
    # 創建 全連接權重 變量
    fc_weights = tf.get_variable(name="weights",
                                 shape=[input_shape, output_size],
                                 initializer=tf.truncated_normal_initializer(stddev=0.01),
                                 dtype=tf.float32,
                                 regularizer=tf.contrib.layers.l2_regularizer(0.01))
    # 創建 全連接偏置 變量
    fc_biases = tf.get_variable(name="biases",
                                shape=[output_size],
                                initializer=tf.zeros_initializer,
                                dtype=tf.float32)

    fc_layer = tf.matmul(input, fc_weights)  # 全連接計算
    fc_layer = tf.nn.bias_add(fc_layer, fc_biases)  # 加上偏置項
    if activeation_func:
        fc_layer = activeation_func(fc_layer)  # rele激活函數
    return fc_layer

View Code

單層 靜態/動態 LSTM/GRU

#######################################
#       單層 靜態/動態 LSTM/GRU        #
#######################################
# 單層靜態LSTM
def single_layer_static_lstm(input_x, time_steps, hidden_size):
    """
    :param input_x: 輸入張量 形狀為[batch_size, n_steps, input_size]
    :param n_steps: 時序總數
    :param n_hidden: LSTM單元輸出的節點個數 即隱藏層節點數
    """
    # 把輸入input_x按列拆分,並返回一個有n_steps個張量組成的list
    # 如batch_sizex28x28的輸入拆成[(batch_size,28),((batch_size,28))....]
    # 如果是調用的是靜態rnn函數,需要這一步處理   即相當於把序列作為第一維度
    input_x1 = tf.unstack(input_x, num=time_steps, axis=1)
    lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size)  # 創建LSTM_cell
    # 靜態rnn函數傳入的是一個張量list  每一個元素都是一個(batch_size,input_size)大小的張量
    output, states = tf.nn.static_rnn(cell=lstm_cell, inputs=input_x1, dtype=tf.float32)  # 通過cell類構建RNN

    return output, states


# 單層靜態gru
def single_layer_static_gru(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size, n_steps, input_size]
    :param n_steps: 時序總數
    :param n_hidden: gru單元輸出的節點個數 即隱藏層節點數
    :return: 返回靜態單層GRU單元的輸出,以及cell狀態
    """
    # 把輸入input_x按列拆分,並返回一個有n_steps個張量組成的list
    # 如batch_sizex28x28的輸入拆成[(batch_size,28),((batch_size,28))....]
    # 如果是調用的是靜態rnn函數,需要這一步處理   即相當於把序列作為第一維度
    input_x = tf.unstack(input, num=time_steps, axis=1)
    gru_cell = tf.nn.rnn_cell.GRUCell(num_units=hidden_size)  # 創建GRU_cell
    # 靜態rnn函數傳入的是一個張量list  每一個元素都是一個(batch_size,input_size)大小的張量
    output, states = tf.nn.static_rnn(cell=gru_cell, inputs=input_x, dtype=tf.float32)  # 通過cell類構建RNN

    return output, states


# 單層動態LSTM
def single_layer_dynamic_lstm(input, time_steps, hidden_size):
    """
    :param input_x: 輸入張量 形狀為[batch_size, time_steps, input_size]
    :param time_steps: 時序總數
    :param hidden_size: LSTM單元輸出的節點個數 即隱藏層節點數
    :return: 返回動態單層LSTM單元的輸出,以及cell狀態
    """
    lstm_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size)  # 創建LSTM_cell
    # 動態rnn函數傳入的是一個三維張量,[batch_size,time_steps, input_size]  輸出也是這種形狀
    output, states = tf.nn.dynamic_rnn(cell=lstm_cell, inputs=input, dtype=tf.float32)  # 通過cell類構建RNN
    output = tf.transpose(output, [1, 0, 2])  # 注意這裏輸出需要轉置  轉換為時序優先的
    return output, states


# 單層動態gru
def single_layer_dynamic_gru(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size, time_steps, input_size]
    :param time_steps: 時序總數
    :param hidden_size: GRU單元輸出的節點個數 即隱藏層節點數
    :return: 返回動態單層GRU單元的輸出,以及cell狀態
    """
    gru_cell = tf.nn.rnn_cell.GRUCell(num_units=hidden_size)  # 創建GRU_cell
    # 動態rnn函數傳入的是一個三維張量,[batch_size,n_steps,input_size]  輸出也是這種形狀
    output, states = tf.nn.dynamic_rnn(cell=gru_cell, inputs=input, dtype=tf.float32)  # 通過cell類構建RNN
    output = tf.transpose(output, [1, 0, 2])  # 注意這裏輸出需要轉置  轉換為時序優先的
    return output, states

View Code

多層 靜態/動態 LSTM/GRU

#######################################
#       多層 靜態/動態 LSTM/GRU        #
#######################################
# 多層靜態LSTM網絡
def multi_layer_static_lstm(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,time_steps,input_size]
    :param time_steps: 時序總數
    :param n_hidden: LSTM單元輸出的節點個數 即隱藏層節點數
    :return: 返回靜態多層LSTM單元的輸出,以及cell狀態
    """
    # 把輸入input_x按列拆分,並返回一個有n_steps個張量組成的list
    # 如batch_sizex28x28的輸入拆成[(batch_size,28),((batch_size,28))....]
    # 如果是調用的是靜態rnn函數,需要這一步處理   即相當於把序列作為第一維度
    input_x1 = tf.unstack(input, num=time_steps, axis=1)

    # 多層RNN的實現 例如cells=[cell1,cell2,cell3],則表示一共有三層
    mcell = tf.nn.rnn_cell.MultiRNNCell(
        [tf.nn.rnn_cell.LSTMCell(num_units=hidden_size) for _ in range(3)])

    # 靜態rnn函數傳入的是一個張量list  每一個元素都是一個(batch_size,input_size)大小的張量
    output, states = tf.nn.static_rnn(cell=mcell, inputs=input_x1, dtype=tf.float32)

    return output, states


# 多層靜態GRU
def multi_layer_static_gru(input, time_steps, hidden_size):
    """
    :param input_x: 輸入張量 形狀為[batch_size,n_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: gru單元輸出的節點個數 即隱藏層節點數
    :return: 返回靜態多層GRU單元的輸出,以及cell狀態
    """
    # 把輸入input_x按列拆分,並返回一個有n_steps個張量組成的list
    # 如batch_sizex28x28的輸入拆成[(batch_size,28),((batch_size,28))....]
    # 如果是調用的是靜態rnn函數,需要這一步處理   即相當於把序列作為第一維度
    input_x = tf.unstack(input, num=time_steps, axis=1)

    # 多層RNN的實現 例如cells=[cell1,cell2,cell3],則表示一共有三層
    mcell = tf.nn.rnn_cell.MultiRNNCell(
        [tf.nn.rnn_cell.GRUCell(num_units=hidden_size) for _ in range(3)])

    # 靜態rnn函數傳入的是一個張量list  每一個元素都是一個(batch_size,input_size)大小的張量
    output, states = tf.nn.static_rnn(cell=mcell, inputs=input_x, dtype=tf.float32)

    return output, states


# 多層靜態GRU和LSTM 混合
def multi_layer_static_mix(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,n_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: gru單元輸出的節點個數 即隱藏層節點數
    :return: 返回靜態多層GRU和LSTM混合單元的輸出,以及cell狀態
    """
    # 把輸入input_x按列拆分,並返回一個有n_steps個張量組成的list
    # 如batch_sizex28x28的輸入拆成[(batch_size,28),((batch_size,28))....]
    # 如果是調用的是靜態rnn函數,需要這一步處理   即相當於把序列作為第一維度
    input_x = tf.unstack(input, num=time_steps, axis=1)

    # 可以看做2個隱藏層
    lstm_cell = tf.nn.rnn_cell.LSTMCell(num_units=hidden_size)
    gru_cell = tf.nn.rnn_cell.GRUCell(num_units=hidden_size * 2)

    # 多層RNN的實現 例如cells=[cell1,cell2],則表示一共有兩層,數據經過cell1后還要經過cells
    mcell = tf.nn.rnn_cell.MultiRNNCell(cells=[lstm_cell, gru_cell])

    # 靜態rnn函數傳入的是一個張量list  每一個元素都是一個(batch_size,input_size)大小的張量
    output, states = tf.nn.static_rnn(cell=mcell, inputs=input_x, dtype=tf.float32)

    return output, states


# 多層動態LSTM
def multi_layer_dynamic_lstm(input, time_steps, hidden_size):
    """
    :param input: 輸入張量  形狀為[batch_size,n_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: LSTM單元輸出的節點個數 即隱藏層節點數
    :return: 返回動態多層LSTM單元的輸出,以及cell狀態
    """
    # 多層RNN的實現 例如cells=[cell1,cell2],則表示一共有兩層,數據經過cell1后還要經過cells
    mcell = tf.nn.rnn_cell.MultiRNNCell(
        [tf.nn.rnn_cell.LSTMCell(num_units=hidden_size) for _ in range(3)])

    # 動態rnn函數傳入的是一個三維張量,[batch_size,n_steps,input_size]  輸出也是這種形狀
    output, states = tf.nn.dynamic_rnn(cell=mcell, inputs=input, dtype=tf.float32)

    # 注意這裏輸出需要轉置  轉換為時序優先的
    output = tf.transpose(output, [1, 0, 2])
    return output, states


# 多層動態GRU
def multi_layer_dynamic_gru(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,n_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: gru單元輸出的節點個數 即隱藏層節點數
    :return: 返回動態多層GRU單元的輸出,以及cell狀態
    """
    # 多層RNN的實現 例如cells=[cell1,cell2],則表示一共有兩層,數據經過cell1后還要經過cells
    mcell = tf.nn.rnn_cell.MultiRNNCell(
        [tf.nn.rnn_cell.GRUCell(num_units=hidden_size) for _ in range(3)])

    # 動態rnn函數傳入的是一個三維張量,[batch_size,n_steps,input_size]  輸出也是這種形狀
    output, states = tf.nn.dynamic_rnn(cell=mcell, inputs=input, dtype=tf.float32)

    # 注意這裏輸出需要轉置  轉換為時序優先的
    output = tf.transpose(output, [1, 0, 2])
    return output, states


# 多層動態GRU和LSTM 混合
def multi_layer_dynamic_mix(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,n_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: gru單元輸出的節點個數 即隱藏層節點數
    :return: 返回動態多層GRU和LSTM混合單元的輸出,以及cell狀態
    """
    # 可以看做2個隱藏層
    gru_cell = tf.nn.rnn_cell.GRUCell(num_units=hidden_size * 2)
    lstm_cell = tf.nn.rnn_cell.LSTMCell(num_units=hidden_size)

    # 多層RNN的實現 例如cells=[cell1,cell2],則表示一共有兩層,數據經過cell1后還要經過cells
    mcell = tf.nn.rnn_cell.MultiRNNCell(cells=[lstm_cell, gru_cell])

    # 動態rnn函數傳入的是一個三維張量,[batch_size,n_steps,input_size]  輸出也是這種形狀
    output, states = tf.nn.dynamic_rnn(cell=mcell, inputs=input, dtype=tf.float32)

    # 注意這裏輸出需要轉置  轉換為時序優先的
    output = tf.transpose(output, [1, 0, 2])
    return output, states

View Code

單層/多層 雙向 靜態/動態 LSTM/GRU

#######################################
#   單層/多層 雙向 靜態/動態 LSTM/GRU   #
#######################################
# 單層靜態雙向LSTM
def single_layer_static_bi_lstm(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,time_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: LSTM單元輸出的節點個數 即隱藏層節點數
    :return: 返回單層靜態雙向LSTM單元的輸出,以及cell狀態
    """
    # 把輸入input_x按列拆分,並返回一個有n_steps個張量組成的list
    # 如batch_sizex28x28的輸入拆成[(batch_size,28),((batch_size,28))....]
    # 如果是調用的是靜態rnn函數,需要這一步處理   即相當於把序列作為第一維度
    input_x = tf.unstack(input, num=time_steps, axis=1)
    lstm_fw_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size)  # 正向
    lstm_bw_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size)  # 反向

    # 靜態rnn函數傳入的是一個張量list  每一個元素都是一個(batch_size,input_size)大小的張量
    # 這裏的輸出output是一個list 每一個元素都是前向輸出,後向輸出的合併
    output, fw_state, bw_state = tf.nn.static_bidirectional_rnn(cell_fw=lstm_fw_cell,
                                                                cell_bw=lstm_bw_cell,
                                                                inputs=input_x,
                                                                dtype=tf.float32)
    print(type(output))  # <class 'list'>
    print(len(output))  # 28
    print(output[0].shape)  # (?, 256)

    return output, fw_state, bw_state


# 單層動態雙向LSTM
def single_layer_dynamic_bi_lstm(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,time_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: gru單元輸出的節點個數 即隱藏層節點數
    :return: 返回單層動態雙向LSTM單元的輸出,以及cell狀態
    """
    lstm_fw_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size)  # 正向
    lstm_bw_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size)  # 反向

    # 動態rnn函數傳入的是一個三維張量,[batch_size,time_steps,input_size]  輸出是一個元組 每一個元素也是這種形狀
    output, state = tf.nn.bidirectional_dynamic_rnn(cell_fw=lstm_fw_cell,
                                                    cell_bw=lstm_bw_cell,
                                                    inputs=input,
                                                    dtype=tf.float32)
    print(type(output))  # <class 'tuple'>
    print(len(output))  # 2
    print(output[0].shape)  # (?, 28, 128)
    print(output[1].shape)  # (?, 28, 128)

    output = tf.concat(output, axis=2)  # 按axis=2合併 (?,28,128) (?,28,128)按最後一維合併(?,28,256)
    output = tf.transpose(output, [1, 0, 2])  # 注意這裏輸出需要轉置  轉換為時序優先的

    return output, state


# 多層靜態雙向LSTM
def multi_layer_static_bi_lstm(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,time_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: LSTM單元輸出的節點個數 即隱藏層節點數
    :return: 返回多層靜態雙向LSTM單元的輸出,以及cell狀態
    """
    # 把輸入input_x按列拆分,並返回一個有n_steps個張量組成的list
    # 如batch_sizex28x28的輸入拆成[(batch_size,28),((batch_size,28))....]
    # 如果是調用的是靜態rnn函數,需要這一步處理   即相當於把序列作為第一維度
    input_x = tf.unstack(input, num=time_steps, axis=1)

    stacked_fw_rnn = []
    stacked_bw_rnn = []
    for i in range(3):
        stacked_fw_rnn.append(tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size))  # 正向
        stacked_bw_rnn.append(tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size))  # 反向

    # 靜態rnn函數傳入的是一個張量list  每一個元素都是一個(batch_size,input_size)大小的張量
    # 這裏的輸出output是一個list 每一個元素都是前向輸出,後向輸出的合併
    output, fw_state, bw_state = tf.contrib.rnn.stack_bidirectional_rnn(stacked_fw_rnn,
                                                                        stacked_bw_rnn,
                                                                        inputs=input_x,
                                                                        dtype=tf.float32)
    print(type(output))  # <class 'list'>
    print(len(output))  # 28
    print(output[0].shape)  # (?, 256)

    return output, fw_state, bw_state


# 多層動態雙向LSTM
def multi_layer_dynamic_bi_lstm(input, time_steps, hidden_size):
    """
    :param input: 輸入張量 形狀為[batch_size,n_steps,input_size]
    :param time_steps: 時序總數
    :param hidden_size: gru單元輸出的節點個數 即隱藏層節點數
    :return: 返回多層動態雙向LSTM單元的輸出,以及cell狀態
    """
    stacked_fw_rnn = []
    stacked_bw_rnn = []
    for i in range(3):
        stacked_fw_rnn.append(tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size))  # 正向
        stacked_bw_rnn.append(tf.nn.rnn_cell.BasicLSTMCell(num_units=hidden_size))  # 反向

    # 動態rnn函數傳入的是一個三維張量,[batch_size,n_steps,input_size]  輸出也是這種形狀,
    # input_size變成了正向和反向合併之後的 即input_size*2
    output, fw_state, bw_state = tf.contrib.rnn.stack_bidirectional_dynamic_rnn(stacked_fw_rnn,
                                                                                stacked_bw_rnn,
                                                                                inputs=input,
                                                                                dtype=tf.float32)
    print(type(output))  # <class 'tensorflow.python.framework.ops.Tensor'>
    print(output.shape)  # (?, 28, 256)

    output = tf.transpose(output, [1, 0, 2])  # 注意這裏輸出需要轉置  轉換為時序優先的

    return output, fw_state, bw_state

View Code

然後我們定義神經網絡框架

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

def RNN_inference(inputs, class_num, time_steps, hidden_size):
    """
    :param inputs: [batch_size, n_steps, input_size]
    :param class_num: 類別數
    :param time_steps: 時序總數
    :param n_hidden: LSTM單元輸出的節點個數 即隱藏層節點數
    """
    #######################################
    #       單層 靜態/動態 LSTM/GRU        #
    #######################################
    # outputs, states = single_layer_static_lstm(inputs, time_steps, hidden_size)  # 單層靜態LSTM
    # outputs, states = single_layer_static_gru(inputs, time_steps, hidden_size)   # 單層靜態gru
    # outputs, states = single_layer_dynamic_lstm(inputs, time_steps, hidden_size)  # 單層動態LSTM
    # outputs, states = single_layer_dynamic_gru(inputs, time_steps, hidden_size)  # 單層動態gru
    #######################################
    #       多層 靜態/動態 LSTM/GRU        #
    #######################################
    # outputs, states = multi_layer_static_lstm(inputs, time_steps, hidden_size)  # 多層靜態LSTM網絡
    # outputs, states = multi_layer_static_gru(inputs, time_steps, hidden_size)  # 多層靜態GRU
    # outputs, states = multi_layer_static_mix(inputs, time_steps, hidden_size)  # 多層靜態GRU和LSTM 混合
    # outputs, states = multi_layer_dynamic_lstm(inputs, time_steps, hidden_size)  # 多層動態LSTM
    # outputs, states = multi_layer_dynamic_gru(inputs, time_steps, hidden_size)  # 多層動態GRU
    # outputs, states = multi_layer_dynamic_mix(inputs, time_steps, hidden_size)  # 多層動態GRU和LSTM 混合
    #######################################
    #   單層/多層 雙向 靜態/動態 LSTM/GRU  #
    #######################################
    # outputs, fw_state, bw_state = single_layer_static_bi_lstm(inputs, time_steps, hidden_size)  # 單層靜態雙向LSTM
    # outputs, state = single_layer_dynamic_bi_lstm(inputs, time_steps, hidden_size)  # 單層動態雙向LSTM
    # outputs, fw_state, bw_state = multi_layer_static_bi_lstm(inputs, time_steps, hidden_size)  # 多層靜態雙向LSTM
    outputs, fw_state, bw_state = multi_layer_dynamic_bi_lstm(inputs, time_steps, hidden_size)  # 多層動態雙向LSTM

    # output靜態是 time_step=28個(batch=128, output=128)組成的列表
    # output動態是 (time_step=28, batch=128, output=128)
    print('hidden:', outputs[-1].shape)  # 最後一個時序的shape(128,128)

    # 取LSTM最後一個時序的輸出,然後經過全連接網絡得到輸出值
    fc_output = fc(input=outputs[-1], output_size=class_num, activeation_func=tf.nn.relu)

    return fc_output

設置全局變量和超參數

  在模型訓練之前我們首先會定義一些超參數:batch_size、batch_nums、class_num、epochs、learning_rate

batch_size = FLAGS.batch_size
batch_nums = mnist.train.images.shape[0] // batch_size  # 一個epoch中應該包含多少batch數據
class_num = FLAGS.class_num
epochs = FLAGS.epochs
learning_rate = FLAGS.learning_rate

保存檢查點的地址

############    保存檢查點的地址   ############
checkpoints_dir = FLAGS.checkpoints_dir  # checkpoints
# 如果檢查點不存在,則創建
if not os.path.exists(checkpoints_dir):
    os.makedirs(FLAGS.checkpoints_dir)

創建圖

  這一步可以不設置,因為tensorflow有一個默認圖,我們定義的操作都是在默認圖上的,當然我們也可以定義自己的,方便管理。

######################################################
#                    創建圖                          #
######################################################
graph = tf.Graph()  # 自定義圖
# 在自己的圖中定義數據和操作
with graph.as_default():

佔位符

  一般我們會把input和label做成placeholder,方便我們使用把不同的batch數據傳入網絡,一些其他的超參數也可以做成placeholder,比如learning_rate、dorpout_keep_prob。一般在搭建模型的時候把placeholder的變量傳入模型,在訓練模型sess.run(train_op, feed_dict)的時候通過參數feed_dict={input:真實數據,label:真實標籤} 把真實的數據傳入神經網絡。

inputs = tf.placeholder(dtype="float", shape=[None, 28, 28, 1], name='inputs')
labels = tf.placeholder(dtype="float", shape=[None, class_num], name='labels')
# 看個人喜歡,有的人在初始化定義中就定義了learning_rate,有的人喜歡通過feed傳learning_rate
learning_rate = tf.placeholder("float", None, name='learning_rate')
# 如果網絡結構有dropout層,需要定義keep_probn,如果沒有則不需要
# 訓練的時候需要,測試的時候需要設置成1
keep_prob = tf.placeholder(dtype="float", name='keep_prob')  

搭建模型

  傳進入的都是placeholder數據,不是我們之前整理好的batch數據。

############    搭建模型   ############
logits = alexNet(inputs, class_num, keep_prob=keep_prob)  # 使用placeholder搭建模型

構建損失

  分類任務一般輸出的是每個類別的概率向量,因此模型輸出最後都要經過softmax轉換成概率。一般經過softmax的輸出損失函數都是交叉熵損失函數,tensorflow有將以上兩步合在一起的現成函數 tf.nn.softmax_cross_entropy_with_logits

############    損失函數   ############
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=labels, logits=logits))
tf.add_to_collection('losses', loss)
total_loss = tf.add_n(tf.get_collection("loss"))  # total_loss=模型損失+權重正則化損失

自定義損失

  以後更新,歡迎大家催我。

模型精度

  在測試數據集上的精度

############    模型精度   ############
predict = tf.argmax(logits, 1)      # 模型預測結果
accuracy = tf.reduce_mean(tf.cast(tf.equal(predict, tf.argmax(labels, 1)), tf.float32))

自定義度量

  以後更新,歡迎大家催我。

優化器

  創建優化器,更新網絡參數,最小化loss

  優化器的種類有很多種,但是用法都差不多,常用的優化器有:

  • tf.train.AdamOptimizer
  • tf.train.GradientDescentOptimizer

  • tf.train.RMSPropOptimizer

下面以Adam優化器為例

############    優化器   ############
variable_to_train = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES)  # 可訓練變量列表
global_step = tf.Variable(0, trainable=False)    # 訓練step
# 設置學習率衰減
learning_rate = tf.train.exponential_decay(learning_rate=learning_rate,  # 初始學習率
                                           global_step=global_step,
                                           decay_steps=batch_nums,  # 多少步衰減一次
                                           decay_rate=0.1,  # 衰減率
                                           staircase=True)  # 以階梯的形式衰減
# 創建Adam優化器,更新模型參數,最小化損失函數
train_op = tf.train.AdamOptimizer(learning_rate).minimize(loss=total_loss,  # 損失函數
                                                          global_step=global_step,
                                                          var_list=variable_to_train)  # 通過訓練需要更新的參數列表

講解

  • variable_to_train:上面的代碼定義了可訓練變量,我只是把列出了模型默認的可訓練變量,這一個步是tensorflow默認的,如果不設置也沒有關係。我寫出來的原因是,有的大牛會這麼寫,對不同的可訓練變量分別進行不同的優化,希望大家看到我的代碼,下次看到別人的不會覺得陌生。
  • global_step:大多數人會用step=0,然後在訓練的時候step+=1的方式更新step,但是本文介紹的是另一種方式,以tf.Variable的方式定義step,在模型訓練的時候傳入sess.run,global_step會自動+1更新
  • learning_rate:本文還設置了學習率衰減,大家也可以不設置,以固定的學習率訓練模型,但是對於大型項目,還是推薦設置。

移動平均值更新參數

採用移動平均值的方式更新損失值和模型參數

def train(total_loss, global_step):
    lr = tf.train.exponential_decay(0.01, global_step, decay_steps=350, decay_rate=0.1, staircase=True)
    # 採用滑動平均的方法更新損失值
    loss_averages = tf.train.ExponentialMovingAverage(decay=0.9, name='avg')
    losses = tf.get_collection('losses')  # losses的列表
    loss_averages_op = loss_averages.apply(losses + [total_loss])  # 計算損失值的影子變量op

    # 計算梯度
    with tf.control_dependencies([loss_averages_op]):  # 控制計算指定,只有執行了括號中的語句才能執行下面的語句
        opt = tf.train.GradientDescentOptimizer(lr)  # 創建優化器
        grads = opt.compute_gradients(total_loss)  # 計算梯度

    # 應用梯度
    apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)

    # 採用滑動平均的方法更新參數
    variable_averages = tf.train.ExponentialMovingAverage(0.999, num_updates=global_step)
    variables_averages_op = variable_averages.apply(tf.trainable_variables())

    with tf.control_dependencies([apply_gradient_op, variables_averages_op]):
        # tf.no_op()表示執行完apply_gradient_op, variable_averages_op操作之後什麼都不做
        train_op = tf.no_op(name='train')

    return train_op

View Code

TensorBoard可視化 summary

############    TensorBoard可視化 summary  ############
summary_writer = tf.summary.FileWriter(logdir="./logs", graph=graph)  # 創建事件文件
tf.summary.scalar(name="losses", tensor=total_loss)  # 收集損失值變量
tf.summary.scalar(name="acc", tensor=accuracy)  # 收集精度值變量
tf.summary.scalar(name='learning_rate', tensor=learning_rate)
merged_summary_op = tf.summary.merge_all()  # 將所有的summary合併為一個op

模型保存和恢復 Saver

saver = tf.train.Saver(max_to_keep=5)  # 保存最新的5個檢查點

創建會話

配置會話

  在創建會話之前我們一般都要配置會話,比如使用GPU還是CPU,用多少GPU等等。

我們一般使用 tf.ConfigProto()配置Session運行參數&&GPU設備指定

config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)
config.gpu_options.per_process_gpu_memory_fraction = 0.4  # 佔用40%顯存 sess = tf.Session(config=config)
# 或者
config = tf.ConfigProto()
config.allow_soft_placement = True
config.log_device_placement = True

with tf.Session(config=config) as sess:
# 或者
sess = tf.Session(config=config)

tf.ConfigProto(log_device_placement=True):記錄設備指派情況

  設置tf.ConfigProto()中參數log_device_placement = True,獲取 operations 和 Tensor 被指派到哪個設備(幾號CPU或幾號GPU)上運行,會在終端打印出各項操作是在哪個設備上運行的。

tf.ConfigProto(allow_soft_placement=True):自動選擇運行設備

  在TensorFlow中,通過命令 “with tf.device(‘/cpu:0’):“,允許手動設置操作運行的設備。如果手動設置的設備不存在或者不可用,就會導致tf程序等待或異常,為了防止這種情況,可以設置tf.ConfigProto()中參數allow_soft_placement=True,自動選擇一個存在並且可用的設備來運行操作。

config.gpu_options.allow_growth = True

當使用GPU時候,Tensorflow運行自動慢慢達到最大GPU的內存

tf.test.is_built_with_cuda():返回是否能夠使用GPU進行運算

  為了加快運行效率,TensorFlow在初始化時會嘗試分配所有可用的GPU顯存資源給自己,這在多人使用的服務器上工作就會導致GPU佔用,別人無法使用GPU工作的情況。這時我們需要限制GPU資源使用,詳細實現方法請參考我的另一篇博客 tensorflow常用函數 Ctrl+F搜索“限制GPU資源使用”

創建會話Session

  Session有兩種創建方式:

sess = tf.Session(config=config, graph=graph)
# 或通過with的方式創建Session
with tf.Session(config=config, graph=graph) as sess:

  如果我們之前自定義了graph,則在會話中也要配置graph,如果之前沒有自定義graph,使用的是tensorflow默認graph,則在會話不用自己去定義,tensorflow會自動找到默認圖。

  在訓練模型之前我們首先要設置一個高級一點的東西,那就是檢查是否有之前保存好的模型,如果有着接着前面的繼續訓練,如果沒有則從頭開始訓練模型。

恢復/重新訓練

  定義一個檢查模型是否存在的函數,為了美觀,可以把這個函數放在最上面,或者其他腳本中,通過import導入。

def load_model(sess, saver, checkpoint_dir):
    """加載模型,看看還能不能加一個功能,必須現在的檢查檢點是1000,但是我的train是100,要報錯
        還有就是讀取之前的模型繼續訓練的問題
        checkpoint_dir = checkpoint"""

    # 通過checkpoint找到模型文件名
    ckpt = tf.train.get_checkpoint_state(checkpoint_dir=checkpoint_dir)
    if ckpt and ckpt.model_checkpoint_path:
        ckpt_name = os.path.basename(ckpt.model_checkpoint_path)  # 返回最新的chechpoint文件名 model.ckpt-1000
        print("新的chechpoint文件名", ckpt_name)  # model.ckpt-2
        saver.restore(sess, os.path.join(checkpoint_dir, ckpt_name))
        # 現在不知道checkpoint文件名時怎樣的,因此不知道裏面如何運行
        counter = int(next(re.finditer("(\d+)(?!.*\d)", ckpt_name)).group(0))  # 2
        print(" [*] 成功模型 {}".format(ckpt_name))
        return True, counter
    else:
        print(" [*] 找不到checkpoint")
        return False, 0

View Code

  如果大家之前用的是global_step = tf.Variable(0, trainable=False),則使用下面diamante

# 加載模型,如果模型存在返回 是否加載成功和訓練步數
could_load, checkpoint_step = load_model(sess, saver, "./log")
if could_load:
    print(" [*] 加載成功")
else:
    print(" [!] 加載失敗")
    try:
        tf.global_variables_initializer().run()
    except:
        tf.initialize_all_variables().run()

  如果大家想使用step=0,step+=1,則可以使用下面代碼

# 加載模型,如果模型存在返回 是否加載成功和訓練步數
could_load, checkpoint_step = load_model(sess, saver, FLAGS.checkpoints_dir)
if could_load:
    step = checkpoint_step
    print(" [*] 模型加載成功")
else:
    print(" [!] 模型加載失敗")
    try:
        tf.global_variables_initializer().run()
    except:
        tf.initialize_all_variables().run()
    step = 0

開始訓練

for epoch in range(epochs):
    for i in range(batch_nums):
        start_time = time.time()
        # batch_images = data_X[i * batch_size:(i + 1) * batch_size]
        # batch_labels = data_y[i * batch_size:(i + 1) * batch_size]
        train_batch_x, train_batch_y = mnist.train.next_batch(batch_size)

        # 使用真實數據填充placeholder,運行訓練模型和合併變量操作
        _, summary, loss, step = sess.run([train_op, merged_summary_op, total_loss, global_step],
                                          feed_dict={inputs: train_batch_x,
                                                     labels: train_batch_y,
                                                     keep_prob: 0.5})
        if step % 100 == 0:
            summary_writer.add_summary(summary, step)  # 將每次迭代后的變量寫入事件文件
            summary_writer.flush()  # 強制summary_writer將緩存中的數據寫入到日誌文件中(可選)

            ############    可視化打印   ############
            print("Epoch:[%2d] [%4d/%4d] time:%4.4f,loss:%.8f" % (
                epoch, i, batch_nums, time.time() - start_time, loss))

        # 打印一些可視化的數據,損失...
        if step % 100 == 0:
            acc = sess.run(accuracy, feed_dict={inputs: mnist.validation.images,
                                                labels: mnist.validation.labels,
                                                keep_prob: 1.0})
            print("Epoch:[%2d] [%4d/%4d] accuracy:%.8f" % (epoch, i, batch_nums, acc))
            ############    保存模型   ############
            if acc > max_acc:
                max_acc = acc
                save_path = saver.save(sess,
                                       save_path=os.path.join(checkpoints_dir, "model.ckpt"),
                                       global_step=step)
                tf.logging.info("模型保存在: %s" % save_path)
print("優化完成!")

模型評估

eval.py

模型評估的代碼和模型訓練的代碼很像,只不過不需要對模型進行訓練而已。

from ops import *
import tensorflow as tf
from nets.my_alex import alexNet
from tensorflow.examples.tutorials.mnist import input_data

tf.flags.DEFINE_integer('batch_size', 50, 'batch size, default: 1')
tf.flags.DEFINE_integer('class_num', 10, 'batch size, default: 1')
tf.flags.DEFINE_integer('epochs', 10, 'batch size, default: 1')
tf.flags.DEFINE_string('checkpoints_dir', "checkpoints", '保存檢查點的地址')
FLAGS = tf.flags.FLAGS

# 從MNIST_data/中讀取MNIST數據。當數據不存在時,會自動執行下載
mnist = input_data.read_data_sets('./data', one_hot=True, reshape=False)

# 將數組張換成圖片形式
print(mnist.train.images.shape)  # 訓練數據圖片(55000, 28, 28, 1)
print(mnist.train.labels.shape)  # 訓練數據標籤(55000, 10)
print(mnist.test.images.shape)  # 測試數據圖片(10000, 28, 28, 1)
print(mnist.test.labels.shape)  # 測試數據圖片(10000, 10)
print(mnist.validation.images.shape)  # 驗證數據圖片(5000, 28, 28, 1)
print(mnist.validation.labels.shape)  # 驗證數據圖片(5000, 10)


def evaluate():
    batch_size = FLAGS.batch_size
    batch_nums = mnist.train.images.shape[0] // batch_size  # 一個epoch中應該包含多少batch數據
    class_num = FLAGS.class_num
    test_batch_size = 5000
    test_batch_num = mnist.test.images.shape[0] // test_batch_size

    ############    保存檢查點的地址   ############
    checkpoints_dir = FLAGS.checkpoints_dir  # checkpoints
    # 如果檢查點不存在,則創建
    if not os.path.exists(checkpoints_dir):
        print("模型文件不存在,無法進行評估")

    ######################################################
    #                    創建圖                          #
    ######################################################
    graph = tf.Graph()  # 自定義圖
    # 在自己的圖中定義數據和操作
    with graph.as_default():
        inputs = tf.placeholder(dtype="float", shape=[None, 28, 28, 1], name='inputs')
        labels = tf.placeholder(dtype="float", shape=[None, class_num], name='labels')
        ############    搭建模型   ############
        logits = alexNet(inputs, FLAGS.class_num, keep_prob=1)  # 使用placeholder搭建模型
        ############    模型精度   ############
        predict = tf.argmax(logits, 1)
        accuracy = tf.reduce_mean(tf.cast(tf.equal(predict, tf.argmax(labels, 1)), tf.float32))
        ############    模型保存和恢復 Saver   ############
        saver = tf.train.Saver(max_to_keep=5)

    ######################################################
    #                   創建會話                          #
    ######################################################
    config = tf.ConfigProto(allow_soft_placement=True, log_device_placement=True)
    with tf.Session(config=config, graph=graph) as sess:
        # 加載模型,如果模型存在返回 是否加載成功和訓練步數
        could_load, checkpoint_step = load_model(sess, saver, FLAGS.checkpoints_dir)
        if could_load:
            print(" [*] 加載成功")
        else:
            print(" [!] 加載失敗")
            raise ValueError("模型文件不存在,無法進行評估")

        for i in range(test_batch_num):
            test_batch_x, test_batch_y = mnist.test.next_batch(test_batch_num)
            acc = sess.run(accuracy, feed_dict={inputs: test_batch_x,
                                                labels: test_batch_y})
            print("模型精度為:", acc)
        one_image = mnist.test.images[1].reshape(1, 28, 28, 1)
        predict_label = sess.run(predict, feed_dict={inputs: one_image})
        # print("123", tf.argmax(pre_yyy, 1).eval())  # [7]
        # print("123", tf.argmax(yyy, 1).eval())  # 7


def main(argv=None):
    evaluate()


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

 

參考文獻

CSDN_AlexNet神經網絡結構

CSDN_【深度學習理論3】ALexNet模型的詳解

github搜索tensorflow AlexNet

github_finetune_alexnet_with_tensorflow

github_AlexNet_with_tensorflow

github tensorflow vgg

ResNet詳解與分析

tensorflow中使用tf.ConfigProto()配置Session運行參數&&GPU設備指定

比較完整且容易入門的MNIST案例

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

台中搬家公司推薦超過30年經驗,首選台中大展搬家

Golang-自動化監控教務系統成績單_台中搬家公司

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」

目錄

  • 一、Golang模擬用戶登陸,突破教務系統
    • 1.1 請求登陸頁面
    • 1.2 抓包分析登陸請求
    • 1.3 golang使用js引擎合成salt
    • 1.4 模擬表單提交,完成登陸
    • 1.5 進入成績查詢頁,解析用戶成績
  • 二、植入微信公共號後台

一、Golang模擬用戶登陸,突破教務系統

1.1 請求登陸頁面

整個流程中的第一步是獲取登陸頁面,就像下圖這樣人為的通過瀏覽器訪問服務端,服務端返回反饋返回登陸頁面

訪問登陸頁面的目的上圖中標註出來了,為了獲取到Cookie,給真正發起登陸到請求方法使用。

下面的golang發送http到get請求,獲取登陸頁面的代碼:

// 訪問登陸也,獲取cookie
func GetCookieFromLoginhtml(url string) (cookie string, e error) {
   res, err := http.Get(url)
   if err != nil {
   	e = err
   }
   // 獲取cookie
   cookie = res.Header.Get("Set-Cookie")
   cookie = util.GetOneValueByPrefixAndSurfix("JSESSIONID=", "; Path=/", cookie)
   res.Body.Close()
   return
}

1.2 抓包分析登陸請求

輸入賬號賬號密碼後點擊登陸,將向後端發送登陸請求,如下圖:

分析向後端發送到登陸請求都攜帶了哪些請求參數,攜帶了哪些請求頭信息,以及需要通過Content-Type判斷,該如何處理form表單中的數據發送到後台。後台才能正常響應。

在瀏覽器的控制台中我們可以去看下登陸頁面源碼

登陸頁面對應的js源碼

1.3 golang使用js引擎合成salt

這一步也是必須的,所謂獲取salt,其實就是通過golang使用js引擎執行encodeInp(xxx), 這樣我們才能得到經過加密后的username和password,進一步獲取到encoded

import (
	"github.com/robertkrimen/otto"
	"io/ioutil"
)
func EncodeInp(input string)(result string,e error)  {
	jsfile := "js/encodeUriJs.js"
	bytes, err := ioutil.ReadFile(jsfile)
	if err != nil {
		e = err
	}
	vm := otto.New()
	_, err = vm.Run(string(bytes))
	if err != nil {
		e = err
	}
	enc,err :=vm.Call("encodeInp",nil,input)
	if err != nil {
		e = err
	}
	result = enc.String()
	return
}

js部分的代碼就不往外貼了,可以去下面的github地址中獲取

1.4 模擬表單提交,完成登陸

使用golang模擬登陸請求

※推薦台中搬家公司優質服務,可到府估價

台中搬鋼琴,台中金庫搬運,中部廢棄物處理,南投縣搬家公司,好幫手搬家,西屯區搬家

// 模擬登陸
func login(salt, cookie string) (html string) {
	
	req, err := http.NewRequest("POST", LoginUrl, strings.NewReader("encoded="+salt))
  
	// 添加請求頭
	req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36")
	req.Header.Add("Cookie", "JSESSIONID="+cookie)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9")

	//發送請求
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("無密版qlu教務系統登陸請求失敗 : %v", err)
		return
	}
	// todo 根據狀態碼判斷下一步如何操作,如果狀態碼是302,表示操作成功
	fmt.Println("resp.Status:  ", resp.Status)

	b, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("error : %v", err)
		return
	}
	// 返回個人主頁的html
	html = string(b)
	// 手動關閉
	resp.Body.Close()
	return
}

這一步中值得注意的地方:

第一:我們發送的請求的類型是POST請求

第二:我們應該如何處理form表單中的數據后,再發送給後端,後端才能正常處理呢?

具體處理成什麼樣,是需要根據請求頭中的Content-Type決定的。

不知道大家知不知道常見的Content-Type的幾種類型:在form 表單中有一個屬性叫做 entype可以間接將數據處理成Content-Type指定數據格式, 比如我們可以像這樣設置:

  • enctype = text/plain 那麼form表單最終提交的格式就是: 用純文本的形式發送。

  • enctype = application/x-www-form-urlencoded

    • 表單中的enctype值如果不設置,則默認是application/x-www-form-urlencoded,它會將表單中的數據變為鍵值對的形式。
    • 如果action為get,則將表單數據編碼為(name1=value1&name2=value2…),然後把這個字符串加到url後面,中間用?分隔。
    • 如果action為post,瀏覽器把form數據封裝到http body中,然後發送到服務器。
  • enctype = mutipart/form-data

    • 上傳的是非文本內容,比如是個圖片,文件,mp3。

根據這個知識點,結合我們當前的情況,method=post,Content-Type = application/x-www-form-urlencoded

所以,在選擇golang的api時,我們選擇下圖這個api使用

1.5 進入成績查詢頁,解析用戶成績

如果不出意外,經過上面的處理,我們已經完成登陸,並且獲取到後台頁面的html源碼了。

再之後我們就直奔成績查詢模塊,還是使用如何的分析思路

func getAllScore(stuIdentify, cookie string) ([]mtStruct.Score, error) {
	// 發送查詢成績的請求
	u := "http://jwxt.qlu.edu.cn/jsxsd/kscj/cjcx_list"
	req, err := http.NewRequest("POST", u, strings.NewReader("kksj=&kcxz=&kcmc=&xsfs=all"))
	if err != nil {
		fmt.Printf("error : %v", err)
		return nil, err
	}
	req.Header.Add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36")
	req.Header.Add("Cookie", "JSESSIONID="+cookie)
	req.Header.Add("Referer", "http://jwxt.qlu.edu.cn/jsxsd/kscj/cjcx_query?Ves632DSdyV=NEW_XSD_XJCJ")
	req.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*;q=0.8,application/signed-exchange;v=b3;q=0.9")

	client := &http.Client{}
	resp, err := client.Do(req)
  ...
  
}

代碼詳情可以去github上查看。

二、植入微信公共號後台

上面的功能實現后再結合Golang開發微信公眾號就能實現一款好玩的應用。

讓用戶通過微信公共號平台和後端進行數據的交互,我們獲取到用戶的信息,拿着用戶的信息幫用戶監聽教務系統的成績單的狀態。一旦有成績第一時間推送給用戶。

點擊查看公眾號端設計思路

項目GitHub地址:https://github.com/zhuchangwu/golang-wechat-backend

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」