fbpx
  • Работа с NLP-моделями Keras в браузере с TensorFlow.js

    nlp javascript
    https://www.robinwieruch.de

    Этот туториал для тех, кто знаком с основами JavaScript и основами глубокого обучения для задач NLP (RNN, Attention). Если вы плохо разбираетесь в RNN, я рекомендую вам прочитать «Необоснованную эффективность рекуррентных нейронных сетей» Андрея Карпати.

    Перевод статьи «NLP Keras model in browser with TensorFlow.js», автор — Mikhail Salnikov, ссылка на оригинал — в подвале статьи.

    В этой статье я попытаюсь охватить три вещи:

    1. Как написать простую Named-entity recognition (NER) модель — типичная задача NLP.
    2. Как экспортировать эту модель в формат TensorFlow.js.
    3. Как сделать простое веб-приложение для поиска именованных объектов в строке без серверной части.

    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.htmlForex.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&gt;");
    
      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.