Реалізація класифікації тексту сверточной мережею на keras

Мова, як не дивно, піде про використовує сверточную мережа класифікаторі текстів (векторизація окремих слів — це вже інше питання). Код, тестові дані та приклади їх застосування — на bitbucket (уперся в обмеження розміру від github і пропозиція застосувати Git Large File Storage (LFS), поки не подужав пропоноване рішення).

Набори даних
Використані конвертовані набори: 22000 записів, 530 записів, 50 записів. до Речі, не відмовився б від подкинутого в коменти/ЛЗ (але краще таки в коменти) набору текстів російською.

Пристрій мережі
За основу взята одна реалізація описаної тут мережі. Код використаної реалізації.

У моєму випадку — на вході мережі знаходяться вектори слів (використана gensim-я реалізація word2vec). Структура мережі зображена нижче:


Коротко:

  • Текст подається як матриця виду word_count x word_vector_size. Вектори окремих слів — від word2vec, про який можна почитати, наприклад, у цьому пості. Так як мені заздалегідь невідомо, який текст підсуне користувач — беру довжину 2 * N, де N — кількість векторів у длиннейшем тексті навчальної вибірки. Так, тицьнув пальцем в небо.
  • Матриця обробляється згортковими ділянками мережі (на виході отримуємо перетворені ознаки слова)
  • Виділені ознаки обробляються повнозв'язним ділянкою мережі
Стоп слова отфильтровываю попередньо (на reuter-м dataset-е це не відбивалося, але в менших за обсягом наборах — зробило вплив). Про це нижче.

Установка необхідного ПЗ (keras/theano, cuda) в Windows
Установка для linux була відчутно простіше. Потрібні:

  • python3.5
  • відмінності файли python (python-dev debian)
  • gcc
  • cuda
  • python-е бібліотеки — ті ж, що і в наведеному нижче списку
В моєму випадку з win10 x64 приблизна послідовність була наступною:

  • Anaconda з python3.5.
  • Cuda 8.0. Можна запускати і на CPU (тоді досить gcc і наступні 4 кроку не потрібні), але на відносно великих датасетах падання в швидкості повинно бути істотним (не перевіряв).
  • Шлях до nvcc доданий в PATH (в іншому випадку — theano його не виявить).
  • Visual Studio 2015 з C++, включаючи windows 10 kit (потрібно corecrt.h).
  • Шлях до cl.exe доданий у PATH.
  • Шлях до corecrt.exe в INCLUDE (в моєму випадку — C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt).
  • conda install mingw libpython
    — gcc і libpython потрібно при компіляції сітки.
  • ну і
    pip install keras theano python-levenshtein gensim nltk
    (можливо заведеться і з заміною keras-го бекенду з theano на tensorflow, але мною не перевірялося).
  • .theanorc вказаний наступний прапор для gcc:

    [gcc]
    cxxflags = -D_hypot=hypot
    

  • Запустити python і виконати

    import nltk
    nltk.завантажити()
    

Обробка тексту
На цій стадії відбувається видалення стопслов, що не увійшли в комбінації з «білого списку» (про нього далі) і векторизація залишилися. Вхідні дані для вживаного алгоритму:

  • мова — потрібно nltk для токенизации і повернення списку стопслов
  • «білий список» комбінацій слів, в яких використовуються стопслова. Наприклад — «on» віднесено до стопсловам, але [«turn», «on»] — вже інша справа
  • вектори word2vec
Ну і алгоритм (бачу як мінімум 2 можливих поліпшення, але не подужав):

  • Розбиваю вхідний текст на токени ntlk.tokenize-м (умовно — «Hello, world!» перетвориться в [«hello», ",", «world», "!"]).
  • Відкидаю токени, яких немає в word2vec-му словнику.

    Насправді — яких там немає і виділити схожий на відстані не вийшло. Поки тільки відстань Левенштейна, є ідея фільтрувати токени з найменшою відстанню Левенштейна за відстанню від їх векторів до векторів, що входять в навчальну вибірку.

  • Вибрати токени:

    • яких немає в списку стопслов (знизило помилку на погодному датасете, але без наступного кроку — дуже зіпсувало результат на «car_intents»-м).

    • якщо токен у списку стопслов — перевірити входження в текст послідовностей з білого списку, в яких він є (умовно — по знаходженні «on» перевірити наявність послідовностей зі списку [[«turn», «on»]]). Якщо така знайдеться все ж додати його. Є що поліпшити — зараз я перевіряю (в нашому прикладі) наявність «turn», але воно ж може і не ставитися до цього «on».

  • Замінити вибрані токени їх векторами.
Коду нам, коду
Власне, код яким я і оцінював вплив змін
import itertools
import json
import numpy
from gensim.models import Word2Vec
from pynlc.test_data import reuters_classes, word2vec, car_classes, weather_classes
from pynlc.text_classifier import TextClassifier
from pynlc.text_processor import TextProcessor
from sklearn.metrics import mean_squared_error

def classification_demo(data_path, train_before, test_before, train_epochs, test_labels_path, instantiated_test_labels_path, trained_path):
with open(data_path, 'r', encoding='utf-8') as data_source:
data = json.load(data_source)
texts = [item["text"] for item in data]
class_names = [item["classes"] for item in data]
train_texts = texts[:train_before]
train_classes = class_names[:train_before]
test_texts = texts[train_before:test_before]
test_classes = class_names[train_before:test_before]
text_processor = TextProcessor("english", [["turn", "on"], ["turn", "off"]], Word2Vec.load_word2vec_format(word2vec))
classifier = TextClassifier(text_processor)
classifier.train(train_texts, train_classes, train_epochs, True)
prediction = classifier.predict(test_texts)
with open(test_labels_path, "w", encoding="utf-8") as test_labels_output:
test_labels_output_lst = []
for i in range(0, len(prediction)):
test_labels_output_lst.append({
"real": test_classes[i],
"classified": prediction[i]
})
json.dump(test_labels_output_lst, test_labels_output)
instantiated_classifier = TextClassifier(text_processor, **classifier.config)
instantiated_prediction = instantiated_classifier.predict(test_texts)
with open(instantiated_test_labels_path, "w", encoding="utf-8") as instantiated_test_labels_output:
instantiated_test_labels_output_lst = []
for i in range(0, len(instantiated_prediction)):
instantiated_test_labels_output_lst.append({
"real": test_classes[i],
"classified": instantiated_prediction[i]
})
json.dump(instantiated_test_labels_output_lst, instantiated_test_labels_output)
with open(trained_path, "w", encoding="utf-8") as trained_output:
json.dump(classifier.config, trained_output, ensure_ascii=True)

def classification_error(files):
for name in files:
with open(name, "r", encoding="utf-8") as src:
data = json.load(src)
classes = []
real = []
for row in data:
classes.append(row["real"])
classified = row["classified"]
row_classes = list(classified.keys())
row_classes.sort()
real.append([classified[class_name] for class_name in row_classes])
labels = []
class_names = list(set(itertools.chain(*classes)))
class_names.sort()
for item_classes in classes:
labels.append([int(class_name in item_classes) for class_name in class_names])
real_np = numpy.array(real)
mse = mean_squared_error(numpy.array(labels), real_np)
print(name, mse)

if __name__ == '__main__':
print("Reuters:\n")
classification_demo(reuters_classes, 10000, 15000, 10,
"reuters_test_labels.json", "reuters_car_test_labels.json",
"reuters_trained.json")
classification_error(["reuters_test_labels.json", "reuters_car_test_labels.json"])
print("Car intents:\n")
classification_demo(car_classes, 400, 500, 20,
"car_test_labels.json", "instantiated_car_test_labels.json",
"car_trained.json")
classification_error(["cars_test_labels.json", "instantiated_cars_test_labels.json"])
print("Weather:\n")
classification_demo(weather_classes, 40, 50, 30,
"weather_test_labels.json", "instantiated_weather_test_labels.json",
"weather_trained.json")
classification_error(["weather_test_labels.json", "instantiated_weather_test_labels.json"])


Тут ви бачите:

  • Підготовку даних:

    with open(data_path, 'r', encoding='utf-8') as data_source:
    data = json.load(data_source)
    texts = [item["text"] for item in data]
    class_names = [item["classes"] for item in data]
    train_texts = texts[:train_before]
    train_classes = class_names[:train_before]
    test_texts = texts[train_before:test_before]
    test_classes = class_names[train_before:test_before]
    

  • Створення нового класифікатора:

    text_processor = TextProcessor("english", [["turn", "on"], ["turn", "off"]], Word2Vec.load_word2vec_format(word2vec))
    classifier = TextClassifier(text_processor)
    

  • Його навчання:

    classifier.train(train_texts, train_classes, train_epochs, True)
    

  • Передбачення класів для тестової вибірки і збереження пар «справжні класи»-«передбачені ймовірності класів»:

    prediction = classifier.predict(test_texts)
    with open(test_labels_path, "w", encoding="utf-8") as test_labels_output:
    test_labels_output_lst = []
    for i in range(0, len(prediction)):
    test_labels_output_lst.append({
    "real": test_classes[i],
    "classified": prediction[i]
    })
    json.dump(test_labels_output_lst, test_labels_output)
    

  • Створення нового екземпляра класифікатора по конфігурації (dict, може бути сериализована в/десериализована з, наприклад json):

    instantiated_classifier = TextClassifier(text_processor, **classifier.config)
    
Вихлоп приблизно такий:

C:\Users\user\pynlc-env\lib\site-packages\gensim\utils.py:840: UserWarning: detected Windows; aliasing chunkize to chunkize_serial
warnings.warn("detected Windows; aliasing chunkize to chunkize_serial")
C:\Users\user\pynlc-env\lib\site-packages\gensim\utils.py:1015: UserWarning: library Pattern is not installed, lemmatization won't be available.
warnings.warn("library Pattern is not installed, lemmatization won't be available.")
Using Theano backend.
Using gpu device 0: GeForce GT 730 (CNMeM is disabled, cuDNN not available)
Reuters:
Train on 3000 samples, validate on 7000 samples
Epoch 1/10
20/3000 [..............................] - ETA: 307s - loss: 0.6968 - acc: 0.5376
....
3000/3000 [==============================] - 640s - loss: 0.0018 - acc: 0.9996 - val_loss: 0.0019 - val_acc: 0.9996
Epoch 8/10
20/3000 [..............................] - ETA: 323s - loss: 0.0012 - acc: 0.9994
...
3000/3000 [==============================] - 635s - loss: 0.0012 - acc: 0.9997 - val_loss: 9.2200 e-04 - val_acc: 0.9998
Epoch 9/10
20/3000 [..............................] - ETA: 315s - loss: 3.4387 e-05 - acc: 1.0000
...
3000/3000 [==============================] - 879s - loss: 0.0012 - acc: 0.9997 - val_loss: 0.0016 - val_acc: 0.9995
Epoch 10/10
20/3000 [..............................] - ETA: 327s - loss: 8.0144 e-04 - acc: 0.9997
...
3000/3000 [==============================] - 655s - loss: 0.0012 - acc: 0.9997 - val_loss: 7.4761 e-04 - val_acc: 0.9998
reuters_test_labels.json 0.000151774189194
reuters_car_test_labels.json 0.000151774189194

Car intents:
Train on 280 samples, validate on 120 samples
Epoch 1/20
20/280 [=>............................] - ETA: 0s - loss: 0.6729 - acc: 0.5250
...
280/280 [==============================] - 0s - loss: 0.2914 - acc: 0.8980 - val_loss: 0.2282 - val_acc: 0.9375
...
Epoch 19/20
20/280 [=>............................] - ETA: 0s - loss: 0.0552 - acc: 0.9857
...
280/280 [==============================] - 0s - loss: 0.0464 - acc: 0.9842 - val_loss: 0.1647 - val_acc: 0.9494
Epoch 20/20
20/280 [=>............................] - ETA: 0s - loss: 0.0636 - acc: 0.9714
...
280/280 [==============================] - 0s - loss: 0.0447 - acc: 0.9849 - val_loss: 0.1583 - val_acc: 0.9530
cars_test_labels.json 0.0520754688092
instantiated_cars_test_labels.json 0.0520754688092

Weather:
Train on 28 samples, validate on 12 samples
Epoch 1/30
20/28 [====================>.........] - ETA: 0s - loss: 0.6457 - acc: 0.6000
...
Epoch 29/30
20/28 [====================>.........] - ETA: 0s - loss: 0.0021 - acc: 1.0000
...
28/28 [==============================] - 0s - loss: 0.0019 - acc: 1.0000 - val_loss: 0.1487 - val_acc: 0.9167
Epoch 30/30
...
28/28 [==============================] - 0s - loss: 0.0018 - acc: 1.0000 - val_loss: 0.1517 - val_acc: 0.9167
weather_test_labels.json 0.0136964029149
instantiated_weather_test_labels.json 0.0136964029149

По ходу експериментів з стопсловами:

  • помилка в reuter-му наборі залишалася порівнянна незалежно від видалення/збереження стопслов.

  • помилка в weather-м — впала з 8% при видаленні стопслов. Ускладнення алгоритму не вплинуло (т. к. комбінацій, при яких стопслово таки потрібно зберегти тут немає).

  • помилка в car_intent-м — зросла приблизно до 15% при видаленні стопслов (наприклад, умовне «turn on» урезалось до «turn»). При додаванні обробки «білого списку» — повернулася на колишній рівень.
Приклад з запуском заздалегідь навченого класифікатора
Власне, властивість TextClassifier.config — словник, який можна отрендерить, наприклад, в json і після відновлення json-а — передати його елементи в конструктор TextClassifier-а. Наприклад:

import json
from gensim.models import Word2Vec
from pynlc.test_data import word2vec
from pynlc import TextProcessor, TextClassifier

if __name__ == '__main__':
text_processor = TextProcessor("english", [["turn", "on"], ["turn", "off"]],
Word2Vec.load_word2vec_format(word2vec))
with open("weather_trained.json", "r", encoding="utf-8") as classifier_data_source:
classifier_data = json.load(classifier_data_source)
classifier = TextClassifier(text_processor, **classifier_data)
texts = [
"it Will be windy or rainy at evening?",
"How cold it'll be today?"
]
predictions = classifier.predict(texts)
for i in range(0, len(texts)):
print(texts[i])
print(predictions[i])

І його вихлоп:

C:\Users\user\pynlc-env\lib\site-packages\gensim\utils.py:840: UserWarning: detected Windows; aliasing chunkize to chunkize_serial
warnings.warn("detected Windows; aliasing chunkize to chunkize_serial")
C:\Users\user\pynlc-env\lib\site-packages\gensim\utils.py:1015: UserWarning: library Pattern is not installed, lemmatization won't be available.
warnings.warn("library Pattern is not installed, lemmatization won't be available.")
Using Theano backend.
Will it be windy or rainy at evening?
{'temperature': 0.039208538830280304, 'conditions': 0.9617446660995483}
How cold it'll be today?
{'temperature': 0.9986168146133423, 'conditions': 0.0016815820708870888}

І так, конфіг мережі навченої на датасете від reuters — тут. Гігабайт сітки для 19Мб датасета, так :-)
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.