用于深度学习的文本预处理方法

633 阅读10分钟

深度学习的文本预处理方法

了解与任何自然语言处理(NLP)问题有关的深度学习神经网络的预处理步骤的各种观点。

深度学习,尤其是自然语言处理(NLP),如今已经聚集了巨大的兴趣。前段时间,Kaggle上有一个NLP比赛,叫做Quora问题不真诚性挑战。该比赛是一个文本分类问题,在通过比赛,以及通过Kaggle专家提出的宝贵内核的工作后,它变得更容易理解。

首先,我们先来解释一下比赛中的文本分类问题。

文本分类是自然语言处理中的一项常见任务,它将一个不确定长度的文本序列转变为一个文本类别。你怎么能用这个?

  • 为了找到评论的情绪
  • 在Facebook这样的平台上找到有毒的评论
  • 在Quora上查找不真诚的问题--这是Kaggle上目前正在进行的一项比赛
  • 查找网站上的虚假评论
  • 弄清一个文本广告是否会被点击的问题

现在,这些问题中的每一个都有一些共同点。从机器学习的角度来看,这些问题本质上都是一样的,只是目标标签发生了变化,其他的都没有。也就是说,增加业务知识可以帮助这些模型更加稳健,这也是我们在对测试分类的数据进行预处理时想要加入的内容。

虽然我们在这篇文章中关注的预处理管道主要以深度学习为中心,但其中大部分也适用于传统机器学习模型。

首先,在经历所有步骤之前,让我们先了解一下文本数据的深度学习管道的流程,以便对整个过程有一个更高层次的看法。

我们通常从清理文本数据和执行基本EDA开始。在这里,我们试图通过清理数据来提高我们的数据质量。我们还试图通过去除OOV(词汇外)词来提高我们的Word2Vec嵌入的质量。这前两个步骤之间通常没有什么顺序,我们一般在这两个步骤之间来回走动。

接下来,我们为文本创建一个可以输入深度学习模型的表示。然后,我们开始创建我们的模型并训练它们。最后,我们使用适当的指标对模型进行评估,并获得各自股东的批准,以部署我们的模型。如果这些术语现在没有什么意义,请不要担心。我们将尝试通过本文的过程来解释它们。

在这个路口,让我们绕个小弯,谈一谈词嵌入。在为我们的深度学习模型预处理数据时,我们将不得不考虑它们。

词嵌入的入门知识

我们需要有一种方法来表示词汇中的单词。一种方法是使用词向量的一热编码,但这并不是一个好的选择。其中一个主要原因是一热词向量不能准确表达不同词之间的相似性,如余弦相似性。

鉴于一热编码向量的结构,不同词之间的相似度总是以0的形式出现。另一个原因是,随着词汇量的增加,这些一热编码的向量变得非常大。

Word2Vec通过为我们提供一个固定长度的词的向量表示,以及捕捉不同词之间的相似性和类比关系来克服上述困难。

Word2vec的单词向量是以这样的方式学习的,它允许我们学习不同的类比。它使我们能够对词进行以前不可能的代数操作。比如说。什么是国王--男人+女人?结果是女王。

Word2Vec向量还可以帮助我们找出单词之间的相似性。如果我们试图寻找与 "good "相似的词,我们会发现awesome, great等。正是word2vec的这一特性,使其在文本分类中具有无价之宝。现在,我们的深度学习网络理解了 "好 "和 "伟大 "本质上是具有相似含义的词。

**因此,在非常简单的术语中,word2vec为单词创建了向量。因此,我们对字典中的每个词(常见的大词也是)都有一个d维向量。**我们通常使用预先训练好的词向量,这些词向量是别人在对维基百科、Twitter等大型文本进行训练后提供给我们的。最常用的预训练词向量是Glove和Fasttext的300维词向量。我们将在这篇文章中使用Glove。

文本数据的基本预处理技术

在大多数情况下,我们观察到文本数据并不完全干净。来自不同来源的数据有不同的特点,这使得文本预处理成为分类管道中最重要的步骤之一。

例如,来自Twitter的文本数据与Quora或一些新闻/博客平台的文本数据完全不同,因此需要区别对待。幸运的是,我们将在这篇文章中讨论的技术对于你在NLP的丛林中可能遇到的任何类型的数据都是通用的。

清理特殊字符和删除标点符号

我们的预处理管道在很大程度上取决于我们将用于分类任务的word2vec嵌入。原则上,我们的预处理应该与训练词嵌入前的预处理相匹配。由于大多数嵌入不提供标点符号和其他特殊字符的向量值,所以你要做的第一件事就是把文本数据中的特殊字符去掉。这些是Quora问题数据中存在的一些特殊字符,我们使用替换函数来去除这些特殊字符。

# 一些预处理,这将是你将看到的所有文本分类方法所共有的。

Python

puncts = [',', '.', '"', ':', ')', '(', '-', '!', '?', '|', ';', "'", '$', '&', '/', '[', ']', '>', '%', '=', '#', '*', '+', '\\', '•',  '~', '@', '£',  '·', '_', '{', '}', '©', '^', '®', '`',  '<', '→', '°', '€', '™', '›',  '♥', '←', '×', '§', '″', '′', ' ', '█', '½', 'à', '…',  '“', '★', '”', '–', '●', 'â', '►', '−', '¢', '²', '¬', '░', '¶', '↑', '±', '¿', '▾', '═', '¦', '║', '―', '¥', '▓', '—', '‹', '─',  '▒', ':', '¼', '⊕', '▼', '▪', '†', '■', '’', '▀', '¨', '▄', '♫', '☆', 'é', '¯', '♦', '¤', '▲', 'è', '¸', '¾', 'Ã', '⋅', '‘', '∞',  '∙', ')', '↓', '、', '│', '(', '»', ',', '♪', '╩', '╚', '³', '・', '╦', '╣', '╔', '╗', '▬', '❤', 'ï', 'Ø', '¹', '≤', '‡', '√', ]

Python

def clean_text(x):    x = str(x)    for punct in puncts:        if punct in x:            x = x.replace(punct, '')    return x

这也可以借助于一个简单的regex来完成。但是我们通常喜欢上面的方法,因为它有助于理解我们要从数据中删除的那种字符。

蟒蛇

def clean_text(x):    pattern = r'[^a-zA-z0-9\s]'    text = re.sub(pattern, '', x)    return x

清理数字

为什么我们要用#号替换数字?因为大多数嵌入的文本都是这样预处理的。

**Python的小技巧。**我们在下面的代码中使用一个if语句来事先检查文本中是否存在数字。这是因为if总是比re.sub 命令快,而且我们的大部分文本都不包含数字。

蟒蛇

def clean_numbers(x):    if bool(re.search(r'\d', x)):        x = re.sub('[0-9]{5,}', '#####', x)        x = re.sub('[0-9]{4}', '####', x)        x = re.sub('[0-9]{3}', '###', x)        x = re.sub('[0-9]{2}', '##', x)    return x

去除错别字

找出数据中的错别字总是有帮助的。由于这些词的嵌入不存在于word2vec中,我们应该用它们的正确拼写来替换单词,以获得更好的嵌入覆盖。

下面的代码工件是对Peter Norvig的拼写检查器的改编。它使用word2vec 词的排序来近似词的概率,因为Googleword2vec 显然是按照训练语料库中的频率递减顺序来排序。你可以用它来找出你所掌握的数据中的一些拼写错误的词。

这来自于Quora问题相似性挑战中的CPMP脚本。

Python

import re from collections import Counter import gensim import heapq from operator import itemgetter from multiprocessing import Pool 
model = gensim.models.KeyedVectors.load_word2vec_format('../input/embeddings/GoogleNews-vectors-negative300/GoogleNews-vectors-negative300.bin',                                                         binary=True) words = model.index2word 
w_rank = {} for i,word in enumerate(words):    w_rank[word] = i 
WORDS = w_rank 
def words(text): return re.findall(r'\w+', text.lower()) 
def P(word):     "Probability of `word`."    # use inverse of rank as proxy    # returns 0 if the word isn't in the dictionary    return - WORDS.get(word, 0) 
def correction(word):     "Most probable spelling correction for word."    return max(candidates(word), key=P) 
def candidates(word):     "Generate possible spelling corrections for word."    return (known([word]) or known(edits1(word)) or known(edits2(word)) or [word]) 
def known(words):     "The subset of `words` that appear in the dictionary of WORDS."    return set(w for w in words if w in WORDS) 
def edits1(word):    "All edits that are one edit away from `word`."    letters    = 'abcdefghijklmnopqrstuvwxyz'    splits     = [(word[:i], word[i:])    for i in range(len(word) + 1)]    deletes    = [L + R[1:]               for L, R in splits if R]    transposes = [L + R[1] + R[0] + R[2:] for L, R in splits if len(R)>1]    replaces   = [L + c + R[1:]           for L, R in splits if R for c in letters]    inserts    = [L + c + R               for L, R in splits for c in letters]    return set(deletes + transposes + replaces + inserts) 
def edits2(word):     "All edits that are two edits away from `word`."    return (e2 for e1 in edits1(word) for e2 in edits1(e1)) 
def build_vocab(texts):    sentences = texts.apply(lambda x: x.split()).values    vocab = {}    for sentence in sentences:        for word in sentence:            try:                vocab[word] += 1            except KeyError:                vocab[word] = 1    return vocab 
vocab = build_vocab(train.question_text) 
top_90k_words = dict(heapq.nlargest(90000, vocab.items(), key=itemgetter(1))) 
pool = Pool(4) corrected_words = pool.map(correction,list(top_90k_words.keys())) 
for word,corrected_word in zip(top_90k_words,corrected_words):    if word!=corrected_word:        print(word,":",corrected_word)

一旦我们找到了拼写错误的数据,接下来的事情就是使用拼写错误的映射和regex函数来替换它们。

Python

mispell_dict = {'colour': 'color', 'centre': 'center', 'favourite': 'favorite', 'travelling': 'traveling', 'counselling': 'counseling', 'theatre': 'theater', 'cancelled': 'canceled', 'labour': 'labor', 'organisation': 'organization', 'wwii': 'world war 2', 'citicise': 'criticize', 'youtu ': 'youtube ', 'Qoura': 'Quora', 'sallary': 'salary', 'Whta': 'What', 'narcisist': 'narcissist', 'howdo': 'how do', 'whatare': 'what are', 'howcan': 'how can', 'howmuch': 'how much', 'howmany': 'how many', 'whydo': 'why do', 'doI': 'do I', 'theBest': 'the best', 'howdoes': 'how does', 'mastrubation': 'masturbation', 'mastrubate': 'masturbate', "mastrubating": 'masturbating', 'pennis': 'penis', 'Etherium': 'Ethereum', 'narcissit': 'narcissist', 'bigdata': 'big data', '2k17': '2017', '2k18': '2018', 'qouta': 'quota', 'exboyfriend': 'ex boyfriend', 'airhostess': 'air hostess', "whst": 'what', 'watsapp': 'whatsapp', 'demonitisation': 'demonetization', 'demonitization': 'demonetization', 'demonetisation': 'demonetization'}

语法

def _get_mispell(mispell_dict):    mispell_re = re.compile('(%s)' % '|'.join(mispell_dict.keys()))    return mispell_dict, mispell_re 
mispellings, mispellings_re = _get_mispell(mispell_dict) def replace_typical_misspell(text):    def replace(match):       return mispellings[match.group(0)]    return mispellings_re.sub(replace, text) 
# Usage replace_typical_misspell("Whta is demonitisation") 

删除缩略语

缩略词是指我们用一撇一捺来写的词。缩略词的例子是 "ain't "或 "aren't "这样的词。由于我们想使我们的文本标准化,扩展这些缩略词是有意义的。下面我们使用缩略语映射和regex函数来完成这一工作。

Python

contraction_dict = {"ain't": "is not", "aren't": "are not","can't": "cannot", "'cause": "because", "could've": "could have", "couldn't": "could not", "didn't": "did not",  "doesn't": "does not", "don't": "do not", "hadn't": "had not", "hasn't": "has not", "haven't": "have not", "he'd": "he would","he'll": "he will", "he's": "he is", "how'd": "how did", "how'd'y": "how do you", "how'll": "how will", "how's": "how is",  "I'd": "I would", "I'd've": "I would have", "I'll": "I will", "I'll've": "I will have","I'm": "I am", "I've": "I have", "i'd": "i would", "i'd've": "i would have", "i'll": "i will",  "i'll've": "i will have","i'm": "i am", "i've": "i have", "isn't": "is not", "it'd": "it would", "it'd've": "it would have", "it'll": "it will", "it'll've": "it will have","it's": "it is", "let's": "let us", "ma'am": "madam", "mayn't": "may not", "might've": "might have","mightn't": "might not","mightn't've": "might not have", "must've": "must have", "mustn't": "must not", "mustn't've": "must not have", "needn't": "need not", "needn't've": "need not have","o'clock": "of the clock", "oughtn't": "ought not", "oughtn't've": "ought not have", "shan't": "shall not", "sha'n't": "shall not", "shan't've": "shall not have", "she'd": "she would", "she'd've": "she would have", "she'll": "she will", "she'll've": "she will have", "she's": "she is", "should've": "should have", "shouldn't": "should not", "shouldn't've": "should not have", "so've": "so have","so's": "so as", "this's": "this is","that'd": "that would", "that'd've": "that would have", "that's": "that is", "there'd": "there would", "there'd've": "there would have", "there's": "there is", "here's": "here is","they'd": "they would", "they'd've": "they would have", "they'll": "they will", "they'll've": "they will have", "they're": "they are", "they've": "they have", "to've": "to have", "wasn't": "was not", "we'd": "we would", "we'd've": "we would have", "we'll": "we will", "we'll've": "we will have", "we're": "we are", "we've": "we have", "weren't": "were not", "what'll": "what will", "what'll've": "what will have", "what're": "what are",  "what's": "what is", "what've": "what have", "when's": "when is", "when've": "when have", "where'd": "where did", "where's": "where is", "where've": "where have", "who'll": "who will", "who'll've": "who will have", "who's": "who is", "who've": "who have", "why's": "why is", "why've": "why have", "will've": "will have", "won't": "will not", "won't've": "will not have", "would've": "would have", "wouldn't": "would not", "wouldn't've": "would not have", "y'all": "you all", "y'all'd": "you all would","y'all'd've": "you all would have","y'all're": "you all are","y'all've": "you all have","you'd": "you would", "you'd've": "you would have", "you'll": "you will", "you'll've": "you will have", "you're": "you are", "you've": "you have"}

Python

def _get_contractions(contraction_dict):    contraction_re = re.compile('(%s)' % '|'.join(contraction_dict.keys()))    return contraction_dict, contraction_re 
contractions, contractions_re = _get_contractions(contraction_dict) 
def replace_contractions(text):    def replace(match):        return contractions[match.group(0)]    return contractions_re.sub(replace, text) 
# Usage replace_contractions("this's a text with contraction")

除了上述技术外,还有其他的文本预处理技术,如词干化、词组化和去掉止损词。由于这些技术并不与深度学习NLP模型一起使用,所以我们在这里不谈这些。

表征 序列创建

使得深度学习成为NLP的 "首选 "的原因之一是,我们其实不必从文本数据中手工设计特征。深度学习算法将文本序列作为输入,像人类一样学习文本的结构。由于机器无法理解文字,他们希望以数字形式来表达他们的数据。因此,我们希望将文本数据表示为一系列的数字。

为了了解如何做到这一点,我们需要了解一下Keras标记器函数。人们可以使用任何其他的标记器,但Keras标记器是一个受欢迎的选择。

标记器

简单地说,标记器是一个将句子分割成单词的实用函数。keras.preprocessing.text.Tokenizer ,将文本标记(分割)成tokens(单词),同时只保留文本语料库中出现最多的单词。

Python

#Signature: Tokenizer(num_words=None, filters='!"#$%&()*+,-./:;<=>?@[\\]^_`{|}~\t\n', lower=True, split=' ', char_level=False, oov_token=None, document_count=0, **kwargs)

num_words 参数只保留文本中预先指定的词数。这很有帮助,因为我们不希望我们的模型通过考虑那些出现频率很低的词而得到大量的噪音。在现实世界的数据中,我们使用num_words参数留下的大多数词通常是拼写错误的。标记器还默认过滤一些不需要的标记,并将文本转换为小写。

一旦与数据拟合,标记器也会保留一个词的索引(词的字典,我们可以用它来给一个词分配一个唯一的数字),可以通过访问。

tokenizer.word_index

索引的字典中的词按频率排列。

因此,使用tokenizer的整个代码如下。

Python

from keras.preprocessing.text import Tokenizer ## Tokenize the sentences tokenizer = Tokenizer(num_words=max_features) tokenizer.fit_on_texts(list(train_X)+list(test_X)) train_X = tokenizer.texts_to_sequences(train_X) test_X = tokenizer.texts_to_sequences(test_X)

其中train_Xtest_X 是语料库中的文档列表。

填充序列

通常情况下,我们的模型希望每个序列(每个训练例子)都有相同的长度(相同的单词/符号数量)。我们可以使用maxlen 参数来控制这一点。

比如说。

Python

train_X = pad_sequences(train_X, maxlen=maxlen) test_X = pad_sequences(test_X, maxlen=maxlen)

现在我们的训练数据包含一个数字的列表。每个列表都有相同的长度。我们还有word_index ,这是一个在文本语料库中出现最多的词的字典。

嵌入充实

如上所述,我们将使用GLoVEWord2Vec 嵌入来解释丰富性。GLoVE的预训练向量是在维基百科的语料库上训练的。

这意味着一些可能出现在你的数据中的词可能不会出现在嵌入中。我们可以如何处理这个问题呢?让我们首先加载Glove Embeddings。

Python

def load_glove_index():    EMBEDDING_FILE = '../input/embeddings/glove.840B.300d/glove.840B.300d.txt'    def get_coefs(word,*arr): return word, np.asarray(arr, dtype='float32')[:300]    embeddings_index = dict(get_coefs(*o.split(" ")) for o in open(EMBEDDING_FILE))    return embeddings_index 
glove_embedding_index = load_glove_index()

一定要把你下载这些GLoVE向量的文件夹的路径。

这个glove_embedding_index 包含什么?它只是一个字典,其中的键是单词,值是单词向量,一个长度为300的np.array 。这个字典的长度大约是10亿。由于我们只想要我们的word_index 中的词的嵌入,我们将创建一个只包含所需嵌入的矩阵。

tokenizer.word 文本预处理的索引

Python

def create_glove(word_index,embeddings_index):    emb_mean,emb_std = -0.005838499,0.48782197    all_embs = np.stack(embeddings_index.values())    embed_size = all_embs.shape[1]    nb_words = min(max_features, len(word_index))    embedding_matrix = np.random.normal(emb_mean, emb_std, (nb_words, embed_size))    count_found = nb_words    for word, i in tqdm(word_index.items()):       if i >= max_features: continue       embedding_vector = embeddings_index.get(word)       if embedding_vector is not None:             embedding_matrix[i] =  embedding_vector       else:                count_found-=1    print("Got embedding for ",count_found," words.")    return embedding_matrix

上面的代码工作得很好,但是否有一种方法可以让我们利用GLoVE中的预处理来发挥我们的优势?

是的。当对手套进行预处理时,创作者没有将单词转换为小写字母。这意味着它包含一个词的多种变化,如'USA'、'usa'和'Usa'。这也意味着,在某些情况下,虽然像'Word'这样的单词出现了,但它的小写类似物,即'word'却没有出现。

我们可以通过使用下面的代码来解决这种情况。

蟒蛇

def create_glove(word_index,embeddings_index):    emb_mean,emb_std = -0.005838499,0.48782197    all_embs = np.stack(embeddings_index.values())    embed_size = all_embs.shape[1]    nb_words = min(max_features, len(word_index))    embedding_matrix = np.random.normal(emb_mean, emb_std, (nb_words, embed_size))     count_found = nb_words    for word, i in tqdm(word_index.items()):        if i >= max_features: continue        embedding_vector = embeddings_index.get(word)        if embedding_vector is not None:             embedding_matrix[i] =  embedding_vector        else:            if word.islower():                # try to get the embedding of word in titlecase if lowercase is not present                embedding_vector = embeddings_index.get(word.capitalize())                if embedding_vector is not None:                     embedding_matrix[i] = embedding_vector                else:                    count_found-=1            else:                count_found-=1    print("Got embedding for ",count_found," words.")    return embedding_matrix

以上只是一个例子,说明我们如何利用嵌入的知识来获得更好的覆盖率。有时,根据问题的不同,我们也可能通过使用一些领域知识和NLP技能向嵌入添加额外的信息来获得价值。

例如,我们可以通过添加Python中TextBlob包中的一个词的极性和主观性,将外部知识添加到嵌入本身。

Python

from textblob import TextBlob word_sent = TextBlob("good").sentiment print(word_sent.polarity,word_sent.subjectivity) # 0.7 0.6

我们可以使用TextBlob获得任何单词的极性和主观性。相当不错!因此,让我们尝试将这些额外的信息添加到我们的嵌入中。

Python

def create_glove(word_index,embeddings_index):    emb_mean,emb_std = -0.005838499,0.48782197    all_embs = np.stack(embeddings_index.values())    embed_size = all_embs.shape[1]    nb_words = min(max_features, len(word_index))    embedding_matrix = np.random.normal(emb_mean, emb_std, (nb_words, embed_size+4))        count_found = nb_words    for word, i in tqdm(word_index.items()):        if i >= max_features: continue        embedding_vector = embeddings_index.get(word)        word_sent = TextBlob(word).sentiment        # Extra information we are passing to our embeddings        extra_embed = [word_sent.polarity,word_sent.subjectivity]        if embedding_vector is not None:             embedding_matrix[i] =  np.append(embedding_vector,extra_embed)        else:            if word.islower():                embedding_vector = embeddings_index.get(word.capitalize())                if embedding_vector is not None:                     embedding_matrix[i] = np.append(embedding_vector,extra_embed)                else:                    embedding_matrix[i,300:] = extra_embed                    count_found-=1            else:                embedding_matrix[i,300:] = extra_embed                count_found-=1    print("Got embedding for ",count_found," words.")    return embedding_matrix

工程嵌入是在后期从深度学习模型中获得更好性能的重要部分。一般来说,在项目阶段,我们会多次重温这部分代码,同时试图进一步改善我们的模型。你可以在这里展现大量的创造力,以提高对你的word_index ,并在你的嵌入中包含额外的功能。

更多的工程功能

Embedding_matrix文本预处理方法

人们总是可以添加特定的句子特征,如句子长度、独特的单词数等,作为另一个输入层,给深度神经网络提供额外的信息。

例如,我们创建了这些额外的特征,作为Quora不真实性分类挑战的特征工程管道的一部分。

蟒蛇

def add_features(df):    df['question_text'] = df['question_text'].progress_apply(lambda x:str(x))    df["lower_question_text"] = df["question_text"].apply(lambda x: x.lower())    df['total_length'] = df['question_text'].progress_apply(len)    df['capitals'] = df['question_text'].progress_apply(lambda comment: sum(1 for c in comment if c.isupper()))    df['caps_vs_length'] = df.progress_apply(lambda row: float(row['capitals'])/float(row['total_length']),                                axis=1)    df['num_words'] = df.question_text.str.count('\S+')    df['num_unique_words'] = df['question_text'].progress_apply(lambda comment: len(set(w for w in comment.split())))    df['words_vs_unique'] = df['num_unique_words'] / df['num_words']     return df

总结

在深度学习领域,NLP仍然是一个非常有趣的问题,所以我们鼓励你做大量的实验,看看什么有用,什么没用。我们试图为任何NLP问题的深度学习神经网络的预处理步骤提供一个健康的视角,但这并不意味着它是确定的。

主题。

深度学习, NLP, 机器学习, AI, 人工智能, python, 神经网络