Text mining, Анализ данных

Как заменить регулярные выражения нейронной сетью?

Время прочтения: 5 мин.

Наиболее часто используемый инструмент для поиска подстроки определенного вида в тексте – это регулярные выражения. Но можно ли вместо регулярного выражения использовать нейронную сеть, которая бы выполняла ту же самую задачу?

Задача: найти в тексте описание стоимости недвижимости, то есть численное обозначение и стоимость, записанную прописью. Например, 2 050 000 (два миллиона пятьдесят тысяч) руб., 00 коп. Задача усложняется тем, что «рубли» и «копейки» могут быть в любом месте (перед скобками или после) и могут быть сокращены.

Чтобы решить данную задачу, будем использовать NLP (Natural Language Processing), морфологический анализатор и нейронную сеть. Подключаем соответствующие библиотеки:

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import pymorphy2
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

Прежде всего необходимо выполнить обработку текста.

  1. Токенизируем текст с помощью nltk.
f = open('input/'+file, 'r', encoding='ansi')  
strings = f.read().replace('\n', ' ')  
words = word_tokenize(strings, language='russian')

2. Уберем стоп-слова из текста и знаки препинания, которые нам не нужны (например, : или “).

words = [word for word in words if not word in new_stopwords]

3. После этого пройдемся по тексту и выберем фрагменты, в которых встречается слово «рубли» в полном варианте или в сокращенном. В итоге получаем фрагменты предложений, которые содержат одинаковое количество слов/знаков/чисел. Именно в этих фрагментах мы и будем искать стоимость.

Следующим шагом нужно представить «слова» в виде чисел, так как нейронная сеть работает только с числами. Для этого пройдемся по каждому полученному фрагменту и определим части речи для каждого «слова». В этом нам поможет морфологический анализатор pymorphy2.

morph = pymorphy2.MorphAnalyzer()
def set_morphema(x):
    morphema = str(morph.parse(x)[0].tag).split(',')[0]  
    if morphema.find(' ') != -1:
        morphema = morphema.split(' ')[0]
    return morphema

При анализе выделим 6 групп значений:

  • PNCT – знаки пунктуации: ( ) . ,
  • NUMB – числа
  • NUMR – числительные
  • NOUN – существительные: «тысяча», «миллион», «миллиард»
  • NOUN – существительные: «рубль», «копейка» и их сокращенные формы
  • Все остальные части речи, которые не встречаются в нужных нам фрагментах

Каждое «слово» в зависимости от того, в какую группу оно попало, представим в виде вектора значений из 6 чисел, содержащего 0 и 1 – 0, если число не относится к данной группе, 1 – если относится. Получается, что каждое «слово» закодировано пятью нулями и одной единицей. Каждый фрагмент, в котором мы будем искать стоимость, содержит 23 «слова», соответственно получаем 138 чисел для одного фрагмента. Именно эти значения будем подавать на вход нейронной сети.

Чтобы обучить нейронную сеть, составим выборку. Входные данные уже имеются, остается составить выходные. Выходные данные для одного фрагмента будут представлены в виде 23 чисел – 0 и 1. Единицами обозначим тот фрагмент, который в итоге нужно выбрать из текста и который содержит стоимость.

Как преобразовывались данные:

Фрагмент, содержащий стоимость:

['Цена', 'Договора', 'порядок', 'расчетов', '.', '2.1', '.', 'Стоимость', 'Объекта', 'составляет', '810', '000', '(', 'Восемьсот', 'десять', 'тысяч', ')', 'рублей', '.', 'Цена', 'является', 'окончательной', 'изменению']

Фрагмент, преобразованный в числа:

[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]

Выходные данные для фрагмента:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0]

Создаем модель нейронной сети. Она будет состоять из 4 плотно связанных слоев Dense, с учетом входного и выходного. Используем наиболее распространенную функцию активации «relu» для каждого слоя, кроме выходного. Также добавим слой Dropout, чтобы предотвратить переобучение модели.

Важным критерием работы нейронной сети оказалась функция потерь. Наиболее стабильный и достоверный результат получился при функции потерь MSE (средняя квадратическая ошибка).

model = keras.Sequential([
    layers.Dense(138, activation='relu'),
    layers.Dense(138, activation='relu'),
    layers.Dropout(0.1),
    layers.Dense(23, activation='relu'),
    layers.Dense(23, activation='sigmoid')])
optimizer = tf.optimizers.Adam()
model.compile(loss=tf.keras.losses.MSE, optimizer=optimizer, metrics=['accuracy'])

history = model.fit(train_data, vyhod_data_train, epochs=1000)

Обучаем модель и выполняем предсказания для тестовых данных. На выходе получаем последовательность из 23 чисел для каждого фрагмента. Числа, которые больше 0,9 – нужные нам значения, заменим их на 1. Находим начало и конец последовательности единиц. Далее по индексам начала и конца последовательности единиц выбираем стоимость из фрагмента текста.

Что получается при обработке тестовых данных?

Фрагмент, в котором ищем стоимость

['Цена', 'Объекта‚', 'являющаяся', 'предметом', 'настоящего', 'Договора', ',', 'составляет', '2', '100', '000', '(', 'два', 'миллиона', 'сто', 'тысяч', ')', 'рублей', ',', 'именно', 'цена', 'земельного', 'участка']

Фрагмент, преобразованный в числа

[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]

Значения, полученные после работы нейронной сети

[5.8360482e-14 5.7737207e-19 7.7303122e-18 6.7739243e-18 5.8828078e-14
 1.1499115e-18 2.8125218e-13 9.2836785e-08 1.0000000e+00 1.0000000e+00
 1.0000000e+00 1.0000000e+00 1.0000000e+00 1.0000000e+00 1.0000000e+00
 1.0000000e+00 1.0000000e+00 1.0000000e+00 5.1083343e-10 2.7263733e-10
 3.0139889e-14 2.2163703e-13 1.4157570e-14]

Выбранный фрагмент стоимости

['2', '100', '000', '(', 'два', 'миллиона', 'сто', 'тысяч', ')', 'рублей']

Процент корректно выбранных фрагментов стоимости составляет 94% по результатам работы данной нейронной сети.

Советуем почитать