轉自 https://segmentfault.com/a/1190000020413887
前言
TF2.0 是之前學習的內容,當時是寫在了私有的YNote中,重寫於SF。
TF2.0-GPU 安裝教程傳送門:https://segmentfault.com/a/11...
之前接觸過 TF1, 手動session機制,看着很是頭疼。 TF2.0不需要做這些
TF2.0 理解起來更容易(逐漸 Pythonic and Numpic)
TF2.0 后端采用keras接口 (構建網絡層),更方便。
TF2.0 的keras接口定義的模型層,都實現了 call方法。意味大多數實例對象可以當作函數來直接調用
行列軸
以列表為例(抽象舉例,摞起來的面包片。。。。)
[ # 最外層,無意義不用記
[1,2,3], # 面包片1 (第一個樣本) [4,5,6], # 面包片2 (第二個樣本) ]
- 每個 次內層列表 代表一個樣本, 比如 [1,2,3] 整體代表 第一個樣本
- 最內層元素代表屬性值。 eg: 1,2,3 單個拿出來都是屬性值。
- 例子: 元素5 單獨拿出來,它就被看做 "第二個樣本的,屬性值5" (當然橫縱索引依然都是從0取的)
以剛才的數據為例:
t = tf.constant(
[
[1., 2., 3.], [4., 5., 6.] ] ) print(tf.reduce_sum(t, axis=0)) # 求和操作,上下壓扁, 聚合樣本 >> tf.Tensor([5. 7. 9.], shape=(3,), dtype=float32) print(tf.reduce_sum(t, axis=1)) # 求和操作,左右壓扁, 聚合屬性 >> tf.Tensor([ 6. 15.], shape=(2,), dtype=float32)
注:Numpy軸也是這樣的,我最初用x,y軸方式 抽象 去記憶, 基本上是記不住的。。太多概念混淆。
但如果你記不住,你每次使用各種操作和聚合API時,都會自己在心理重新花大量時間理一遍。浪費時間。
所以你一定要練習理解,要做到:“瞄一眼,就能知道這種維度的數據的意義,以及軸操作的意義”
我自己的記憶方式(axis=0, axis=1):
- 0軸通常代表,樣本(上下壓扁)
- 1軸通常代表,屬性(左右壓扁)
常需要用 axis參數 的相關聚合函數:
tf.reduce_sum() # 求和 tf.reduce_mean() # 平均值 tf.reduce_max() # 最大值 tf.reduce_min() # 最小值 tf.square() # 平方 tf.concat() # 拼接 注: 如果 axis參數 "不傳", 那么"所有維度"都會被操作。
常用導入
# 基本會用到的 import numpy as np import tensorflow as tf from tensorflow import keras # 可選導入 import os, sys, pickle import scipy import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler # 標准化 from sklearn.model_selection import train_test_split # 訓測分離
張量&操作符
常量(普通的張量)
定義:
c = tf.constant( [[1., 2., 3.], [4., 5., 6.]] ) # 數字后面加個點代表 轉float32 類型 print(c) >> tf.Tensor([[1. 2. 3.] [4. 5. 6.]], shape=(2, 3), dtype=float32)
六則運算(加減乘除,矩陣乘, 矩陣轉置)
先說矩陣乘法(大學都學過的,運算過程不說了):
語法格式: a @ b 條件要求: a的列數 === b的行數 (必須相等) eg: (5行2列 @ 2行10列 = 5行10列) 特例: (第0維度,必須相等) t1 = tf.ones([2, 20, 30]) t2 = tf.ones([2, 30, 50]) print( (t1@t2).shape ) >> (2, 20, 50) # 第0維沒變, 后2維照常按照矩陣乘法運算
矩陣轉置:
tf.transpose(t)
# 不僅可以普通轉置,還可以交換維度
t2 = tf.transpose(t,[1,0]) # 行變列,列變行。 和基本的轉置差不多(逆序索引,軸變逆序) # 或假如以 (2,100,200,3)形狀 為例 t = tf.ones([2, 100, 200, 3]) print(tf.transpose(t, [1, 3, 0, 2]).shape) # 軸交換位置 >> (100, 3, 2, 200) # 原1軸 -> 放在現在0軸 # 原3軸 -> 放在現在1軸 # 原0軸 -> 放在現在2軸 # 原2軸 -> 放在現在3軸
加減乘除都具有"廣播機制" :
形象(廣播機制解釋)解釋:
我嘗試用白話解釋下:
1. 我形狀和你不一樣, 但我和你運算的時候,我會盡力擴張成 你的形狀 來和你運算。
2. 擴張后如果出現空缺, 那么把自己復制一份,填補上 (如果補不全,就說明不能運算)
3. 小形狀 服從 大形狀 (我比你瘦,我動就行。 你不用動。。。)
eg:
t = tf.constant(
[ [1, 2, 3], [4, 5, 6], ] ) t + [1,2,1] 過程分析: [1,2,1] 顯然是 小形狀, 它會自動嘗試變化成大形狀 -> 第一步變形(最外層大框架滿足, 里面還有空缺): [ [1,2,1], ] 第二步變形 (把自己復制,然后填補空缺): [ [1,2,1], [1,2,1], # 這就是復制的自己 ] 第三步運算(逐位相加) [ + [ = [ [1,2,3], [1,2,1], [2,4,4], [4,5,6], [1,2,1], [5,7,7], ] ] [
抽象(廣播機制)演示:
假如 t1 的 shape為 [5,200,100,50] 假如 t2 的 shape為 [5,200] 注意:我以下的數據演示,全是表示 Tensor的形狀,形狀,形狀! [5,200,1,50] # 很明顯,開始這2行數據 維度沒匹配, 形狀也沒對齊 [5,1] ------------------------ [5,200,1,50] [5,50] # 這行對齊補50 ------------------------ [5,200,5,50] # 這行對齊補5 [5,50] ------------------------ [5,200,5,50] [1, 1, 5,50] # 這行擴張了2個, 默認填1 ------------------------ [5,200,5,50] [1,200, 5,50] # 這行對齊補200 ------------------------ [5,200,5,50] [5,200,5,50] # 這行對齊補5 注意: 1. 每個維度形狀:二者必須有一個是1, 才能對齊。 (不然ERROR,下例ERROR->) [5,200,1,50] [5,20] # 同理開始向右對齊,但是 50與20都不是1,所以都不能對齊,所以ERROR 2. 若維度缺失: 依然是全部貼右對齊 然后先從右面開始,補每個維度的形狀 然后擴展維度,並默認設形狀為1 然后補擴展后維度的形狀(因為默認設為1了,所以是一定可以補齊的)
當然上面說的都是運算時的自動廣播機制
你也可以手動廣播:
t1 = tf.ones([2, 20, 1]) # 原始形狀 【2,20,1】 print(tf.broadcast_to(t1, [5,2,20,30]).shape) # 目標形狀【5,2,20,30】 [5,2,20,30] [2,20, 1] ----------- [5,2,20,30] [2,20,30] ----------- [5,2,20,30] [1,2,20,30] ----------- [5,2,20,30] [5,2,20,30] 注:因為是手動廣播,所以只能 原始形狀 自己向 目標形狀 ”補充維度,或者補充形狀“ 而目標形狀是一點也不能動的。
擴充維度(f.expand_dims)+ 復制(tile) 代替 => 廣播(tf.broadcasting)
同樣是上面的例子,我想把形狀 [2,20,1] ,變成 [5,2,20,30]
t1 = tf.ones([2, 20, 1]) a = tf.expand_dims(t1,axis=0) # 0軸索引處插入一個軸, 結果[1,2,20,1] print(tf.tile(a,[5,1,1,30]).shape) # 結果 [5, 2, 20, 30] 流程: [5,2,20,30] [2,20,1] ----------- [5,2,20,30] # tf.expand_dims(t1,axis=0) [1,2,20,1] # 0號索引插入一個新軸(增維) ----------- [5,2,20,30] # tf.tile(5,1,1,30) (形狀對齊,tile每個參數代表對應軸的形狀擴充幾倍) [5,2,30,30] 1*5 2*1 20*1 1*30
tile 與 broadcasting的區別:
- tile是物理復制,物理空間增加
- 而broadcasting是虛擬復制,(為了計算,隱式實現的復制,並沒有物理空間增加)
- tile可以對任意(整數倍復制n*m, mn同為整數)
- 而broadcasting(原始數據形狀只能存在1的情況下才能擴張。 1*n , n為整數)
壓縮維度(tf.squeeze):
就是把每個維度為1的維度都刪除掉 (就像數學 a * 1 = a)
print(tf.squeeze(tf.ones([2,1,3,1])).shape) >>> (2, 3)
當然你也可以指定維度壓縮(默認不指定,所有維度為1的全部壓縮):
print(tf.squeeze(tf.ones([2,1,3,1]), axis=-1).shape) >>> (2, 1, 3)
索引&切片
靈魂說明:無論索引還是切片, (行列 是使用 逗號 分隔的), 並且無論行列,索引都是從0開始的。
索引:取一個值
print(t[1,2]) # 逗號前面代表行的索引, 逗號后面是列的索引 >> tf.Tensor(6.0, shape=(), dtype=float32)
切片:取子結構 (有兩種方式)
方式1(冒號切片):
print(t[:, 1:]) # 逗號前面是行。只寫: 代表取所有行。逗號后面是列。 1: 代表第二列到最后 >> tf.Tensor([[2. 3.] [5. 6.]], shape=(2, 2), dtype=float32)
方式2(省略號切片): (我相信不了解Numpy的人都沒聽說過 python的 Ellipsis , 就是省略號類)
先自己去運行玩玩這行代碼:
print(... is Ellipsis) >>> True
回到正題:(省略號 ... 切片,是針對多維度的, 如果是二維直接用:即可)
(我們以三維為例,這個就不適合稱作行列了)
# shape 是 (2, 2, 2)
t = tf.constant(
[ # 一維 [ # 二維 [1, 2], # 三維 [3, 4], ], [ [5, 6], [7, 8], ], ] ) 偽碼:t[1維切片, 二維切片, 三維切片] 代碼:t[:, :, 0:1] # 1維不動, 2維不動, 3維 取一條數據 結果: shape為 (2,2,1) [ # 一維 [ # 二維 [1], # 三維 [3], ], [ [5], [7], ], ]
看不明白就多看幾遍。
發現沒,即使我不對 1維,和 2維切片,我也被迫要寫 2個: 來占位
那假如有100個維度,我只想對最后一個維度切片。 前99個都不用動, 那難道我要寫 99個 : 占位??
不,如下代碼即可解決:
print(t[..., 0:1]) # 這就是 ... 的作用 (注意,只在 numpy 和 tensorflow中有用)
tensor 轉 numpy 類型
t.numpy() # tensor 轉為 numpy 類型
變量
定義:
v = tf.Variable( # 注意: V是大寫
[
[1, 2, 3], [4, 5, 6] ] )
變量賦值(具有自身賦值的性質):
注意: 變量一旦被定義,形狀就定下來了。 賦值(只能賦給同形狀的值)
v.assign(
[
[1,1,1], [1,1,1], ] ) print(v) >> <tf.Variable 'Variable:0' shape=(2, 3) dtype=int32, numpy=array([[1, 1, 1],[1, 1, 1]])>
變量取值(相當於轉換為Tensor):
特別: 變量本身就是 Variable類型, 取值取出得是 Tensor (包括切片取值,索引取值等)
print( v.value() )
>> tf.Tensor([[1 2 3] [4 5 6]], shape=(2, 3), dtype=int32)
變量 索引&切片 賦值:
常量:是不可變的。所以只有取值,沒有賦值。
變量:取值、賦值都可以
v.assign(xx) 類似於 python的 v=xx v[0, 1].assign(100) # 索引賦值, v.assign 等價於 v[0, :].assign([10, 20, 30]) # 注意,切片賦值傳遞的需要是容器類型 特別注意: 前面說過,變量 結構形狀 是 不可變的,賦值的賦給的是數據。 但是你賦值的時候要時刻注意,不能改變變量原有形狀 拿切片賦值為例: 你切多少個,你就得賦多少個。 並且賦的值結構要一致。 舉個栗子: 你從正方體里面挖出來一個小正方體。那么你必須填補一塊一模一樣形狀的小正方體) 還有兩種擴展API: v.assign_add() # 類似python 的 += v.assign_sub() # 類似python 的 -=
變量 索引&切片 取值
同 常量切片取值(略)
Variable 轉 Numpy
print(v.numpy())
不規則張量(RaggedTensor)
定義:
rag_tensor = tf.ragged.constant(
[
[1,2], [2,3,4,5], ] ) # 允許每個維度的數據長度參差不齊
拼接:假如需要"拼接 不規則張量" (可使用 tf.concat(axis=) )
0軸:豎着拼接(樣本豎着摞起來)可隨意拼接。 拼接后的依然是"不規則張量" 1軸:橫着拼接(屬性水平拼起來)這時候需要你樣本個數必須相等, 否則對不上,報錯 總結: 樣本豎着隨便拼, 屬性橫着(必須樣本個數相等) 才能拼
RaggedTensor 普通 Tensor:
說明:普通Tensor是必須要求, 長度對齊的。入 對不齊的 末尾補0 tensor = rag_tensor.to_tensor()
稀疏張量 (Sparse Tensor)
特點(可理解為 記錄索引):
- 只記錄非0的坐標位置, indices參數:每個 子列表 表示 一個坐標
- 雖然只記錄坐標,但是轉為普通Tensor后,只有坐標位置 有值, 其他位置的值全是0
- 填充范圍,取決於 dense_shape的設定
定義:
s = tf.SparseTensor(
indices=[[0, 1], [1, 0], [2, 3]], # 注意,這個索引設置需要是(從左到右,從上到下)的順序設置 values=[1, 2, 3], # 將上面3個坐標值分別設值為 1,2,3 dense_shape=[3, 4] # Tensor總范圍 ) print(s) >> SparseTensor(indices=tf.Tensor([[0 1], [1 0],[2 3]], shape=(3, 2), dtype=int64)。。。
轉為普通 Tensor (轉為普通Tensor后,看見的才是存儲真正的值)
tensor = tf.sparse.to_dense(s)
print(tensor)
>> tf.Tensor([ [0 1 0 0],[2 0 0 0],[0 0 0 3] ], shape=(3, 4), dtype=int32)
如果上面使用 to_dense() 可能會遇到錯誤:
error: is out of range 這個錯誤的原因是創建 tf.SparseTensor(indices=) ,前面也說了indices,要按(從左到右,從上到下)順序寫 當然你也可以用排序API,先排序,然后再轉: eg: _ = tf.sparse.reorder(s) # 先將索引排序 tensor = tf.sparse.to_dense(_) # 再轉
tf.function
這個API作為一個裝飾器使用, 用來將 Python 語法轉換 盡可能有效的轉換為 TF語法、圖結構
import tensorflow as tf import numpy as np @tf.function def f(): a = np.array([1, 2, 3]) b = np.array([4, 5, 6]) return a + b print( f() ) >>> tf.Tensor([5 7 9], shape=(3,), dtype=int32)
你應該發現了一個特點,我們定義的 f()函數內部,一個tf語法都沒寫。 只裝飾了一行 @tf.function
而調用結果返回值居然是個 tensor 。
這就是 @tf.function 裝飾器的作用了!
當然函數里面,也可以寫 tf的操作,也是沒問題的。
但注意一點, 函數里面不允許定義 變量, 需要定義的變量 應 拿到函數外面定義
a = tf.Variable([1,2,3]) # 如需tensor變量,應該放在外面 @tf.function def f(): # a = tf.Variable([1,2,3]) # 這里面不允許定義變量! pass
合並相加(tf.concat)
我的理解就是(基本數學的 合並同類項)
# 合並同類項的原則就是有1項不同,其他項完全相同。 # 前提條件:(最多,有一個維度的形狀不相等。 注意是最多) t1 = tf.ones([2,5,6]) t2 = tf.ones([6,5,6]) print( tf.concat([t1,t2],axis=0).shape ) # axis=0,傳了0軸,那么其他軸捏死不變。只合並0軸 >> (8,5,8)
堆疊降維(tf.stack)
我的理解就是(小學算術的,進位,(進位就是擴充一個維度表示個數))
# 前提條件:所有維度形狀必須全部相等。
tf1 = tf.ones([2,3,4]) tf2 = tf.ones([2,3,4]) tf3 = tf.ones([2,3,4]) print(tf.stack([tf1,tf2,tf3], axis=0).shape) # 你可以想象有3組 [2,3,4],然后3組作為一個新維度,插入到 axis對應的索引處。 >> (3, 2, 3, 4) # 對比理解,如果這是tf.concat(), 那么結果就是 (6,3,4)
拆分降維(tf.unstack)
和tf.stack正好是互逆過程,指定axis維度是幾,它就會拆分成幾個數據,同時降維。
a = tf.ones([3, 2, 3, 4]) for x in tf.unstack(a, axis=0): print(x.shape) 結果如下(分成了3個 [2,3,4]) >>> (2, 3, 4) >>> (2, 3, 4) >>> (2, 3, 4)
拆分不降維(tf.split)
語法:
和tf.unstack的區別就是,tf.unstack是均分降維, tf.stack是怎么分都不會降維,且能指定分隔份數
a = tf.ones([2,4,35,8]) for x in tf.split(a, axis=3,num_or_size_splits=[2,2,4]): print(x.shape) 結果: >> (2, 4, 35, 2) # 最后一維2 >> (2, 4, 35, 2) # 最后一維2 >> (2, 4, 35, 4) # 最后一維4
使用場景:
假如我們想切分數據集為(train-test-valid) 3分比例為 6:2:2
方法1:(scikit-learn 連續分切2次)
x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.2) x_train, x_valid, y_train, y_valid = train_test_split(x_train, y_train,test_size=0.2) # 源碼中顯示 test_size如果不傳。默認為0.25。 # 思路,因為 scikit-learn 只能切出2個結果: 所以我們需要切2次: # 第一次 從完整訓練集 切出來 (剩余訓練集, 測試集) # 第二次 從剩余數據集 切出來 (剩余訓練集2, 驗證集)
方法2: (tf.split)
x = tf.ones([1000, 5000]) y = tf.ones([1000, 1]) x_train, x_test, x_valid = tf.split( x, num_or_size_splits=[600,200,200], # 切3份 axis=0 ) y_train, y_test, y_valid = tf.split( y, num_or_size_splits=[600,200,200], # 同樣切3份 axis=0 ) print(x_train.shape, y_train.shape) print(x_test.shape, y_test.shape) print(x_valid.shape, y_valid.shape) 結果 >>> (600, 5000) (600, 1) >>> (200, 5000) (200, 1) >>> (200, 5000) (200, 1)
高級索引(tf.gather)
numpy這種索引叫做 fancy indexing(如果我沒記錯的話)
data = tf.constant([6,7,8]) # 當作真實數據 index = tf.constant([2, 1, 0]) # 當作索引 print(tf.gather(data, index)) >> tf.Tensor([8 7 6], shape=(3,), dtype=int32)
排序(tf.sort)
data = tf.constant([6, 7, 8]) print(tf.sort(data, direction='DESCENDING')) # 'ASCENDING' # 默認是ASCENDING升序 tf.argsort() # 同上, 只不過返回的是排序后的,對應數據的index
Top-K(tf.math.top_k)
查找出最大的n個(比先排序然后切片的性能要好)
a = tf.math.top_k([6,7,8],2) # 找出最大的兩個,返回是個對象 print(a.indices) # 取出最大的兩個 索引 () print(a.values) # 取出最大的兩個 值 >> tf.Tensor([2 1], shape=(2,), dtype=int32) >> tf.Tensor([8 7], shape=(2,), dtype=int32)
tf.GradientTape (自定義求導)
求偏導
v1, v2 = tf.Variable(1.), tf.Variable(2.) # 變量 會 被自動偵測更新的 c1, c2 = tf.constant(1.), tf.constant(2.) # 常量 不會 自動偵測更新 y = lambda x1,x2: x1**2 + x2**2 with tf.GradientTape(persistent=True) as tape: """默認這個 tape使用一次就會被刪除, persistent=True 代表永久存在,但后續需要手動釋放""" # 因為常量不會被自動偵測,所以我們需要手動調用 watch() 偵測 tape.watch(c1) # 如果是變量,就不用watch這兩步了 tape.watch(c2) f = y(c1,c2) # 調用函數,返回結果 c1_, c2_ = tape.gradient(f, [c1,c2]) # 參數2:傳遞幾個自變量,就會返回幾個 偏導結果 # c1_ 為 c1的偏導 # c2_ 為 c2的偏導 del tape # 手動釋放 tape
求二階偏導(gradient嵌套)
v1, v2 = tf.Variable(1.), tf.Variable(2.) # 我們使用變量 y = lambda x1,x2: x1**2 + x2**2 with tf.GradientTape(persistent=True) as tape2: with tf.GradientTape(persistent=True) as tape1: f = y(v1,v2) once_grads = tape1.gradient(f, [v1, v2]) # 一階偏導 # 此列表推導式表示:拿着一階偏導,來繼續求二階偏導(注意,用tape2) twice_grads = [tape2.gradient(once_grad, [v1,v2]) for once_grad in once_grads] # 二階偏導 print(twice_grads) del tape1 # 釋放 del tape2 # 釋放
說明
求導數(一個自變量):tape1.gradient(f, v1) # gradient傳 1個自變量 求偏導(多個自變量):tape1.gradient(f, [v1,v2]) # gradient傳 1個列表, 列表內填所有自變量
SGD(隨機梯度下降)
方式1:手撕(不使用優化器)
v1, v2 = tf.Variable(1.), tf.Variable(2.) # 我們使用變量 y = lambda x1, x2: x1 ** 2 + x2 ** 2 # 二元二次方程 learning_rate = 0.1 # 學習率 for _ in range(30): # 迭代次數 with tf.GradientTape() as tape: # 求導作用域 f = y(v1,v2) d1, d2 = tape.gradient(f, [v1,v2]) # 求導, d1為 v1的偏導, d2為v2的偏導 v1.assign_sub(learning_rate * d1) v2.assign_sub(learning_rate * d2) print(v1) print(v2) 實現流程總結: 1. 偏導 自變量v1,v2求出來的。 (d1, d2 = tape.gradient(f, [v1,v2])) 2. 自變量v1,v2的衰減 是關聯 偏導的( 衰減值 = 學習率*偏導) 3. 我們把前2步套了一個大循環(並設定迭代次數), 1-2-1-2-1-2-1-2-1-2 步驟往復執行
方式2:借用 Tensorflow 優化器(optimizer) 實現梯度下降
v1, v2 = tf.Variable(1.), tf.Variable(2.) # 我們使用變量 y = lambda x1, x2: x1 ** 2 + x2 ** 2 # 二元二次函數 , 通常這個函數我們用作計算loss learning_rate = 0.1 # 學習率 optimizer = keras.optimizers.SGD(learning_rate=learning_rate) # 初始化優化器 for _ in range(30): # 迭代次數 with tf.GradientTape() as tape: f = y(v1,v2) d1, d2 = tape.gradient(f, [v1,v2]) # d1為 v1的偏導, d2為v2的偏導 optimizer.apply_gradients( # 注意這里不一樣了,我們之前手動衰減 [ # 而現在這些事情, optimizer.SGD幫我們做了 (d1, v1), # 我們只需把偏導值,和自變量按這種格式傳給它即可 (d2, v2), ] ) # 通常這種格式,我們用 zip() 實現 # eg: # model = keras.models.Sequential([......]) # ....... # grads = tape.gradient(f, [v1,v2]) # optimizer.apply_gradients( # zip(grads, model.trainable_variables) # ) print(v1) print(v2) 實現流程總結: 1. 偏導 是自變量v1,v2求出來的 (d1, d2 = tape.gradient(f, [v1,v2])) # 此步驟不變 2. 把偏導 和 自變量 傳給optimizer.apply_gradients() optimizer.SGD() 自動幫我們衰減。 3. 我們還是把前2步套了一個大循環(並設定迭代次數), 1-2-1-2-1-2-1-2-1-2 步驟往復執行。 注: 假如你用adam等之類的其他優化器,那么可能有更復雜的公式,如果我們手撕,肯能有些費勁。 這時候我們最好使用 optimizer.Adam ...等各種 成品,優化器。通用步驟如下 1. 先實例化出一個優化器對象 2. 實例化對象.apply_gradients([(偏導,自變量)])