第 3 步:准备数据

在将数据馈送给模型之前,需要先将数据转换为模型可以理解的格式。

首先,我们收集的数据样本可能会按特定顺序排列。我们不希望与样本排序相关的任何信息影响文本和标签之间的关系。例如,如果数据集按类别排序,然后拆分为训练/验证集,则这些集并不代表数据的整体分布情况。

要确保模型不受数据顺序影响,一种简单的最佳做法是始终在执行任何其他操作之前重排数据。如果您的数据已拆分为训练集和验证集,请确保转换验证数据的方式与转换训练数据相同。如果您没有单独的训练集和验证集,可以在重排后拆分样本;通常将 80% 的样本用于训练,将 20% 的样本用于验证。

其次,机器学习算法接受数字作为输入。这意味着我们需要将文本转换为数值向量。此过程分为两个步骤:

  1. 词元化:将文本拆分为字词或较小的子文本,以便很好地泛化文本与标签之间的关系。这决定了数据集(数据中存在的一组唯一词元)的“词汇”。

  2. 矢量化:定义一个良好的数值度量来描述这些文本的特征。

我们来看看如何针对 N 元语法向量和序列向量执行这两个步骤,以及如何使用特征选择和归一化技术优化向量表示。

N 元语法向量 [选项 A]

在后续段落中,我们将了解如何对 N 元语法模型进行标记化和矢量化。我们还将介绍如何使用特征选择和归一化技术优化 N 元语法表示法。

在 N 元语法向量中,文本表示为唯一 n 元语法的集合:n 个相邻词元的组(通常为字词)。请考虑文本 The mouse ran up the clock。在这里:

  • 一元语法 (n = 1) 是 ['the', 'mouse', 'ran', 'up', 'clock']
  • 二元语法 (n = 2) 是 ['the mouse', 'mouse ran', 'ran up', 'up the', 'the clock']
  • 此类的例子比比皆是。

词法单元化

我们发现,将词元化为一元语法 + 二元语法可实现较高的准确率,同时所需的计算时间更少。

矢量化

将文本样本拆分为 N 元语法后,我们需要将这些 n 元语法转换为可供机器学习模型处理的数值向量。以下示例显示了为两个文本生成的一元语法和二元语法分配的索引。

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'the': 7, 'mouse': 2, 'ran': 4, 'up': 10,
  'clock': 0, 'the mouse': 9, 'mouse ran': 3, 'ran up': 6, 'up the': 11, 'the
clock': 8, 'down': 1, 'ran down': 5}

将索引分配给 N 元语法后,我们通常使用以下某个选项进行矢量化。

独热编码:每个样本文本都表示为一个向量,指示文本中是否存在词法单元。

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]

计数编码:每个样本文本都表示为一个向量,指示文本中词元的计数。请注意,与一元语法“the”对应的元素现在表示为 2,因为单词“the”在文本中出现了两次。

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1]

Tf-idf 编码:上述两种方法的问题在于,所有文档中以相似频率出现的常用字词(即,数据集中文本样本并不特别特有的字词)不会受到处罚。例如,“a”之类的字词在所有文本中都会非常频繁地出现。因此,“the”的词元数比其他更有意义的字词的词元计数不是很有用。

'The mouse ran up the clock' = [0.33, 0, 0.23, 0.23, 0.23, 0, 0.33, 0.47, 0.33, 0.23, 0.33, 0.33]

(请参阅 Scikit-learn TfidfTransformer

还有许多其他向量表示,但前三种表示是最常用的。

我们发现,tf-idf 编码在准确率方面略优于其他两种编码方式(平均高出 0.25-15%),因此建议使用此方法向量化 n-gram。但请注意,它占用的内存更多(因为它使用浮点表示法),并且计算时间也更多,尤其是对于大型数据集(在某些情况下,所需的时间可能增加一倍)。

特征选择

当我们将数据集中的所有文本转换为单词“Uni+bigram”词元时,最终可能会出现数万个词元。并非所有这些词元/特征都对标签预测有贡献。因此,我们可以删除某些词元,例如在数据集中极少出现的词元。我们还可以测量特征重要性(每个词元对标签预测的影响程度),并且仅包含信息最丰富的词元。

有许多统计函数会获取特征和相应的标签并输出特征重要性得分。f_classifchi2 是两个常用的函数。我们的实验表明,这两个函数的效果一样好。

更重要的是,我们发现许多数据集的准确率最高可达到约 20000 个特征(参见图 6)。添加的特征超过此阈值的影响微乎其微,有时甚至会导致过拟合并降低性能。

Top-K 与准确率

图 6:Top K 特征与准确率。在所有数据集中,准确率达到前 2 万名左右。

规范化

归一化会将所有特征/样本值转换为较小的相似值。这简化了学习算法中的梯度下降法收敛。如我们所见,数据预处理期间的归一化似乎在文本分类问题中并没有太大价值;我们建议跳过此步骤。

以下代码整合了上述所有步骤:

  • 将文本样本词元化为单词 uni+bigrams,
  • 使用 tf-idf 编码向量化,
  • 通过舍弃出现次数少于 2 次的令牌并使用 f_classif 计算特征重要性,仅从词元向量中选择前 20,000 个特征。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import f_classif

# Vectorization parameters
# Range (inclusive) of n-gram sizes for tokenizing text.
NGRAM_RANGE = (1, 2)

# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Whether text should be split into word or character n-grams.
# One of 'word', 'char'.
TOKEN_MODE = 'word'

# Minimum document/corpus frequency below which a token will be discarded.
MIN_DOCUMENT_FREQUENCY = 2

def ngram_vectorize(train_texts, train_labels, val_texts):
    """Vectorizes texts as n-gram vectors.

    1 text = 1 tf-idf vector the length of vocabulary of unigrams + bigrams.

    # Arguments
        train_texts: list, training text strings.
        train_labels: np.ndarray, training labels.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val: vectorized training and validation texts
    """
    # Create keyword arguments to pass to the 'tf-idf' vectorizer.
    kwargs = {
            'ngram_range': NGRAM_RANGE,  # Use 1-grams + 2-grams.
            'dtype': 'int32',
            'strip_accents': 'unicode',
            'decode_error': 'replace',
            'analyzer': TOKEN_MODE,  # Split text into word tokens.
            'min_df': MIN_DOCUMENT_FREQUENCY,
    }
    vectorizer = TfidfVectorizer(**kwargs)

    # Learn vocabulary from training texts and vectorize training texts.
    x_train = vectorizer.fit_transform(train_texts)

    # Vectorize validation texts.
    x_val = vectorizer.transform(val_texts)

    # Select top 'k' of the vectorized features.
    selector = SelectKBest(f_classif, k=min(TOP_K, x_train.shape[1]))
    selector.fit(x_train, train_labels)
    x_train = selector.transform(x_train).astype('float32')
    x_val = selector.transform(x_val).astype('float32')
    return x_train, x_val

使用 N 元语法向量表示时,我们会舍弃大量有关词序和语法的信息(当 n > 1 时,我们可以保留一些部分排序信息)。这称为词袋方法。这种表示法与不考虑排序的模型结合使用,例如逻辑回归、多层感知器、梯度提升机和支持向量机。

序列向量 [选项 B]

在后续段落中,我们将了解如何对序列模型进行标记化和矢量化。我们还将介绍如何使用特征选择和归一化技术优化序列表示法。

对于某些文本样本,字词顺序对于文本的含义至关重要。例如,“我以前讨厌通勤,我的新自行车彻底改过了 只有按顺序阅读才能理解CNN/RNN 等模型可以根据样本中字词的顺序推断含义。对于这些模型,我们将文本表示为一系列词元,并保留顺序。

词法单元化

文本可以表示为一连串字符或一连串字词。我们发现,使用字词级表示法比字符词元的性能更佳。这也是行业遵循的一般规范。仅当文本存在大量拼写错误时,使用字符标记才有意义,通常情况并非如此。

矢量化

将文本样本转换为字词序列后,我们需要将这些序列转换为数值向量。以下示例展示了分配给为两个文本生成的一元语法的索引,以及将第一个文本转换为的词元索引序列。

Texts: 'The mouse ran up the clock' and 'The mouse ran down'

为每个词元分配的索引:

{'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}

注意:“the”一词出现得最频繁,因此为其分配了索引值 1。一些库会为未知令牌预留索引 0,正如这里所示。

词元索引序列:

'The mouse ran up the clock' = [1, 2, 3, 4, 1, 5]

有两种方法可用于对词元序列进行矢量化:

独热编码:序列使用 n 维空间中的字词向量表示,其中 n = 词汇量。在将词元化为字符时,这种表示法非常有效,因此词汇量很小。当我们将词元化为字词时,词汇表通常会有数万个词元,这使得独热矢量非常稀疏且效率低下。例如:

'The mouse ran up the clock' = [
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 1, 0]
]

字词嵌入:字词具有关联的含义。因此,我们可以在密集向量空间(大约几百个实数)中表示字词词元,其中字词的位置和距离表示它们在语义上的相似程度(参见图 7)。这种表示法称为字词嵌入

字词嵌入

图 7:字词嵌入

序列模型通常将这种嵌入层用作其第一层。该层学习在训练过程中将字词索引序列转换为字词嵌入向量,使每个字词索引都映射到表示该字词在语义空间中位置的实值的密集向量(参见图 8)。

嵌入层

图 8:嵌入层

特征选择

并非数据中的所有字词都参与标签预测。我们可以从词汇表中舍弃生僻字词或不相关的字词,从而优化学习过程。事实上,我们发现,使用频率最高的 2 万个特征通常就足够了。这也适用于 N 元语法模型(参见图 6)。

我们把上述所有步骤放在序列矢量化上。以下代码会执行这些任务:

  • 将文本标记化为单词
  • 使用前 20,000 个词元创建词汇
  • 将词元转换为序列向量
  • 将序列填充为固定的序列长度
from tensorflow.python.keras.preprocessing import sequence
from tensorflow.python.keras.preprocessing import text

# Vectorization parameters
# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Limit on the length of text sequences. Sequences longer than this
# will be truncated.
MAX_SEQUENCE_LENGTH = 500

def sequence_vectorize(train_texts, val_texts):
    """Vectorizes texts as sequence vectors.

    1 text = 1 sequence vector with fixed length.

    # Arguments
        train_texts: list, training text strings.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val, word_index: vectorized training and validation
            texts and word index dictionary.
    """
    # Create vocabulary with training texts.
    tokenizer = text.Tokenizer(num_words=TOP_K)
    tokenizer.fit_on_texts(train_texts)

    # Vectorize training and validation texts.
    x_train = tokenizer.texts_to_sequences(train_texts)
    x_val = tokenizer.texts_to_sequences(val_texts)

    # Get max sequence length.
    max_length = len(max(x_train, key=len))
    if max_length > MAX_SEQUENCE_LENGTH:
        max_length = MAX_SEQUENCE_LENGTH

    # Fix sequence length to max value. Sequences shorter than the length are
    # padded in the beginning and sequences longer are truncated
    # at the beginning.
    x_train = sequence.pad_sequences(x_train, maxlen=max_length)
    x_val = sequence.pad_sequences(x_val, maxlen=max_length)
    return x_train, x_val, tokenizer.word_index

标签矢量化

我们了解了如何将样本文本数据转换为数值向量。必须对标签应用类似的过程。我们可以直接将标签转换为 [0, num_classes - 1] 范围内的值。例如,如果有 3 个类,我们只需使用值 0、1 和 2 来表示它们。在内部,网络将使用独热矢量来表示这些值(以避免推断标签之间的错误关系)。这种表示法取决于我们在神经网络中使用的损失函数和最后一层激活函数。我们将在下一部分中对此进行详细介绍。