2025年8月

池化层

池化层的目标是对输入图像进行下采样(即缩小),以便减少计算量、内存使用量和参数数量(从而降低过拟合的风险)

池化层中的每个神经元都连接到位于一个小的矩形接受视野中的上一层中有限数量的神经元的输出。必须定义其大小、步幅和填充类型。但是,池化层没有权重。它所做的工作就是使用聚合函数(例如最大值或均值来聚合输入)

池化层通常独立地作用于每个输入通道,因此输出深度与输入深度相同

除减少计算量、内存使用量和参数数量之外,最大池化层还为小变化引入了一定程度的不变性。但是,最大池化也有一些缺点:首先,它显然是非常具有破坏性的,即使使用$2\times2$的小内核并且步幅为2,在两个方向上的输出也将减少到1/2(面积减少到1/4),丢弃了75%输入值。在某些应用中,不变性是不可取的

TensorFlow实现

在TensorFlow中实现最大池化层非常容易。以下代码使用$2\times2$内核创建一个最大池化层。步幅默认为内核大小,因此该层将使用步幅2(水平和垂直)。默认情况下,使用“valid”填充(即完全不填充):

from tensorflow import keras
max_pool = keras.layers.MaxPool2D(pool_size=2)

要创建平均池化层,只需要使用AvgPool2D而不是MaxPool2D。它的工作原理和最大池化层一样,只是它计算的是平均子而不是最大值。平均池化层曾经很受欢迎,但是现在通常使用最大池化层,因为它们通常表现更好。因为计算均值通常比计算最大值损失更少的信息,但是另外一方面,最大池化仅保留最强的特征,将所有无意义的特征都丢弃,因此下一层可获得更清晰的信号来使用。而且与平均池化相比,最大池化提供了更强的变换不变性,并且计算量略少于平均池化。

最大池化和平均池化可以沿深度维度而不是空间维度执行,尽管这不常见。这可以使CNN学习各种特征的不变性。例如,它可以学习多个滤波器,每个滤波器检测相同模式的不同旋转,并且深度最大池化层确保输出是相同的,而不考虑旋转。CNN可以类似地学会对其它任何东西的不变性:厚度、亮度、偏斜、颜色等等

Keras不包括深度最大池化层,但TensorFlow地底层深度学习API包括:只要使用tf.nn.max_pool()函数,并将内核大小和步幅指定为4元组(即大小为4的元组)。前三个值均为1:即内核大小、步幅和批量处理、高度和宽度应为1。最后一个值应为沿深度维度所需的内核大小和跨度。例如,3(这是必须输入深度的除数。如果上一层输出20个特征图,它将不起作用,因为20不是3的倍数):

import tensorflow as tf
output = tf.nn.max_pool(images, ksize=(1, 1, 1, 3),
                        strides=(1, 1, 1, 3), padding='valid')

如果要将其作为Keras模型中的一个层包括在内,将其包装在Lambda层中(或创建一个自定义的Keras层):

depth_pool = keras.layers.Lambda(lambda X: tf.nn.max_pool(
    X, ksize=(1, 1, 1, 3), strides=(1, 1, 1, 3), padding='valid'))

在现代架构中经常会看到最后一种类型的池化层是全局平均池化层。它的工作原理非常不同:它所做的是计算整个特征图的均值(这就像使用与输入有相同空间维度的池化内核的平均池化层)。这意味着它每个特征图和每个实例只输出一个单值。尽管这是极具破坏性的(特征图中的大多数信息都丢失了),但它可以用作输出层。要创建这样的层,只需要使用keras.layers.GlobalAvgPool2D类:

global_avg_pool=keras.layers.GlobalAvgPool2D()

它等效于此简单的Lambda层,该层计算空间维度(高度和宽度)上的平均值:

global_avg_pool=keras.layers.Lambda(lambda X:tf.reduce_mean(X,axis=[1,2]))

卷积层

CNN的最重要的构建层就是卷积层:第一卷积层的神经元不会连接到输入图像中的每个像素,而只于接受视野内的像素相连。反过来,第二卷积层的每个神经元仅连接到位于第一层中的一个小矩阵内的神经元。这种架构允许网络集中在第一个隐藏层的低阶特征中,然后在下一个隐藏层中将它们组装成比较高阶的特征。

之前的神经网络都具有由一长列神经元组成的层,必须将输入图像展平为一维,然后将其输入到神经网络中。在CNN中,每一层都以2D表示,这使得将神经元与其相应的输入进行匹配变得更加容易

位于给定层的第$i$行第$i$列的神经元连接到位于第i到$i+f_h-1$行、第$j$到$j+f_w-1$列的前一层中的神经元的输出,其中$f_h$和$f_w$是接受视野的高度和宽度。为了使层的高度和宽度与上一层相同,通常在输入周围添加零。这成为零填充。

可以通过隔出接受视野的方式来将较大的输入层连接到较小的层。这大大降低了模型的计算复杂度。从一个接受视野移动到另一个接受视野的距离称为步幅。位于上层第i行、第j列的神经元与位于第$i\times s_h$到$i\times s_h+f_h-1$行、第$j\times s_w$到$j\times s_w+f_w-1$列的上一层神经元输出连接,其中$s_h$和$s_w$是垂直步幅和水平步幅。

滤波器

神经元的权重可以表示为一副小图像,其大小相当于接受视野的大小。滤波器可以设置权重,来选择图像中的内容。例如,一个$7\times7$的滤波器,全为0,除了中间列(全为1))。使用这些权重的神经元将忽略接受视野中除中心垂直线以外的所有内容(因为所有输入都将乘以0,位于中心垂直线的输入除外)

堆叠多个特征图

到目前为止,每个卷积层的输出表示为2D层,但实际上卷积层具有多个滤波器并为每个滤波器输出一个特征图,因此可以更精确的以3D模式显示。它在每个特征图中每个像素有一个神经元,给定特征图中的所有神经元共享相同的参数(即相同的权重和偏置项)。不同的特征图中的神经元使用不同的参数。神经元的接受视野与之前描述的相同,但是它拓展到了所有先前层的特征。简而言之,卷积层将多个可训练滤波器同时应用于其输入,从而使其能够检测出输入中任何位置的多个特征。

特征图中所有神经元共享相同的参数,大大减少了模型中的参数数量。CNN一旦学会了在一个位置识别模式,就可以在其他任何位置识别模式。相反,常规DNN学会了在一个位置识别模式,它就只能在那个特定位置识别它。

输入图像也由多个子层组成:每个颜色一个子层。通常有三种:红色、绿色和蓝色(RGB)。灰度图像只有一个通道,但是某些图像可能具有更多通道,例如有额外光频率(例如红外)的卫星图像。

计算卷积层中神经元的输出

$$ z_{i,j,k}=b_k+\sum_{u=0}^{f_h-1}\sum_{v=0}^{f_w-1}\sum^{f_{n'}-1}_{k'=0}x_{i',j',k'}\cdot w_{u,v,k',k} $$

其中:

$$ i'=i\times s_h+u $$

$$ j'=j\times s_w+v $$

  • $Z_{i,j,k}$是位于卷积层(第$l$层)的特征图$k$中第$i$行$j$列中的神经元的输出
  • $s_h$和$s_w$是垂直步幅和水平步幅,$f_h$和$f_w$是接受视野的高度和宽度,$f_{n'}$是上一层(层$l-1$)中特征图的数量
  • $x_{i',j',k'}$是位于第$l-1$层、第$i'$行、第$j'$列,特征图$k'$(或通道$k'$,如果前一层是输入层)的神经元的输出
  • $b_k$是特征图$k$(在$l$层中)的偏置项。可以将其视为用于调整特征图$k$整体亮度的旋钮
  • $w_{u,v,k',k}$是层$l$的特征图$k$中的任何神经元与其位于$u$行、$v$列(相对于神经元的接受视野)和特征图$k$的输入之间的连接权重

TensorFlow实现

在TensorFlow中,每个输入图像通常表示为形状为[height, width, channels]的3D张量。小批量表示为形状为[min batch size, heigh, width, channels]的4D张量。卷积层的权重表示为形状为$[f_h,f_w,f_{n'},f_n]$的4D张量。卷积层的偏置项简单表示为形状$[f_n]$的一维张量。

下面使用Sickit-Learn的load_sample_image()加载两个样本图像(加载两个彩色图像:一个是中国寺庙,另一个是花朵),然后创建两个滤波器并将其应用于两个图像,最后显示其中一个结果特征图

from sklearn.datasets import load_sample_image
import tensorflow as tf

china = load_sample_image('china.jpg')/255
flower = load_sample_image('flower.jpg')/255
images = np.array([china, flower])
batch_size, height, width, channels = images.shape

filters = np.zeros(shape=(7, 7, channels, 2), dtype=np.float32)
filters[:, 3, :, 0] = 1
filters[3, :, :, 1] = 1

outputs = tf.nn.conv2d(images, filters, strides=1, padding='SAME')

plt.imshow(outputs[0, :, :, 1], cmap='gray')
plt.show
  • 每个颜色通道的像素强度表示从0到255的字节,因此我们除以255即可缩放这些特征,得到从0到1的浮点数
  • 创建两个$7\times7$的滤波器(一个在中间带有垂直白线,另一个在中间带有水平白线)
  • 使用tf.nn.conv2d()函数应用于两个图像,这是TensorFlow的底层深度学习API的一部分。使用零填充(padding='SAME')和步幅为1
  • 最后,绘制一个结果特征图

tf.nn.conv2d():

  • images是输入小批量(4D张量)
  • filter是要应用的一组滤波器(4D张量)
  • strides等于1,但也可以是抱哈四个元素的一维度组,其中两个中间元素是垂直步幅和水平步幅($s_h$和$s_w$)。第一个元素和最后一个元素必须等于1。它们可能用于指定批处理步幅(跳过某些实例)和通道步幅(跳过某些上一层的特征图或通道)
  • padding必须设置为“SAME”或“VALID”

    • 如果设置为“SAME”,则卷积层在必要时使用零填充。将输出大小设置为输入神经元数量除以步幅(向上取整)所得1的值。然后根据需要在输入周围尽可能均匀地添加零。当strides=1时,层的输入将具有与其输入相同的空间尺寸(宽度和高度),因此命名为“same”
    • 如果设置为“VALID”,则卷积层将不使用零填充,并且可能会忽略输入图像底部和右侧的某些行和列,具体取决于步幅。这意味着每个神经元的接受视野都严格位于输入内部的有效位之内(不会超出范围),因此命名为“valid”

在此示例中,手动定义了滤波器,但是在实际的CNN中,通向将滤波器定义为可训练变量,以便神经网络可以了解哪种滤波器效果最好。不用手动创建变量,使用keras.layers.Conv2D层:

from tensorflow import keras
conv=keras.layers.Conv2D(filters=32,kernel_size=3,strides=1,padding='same',activation='relu')

这段代码使用步幅为1(水平和垂直)和“same”的填充,创建了一个包含32个滤波器(每个$3\times3$)的Conv2D层,并将ReLU激活函数应用于其输出

内存需求

CNN的另一个问题是卷积层需要大量的RAM。在训练期间尤其如此,因此反向传播需要在正向传播过程中计算出的所有中间值。

例如,考虑一个带$5\times5$滤波器的卷积层,输出200个大小为$150\times100$的特征图,步幅为1,填充为“same”。如果输出是$150\times100$RGB图像(三个通道),则参数的数量为$(5\times5\times3+1)\times200=15200$(+1对应偏置项),与全连接层相比它很小。但是200个特征图中的每个特征图都包含$150\times100$个神经元,并且每个神经元都需要计算其$5\times5\times3=75$个输入的加权和:总共有2.25亿($150\times100\times75\times200=2.25e8$)个浮点乘法。

如果使用32位浮点数来表示特征图,则卷积层的输出将占用$200\times150\times100\times32=96$百万位(12MB)的RAM。

在推理期间(即对新实例进行预测时),只要计算了下一层,就可以释放由前一层占用的RAM,因此只需要两个连续层所需的RAM。但是在训练过程中,需要保留正向传播过程中的所有内容以供反向传播,因此所需的RAM量至少是所有层所需的RAM总量

如果由于内存不足而导致训练失败,则可以尝试减少批量的大小。另外可以尝试使用步幅来减少维度或删除几层。也可以尝试使用16位浮点数而不是32位浮点数。或者在多个设备上分配CNN。

视觉皮层的架构

卷积神经网络(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)]。