预处理输入特征

预处理输入特征

为神经网络准备数据需要将所有特征转换为数值特征,通常将其归一化等。特别是如果数据包含分类特征或文本特征,则需要将它们转换为数字。
在准备数据文件时,可以使用任何喜欢的工具(例如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)]。

posted @ 2021-10-29 19:31  里列昂遗失的记事本  阅读(120)  评论(0编辑  收藏  举报