第 3 步:准备数据

我们需要先将数据转换为模型可以理解的格式,然后才能将其提供给模型。

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

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

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

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

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

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

N-gram 矢量 [选项 A]

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

在 n-gram 矢量中,文本表示为唯一 n-gram 的集合:包含 n 个相邻令牌(通常是单词)的组。以文本 The mouse ran up the clock 为例。其中,1-gram 的单词 (n = 1) 为 ['the', 'mouse', 'ran', 'up', 'clock'],bigramn (n = 2) 的单词为 ['the mouse', 'mouse ran', 'ran up', 'up the', 'the clock'],以此类推。

词法单元化

我们发现,词元化 + 二元语法的词元化处理可以提高准确性,同时缩短计算时间。

矢量化

将文本样本拆分为 n-gram 后,我们需要将这些 n-gram 转换为我们的机器学习模型可以处理的数值向量。下面的示例显示了为两个文本生成的 1-gram 和 2-ramrami 分配的索引。

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-gram 分配索引后,我们通常使用以下选项之一进行矢量化。

独热编码:每个示例文本都表示为一个向量,表示文本中是否存在标记。

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

编码编码:每个示例文本都表示为一个矢量,表示文本中词法单元的计数。请注意,与 1-gram 对应的元素(在下方以粗体显示)对应的元素现在表示为 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] (See Scikit-learn TfidfTransformer)

还有许多其他矢量表示法,但上述三种是最常用的表示法。

我们发现 tf-idf 编码在准确率方面略胜其他(平均为 0.25-15%),因此建议使用这种方法对 n-gram 进行矢量化。但请注意,它占用更多内存(因为它使用浮点表示法),并且需要更多时间进行计算,尤其是对于大型数据集(在某些情况下,可能需要更长时间)。

功能选择

当我们将数据集中的所有文本转换为字词 uni+bigram 令牌时,我们最终可能会拥有数万个令牌。并非所有这些标记/特征都有助于标签预测。因此,我们可以舍弃某些令牌,例如在整个数据集中极其罕见的令牌。我们还可以衡量特征重要性(每个标记对标签预测的贡献程度),并且仅包含信息丰富的标记。

许多统计函数都采用特征和相应的标签并输出特征重要性得分。两个常用的函数是 f_classifchi2。我们的实验表明,这两个函数的效果都相同。

更重要的是,我们发现许多数据集的准确率达到约 20000 个特征(参见图 6)。如果在此阈值的基础上添加更多特征,几乎只会产生很小的影响,有时甚至会导致过拟合并降低性能。

排名前 K 与准确性

图 6:热门 K 特征与准确率。在各个数据集内,准确率达到约 2 万个特征的水平。

规范化

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

以下代码汇总了上述所有步骤:

  • 将文本样本分词化为单词 uni+bigrams;
  • 使用 tf-idf 编码进行矢量化,
  • 通过舍弃出现次数少于 2 次的令牌并使用 f_classif 计算特征重要性,仅从令牌矢量中选择前 20000 个特征。
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-gram 向量表示,我们会舍弃有关单词顺序和语法的大量信息(最好,当 n > 1 时,我们可以保留部分排序顺序信息)。这种方法称为词袋方法。这种表示法与不考虑排序的模型(例如逻辑回归、多层感知器、梯度增强器、支持向量机)结合使用。

序列矢量 [选项 B]

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

对于某些文本样本,字词顺序对文本含义至关重要。例如,“我曾经讨厌我的通勤。我的新自行车完全改变了” 这种新自行车只有在按顺序读取时才能理解CNN/RNN 等模型可以根据样本中字词的顺序推断出含义。对于这些模型,我们将文本表示为一系列序列,并保持顺序不变。

词法单元化

文本可以表示为一个序列或一系列单词。我们发现,使用字词级表示法比使用字符令牌效果更好。这也是行业普遍遵循的规范。只有在文本中有大量拼写错误时,使用字符令牌才有意义,通常并不会出现拼写错误。

矢量化

将文本样本转换为单词序列后,我们需要将这些序列转换为数值向量。以下示例显示了分配给两个文本的 1-gram 的索引,以及第一个文本转换为的令牌索引的序列。

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}.
NOTE: 'the' occurs most frequently, so the index value of 1 is assigned to it.
Some libraries reserve index 0 for unknown tokens, as is the case here.
Sequence of token indexes: '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:嵌入层

功能选择

并非数据中的所有字词都会影响标签预测。我们可以从词汇表中舍弃罕见或不相关的内容,从而优化学习过程。实际上,我们发现使用频率最高的 20000 个特征通常已经足够。这同样适用于 n-gram 模型(参见图 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 来表示它们。在内部,网络将使用独热矢量来表示这些值(以避免推断标签之间的错误关系)。此表示法取决于神经网络中使用的损失函数和最后一层激活函数。我们将在下一部分中详细了解相关信息。