分类 TensorFlow & Keras 下的文章

视觉皮层的架构

卷积神经网络(CNN)起源于对大脑的视觉皮层的研究,从20世纪80年代起被用于图像识别。在过去几年中,由于计算机计算能力的提高、可训练数据数量的增加,以及用于深度学习网络训练技巧的增加,CNN已经在一些复杂的视觉任务中实现了超人性化,广泛用于图片搜索服务、自动驾驶汽车、自动视频分类系统等。此外,不局限于视觉感知,CNN也成功用于其他任务,比如:语言识别或自然语言处理(NLP)

David H.Hubel和Torsten Wiesel在1958年和1959年利用猫做了一系列实验(在随后的几年又利用猴子做过实验),对视觉皮层的结构提出了重要见解(该成果使作者获得了1981年的诺贝尔生理学或医学奖)。另外,他们指出视觉皮层的神经元有一个小的局部接受视野,这就意味着它们只对视野的局部内的视觉刺激做出反应。不同的神经元的接受视野有可能会重复,它们一起平铺在整个视觉区域中。

此外,他们指出一些神经元作用于图片的水平方向,而另一些神经元作用于其他方向(两个神经元可能有相同的接受视野,但是作用于不同方向)。他们也注意到有些神经元有比较大的接受视野,它们作用于由多个低阶模式组成的复杂模式。这个发现可以推测出,高阶神经元基于相邻的低阶神经元的输出(每个神经元只跟上一层的少数神经元连接)。这种强大的组织结构可以检测到视觉区域内的所有复杂模式。

这些关于视觉皮层的研究影响了1980年引入的新认知机,然后逐步演变成现在所说的卷积神经网络。一个重要的里程碑是Yann LeCun等人发表于1998年的论文。该论文介绍了广泛用于识别手写支票号码的著名LeNet-5架构。该架构除了一些广为认知的架构层,例如全全连接层和Sigmoid激活函数,还引入了两个新的架构层:卷积层和池化层

为什么不简单地使用具有全连接层的深度神经网络来执行图像识别任务呢?

尽管这对于较小的图像(例如MNIST)效果很好,但由于需要大量的参数,因此对于较大的图像无能为力。例如,一个$100\times100$像素的图像有10000个像素,如果第一层只有1000个神经元(已经严重限制了传递到下一层的信息量),则意味着总共有1000万个连接。那只是第一层。CNN使用部分连接层和权重共享解决了此问题。

TensorFlow数据集(TFDS)项目使下载通用数据集变得非常容易,从小型数据集(如MNIST或Fashion MNIST)到大型数据集(如ImageNet)。该列表包括图像数据集、文本数据集(包括翻译数据集)以及音频和视频数据集。

TFDS没有和TensorFlow捆绑在一起,因此需要安装tensorflow_datasets库。然后调用tfds.load()函数,它会下载想要的数据,并将该数据作为数据集的目录返回(通常一个用于训练,另一个用于测试,但这取决于选择的数据集)例如,下载MNIST:

import tensorflow_datasets as tfds
dataset=tfds.load(name='mnist')
mnist_train,mnist_test=dataset['train'],dataset['test']
mnist_train=mnist_train.shuffle(10000).batch(32).prefetch(1)
for item in mnist_train:
    images=item['image']
    labels=item['label']

load()函数对下载的每个数据碎片进行乱序(仅针对训练集)

训练集中的项目都是包含特征和标签的字典。但是Keras希望每个项目都是一个包含两个元素(同样是特征和标签)的元组。可以使用map()方法转换数据集:

mnist_train=mnist_train.shuffle(10000).batch(32)
mnist_train=mnist_train.map(lambda item:(item['image'],item['label']))
mnist_train=mnist_train.prefetch(1)

通过设置as_supervied=True来使load()函数执行此操作会更简单。也可以根据需要指定批处理大小。然后直接将数据传递给tf.keras模型

dataset=tfds.load(name='mnist',batch_size=32,as_surpervised=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)

预处理输入特征

为神经网络准备数据需要将所有特征转换为数值特征,通常将其归一化等。特别是如果数据包含分类特征或文本特征,则需要将它们转换为数字。
在准备数据文件时,可以使用任何喜欢的工具(例如NumPy、pandas或Scikit-Learn)提前完成此操作。或者,可以在使用Data API加载数据
时动态地与处理数据(例如使用数据集的map()方法),也在可以在模型中直接包含预处理层。

使用Lambda层实现标准化层的方法,对每个特征,它减去均值并除以其标准差(加上一个微小的平滑项,以免被零除):

import numpy as np
from tensorflow import keras

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.Sequetial([
    keras.layers.Lambda(lambda inputs: (inputs - means) / (stds + eps)),
])

使用一个自包含的自定义层,而不是像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()方法来使适应数据集并将其传递给数据样本。这样它就可以为每个特征使用适当的均值和标准差:

std_layer = Standardization()
std_layer.adapt(data_sample)

该样本必须足够大以代表数据集,但不必是完整的数据集:通常,随机选择几百个实例就足够了。接下来可以像常规一样使用此预处理层:

model = keras.models.Sequential()
model.add(std_layer)
[...]  # 创建剩下模型的层
model.compile([...])
model.fit([...])

也可以使用keras.layers.Normal标准化层,它的工作方式非常类似于自定义标准化层:首先,创建该层,然后通过将一个数据样本传递个adapt()方法使其适应你的数据集,最后正常使用该层

使用独热向量编码分类特征

在加州住房数据集中的ocean_proximity特征,它是一个具有5个可能值的分类特征:'<1H OCEAN' 'INLAND' 'NEAR OCEAN' 'NEAR BAY'和'ISLAND'。再将其提供给
神经网络之前需要对该特征进行编码。由于类别很少,可以使用独热编码。为此首先需要将每个类别映射到其索引(0到4),这可以使用查找表来完成

import tensorflow as tf

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)
  • 首先定义词汇表:这是所有可能类别的列表
  • 创建带有相应索引(0-4)的张量
  • 接下啦,为查找表创建一个初始化程序,将类别列表以及其对应的索引传递给它。在此示例中,已经有此数据所以使用KeyValueTensorInitializer。如果类别在文本文件中列出(每行一个类别),要使用TextFileInitializer
  • 在最后两行中,创建了查找表,为其初始化程序并制定了词汇表外(out-of-vocabulary,oov)桶的数量。如果查找词汇表中不存在的类别,则查找表将计算该类别的哈希并将这个位置类别分配给oov桶之中的一个。它们的索引从已知类别开始,在此示例中,两个oov桶的索引为5和6

为什么要使用oov桶?如果类别数量很大(例如邮政编码、城市、单词、产品或用户)并且数据集也很大,或者它们一直在变化,吗,而得到类别的完整列表可能不是和那方便。一种解决方法是基于数据样本(而不是整个训练集)定义词汇表,并为不在数据样本中的其他类添加一些桶。希望在训练期间找到的类别越多,就应该使用越多的oov桶。如果没有足够的oov桶,就会发生冲突:不同的类别最终会出现在同一个桶中,因此神经网络将无法区分它们

使用查找表将一小批分类特征编码为独热向量

categories = tf.constant(['NEAR BAY', 'DESERT', 'INLAND', 'INLAND'])
cat_indices = table.lookup(categories)
cat_indices
<tf.Tensor: shape=(4,), dtype=int64, numpy=array([3, 5, 1, 1], dtype=int64)>



cat_one_hot = tf.one_hot(cat_indices, depth=len(vocab) + num_oov_buckets)
cat_one_hot
<tf.Tensor: 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'被映射到两个oov桶之一(在索引5),而'INLAND'被映射到索引1两次。然后使用tf.one_hot()对这些索引进行独热编码。必须告诉该函数索引的总数,该总数等于词汇表大小加上oov桶的数量。

如果词汇表很大,则使用嵌入对它们进行编码会更加有效

使用嵌入编码分类特征

嵌入是表示类别的可训练密集向量。默认情况下,嵌入是随机初始化的,例如,'NEAT BAY'类别最初可以由[0.131,0.890]的随机向量表示,而'NEAT OCEAN'类别可以由[0.631,0.791]表示。在此示例中,使用2D嵌入,但是维度是可以调整的超参数。由于这些嵌入是可训练的,因此它们在训练过程中会逐步改善。由于它们代表的类别相当相似,梯度下降肯定最终会把它们推到接近的位置,而把它们推离'INLAND'类别的嵌入。实际上,表征越好,神经网络就越容易做出准确的预测,因此训练使嵌入成为类别的有用表征。这称为表征学习

词嵌入:

嵌入通常不仅是当前任务的有用表示,而且很多时候这些相同的嵌入可以成功地重用于其他任务。最常见的示例是词嵌入(即单个单词的嵌入):在执行自然语言处理任务时,与训练自己的词嵌入相比,重用预先训练好的词嵌入通常效果更好。

使用向量来表示词的想法可以追溯到20世纪60年代,许多复杂的技术已经被用来生成有用的向量,包括使用神经网络。但是事情真正在2013年取得了成功,当时Tomas Mikolov和其他Google研究人员发表了一篇论文,描述了一种使用神经网络学习词嵌入地有效技术,大大优于以前的尝试。这使他们能够在非常大的文本语料库上学习嵌入,法国、西班牙和意大利等于语义相关的词最终聚类在一起。

但是这不仅于邻近性有关:词嵌入还沿着嵌入空间中有意义的轴进行组织。这是一个著名的示例:如果计算King-Man+Woman(添加和减去这些单词的嵌入向量),则结果非常接近Queen单词的嵌入。换句话说,词嵌入编码了性别的概念

同样,可以计算Madrid-Spain+France,其结果接近Paris,这似乎表明首都的概念也在嵌入中进行了编码

不幸的是,词嵌入有时会捕捉最严重的偏见。例如,尽管它们正确地学习到男人是国王,女人是女王,但它们似乎也学习到了男人是医生,而女人是护士:这是一种性别歧视!确保深度学习算法的公平性是重要且活跃的研究课题

如果手动实现嵌入以了解它们的工作原理(使用一个简单的Keras层)。首先需要创建一个包含每个类别嵌入的嵌入矩阵,并随机初始化。每个类别和每个oov桶一行,每个嵌入维度都有一列:

embedding_dim = 2
embed_init = tf.random.uniform([len(vocab) + num_oov_buckets, embedding_dim])
embedding_matrix = tf.Variable(embed_init)

在此示例中,使用2D其阿奴,但是根据经验,嵌入通常有10到300个维度,具体取决于任务和词汇表
该嵌入矩阵是一个随机的7x2矩阵,存储在一个变量中(可以在训练过程中通过梯度下降对其进行调整)

embedding_matrix
<tf.Variable 'Variable:0' shape=(7, 2) dtype=float32, numpy=
array([[0.24102378, 0.67658544],
       [0.43882644, 0.7676766 ],
       [0.04228592, 0.18631208],
       [0.5599886 , 0.39891016],
       [0.47777188, 0.18932772],
       [0.8707634 , 0.6436963 ],
       [0.6798345 , 0.21071827]], dtype=float32)>


对与之前相同的分类特征进行编码,但是这次使用这些嵌入:

categories = tf.constant(['NEAR BAY', 'DESERT', 'INLAND', 'INLAND'])
cat_indices = table.lookup(categories)
cat_indices
<tf.Tensor: shape=(4,), dtype=int64, numpy=array([3, 5, 1, 1], dtype=int64)>



tf.nn.embedding_lookup(embedding_matrix, cat_indices)
<tf.Tensor: shape=(4, 2), dtype=float32, numpy=
array([[0.5599886 , 0.39891016],
       [0.8707634 , 0.6436963 ],
       [0.43882644, 0.7676766 ],
       [0.43882644, 0.7676766 ]], dtype=float32)>


tf.nn.embedding_lookup()函数以给定的索引查找在嵌入矩阵中的行,这就是它所做的全部。例如查找表中'INLAND'类别位于索引1,因此tf.nn.embeddnig_lookup()函数返回嵌入矩阵中第一行第一行的嵌入(两次)[0.43882644,0.7676766]。

Keras提供了一个keras.layers.Embedding层来处理嵌入矩阵(默认情况下是可训练的)。创建层时,它将随机初始化嵌入矩阵,然后使用某些类别索引进行调用时,它将返回嵌入矩阵中这些索引处的行:

from tensorflow import keras

embedding = keras.layers.Embedding(input_dim=len(vocab) + num_oov_buckets, output_dim=embedding_dim)
embedding(cat_indices)
<tf.Tensor: shape=(4, 2), dtype=float32, numpy=
array([[-0.0440745 ,  0.01170705],
       [ 0.04262433, -0.00528568],
       [ 0.03031978,  0.02593242],
       [ 0.03031978,  0.02593242]], dtype=float32)>


将所有内容放在一起,可以创建一个Keras模型,该模型可以处理分类特征(以及常规的数值特征)并学习每个类别(以及每个oov桶)的嵌入:

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])

该模型有两个输入:一个常规输入(每个实例包含8个数字特征),以及一个分类输入(每个实例包含一个分类特征)。它使用Lambda层查找每个类别的索引,然后查找这些索引的嵌入。接下来它将嵌入和常规输入合并起来以提供已编码的输入,这些输入准备好被馈送到神经网络。此时可以添加任何种类的神经网络

当keras.layers.TextVectorization层可用时,可以调用其adapt()方法以使其从一个数据样本中提取词汇表(它会创建查找表)。然后可以将其添加到模型中,它会执行索引查找(替换之前代码示例的Lambda层)

Keras预处理层

  • kears.layers.Normalization层——执行特征标准化(相当于前面定义的Standardization层)
  • TextVectorization层——能够将输出中的每个单词编码为它在词汇表中的索引
  • keras.layers.Discretization层,它将连续的数据切成不同的离散块,并将每个块编码成一个独热向量

Discretization层不可微分,应该仅在模型开始时使用。实际上,模型的预处理层在训练过程中冻结,因此它们的参数不受梯度下降的影响,因此不需要微分。如果希望它可训练,则不要再自定义预处理层中直接使用Embedding层:相反,应该将其单独添加到模型中

TextVectorization层还可以选择输出单词计数向量,而不是单词索引。例如,如果词汇表包含三个单词:['and','basketball','more'],则文本'more and more'将行摄到向量[1,0,2]:单词'and'出现一次,单词'basketball'根本不出现,而单词'more'出现两次。这种文本表示形式称为词袋,因为它完全失去了单词的顺序。大多数情况下,像'and'之类的常用词大多在文本中具有很大的值,即使它们通常是无趣的(例如,在'more and more basketball'文本中,'basketball'一词显然是最重要的,因为它不是一个很常见的词)。因此应该用减少常用单词重要性的方式对单词进行归一化。

一种常见的方法是将每个单词计数除以出现单词的训练实例总数的对数。此技术称为术语频率 X 反文档频率((Tern-Frequency X Inverse-Document-Frequency)(TF-IDF))。

例如,假设单词'and'、'basketball'和'more'分别出现在训练集中的200、10和100个文本实例中:在这种情况下,最终向量将为[1/log(200),0/log(10),2/log(100)]。

TFRecord格式

TFRecord格式是TensorFlow首选的格式,用于储存大量数据并有效读取数据。这是一种非常简单的二进制格式,只包含大小不同的二进制记录序列(每个记录由一个长度、一个用于检查长度是否受损的CRC校验和、实际数据
以及最后一个CRC校验和组成)。可以使用tf.io.TFRecordWriter 类轻松创建TFRecord文件

import tensorflow as tf

with tf.io.TFRecordWriter('my_data.tfrecord') as f:
    f.write(b'This is the first tfrecord')
    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 tfrecord', shape=(), dtype=string)
tf.Tensor(b'And this is the second record', shape=(), dtype=string)

默认情况下,TFRecordDataset将一个接一个地读取文件,但是可以用过num_parallel_reads使其并行读取多个文件并交织记录。另外,可以使用list_files()和interleave()得到与前面读取多个csv文件相同的结果

压缩的TFRecord文件

有时压缩TFRecord文件可能很有用,尤其是在需要通过网络连接加载它们的情况下。可以通过设置options参数来创建压缩的TFRecord文件:

options = tf.io.TFRecordOptions(compression_type='GZIP')
with tf.io.TFRecordWriter('my_compressed.tfrecord', options) as f:
    f.write(b'This is the first tfrecord')
    f.write(b'And this is the second record')
    filepaths = ['my_compressed.tfrecord']
dataset = tf.data.TFRecordDataset(filepaths, compression_type='GZIP')
for item in dataset:
    print(item)
tf.Tensor(b'This is the first tfrecord', shape=(), dtype=string)
tf.Tensor(b'And this is the second record', shape=(), dtype=string)

协议缓冲区简介

即使每个记录可以使用想要的任何二进制格式,TFRecord文件通常包含序列化地协议缓冲区(也成为protobufs)。这是一种可移植
、可拓展且高效的二进制格式,在2001年由Google开发,并于2008年开源。protobufs现在被广泛使用,尤其是在Google的远程过程调用系统gRPC中

def cube(x):
    return x**3
cube(2)
8
import tensorflow as tf
cube(tf.constant(2.0))
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>

现在用tf.function()函数将此Python函数转化为TensorFlow函数

tf_cube=tf.function(cube)
tf_cube
<tensorflow.python.eager.def_function.Function at 0x23900209dc0>

现在可以像原Python函数一样使用此TF函数,并且会返回相同的结果(但作为张量)

tf_cube(2)
<tf.Tensor: shape=(), dtype=int32, numpy=8>
tf_cube(tf.constant(2.0))
<tf.Tensor: shape=(), dtype=float32, numpy=8.0>

在后台,tf.function()分析了cube()函数执行的计算,并生成等效的计算图。另外可以使用tf.function()作为装饰器。

@tf.function
def tf_cube(x):
    return x**3

如果有需要可以通过TF函数的python_function属性使用原Python函数

tf_cube.python_function(2)
8

TensorFlow 可以优化计算图,修建未使用的节点,简化表达式(例如将1+2替换为),等等。准备好优化的图,TF函数会以适当的顺序(并在可能时并行执行)有效地执行图中的操作。因此TF通道函数通常比原始的Python函数运行得更快,尤其是在执行复杂计算的情况下。大多数情况下,不用真正了解很多:
当想要增强Python函数时,只需要将其转换为TF函数即可。

此外,编写的自定义函数、自定义损失、自定义层或任何其他自定义函数,并在Keras模型中使用它,Keras会自动将自定义函数转换为TF函数——不需要使用tf.function()。因此大多数情况,这些处理都是100%透明的。

也可以在创建自定义层或自定义模型时设置dynamic=True来告诉Keras不要将Python函数转换为TF函数。或者可以在调用模型的compile()方法时设置run_eagerly=True

默认情况下,TF函数会为每个不同的输入形状和数据类型集生成一个新图形,并将其缓存以供后续调用。例如,如果调用tf_cube(tf.constant(10))),将形状为[]的int32张量生成图形。如果调用tf_cube(tf.constant(20)),则会重用相同的图。但是如果随后调用tf_cube(tf.constant([10,20])),则会为形状为[2]的int32张量生成一个新图。这就是TF函数处理多态(即变换的参数类型和形状)的方式。但是这仅适用于张量参数:如果将Python数值传递给TF函数,则将为每个不同的值生成一个新图:例如调用tf_cube(10)和tf_cube(20)将生成两个图

如果不同的Python数值多次调用TF函数,则会生成许多图,这会降低程序的运行速度并消耗大量RAM(必须删除TF函数才能释放它)。Python值应该保留给很少有唯一值的参数,例如每层神经元的数量那样的超参数,这使TensorFlow可以更好地优化模型的每个变体。

TF函数规则

大多数情况下,将实行TensorFlow操作的Python函数转换为TF函数很简单,用@tf.function修饰它或让Keras处理。但是有一些规则需要遵守:

  • 如果调用任何外部库,包括NumPy库甚至标准库,此调用将仅在跟踪过程中运行。它不会成为图表的一部分,实际上,TensorFlow图只能包含TensorFlow构造(张量、运算、变量、数据集等)。因此需要使用tf.reduce_sum()函数代替np.sum(),使用tf.sort()代替内置的sorted()函数,以此类推。

    • 如果定义了一个返回np.random.rand()函数的TF函数f(x),则仅在跟踪该函数时才会生成随机数,因此f(tf.constant(2.))和f(tf.constant(3.))将返回相同的随机数,但f(tf.constant([2.,3.]))将返回不同的随机数。如果把np.random.rand()替换为tf.random.uniform([]),则每次操作都会生成一个新的随机数,因为该操作将成为图形的一部分
    • 如果非TensorFlow代码具有副作用(例如记录某些内容或更新Python计数器),那么每次调用TF函数不会发生这些副作用,因为它们只会在跟踪该函数时发生
    • 可以在tf.py_function()操作中包装任何Python代码,但这样会降低性能,因为TensorFlow无法对此代码进行任何图优化。这也会降低可移植性,因为该图仅可在安装了Python(并且安装了正确的库)的平台上运行
  • 可以调用其他Python函数或TF函数,但它们应遵循相同的规则,因为TensorFlow会在计算图中捕捉它们的操作。这些函数不需要@tf.function修饰。
  • 如果该函数创建了一个TensorFlow变量(或任何其他有状态的TensorFlow对象,例如数据集或队列),则必须在第一次调用时这样做,否则会得到一个异常。通常最好在TF函数(例如在自定义层的build()方法中)外部创建变量。如果要为变量分配一个新值,确保调用它的assign方法,而不要使用=运算符
  • Python函数的源代码应可用于TensorFlow。如果源代码不可用(如果,如果在Python shell中定义函数,而该函数不提供对源代码的访问权,或者仅将已编译的*.pyc Python文件部署到环境中),则生成图的过程会失败或功能受限。
  • TensorFlow只能捕获在张量或数据集上迭代的for循环,。因此确保使用for i in tf.range(x),而不是for i in range(x),否则这个循环不会在图中被捕捉。相反它会在跟踪过程中运行。
  • 出于性能原因,应尽可能使用向量化实现,而不是使用循环。