其他

自然语言处理如何检查拼写错误?(Tensorflow实例教程&源代码)

2017-05-26 Dave Currie 数据派THU

原文:Towards Data Science

作者:Dave Currie

来源:机器人圈

本文长度为2400字,建议阅读5分钟

本文教你用TensorFlow搭建拼写检查器,用于处理自然语言处理(NLP)的数据。


机器学习的一个最重要的问题就是,我们需要干净的数据。自然语言处理项目存在着一个问题——使用人类书写的文本。而不幸的是,我们并不擅长写作。想象一下,如果在Reddit上的有关帖子和评论的数据集中存在着许多拼写错误,这对于机器来说,是不是有些不好处理呢?


因此,制作一个拼写检查器将会是一个非常有价值的项目,这将有助于缓解这些问题。


我们即将用于此项目的模型与我在文章“亚马逊评论中的文本汇总”(https://medium.com/towards-data-science/text-summarization-with-amazon-reviews-41801c2210b)(都是seq2seq模型)中写的是很相似的,但是我添加了一些额外的代码行,以便可以使用grid search来调整整个架构和超参数,并且可以使用TensorBoard来分析结果。如果你想要更详细地演示如何在你的代码中添加TensorBoard,请查看“使用TensorFlow和TensorBoard预测Movie Review Sentiment”(https://medium.com/@Currie32/predicting-movie-review-sentiment-with-tensorflow-and-tensorboard-53bf16af0acf)


本文的着重点将在于如何为模型准备数据,同时我还将讨论该模型的一些其他功能。我们将在此项目中使用Python 3和TensorFlow 1.1。数据是由古腾堡项目中的二十本流行书籍组成。如果你有兴趣扩大这个项目以使其更准确,那么你可以在古腾堡项目上下载数百本图书。此外,如果看到人们使用这种模式制作出的拼写检查器是多么的好用,那将是非常有趣的。


如果你想要查看完整的代码,可以在GitHub页面查看:https://github.com/Currie32/Spell-Checker


为了让你预览这个模型所具有的能力,这里有一些策划的例子可以当做参考:


Spellin is difficult, whch is wyh you need to study everyday.

Spelling is difficult, which is why you need to study everyday.

The first days of her existence in th country were vrey hard for Dolly.

The first days of her existence in the country were very hard for Dolly.

Thi is really something impressiv thaat we should look into right away!

This is really something impressive that we should look into right away!


为了使事情更有条理,我把我们将使用的所有书籍放在他们自己的文件夹中,名称定为“books”。这是我们将用来加载所有书籍的函数:

def load_book(path):
   input_file = os.path.join(path)
   with open(input_file) as f:
       book = f.read()
   return book


同时,我们还需要为每本书定下一个唯一的文件名:

path = './books/'
book_files = [f for f in listdir(path) if isfile(join(path, f))]
book_files = book_files[1:]


当我们将这两个代码块放在一起时,我们将能够将所有书籍中的文本加载到列表中。

books = []
for book in book_files:
   books.append(load_book(path+book))

  

如果你有兴趣了解每本书中有多少单词,你可以使用以下代码行:

for i in range(len(books)):
   print("There are {} words in {}.".format(len(books[i].split()), book_files[i]))


注意:如果你的代码中不包括.split(),那么它将返回的是每本书中的字符数。

 

清理这些书的文本是相当简单的。由于我们将使用的是字符,而不是单词作为我们模型的输入,所以我们不需要担心去除停用词,或者将单词缩短到只留下主干。我们只需要删除我们不想要的字符和多余的空格。

def clean_text(text):
   '''Remove unwanted characters and extra spaces from the text'''
   text = re.sub(r'\n', ' ', text)
   text = re.sub(r'[{}
@_*>()\\#%+=\[\]]','', text)
   text = re.sub('a0','', text)
   text = re.sub('\'92t','\'t', text)
   text = re.sub('\'92s','\'s', text)
   text = re.sub('\'92m','\'m', text)
   text = re.sub('\'92ll','\'ll', text)
   text = re.sub('\'91','', text)
   text = re.sub('\'92','', text)
   text = re.sub('\'93','', text)
   text = re.sub('\'94','', text)
   text = re.sub('\.','. ', text)
   text = re.sub('\!','! ', text)
   text = re.sub('\?','? ', text)
   text = re.sub(' +',' ', text) # Removes extra spaces
   return text


我将跳过如何来制作vocab_to_int和int_to_vocab字典,因为这是非常标准的东西,你可以在这个项目的GitHub页面(https://github.com/Currie32/Spell-Checker)上找到它。但是,我认为值得向你展示输入数据中包含的字符:

The vocabulary contains 78 characters.
[' ', '!', '"', '$', '&', "'", ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<EOS>', '<GO>', '<PAD>', '?', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']


我们可以删除更多的特殊字符,或者使文本全部小写,但是我想让这个拼写检查器尽可能有用。


数据在被输入到模型之前被组织成句子。我们将在每个时间段后跟一个空格(“.”)来拆分数据。一个问题是,一些句子是以问号或感叹号结尾的,但我们说话的时候不是这样的。幸运的是,我们的模型仍然能够理解使用问号和感叹号,只要与以下句子相结合,不超过最大句子长度。


举个例子来说明这个问题:


Today is a lovely day. I want to go to the beach. (这将被拆分为两个输入句子)

Is today a lovely day? I want to go to the beach. (这将是一个长的输入句子)

sentences = []
for book in clean_books:
   for sentence in book.split('. '):
       sentences.append(sentence + '.')


我在floydhub.com(https://www.floydhub.com/)上使用GPU来训练我的模型(我强烈推荐他们的服务),这节省了我几个小时的训练时间。尽管如此,为了正确调整这个模型,运行迭代仍然需要30-60分钟的时间,这就是为什么我要限制数据,从而不需要花费更长的时间来做这件事情。这当然会降低我们的模型的准确性,但由于这只是一个个人项目,所以,我不是很在乎。

max_length = 92
min_length = 10

good_sentences = []

for sentence in int_sentences:
   if len(sentence) <= max_length and len(sentence) >= min_length:
       good_sentences.append(sentence)


为了跟踪这个模型的性能,我将把数据拆分成一个训练集和一个测试集。测试集将由数据15%的组成。

training, testing = train_test_split(good_sentences,
                                    test_size = 0.15,
                                    random_state = 2)


就像我最近的一些项目一样,我将按照长度来给数据进行排序。这导致一批量的句子具有相似的长度,因此只需要使用较少的填充,并且模型训练的速度将更快。

training_sorted = []
testing_sorted = []

for i in range(min_length, max_length+1):
   for sentence in training:
       if len(sentence) == i:
           training_sorted.append(sentence)
   for sentence in testing:
       if len(sentence) == i:
           testing_sorted.append(sentence)


也许这个项目最有趣/最重要的部分就是将句子转换为含有错误的句子的函数,这些函数将被用作输入数据。在这个函数中创建的错误的方式将以下面三种之一的一种进行:


两个字符的顺序将被交换(hlelo〜hello)

将添加一个额外的字母(heljlo〜hello)

其中一个字符没有被打印出来(helo〜hello)


这三个错误发生的可能性是相等的,任一个错误发生的可能性为5%。因此,平均而言,每20个字符中就会有一个包含一个错误。

letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m',
          'n','o','p','q','r','s','t','u','v','w','x','y','z',]

def noise_maker(sentence, threshold):
   
   noisy_sentence = []
   i = 0
   while i < len(sentence):
       random = np.random.uniform(0,1,1)
       if random < threshold:
           noisy_sentence.append(sentence[i])
       else:
           new_random = np.random.uniform(0,1,1)
           if new_random > 0.67:
               if i == (len(sentence) - 1):
                   continue
               else:
                   noisy_sentence.append(sentence[i+1])
                   noisy_sentence.append(sentence[i])
                   i += 1
           elif new_random < 0.33:
               random_letter = np.random.choice(letters, 1)[0]
               noisy_sentence.append(vocab_to_int[random_letter])
               noisy_sentence.append(sentence[i])
           else:
               pass    
       i += 1
   return noisy_sentence


在本文中,我想向你展示的最后一件事是如何创建批次。通常,在训练他们的模型之前,会先创建他们的输入数据,这意味着他们具有固定数量的训练数据。然而,当我们训练我们的模型时,通过将noise_maker应用于每个批次,我们将要创建新的输入数据。这意味着对于每个时期,目标(正确的)句子将通过noise_maker进行反馈,并应该接收一个新的输入句子。使用这种方法的话,我们略微夸张地说,将会有无数量的训练数据。

def get_batches(sentences, batch_size, threshold):
   
   for batch_i in range(0, len(sentences)//batch_size):
       start_i = batch_i * batch_size
       sentences_batch = sentences[start_i:start_i + batch_size]
       
       sentences_batch_noisy = []
       for sentence in sentences_batch:
           sentences_batch_noisy.append(
               noise_maker(sentence, threshold))
           
       sentences_batch_eos = []
       for sentence in sentences_batch:
           sentence.append(vocab_to_int['<EOS>'])
           sentences_batch_eos.append(sentence)
           
       pad_sentences_batch = np.array(
           pad_sentence_batch(sentences_batch_eos))
       pad_sentences_noisy_batch = np.array(
           pad_sentence_batch(sentences_batch_noisy))
       
       pad_sentences_lengths = []
       for sentence in pad_sentences_batch:
           pad_sentences_lengths.append(len(sentence))
       
       pad_sentences_noisy_lengths = []
       for sentence in pad_sentences_noisy_batch:
           pad_sentences_noisy_lengths.append(len(sentence))
       
       yield (pad_sentences_noisy_batch,
              pad_sentences_batch,
              pad_sentences_noisy_lengths,
              pad_sentences_lengths)


这就是整个这个项目!虽然结果是令人鼓舞的,但这种模式仍然存在着一定的局限性。我真的会很感激,如果有人可以扩大这个模型或改进其设计!如果你可以这样做,请在评论中发表一下。新设计的想法将会应用到Facebook AI实验室最新的CNN模型(https://code.facebook.com/posts/1978007565818999/a-novel-approach-to-neural-machine-translation/?utm_campaign=Artificial%2BIntelligence%2Band%2BDeep%2BLearning%2BWeekly&utm_medium=email&utm_source=Artificial_Intelligence_and_Deep_Learning_Weekly_13)中去(它可以获得最先进的翻译结果)。


感谢你的阅读,希望你可以从中学到新的知识!


为保证发文质量、树立口碑,数据派现设立“错别字基金”,鼓励读者积极纠错

若您在阅读文章过程中发现任何错误,请在文末留言,或到后台反馈,经小编确认后,数据派将向检举读者发8.8元红包

感谢一直以来您的关注和支持,希望您能够监督数据派产出更加高质的内容。


公众号底部菜单有惊喜哦!

企业,个人加入组织请查看“联合会”

往期精彩内容请查看“号内搜”

加入志愿者或联系我们请查看“关于我们”

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存