1. Embedding
在 自然语言处理(Natural Language Processing,NLP) 过程中,神经网络的输入一般是一段句子,句子有一个一个的字组成。
在输入神经网络之前,需要将这些字进行编码,常规的编码方式比如 One-hot 是有多少种字,编码出来的向量就有多长,这会导致编码后的数据特别的大,从而导致对应的神经网络接收的输入也特别多的大,不便于训练。除了编码后的数据大这个缺点外,one-hot 编码还有一个更重要的缺点就是没法表达词与词关系,比如猫和狗之间的关系,显然要比猫和电脑之间的关系近,这点 one-hot 无法表达出来。
所以在 NLP 中常用的编码方式是 Embedding,其本质是把一个数用几个不同的数字来表示,而不是像 One-hot 那样,只用一个 1 来表示。
比如,我们有 50000
个数字范围在 [0-1000)
之间的数字,需要编码:
import numpy as np
nums = np.random.randint(0, 1000, 50000)
nums.shape # (50000, )
使用 One-hot 编码后的大小为:
one_hot = np.eye(1000)[nums]
one_hot.shape # (50000, 1000)
而使用 Embedding 编码后的大小为(假设 embedding 的长度为 10)后的:
data = np.random.rand(50000, 10)
embedding = data[nums]
embedding.shape # (50000, 10)
2. Padding
在 NLP 中,Embedding 只是解决了编码后向量特别大的问题,依然没有解决句子长度不同的问题。这是就需要 Padding。
Padding 的本质就是定一个句子长度 N
,然后把待训练的数据中句子长于 N 的句子截断到 N 长度,把短于 N 的句子使用一个特殊的字填充到长度 N 。
3. Embedding 和 Padding 简单的实现
import numpy as np
import torch
from tensorflow import convert_to_tensor, keras
# 待处理的文本
text = (
"""
The clock is running.
Make the most of today.
Time waits for no man.
Yesterday is history.
Tomorrow is a mystery.
Today is a gift.
That's why it is called the present.
""".lower()
.strip("\n ")
.replace(".", "")
.replace("'s", " is")
)
def make_word_index(text, base):
"""把一段文本中的单词以空格为分隔符分开,生成一个{单词,ID}的字段对应表
Args:
text (str): 待处理的文本
base (int): 起始ID
Returns:
dict: {单词,ID}对应表
"""
words = list(set(text.replace("\n", " ").split(" ")))
words.sort()
word2id = {}
for id, word in enumerate(words, base):
word2id[word] = id
return word2id
def pad_or_truncate(some_list, target_len, pad_value):
"""把列表截断或补齐到指定的长度,如果长度不足,则以指定的补齐
Args:
some_list (list[int])): 待处理的列表
target_len (int): 指定的长度
pad_value (int): 补齐时用到的值
Returns:
list[int]: 处理后的列表
"""
return some_list[:target_len] + [pad_value] * (target_len - len(some_list))
def process_text(word2id, seq_len, pad_value, text):
"""预处理文本,将文本以行为单位分成句子,每个句子中的单词根据字典替换成ID,并将句子截断或补齐到指定的长度
Args:
word2id (dict[str, int]): {单词,ID}字典
seq_len (int): 句子的长度
pad_value (int): 补齐时用到的值
text (str)): 待处理的问题
Returns:
list[list[int]]: 处理后的包含句子的列表
"""
sentences = [sentence.split(" ") for sentence in text.split("\n")]
sentences_ids = [[word2id[word] for word in sentence] for sentence in sentences]
sentences_ids = [pad_or_truncate(sentence_ids, seq_len, pad_value) for sentence_ids in sentences_ids]
return sentences_ids
def embedding_sentences_ids(word_num, embedding_len, sentences_ids):
"""embedding句子
Args:
word_num (int): 单词类型的总数
embedding_len (int): embedding的长度
sentences_ids (list[list[int]]): 包含句子的列表
Returns:
np.ndarray: embedding的矩阵
np.ndarray: 句子embedding后的值
"""
embeddings = np.random.rand(word_num, embedding_len)
return embeddings, np.array([embeddings[sentence_ids] for sentence_ids in sentences_ids])
PAD_TEXT = "<PAD>" # 补齐时使用的单词
PAD_INDEX = 0 # 补齐时使用的值
SENTENCE_LEN = 6 # 处理后句子的长度(更长的截断,更短的补齐)
# 根据文本生成,单词=>ID的表,其中ID有一个偏移,用于存放补齐单词
word2id = make_word_index(text=text, base=1)
# 放入把补齐的单词
word2id[PAD_TEXT] = PAD_INDEX
# 生成句子
sentences_ids = process_text(word2id, SENTENCE_LEN, PAD_INDEX, text)
# 总的单词种类
word_num = len(word2id)
# embedding的长度
embedding_len = 10
# 使用自己实现的函数embedding句子
embeddings, embedding_sentences = embedding_sentences_ids(len(word2id), embedding_len, sentences_ids)
print(embedding_sentences.shape) # (7, 6, 10)
# 使用pytorch内置的Embedding来embedding句子
torch_embedding = torch.nn.Embedding.from_pretrained(torch.tensor(embeddings))
torch_embedding_sentences = torch_embedding(torch.tensor(sentences_ids))
print(torch_embedding_sentences.shape) # torch.Size([7, 6, 10])
# 确保两种方式生成的结果是相同的
assert np.allclose(embedding_sentences, torch_embedding_sentences.numpy())
# 使用keras内置的Embedding来embedding句子
keras_embedding = keras.layers.Embedding(
word_num, embedding_len, embeddings_initializer=keras.initializers.Constant(embeddings)
)
keras_embedding_sentences = keras_embedding(convert_to_tensor(sentences_ids))
print(keras_embedding_sentences.shape)
# 确保两种方式生成的结果是相同的
assert np.allclose(embedding_sentences, keras_embedding_sentences.numpy())
4. 使用 Keras 中的 Embedding 来训练 IMDB 预测模型
from tensorflow.keras import layers, models
from tensorflow.keras.datasets import imdb
from tensorflow.keras.preprocessing import sequence
num_words = 1000
embedding_dim = 16
maxlen = 80
batch_size = 32
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=num_words)
x_train = sequence.pad_sequences(x_train, maxlen=maxlen, padding="post")
x_test = sequence.pad_sequences(x_test, maxlen=maxlen, padding="post")
model = models.Sequential(
[
layers.Embedding(num_words, embedding_dim, input_length=maxlen),
layers.GlobalAveragePooling1D(),
layers.Dense(64, activation="relu"),
layers.Dense(1, activation="sigmoid"),
]
)
model.summary()
model.compile(optimizer="adam", loss="binary_crossentropy", metrics=["accuracy"])
model.fit(x_train, y_train, epochs=30, batch_size=batch_size, validation_split=0.2)
score, acc = model.evaluate(x_test, y_test, batch_size=batch_size)
print("Test score:", score)
print("Test accuracy:", acc)