第10章 使用Keras搭建人工神經(jīng)網(wǎng)絡(luò)
第11章 訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)
第12章 使用TensorFlow自定義模型并訓(xùn)練
第13章 使用TensorFlow加載和預(yù)處理數(shù)據(jù)
第14章 使用卷積神經(jīng)網(wǎng)絡(luò)實現(xiàn)深度計算機視覺
第15章 使用RNN和CNN處理序列
第16章 使用RNN和注意力機制進行自然語言處理
第17章 使用自編碼器和GAN做表征學(xué)習(xí)和生成式學(xué)習(xí)
第18章 強化學(xué)習(xí)
第19章 規(guī)模化訓(xùn)練和部署TensorFlow模型
目前為止,我們只是使用了存放在內(nèi)存中的數(shù)據(jù)集碎浇,但深度學(xué)習(xí)系統(tǒng)經(jīng)常需要在大數(shù)據(jù)集上訓(xùn)練,而內(nèi)存放不下大數(shù)據(jù)集璃俗。其它的深度學(xué)習(xí)庫通過對大數(shù)據(jù)集做預(yù)處理奴璃,繞過了內(nèi)存限制,但TensorFlow通過Data API城豁,使一切都容易了:只需要創(chuàng)建一個數(shù)據(jù)集對象苟穆,告訴它去哪里拿數(shù)據(jù),以及如何做轉(zhuǎn)換就行钮蛛。TensorFlow負責(zé)所有的實現(xiàn)細節(jié)鞭缭,比如多線程剖膳、隊列魏颓、批次和預(yù)提取。另外吱晒,Data API和tf.keras可以無縫配合甸饱!
Data API還可以從現(xiàn)成的文件(比如CSV文件)、固定大小的二進制文件、使用TensorFlow的TFRecord格式的文件(支持大小可變的記錄)讀取數(shù)據(jù)叹话。TFRecord是一個靈活高效的二進制格式偷遗,基于Protocol Buffers(一個開源二進制格式)。Data API還支持從SQL數(shù)據(jù)庫讀取數(shù)據(jù)驼壶。另外氏豌,許多開源插件也可以用來從各種數(shù)據(jù)源讀取數(shù)據(jù),包括谷歌的BigQuery热凹。
高效讀取大數(shù)據(jù)集不是唯一的難點:數(shù)據(jù)還需要進行預(yù)處理泵喘,通常是歸一化。另外般妙,數(shù)據(jù)集中并不是只有數(shù)值字段:可能還有文本特征纪铺、類型特征,等等碟渺。這些特征需要編碼鲜锚,比如使用獨熱編碼或嵌入(后面會看到,嵌入嵌入是用來標識類型或token的緊密矢量)苫拍。預(yù)處理的一種方式是寫自己的自定義預(yù)處理層芜繁,另一種是使用Kera的標準預(yù)處理層。
本章中绒极,我們會介紹Data API浆洗,TFRecord格式,以及如何創(chuàng)建自定義預(yù)處理層集峦,和使用Keras的預(yù)處理層伏社。還會快速學(xué)習(xí)TensorFlow生態(tài)的一些項目:
TF Transform (tf.Transform):可以用來編寫單獨的預(yù)處理函數(shù),它可以在真正訓(xùn)練前塔淤,運行在完整訓(xùn)練集的批模式中摘昌,然后輸出到TF Function,插入到訓(xùn)練好的模型中高蜂。只要模型在生產(chǎn)環(huán)境中部署好了聪黎,就能隨時預(yù)處理新的實例。
TF Datasets (TFDS)备恤。提供了下載許多常見數(shù)據(jù)集的函數(shù)稿饰,包括ImageNet,和數(shù)據(jù)集對象(可用Data API操作)露泊。
Data API
整個Data API都是圍繞數(shù)據(jù)集dataset
的概念展開的:可以猜得到喉镰,數(shù)據(jù)集表示一連串?dāng)?shù)據(jù)項。通常你是用的數(shù)據(jù)集是從硬盤里逐次讀取數(shù)據(jù)的惭笑,簡單起見侣姆,我們是用tf.data.Dataset.from_tensor_slices()
創(chuàng)建一個存儲于內(nèi)存中的數(shù)據(jù)集:
>>> X = tf.range(10) # any data tensor
>>> dataset = tf.data.Dataset.from_tensor_slices(X)
>>> dataset
<TensorSliceDataset shapes: (), types: tf.int32>
函數(shù)from_tensor_slices()
取出一個張量生真,創(chuàng)建了一個tf.data.Dataset
,它的元素是X
的全部切片捺宗,因此這個數(shù)據(jù)集包括10項:張量 0柱蟀、1、2蚜厉、长已。。昼牛。痰哨、9。在這個例子中匾嘱,使用tf.data.Dataset.range(10)
也能達到同樣的效果斤斧。
可以像下面這樣對這個數(shù)據(jù)集迭代:
>>> for item in dataset:
... print(item)
...
tf.Tensor(0, shape=(), dtype=int32)
tf.Tensor(1, shape=(), dtype=int32)
tf.Tensor(2, shape=(), dtype=int32)
[...]
tf.Tensor(9, shape=(), dtype=int32)
鏈式轉(zhuǎn)換
有了數(shù)據(jù)集之后,通過調(diào)用轉(zhuǎn)換方法霎烙,可以對數(shù)據(jù)集做各種轉(zhuǎn)換撬讽。每個方法會返回一個新的數(shù)據(jù)集,因此可以將轉(zhuǎn)換像下面這樣鏈接起來(見圖13-1):
>>> dataset = dataset.repeat(3).batch(7)
>>> for item in dataset:
... print(item)
...
tf.Tensor([0 1 2 3 4 5 6], shape=(7,), dtype=int32)
tf.Tensor([7 8 9 0 1 2 3], shape=(7,), dtype=int32)
tf.Tensor([4 5 6 7 8 9 0], shape=(7,), dtype=int32)
tf.Tensor([1 2 3 4 5 6 7], shape=(7,), dtype=int32)
tf.Tensor([8 9], shape=(2,), dtype=int32)
在這個例子中悬垃,我們先在原始數(shù)據(jù)集上調(diào)用了repeat()
方法游昼,返回了一個重復(fù)了原始數(shù)據(jù)集3次的新數(shù)據(jù)集。當(dāng)然尝蠕,這步不會復(fù)制數(shù)據(jù)集中的數(shù)據(jù)三次(如果調(diào)用這個方法時沒有加參數(shù)烘豌,新數(shù)據(jù)集會一直重復(fù)源數(shù)據(jù)集,必須讓迭代代碼決定何時退出)看彼。然后我們在新數(shù)據(jù)集上調(diào)用了batch()
方法廊佩,這步又產(chǎn)生了一個新數(shù)據(jù)集。這一步會將上一個數(shù)據(jù)集的分成7個一批次靖榕。最后标锄,做一下迭代∽录疲可以看到料皇,最后的批次只有兩個元素,可以設(shè)置drop_remainder=True
星压,丟棄最后的兩項践剂,將數(shù)據(jù)對齊。
警告:數(shù)據(jù)集方法不修改數(shù)據(jù)集娜膘,只是生成新的數(shù)據(jù)集而已逊脯,所以要做新數(shù)據(jù)集的賦值(即使用
dataset = ...
)。
還可以通過map()
方法轉(zhuǎn)換元素劲绪。比如男窟,下面的代碼創(chuàng)建了一個每個元素都翻倍的新數(shù)據(jù)集:
>>> dataset = dataset.map(lambda x: x * 2) # Items: [0,2,4,6,8,10,12]
這個函數(shù)可以用來對數(shù)據(jù)做預(yù)處理。有時可能會涉及復(fù)雜的計算贾富,比如改變形狀或旋轉(zhuǎn)圖片歉眷,所以通常需要多線程來加速:只需設(shè)置參數(shù)num_parallel_calls
就行。注意颤枪,傳遞給map()
方法的函數(shù)必須是可以轉(zhuǎn)換為TF Function汗捡。
map()
方法是對每個元素做轉(zhuǎn)換的,apply()
方法是對數(shù)據(jù)整體做轉(zhuǎn)換的畏纲。例如扇住,下面的代碼對數(shù)據(jù)集應(yīng)用了unbatch()
函數(shù)(這個函數(shù)目前是試驗性的,但很有可能加入到以后的版本中)盗胀。新數(shù)據(jù)集中的每個元素都是一個單整數(shù)張量艘蹋,而不是批次大小為7的整數(shù)。
>>> dataset = dataset.apply(tf.data.experimental.unbatch()) # Items: 0,2,4,...
還可以用filter()
方法做過濾:
>>> dataset = dataset.filter(lambda x: x < 10) # Items: 0 2 4 6 8 0 2 4 6...
take()
方法可以用來查看數(shù)據(jù):
>>> for item in dataset.take(3):
... print(item)
...
tf.Tensor(0, shape=(), dtype=int64)
tf.Tensor(2, shape=(), dtype=int64)
tf.Tensor(4, shape=(), dtype=int64)
打散數(shù)據(jù)
當(dāng)訓(xùn)練集中的實例是獨立同分布時票灰,梯度下降的效果最好(見第4章)女阀。實現(xiàn)獨立同分布的一個簡單方法是使用shuffle()
方法。它能創(chuàng)建一個新數(shù)據(jù)集屑迂,新數(shù)據(jù)集的前面是一個緩存浸策,緩存中是源數(shù)據(jù)集的開頭元素。然后惹盼,無論什么時候取元素庸汗,就會從緩存中隨便隨機取出一個元素,從源數(shù)據(jù)集中取一個新元素替換手报。從緩沖器取元素蚯舱,直到緩存為空。必須要指定緩存的大小掩蛤,最好大一點晓淀,否則隨機效果不明顯。不要查出內(nèi)存大小盏档,即使內(nèi)存夠用凶掰,緩存超過數(shù)據(jù)集也是沒有意義的◎谀叮可以提供一個隨機種子懦窘,如果希望隨機的順序是固定的。例如稚配,下面的代碼創(chuàng)建并顯示了一個包括0到9的數(shù)據(jù)集畅涂,重復(fù)3次,用大小為5的緩存做隨機道川,隨機種子是42午衰,批次大小是7:
>>> dataset = tf.data.Dataset.range(10).repeat(3) # 0 to 9, three times
>>> dataset = dataset.shuffle(buffer_size=5, seed=42).batch(7)
>>> for item in dataset:
... print(item)
...
tf.Tensor([0 2 3 6 7 9 4], shape=(7,), dtype=int64)
tf.Tensor([5 0 1 1 8 6 5], shape=(7,), dtype=int64)
tf.Tensor([4 8 7 1 2 3 0], shape=(7,), dtype=int64)
tf.Tensor([5 4 2 7 8 9 9], shape=(7,), dtype=int64)
tf.Tensor([3 6], shape=(2,), dtype=int64)
提示:如果在隨機數(shù)據(jù)集上調(diào)用
repeat()
方法立宜,默認下,每次迭代的順序都是新的臊岸。通常這樣沒有問題橙数,但如果你想讓每次迭代的順序一樣(比如,測試或調(diào)試)帅戒,可以設(shè)置reshuffle_each_iteration=False
灯帮。
對于內(nèi)存放不下的大數(shù)據(jù)集,這個簡單的隨機緩存方法就不成了逻住,因為緩存相比于數(shù)據(jù)集就小太多了钟哥。一個解決方法是將源數(shù)據(jù)本身打亂(例如,Linux可以用shuf
命令打散文本文件)瞎访。這樣肯定能提高打散的效果腻贰!即使源數(shù)據(jù)打散了,你可能還想再打散一點扒秸,否則每個周期可能還會出現(xiàn)同樣的順序银受,模型最后可能是偏的(比如,源數(shù)據(jù)順序偶然導(dǎo)致的假模式)鸦采。為了將實例進一步打散宾巍,一個常用的方法是將源數(shù)據(jù)分成多個文件,訓(xùn)練時隨機順序讀取渔伯。但是顶霞,相同文件中的實例仍然靠的太近姐直。為了避免這點明也,可以同時隨機讀取多個文件,做交叉顿仇。在最頂層玄叠,可以用shuffle()
加一個隨機緩存古徒。如果這聽起來很麻煩,不用擔(dān)心:Data API都為你實現(xiàn)了读恃,幾行代碼就行隧膘。
多行數(shù)據(jù)交叉
首先,假設(shè)加載了加州房價數(shù)據(jù)集寺惫,打散它(除非已經(jīng)打散了)疹吃,分成訓(xùn)練集、驗證集西雀、測試集萨驶。然后將每個數(shù)據(jù)集分成多個csv文件,每個如下所示(每行包含8個輸入特征加上目標中位房價):
MedInc,HouseAge,AveRooms,AveBedrms,Popul,AveOccup,Lat,Long,MedianHouseValue
3.5214,15.0,3.0499,1.1065,1447.0,1.6059,37.63,-122.43,1.442
5.3275,5.0,6.4900,0.9910,3464.0,3.4433,33.69,-117.39,1.687
3.1,29.0,7.5423,1.5915,1328.0,2.2508,38.44,-122.98,1.621
[...]
再假設(shè)train_filepaths
包括了訓(xùn)練文件路徑的列表(還要valid_filepaths
和test_filepaths
):
>>> train_filepaths
['datasets/housing/my_train_00.csv', 'datasets/housing/my_train_01.csv',...]
另外艇肴,可以使用文件模板腔呜,比如train_filepaths = "datasets/housing/my_train_*.csv"
∪拢現(xiàn)在,創(chuàng)建一個數(shù)據(jù)集核畴,包括這些文件路徑:
filepath_dataset = tf.data.Dataset.list_files(train_filepaths, seed=42)
默認膝但,list_files()
函數(shù)返回一個文件路徑打散的數(shù)據(jù)集。也可以設(shè)置shuffle=False
膛檀,文件路徑就不打散了锰镀。
然后娘侍,可以調(diào)用leave()
方法咖刃,一次讀取5個文件,做交叉操作(跳過第一行表頭憾筏,使用skip()
方法):
n_readers = 5
dataset = filepath_dataset.interleave(
lambda filepath: tf.data.TextLineDataset(filepath).skip(1),
cycle_length=n_readers)
interleave()
方法會創(chuàng)建一個數(shù)據(jù)集嚎杨,它從filepath_dataset
讀5條文件路徑,對每條路徑調(diào)用函數(shù)(例子中是用的匿名函數(shù))來創(chuàng)建數(shù)據(jù)集(例子中是TextLineDataset
)氧腰。為了更清楚點枫浙,這一步總歐諾個由七個數(shù)據(jù)集:文件路徑數(shù)據(jù)集,交叉數(shù)據(jù)集古拴,和五個TextLineDatasets
數(shù)據(jù)集箩帚。當(dāng)?shù)徊鏀?shù)據(jù)集時,會循環(huán)TextLineDatasets
黄痪,每次讀取一行紧帕,知道數(shù)據(jù)集為空。然后會從filepath_dataset
再獲取五個文件路徑桅打,做同樣的交叉是嗜,直到文件路徑為空。
提示:為了交叉得更好挺尾,最好讓文件有相同的長度鹅搪,否則長文件的尾部不會交叉。
默認情況下遭铺,interleave()
不是并行的丽柿,只是順序從每個文件讀取一行。如果想變成并行讀取文件魂挂,可以設(shè)定參數(shù)num_parallel_calls
為想要的線程數(shù)(map()
方法也有這個參數(shù))航厚。還可以將其設(shè)置為tf.data.experimental.AUTOTUNE
,讓TensorFlow根據(jù)CPU自己找到合適的線程數(shù)(目前這是個試驗性的功能)锰蓬♂2牵看看目前數(shù)據(jù)集包含什么:
>>> for line in dataset.take(5):
... print(line.numpy())
...
b'4.2083,44.0,5.3232,0.9171,846.0,2.3370,37.47,-122.2,2.782'
b'4.1812,52.0,5.7013,0.9965,692.0,2.4027,33.73,-118.31,3.215'
b'3.6875,44.0,4.5244,0.9930,457.0,3.1958,34.04,-118.15,1.625'
b'3.3456,37.0,4.5140,0.9084,458.0,3.2253,36.67,-121.7,2.526'
b'3.5214,15.0,3.0499,1.1065,1447.0,1.6059,37.63,-122.43,1.442'
忽略表頭行,這是五個csv文件的第一行芹扭,隨機選取的麻顶∩舛叮看起來不錯。但是也看到了辅肾,都是字節(jié)串队萤,需要解析數(shù)據(jù),縮放數(shù)據(jù)矫钓。
預(yù)處理數(shù)據(jù)
實現(xiàn)一個小函數(shù)來做預(yù)處理:
X_mean, X_std = [...] # mean and scale of each feature in the training set
n_inputs = 8
def preprocess(line):
defs = [0.] * n_inputs + [tf.constant([], dtype=tf.float32)]
fields = tf.io.decode_csv(line, record_defaults=defs)
x = tf.stack(fields[:-1])
y = tf.stack(fields[-1:])
return (x - X_mean) / X_std, y
逐行看下代碼:
首先要尔,代碼假定已經(jīng)算好了訓(xùn)練集中每個特征的平均值和標準差。
X_mean
和X_std
是1D張量(或NumPy數(shù)組)新娜,包含八個浮點數(shù)赵辕,每個都是特征。preprocess()
函數(shù)從csv取一行概龄,開始解析还惠。使用tf.io.decode_csv()
函數(shù),接收兩個參數(shù)私杜,第一個是要解析的行蚕键,第二個是一個數(shù)組,包含csv文件每列的默認值衰粹。這個數(shù)組不僅告訴TensorFlow每列的默認值锣光,還有總列數(shù)和數(shù)據(jù)類型。在這個例子中铝耻,是告訴TensorFlow誊爹,所有特征列都是浮點數(shù),缺失值默認為田篇,但提供了一個類型是tf.float32
的空數(shù)組替废,作為最后一列(目標)的默認值:數(shù)組告訴TensorFlow這一列包含浮點數(shù),但沒有默認值泊柬,所以碰到空值時會報異常椎镣。decode_csv()
函數(shù)返回一個標量張量(每列一個)的列表,但應(yīng)該返回1D張量數(shù)組兽赁。所以在所有張量上調(diào)用了tf.stack()
状答,除了最后一個。然后對目標值做同樣的操作(讓其成為只包含一個值刀崖,而不是標量張量的1D張量數(shù)組)惊科。最后,對特征做縮放亮钦,減去平均值馆截,除以標準差,然后返回包含縮放特征和目標值的元組。
測試這個預(yù)處理函數(shù):
>>> preprocess(b'4.2083,44.0,5.3232,0.9171,846.0,2.3370,37.47,-122.2,2.782')
(<tf.Tensor: id=6227, shape=(8,), dtype=float32, numpy=
array([ 0.16579159, 1.216324 , -0.05204564, -0.39215982, -0.5277444 ,
-0.2633488 , 0.8543046 , -1.3072058 ], dtype=float32)>,
<tf.Tensor: [...], numpy=array([2.782], dtype=float32)>)
很好蜡娶,接下來將函數(shù)應(yīng)用到數(shù)據(jù)集上混卵。
整合
為了讓代碼可復(fù)用,將前面所有討論過的東西編程一個小函數(shù):創(chuàng)建并返回一個數(shù)據(jù)集窖张,可以高效從多個csv文件加載加州房價數(shù)據(jù)集幕随,做預(yù)處理、打散宿接、選擇性重復(fù)赘淮,做批次(見圖3-2):
def csv_reader_dataset(filepaths, repeat=1, n_readers=5,
n_read_threads=None, shuffle_buffer_size=10000,
n_parse_threads=5, batch_size=32):
dataset = tf.data.Dataset.list_files(filepaths)
dataset = dataset.interleave(
lambda filepath: tf.data.TextLineDataset(filepath).skip(1),
cycle_length=n_readers, num_parallel_calls=n_read_threads)
dataset = dataset.map(preprocess, num_parallel_calls=n_parse_threads)
dataset = dataset.shuffle(shuffle_buffer_size).repeat(repeat)
return dataset.batch(batch_size).prefetch(1)
代碼條理很清晰,除了最后一行的prefetch(1)
睦霎,對于提升性能很關(guān)鍵梢卸。
預(yù)提取
通過調(diào)用prefetch(1)
,創(chuàng)建了一個高效的數(shù)據(jù)集碎赢,總能提前一個批次低剔。換句話說速梗,當(dāng)訓(xùn)練算法在一個批次上工作時肮塞,數(shù)據(jù)集已經(jīng)準備好下一個批次了(從硬盤讀取數(shù)據(jù)并做預(yù)處理)。這樣可以極大提升性能姻锁,解釋見圖13-3枕赵。如果加載和預(yù)處理還要是多線程的(通過設(shè)置interleave()
和map()
的num_parallel_calls
),可以利用多CPU位隶,準備批次數(shù)據(jù)可以比在GPU上訓(xùn)練還快:這樣GPU就可以100%利用起來了(排除數(shù)據(jù)從CPU傳輸?shù)紾PU的時間)拷窜,訓(xùn)練可以快很多。
提示:如果想買一塊GPU顯卡的話,它的處理能力和顯存都是非常重要的笋妥。另一個同樣重要的懊昨,是顯存帶寬,即每秒可以進入或流出內(nèi)存的GB數(shù)春宣。
如果數(shù)據(jù)集不大酵颁,內(nèi)存放得下,可以使用數(shù)據(jù)集的cache()
方法將數(shù)據(jù)集存入內(nèi)存月帝。通常這步是在加載和預(yù)處理數(shù)據(jù)之后躏惋,在打散、重復(fù)嚷辅、分批次之前簿姨。這樣做的話,每個實例只需做一次讀取和處理簸搞,下一個批次仍能提前準備扁位。
你現(xiàn)在知道如何搭建高效輸入管道深寥,從多個文件加載和預(yù)處理數(shù)據(jù)了。我們討論了最常用的數(shù)據(jù)集方法贤牛,但還有一些你可能感興趣:concatenate()
惋鹅、zip()
、window()
殉簸、reduce()
闰集、shard()
、flat_map()
般卑、和padded_batch()
武鲁。還有兩個類方法:from_generator()
和from_tensors()
,它們能從Python生成器或張量列表創(chuàng)建數(shù)據(jù)集蝠检。更多細節(jié)請查看API文檔沐鼠。tf.data.experimental
中還有試驗性功能,其中許多功能可能會添加到未來版本中叹谁。
tf.keras使用數(shù)據(jù)集
現(xiàn)在可以使用csv_reader_dataset()
函數(shù)為訓(xùn)練集創(chuàng)建數(shù)據(jù)集了饲梭。注意,不需要將數(shù)據(jù)重復(fù)焰檩,tf.keras會做重復(fù)憔涉。還為驗證集和測試集創(chuàng)建了數(shù)據(jù)集:
train_set = csv_reader_dataset(train_filepaths)
valid_set = csv_reader_dataset(valid_filepaths)
test_set = csv_reader_dataset(test_filepaths)
現(xiàn)在就可以利用這些數(shù)據(jù)集來搭建和訓(xùn)練Keras模型了。我們要做的就是將訓(xùn)練和驗證集傳遞給fit()
方法析苫,而不是X_train
兜叨、y_train
、X_valid
衩侥、y_valid
:
model = keras.models.Sequential([...])
model.compile([...])
model.fit(train_set, epochs=10, validation_data=valid_set)
相似的国旷,可以將數(shù)據(jù)集傳遞給evaluate()
和predict()
方法:
model.evaluate(test_set)
new_set = test_set.take(3).map(lambda X, y: X) # pretend we have 3 new instances
model.predict(new_set) # a dataset containing new instances
跟其它集合不同,new_set
通常不包含標簽(如果包含標簽茫死,也會被Keras忽略)跪但。注意,在所有這些情況下璧榄,還可以使用NumPy數(shù)組(但仍需要加載和預(yù)處理)特漩。
如果你想創(chuàng)建自定義訓(xùn)練循環(huán)(就像12章那樣),你可以在訓(xùn)練集上迭代:
for X_batch, y_batch in train_set:
[...] # perform one Gradient Descent step
事實上骨杂,還可以創(chuàng)建一個TF函數(shù)(見第12章)來完成整個訓(xùn)練循環(huán):
@tf.function
def train(model, optimizer, loss_fn, n_epochs, [...]):
train_set = csv_reader_dataset(train_filepaths, repeat=n_epochs, [...])
for X_batch, y_batch in train_set:
with tf.GradientTape() as tape:
y_pred = model(X_batch)
main_loss = tf.reduce_mean(loss_fn(y_batch, y_pred))
loss = tf.add_n([main_loss] + model.losses)
grads = tape.gradient(loss, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
祝賀涂身,你現(xiàn)在知道如何使用Data API創(chuàng)建強大的輸入管道了!但是搓蚪,目前為止我們使用的CSV文件蛤售,雖然常見又簡單方便,但不夠高效,不支持大或復(fù)雜的數(shù)據(jù)結(jié)構(gòu)(比如圖片或音頻)悴能。這就是TFRecord要解決的揣钦。
提示:如果你對csv文件感到滿意(或其它任意格式),就不必使用TFRecord漠酿。就像老話說的冯凹,只要沒壞就別修!TFRecord是為解決訓(xùn)練過程中加載和解析數(shù)據(jù)時碰到的瓶頸炒嘲。
TFRecord格式
TFRecord格式是TensorFlow偏愛的存儲大量數(shù)據(jù)并高效讀取的數(shù)據(jù)宇姚。它是非常簡單的二進制格式,只包含不同大小的二進制記錄的數(shù)據(jù)(每個記錄包括一個長度夫凸、一個CRC校驗和浑劳,校驗和用于檢查長度是否正確,真是的數(shù)據(jù)夭拌,和一個數(shù)據(jù)的CRC校驗和魔熏,用于檢查數(shù)據(jù)是否正確)「氡猓可以使用tf.io.TFRecordWriter
類輕松創(chuàng)建TFRecord文件:
with tf.io.TFRecordWriter("my_data.tfrecord") as f:
f.write(b"This is the first record")
f.write(b"And this is the second record")
然后可以使用tf.data.TFRecordDataset
來讀取一個或多個TFRecord文件:
filepaths = ["my_data.tfrecord"]
dataset = tf.data.TFRecordDataset(filepaths)
for item in dataset:
print(item)
輸出是:
tf.Tensor(b'This is the first record', shape=(), dtype=string)
tf.Tensor(b'And this is the second record', shape=(), dtype=string)
提示:默認情況下蒜绽,
TFRecordDataset
會逐一讀取數(shù)據(jù),但通過設(shè)定num_parallel_reads
可以并行讀取并交叉數(shù)據(jù)献烦。另外滓窍,你可以使用list_files()
和interleave()
獲得同樣的結(jié)果卖词。
壓縮TFRecord文件
有的時候壓縮TFRecord文件很有必要巩那,特別是當(dāng)需要網(wǎng)絡(luò)傳輸?shù)臅r候。你可以通過設(shè)定options
參數(shù)此蜈,創(chuàng)建壓縮的TFRecord文件:
options = tf.io.TFRecordOptions(compression_type="GZIP")
with tf.io.TFRecordWriter("my_compressed.tfrecord", options) as f:
[...]
當(dāng)讀取壓縮TFRecord文件時即横,需要指定壓縮類型:
dataset = tf.data.TFRecordDataset(["my_compressed.tfrecord"],
compression_type="GZIP")
簡要介紹協(xié)議緩存
即便每條記錄可以使用任何二進制格式,TFRecord文件通常包括序列化的協(xié)議緩存(也稱為protobuf)裆赵。這是一種可移植东囚、可擴展的高效二進制格式,是谷歌在2001年開發(fā)战授,并在2008年開源的页藻;協(xié)議緩存現(xiàn)在使用廣泛,特別是在gRPC植兰,谷歌的遠程調(diào)用系統(tǒng)中份帐。定義語言如下:
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
repeated string email = 3;
}
定義寫道,使用的是協(xié)議緩存的版本3楣导,指定每個Person
對象可以有一個name
废境,類型是字符串,類型是int32的id
,0個或多個email
字段噩凹,每個都是字符串巴元。數(shù)字1、2驮宴、3是字段標識符:用于每條數(shù)據(jù)的二進制表示逮刨。當(dāng)你在.proto
文件中有了一個定義,就可以編譯了堵泽。這就需要protoc
禀忆,協(xié)議緩存編譯器,來生成Python(或其它語言)的訪問類落恼。注意箩退,要使用的緩存協(xié)議的定義已經(jīng)編譯好了,它們的Python類是TensorFlow的一部分佳谦,所以就不必使用protoc
了戴涝。你需要知道的知識如何使用Python的緩存協(xié)議訪問類。為了講解钻蔑,看一個簡單的例子啥刻,使用訪問類來生成Person
緩存協(xié)議:
>>> from person_pb2 import Person # 引入生成的訪問類
>>> person = Person(name="Al", id=123, email=["a@b.com"]) # 創(chuàng)建一個Person
>>> print(person) # 展示Person
name: "Al"
id: 123
email: "a@b.com"
>>> person.name # 讀取一個字段
"Al"
>>> person.name = "Alice" # 修改一個字段
>>> person.email[0] # 重復(fù)的字段可以像數(shù)組一樣訪問
"a@b.com"
>>> person.email.append("c@d.com") # 添加email地址
>>> s = person.SerializeToString() # 將對象序列化為字節(jié)串
>>> s
b'\n\x05Alice\x10{\x1a\x07a@b.com\x1a\x07c@d.com'
>>> person2 = Person() # 創(chuàng)建一個新Person
>>> person2.ParseFromString(s) #解析字節(jié)串(字節(jié)長度27)
27
>>> person == person2 # 現(xiàn)在相等
True
簡而言之,我們引入了protoc
生成的類Person
咪笑,創(chuàng)建了一個實例可帽,展示、讀取窗怒、并寫入新字段映跟,然后使用SerializeToString()
將其序列化。序列化的數(shù)據(jù)就可以保存或通過網(wǎng)絡(luò)傳輸了扬虚。當(dāng)讀取或接收二進制數(shù)據(jù)時努隙,可以使用ParseFromString()
方法來解析,就得到了序列化對象的復(fù)制辜昵。
可以將序列化的Person
對象存儲為TFRecord文件荸镊,然后可以加載和解析。但是SerializeToString()
和ParseFromString()
不是TensorFlow運算(這段代碼中的其它代碼也不是TensorFlow運算)堪置,因此TensorFlow函數(shù)中不能含有這兩個方法(除非將其包裝進tf.py_function()
運算躬存,但會使代碼速度變慢,移植性變差)舀锨。幸好岭洲,TensorFlow還有提供了解析運算的特殊協(xié)議緩存。
TensorFlow協(xié)議緩存
TFRecord文件主要使用的協(xié)議緩存是Example
雁竞,它表示數(shù)據(jù)集中的一個實例钦椭,包括命名特征的列表拧额,每個特征可以是字節(jié)串列表、或浮點列表彪腔、或整數(shù)列表侥锦。下面是一個協(xié)議緩存的定義:
syntax = "proto3";
message BytesList { repeated bytes value = 1; }
message FloatList { repeated float value = 1 [packed = true]; }
message Int64List { repeated int64 value = 1 [packed = true]; }
message Feature {
oneof kind {
BytesList bytes_list = 1;
FloatList float_list = 2;
Int64List int64_list = 3;
}
};
message Features { map<string, Feature> feature = 1; };
message Example { Features features = 1; };
BytesList
、FloatList
德挣、Int64List
的定義都很清楚恭垦。注意,重復(fù)的數(shù)值字段使用了[packed = true]
格嗅,目的是高效編碼番挺。Feature
包含的是BytesList
、FloatList
屯掖、Int64List
三者之一玄柏。Features
(帶s)是包含特征名和對應(yīng)特征值的字典。最后贴铜,一個Example
值包含一個Features
對象粪摘。下面是一個如何創(chuàng)建tf.train.Example
的例子,表示的是之前同樣的人绍坝,并存儲為TFRecord文件:
from tensorflow.train import BytesList, FloatList, Int64List
from tensorflow.train import Feature, Features, Example
person_example = Example(
features=Features(
feature={
"name": Feature(bytes_list=BytesList(value=[b"Alice"])),
"id": Feature(int64_list=Int64List(value=[123])),
"emails": Feature(bytes_list=BytesList(value=[b"a@b.com",
b"c@d.com"]))
}))
這段代碼有點冗長和重復(fù)徘意,但很清晰(可以很容易將其包裝起來)。現(xiàn)在有了Example
協(xié)議緩存轩褐,可以調(diào)用SerializeToString()
方法將其序列化椎咧,然后將結(jié)果數(shù)據(jù)存入TFRecord文件:
with tf.io.TFRecordWriter("my_contacts.tfrecord") as f:
f.write(person_example.SerializeToString())
通常需要寫不止一個Example
!一般來說把介,你需要寫一個轉(zhuǎn)換腳本勤讽,讀取當(dāng)前格式(例如csv),為每個實例創(chuàng)建Example
協(xié)議緩存劳澄,序列化并存儲到若干TFRecord文件中地技,最好再打散。這些需要花費不少時間秒拔,如有必要再這么做(也許CSV文件就足夠了)。
有了序列化好的Example
TFRecord文件之后飒硅,就可以加載了砂缩。
加載和解析Example
要加載序列化的Example
協(xié)議緩存,需要再次使用tf.data.TFRecordDataset
三娩,使用tf.io.parse_single_example()
解析每個Example
庵芭。這是一個TensorFlow運算,所以可以包裝進TF函數(shù)雀监。它至少需要兩個參數(shù):一個包含序列化數(shù)據(jù)的字符串標量張量双吆,和每個特征的描述眨唬。描述是一個字典,將每個特征名映射到tf.io.FixedLenFeature
描述符好乐,描述符指明特征的形狀匾竿、類型和默認值,或(當(dāng)特征列表長度可能變化時蔚万,比如"email"特征
)映射到tf.io.VarLenFeature
描述符岭妖,它只指向類型。
下面的代碼定義了描述字典反璃,然后迭代TFRecordDataset
昵慌,解析序列化的Example
協(xié)議緩存:
feature_description = {
"name": tf.io.FixedLenFeature([], tf.string, default_value=""),
"id": tf.io.FixedLenFeature([], tf.int64, default_value=0),
"emails": tf.io.VarLenFeature(tf.string),
}
for serialized_example in tf.data.TFRecordDataset(["my_contacts.tfrecord"]):
parsed_example = tf.io.parse_single_example(serialized_example,
feature_description)
長度固定的特征會像常規(guī)張量那樣解析,而長度可變的特征會作為稀疏張量解析淮蜈≌剩可以使用tf.sparse.to_dense()
將稀疏張量轉(zhuǎn)變?yōu)榫o密張量,但只是簡化了值的訪問:
>>> tf.sparse.to_dense(parsed_example["emails"], default_value=b"")
<tf.Tensor: [...] dtype=string, numpy=array([b'a@b.com', b'c@d.com'], [...])>
>>> parsed_example["emails"].values
<tf.Tensor: [...] dtype=string, numpy=array([b'a@b.com', b'c@d.com'], [...])>
BytesList
可以包含任意二進制數(shù)據(jù)梧田,序列化對象也成蜻韭。例如,可以使用tf.io.encode_jpeg()
將圖片編碼為JPEG格式柿扣,然后將二進制數(shù)據(jù)放入BytesList
肖方。然后,當(dāng)代碼讀取TFRecord
時未状,會從解析Example
開始俯画,再調(diào)用tf.io.decode_jpeg()
解析數(shù)據(jù),得到原始圖片(或者可以使用tf.io.decode_image()
司草,它能解析任意BMP
艰垂、GIF
、JPEG
埋虹、PNG
格式)猜憎。你還可以通過tf.io.serialize_tensor()
序列化張量,將結(jié)果字節(jié)串放入BytesList
特征搔课,將任意張量存儲在BytesList
中胰柑。之后,當(dāng)解析TFRecord
時爬泥,可以使用tf.io.parse_tensor()
解析數(shù)據(jù)柬讨。
除了使用tf.io.parse_single_example()
逐一解析Example
,你還可以通過tf.io.parse_example()
逐批次解析:
dataset = tf.data.TFRecordDataset(["my_contacts.tfrecord"]).batch(10)
for serialized_examples in dataset:
parsed_examples = tf.io.parse_example(serialized_examples,
feature_description)
可以看到Example
協(xié)議緩存對大多數(shù)情況就足夠了袍啡。但是踩官,如果處理的是嵌套列表,就會比較麻煩境输。比如蔗牡,假設(shè)你想分類文本文檔颖系。每個文檔可能都是句子的列表,而每個句子又是詞的列表辩越。每個文檔可能還有評論列表茁瘦,評論又是詞的列表撒遣。可能還有上下文數(shù)據(jù),比如文檔的作者破托、標題和出版日期哼勇。TensorFlow的SequenceExample
協(xié)議緩存就是為了處理這種情況的契沫。
使用SequenceExample
協(xié)議緩存處理嵌套列表
下面是SequenceExample
協(xié)議緩存的定義:
message FeatureList { repeated Feature feature = 1; };
message FeatureLists { map<string, FeatureList> feature_list = 1; };
message SequenceExample {
Features context = 1;
FeatureLists feature_lists = 2;
};
SequenceExample
包括一個上下文數(shù)據(jù)的Features
對象捧毛,和一個包括一個或多個命名FeatureList
對象(比如,一個FeatureList
命名為"content"
姑丑,另一個命名為"comments"
)的FeatureLists
對象蛤签。每個FeatureList
包含Feature
對象的列表,每個Feature
對象可能是字節(jié)串栅哀、64位整數(shù)或浮點數(shù)的列表(這個例子中震肮,每個Feature
表示的是一個句子或一條評論,格式或許是詞的列表)留拾。創(chuàng)建SequenceExample
戳晌,將其序列化、解析痴柔,和創(chuàng)建沦偎、序列化、解析Example
很像咳蔚,但必須要使用tf.io.parse_single_sequence_example()
來解析單個的SequenceExample
或用tf.io.parse_sequence_example()
解析一個批次豪嚎。兩個函數(shù)都是返回一個包含上下文特征(字典)和特征列表(也是字典)的元組。如果特征列表包含大小可變的序列(就像前面的例子)谈火,可以將其轉(zhuǎn)化為嵌套張量侈询,使用tf.RaggedTensor.from_sparse()
:
parsed_context, parsed_feature_lists = tf.io.parse_single_sequence_example(
serialized_sequence_example, context_feature_descriptions,
sequence_feature_descriptions)
parsed_content = tf.RaggedTensor.from_sparse(parsed_feature_lists["content"])
現(xiàn)在你就知道如何高效存儲、加載和解析數(shù)據(jù)了糯耍,下一步是準備數(shù)據(jù)扔字。
預(yù)處理輸入特征
為神經(jīng)網(wǎng)絡(luò)準備數(shù)據(jù)需要將所有特征轉(zhuǎn)變?yōu)閿?shù)值特征,做一些歸一化工作等等谍肤。特別的啦租,如果數(shù)據(jù)包括類型特征或文本特征,也需要轉(zhuǎn)變?yōu)閿?shù)字荒揣。這些工作可以在準備數(shù)據(jù)文件的時候做,使用NumPy焊刹、Pandas系任、Scikit-Learn這樣的工作恳蹲。或者俩滥,可以在用Data API加載數(shù)據(jù)時嘉蕾,實時預(yù)處理數(shù)據(jù)(比如,使用數(shù)據(jù)集的map()
方法霜旧,就像前面的例子)错忱,或者可以給模型加一個預(yù)處理層。接下來挂据,來看最后一種方法以清。
例如,這個例子是使用Lambda
層實現(xiàn)標準化層崎逃。對于每個特征掷倔,減去其平均值,再除以標準差(再加上一個平滑項个绍,避免0除):
means = np.mean(X_train, axis=0, keepdims=True)
stds = np.std(X_train, axis=0, keepdims=True)
eps = keras.backend.epsilon()
model = keras.models.Sequential([
keras.layers.Lambda(lambda inputs: (inputs - means) / (stds + eps)),
[...] # 其它層
])
并不難勒葱。但是,你也許更想要一個獨立的自定義層(就像Scikit-Learn的StandardScaler
)巴柿,而不是像means
和stds
這樣的全局變量:
class Standardization(keras.layers.Layer):
def adapt(self, data_sample):
self.means_ = np.mean(data_sample, axis=0, keepdims=True)
self.stds_ = np.std(data_sample, axis=0, keepdims=True)
def call(self, inputs):
return (inputs - self.means_) / (self.stds_ + keras.backend.epsilon())
使用這個標準化層之前凛虽,你需要使用adapt()
方法將其適配到數(shù)據(jù)集樣本。這么做就能使用每個特征的平均值和標準差:
std_layer = Standardization()
std_layer.adapt(data_sample)
這個樣本必須足夠大广恢,可以代表數(shù)據(jù)集凯旋,但不必是完整的訓(xùn)練集:通常幾百個隨機實例就夠了(但還是要取決于任務(wù))。然后袁波,就可以像普通層一樣使用這個預(yù)處理層了:
model = keras.Sequential()
model.add(std_layer)
[...] # create the rest of the model
model.compile([...])
model.fit([...])
可能以后還會有keras.layers.Normalization
層瓦阐,和這個自定義Standardization
層差不多:先創(chuàng)建層,然后對數(shù)據(jù)集做適配(向adapt()
方法傳遞樣本)篷牌,最后像普通層一樣使用睡蟋。
接下來看看類型特征。先將其編碼為獨熱矢量枷颊。
使用獨熱矢量編碼類型特征
考慮下第2章中的加州房價數(shù)據(jù)集的ocean_proximity
特征:這是一個類型特征戳杀,有五個值:"<1H OCEAN"
、"INLAND"
夭苗、"NEAR OCEAN"
信卡、"NEAR BAY"
、"ISLAND"
题造。輸入給神經(jīng)網(wǎng)絡(luò)之前傍菇,需要對其進行編碼。因為類型不多界赔,可以使用獨熱編碼丢习。先將每個類型映射為索引(0到4)牵触,使用一張查詢表:
vocab = ["<1H OCEAN", "INLAND", "NEAR OCEAN", "NEAR BAY", "ISLAND"]
indices = tf.range(len(vocab), dtype=tf.int64)
table_init = tf.lookup.KeyValueTensorInitializer(vocab, indices)
num_oov_buckets = 2
table = tf.lookup.StaticVocabularyTable(table_init, num_oov_buckets)
逐行看下代碼:
先定義詞典:也就是所有類型的列表。
然后創(chuàng)建張量咐低,具有索引0到4揽思。
接著,創(chuàng)建查找表的初始化器见擦,傳入類型列表和對應(yīng)索引钉汗。在這個例子中,因為已經(jīng)有了數(shù)據(jù)鲤屡,所以直接用
KeyValueTensorInitializer
就成了损痰;但如果類型是在文本中(一行一個類型),就要使用TextFileInitializer
执俩。最后兩行創(chuàng)建了查找表徐钠,傳入初始化器并指明未登錄詞(oov)桶的數(shù)量。如果查找的類型不在詞典中役首,查找表會計算這個類型的哈希尝丐,使用哈希分配一個未知的類型給未登錄詞桶。索引序號接著現(xiàn)有序號衡奥,所以這個例子中的兩個未登錄詞的索引是5和6爹袁。
為什么使用桶呢?如果類型數(shù)足夠大(例如矮固,郵編失息、城市、詞档址、產(chǎn)品盹兢、或用戶),數(shù)據(jù)集也足夠大守伸,或者數(shù)據(jù)集持續(xù)變化绎秒,這樣的話,獲取類型的完整列表就不容易了尼摹。一個解決方法是根據(jù)數(shù)據(jù)樣本定義(而不是整個訓(xùn)練集)见芹,為其它不在樣本中的類型加上一些未登錄詞桶。訓(xùn)練中碰到的未知類型越多蠢涝,要使用的未登錄詞桶就要越多玄呛。事實上,如果未登錄詞桶的數(shù)量不夠和二,就會發(fā)生碰撞:不同的類型會出現(xiàn)在同一個桶中徘铝,所以神經(jīng)網(wǎng)絡(luò)就無法區(qū)分了。
現(xiàn)在用查找表將小批次的類型特征編碼為獨熱矢量:
>>> categories = tf.constant(["NEAR BAY", "DESERT", "INLAND", "INLAND"])
>>> cat_indices = table.lookup(categories)
>>> cat_indices
<tf.Tensor: id=514, shape=(4,), dtype=int64, numpy=array([3, 5, 1, 1])>
>>> cat_one_hot = tf.one_hot(cat_indices, depth=len(vocab) + num_oov_buckets)
>>> cat_one_hot
<tf.Tensor: id=524, shape=(4, 7), dtype=float32, numpy=
array([[0., 0., 0., 1., 0., 0., 0.],
[0., 0., 0., 0., 0., 1., 0.],
[0., 1., 0., 0., 0., 0., 0.],
[0., 1., 0., 0., 0., 0., 0.]], dtype=float32)>
可以看到,"NEAR BAY"
映射到了索引3庭砍,未知類型"DESERT"
映射到了兩個未登錄詞桶之一(索引5)场晶,"INLAND"
映射到了索引1兩次混埠。然后使用tf.one_hot()
來做獨熱編碼怠缸。注意,需要告訴該函數(shù)索引的總數(shù)量钳宪,索引總數(shù)等于詞典大小加上未登錄詞桶的數(shù)量〗冶保現(xiàn)在你就知道如何用TensorFlow將類型特征編碼為獨熱矢量了。
和之前一樣吏颖,將這些操作寫成一個獨立的類并不難搔体。adapt()
方法接收一個數(shù)據(jù)樣本,提取其中的所有類型半醉。創(chuàng)建一張查找表疚俱,將類型和索引映射起來。call()
方法會使用查找表將輸入類型和索引建立映射缩多。目前呆奕,Keras已經(jīng)有了一個名為keras.layers.TextVectorization
的層,它的功能就是上面這樣:adapt()
從樣本中提取詞表衬吆,call()
將每個類型映射到詞表的索引梁钾。如果要將索引變?yōu)楠殶崾噶康脑挘梢詫⑦@個層添加到模型開始的地方逊抡,后面根生一個可以用tf.one_hot()
的Lambda
層姆泻。
這可能不是最佳解決方法。每個獨熱矢量的大小是詞表長度加上未登錄詞桶的大小冒嫡。當(dāng)類型不多時拇勃,這么做可以,但如果詞表很大孝凌,最好使用“嵌入“來做方咆。
提示:一個重要的原則,如果類型數(shù)小于10胎许,可以使用獨熱編碼峻呛。如果類型超過50個(使用哈希桶時通常如此),最好使用嵌入辜窑。類型數(shù)在10和50之間時钩述,最好對兩種方法做個試驗,看哪個更合適穆碎。
使用嵌入編碼類型特征
嵌入是一個可訓(xùn)練的表示類型的緊密矢量牙勘。默認時,嵌入是隨機初始化的,"NEAR BAY"
可能初始化為[0.131, 0.890]
方面,"NEAR OCEAN"
可能初始化為[0.631, 0.791]
放钦。
這個例子中,使用的是2D嵌入恭金,維度是一個可調(diào)節(jié)的超參數(shù)操禀。因為嵌入是可以訓(xùn)練的,它能在訓(xùn)練中提高性能横腿;當(dāng)嵌入表示相似的類時颓屑,梯度下降會使相似的嵌入靠的更近,而"INLAND"
會偏的更遠(見圖13-4)耿焊。事實上揪惦,表征的越好,越利于神經(jīng)網(wǎng)絡(luò)做出準確的預(yù)測罗侯,而訓(xùn)練會讓嵌入更好的表征類型钩杰,這被稱為表征學(xué)習(xí)(第17章會介紹其它類型的表征學(xué)習(xí))。
詞嵌入
嵌入不僅可以實現(xiàn)當(dāng)前任務(wù)的表征榜苫,同樣的嵌入也可以用于其它的任務(wù)。最常見的例子是詞嵌入(即媳荒,單個詞的嵌入):對于自然語言處理任務(wù),最好使用預(yù)訓(xùn)練的詞嵌入驹饺,而不是使用自己訓(xùn)練的。
使用矢量表征詞可以追溯到1960年代鱼炒,許多復(fù)雜的技術(shù)用于生成向量昔瞧,包括使用神經(jīng)網(wǎng)絡(luò)菩佑。進步發(fā)生在2013年稍坯,Tomá? Mikolov和谷歌其它的研究院發(fā)表了一篇論文《Distributed Representations of Words and Phrases and their Compositionality》(https://arxiv.org/abs/1310.4546),介紹了一種用神經(jīng)網(wǎng)絡(luò)學(xué)習(xí)詞嵌入的技術(shù)枪向,效果遠超以前的技術(shù)秘蛔〔可以實現(xiàn)在大文本語料上學(xué)習(xí)嵌入:用神經(jīng)網(wǎng)絡(luò)預(yù)測給定詞附近的詞,得到了非常好的詞嵌入箱残。例如被辑,同義詞有非常相近的詞嵌入敬惦,語義相近的詞俄删,比如法國畴椰、西班牙和意大利靠的也很近斜脂。
不止是相近:詞嵌入在嵌入空間的軸上的分布也是有意義的帚戳。下面是一個著名的例子:如果計算 King – Man + Woman片任,結(jié)果與Queen非常相近(見圖13-5)蚂踊。換句話,詞嵌入編碼了性別棱诱。相似的迈勋,可以計算 Madrid – Spain + France靡菇,結(jié)果和Paris很近厦凤。
圖13-5 相似詞的詞嵌入也相近椎木,一些軸編碼了概念但是香椎,詞嵌入有時偏差很大畜伐。例如玛界,盡管詞嵌入學(xué)習(xí)到了男人是國王脚仔,女人是王后鲤脏,詞嵌入還學(xué)到了男人是醫(yī)生猎醇、女人是護士硫嘶。這是非常大的性別偏差沦疾。
來看下如何手動實現(xiàn)嵌入哮塞。首先忆畅,需要創(chuàng)建一個包含每個類型嵌入(隨機初始化)的嵌入矩陣家凯。每個類型就有一行绊诲,每個未登錄詞桶就有一行驯镊,每個嵌入維度就有一列:
embedding_dim = 2
embed_init = tf.random.uniform([len(vocab) + num_oov_buckets, embedding_dim])
embedding_matrix = tf.Variable(embed_init)
這個例子用的是2D嵌入板惑,通常的嵌入是10到300維冯乘,取決于任務(wù)和詞表大旭陕(需要調(diào)節(jié)詞表大小超參數(shù))喷好。
嵌入矩陣是一個隨機的6 × 2矩陣梗搅,存入一個變量(因此可以在訓(xùn)練中被梯度下降調(diào)節(jié)):
>>> embedding_matrix
<tf.Variable 'Variable:0' shape=(6, 2) dtype=float32, numpy=
array([[0.6645621 , 0.44100678],
[0.3528825 , 0.46448255],
[0.03366041, 0.68467236],
[0.74011743, 0.8724445 ],
[0.22632635, 0.22319686],
[0.3103881 , 0.7223358 ]], dtype=float32)>
使用嵌入編碼之前的類型特征:
>>> categories = tf.constant(["NEAR BAY", "DESERT", "INLAND", "INLAND"])
>>> cat_indices = table.lookup(categories)
>>> cat_indices
<tf.Tensor: id=741, shape=(4,), dtype=int64, numpy=array([3, 5, 1, 1])>
>>> tf.nn.embedding_lookup(embedding_matrix, cat_indices)
<tf.Tensor: id=864, shape=(4, 2), dtype=float32, numpy=
array([[0.74011743, 0.8724445 ],
[0.3103881 , 0.7223358 ],
[0.3528825 , 0.46448255],
[0.3528825 , 0.46448255]], dtype=float32)>
tf.nn.embedding_lookup()
函數(shù)根據(jù)給定的索引在嵌入矩陣中查找行。例如哆键,查找表說"INLAND"
類型位于索引1籍嘹,tf.nn.embedding_lookup()
就返回嵌入矩陣的行1:[0.3528825, 0.46448255]
辱士。
Keras提供了keras.layers.Embedding
層來處理嵌入矩陣(默認可訓(xùn)練)识补;當(dāng)這個層初始化時凭涂,會隨機初始化嵌入矩陣切油,當(dāng)被調(diào)用時澎胡,就返回索引所在的嵌入矩陣的那行:
>>> embedding = keras.layers.Embedding(input_dim=len(vocab) + num_oov_buckets,
... output_dim=embedding_dim)
...
>>> embedding(cat_indices)
<tf.Tensor: id=814, shape=(4, 2), dtype=float32, numpy=
array([[ 0.02401174, 0.03724445],
[-0.01896119, 0.02223358],
[-0.01471175, -0.00355174],
[-0.01471175, -0.00355174]], dtype=float32)>
將這些內(nèi)容放到一起稚伍,創(chuàng)建一個Keras模型个曙,可以處理類型特征(和數(shù)值特征)垦搬,學(xué)習(xí)每個類型(和未登錄詞)的嵌入:
regular_inputs = keras.layers.Input(shape=[8])
categories = keras.layers.Input(shape=[], dtype=tf.string)
cat_indices = keras.layers.Lambda(lambda cats: table.lookup(cats))(categories)
cat_embed = keras.layers.Embedding(input_dim=6, output_dim=2)(cat_indices)
encoded_inputs = keras.layers.concatenate([regular_inputs, cat_embed])
outputs = keras.layers.Dense(1)(encoded_inputs)
model = keras.models.Model(inputs=[regular_inputs, categories],
outputs=[outputs])
這個模型有兩個輸入:一個常規(guī)輸入猴贰,每個實例包括8個數(shù)值特征,機上一個類型特征义郑。使用Lambda
層查找每個類型的索引非驮,然后用索引查找嵌入劫笙。接著填大,將嵌入和常規(guī)輸入連起來允华,作為編碼輸入進神經(jīng)網(wǎng)絡(luò)靴寂。此時可以加入任意種類的神經(jīng)網(wǎng)絡(luò)百炬,但只是添加了一個緊密輸出層庶弃。
當(dāng)keras.layers.TextVectorization
準備好之后歇攻,可以調(diào)用它的adapt()
方法掉伏,從數(shù)據(jù)樣本提取詞表(會自動創(chuàng)建查找表)。然后加入到模型中摊聋,就可以執(zhí)行索引查找了(替換前面代碼的Lambda
層)。
筆記:獨熱編碼加緊密層(沒有激活函數(shù)和偏差項)煎源,等價于嵌入層手销。但是锋拖,嵌入層用的計算更少(嵌入矩陣越大兽埃,性能差距越明顯)柄错。緊密層的權(quán)重矩陣扮演的是嵌入矩陣的角色给猾。例如耙册,大小為20的獨熱矢量和10個單元的緊密層加起來详拙,等價于
input_dim=20
饶辙、output_dim=10
的嵌入層。作為結(jié)果矿微,嵌入的維度超過后面的層的神經(jīng)元數(shù)是浪費的涌矢。
再進一步看看Keras的預(yù)處理層娜庇。
Keras預(yù)處理層
Keras團隊打算提供一套標準的Keras預(yù)處理層,現(xiàn)在已經(jīng)可用了匕得,鏈接耗跛。新的API可能會覆蓋舊的Feature Columns API调塌。
我們已經(jīng)討論了其中的兩個:keras.layers.Normalization
用來做特征標準化羔砾,TextVectorization
層用于將文本中的詞編碼為詞典的索引政溃。對于這兩個層董虱,都是用數(shù)據(jù)樣本調(diào)用它的adapt()
方法愤诱,然后如常使用。其它的預(yù)處理層也是這么使用的科吭。
API中還提供了keras.layers.Discretization
層对人,它能將連續(xù)數(shù)據(jù)切成不同的組规伐,將每個組斌嗎為獨熱矢量鲜棠。例如豁陆,可以用它將價格分成是三類盒音,低譬圣、中厘熟、高登澜,編碼為[1, 0, 0]脑蠕、[0, 1, 0]谴仙、[0, 0, 1]狞甚。當(dāng)然,這么做會損失很多信息涩盾,但有時,相對于連續(xù)數(shù)據(jù)址儒,這么做可以發(fā)現(xiàn)不那么明顯的規(guī)律莲趣。
警告:
Discretization
層是不可微的,只能在模型一開始使用潘鲫。事實上溉仑,模型的預(yù)處理層會在訓(xùn)練時凍結(jié)挪圾,因此預(yù)處理層的參數(shù)不會被梯度下降影響哲思,所以可以是不可微的。這還意味著靠益,如果想讓預(yù)處理層可訓(xùn)練的話,不能在自定義預(yù)處理層上直接使用嵌入層壳快,而是應(yīng)該像前民的例子那樣分開來做。
還可以用類PreprocessingStage
將多個預(yù)處理層鏈接起來竖伯。例如七婴,下面的代碼創(chuàng)建了一個預(yù)處理管道,先將輸入歸一化,然后離散(有點類似Scikit-Learn的管道)先舷。當(dāng)將這個管道應(yīng)用到數(shù)據(jù)樣本時蒋川,可以作為常規(guī)層使用(還得是在模型的前部,因為包含不可微分的預(yù)處理層):
normalization = keras.layers.Normalization()
discretization = keras.layers.Discretization([...])
pipeline = keras.layers.PreprocessingStage([normalization, discretization])
pipeline.adapt(data_sample)
TextVectorization
層也有一個選項用于輸出詞頻向量氮兵,而不是詞索引。例如南片,如果詞典包括三個詞,比如["and", "basketball", "more"]
伞广,則"more and more"
會映射為[1, 0, 2]
:"and"
出現(xiàn)了一次,"basketball"
沒有出現(xiàn)灾票,"more"
出現(xiàn)了兩次。這種詞表征稱為詞袋茫虽,因為它完全失去了詞的順序刊苍。常見詞,比如"and"
濒析,會在文本中有更高的值正什,盡管沒什么實際意義号杏。因此婴氮,詞頻向量中應(yīng)該降低常見詞的影響斯棒。一個常見的方法是將詞頻除以出現(xiàn)該詞的文檔數(shù)的對數(shù)。這種方法稱為詞頻-逆文檔頻率(TF-IDF)主经。例如荣暮,假設(shè)"and"
、"basketball"
罩驻、"more"
分別出現(xiàn)在了200穗酥、10、100個文檔中:最終的矢量應(yīng)該是[1/log(200), 0/log(10), 2/log(100)]
惠遏,大約是[0.19, 0., 0.43]
砾跃。TextVectorization
層會有TF-IDF的選項。
筆記:如果標準預(yù)處理層不能滿足你的任務(wù)节吮,你還可以選擇創(chuàng)建自定義預(yù)處理層抽高,就像前面的
Standardization
。創(chuàng)建一個keras.layers.PreprocessingLayer
子類课锌,adapt()
方法用于接收一個data_sample
參數(shù)厨内,或者再有一個reset_state
參數(shù):如果是True
,則adapt()
方法在計算新狀態(tài)之前重置現(xiàn)有的狀態(tài)渺贤;如果是False
雏胃,會更新現(xiàn)有的狀態(tài)。
可以看到志鞍,這些Keras預(yù)處理層可以使預(yù)處理更容易瞭亮!現(xiàn)在,無論是自定義預(yù)處理層固棚,還是使用Keras的统翩,預(yù)處理都可以實時進行了。但在訓(xùn)練中此洲,最好再提前進行預(yù)處理厂汗。下面來看看為什么,以及怎么做呜师。
TF Transform
預(yù)處理非常消耗算力娶桦,訓(xùn)練前做預(yù)處理相對于實時處理,可以極大的提高速度:數(shù)據(jù)在訓(xùn)練前汁汗,每個實例就處理一次衷畦,而不是在訓(xùn)練中每個實例在每個周期就處理一次。前面提到過知牌,如果數(shù)據(jù)集小到可以存入內(nèi)存祈争,可以使用cache()
方法。但如果太大角寸,可以使用Apache Beam或Spark菩混。它們可以在大數(shù)據(jù)上做高效的數(shù)據(jù)預(yù)處理忿墅,還可以分布進行,使用它們就能在訓(xùn)練前處理所有訓(xùn)練數(shù)據(jù)了沮峡。
雖然訓(xùn)練加速了球匕,但帶來一個問題:一旦模型訓(xùn)練好了,假如想部署到移動app上帖烘,還是需要寫一些預(yù)處理數(shù)據(jù)的代碼。假如想部署到TensorFlow.js橄杨,還是需要預(yù)處理代碼秘症。這是一個維護難題:無論何時想改變預(yù)處理邏輯,都需要更新Apache Beam的代碼式矫、移動端代碼乡摹、JavaScript代碼。不僅耗時采转,也容易出錯:不同端的可能有細微的差別聪廉。訓(xùn)練/實際產(chǎn)品表現(xiàn)之間的偏差會導(dǎo)致bug或使效果大打折扣。
一種解決辦法是在部署到app或瀏覽器之前故慈,給訓(xùn)練好的模型加上額外的預(yù)處理層板熊,來做實時的預(yù)處理。這樣好多了察绷,只有兩套代碼Apache Beam 或 Spark 代碼干签,和預(yù)處理層代碼。
如果只需定義一次預(yù)處理操作呢拆撼?這就是TF Transform要做的容劳。TF Transform是TensorFlow Extended (TFX)的一部分,這是一個端到端的TensorFlow模型生產(chǎn)化平臺闸度。首先竭贩,需要安裝(TensorFlow沒有捆綁)。然后通過TF Transform函數(shù)來做縮放莺禁、分桶等操作留量,一次性定義預(yù)處理函數(shù)。你還可以使用任意需要的TensorFlow運算睁宰。如果只有兩個特征肪获,預(yù)處理函數(shù)可能如下:
import tensorflow_transform as tft
def preprocess(inputs): # inputs = 輸入特征批次
median_age = inputs["housing_median_age"]
ocean_proximity = inputs["ocean_proximity"]
standardized_age = tft.scale_to_z_score(median_age)
ocean_proximity_id = tft.compute_and_apply_vocabulary(ocean_proximity)
return {
"standardized_median_age": standardized_age,
"ocean_proximity_id": ocean_proximity_id
}
然后,TF Transform可以使用Apache Beam(可以使用其AnalyzeAndTransformDataset
類)在整個訓(xùn)練集上應(yīng)用這個preprocess()
函數(shù)柒傻。在使用過程中孝赫,還會計算整個訓(xùn)練集上的必要統(tǒng)計數(shù)據(jù):這個例子中,是housing_median_age
和the ocean_proximity
的平均值和標準差。計算這些數(shù)據(jù)的組件稱為分析器挟阻。
更重要的,TF Transform還會生成一個等價的TensorFlow函數(shù)木羹,可以放入部署的模型中致开。這個TF函數(shù)包括一些常量峰锁,對應(yīng)于Apache Beam的統(tǒng)計值(平均值、標準差和詞典)双戳。
有了Data API虹蒋、TFRecord,Keras預(yù)處理層和TF Transform飒货,可以為訓(xùn)練搭建高度伸縮的輸入管道魄衅,可以是生產(chǎn)又快,遷移性又好塘辅。
但是晃虫,如果只想使用標準數(shù)據(jù)集呢?只要使用TFDS就成了扣墩。
TensorFlow Datasets(TFDS)項目
從TensorFlow Datasets項目哲银,可以非常方便的下載一些常見的數(shù)據(jù)集,從小數(shù)據(jù)集呻惕,比如MNIST或Fashion MNIST荆责,到大數(shù)據(jù)集,比如ImageNet(需要大硬盤)蟆融。包括了圖片數(shù)據(jù)集草巡、文本數(shù)據(jù)集(包括翻譯數(shù)據(jù)集)、和音頻視頻數(shù)據(jù)集型酥∩胶可以訪問https://www.tensorflow.org/datasets/datasets,查看完整列表弥喉,每個數(shù)據(jù)集都有介紹郁竟。
TensorFlow沒有捆綁TFDS,所以需要使用pip安裝庫tensorflow-datasets
由境。然后調(diào)用函數(shù)tfds.load()
棚亩,就能下載數(shù)據(jù)集了(除非之前下載過),返回的數(shù)據(jù)是數(shù)據(jù)集的字典(通常是一個是訓(xùn)練集虏杰,一個是測試集)讥蟆。例如,下載MNIST:
import tensorflow_datasets as tfds
dataset = tfds.load(name="mnist")
mnist_train, mnist_test = dataset["train"], dataset["test"]
然后可以對其應(yīng)用任意轉(zhuǎn)換(打散纺阔、批次瘸彤、預(yù)提取)笛钝,然后就可以訓(xùn)練模型了质况。下面是一個簡單的例子:
mnist_train = mnist_train.shuffle(10000).batch(32).prefetch(1)
for item in mnist_train:
images = item["image"]
labels = item["label"]
[...]
提示:
load()
函數(shù)打散了每個下載的數(shù)據(jù)分片(只是對于訓(xùn)練集)愕宋。但還不夠,最好再自己做打散结榄。
注意中贝,數(shù)據(jù)集中的每一項都是一個字典,包含特征和標簽臼朗。但Keras期望每項都是一個包含兩個元素(特征和標簽)的元組邻寿。可以使用map()
對數(shù)據(jù)集做轉(zhuǎn)換视哑,如下:
mnist_train = mnist_train.shuffle(10000).batch(32)
mnist_train = mnist_train.map(lambda items: (items["image"], items["label"]))
mnist_train = mnist_train.prefetch(1)
更簡單的方式是讓load()
函數(shù)來做這個工作老厌,只要設(shè)定as_supervised=True
(顯然這只適用于有標簽的數(shù)據(jù)集)。你還可以將數(shù)據(jù)集直接傳給tf.keras模型:
dataset = tfds.load(name="mnist", batch_size=32, as_supervised=True)
mnist_train = dataset["train"].prefetch(1)
model = keras.models.Sequential([...])
model.compile(loss="sparse_categorical_crossentropy", optimizer="sgd")
model.fit(mnist_train, epochs=5)
這一章很技術(shù)黎炉,你可能覺得沒有神經(jīng)網(wǎng)絡(luò)的抽象美,但事實是深度學(xué)習(xí)經(jīng)常要涉及大數(shù)據(jù)集醋拧,知道如果高效加載慷嗜、解析和預(yù)處理,是一個非常重要的技能丹壕。下一章會學(xué)習(xí)卷積神經(jīng)網(wǎng)絡(luò)庆械,它是一種用于圖像處理和其它應(yīng)用的、非常成功的神經(jīng)網(wǎng)絡(luò)菌赖。
練習(xí)
為什么要使用Data API 缭乘?
將大數(shù)據(jù)分成多個文件有什么好處?
訓(xùn)練中琉用,如何斷定輸入管道是瓶頸堕绩?如何處理瓶頸?
可以將任何二進制數(shù)據(jù)存入TFRecord文件嗎邑时,還是只能存序列化的協(xié)議緩存奴紧?
為什么要將數(shù)據(jù)轉(zhuǎn)換為Example協(xié)議緩存?為什么不使用自己的協(xié)議緩存晶丘?
使用TFRecord時黍氮,什么時候要壓縮?為什么不系統(tǒng)化的做浅浮?
數(shù)據(jù)預(yù)處理可以在寫入數(shù)據(jù)文件時沫浆,或在tf.data管道中,或在預(yù)處理層中滚秩,或使用TF Transform专执。這幾種方法各有什么優(yōu)缺點?
說出幾種常見的編碼類型特征的方法叔遂。文本如何編碼他炊?
9.加載Fashion MNIST數(shù)據(jù)集争剿;將其分成訓(xùn)練集、驗證集和測試集痊末;打散訓(xùn)練集蚕苇;將每個數(shù)據(jù)及村委多個TFRecord文件。每條記錄應(yīng)該是有兩個特征的序列化的Example協(xié)議緩存:序列化的圖片(使用tf.io.serialize_tensor()
序列化每張圖片)凿叠,和標簽涩笤。然后使用tf.data為每個集合創(chuàng)建一個高效數(shù)據(jù)集。最后盒件,使用Keras模型訓(xùn)練這些數(shù)據(jù)集蹬碧,用預(yù)處理層標準化每個特征。讓輸入管道越高效越好炒刁,使用TensorBoard可視化地分析數(shù)據(jù)恩沽。
- 在這道題中,你要下載一個數(shù)據(jù)集翔始,分割它罗心,創(chuàng)建一個tf.data.Dataset,用于高效加載和預(yù)處理城瞎,然后搭建一個包含嵌入層的二分類模型:
a. 下載Large Movie Review Dataset渤闷,它包含50000條IMDB的影評。數(shù)據(jù)分為兩個目錄脖镀,train和test飒箭,每個包含12500條正面評價和12500條負面評價。每條評價都存在獨立的文本文件中蜒灰。還有其他文件和文件夾(包括預(yù)處理的詞袋)弦蹂,但這個練習(xí)中用不到。
b. 將測試集分給成驗證集(15000)和測試集(10000)强窖。
c. 使用tf.data盈匾,為每個集合創(chuàng)建高效數(shù)據(jù)集。
d.創(chuàng)建一個二分類模型毕骡,使用TextVectorization
層來預(yù)處理每條影評削饵。如果TextVectorization
層用不了(或者你想挑戰(zhàn)下),則創(chuàng)建自定義的預(yù)處理層:使用tf.strings
包中的函數(shù)未巫,比如lower()
來做小寫窿撬,regex_replace()
來替換帶有空格的標點,split()
來分割詞叙凡。用查找表輸出詞索引劈伴,adapt()
方法中要準備好。
e. 加入嵌入層,計算每條評論的平均嵌入跛璧,乘以詞數(shù)的平方根严里。這個縮放過的平均嵌入可以傳入剩余的模型中。
f. 訓(xùn)練模型追城,看看準確率能達到多少刹碾。嘗試優(yōu)化管道,讓訓(xùn)練越快越好座柱。
g. 施一公TFDS加載同樣的數(shù)據(jù)集:tfds.load("imdb_reviews")
迷帜。
參考答案見附錄A。
第10章 使用Keras搭建人工神經(jīng)網(wǎng)絡(luò)
第11章 訓(xùn)練深度神經(jīng)網(wǎng)絡(luò)
第12章 使用TensorFlow自定義模型并訓(xùn)練
第13章 使用TensorFlow加載和預(yù)處理數(shù)據(jù)
第14章 使用卷積神經(jīng)網(wǎng)絡(luò)實現(xiàn)深度計算機視覺
第15章 使用RNN和CNN處理序列
第16章 使用RNN和注意力機制進行自然語言處理
第17章 使用自編碼器和GAN做表征學(xué)習(xí)和生成式學(xué)習(xí)
第18章 強化學(xué)習(xí)
第19章 規(guī)纳矗化訓(xùn)練和部署TensorFlow模型