Этот туториал для тех, кто знаком с основами JavaScript и основами глубокого обучения для задач NLP (RNN, Attention). Если вы плохо разбираетесь в RNN, я рекомендую вам прочитать «Необоснованную эффективность рекуррентных нейронных сетей» Андрея Карпати.
Перевод статьи «NLP Keras model in browser with TensorFlow.js», автор — Mikhail Salnikov, ссылка на оригинал — в подвале статьи.
В этой статье я попытаюсь охватить три вещи:
- Как написать простую Named-entity recognition (NER) модель — типичная задача NLP.
- Как экспортировать эту модель в формат TensorFlow.js.
- Как сделать простое веб-приложение для поиска именованных объектов в строке без серверной части.
TensorFlow.js — это библиотека JavaScript для разработки и обучения ML-моделей на JavaScript, а также для развертывания в браузере или на Node.js.
В этом примере мы будем использовать простую модель Keras для решения классической задачи NER. Мы будем тренироваться на датасете CoNLL2003 . Наша модель — это просто векторное представление слов, GRU и очень простой механизм внимания. После мы визуализируем вектор внимания в браузере. Если вы знакомы с современными подходами для решения аналогичной задачи, вы знаете, что этот подход не является state-of-the-art подходом. Однако для запуска его в браузере этого вполне достаточно.
Задача и данные
В зависимости от вашего опыта, вы могли слышать об этом под разными названиями — тегирование последовательностей (sequence tagging), Part-of-Speech тегирование или, как в нашей задаче, распознавание именованных объектов (Named-entity recognition).
Обычно задача NER — это задача seq2seq. Для каждого x_i мы должны предсказать y_i, где x — входная последовательность, а y — последовательность именованных объектов.
В этом примере мы будем искать людей (B-PER, I-PER), местоположения (B-LOC, I-LOC) и организации (B-ORG, I-ORG). Кроме того, в модели будут определены специальные сущности, MISC — именованные сущности, которые не являются личностями, местами или организациями.
Для начала, нам нужно подготовить данные для компьютера (да, мы будем использовать компьютер для решения этой задачи :)).
В этом туториале мы не ставим перед собой цель получить результат SOTA для набора данных CoNLL2003, поэтому качество нашей предобработки данных будет не на самом высоком уровне. В качестве примера, мы будем загружать данные методом load_data:
def word_preprocessor(word): word = re.sub(r'd+', '1', re.sub(r"[-|.|,|?|!]+", '', word)) word = word.lower() if word != '': return word else: return '.' def load_data(path, word_preprocessor=word_preprocessor): tags = [] words = [] data = {'words': [], 'tags': []} with open(path) as f: for line in f.readlines()[2:]: if line != 'n': parts = line.replace('n', '').split(' ') words.append(word_preprocessor(parts[0])) tags.append(parts[-1]) else: data['words'].append(words) data['tags'].append(tags) words, tags = [], [] return data
Как вы знаете, нейронные сети не могут работать со словами, только с числами. Вот почему мы должны представлять слова в виде чисел. Это не сложная задача, мы можем перечислить все уникальные слова и записать номер каждого слова вместо него самого. Для хранения цифр и слов мы можем составить словарь. Этот словарь должен поддерживать слова «неизвестный» (UNK) для ситуации, когда мы будем делать прогноз для новой строки со словами, которых нет в словаре. Также словарь должен содержать слово «дополнено» (PAD). Для нейронной сети все строки должны иметь одинаковый размер, поэтому, когда одна строка будет меньше другой, мы заполним пробелы этим словом.
def make_vocab(sentences, tags=False): vocab = {"(PAD)": PAD_ID, "(UNK)": UNK_ID} idd = max([PAD_ID, UNK_ID]) + 1 for sen in sentences: for word in sen: if word not in vocab: vocab[word] = idd idd += 1 return vocab
Далее, давайте напишем простой помощник для перевода предложений в последовательность чисел.
def make_sequences(list_of_words, vocab, word_preprocessor=None): sequences = [] for words in list_of_words: seq = [] for word in words: if word_preprocessor: word = word_preprocessor(word) seq.append(vocab.get(word, UNK_ID)) sequences.append(seq) return sequences
Как вы можете увидеть выше, мы должны дополнить последовательности для работы с нейронной сетью, для этого вы можете использовать внутренний метод Keras — pad sequence.
train_X = pad_sequences(train_data['words_sequences'], maxlen=MAX_SEQUENCE_LENGTH, value=PAD_ID, padding='post', truncating='post') valid_X = pad_sequences(valid_data['words_sequences'], maxlen=MAX_SEQUENCE_LENGTH, value=PAD_ID, padding='post', truncating='post') train_y = pad_sequences(train_data['tags_sequences'], maxlen=MAX_SEQUENCE_LENGTH, value=PAD_ID, padding='post', truncating='post') valid_y = pad_sequences(valid_data['tags_sequences'], maxlen=MAX_SEQUENCE_LENGTH, value=PAD_ID, padding='post', truncating='post')
Модель
Дай угадаю.. RNN?
Верно, она самая. Если точнее, это GRU с простым слоем внимания. Для представления слов используется GloVe. В этом посте я не буду вдаваться в подробности, а просто оставлю здесь код модели. Я надеюсь, что это легко понять.
from tensorflow.keras.layers import (GRU, Dense, Dropout, Embedding, Flatten, Input, Multiply, Permute, RepeatVector, Softmax) from tensorflow.keras.models import Model from utils import MAX_SEQUENCE_LENGTH def make_ner_model(embedding_tensor, words_vocab_size, tags_vocab_size, num_hidden_units=128, attention_units=64): EMBEDDING_DIM = embedding_tensor.shape[1] words_input = Input(dtype='int32', shape=[MAX_SEQUENCE_LENGTH]) x = Embedding(words_vocab_size + 1, EMBEDDING_DIM, weights=[embedding_tensor], input_length=MAX_SEQUENCE_LENGTH, trainable=False)(words_input) outputs = GRU(num_hidden_units, return_sequences=True, dropout=0.5, name='RNN_Layer')(x) # Simple attention hidden_layer = Dense(attention_units, activation='tanh')(outputs) hidden_layer = Dropout(0.25)(hidden_layer) hidden_layer = Dense(1, activation=None)(hidden_layer) hidden_layer = Flatten()(hidden_layer) attention_vector = Softmax(name='attention_vector')(hidden_layer) attention = RepeatVector(num_hidden_units)(attention_vector) attention = Permute([2, 1])(attention) encoding = Multiply()([outputs, attention]) encoding = Dropout(0.25)(encoding) ft1 = Dense(num_hidden_units)(encoding) ft1 = Dropout(0.25)(ft1) ft2 = Dense(tags_vocab_size)(ft1) out = Softmax(name='Final_Sofmax')(ft2) model = Model(inputs=words_input, outputs=out) return model
После построения модели, мы должны скомпилировать, обучить и сохранить ее. Вы можете догадаться, что для запуска этой модели в браузере мы должны сохранять не только веса для нее, но также описание модели и словари для слов и тегов. Давайте определим метод для экспорта модели и словарей в формат, поддерживаемый JavaScript (в основном JSON):
import json import os import tensorflowjs as tfjs def export_model(model, words_vocab, tags_vocab, site_path): tfjs.converters.save_keras_model( model, os.path.join(site_path, './tfjs_models/ner/') ) with open(os.path.join(site_path, "./vocabs.js"), 'w') as f: f.write('const words_vocab = {n') for l in json.dumps(words_vocab)[1:-1].split(","): f.write("t"+l+',n') f.write('};n') f.write('const tags_vocab = {n') for l in json.dumps(tags_vocab)[1:-1].split(","): f.write("t"+l+',n') f.write('};') print('model exported to ', site_path)
Наконец, давайте скомпилируем, обучим и экспортируем модель:
model.compile( loss='categorical_crossentropy', optimizer='Adam', metrics=['categorical_accuracy'] ) model.fit(train_X, train_y, epochs=args.epoches, batch_size=args.batch_size, validation_data=(valid_X, valid_y)) export_model(model, words_vocab, tags_vocab, args.site_path)
Полный код для этих шагов вы можете найти в моем репозитории на GitHub в train.py.
Разработка веб-приложения
Итак, модель готова, и теперь мы должны начать разработку веб-приложения для проверки нашей модели режима в браузере. Нам нужно настроить рабочую среду. В принципе, не важно, как вы будете хранить свою модель, веса и словарь, но в качестве примера я покажу вам мое простое решение — локальный сервер node.js.
Нам потребуется два файла: package.json и server.js.
Package.json:
{ "name": "tfjs_ner", "version": "1.0.0", "dependencies": { "express": "latest" } }
Server.js:
let express = require("express") let app = express(); app.use(function(req, res, next) { console.log(`${new Date()} - ${req.method} request for ${req.url}`); next(); }); app.use(express.static("./static")); app.listen(8081, function() { console.log("Serving static on http://localhost:8081"); });
В server.js мы определили статическую папку для хранения модели, js-скриптов и всех других файлов. Для использования этого сервера, вы должны ввести
npm install && node server.js
в вашем терминале. После этого вы можете получить доступ к своим файлам в браузере по адресу http://localhost:8081.
Далее перейдем к основной части веб-приложения. Существуют index.html, Forex.js и файлы, которые были созданы на предыдущем шаге. Как видите, это очень маленькое веб-приложение. index.html содержит требования и поле для ввода строк пользователем.
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> </head> <body> <main role="main"> <form class="form" onkeypress="return event.keyCode != 13;"> <input type="text" id='input_text'> <button type="button" id="get_ner_button">Search Entities</button> </form> <div class="results"> </div> </main> <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.0.0/dist/tf.min.js"></script> <script src="vocabs.js"></script> <script src="predict.js"></script> </body> </html>
Теперь самая интересная часть туториала — про TensorFlow.js. Вы можете загрузить модель с помощью метода tf.loadLayersModel с использованием оператора await. Это важно, потому что мы не хотим блокировать наше веб-приложение при загрузке модели. Если мы загрузим модель, то у нас будет модель, которая может предсказывать только токены, но как насчет вектора внимания? Для получения данных из внутренних слоев в TensorFlow.js мы должны создать новую модель, в которой выходные слои будут содержать выходные данные и другие слои из исходной модели, например:
let model, emodel; (async function() { model = await tf.loadLayersModel('http://localhost:8081/tfjs_models/ner/model.json'); let outputs_ = [model.output, model.getLayer("attention_vector").output]; emodel = tf.model({inputs: model.input, outputs: outputs_}); })();
Здесь model — это оригинальная модель, emodel — модель с attention_vector на выходе.
Предобработка
Теперь мы должны реализовать предварительную обработку строк, как мы это делали в нашем скрипте Python. Для нас это не сложная задача, потому что регулярные выражения в Python и JavaScript очень похожи, как и многие другие методы.
const MAX_SEQUENCE_LENGTH = 113; function word_preprocessor(word) { word = word.replace(/[-|.|,|?|!]+/g, ''); word = word.replace(/d+/g, '1'); word = word.toLowerCase(); if (word != '') { return word; } else { return '.' } }; function make_sequences(words_array) { let sequence = Array(); words_array.slice(0, MAX_SEQUENCE_LENGTH).forEach(function(word) { word = word_preprocessor(word); let id = words_vocab[word]; if (id == undefined) { sequence.push(words_vocab['']); } else { sequence.push(id); } }); // pad sequence if (sequence.length < MAX_SEQUENCE_LENGTH) { let pad_array = Array(MAX_SEQUENCE_LENGTH - sequence.length); pad_array.fill(words_vocab['']); sequence = sequence.concat(pad_array); } return sequence; };
Предсказания
Теперь мы должны обеспечить передачу данных из простого текстового формата строки в формат TF — тензор. В предыдущем разделе мы написали помощника для перевода строки в последовательность чисел. Теперь мы должны создать tf.tensor из этого массива. Как вы помните, входной слой модели имеет форму (None, 113) , поэтому мы должны расширить размер входного тензора. Ну вот и все, теперь мы можем делать предсказание в браузере методом .predict. После этого вам нужно вывести прогнозируемые данные в браузере, и ваше веб-приложение с нейронной сетью без серверной части готово.
const getKey = (obj,val) => Object.keys(obj).find(key => obj[key] === val); // For getting tags by tagid $("#get_ner_button").click(async function() { $(".results").html("word - tag - attention</br><hr>"); let words = $('#input_text').val().split(' '); let sequence = make_sequences(words); let tensor = tf.tensor1d(sequence, dtype='int32').expandDims(0); let [predictions, attention_probs] = await emodel.predict(tensor); attention_probs = await attention_probs.data(); predictions = await predictions.argMax(-1).data(); let predictions_tags = Array(); predictions.forEach(function(tagid) { predictions_tags.push(getKey(tags_vocab, tagid)); }); words.forEach(function(word, index) { $(".results").append(word+' - '+predictions_tags[index]+' - '+attention_probs[index]+''); }); });
Итог
TensorFlow.js — это библиотека для использования нейронных сетей в браузерах, таких как Chrome, Firefox или Safari. Если вы откроете это веб-приложение на смартфоне iPhone или Android, оно тоже будет работать.
Вы можете найти код этого приложения с некоторыми дополнениями на моем GitHub.