Как фанат НЛП, я всегда задавался вопросом, как Google Ассистент или Алекса понимают, когда я просил его что-то сделать. Вопрос продолжался, могу ли я заставить мою машину тоже меня понимать? Решением было - Классификация намерений.

Классификация намерений - это часть Natural Language Understanding, где алгоритм машинного обучения / глубокого обучения учится классифицировать данную фразу на основе тех, на которых она была обучена.

Возьмем забавный пример. Я делаю ассистента, как Алекса.

Для простоты мы возьмем 3 задачи: включим свет, выключим его и расскажем, какая сейчас погода. Дадим названия всем трем задачам TurnOnLights, TurnOffLights и Weather. Все эти задачи в NLU называются «намерениями». Другими словами, намерение - это группа похожих фраз, подпадающих под общее имя, чтобы алгоритму глубокого обучения было легко понять, что должен сказать пользователь. Каждому намерению дается определенное количество обучающих фраз, чтобы оно могло научиться классифицировать фразы в реальном времени.

Теперь, когда мы знаем, что такое классификация по намерениям, приступим к интересным вещам! Я написал блокнот, если вы хотите пройтись по мне, который вы можете найти в моем репозитории на Github здесь.

Для простоты воспользуемся следующей структурой каталогов:

Your directory
├───models 
├───utils
└───intent_classification.ipynb

Установка зависимостей

Установите необходимые зависимости, используя следующую команду:

pip install wget tensorflow==1.5 pandas numpy keras

Набор данных

Мы будем использовать общедоступный набор данных CLINC150. Это набор фраз для 150 различных целей в 10 доменах. Подробнее о наборе данных можно прочитать здесь.

Мы загрузим набор данных, используя:

import wget
url = 'https://raw.githubusercontent.com/clinc/oos-eval/master/data/data_full.json'
wget.download(url)

Подготовка набора данных

Набор данных уже разделен на наборы «поезд», «тест» и «проверка», но мы создадим свои собственные наборы для обучения и проверки, поскольку нам не нужен набор тестов. Мы сделаем это, объединив все наборы, а затем разделив их с помощью scikit-learn на наборы «train» и «validation». Это также создаст больше обучающих данных.

import numpy as np
import json
# Loading json data
with open('data_full.json') as file:
  data = json.loads(file.read())

# Loading out-of-scope intent data
val_oos = np.array(data['oos_val'])
train_oos = np.array(data['oos_train'])
test_oos = np.array(data['oos_test'])

# Loading other intents data
val_others = np.array(data['val'])
train_others = np.array(data['train'])
test_others = np.array(data['test'])

# Merging out-of-scope and other intent data
val = np.concatenate([val_oos,val_others])
train = np.concatenate([train_oos,train_others])
test = np.concatenate([test_oos,test_others])
data = np.concatenate([train,test,val])
data = data.T

text = data[0]
labels = data[1]

Затем мы создадим разделение поезда и проверки, используя:

from sklearn.model_selection import train_test_split
train_txt,test_txt,train_label,test_labels = train_test_split(text,labels,test_size = 0.3)

Предварительная обработка набора данных

Поскольку глубокое обучение - это игра чисел, мы ожидаем, что наши данные будут в числовой форме, чтобы с ними можно было играть. Мы будем токенизировать наш набор данных; это означает разбивать предложения на отдельные части и преобразовывать этих лиц в числовые представления. Мы будем использовать K eras Tokenizer для токенизации наших фраз, используя следующий код:

from tensorflow.python.keras.preprocessing.text import Tokenizer
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
max_num_words = 40000
classes = np.unique(labels)

tokenizer = Tokenizer(num_words=max_num_words)
tokenizer.fit_on_texts(train_txt)
word_index = tokenizer.word_index

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

ls=[]
for c in train_txt:
    ls.append(len(c.split()))
maxLen=int(np.percentile(ls, 98))
train_sequences = tokenizer.texts_to_sequences(train_txt)
train_sequences = pad_sequences(train_sequences, maxlen=maxLen,              padding='post')
test_sequences = tokenizer.texts_to_sequences(test_txt)
test_sequences = pad_sequences(test_sequences, maxlen=maxLen, padding='post')

Затем нам нужно преобразовать наши метки в форму с горячим кодированием. Подробнее о горячем кодировании можно прочитать здесь.

from sklearn.preprocessing import OneHotEncoder,LabelEncoder

label_encoder = LabelEncoder()
integer_encoded = label_encoder.fit_transform(classes)

onehot_encoder = OneHotEncoder(sparse=False)
integer_encoded = integer_encoded.reshape(len(integer_encoded), 1)
onehot_encoder.fit(integer_encoded)
train_label_encoded = label_encoder.transform(train_label)
train_label_encoded = train_label_encoded.reshape(len(train_label_encoded), 1)
train_label = onehot_encoder.transform(train_label_encoded)
test_labels_encoded = label_encoder.transform(test_labels)
test_labels_encoded = test_labels_encoded.reshape(len(test_labels_encoded), 1)
test_labels = onehot_encoder.transform(test_labels_encoded)

Прежде чем мы создадим нашу Модель ..

Прежде чем мы начнем обучение нашей модели, мы будем использовать Глобальные векторы. GloVe - это N-мерное векторное представление слов, обученное Стэнфордским университетом на большом корпусе. Поскольку он обучен на большом корпусе, это поможет модели еще лучше выучить фразы.

Мы загрузим GloVe, используя:

import wget
url ='https://www.dropbox.com/s/a247ju2qsczh0be/glove.6B.100d.txt?dl=1'
wget.download(url)

После завершения загрузки мы сохраним его в словаре Python:

embeddings_index={}
with open('glove.6B.100d.txt', encoding='utf8') as f:
    for line in f:
        values = line.split()
        word = values[0]
        coefs = np.asarray(values[1:], dtype='float32')
        embeddings_index[word] = coefs

Поскольку GloVe содержит векторное представление всех слов из большого корпуса, нам понадобятся только те векторы слов, которые присутствуют в нашем корпусе. Мы создадим матрицу внедрения, которая будет содержать векторные представления только тех слов, которые присутствуют в нашем наборе данных. Поскольку наш набор данных уже был токенизирован, каждому токену в наборе данных присваивается уникальный номер токенизатором Keras. Этот уникальный номер можно рассматривать как индекс для вектора каждого слова в матрице вложения; это означает, что каждое n-е слово из токенизатора представлено вектором в n-й позиции в матрице внедрения.

all_embs = np.stack(embeddings_index.values())
emb_mean,emb_std = all_embs.mean(), all_embs.std()
num_words = min(max_num_words, len(word_index))+1
embedding_dim=len(embeddings_index['the'])
embedding_matrix = np.random.normal(emb_mean, emb_std, (num_words, embedding_dim))
for word, i in word_index.items():
    if i >= max_num_words:
        break
    embedding_vector = embeddings_index.get(word)
    if embedding_vector is not None:
        embedding_matrix[i] = embedding_vector

Подготовка модели

Давайте представим архитектуру нашей модели, чтобы увидеть ее в действии.

from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.layers import Dense, Input, Dropout, LSTM, Activation, Bidirectional,Embedding
model = Sequential()

model.add(Embedding(num_words, 100, trainable=False,input_length=train_sequences.shape[1], weights=[embedding_matrix]))
model.add(Bidirectional(LSTM(256, return_sequences=True, recurrent_dropout=0.1, dropout=0.1), 'concat'))
model.add(Dropout(0.3))
model.add(LSTM(256, return_sequences=False, recurrent_dropout=0.1, dropout=0.1))
model.add(Dropout(0.3))
model.add(Dense(50, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(classes.shape[0], activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

Мы передадим матрицу вложения в слой вложения как веса.

Модельное обучение

Наконец-то пришло время обучить модель.

history = model.fit(train_sequences, train_label, epochs = 20,
          batch_size = 64, shuffle=True,
          validation_data=[test_sequences, test_labels])

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

import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.title('Model Accuracy')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend(['Train', 'Validation'], loc='upper left')
plt.show()

Wohoo !! мы получаем точность обучения 92,45% и точность проверки 88,86%, что довольно прилично.

Вот кривая потерь:

import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Model Loss')
plt.ylabel('Loss')
plt.xlabel('Epoch')
plt.legend(['Train', 'Validation'], loc='upper left')
plt.show()

Потеря обучения составляет около 0,2, а потеря проверки составляет около 0,5. Вы можете поиграть с архитектурой модели и посмотреть, уменьшатся ли потери даже 😉

Сохранение модели, токенизатора, кодировщика этикеток и этикеток

Давайте сохраним обученную модель, токенизатор, кодировщик меток и метки, чтобы использовать их в будущем.

import pickle
import json
model.save('models/intents.h5')

with open('utils/classes.pkl','wb') as file:
   pickle.dump(classes,file)

with open('utils/tokenizer.pkl','wb') as file:
   pickle.dump(tokenizer,file)

with open('utils/label_encoder.pkl','wb') as file:
   pickle.dump(label_encoder,file)

Время увидеть все в действии

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

Я создал следующий класс, чтобы использовать нашу модель:

import numpy as np
from tensorflow.python.keras.preprocessing.sequence import pad_sequences
class IntentClassifier:
    def __init__(self,classes,model,tokenizer,label_encoder):
        self.classes = classes
        self.classifier = model
        self.tokenizer = tokenizer
        self.label_encoder = label_encoder

    def get_intent(self,text):
        self.text = [text]
        self.test_keras = self.tokenizer.texts_to_sequences(self.text)
        self.test_keras_sequence = pad_sequences(self.test_keras, maxlen=16, padding='post')
        self.pred = self.classifier.predict(self.test_keras_sequence)
        return self.label_encoder.inverse_transform(np.argmax(self.pred,1))[0]

Чтобы использовать класс, мы сначала загрузим наши сохраненные файлы:

import pickle

from tensorflow.python.keras.models import load_model
model = load_model('models/intents.h5')

with open('utils/classes.pkl','rb') as file:
  classes = pickle.load(file)

with open('utils/tokenizer.pkl','rb') as file:
  tokenizer = pickle.load(file)

with open('utils/label_encoder.pkl','rb') as file:
  label_encoder = pickle.load(file)

Время для теста! 😋

nlu = IntentClassifier(classes,model,tokenizer,label_encoder)
print(nlu.get_intent("is it cold in India right now"))
# Prints 'weather'

Вот и все, ребята! Спасибо за чтение😃. Удачного обучения!