概要
本文内容来源于TensorFlow教程
文章内容主要分两个部分:
- 从磁盘上加载数据。
- 数据预处理成可训练数据。
但是本文主要讲数据加载,对于数据预处理只简单举了几个例子。更多关于预处理的内容参见预处理层向导和指南。
虽然本文有点长,但是内容非常简单,主要是tf.data
的几个示例,数据加载,构建包含数据预处理的深度模型,以及数据加载的缓存用法。而一些更底层的api,本文展示忽略。有兴趣的小伙伴可以阅读原教程。
内容
对于小的CSV数据集可以直接以pandas的DataFrame类型或者Numpy的Array类型加载到内存中供TensofFlow模型训练用。下面是一个相对简单的数据集:abalone数据集。该数据集小、且所有的特征都是范围限制的浮点值。
使用该数据集进行一个简单的任务:预测abalone的年龄。
import pandas as pd
import numpy as np
# make numpy values easier to read
np.set_printoptions(precision=3, suppress=True)
import tensorflow as tf
from tensorflow.keras import layers
# 加载数据集存储以DataFrame
abalone_train = pd.read_csv("https://storage.googleapis.com/download.tensorflow.org/data/abalone_train.csv",
names=["Length", "Diameter", "Height", "Whole weight", "Shucked weight", "Viscera weight", "Shell weight", "Age"])
abalone_train.head()
# 分离数据特征和标签
abalone_features = abalone_train.copy()
abalone_labels = abalone_features.pop('Age')
# 将数据特征转化成numpy的array类型,以便模型训练
abalone_features = np.array(abalone_features)
# 搭建回归模型
abalone_model = tf.keras.Sequential([
layers.Dense(64),
layers.Dense(1)
])
abalone_model.compile(loss = tf.keras.losses.MeanSquaredError(),
optimizer = tf.optimizers.Adam())
# 模型训练
abalone_model.fit(abalone_features, abalone_labels, epochs=10)
以上是最简单的方式构建模型和训练模型。我们还可以在模型构建的过程加上数据预处理操作。
# 归一化处理
normalize = layers.Normalization()
normalize.adapt(abalone_features)
norm_abalone_model = tf.keras.Sequential([
normalize,
layers.Dense(64),
layers.Dense(1)
])
norm_abalone_model.compile(loss = tf.losses.MeanSquaredError(),
optimizer = tf.optimizers.Adam())
norm_abalone_model.fit(abalone_features, abalone_labels, epochs=10)
从结果可以看出,数据归一化处理之后的模型训练的loss值更小了,模型的效果更好。
在实际应用中,数据特征的类型并不像abalone数据集一样保持一致,而是多类型的数据。比如Titanic数据集。因为不同的数据类型和数值范围,我们不能像上面的例子一样直接传给模型进行训练,需要我们做更复杂的数据处理。
这个数据预处理你可以独立进行,使用你熟悉的任何工具处理成模型可以训练的数据集,也可以使用keras preprocessing层嵌入到模型中,使数据预处理为模型的一部分。下面使用keras function api实现一个可以数据预处理的模型。
titanic = pd.read_csv("https://storage.googleapis.com/tf-datasets/titanic/train.csv")
titanic.head()
titanic_features = titanic.copy()
titanic_labels = titanic_features.pop('survived')
# Create a symbolic input
input = tf.keras.Input(shape=(), dtype=tf.float32)
# 定义输入到输出的计算逻辑,Perform a calculation using the input
result = 2*input + 1
calc = tf.keras.Model(inputs=input, outputs=result)
# 测试calc
print(calc(1).numpy()) # 3
print(calc(2).numpy()) # 5
# 处理数据的每个特征
inputs = {}
for name, column in titanic_features.items():
dtype = column.dtype
if dtype == object:
dtype = tf.string
else:
dtype = tf.float32
inputs[name] = tf.keras.Input(shape=(1,), name=name, dtype=dtype)
# 将所有数字类型的输入连接起来进行正则化。
numeric_inputs = {name:input for name,input in inputs.items()
if input.dtype==tf.float32}
x = layers.Concatenate()(list(numeric_inputs.values()))
norm = layers.Normalization()
norm.adapt(np.array(titanic[numeric_inputs.keys()]))
all_numeric_inputs = norm(x)
# 保存已处理的数字类型数据
preprocessed_inputs = [all_numeric_inputs]
# 对于string类型的数据处理,使用one-hot编码
for name, input in inputs.items():
if input.dtype == tf.float32:
continue
# 生成lookup
lookup = layers.StringLookup(vocabulary=np.unique(titanic_features[name]))
# 进行类别独热编码
one_hot = layers.CategoryEncoding(num_tokens=lookup.vocabulary_size())
x = lookup(input)
x = one_hot(x)
preprocessed_inputs.append(x)
preprocessed_inputs_cat = layers.Concatenate()(preprocessed_inputs)
titanic_preprocessing = tf.keras.Model(inputs, preprocessed_inputs_cat)
tf.keras.utils.plot_model(model = titanic_preprocessing , rankdir="LR", dpi=72, show_shapes=True)
这个模型仅仅包含输入数据预处理,你可以运行它,看它对数据做了哪些操作。Keras模型不能自动转化Pandas的DataFrame数据,因为如果它不清楚数据应该转化成一个张量还是一个张量字典。以下代码是构建一个包含数据预处理的模型,可以保存、加载重复使用的模型。
# 将数据转化成一个张量字典
titanic_features_dict = {name: np.array(value)
for name, value in titanic_features.items()}
features_dict = {name:values[:1] for name, values in titanic_features_dict.items()}
titanic_preprocessing(features_dict)
# 构建模型,
def titanic_model(preprocessing_head, inputs):
body = tf.keras.Sequential([
layers.Dense(64),
layers.Dense(1)
])
preprocessed_inputs = preprocessing_head(inputs)
result = body(preprocessed_inputs)
model = tf.keras.Model(inputs, result)
model.compile(loss=tf.losses.BinaryCrossentropy(from_logits=True),
optimizer=tf.optimizers.Adam())
return model
titanic_model = titanic_model(titanic_preprocessing, inputs)
titanic_model.fit(x=titanic_features_dict, y=titanic_labels, epochs=10)
titanic_model.save('test')
reloaded = tf.keras.models.load_model('test')
features_dict = {name:values[:1] for name, values in titanic_features_dict.items()}
before = titanic_model(features_dict)
after = reloaded(features_dict)
assert (before-after)<1e-3
print(before)
print(after)
以上方法在模型训练时依赖模型内置的数据变换和批处理,但是如果你需要更多的控制数据通道或者不想数据被简单地加载到内存。可以使用tf.data
tf.data
import itertools
# 自定义函数获取特征的index
def slices(features):
for i in itertools.count():
# For each feature take index `i`
example = {name:values[i] for name, values in features.items()}
yield example
for example in slices(titanic_features_dict):
for name, value in example.items():
print(f"{name:19s}: {value}")
break
# 利用tf.data中的api获取数据特征的index
features_ds = tf.data.Dataset.from_tensor_slices(titanic_features_dict)
for example in features_ds:
for name, value in example.items():
print(f"{name:19s}: {value}")
break
# 使用tf.data处理数据和训练数据
titanic_ds = tf.data.Dataset.from_tensor_slices((titanic_features_dict, titanic_labels))
titanic_batches = titanic_ds.shuffle(len(titanic_labels)).batch(32)
titanic_model.fit(titanic_batches, epochs=5)
加载单个文件,tf.data
对于构建数据通道具有高扩展性,处理加载csv文件需要很少的接口。
titanic_file_path = tf.keras.utils.get_file("train.csv", "https://storage.googleapis.com/tf-datasets/titanic/train.csv")
"""
这个函数包含很多方面的特性,以致更简单的处理数据:
1、使用列头(columns)作为字典的key;
2、自动决定每列的类型
"""
titanic_csv_ds = tf.data.experimental.make_csv_dataset(
titanic_file_path,
batch_size=5, # Artificially small to make examples easier to show.
label_name='survived',
num_epochs=1,
ignore_errors=True,)
"""
【注意】如果你跑两次下面的代码,可能得到不同的结果。因此make_csv_dataset函数包含了shuffle_buffer_size=1000。
"""
for batch, label in titanic_csv_ds.take(1):
for key, value in batch.items():
print(f"{key:20s}: {value}")
print()
print(f"{'label':20s}: {label}")
此函数还可以读取压缩文件:
traffic_volume_csv_gz = tf.keras.utils.get_file(
'Metro_Interstate_Traffic_Volume.csv.gz',
"https://archive.ics.uci.edu/ml/machine-learning-databases/00492/Metro_Interstate_Traffic_Volume.csv.gz",
cache_dir='.', cache_subdir='traffic')
# 设置压缩类型:compression_type="GZIP"
traffic_volume_csv_gz_ds = tf.data.experimental.make_csv_dataset(
traffic_volume_csv_gz,
batch_size=256,
label_name='traffic_volume',
num_epochs=1,
compression_type="GZIP")
for batch, label in traffic_volume_csv_gz_ds.take(1):
for key, value in batch.items():
print(f"{key:20s}: {value[:5]}")
print()
print(f"{'label':20s}: {label[:5]}")
缓存机制
解析csv数据需要一些开销,对于小模型这可能是模型训练的瓶颈。
根据你的案例,使用Dataset.cache
或者data.experimental.snapshot
,可以使得csv数据只在模型训练第一轮解析一次。
这Dataset.cache
和data.experimental.snapshot
的主要不同点在于cache
文件能仅仅在TensorFlow过程创建它们时被使用,而snapshot
文件能在其他过程中被读取。
例如:迭代traffic_volume_csv_gz_ds
20次没有使用缓存需要15s,而使用缓存只用了2s。
%%time
# 没有使用cache
for i, (batch, label) in enumerate(traffic_volume_csv_gz_ds.repeat(20)):
if i % 40 == 0:
print('.', end='')
print()
"""
CPU times: user 16.6 s, sys: 2.72 s, total: 19.4 s
Wall time: 13.2 s
"""
%%time
# 使用cache
caching = traffic_volume_csv_gz_ds.cache().shuffle(1000)
for i, (batch, label) in enumerate(caching.shuffle(1000).repeat(20)):
if i % 40 == 0:
print('.', end='')
print()
"""
CPU times: user 2.54 s, sys: 154 ms, total: 2.7 s
Wall time: 5.13 s
"""
上面的代码要【注意】:Dataset.cache
存储数据在第一轮,后面重复使用。因此在通道中使用.cache
不能有任何的shuffle,shuffle需要在cache之后,如上面的代码。从结果可以看出,使用cache之后快了很多。
%%time
snapshot = tf.data.experimental.snapshot('titanic.tfsnap')
snapshotting = traffic_volume_csv_gz_ds.apply(snapshot).shuffle(1000)
for i, (batch, label) in enumerate(snapshotting.shuffle(1000).repeat(20)):
if i % 40 == 0:
print('.', end='')
print()
"""
CPU times: user 4.09 s, sys: 433 ms, total: 4.53 s
Wall time: 10.7 s
"""
data.experimental.snapshot
是一个临时的数据存储,这个文件格式被认为是内部细节,在TensorFlow版本之间不能保证。
如果你的csv文件数据加载很慢,使用
cache
和snapshot
是不够的,需要考虑重新编码你的数据为流格式(consider re-encoding your data into a more streamlined format)
多文件
fonts_zip = tf.keras.utils.get_file(
'fonts.zip', "https://archive.ics.uci.edu/ml/machine-learning-databases/00417/fonts.zip",
cache_dir='.', cache_subdir='fonts',
extract=True)
import pathlib
font_csvs = sorted(str(p) for p in pathlib.Path('fonts').glob("*.csv"))
font_csvs[:10]
"""
file_pattern 文件路径:fonts文件夹下的所有csv文件
num_parallel_reads 同时读文件的个数
"""
fonts_ds = tf.data.experimental.make_csv_dataset(
file_pattern = "fonts/*.csv",
batch_size=10, num_epochs=1,
num_parallel_reads=20,
shuffle_buffer_size=10000)
# 查看数据内部结构
for features in fonts_ds.take(1):
for i, (name, value) in enumerate(features.items()):
if i>15:
break
print(f"{name:20s}: {value}")
print('...')
print(f"[total: {len(features)} features]")