NLP中的Embedding和Padding


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)

文章作者: Kiba Amor
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 Kiba Amor !
  目录