Один из наиболее важных аспектов машинного обучения - это работа с хорошими, чистыми данными. В проектах по развитию естественного языка есть проблема использования текста, написанного людьми, а мы, к сожалению, плохо пишем. Просто подумайте о множестве орфографических ошибок, которые могут быть в наборе данных о публикациях и комментариях Reddit. По этой причине я подумал, что очень полезным будет создание программы проверки орфографии, которая поможет решить некоторые из этих проблем.

Модель, которую мы будем использовать для этого проекта, очень похожа на ту, о которой я писал в своей статье Обобщение текста с помощью Amazon Reviews (обе модели являются seq2seq), но я добавил несколько дополнительных строк кода, чтобы Архитектуру и гиперпараметры можно настроить с помощью поиска по сетке, а результаты можно проанализировать с помощью TensorBoard. Если вы хотите получить более подробное пошаговое руководство о том, как добавить TensorBoard в свой код, ознакомьтесь с Прогнозированием настроения при просмотре фильмов с помощью TensorFlow и TensorBoard.

Основное внимание в этой статье будет уделяться подготовке данных для модели, а также я расскажу о некоторых других функциях модели. В этом проекте мы будем использовать Python 3 и TensorFlow 1.1. Данные составлены из двадцати популярных книг от Project Gutenberg. Если вы заинтересованы в расширении этого проекта, чтобы сделать его более точным, есть сотни книг, которые вы можете скачать на Project Gutenberg. Кроме того, было бы действительно интересно посмотреть, насколько хорошо кто-то может проверить орфографию с этой моделью.

Чтобы увидеть полный код, перейдите на его страницу GitHub.

Чтобы вы могли увидеть, на что способна эта модель, вот несколько тщательно отобранных примеров:

  • Правописание - трудное дело, которое означает что вам нужно изучать каждый день.
  • Правописание затруднено, что поэтому нужно изучать каждый день.
  • Первые дни ее существования в этой стране были очень тяжелыми для Долли.
  • Первые дни ее существования в стране были для Долли очень тяжелыми.
  • Это действительно нечто впечатляющее , на которое мы должны сразу обратить внимание!
  • Это действительно нечто впечатляющее , на которое мы должны сразу обратить внимание!

Чтобы сделать вещи немного более организованными, я поместил все книги, которые мы будем использовать, в отдельную папку, названную «книги». Вот функция, которую мы будем использовать для загрузки всех книг:

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 этого проекта. Однако я думаю, что стоит показать вам символы, которые входят во входные данные:

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']

Мы могли бы удалить еще несколько специальных символов или сделать текст полностью строчными, но я хотел сделать эту проверку орфографии максимально полезной.

Перед подачей в модель данные будут организованы в предложения. Мы разделим данные по каждому периоду, за которым следует пробел («.»). Одна из проблем заключается в том, что некоторые предложения заканчиваются вопросительным или восклицательным знаком, но мы не учитываем это. К счастью, наша модель по-прежнему сможет узнать об использовании вопросительных знаков и восклицательных знаков, если только это и следующее предложение вместе взятые не имеют длины, равной максимальной длине предложения.

Приведу пример:

  • Сегодня прекрасный день. Я хочу пойти на пляж. (Это будет разделено на два входных предложения)
  • Сегодня прекрасный день? Я хочу пойти на пляж. (Это будет одно длинное предложение ввода)
sentences = []
for book in clean_books:
    for sentence in book.split('. '):
        sentences.append(sentence + '.')

Я использовал графический процессор на сайте f loydhub.com для обучения моей модели (я очень рекомендую их услуги), что сэкономило мне часы обучения. Тем не менее, чтобы правильно настроить эту модель, для выполнения итерации все еще требуется 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)

Возможно, наиболее интересной / важной частью этого проекта является функция, которая преобразует предложения в предложения с ошибками, которые будут использоваться в качестве входных данных. Ошибки создаются в этой функции одним из трех способов:

  • порядок двух символов будет поменяться местами (привет ~ привет)
  • будет добавлено дополнительное письмо (привет ~ привет)
  • персонаж не будет набран (привет ~ привет)

Вероятность возникновения любой из трех ошибок равна, а вероятность возникновения любой ошибки составляет 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)

Вот и все для этого проекта! Хотя результаты обнадеживают, у этой модели все еще есть ограничения. Я был бы очень признателен, если бы кто-нибудь увеличил масштаб этой модели или улучшил ее дизайн! Пожалуйста, напишите об этом в комментариях, если вы это сделаете. Можно было бы подумать о новом дизайне, чтобы применить новую модель CNN FAIR (она обеспечивает самые современные результаты для перевода).

Спасибо за чтение и надеюсь, что вы узнали что-то новое!