Соответствие требованиям отрасли за счет применения современных методов глубокого обучения

Несколько лет назад, когда я работал стажером по разработке программного обеспечения в стартапе, я увидел новую функцию в веб-приложении для размещения вакансий. Приложение могло распознавать и анализировать важную информацию из резюме, такую ​​как адрес электронной почты, номер телефона, названия ученых степеней и т. Д. Я начал обсуждать возможные подходы с нашей командой, и мы решили создать парсер на основе правил на Python, чтобы просто анализировать разные разделы резюме. Потратив некоторое время на разработку парсера, мы поняли, что ответом может быть не инструмент, основанный на правилах. Мы начали гуглить, как это делается, и натолкнулись на термин Обработка естественного языка (NLP) и, более конкретно, Распознавание именованных сущностей (NER), связанный с Машинным обучением.

NER - это метод извлечения информации для идентификации и классификации именованных объектов в тексте. Эти объекты могут быть предопределенными и общими, например, названия местоположений, организаций, времени и т. Д., Или они могут быть очень конкретными, как пример с резюме. NER имеет множество вариантов использования в бизнесе. Я думаю, что gmail применяет NER, когда вы пишете электронное письмо и указываете время в своем электронном письме или прикрепляете файл, Gmail предлагает установить уведомление календаря или напоминает вам прикрепить файл, если вы отправляете электронное письмо без вложения. Другие приложения NER включают: извлечение важных именованных объектов из юридических, финансовых и медицинских документов, классификацию содержания для поставщиков новостей, улучшение алгоритмов поиска. и т. д. В оставшейся части этой статьи у нас будет краткое введение в различные подходы к решению проблемы NER, а затем мы перейдем к кодированию современного метода. Вот более подробное введение в NER от Suvro.

Подходы к NER

  • Классические подходы: в основном основаны на правилах. вот ссылка на короткое удивительное видео от Sentdex, которое использует пакет NLTK в python для NER.
  • Подходы к машинному обучению: в этой категории есть два основных метода: A- рассматривает проблему как мультиклассовую классификацию, где именованные объекты являются нашими метками, поэтому мы можем применять другую классификацию. алгоритмы. Проблема здесь в том, что идентификация и маркировка именованных объектов требует тщательного понимания контекста предложения и последовательности слов в нем, что этот метод игнорирует. B- Еще один метод в этой категории - модель условного случайного поля (CRF). Это вероятностная графическая модель, которую можно использовать для моделирования последовательных данных, таких как метки слов в предложении. для получения дополнительных сведений и полной реализации CRF на python см. статью Тобиаса. модель CRF способна улавливать особенности текущей и предыдущей меток в последовательности, но не может понять контекст прямых меток; этот недостаток плюс дополнительные функции, связанные с обучением модели CRF, делают ее менее привлекательной для адаптации в отрасли.

  • Подходы к глубокому обучению:

Прежде чем обсуждать детали подходов глубокого обучения (современных) к NER, нам необходимо проанализировать правильные и четкие метрики для оценки производительности наших моделей. При обучении нейронной сети на разных итерациях (эпохах) в качестве метрики оценки принято использовать точность. Однако в случае NER мы можем иметь дело с важными финансовыми, медицинскими или юридическими документами, и точная идентификация названных лиц в этих документах определяет успех модели. Другими словами, ложные срабатывания и ложные отрицательные результаты в задаче NER требуют больших затрат для бизнеса. Таким образом, нашим основным показателем для оценки наших моделей будет оценка F1, потому что нам нужен баланс между точностью и отзывчивостью.

Другой важной стратегией в создании высокопроизводительного метода глубокого обучения является понимание того, какой тип нейронной сети лучше всего подходит для решения проблемы NER, учитывая, что текст является последовательным форматом данных. да, вы правильно догадались ... Долгосрочная краткосрочная память (LSTM). подробнее о LSTM по этой ссылке. Но не любой тип LSTM, нам нужно использовать двунаправленные LSTM, потому что использование стандартного LSTM для прогнозирования будет учитывать только прошлую информацию в последовательности текста. для NER, поскольку контекст охватывает прошлые и будущие ярлыки в последовательности, мы должны принимать во внимание как прошлую, так и будущую информацию. Двунаправленный LSTM представляет собой комбинацию двух LSTM: один идет вперед справа налево, а другой - назад, слева направо.

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

  1. Двунаправленный LSTM-CRF:

Подробности и реализация в keras.

2. Двунаправленные LSTM-CNN :

Подробности и реализация в keras.

3. Двунаправленный LSTM-CNNS-CRF:

4. ELMo (встраивание из языковых моделей):

одна из самых недавних статей (Глубокие контекстуализированные представления слов) вводит новый тип глубоко контекстуализированного представления слов, который моделирует как сложные характеристики употребления слов (например, синтаксис и семантику), так и то, как это использование различается в зависимости от языкового контекста (т. е. моделировать многозначность). Новый подход (ELMo) имеет три важных представления:

1. Контекстный: представление каждого слова зависит от всего контекста, в котором оно используется.

2. Глубокий: представления слов объединяют все уровни глубоко обученной нейронной сети.

3. На основе символов: представления ELMo основаны исключительно на символах, что позволяет сети использовать морфологические подсказки для формирования надежных представлений для токенов вне словарного запаса, невидимых при обучении.

ELMo прекрасно понимает язык, потому что он обучен на огромном наборе данных, а встраивания ELMo обучаются на 1 миллиард слов Benchmark. обучение называется двунаправленной языковой моделью (biLM), которая может извлекать уроки из прошлого и предсказывать следующее слово в последовательности слов, например в предложении. Давайте посмотрим, как мы можем реализовать этот подход. мы собираемся использовать набор данных из kaggle.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.style.use("ggplot")
data = pd.read_csv("ner_dataset.csv", encoding="latin1")
data = data.drop(['POS'], axis =1)
data = data.fillna(method="ffill")
data.tail(12)
words = set(list(data['Word'].values))
words.add('PADword')
n_words = len(words)
n_words
35179
tags = list(set(data["Tag"].values))
n_tags = len(tags)
n_tags
17

у нас есть 47958 предложений в нашем наборе данных, 35179 различных слов и 17 различных именованных объектов (тегов).

Давайте посмотрим на распределение длин предложений в наборе данных:

class SentenceGetter(object):
    
    def __init__(self, data):
        self.n_sent = 1
        self.data = data
        self.empty = False
        agg_func = lambda s: [(w, t) for w, t in zip(s["Word"].values.tolist(),s["Tag"].values.tolist())]
        self.grouped = self.data.groupby("Sentence #").apply(agg_func)
        self.sentences = [s for s in self.grouped]
    
    def get_next(self):
        try:
            s = self.grouped["Sentence: {}".format(self.n_sent)]
            self.n_sent += 1
            return s
        except:
            return None

этот класс отвечает за преобразование каждого предложения с его именованными объектами (тегами) в список кортежей [(слово, именованный объект),…]

getter = SentenceGetter(data)
sent = getter.get_next()
print(sent)
[('Thousands', 'O'), ('of', 'O'), ('demonstrators', 'O'), ('have', 'O'), ('marched', 'O'), ('through', 'O'), ('London', 'B-geo'), ('to', 'O'), ('protest', 'O'), ('the', 'O'), ('war', 'O'), ('in', 'O'), ('Iraq', 'B-geo'), ('and', 'O'), ('demand', 'O'), ('the', 'O'), ('withdrawal', 'O'), ('of', 'O'), ('British', 'B-gpe'), ('troops', 'O'), ('from', 'O'), ('that', 'O'), ('country', 'O'), ('.', 'O')]
sentences = getter.sentences
print(len(sentences))
47959
largest_sen = max(len(sen) for sen in sentences)
print('biggest sentence has {} words'.format(largest_sen))
biggest sentence has 104 words

поэтому в самом длинном предложении 140 слов, и мы видим, что почти все предложения содержат менее 60 слов.

Одно из самых больших преимуществ этого подхода заключается в том, что нам не нужна никакая разработка функций; все, что нам нужно, это предложения и помеченные слова, остальная работа выполняется с помощью вложений ELMo. Чтобы передать наши предложения в сеть LSTM, все они должны быть одинакового размера. глядя на график распределения, мы можем установить длину всех предложений равной 50 и добавить общее слово для пустых мест; этот процесс называется заполнением (еще одна причина того, что 50 - хорошее число, заключается в том, что мой ноутбук не может обрабатывать более длинные предложения).

max_len = 50
X = [[w[0]for w in s] for s in sentences]
new_X = []
for seq in X:
    new_seq = []
    for i in range(max_len):
        try:
            new_seq.append(seq[i])
        except:
            new_seq.append("PADword")
    new_X.append(new_seq)
new_X[15]
['Israeli','officials','say','Prime','Minister','Ariel',
 'Sharon', 'will','undergo','a', 'medical','procedure','Thursday',
 'to','close','a','tiny','hole','in','his','heart','discovered',
 'during','treatment', 'for','a', 'minor', 'stroke', 'suffered', 'last', 'month', '.', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword',
 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword',
 'PADword', 'PADword']

То же самое относится и к названным объектам, но на этот раз нам нужно сопоставить наши метки с числами:

from keras.preprocessing.sequence import pad_sequences
tags2index = {t:i for i,t in enumerate(tags)}
y = [[tags2index[w[1]] for w in s] for s in sentences]
y = pad_sequences(maxlen=max_len, sequences=y, padding="post", value=tags2index["O"])
y[15]
array([4, 7, 7, 0, 1, 1, 1, 7, 7, 7, 7, 7, 9, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,7, 7, 7, 7, 7, 7])

Затем мы разделяем наши данные на набор для обучения и тестирования, а затем импортируем Tenorflow Hub (библиотеку для публикации, обнаружения и использования многократно используемых частей моделей машинного обучения), чтобы загрузить функцию встраивания ELMo и керасы, чтобы начать построение нашей сети.

from sklearn.model_selection import train_test_split
import tensorflow as tf
import tensorflow_hub as hub
from keras import backend as K
X_tr, X_te, y_tr, y_te = train_test_split(new_X, y, test_size=0.1, random_state=2018)
sess = tf.Session()
K.set_session(sess)
elmo_model = hub.Module("https://tfhub.dev/google/elmo/2", trainable=True)
sess.run(tf.global_variables_initializer())
sess.run(tf.tables_initializer())

Первый запуск вышеуказанного блока кода займет некоторое время, потому что ELMo занимает почти 400 МБ. Затем мы используем функцию для преобразования наших предложений во вложения ELMo:

batch_size = 32
def ElmoEmbedding(x):
    return elmo_model(inputs={"tokens": tf.squeeze(tf.cast(x,    tf.string)),"sequence_len": tf.constant(batch_size*[max_len])
                     },
                      signature="tokens",
                      as_dict=True)["elmo"]

А теперь давайте построим нашу нейронную сеть:

from keras.models import Model, Input
from keras.layers.merge import add
from keras.layers import LSTM, Embedding, Dense, TimeDistributed, Dropout, Bidirectional, Lambda
input_text = Input(shape=(max_len,), dtype=tf.string)
embedding = Lambda(ElmoEmbedding, output_shape=(max_len, 1024))(input_text)
x = Bidirectional(LSTM(units=512, return_sequences=True,
                       recurrent_dropout=0.2, dropout=0.2))(embedding)
x_rnn = Bidirectional(LSTM(units=512, return_sequences=True,
                           recurrent_dropout=0.2, dropout=0.2))(x)
x = add([x, x_rnn])  # residual connection to the first biLSTM
out = TimeDistributed(Dense(n_tags, activation="softmax"))(x)
model = Model(input_text, out)
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"])

Поскольку размер пакета равен 32, питание сети должно осуществляться частями, кратными 32:

X_tr, X_val = X_tr[:1213*batch_size], X_tr[-135*batch_size:]
y_tr, y_val = y_tr[:1213*batch_size], y_tr[-135*batch_size:]
y_tr = y_tr.reshape(y_tr.shape[0], y_tr.shape[1], 1)
y_val = y_val.reshape(y_val.shape[0], y_val.shape[1], 1)
history = model.fit(np.array(X_tr), y_tr, validation_data=(np.array(X_val), y_val),batch_size=batch_size, epochs=3, verbose=1)
Train on 38816 samples, validate on 4320 samples
Epoch 1/3
38816/38816 [==============================] - 834s 21ms/step - loss: 0.0625 - acc: 0.9818 - val_loss: 0.0449 - val_acc: 0.9861
Epoch 2/3
38816/38816 [==============================] - 833s 21ms/step - loss: 0.0405 - acc: 0.9869 - val_loss: 0.0417 - val_acc: 0.9868
Epoch 3/3
38816/38816 [==============================] - 831s 21ms/step - loss: 0.0336 - acc: 0.9886 - val_loss: 0.0406 - val_acc: 0.9873

Первоначальной целью было поиграться с настройкой параметров для достижения более высокой точности, но мой ноутбук не смог обработать более 3 эпох и размер пакетов больше 32 или увеличить размер теста. Я использую keras на Geforce GTX 1060, и на обучение этих трех эпох потребовалось почти 45 минут, если у вас лучший графический процессор, дайте ему шанс, изменив некоторые из этих параметров.

Точность проверки 0,9873 - отличный показатель, однако нам не интересно оценивать нашу модель с помощью метрики точности. Давайте посмотрим, как мы можем получить оценки за точность, отзывчивость и F1:

from seqeval.metrics import precision_score, recall_score, f1_score, classification_report
X_te = X_te[:149*batch_size]
test_pred = model.predict(np.array(X_te), verbose=1)
4768/4768 [==============================] - 64s 13ms/step
idx2tag = {i: w for w, i in tags2index.items()}
def pred2label(pred):
    out = []
    for pred_i in pred:
        out_i = []
        for p in pred_i:
            p_i = np.argmax(p)
            out_i.append(idx2tag[p_i].replace("PADword", "O"))
        out.append(out_i)
    return out
def test2label(pred):
    out = []
    for pred_i in pred:
        out_i = []
        for p in pred_i:
            out_i.append(idx2tag[p].replace("PADword", "O"))
        out.append(out_i)
    return out
    
pred_labels = pred2label(test_pred)
test_labels = test2label(y_te[:149*32])
print(classification_report(test_labels, pred_labels))
               precision   recall  f1-score   support

        org       0.69      0.66      0.68      2061
        tim       0.88      0.84      0.86      2148
        gpe       0.95      0.93      0.94      1591
        per       0.75      0.80      0.77      1677
        geo       0.85      0.89      0.87      3720
        art       0.23      0.14      0.18        49
        eve       0.33      0.33      0.33        33
        nat       0.47      0.36      0.41        22

avg / total       0.82      0.82      0.82     11301

Оценка 0,82 F1 - выдающееся достижение. он превосходит все три других метода глубокого обучения, упомянутых в начале этого раздела, и может быть легко адаптирован отраслью.

Наконец, давайте посмотрим, как выглядят наши прогнозы:

i = 390
p = model.predict(np.array(X_te[i:i+batch_size]))[0]
p = np.argmax(p, axis=-1)
print("{:15} {:5}: ({})".format("Word", "Pred", "True"))
print("="*30)
for w, true, pred in zip(X_te[i], y_te[i], p):
    if w != "__PAD__":
        print("{:15}:{:5} ({})".format(w, tags[pred], tags[true]))
Word            Pred : (True)
==============================
Citing         :O     (O)
a              :O     (O)
draft          :O     (O)
report         :O     (O)
from           :O     (O)
the            :O     (O)
U.S.           :B-org (B-org)
Government     :I-org (I-org)
Accountability :I-org (O)
office         :O     (O)
,              :O     (O)
The            :B-org (B-org)
New            :I-org (I-org)
York           :I-org (I-org)
Times          :I-org (I-org)
said           :O     (O)
Saturday       :B-tim (B-tim)
the            :O     (O)
losses         :O     (O)
amount         :O     (O)
to             :O     (O)
between        :O     (O)
1,00,000       :O     (O)
and            :O     (O)
3,00,000       :O     (O)
barrels        :O     (O)
a              :O     (O)
day            :O     (O)
of             :O     (O)
Iraq           :B-geo (B-geo)
's             :O     (O)
declared       :O     (O)
oil            :O     (O)
production     :O     (O)
over           :O     (O)
the            :O     (O)
past           :B-tim (B-tim)
four           :I-tim (I-tim)
years          :O     (O)
.              :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)
PADword        :O     (O)

как всегда, код и блокнот jupyter доступны на моем Github.

Вопросы и комментарии приветствуются.

Использованная литература:

  1. Https://www.depends-on-the-definition.com/ named-entity-recognition-with-residual-lstm-and-elmo/
  2. Http://www.wildml.com/2016/08/rnns-in-tensorflow-a-practical-guide-and-undocumented-features/
  3. Https://allennlp.org/elmo
  4. Https://jalammar.github.io/illustrated-bert/