深度学习中Keras中的Embedding层的理解与使用

原文链接: frankchen.xyz


单词嵌入提供了单词的密集表示及其相对含义,它们是对简单包模型表示中使用的稀疏表示的改进,可以从文本数据中学习字嵌入,并在项目之间重复使用。它们也可以作为拟合文本数据的神经网络的一部分来学习。

Word Embedding

单词嵌入是使用密集的矢量表示来表示单词和文档的一类方法。

词嵌入是对传统的词袋模型编码方案的改进,传统方法使用大而稀疏的矢量来表示每个单词或者在矢量内对每个单词进行评分以表示整个词汇表,这些表示是稀疏的,因为每个词汇的表示是巨大的,给定的词或文档主要由零值组成的大向量表示。

相反,在嵌入中,单词由密集向量表示,其中向量表示将单词投影到连续向量空间中。

向量空间中的单词的位置是从文本中学习的,并且基于在使用单词时围绕单词的单词。

学习到的向量空间中的单词的位置被称为它的嵌入:Embedding。

从文本学习单词嵌入方法的两个流行例子包括:

  • Word2Vec.
  • GloVe.

除了这些精心设计的方法之外,还可以将词嵌入学习作为深度学习模型的一部分。这可能是一个较慢的方法,但可以通过这样为特定数据集定制模型。

Keras Embedding Layer

Keras提供了一个嵌入层,适用于文本数据的神经网络。

它要求输入数据是整数编码的,所以每个字都用一个唯一的整数表示。这个数据准备步骤可以使用Keras提供的Tokenizer API来执行。

嵌入层用随机权重进行初始化,并将学习训练数据集中所有单词的嵌入。

它是一个灵活的图层,可以以多种方式使用,例如:

  • 它可以单独使用来学习一个单词嵌入,以后可以保存并在另一个模型中使用。
  • 它可以用作深度学习模型的一部分,其中嵌入与模型本身一起学习。
  • 它可以用来加载预先训练的词嵌入模型,这是一种迁移学习。

嵌入层被定义为网络的第一个隐藏层。它必须指定3个参数:

  • input_dim:这是文本数据中词汇的取值可能数。例如,如果您的数据是整数编码为0-9之间的值,那么词汇的大小就是10个单词;
  • output_dim:这是嵌入单词的向量空间的大小。它为每个单词定义了这个层的输出向量的大小。例如,它可能是32或100甚至更大,可以视为具体问题的超参数;
  • input_length:这是输入序列的长度,就像您为Keras模型的任何输入层所定义的一样,也就是一次输入带有的词汇个数。例如,如果您的所有输入文档都由1000个字组成,那么input_length就是1000。

例如,下面我们定义一个词汇表为200的嵌入层(例如从0到199的整数编码的字,包括0到199),一个32维的向量空间,其中将嵌入单词,以及输入文档,每个单词有50个单词。

e = Embedding(input_dim=200, output_dim=32, input_length=50)

嵌入层自带学习的权重,如果将模型保存到文件中,则将包含嵌入图层的权重。

嵌入层的输出是一个二维向量,每个单词在输入文本(输入文档)序列中嵌入一个。

如果您希望直接将Dense层接到Embedding层后面,则必须先使用Flatten层将Embedding层的2D输出矩阵平铺为一维矢量。

现在,让我们看看我们如何在实践中使用嵌入层。

学习 Embedding的例子

在本节中,我们将看看如何在文本分类问题上拟合神经网络的同时学习单词嵌入。

我们将定义一个小问题,我们有10个文本文档,每个文档都有一个学生提交的工作评论。每个文本文档被分类为正的“1”或负的“0”。这是一个简单的情感分析问题。

首先,我们将定义文档及其类别标签。

# define documents 定义文档
docs = ['Well done!',
		'Good work',
		'Great effort',
		'nice work',
		'Excellent!',
		'Weak',
		'Poor effort!',
		'not good',
		'poor work',
		'Could have done better.']
# define class labels 定义分类标签
labels = [1,1,1,1,1,0,0,0,0,0]

接下来,我们来整数编码每个文件。这意味着把输入,嵌入层将具有整数序列。我们可以尝试其他更复杂的bag of word 模型比如计数或TF-IDF。

Keras提供one_hot()函数来创建每个单词的散列作为一个有效的整数编码。我们用估计50的词汇表大小,这大大减少了hash函数的冲突概率。

# integer encode the documents 独热编码
vocab_size = 50
encoded_docs = [one_hot(d, vocab_size) for d in docs]
print(encoded_docs)
[[6, 16], [42, 24], [2, 17], [42, 24], [18], [17], [22, 17], [27, 42], [22, 24], [49, 46, 16, 34]]

这样以后序列具有不同的长度,但是Keras更喜欢输入矢量化和所有输入具有相同的长度。我们将填充所有输入序列的长度为4,同样,我们可以使用内置的Keras函数(在这种情况下为pad_sequences()函数)执行此操作,

# pad documents to a max length of 4 words 将不足长度的用0填充为长度4
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
[[ 6 16  0  0]
 [42 24  0  0]
 [ 2 17  0  0]
 [42 24  0  0]
 [18  0  0  0]
 [17  0  0  0]
 [22 17  0  0]
 [27 42  0  0]
 [22 24  0  0]
 [49 46 16 34]]

我们现在准备将我们的嵌入层定义为我们的神经网络模型的一部分。

嵌入的词汇量为50,输入长度为4,我们将选择一个8维的嵌入空间。

该模型是一个简单的二元分类模型。重要的是,嵌入层的输出将是每个8维的4个矢量,每个单词一个。我们将其平铺到一个32个元素的向量上以传递到密集输出层。

# define the model 定义模型
model = Sequential()
model.add(Embedding(vocab_size, 8, input_length=max_length))
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
# compile the model 编译
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model 打印模型信息
print(model.summary())
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
embedding_1 (Embedding)      (None, 4, 8)              400
_________________________________________________________________
flatten_1 (Flatten)          (None, 32)                0
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 33
=================================================================
Total params: 433
Trainable params: 433
Non-trainable params: 0
_________________________________________________________________

最后,我们可以拟合和评估分类模型。

# fit the model 拟合
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model 评估
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy*100))
Accuracy: 100.000000

下面是完整的代码,这里我们用函数式API改写了模型定义,不过结构和上面是完全一样的。

from keras.layers import Dense, Flatten, Input
from keras.layers.embeddings import Embedding
from keras.models import Model
from keras.preprocessing.sequence import pad_sequences
from keras.preprocessing.text import one_hot
# define documents
docs = ['Well done!',
        'Good work',
        'Great effort',
        'nice work',
        'Excellent!',
        'Weak',
        'Poor effort!',
        'not good',
        'poor work',
        'Could have done better.']
# define class labels
labels = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0]
# integer encode the documents
vocab_size = 50
encoded_docs = [one_hot(d, vocab_size) for d in docs]
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
# define the model
input = Input(shape=(4, ))
x = Embedding(vocab_size, 8, input_length=max_length)(input)
x = Flatten()(x)
x = Dense(1, activation='sigmoid')(x)
model = Model(inputs=input, outputs=x)
# compile the model
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
# fit the model
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy * 100))

之后,我们可以将嵌入图层中学习的权重保存到文件中,以便以后在其他模型中使用。

通常也可以使用这个模型来分类在测试数据集中看到的同类词汇的其他文档。

接下来,让我们看看在Keras中加载预先训练的词嵌入。

使用预训练GloVE嵌入的示例

Keras嵌入层也可以使用在其他地方学习的嵌入字。

在自然语言处理领域,学习,保存和分享提供词嵌入是很常见的。

例如,GloVe方法背后的研究人员提供了一套在公共领域许可下发布的预先训练的词嵌入。看到:

最小的包是822Mb,叫做“glove.6B.zip”。它训练了10亿个词汇(单词)的数据集,词汇量为40万字,有几种不同的嵌入矢量尺寸,包括50,100,200和300size。

您可以下载这个嵌入的集合,可以作为Keras嵌入层中训练数据集中的单词预先训练嵌入的权重。

这个例子受Keras项目中的一个例子的启发:pretrained_word_embeddings.py

下载并解压缩后,您将看到几个文件,其中一个是“glove.6B.100d.txt”,其中包含一个100维版本的嵌入。

如果你在文件内部偷看,你会看到一个token(单词),后面是每行的权重(100个数字)。例如,下面是嵌入的ASCII文本文件的第一行,显示“the”的嵌入。

the -0.038194 -0.24487 0.72812 -0.39961 0.083172 0.043953 -0.39141 0.3344 -0.57545 0.087459 0.28787 -0.06731 0.30906 -0.26384 -0.13231 -0.20757 0.33395 -0.33848 -0.31743 -0.48336 0.1464 -0.37304 0.34577 0.052041 0.44946 -0.46971 0.02628 -0.54155 -0.15518 -0.14107 -0.039722 0.28277 0.14393 0.23464 -0.31021 0.086173 0.20397 0.52624 0.17164 -0.082378 -0.71787 -0.41531 0.20335 -0.12763 0.41367 0.55187 0.57908 -0.33477 -0.36559 -0.54857 -0.062892 0.26584 0.30205 0.99775 -0.80481 -3.0243 0.01254 -0.36942 2.2167 0.72201 -0.24978 0.92136 0.034514 0.46745 1.1079 -0.19358 -0.074575 0.23353 -0.052062 -0.22044 0.057162 -0.15806 -0.30798 -0.41625 0.37972 0.15006 -0.53212 -0.2055 -1.2526 0.071624 0.70565 0.49744 -0.42063 0.26148 -1.538 -0.30223 -0.073438 -0.28312 0.37104 -0.25217 0.016215 -0.017099 -0.38984 0.87424 -0.72569 -0.51058 -0.52028 -0.1459 0.8278 0.27062

如前一节所述,第一步是定义这些示例,将它们编码为整数,然后将这些序列填充为相同的长度。

在这种情况下,我们需要能够将单词映射到整数以及整数到单词。

Keras提供了一个Tokenizer类,可以适应训练数据,通过调用Tokenizer类的texts_to_sequences()方法,可以一致地将文本转换为序列,并且可以访问单词在word_index属性中的整数字典映射。

# define documents
docs = ['Well done!',
		'Good work',
		'Great effort',
		'nice work',
		'Excellent!',
		'Weak',
		'Poor effort!',
		'not good',
		'poor work',
		'Could have done better.']
# define class labels
labels = [1,1,1,1,1,0,0,0,0,0]
# prepare tokenizer
t = Tokenizer()
t.fit_on_texts(docs)
vocab_size = len(t.word_index) + 1
# integer encode the documents
encoded_docs = t.texts_to_sequences(docs)
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)

接下来,我们需要将整个Glove字嵌入文件作为字的字典加载到内存中以嵌入数组。

# load the whole embedding into memory
embeddings_index = dict()
f = open('glove.6B.100d.txt')
for line in f:
	values = line.split()
	word = values[0]
	coefs = asarray(values[1:], dtype='float32')
	embeddings_index[word] = coefs
f.close()
print('Loaded %s word vectors.' % len(embeddings_index))

这很慢。在训练数据中过滤特殊字词的嵌入可能会更好。

接下来,我们需要为训练数据集中的每个单词创建一个嵌入矩阵。我们可以通过枚举Tokenizer.word_index中的所有唯一单词并从加载的GloVe嵌入中找到嵌入权重向量来实现这一点。

结果是一个仅用于训练期间将会看到的单词的权重矩阵。

# create a weight matrix for words in training docs
embedding_matrix = zeros((vocab_size, 100))
for word, i in t.word_index.items():
	embedding_vector = embeddings_index.get(word)
	if embedding_vector is not None:
		embedding_matrix[i] = embedding_vector

现在我们可以像以前一样定义我们的模型,并进行评估。

关键的区别是嵌入层可以用GloVe字嵌入权重来播种。我们选择了100维版本,因此必须使用output_dim将其设置为100来定义嵌入层。最后,我们不希望更新此模型中的学习单词权重,因此我们将设置模型的可训练属性为False 。

e = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=4, trainable=False)

下面列出了完整的工作示例。

from numpy import asarray
from numpy import zeros
from keras.preprocessing.text import Tokenizer
from keras.preprocessing.sequence import pad_sequences
from keras.models import Sequential
from keras.layers import Dense
from keras.layers import Flatten
from keras.layers import Embedding
# define documents
docs = ['Well done!',
		'Good work',
		'Great effort',
		'nice work',
		'Excellent!',
		'Weak',
		'Poor effort!',
		'not good',
		'poor work',
		'Could have done better.']
# define class labels
labels = [1,1,1,1,1,0,0,0,0,0]
# prepare tokenizer
t = Tokenizer()
t.fit_on_texts(docs)
vocab_size = len(t.word_index) + 1
# integer encode the documents
encoded_docs = t.texts_to_sequences(docs)
print(encoded_docs)
# pad documents to a max length of 4 words
max_length = 4
padded_docs = pad_sequences(encoded_docs, maxlen=max_length, padding='post')
print(padded_docs)
# load the whole embedding into memory
embeddings_index = dict()
f = open('../glove_data/glove.6B/glove.6B.100d.txt')
for line in f:
	values = line.split()
	word = values[0]
	coefs = asarray(values[1:], dtype='float32')
	embeddings_index[word] = coefs
f.close()
print('Loaded %s word vectors.' % len(embeddings_index))
# create a weight matrix for words in training docs
embedding_matrix = zeros((vocab_size, 100))
for word, i in t.word_index.items():
	embedding_vector = embeddings_index.get(word)
	if embedding_vector is not None:
		embedding_matrix[i] = embedding_vector
# define model
model = Sequential()
e = Embedding(vocab_size, 100, weights=[embedding_matrix], input_length=4, trainable=False)
model.add(e)
model.add(Flatten())
model.add(Dense(1, activation='sigmoid'))
# compile the model
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])
# summarize the model
print(model.summary())
# fit the model
model.fit(padded_docs, labels, epochs=50, verbose=0)
# evaluate the model
loss, accuracy = model.evaluate(padded_docs, labels, verbose=0)
print('Accuracy: %f' % (accuracy*100))

运行这个例子可能需要更长的时间,但是这表明它能够适应这个简单的问题。

[[6, 2], [3, 1], [7, 4], [8, 1], [9], [10], [5, 4], [11, 3], [5, 1], [12, 13, 2, 14]]
[[ 6  2  0  0]
 [ 3  1  0  0]
 [ 7  4  0  0]
 [ 8  1  0  0]
 [ 9  0  0  0]
 [10  0  0  0]
 [ 5  4  0  0]
 [11  3  0  0]
 [ 5  1  0  0]
 [12 13  2 14]]
Loaded 400000 word vectors.
_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
embedding_1 (Embedding)      (None, 4, 100)            1500
_________________________________________________________________
flatten_1 (Flatten)          (None, 400)               0
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 401
=================================================================
Total params: 1,901
Trainable params: 401
Non-trainable params: 1,500
_________________________________________________________________
Accuracy: 100.000000

在实践中,最好还是尝试使用预先训练好的嵌入来学习单词嵌入,因为它是固定的,并尝试在预先训练好的嵌入之上进行学习,这就类似于计算机视觉里面用预训练的VGG或者res-net迁移具体问题那样。

不过这取决于什么最适合你的具体问题。

IMDB 数据集Embedding实例

from keras.models import Sequential,Model
from keras.layers import Flatten, Dense, Embedding, Input
input_layer = Input(shape=(maxlen,)) 
x = Embedding(input_dim=10000,output_dim=8)(input_layer)
# 单独做一个embedding模型,利于后面观察
embedding = Model(input_layer,x)
x = Flatten()(x)
x = Dense(1,activation='sigmoid')(x)
model = Model(input_layer,x)
model.compile(optimizer='rmsprop',loss='binary_crossentropy',metrics=['acc'])
model.summary()
history = modhistory = modhistory = mod> history = model.fit(x_train,y_train,epochs=10,batch_size=32,validation_split=0.2)
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_4 (InputLayer)         (None, 20)                0         
_________________________________________________________________
embedding_5 (Embedding)      (None, 20, 8)             80000     
_________________________________________________________________
flatten_5 (Flatten)          (None, 160)               0         
_________________________________________________________________
dense_5 (Dense)              (None, 1)                 161       
=================================================================
Total params: 80,161
Trainable params: 80,161
Non-trainable params: 0
_________________________________________________________________
Train on 20000 samples, validate on 5000 samples
Epoch 1/10
20000/20000 [==============================] - 2s 105us/step - loss: 0.6772 - acc: 0.6006 - val_loss: 0.6448 - val_acc: 0.6704
Epoch 2/10
20000/20000 [==============================] - 2s 93us/step - loss: 0.5830 - acc: 0.7188 - val_loss: 0.5629 - val_acc: 0.7046
Epoch 3/10
20000/20000 [==============================] - 2s 95us/step - loss: 0.5152 - acc: 0.7464 - val_loss: 0.5362 - val_acc: 0.7208
Epoch 4/10
20000/20000 [==============================] - 2s 93us/step - loss: 0.4879 - acc: 0.7607 - val_loss: 0.5299 - val_acc: 0.7292
Epoch 5/10
20000/20000 [==============================] - 2s 97us/step - loss: 0.4731 - acc: 0.7694 - val_loss: 0.5290 - val_acc: 0.7334
Epoch 6/10
20000/20000 [==============================] - 2s 98us/step - loss: 0.4633 - acc: 0.7773 - val_loss: 0.5317 - val_acc: 0.7344
Epoch 7/10
20000/20000 [==============================] - 2s 96us/step - loss: 0.4548 - acc: 0.7819 - val_loss: 0.5333 - val_acc: 0.7318
Epoch 8/10
20000/20000 [==============================] - 2s 93us/step - loss: 0.4471 - acc: 0.7870 - val_loss: 0.5377 - val_acc: 0.7288
Epoch 9/10
20000/20000 [==============================] - 2s 95us/step - loss: 0.4399 - acc: 0.7924 - val_loss: 0.5422 - val_acc: 0.7278
Epoch 10/10
20000/20000 [==============================] - 2s 90us/step - loss: 0.4328 - acc: 0.7957 - val_loss: 0.5458 - val_acc: 0.7290

我们观察一下input的shape

x_train[1].shape
x_train[1]
x_train[:1].shape
x_train[:1]
(20,)
array([ 23,   4,   2,  15,  16,   4,   2,   5,  28,   6,  52, 154, 462,
        33,  89,  78, 285,  16, 145,  95], dtype=int32)
(1, 20)
array([[ 65,  16,  38,   2,  88,  12,  16, 283,   5,  16,   2, 113, 103,
         32,  15,  16,   2,  19, 178,  32]], dtype=int32)

再看看embedding的输出,

embedding.predict(x_train[:1]).shape
embedding.predict(x_train[:1])
(1, 20, 8)
array([[[-0.17401133, -0.08743777,  0.15631911, -0.06831486, -0.09105065,
          0.06253908, -0.0798945 ,  0.07671431],
        [ 0.18718374,  0.10347525, -0.06668846,  0.25818944,  0.07522523,
          0.07082067,  0.05170904,  0.22902426],
        [ 0.06872956, -0.00586612,  0.07713806, -0.00182899,  0.00882899,
         -0.18892162, -0.13580748, -0.03166043],
        [-0.01912907, -0.01732869,  0.00391375, -0.02338142,  0.02787969,
         -0.02744135,  0.0074541 ,  0.01806928],
        [ 0.20604047,  0.10910885,  0.06304865, -0.14038748,  0.12123005,
          0.06124007,  0.0532628 ,  0.17591232],
        [-0.19636872, -0.0027669 ,  0.01087157, -0.02332311, -0.04321857,
         -0.09228673, -0.03061322, -0.13376454],
        [ 0.18718374,  0.10347525, -0.06668846,  0.25818944,  0.07522523,
          0.07082067,  0.05170904,  0.22902426],
        [-0.27160701, -0.29296583,  0.1055108 ,  0.15896739, -0.24833643,
         -0.17791845, -0.27316946, -0.241273  ],
        [-0.02175452, -0.0839383 ,  0.04338101,  0.01062139, -0.11473208,
         -0.18394938, -0.05141308, -0.10405254],
        [ 0.18718374,  0.10347525, -0.06668846,  0.25818944,  0.07522523,
          0.07082067,  0.05170904,  0.22902426],
        [-0.01912907, -0.01732869,  0.00391375, -0.02338142,  0.02787969,
         -0.02744135,  0.0074541 ,  0.01806928],
        [-0.14751843,  0.05572686,  0.20332271, -0.01759946, -0.0946402 ,
         -0.14416233,  0.16961734,  0.01381243],
        [ 0.00282665, -0.17532936, -0.09342033,  0.04514923, -0.04684081,
          0.1748796 , -0.09669576, -0.10699435],
        [ 0.00225757, -0.12751001, -0.12703758,  0.17167819, -0.03712473,
          0.04252302,  0.04741228, -0.02731293],
        [ 0.02198115,  0.03989581,  0.13165356,  0.06523556,  0.14900513,
          0.01858517, -0.01644249, -0.02377043],
        [ 0.18718374,  0.10347525, -0.06668846,  0.25818944,  0.07522523,
          0.07082067,  0.05170904,  0.22902426],
        [-0.01912907, -0.01732869,  0.00391375, -0.02338142,  0.02787969,
         -0.02744135,  0.0074541 ,  0.01806928],
        [-0.01993229, -0.04436176,  0.07624088,  0.04268746, -0.00883252,
          0.00789542, -0.03039453,  0.05851226],
        [-0.12873659, -0.00083202, -0.03246918,  0.23910245, -0.24635716,
          0.10966355,  0.02079294, -0.03829115],
        [ 0.00225757, -0.12751001, -0.12703758,  0.17167819, -0.03712473,
          0.04252302,  0.04741228, -0.02731293]]], dtype=float32)

可以看出,embedding层将(1, 20)的一个输入sample(最长为20个单词的句子,其中每个单词表示为一个int数字),嵌入为一个(1, 20, 8)的向量,即将每个单词embed为一个8维的向量,而整个embedding层的参数就由神经网络学习得到,数据经过embedding层之后就方便地转换为了可以由CNN或者RNN进一步处理的格式。

参考