构建数据集

构建数据集

    • 2.1 什么是数据集
    • 2.2 构建数据集的方式
      • 2.2.1 从内存数据中构建数据集
      • 2.2.2 从磁盘数据中构建数据集
        • TFRecord 数据结构
        • TFRecord 文件制作
        • TFRecord 文件读取
    • 2.3 小结

一个深度学习 AI 项目是从收集和整理数据集开始的。正如我们开始写一本书或开始最准备做一件事情,总是从收集素材收集资料开始,完成一个 AI 项目也不例外。但一般的 TensorFlow 教程都是使用之前别人已经做好的数据集。使得即使读者完全重复了教程中的实例,在面对一个新的 AI 项目时仍然 不知道如何去下手。本章主要介绍如何去构建一个深度学习 AI 项目的数据集,TensorFlow2.0 构建数据集主要用到的 tf.data 以及 tf.io 两个模块。TensorFlow2.0 中包含了很多的类和方法,下面构建数据集的方法可能不是唯一的但确实笔者亲自试验过可行的方法。正如世上的路有千千万万,不可能带着大家全都走一遍,但我可以带着大家先走通一条路。

2.1 什么是数据集

鉴于很多人不太理解什么是数据集,在介绍如何创建数据集前容笔者唠叨一下什么是数据集,只有清楚了什么数据集我们才知道如何去制作数据集。

通常人们认为我有了一堆数字、图片、视频或音频的数据后把它们放在一个文件夹里不就是一个数据集了吗?从百度上我们可以简单地查到数据集(Dataset)的定义,数据组成的集合,通常以表格的形式出现每一行表示一个成员(Member)每一列表示一个特征(Feature)。也就是说我们可以这么理解数据集,数据集是一个整理好的有一定结构的数据的集合(你可以想象成一个表格),它其中有很多成员,而每个成员又有很多特征。

除此之外,笔者还认为数据集所谓的整理好有一定结构是针对读取对象而言的。就拿一个图片数据集来举例:对于人来说,我们认为整理好的数据就是把收集到的图片放在一个文件夹里并按编号进行命名。或着对所有图片进行分类,如将植物的图片全放在一个文件夹里,再在把动物的图片放到另一个文件夹里。这样的一个数据集对于人来说是已经整理好的有一定结构的。但是将这样的数据放到 TensorFlow 面前直接让其读取原始数据,TensorFlow 必将苦不堪言。这就像把计算机整理好的认为有条理的二进制代码放到人面前,人也会认为这段代码是一堆没有条理结构的乱码一样。

从上面我们可以知道,数据集就是,根据读取的对象,将现在已有的数据组织成方便使用对象读取的、有结构的数据的集合。而对于一个 AI 项目来说,我们使用的模块是 Python 下的 TensorFlow 2.0 ,那么我们也应该将我们已经收集到的数据制作成方便 TensorFlow 2.0 进行读取的有条理的数据集合。以上便是构建数据集的基本思想。

2.2 构建数据集的方式

已经了解了什么是数据集以及构建数据集的基本思想之后,下面我们介绍几种常用的构建数据集的方法,以便满足各种 AI 项目数据集的构建。

2.2.1 从内存数据中构建数据集

首先我们来看一种最简单的情况,使用 TensorFlow 2.0 直接从内存中读取数据创建数据集。对于数据量很小的数据,我们可以先将数据从磁盘中预加载到内存中,使用 Python 的元组、列表或字典等方式进行存储。如下图所示,分别为 Python 使用三种方式组织数据。

# 元组
tuple_example = (1, 2, 3, 4, 5, 6, 7, 8, 9, 0)

# 列表
list_example = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

# 字典
dict_example = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5,
		'f': 6, 'g': 7, 'h': 8, 'i': 9, 'j': 0}

但很不幸,以上的三种形式对于 TensorFlow 来说并不友好。因为上述的三种形式虽然是 Python 自带的数据结构形式,但是当进行线性代数运算或其他科学计算时,这三种形式的数据处理起来十分麻烦,需要人为根据运算的数学形式去定义很复杂的操作。

MATLAB 对于科学计算时不同数据类型需要人为定义复杂的操作的解决方法是将自身的数据格式全部统一为向量(Vector),而 Python 对于这一问题的解决方法是引入科学计算库 numpy 将数据格式转换为 numpy.array。而 TensorFlow 继承了 MATLAB 的解决方案,将自身所有数据格式统一为张量(Tensor)。同时 TensorFlow 继承了 numpy 的优点兼容 Python 中自带的列表或 numpy 中的 numpy.array 数据格式,可以在下一个阶段通过对数据集的预处理将其转化为张量(Tensor)。

从上面的数据集的定义中,我们了解到数据集一方面需要针对读取对象给出适合的数据格式,另一方面需要有一定的数据结构如成员(Member)以及成员的特征(Feature)。在了解了数据格式后,下面我们来说一说数据集的另一方面:数据的组织结构。从张量角度来讲(也可以把它理解为一个多维的列表),一般我们常常将第 0 维用于放置各个成员(Member)其他维度用于放置成员的特征(Feature)。图 2.2为机器学习中常常用到的鸢尾花(Iris)数据集的数据结构示例。

iris_data = [
		["特征一", "特征二", "特征三", "特征四"]# 第一朵花
		["花萼长度", "花萼宽度", "花瓣长度", "花瓣宽度"], # 第二朵花
		...
		[5.1, 3.5, 1.4, 0.2] # 第 n 朵花
]

可以看到这个例子中多维列表的第 0 维存放了很多朵花。而列表的第一个维度存放了花的四个特征,分别为:花萼的长度、花萼的宽度、花瓣的长度、花瓣的宽度。像这样组织好的数据(数据格式为列表(list)/ numpy.array / 张量(Tensor)中的一种,且第 0 维存放各个成员(Member)其他维存放特征(Feature))就可以作为被 TensorFlow 读取和使用的数据集啦。

2.2.2 从磁盘数据中构建数据集

在上述的例子中,由于数据量很小我们可以直接将数据存到电脑内存中,但是当数据量很大时,我们便不方便将数据全部读取到内存中以列表的形式存放了。所幸TensorFlow为我们提供了一种高效地从磁盘读取数据的方法 TFRecord。 与传统的图片或文本等文件相比,使用 TFRecord 格式的文件存储形式可以极大的提升我们读取数据的速度(此前文章有声称读取速度比以前快 10 倍)。 下面我们分别介绍TFRecord 的数据结构、如何制作 TFRecord 文件以及如何从 TFRecord 文件中读取数据。

TFRecord 数据结构

对于 TFRecord 文件,其中的数据组织结构像俄罗斯套娃一样一层套着一层,一共有两种嵌套的方式下所示。

# TFRecord 文件嵌套结构1
Example{
	Features{
		key: Feature
		key: Feature
	}
}

# TFRecord 文件嵌套结构2
SequenceExample{
	FeatureLists{
		key: FeatureList{
				Feature
				Feature
			}
		key: FeatureList{
				Feature
				Feature
			}
	}
}

从图中我们可以看到两种嵌套方式分别为:Example-Features-Feature 以及SequenceExample-FeatureLists-FeatureList-Feature。其中 Feature 可以接受 3 种类型的数据,分别为字符串(ButesList),实数列表(FloatList)和整数列表(Int64List); Features 为一个将键名(key)和值(Feature)相互对应的字典;FeatureList 为 Feature 的列表;FeatureLists 为一个将键名(key)和值(FeatureList)想回对应的字典,如图所示为 TFRecord 各个组成成分的真实格式(为了简单起见,这里其中只展示了部分格式,读完此节如果有兴趣继续了解全部格式可以去查阅相关资料)。

message Example{
	Feature features;
};

message Features{
	map<string,Feature> feature;
};

message Feature{
	one of kind{
		BytesList bytes_list;
		FloatList float_list;
		Int64List int64_list;
	}
};

message SequenceExample{
	FeatureList featurelists;
};

message FeatureLists{
	map<string,FeatureList> featurelist;
};

message FeatureList{
	Feature[] feature;
};

TFRecord 文件制作

上一小节中,我们了解到 TFRecord 文件本质上是一种嵌套的数据结构,其中嵌套的每一层存放什么数据并没有特定的要求。但是为了方便读者的理解和区分上面的两种嵌套形式的区别,我们对其中每一层存放什么样的数据进行了强制要求,同时这样的强制要求便于后面我们从TFRecord 文件中读取数据形成数据集。

为了统一写入规范,现在我们规定写入 TFRecord 文件的内容只能是二维的列表(只有第 0 维和第 1 维)。对于列表的维度小于二维的情况,我们可以将一个元素作为一个维度,将其变为二维列表。对于维度大于二维的情况,我们可以将高维的数据展平为二维,由于这样会丢失数据的形状特征,我们可以用另外的一个列表来储存数据的形状特征。其中第 0 维和之前在数据集中的定义一致用于存放各个成员(Member)而第 1 维用于存放成员的特征(Feature)。如果特征(Feature)的维度大于一维可以将其展平为一维的列表并建立新的列表存放其形状特征。也就是说对于一个新的 AI 项目时,我们如果想要将其写入 TFRecord,那么我们要做的第一步便是将数据整理成上面描述的这种二维列表的形式。

下面的代码展示了如何将两个二维列表写入 TFRecord 文件中。对于初学者来说可能很难理解,我们先展示代码之后再慢慢逐条解析。

import tensorflow as tf

datas = [[1., 1.], [1., 2.]]
labels = [[1, 2], [1, 3]]

writer = tf.io.TFRecordWriter("test.tfrecord")
for data, label in zip(datas,labels):
    data_feature =list(map(lambda data_input: tf.train.Feature(float_list=tf.train.FloatList(value=[data_input])),data))
    label_feature=list(map(lambda label_input: tf.train.Feature(float_list=tf.train.FloatList(value=[label_input])),label))
    data_feature_list = tf.train.FeatureList(feature=data_feature)
    label_feature_list = tf.train.FeatureList(feature=label_feature)
    feature_lists = tf.train.FeatureLists(feature_list={"data":data_feature_list,
                                                        "label":label_feature_list,})
    example = tf.train.SequenceExample(feature_lists=feature_lists)
    writer.write(example.SerializeToString())
writer.close()

下面我们来对上述代码进行逐条解析。对于 TFRecord 文件,TensorFlow 提供了类 tf.io.TFRecordWriter 来对文件进行写入。tf.io.TFRecordWriter 类中的方法如下所示。

class TFRecordWriter:
    # 定义将数据写入 TFRecord 文件类
    def __init__(path, option=None):
        # 打开 path 文件 并创建一个它的 TFRecordWriter
        pass

    def __enter__(self):
        # 进入一个 with 块 打开文件
        pass
    
    def __exit__(self):
        # 退出一个 with 块 关闭文件
        pass
    
    def flush(self):
        # 刷新文件
        pass
    
    def writer(self):
        # 将字符串写入文件
        pass

从其中我们可以看到我们可以通过 init 方法实例化这个类来打开一个文件,通过 writer 方法向其中写入一些信息,最后通过 close 方法来关闭已经打开的文件。同时由于这个类中还包含 enter 以及 exit 方法,所以我们也可以通过 with as 语句来打开一个文件向其中写入信息,如下面代码所示展现了两种TFRecordWriter的使用方法。

import tensorflow as tf

# TFRecordWriter 第一种使用方法
writer = tf.io.TFRecordWriter('test.tfrecord')
writer.write(record)
writer.close()

# TFRecordWriter 第二种使用方法
with tf.io.TFRecordWriter('test.tfrecord')
    writer.write(record)

在掌握了 tf.io.TFRecord 类的使用方法之后,我们现在唯一需要关系的便是如何构建向文件中写入的 record,即如何将二维数组整理成合理的形式写入 TFRecord 文件中。对于一个二维数据,我们按照第 0 维的元素,它的成员(Member),对其进行逐个读取。每次仅读取一个成员(Member)的特征(Feature)并整理成一条 record 按顺序写入 TFRecord 文件中。

下面按照此前讲解的 TFRecord 文件的嵌套格式组织 record。若按照第一种嵌套方式 Example-Features-Feature。在这个例子中, datas 和 labels 各有两个成员其中每个成员(Member)有两个特征(Feature)。每一次循环读取 datas 和 labels 各有一个成员(Member)即 data 和 label,这两个成员一共有四个特征。由此我们可以得到下面的 TensorFlow 代码,将特征(Feature)嵌套在许多特征(Features)的字典中,并将许多特征(Features)的字典嵌套在例子(Example)中。由于向 TFRecord 文件中只能写入字符串,最后通过 Example 中
的方法 SerializedToString() 将 Example 序列化后转化为字符串,即可作为 record写入 TFRecord 文件中。

import tensorflow as tf

datas = [[1., 1.], [1., 2.]]
labels = [[1, 2], [1, 3]]

writer = tf.io.TFRecordWriter('test.tfrecord')
for data, label in zip(datas, labels):
    data_feature1 = tf.train.Feature(float_list=tf.train.FloatList(value=[data[0]]))
    data_feature2 = tf.train.Feature(float_list=tf.train.FloatList(value=[data[1]]))
    label_feature1 = tf.train.Feature(float_list=tf.train.FloatList(value=[label[0]]))
    label_feature2 = tf.train.Feature(float_list=tf.train.FloatList(value=[label[1]]))
    features = tf.train.Features(feature={'data1': data_feature1,
                                          'data2': data_feature2,
                                          'label1': label_feature1,
                                          'label2': label_feature2,})
    example = tf.train.Example(features=features)
    writer.write(example.SerializeToString())
writer.close()

同样我们也可以按照第二种嵌套方式 SequenceExample-FeatureLists-FeatureList￾Feature 来组织 record,由此我们可以得到下面的 TensorFlow 代码,将 data 中的两个特征(Feature)嵌套在一个特征列表(FeatureList)中而把 label 中的两个特征(Feature)嵌套在另一个特征列表(FeatureList)中,并将特征列表(FeatureList)嵌套在许多特征列表(FeatureLists)的字典中,此后将许多特征列表(FeatureLists)嵌套在序列例子(SequenceExample)中,最后通过 Sequence￾Example 中的方法 SeralizedToString() 将 SequenceExample 序列化后转化为字符串,即可作为 record 写入 TFRecord 文件中。

writer = tf.io.TFRecordWriter("test.tfrecord")

for data, label in zip(inverse_datas,inverse_labels):
    data_feature =list(map(lambda data_input: tf.train.Feature(float_list=tf.train.FloatList(value=[data_input])),data))
    label_feature=list(map(lambda label_input: tf.train.Feature(float_list=tf.train.FloatList(value=[label_input])),label))
    data_feature_list = tf.train.FeatureList(feature=data_feature)
    label_feature_list = tf.train.FeatureList(feature=label_feature)
    feature_lists = tf.train.FeatureLists(feature_list={"data":data_feature_list,
                                                        "label":label_feature_list,})
    example = tf.train.SequenceExample(feature_lists=feature_lists)
    writer.write(example.SerializeToString())
writer.close()

由此我们便完成了将收集到的数据制作成 TFRecord 文件的过程,下面我们将学习如何从 TFRecord 文件中读取数据构建数据集。

TFRecord 文件读取

下面的工作便是从 TFRecord 文件中读取数据形成数据集了。从上面 TFRecord 文件的制作过程中,我们可以发现 TFRecord 文件是不同的结构嵌套在一起作为 record 写入文件中的。这样的结构在读取过程中需要根据其中的结构特点来对数据进行读取。我们下面以之前创建的 test.tfrecord 为例讲解如何从 TFRecord 文件中读取数据。废话不多说啦,按老规矩先给出全部的代码下所示,最后再逐条进行解析。

import tensorflow as tf

def single_example_parser(serialized_example):
    sequence_features = {
        "data": tf.io.FixedLenSequenceFeature([],dtype=tf.float32),
        "label": tf.io.FixedLenSequenceFeature([],dtype=tf.float32),
    }
    _, sequence_parsed = tf.io.parse_single_sequence_example(serialized=serialized_example,
                                                             sequence_features=sequence_features)
    data = sequence_parsed['data']
    label = sequence_parsed['label']
    return data,label


raw_dataset=tf.data.TFRecordDataset('test.tfrecord')
dataset=raw_dataset.map(lambda x: single_example_parser(x))

for data, label in dataset:
     print(data.numpy(), label.numpy())

下面我们来对上述代码进行逐条解析。TFRecord 文件的读取最为重要的事便是对解析函数的定义 single_example_parser,随后便可以使用 map 函数批量对 TFRecord 中的数据进行解析。然而 TensorFlow 的内置函数中并没有包含这个 single_example_parser 解析函数,因为每个项目开发人员向 TFRecord 中写入的数据格式各不相同,所以无法得到一个统一的解析函数。但 TensorFlow 给我们提供了 tf.parse_single_example 和 tf.parse_single_sequence_example 两个半成品函数,使得我们可以用来构造 single_exmaple_parser 用来分别解析 Example 以及 SequenceExample。

现在只要完成解析函数的定义就好了。解析函数的定义一共分为两步,第一步是对解析字典的定义,解析字典的作用是告诉 tf.parse_single_example 和 tf.parse_single一sequence_example 两个半成品解析函数将解析的 TFRecord 文件中数据结构是什么样的。从之前对 TFRecord 文件结构的讲解中我们可以看到,
TFRecord 文件的结构通常最外层是一个字典,内部是列表的嵌套。故解析字典的定义需要说明这个字典是什么样的,有哪些键(key),以及其中的值(key)是什么形状以及什么类型。第二步便是使用半成品解析函数来得到解析出来的一个字典,并根据字典中的内容从字典中提取信息进行返回,最终得到的解析函数如下面的代码所示。

def single_example_parser(serialized_example):
    sequence_features = {
        "data": tf.io.FixedLenSequenceFeature([],dtype=tf.float32),
        "label": tf.io.FixedLenSequenceFeature([],dtype=tf.float32),
    }
    _, sequence_parsed = tf.io.parse_single_sequence_example(serialized=serialized_example,
                                                             sequence_features=sequence_features)
    data = sequence_parsed['data']
    label = sequence_parsed['label']
    return data,label

在完成了解析函数的定义之后,后续的工作便很简单了。通过 tf.data.TFRecordDataset 读取 TFRecord 文件中的原始未解析的数据。使用 map 函数将此前定义的解析函
数作用到原始未解析数据上即可得到数据集。此时的数据集可以理解为一个一维列表,其中每个元素为原来的一个成员(Member),而每个成员(Member)由一个元组(data,label)组成,我们可以通过 for 循环将dataset中的成员(Member)逐个打印出来。

2.3 小结

在这一章中我们学习了构建数据集的两种方法,从内存中读取数据构建数据集以及从磁盘中构建数据集的方法。其中虽然从磁盘中构建数据集部分对读者写入 TFRecord 文件的要求较为严格,不如要先把数据整理成二维数组,但这样的目的是为了方便读者进行理解和运用。

除了这一章的两种方法外,还有一些其他的方法如从 csv 文件中读取数据,恕笔者不能一一展示。一开始笔者已经提及,笔者只能带读者走通其中的一条路。如果读者对其他制作数据集的方法感兴趣可以自行进行学习,目的在于抛转引玉。带领读者走进深度学习 TensorFlow 的领域,之后的花花世界需要读者自行探索啦。

你可能感兴趣的:(TensorFlow)