目前为止,我们只使用了能放在内存中的数据集,而深度学习往往使用非常大而无法放在RAM中的数据集。其它深度学习库可能对处理这种大型数据集比较棘手,但是TensorFlow很容易完成,这得归功于其数据API(Data API),即只需创建一个数据对象,然后赋值其数据位置和转换方法即可。TensorFlow会处理好各种细节,例如多线程、队列、批处理等等。同时TensorFlow数据API与tf.keras合作非常好。
TensorFlow数据API可以读取文本文件、二进制文件。TFRecord是一个易用且高效的格式,基于Protocol Buffers格式。TensorFlow数据API也支持从SQL数据库中读取数据。
读取数据后还需要对其进行预处理,例如归一化。对于数据预处理,可以自定义预处理的层,也可以使用Keras提供的标准预处理层。
TF Transform (tf.Transform):编写训练集上按批次进行预处理的函数,然后转换成TF函数并加入到模型结构中,使得模型部署到生产环境中时可以很快地进行样本预处理。
TF Datasets (TFDS):提供各种常用数据集下载的函数,包括ImageNet等大型数据集。
0. 导入所需的库
import tensorflow as tf
from tensorflow import keras
import numpy as np
import matplotlib as mpl
from matplotlib import pyplot as plt
import pandas as pd
import os
import tensorflow_datasets as tfds
import tensorflow_hub as hub
import tensorflow_transform as tft
for i in (tf, np, mpl, pd, tfds, hub, tft):
print("{}: {}".format(i.__name__, i.__version__))
输出:
tensorflow: 2.2.0
numpy: 1.17.4
matplotlib: 3.1.2
pandas: 0.25.3
tensorflow_datasets: 3.1.0
tensorflow_hub: 0.8.0
tensorflow_transform: 0.22.0
1. 数据API(The Data API)
tf.data.Datasets.from_tensor_slices():创建一个存储于内存中的数据集:
X = tf.range(10)
dataset = tf.data.Dataset.from_tensor_slices(X)
dataset
输出:
<TensorSliceDataset shapes: (), types: tf.int32>
from_tensor_slices()函数传入一个张量,返回一个tf.data.Dataset,其中元素为输入张量的所有切片,结果与下面写法是等价的:
dataset = tf.data.Dataset.range(10)
for item in dataset:
print(item)
输出:
tf.Tensor(0, shape=(), dtype=int64)
tf.Tensor(1, shape=(), dtype=int64)
tf.Tensor(2, shape=(), dtype=int64)
tf.Tensor(3, shape=(), dtype=int64)
tf.Tensor(4, shape=(), dtype=int64)
tf.Tensor(5, shape=(), dtype=int64)
tf.Tensor(6, shape=(), dtype=int64)
tf.Tensor(7, shape=(), dtype=int64)
tf.Tensor(8, shape=(), dtype=int64)
tf.Tensor(9, shape=(), dtype=int64)
1.1 链式转换(Chaining Transformations)
下面例子中,首先使用repeat()方法将数据集重复3次,返回一个新的数据集
然后在新数据集上调用batch()方法,每7个数据为一个批次,也返回一个新数据集
dataset = dataset.repeat(3).batch(7)
for item in dataset:
print(item)
输出:
tf.Tensor([0 1 2 3 4 5 6], shape=(7,), dtype=int64)
tf.Tensor([7 8 9 0 1 2 3], shape=(7,), dtype=int64)
tf.Tensor([4 5 6 7 8 9 0], shape=(7,), dtype=int64)
tf.Tensor([1 2 3 4 5 6 7], shape=(7,), dtype=int64)
tf.Tensor([8 9], shape=(2,), dtype=int64)
可以设置drop_remainder=True将最后剩余不足一个batch的数据丢弃:
dataset = tf.data.Dataset.range(10)
dataset = dataset.repeat(3).batch(7, drop_remainder=True)
for item in dataset:
print(item)
输出:
tf.Tensor([0 1 2 3 4 5 6], shape=(7,), dtype=int64)
tf.Tensor([7 8 9 0 1 2 3], shape=(7,), dtype=int64)
tf.Tensor([4 5 6 7 8 9 0], shape=(7,), dtype=int64)
tf.Tensor([1 2 3 4 5 6 7], shape=(7,), dtype=int64)
注意:数据API实例的方法不会修改原始数据,每个方法会返回一个新数据集。
也可以调用map()方法进行数据转换:
dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x * 2)
dataset = dataset.repeat(3).batch(7, drop_remainder=True)
for item in dataset:
print(item)
输出:
tf.Tensor([ 0 2 4 6 8 10 12], shape=(7,), dtype=int64)
tf.Tensor([14 16 18 0 2 4 6], shape=(7,), dtype=int64)
tf.Tensor([ 8 10 12 14 16 18 0], shape=(7,), dtype=int64)
tf.Tensor([ 2 4 6 8 10 12 14], shape=(7,), dtype=int64)
注意:对于计算密集性的转换,可以通过num_parallel_calls参数设置线程数。同时map()中传入的函数必须是可以转换成TF函数的。
map()方法将函数应用到每个样本,而apply()方法将函数应用到数据集整体:
dataset = dataset.apply(tf.data.experimental.unbatch())
dataset = dataset.filter(lambda x: x < 10) # keep only items < 10
for item in dataset.take(5):
print(item)
输出:
tf.Tensor(0, shape=(), dtype=int64)
tf.Tensor(2, shape=(), dtype=int64)
tf.Tensor(4, shape=(), dtype=int64)
tf.Tensor(6, shape=(), dtype=int64)
tf.Tensor(8, shape=(), dtype=int64)
1.2 数据洗牌(Shuffling the Data)
当训练集中的样本是独立同分布的,则梯度下降的效果最好。保证样本独立同分布的最简单的方法就是使用shuffle()方法对数据集进行洗牌操作。shuffle()操作将产生一个buffer大小的新数据集,每次取数据时从buffer中拿一个数据,并从原始数据集中拿数据补充到buffer中,直到取完原始数据集和buffer中的数据。buffer尽量设大一些,否则洗牌操作可能不充分,但不要超过RAM。
dataset = tf.data.Dataset.range(10).repeat(3)
dataset = dataset.shuffle(buffer_size=3, seed=42).batch(7)
for item in dataset:
print(item)
输出:
tf.Tensor([0 3 4 2 1 5 8], shape=(7,), dtype=int64)
tf.Tensor([6 9 7 2 3 1 4], shape=(7,), dtype=int64)
tf.Tensor([6 0 7 9 0 1 2], shape=(7,), dtype=int64)
tf.Tensor([8 4 5 5 3 8 9], shape=(7,), dtype=int64)
tf.Tensor([7 6], shape=(2,), dtype=int64)
如上输出所示,产生0-9的数据并重复3次,设置buffer大小为3,批大小为7。
repeat()方法每次也会产生新的顺序,因此是在进行测试或调试而需要固定顺序时,可以设置reshuffle_each_iteration=False。
对于无法放进内存中的大型数据集,这样洗牌的方法好像不太充分,因为buffer相比于数据集的大小就显得太小了。其中有种解决办法是对数据本身进行洗牌操作,例如可以将原始数据拆分成多个子文件,并在训练时随机读入子文件。然而子文件中的样本还是固定顺序的,这时可以同时读入多个文件,然后每次同时读这些文件(比如每个文件读一行)。这些操作都可以由TensorFlow数据API使用简单的一行代码就能完成。
1.3 多文件行数据交叉(Interleaving lines from multiple files)
加载加利福尼亚房价数据集并进行洗牌操作,拆分成训练集、验证集和测试集。
然后将每个数据集拆分成多个CSV文件:
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
housing = fetch_california_housing() # 加载数据集
X_train_full, X_test, y_train_full, y_test = train_test_split(housing.data, housing.target.reshape(-1, 1),
random_state=42)
X_train, X_valid, y_train, y_valid = train_test_split(X_train_full, y_train_full, random_state=42)
scaler = StandardScaler()
scaler.fit(X_train)
X_mean = scaler.mean_
X_std = scaler.scale_
def save_to_multiple_csv_files(data, name_prefix, header=None, n_parts=10):
housing_dir = os.path.join("datasets", "housing")
os.makedirs(housing_dir, exist_ok=True)
path_format = os.path.join(housing_dir, "my_{}_{:02d}.csv")
filepaths = []
m = len(data)
for file_idx, row_indices in enumerate(np.array_split(np.arange(m), n_parts)):
part_csv = path_format.format(name_prefix, file_idx)
filepaths.append(part_csv)
with open(part_csv, "wt", encoding="utf-8") as f:
if header is not None:
f.write(header)
f.write("\n")
for row_idx in row_indices:
f.write(",".join([repr(col) for col in data[row_idx]]))
f.write("\n")
return filepaths
train_data = np.c_[X_train, y_train]
valid_data = np.c_[X_valid, y_valid]
test_data = np.c_[X_test, y_test]
header_cols = housing.feature_names + ["MedianHouseValue"]
header = ",".join(header_cols)
train_filepaths = save_to_multiple_csv_files(train_data, "train", header, n_parts=20)
valid_filepaths = save_to_multiple_csv_files(valid_data, "valid", header, n_parts=10)
test_filepaths = save_to_multiple_csv_files(test_data, "test", header, n_parts=10)
pd.read_csv(train_filepaths[0]).head()
输出:
with open(train_filepaths[0]) as f:
for i in range(5):
print(f.readline(), end="")
输出:
MedInc,HouseAge,AveRooms,AveBedrms,Population,AveOccup,Latitude,Longitude,MedianHouseValue
3.5214,15.0,3.0499445061043287,1.106548279689234,1447.0,1.6059933407325193,37.63,-122.43,1.442
5.3275,5.0,6.490059642147117,0.9910536779324056,3464.0,3.4433399602385686,33.69,-117.39,1.687
3.1,29.0,7.5423728813559325,1.5915254237288134,1328.0,2.2508474576271187,38.44,-122.98,1.621
7.1736,12.0,6.289002557544757,0.9974424552429667,1054.0,2.6956521739130435,33.55,-117.7,2.621
train_filepaths
输出:
['datasets\\housing\\my_train_00.csv',
'datasets\\housing\\my_train_01.csv',
'datasets\\housing\\my_train_02.csv',
'datasets\\housing\\my_train_03.csv',
'datasets\\housing\\my_train_04.csv',
'datasets\\housing\\my_train_05.csv',
'datasets\\housing\\my_train_06.csv',
'datasets\\housing\\my_train_07.csv',
'datasets\\housing\\my_train_08.csv',
'datasets\\housing\\my_train_09.csv',
'datasets\\housing\\my_train_10.csv',
'datasets\\housing\\my_train_11.csv',
'datasets\\housing\\my_train_12.csv',
'datasets\\housing\\my_train_13.csv',
'datasets\\housing\\my_train_14.csv',
'datasets\\housing\\my_train_15.csv',
'datasets\\housing\\my_train_16.csv',
'datasets\\housing\\my_train_17.csv',
'datasets\\housing\\my_train_18.csv',
'datasets\\housing\\my_train_19.csv']
list_files()返回一个经过洗牌的文件路径数据集,可以设置shuffle=False表示不打散:
filepath_dataset = tf.data.Dataset.list_files(train_filepaths, seed=42)
for filepath in filepath_dataset:
print(filepath)
输出:
tf.Tensor(b'datasets\\housing\\my_train_05.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_16.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_01.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_17.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_00.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_14.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_10.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_02.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_12.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_19.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_07.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_09.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_13.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_15.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_11.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_18.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_04.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_06.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_03.csv', shape=(), dtype=string)
tf.Tensor(b'datasets\\housing\\my_train_08.csv', shape=(), dtype=string)
使用interleave()方法每次交错地从5个文件中读取数据,skip()跳过头几行:
n_readers = 5
dataset = filepath_dataset.interleave(
lambda filepath: tf.data.TextLineDataset(filepath).skip(1), cycle_length=n_readers) # 跳过第一行:表头
for line in dataset.take(5):
print(line.numpy())
输出:
b'4.5909,16.0,5.475877192982456,1.0964912280701755,1357.0,2.9758771929824563,33.63,-117.71,2.418'
b'2.4792,24.0,3.4547038327526134,1.1341463414634145,2251.0,3