咱们接着这个系列的上一篇文章继续:

政安晨:【深度学习处理实践】(六)—— RNN的高级用法政安晨:【深度学习处理实践】(七)—— 文本数据预处理-LMLPHPhttps://blog.csdn.net/snowdenkeke/article/details/136660644

在深度学习中,文本数据预处理是指将原始文本数据转换为可供模型训练使用的向量表示。

首先,需要对文本进行分词,将一个句子或段落分解为词汇单元。中文分词相对于英文分词更具挑战性,因为中文中没有像空格这样明显的分隔符。常用的中文分词工具包括jieba、pkuseg等。

然后,需要构建词汇表。词汇表是将所有出现的词汇按照一定的顺序进行编号,同时还可以设置一些特殊标记比如"<UNK>"表示未知词汇。可以使用统计的方法构建词汇表,根据词频进行排序,选取出现频率最高的词汇。

接下来,将分词后的文本映射为词汇表中的编号。每个词汇对应一个唯一的编号,将文本中的每个词汇替换为其对应的编号,形成一个词汇序列。

然后,需要对词汇序列进行统一长度的处理。由于深度学习模型对输入的长度有要求,需要将所有的词汇序列调整为相同的长度,可以通过填充或截断的方法进行处理。

最后,将处理后的词汇序列转换为向量表示。可以通过one-hot编码将每个词汇的编号转换为一个稀疏向量,也可以使用词嵌入(word embedding)将每个词汇映射为一个固定长度的实数向量,词嵌入可以通过预训练的方式得到,也可以随机初始化并在训练过程中进行优化。

以上就是深度学习中对中文文本数据进行预处理的一般步骤,通过这些步骤可以将文本数据转换为模型可以处理的向量表示,进而进行模型的训练和预测。

咱们接下来具体讲解。



政安晨:【深度学习处理实践】(七)—— 文本数据预处理-LMLPHP


自然语言处理概述

在计算机科学领域,我们将人类语言(如英语或普通话)称为“自然”语言,以区别于为机器设计的语言(如汇编语言、LISP或XML)。

每一种机器语言都是设计出来的人类工程师写下一组正式规则,描述用这种语言可以编写哪些语句以及这些语句的含义。规则在先,只有这组规则是完备的,人们才会开始使用这种语言。

人类语言正好相反:使用在先,规则出现在后。自然语言是进化形成的,就像生物体一样,这是我们称其为“自然语言”的原因。自然语言的“规则”(比如英语语法)是在事后确立的,而且经常被使用者忽视或破坏。因此,虽然机器可读语言是严格、高度结构化的,使用明确的句法规则将固定词表中准确定义的概念组合在一起,但自然语言是模糊、混乱、不断扩展的,并且还在不断变化。

创建出能够理解自然语言的算法,是一件很重要的事情:语言(特别是文本)是我们大多数交流与文化的基础。互联网上大多是文本。

语言是我们存储几乎所有知识的方式。我们的思想在很大程度上建立在语言之上。

但长期以来,理解自然语言的能力一直是机器所无法实现的。有人曾天真地认为,可以简单地写出“英语规则集”,就像写出LISP规则集一样。

因此,早期人们在尝试构建自然语言处理(natural language processing,NLP)系统时,都是从“应用语言学”的视角进行的。

直到20世纪90年代,手动编写规则一直都是主流方法。

因此,在20世纪80年代末,人们开始将机器学习方法应用于自然语言处理。

最早的方法是基于决策树,其目的是自动开发先前系统中的那种if/then/else规则。随后,从logistic回归开始,统计学方法开始加速发展。随着时间的推移,参数学习模型完全占据主导地位,语言学被看作一种障碍而不是有用的工具。早期的语音识别专家Frederick Jelinek在20世纪90年代开玩笑说:“每次我解雇一名语言学家,语音识别的性能都会提高一些。”

这就是现代自然语言处理利用机器学习和大型数据集,让计算机可以不对语言进行理解(这是一个更崇高的目标),而是接收一段语言作为输入,并返回一些有用的内容,比如预测以下内容:

你训练的文本处理模型并不会像人类一样理解语言;相反,模型只是在输入数据中寻找统计规律。

事实证明,这足以在许多简单任务上表现很好。计算机视觉是应用于像素的模式识别,与此类似,自然语言处理是应用于单词、句子和段落的模式识别。

2015年初,人们刚刚开始对RNN重新产生巨大的兴趣。Keras提供了第一个开源、易于使用的LSTM实现。在此之前只有“研究代码”,无法被轻易复用。然后从2015年~2017年,RNN主导了蓬勃发展的自然语言处理领域,特别是双向LSTM模型,它在许多重要的任务(从摘要到问题回答再到机器翻译)上达到了最先进的水平。

在2017年~2018年,一种新的架构取代了RNN,它就是Transformer,咱们以后会介绍它。Transformer在很短时间内就在整个领域取得了巨大进展,如今大多数自然语言处理系统是基于Transformer的。

准备文本数据

深度学习模型是可微函数,只能处理数值张量,不能将原始文本作为输入。文本向量化是指将文本转换为数值张量的过程。

文本向量化有许多种形式,但都遵循相同的流程,如下图所示:

首先将文本标准化,使其更容易处理,比如转换为小写字母或删除标点符号。

然后将文本拆分为单元[称为词元(token)],比如字符、单词或词组。这一步叫作词元化。最后将每个词元转换为一个数值向量。这通常需要首先对数据中的所有词元建立索引。

我们来详细看一下每个步骤。

政安晨:【深度学习处理实践】(七)—— 文本数据预处理-LMLPHP

文本标准化

我们来看下面这两个句子。

两句话的意思相同:“日落来临。我凝视着墨西哥的天空。大自然难道不美好吗?

两个句子非常相似——事实上,它们几乎完全相同。

然而,如果将它们转换成字节串,会得到非常不同的表示,因为“i”和“I”是不同的字符,“Mexico”和“México”是不同的单词,“Isnt”不同于“Isn't”,等等。

机器学习模型不会预先知道“i”和“I”是同一个字母、“é”是带有重音符的“e”,以及“staring”和“stared”是同一个动词的两种形式。

文本标准化是一种简单的特征工程,旨在消除你不希望模型处理的那些编码差异。它不是机器学习所特有的,如果你想搭建一个搜索引擎,那么也需要做同样的事情。

最简单也是最广泛使用的一种标准化方法是:将所有字母转换为小写并删除标点符号。这样前面的两个句子就会变为:

两个句子更加相似了。

另一种常见的变换是将特殊字符转换为标准形式,比如将“é”转换为“e”、将“æ”转换为“ae”等。这样一来,词元“méxico”就会转换为“mexico”。

文本拆分(词元化)

完成文本标准化之后,你需要将文本拆分成能够向量化的单元(词元),这一步叫作词元化。

词元化有以下3种方法:

单词级词元化(word-level tokenization):词元是以空格(或标点)分隔的子字符串。这种方法的一个变体是将部分单词进一步拆分成子词,比如将“staring”拆分成“star+ing”,或者将“called”拆分成“call+ed”。

N 元语法词元化(N-gram tokenization):词元是N个连续单词,比如“the cat”或“he was”都是二元语法词元。

字符级词元化(character-level tokenization):每个字符都是一个词元。我们在实践中很少采用这种方法,只有在专门的领域才会用到,比如文本生成或语音识别。

一般来说,你可以一直使用单词级词元化或N元语法词元化。

有两种文本处理模型:

如果要构建序列模型,则应使用单词级词元化;如果要构建词袋模型,则应使用N元语法词元化。N元语法可以手动向模型注入少量局部词序信息。

咱们将介绍这两种模型及其使用场景。

理解N元语法和词袋

单词N元语法是从一个句子中提取的N个(或更少)连续单词的集合。这一概念中的“单词”也可以替换为“字符”。

下面来看一个简单的例子。对于句子“The cat sat on the mat”(猫坐在垫子上),它可以分解为以下二元语法的集合。

{"the", "the cat", "cat", "cat sat", "sat",
 "sat on", "on", "on the", "the mat", "mat"}

这个句子也可以分解为以下三元语法的集合。

{"the", "the cat", "cat", "cat sat", "the cat sat",
 "sat", "sat on", "on", "cat sat on", "on the",
 "sat on the", "the mat", "mat", "on the mat"}

这样的集合分别叫作二元语法袋(bag-of-2-grams)和三元语法袋(bag-of-3-grams)。

袋(bag)这一术语指的是,我们处理的是词元组成的集合,而不是列表或序列,也就是说,词元没有特定的顺序。这种词元化方法叫作词袋(bag-of-words)或N元语法袋(bag-of-N-grams)。

词袋是一种不保存顺序的词元化方法(生成的词元是一个集合,而不是一个序列,舍弃了句子的总体结构),因此它通常用于浅层的语言处理模型,而不是深度学习模型。

提取N元语法是一种特征工程,深度学习序列模型不需要这种手动方法,而是将其替换为分层特征学习。

一维卷积神经网络、RNN和Transformer都可以通过观察连续的单词序列或字符序列来学习单词组或字符组的数据表示,而无须明确知道这些组的存在。

建立词表索引

将文本拆分成词元之后,你需要将每个词元编码为数值表示。你可以用无状态的方式来执行此操作,比如将每个词元哈希编码为一个固定的二进制向量,但在实践中,你需要建立训练数据中所有单词(“词表”)的索引,并为词表中的每个单词分配唯一整数,如下所示:

vocabulary = {}
for text in dataset:
    text = standardize(text)
    tokens = tokenize(text)
    for token in tokens:
        if token not in vocabulary:
            vocabulary[token] = len(vocabulary)

然后,你可以将这个整数转换为神经网络能够处理的向量编码,比如one-hot向量。

def one_hot_encode_token(token):
    vector = np.zeros((len(vocabulary),))
    token_index = vocabulary[token]
    vector[token_index] = 1
    return vector

请注意,这一步通常会将词表限制为训练数据中前20 000或30 000个最常出现的单词。任何文本数据集中往往都包含大量独特的单词,其中大部分只出现一两次。对这些罕见词建立索引会导致特征空间过大,其中大部分特征几乎没有信息量。

咱们以前在IMDB数据集上训练了第一个深度学习模型,还记得吗?你使用的数据来自keras.datasets.imdb,它已经经过预处理转换为整数序列,其中每个整数代表一个特定单词。当时我们设置num_words=10000,其目的就是将词表限制为训练数据中前10 000个最常出现的单词。

这里有一个不可忽略的重要细节:当我们在词表索引中查找一个新的词元时,它可能不存在。

你的训练数据中可能不包含“cherimoya”一词的任何实例(也可能是你将它从词表中去除了,因为它太罕见了),所以运行token_index =vocabulary["cherimoya"]可能导致KeyError。

要处理这种情况,你应该使用“未登录词”(out of vocabulary,缩写为OOV)索引,以涵盖所有不在索引中的词元。OOV的索引通常是1,即设置token_index =vocabulary.get(token, 1)。将整数序列解码为单词时,你需要将1替换为“[UNK]”之类的词(叫作“OOV词元”)。

你可能会问:“为什么索引是1而不是0?”这是因为0已经被占用了。

有两个特殊词元你会经常用到:OOV词元(索引为1)和掩码词元(mask token,索引为0)。OOV词元表示“这里有我们不认识的一个单词”,掩码词元的含义则是“别理我,我不是一个单词”。你会用掩码词元来填充序列数据:因为数据批量需要是连续的,一批序列数据中的所有序列必须具有相同的长度,所以需要对较短的序列进行填充,使其长度与最长序列相同。

如果你想用序列[5, 7, 124, 4, 89]和[8, 34,21]生成一个数据批量,那么它应该是这个样子:

[[5,  7, 124, 4, 89]
 [8, 34,  21, 0,  0]]

咱们以前所使用的IMDB数据集也使用了这种方法,用0对整数序列批量进行填充。

使用TextVectorization层

到目前为止的每一个步骤都很容易用纯Python实现。你可以写出如下所示的代码:

import string

class Vectorizer:
    def standardize(self, text):
        text = text.lower()
        return "".join(char for char in text
                       if char not in string.punctuation)

    def tokenize(self, text):
        text = self.standardize(text)
        return text.split()

    def make_vocabulary(self, dataset):
        self.vocabulary = {"": 0, "[UNK]": 1}
        for text in dataset:
            text = self.standardize(text)
            tokens = self.tokenize(text)
            for token in tokens:
                if token not in self.vocabulary:
                    self.vocabulary[token] = len(self.vocabulary)
        self.inverse_vocabulary = dict(
            (v, k) for k, v in self.vocabulary.items())

    def encode(self, text):
        text = self.standardize(text)
        tokens = self.tokenize(text)
        eturn [self.vocabulary.get(token, 1) for token in tokens]

    def decode(self, int_sequence):
        return " ".join(
            self.inverse_vocabulary.get(i, "[UNK]") for i in int_sequence)

vectorizer = Vectorizer()
dataset = [
    "I write, erase, rewrite",
    "Erase again, and then",
    "A poppy blooms.",
]
vectorizer.make_vocabulary(dataset)

以上代码的演绎效果如下:

政安晨:【深度学习处理实践】(七)—— 文本数据预处理-LMLPHP

但是,这种做法不是很高效。

在实践中,我们会使用Keras的TextVectorization层。它快速高效,可直接用于tf.data管道或Keras模型中。

TextVectorization层的用法如下所示:

from tensorflow.keras.layers import TextVectorization
text_vectorization = TextVectorization(
    # 设置该层的返回值是编码为整数索引的单词序列。还有其他几种可用的输出模式,稍后会看到其效果
    output_mode="int",
)

默认情况下,TextVectorization层的文本标准化方法是“转换为小写字母并删除标点符号”,词元化方法是“利用空格进行拆分”。

但重要的是,你也可以提供自定义函数来进行标准化和词元化,这表示该层足够灵活,可以处理任何用例。

请注意,这种自定义函数的作用对象应该是tf.string张量,而不是普通的Python字符串。

例如,该层的默认效果等同于下列代码:

import re
import string
import tensorflow as tf

def custom_standardization_fn(string_tensor):
    # 将字符串转换为小写字母
    lowercase_string = tf.strings.lower(string_tensor)
    # 将标点符号替换为空字符串
    return tf.strings.regex_replace(
        lowercase_string, f"[{re.escape(string.punctuation)}]", "")

def custom_split_fn(string_tensor):
    # 利用空格对字符串进行拆分
    return tf.strings.split(string_tensor)

text_vectorization = TextVectorization(
    output_mode="int",
    standardize=custom_standardization_fn,
    split=custom_split_fn,
)

要想对文本语料库的词表建立索引,只需调用该层的adapt()方法,其参数是一个可以生成字符串的Dataset对象或者一个由Python字符串组成的列表。

dataset = [
    "I write, erase, rewrite",
    "Erase again, and then",
    "A poppy blooms.",
]
text_vectorization.adapt(dataset)

请注意,你可以利用get_vocabulary()来获取得到的词表,如下代码所示:

text_vectorization.get_vocabulary()

政安晨:【深度学习处理实践】(七)—— 文本数据预处理-LMLPHP

对于编码为整数序列的文本,如果你需要将其转换回单词,那么这种方法很有用。词表的前两个元素是掩码词元(索引为0)和OOV词元(索引为1)。词表中的元素按频率排列,所以对于来自现实世界的数据集,“the”或“a”这样非常常见的单词会排在前面。

作为演示,我们对一个例句进行编码,然后再解码:

vocabulary = text_vectorization.get_vocabulary()
test_sentence = "I write, rewrite, and still rewrite again"
encoded_sentence = text_vectorization(test_sentence)
print(encoded_sentence)
inverse_vocab = dict(enumerate(vocabulary))
decoded_sentence = " ".join(inverse_vocab[int(i)] for i in encoded_sentence)
print(decoded_sentence)

演绎如下:

政安晨:【深度学习处理实践】(七)—— 文本数据预处理-LMLPHP

在tf.data管道中使用TextVectorization层或者将TextVectorization层作为模型的一部分:

TextVectorization层有两种用法。第一种用法是将其放在tf.data管道中,如下所示:

# string_dataset是一个能够生成字符串张量的数据集
int_sequence_dataset = string_dataset.map(
    text_vectorization,
    # num_parallel_calls参数的作用是在多个CPU内核中并行调用map()
    num_parallel_calls=4)

第二种用法是将其作为模型的一部分(毕竟它是一个Keras层),如下所示:

# 创建输入的符号张量,数据类型为字符串
text_input = keras.Input(shape=(), dtype="string")

# 对输入应用文本向量化层
vectorized_text = text_vectorization(text_input)

# (本行及以下2行)你可以继续添加新层,就像普通的函数式API模型一样
embedded_input = keras.layers.Embedding(...)(vectorized_text)
output = ...
model = keras.Model(text_input, output)

两种用法之间有一个重要区别:

如果向量化是模型的一部分,那么它将与模型的其他部分同步进行。

这意味着在每个训练步骤中,模型的其余部分(在GPU上运行)必须等待TextVectorization层(在CPU上运行)的出准备好,才能开始工作。

与此相对,如果将该层放在tf.data管道中,则可以在CPU上对数据进行异步预处理:模型在GPU上对一批向量化数据进行处理时,CPU可以对下一批原始字符串进行向量化。

因此,如果在GPU或TPU上训练模型,你可能会选择第一种用法,以获得最佳性能。

咱们的所有实例都会使用这种方法。但如果在CPU上训练,那么同步处理也可以:无论选择哪种方法,内核利用率都会达到100%。

接下来,如果想将模型导出到生产环境中,你可能希望导出一个接收原始字符串作为入的模型(类似上面第二种用法的代码片段),否则,你需要在生产环境中(可能是JavaScript)重新实现文本标准化和词元化,可能会引入较小的预处理偏差,从而降低模型精度。

值得庆幸的是,TextVectorization层可以将文本预处理直接包含在模型中,使其更容易部署,即使一开始将该层用在tf.data管道中也是如此。


至此您已经掌握了文本预处理的全部知识,以后咱们就可以准备构建模型了。

03-17 16:47