Перші кроки в NLP: розглядаємо Python-бібліотеку TensorFlow та нейронні мережі в реальному завданні

Усім привіт! Це третя частина статті про класифікацію оголошень з продажу земельних ділянок в Україні методами NLP. У першій і другій частинах я розповів про задачі, які я планував розв'язків зв'язати, ознайомив читачів з датасетом, моделлю «мішка слів» і навів приклади класифікаторів, що підготував за допомогою бібліотек NLTK та scikit-learn. У цій частині розповім про бібліотеку TensorFlow і наведу кілька прикладів реалізації різних архітектур нейронних мереж; покажу, як можна реалізувати модель word2vec та підіб'ю підсумки всієї зробленої роботи. Увесь код до цієї частини статті можна знайте на GitHub .

Нейронні мережі та бібліотека TensorFlow

Нейронні мережі — це обчислювальні системи, натхнені біологічними нейронними ятерами, що утворюють мозок тварин. Такі системи навчаються завдань (поступально покращують свою продуктивність на них), розглядаючи приклади, загалом без спеціального програмування під завдання. Наприклад, у розпізнаванні зображень смороду можуть навчатися ідентифікувати зображення, які містять котів, аналізуючи приклади зображень, мічені як «кіт» і «не кіт», і використовуючи результати для ідентифікування котів в інших зображеннях. Вони роблять це без жодного апріорного знання про котів, наприклад що вони мають хутро, хвости, вуса й котоподібні писки. Натомість вони розвивають свій власний набір доречних характеристик з навчального матеріалу, який оброблюють.

Бібліотека TensorFlow — це потужна програмна бібліотека з відкритим кодом, розроблена компанією Google. Вона дуже добре підходить і точно підігнана під великомасштабне машинне навчання. Її базовий принцип простий: ви визначаєте в Python граф обчислень, які треба виконати, а TensorFlow бере цей граф і ефективно проганяє з використанням оптимізованого коду С++. Найголовніше, граф можна розбивати на частини й проганяти їх паралельно на безлічі центральних процесорів або графічних процесорів. Бібліотека TensorFlow також підтримує розподілені обчислення, тому ви в змозі навчати величезні нейронні мережі на неймовірно великих навчальних наборах за прийнятний час, розподіляючи обчислення за сотні серверів.

Я не буду детально зупинятися на архітектурах нейронних мереж, а наведу лише кілька прикладів, які сам тестував. Отже, одразу перейду до побудови першої нейронної мережі. Вісь код:

from tensorflow.keras.preprocessing.text import Tokenizer
def first_model(n_classes):
 model = tf.keras.models.Sequential([

 tf.keras.layers.Dense(2000, activation='relu'),
 tf.keras.layers.BatchNormalization(), 
 tf.keras.layers.Dropout(0.50), 
 tf.keras.layers.Dense(20, activation='relu'),
 tf.keras.layers.BatchNormalization(), 
 tf.keras.layers.Dropout(0.50), 
 tf.keras.layers.Dense(n_classes, activation='softmax')
])

model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])

 return model

def fit_print (X_train, X_test, y_train, y_test,n_classes):
 t = Tokenizer(num_words=2000)
t.fit_on_texts(X_train)
 X_train = t.texts_to_matrix(X_train, mode='count')
 X_test = t.texts_to_matrix(X_test, mode='count')
 print (X_train.shape)
model=first_model(n_classes)
 model.fit(X_train, y_train, epochs=5)
 results = model.evaluate(X_test, y_test, verbose=2)
 print ('test loss: {0}, test acc: {1}'.format(results[0],results[1]))
 y_pred=model.predict_classes(X_test) 
 con_mat = tf.math.confusion_matrix(labels=y_test, predictions=y_pred)
print(con_mat.numpy())

fit_print (X_train_zab, X_test_zab,np.array(y_train_zab), np.array(y_test_zab),2)
fit_print (X_train, X_test, np.array(y_train)-1, np.array(y_test)-1,7)

Спочатку розглянємо функцію first_model, в якій будуймо нашу першу модель. Аргумент n_classes — позитивні ціле число, що вказує на кількість класів у вихідному шарі. Спочатку оголошуємо екземпляр класу tf.keras.models.Sequential, — що дозволяє лінійно вкладати шарі нейронної мережі, і будуємо просту мережу, яка включає три повнозв'язні шарі Dense (останній шар — вихідний, тому йому передається параметр n_classes), два шарі BatchNormalization (нормалізує й масштабує входи для зменшення перенавчання) й два шарі Dropout («відключає» частину нейронів у нейромережі, знову ж таки для зменшення перенавчання). Метод model.compile потрібен для компіляції моделі. Ось і все, перша нейронна мережа побудована і готова для використання.

Функція fit_print приймає навчальні й тестові вибірки, а також кількість класів. На початковому етапі оголошуємо клас Tokenizer і вказуємо максимальну кількість слів у вокабулярі. Метод fit_on_texts будує вокабуляр на навчальній вибірці, а метод texts_to_matrix перетворює текстові дані у матричний вигляд. Усе за аналогією до моделі «мішка слів», реалізованої в попередній частині. Далі навчаємо нашу модель і друкуємо результати.

Для класифікації за забудованістю ділянок:

Train on 2874 samples
Epoch 1/5
2874/2874 [==============================] - 2s 731us/sample - loss: 0.7161 - accuracy: 0.7088
Epoch 2/5
2874/2874 [==============================] - 2s 594us/sample - loss: 0.3731 - accuracy: 0.8754
Epoch 3/5
2874/2874 [==============================] - 2s 618us/sample - loss: 0.2374 - accuracy: 0.9294
Epoch 4/5
2874/2874 [==============================] - 2s 571us/sample - loss: 0.1489 - accuracy: 0.9576
Epoch 5/5
2874/2874 [==============================] - 2s 583us/sample - loss: 0.1106 - accuracy: 0.9711
1416/1416 - 0s - loss: 0.2052 - accuracy: 0.9273
test loss: 0.20516728302516507, test acc: 0.9272598624229431
[[1195 18]
 [ 85 118]]

Для класифікації за типами ділянок:

Train on 2874 samples
Epoch 1/5
2874/2874 [==============================] - 2s 805us/sample - loss: 1.3955 - accuracy: 0.5612
Epoch 2/5
2874/2874 [==============================] - 2s 649us/sample - loss: 0.6525 - accuracy: 0.8361
Epoch 3/5
2874/2874 [==============================] - 2s 628us/sample - loss: 0.4022 - accuracy: 0.9123
Epoch 4/5
2874/2874 [==============================] - 2s 592us/sample - loss: 0.2985 - accuracy: 0.9315
Epoch 5/5
2874/2874 [==============================] - 2s 631us/sample - loss: 0.2377 - accuracy: 0.9482
1416/1416 - 0s - loss: 0.2796 - accuracy: 0.9301
test loss: 0.2796336193963633, test acc: 0.930084764957428
[[863 2 5 3 0 0 0]
 [ 15 93 5 4 0 0 0]
 [ 18 2 270 0 0 0 0]
 [ 14 2 0 68 0 0 0]
 [ 11 0 0 1 1 0 0]
 [ 0 1 0 0 0 12 0]
 [ 10 5 1 0 0 0 10]]

Передача нейронної мережі як крок у проверяемая Pipeline

Звичайно, це перша нейронна мережа й результати можна покращити. Тут потрібно врахувати, що на етапі токенізації даних ми можемо використати не лише класі TensorFlow, але й будь-які інші перетворювачі, ба більше, нейронну мережу можна передати як крок у проверяемая Pipeline. Однак відразу наголошую: бібліотека TensorFlow працює значно ефективніше за scikit-learn, тож на великих датасетах таке рішення, як на мене, використовувати недоцільно, але враховуючи конкретно наш випадок, можна спробувати. Вісь код:

from tensorflow.keras.wrappers.scikit_learn import KerasClassifier
from sklearn.base import TransformerMixin
def skl_model(units=2000,n_classes=10,n_layers=1,Dropout=0.5):
 model = tf.keras.Sequential()

 model = tf.keras.models.Sequential()
 for n in range(1,n_layers+1):
 model.add(tf.keras.layers.Dense(units/n, activation='relu'))
model.add(tf.keras.layers.BatchNormalization())
model.add(tf.keras.layers.Dropout(Dropout))
 model.add(tf.keras.layers.Dense(n_classes, activation='softmax'))

model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])

 return model

class ToarrayTransformer(TransformerMixin):

 def fit(self, X, y=None, **fit_params):
 return self

 def transform(self, X, y=None, **fit_params):
 return X. toarray()


y_train=np.array(y_train)
y_test=np.array(y_test)
y_train_zab=np.array(y_train_zab)
y_test_zab=np.array(y_test_zab)
models_params_typy={
 'KerasClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),
('feature_selection',SelectFromModel(LinearSVC())),
('ToarrayTransformer',ToarrayTransformer()),
 ('clf',KerasClassifier(build_fn=skl_model, verbose=0))]),
{'Vectorizer':[TfidfVectorizer(),CountVectorizer()],
'Vectorizer__ngram_range':[(1,1),(1,3)],
'Vectorizer__tokenizer':[ua_tokenizer_sklearn],
'feature_selection__threshold':[0.2,0.1,0.5],
'clf__units':[1000,500],
 'clf__n_classes':[7], 
'clf__n_layers':[3,2],
'clf__Dropout':[0.5,0.4],
'clf__epochs':[5],
 }], 


}


GridSearchCV_Classifiers(X=X_train, y=y_train-1,
models_params=models_params_typy,scoring='f1_macro',cv=3)


models_params_zab={
 'KerasClassifier': [Pipeline([('Vectorizer',TfidfVectorizer()),
('feature_selection',SelectFromModel(LinearSVC())),
('ToarrayTransformer',ToarrayTransformer()),
 ('clf',KerasClassifier(build_fn=skl_model, verbose=0))]),
{'Vectorizer':[TfidfVectorizer(),CountVectorizer()],
'Vectorizer__ngram_range':[(1,1),(1,3)],
'Vectorizer__tokenizer':[ua_tokenizer_sklearn],
'feature_selection__threshold':[0.2,0.1,0.5],
'clf__units':[1000,500],
 'clf__n_classes':[2], 
'clf__n_layers':[3,2],
'clf__Dropout':[0.5,0.4],
'clf__epochs':[5],
 }], 


}


GridSearchCV_Classifiers(X=X_train_zab, y=y_train_zab,
models_params=models_params_zab,scoring='f1_macro',cv=3)

Зверніть увагу, що ми у проверяемая Pipeline додали проміжний крок перед класифікатором KerasClassifier. Клас ToarrayTransformer перетворює вектор Х у матрицю бібліотеки NumPy, без цього кроку ми не зможемо передати матрицю Х у класифікатор KerasClassifier.

А вісь результати нейронної мережі з оптимальними гіперпараметрами:

Рис. 1.1. Матриця невідповідностей для класифікації за типами ділянок


Рис. 1.2. Матриця невідповідностей класифікації за забудованістю ділянок

Як бачимо, у такий спосіб вдалося дещо покращити результати класифікації, альо ціною додаткових витрат в обчислювальному плані

Приклад згорткової нейронної мережі

А тепер спробуємо виристати якусь іншу архітектуру, наприклад згорткову нейромережу (convolutional neural network , CNN). CNN складається з шарів входу й виходу, а також з кількох прихованих. Приховані шарі CNN зазвичай складаються зі згорткових шарів (convolutional layers), агрегувальних шарів (pooling layers), повнозв'язних шарів (fully connected layers) і шарів нормалізації (normalization layers). Код:

import matplotlib.pyplot as plt
def plot_graphs(history, metric):
plt.plot(history.history[metric])
 plt.plot(history.history['val_'+metric], ")
plt.xlabel("Epochs")
plt.ylabel(metric)
 plt.legend([metric, 'val_'+metric])
plt.show()
def fit_print_conv(X_train, X_test, y_train, y_test,n_classes):
 # set parameters:
 max_features = 5000
 maxlen = 400
 batch_size = 32
 embedding_dims = 150
 filters = 500
 kernel_size = 3
 hidden_dims = 250
 epochs = 6

 t = Tokenizer(num_words=max_features)
 t.fit_on_texts(X_train) 
 X_train = t.texts_to_sequences(X_train)
 X_test = t.texts_to_sequences(X_test)
 print('Pad sequences (samples x time)')
 X_train = sequence.pad_sequences(X_train, maxlen=maxlen)
 X_test = sequence.pad_sequences(X_test, maxlen=maxlen)
 print('x_train shape:', X_train.shape)
 print('x_test shape:', X_test.shape)

 X_train, X_val, y_train, y_val=train_test_split(X_train,y_train,
stratify=y_train,
test_size=0.10,random_state=42)
 print('Build model...')
 model = Sequential()

 # we start off with an efficient embedding layer which maps
 # our vocab indices into embedding_dims dimensions
model.add(Embedding(max_features,
embedding_dims,
input_length=maxlen))
model.add(Dropout(0.2))

 # we add a Convolution1D, which will learn filters
 # word group of filters size filter_length:
model.add(Conv1D(filters,
kernel_size,
padding='same',
activation='relu'))
model.add(Dropout(0.1))
model.add(Conv1D(filters//10,
kernel_size,
padding='same',
activation='relu'))
model.add(Dropout(0.1))

 # we use max pooling:
model.add(GlobalMaxPooling1D())

 # We add a vanilla hidden layer:
model.add(Dense(hidden_dims))
model.add(Dropout(0.5))
model.add(Activation('relu'))

 # We project onto a single unit output layer, and squash it with a sigmoid:
model.add(Dense(n_classes))
model.add(Activation('sigmoid'))

model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
 history = model.fit(X_train, y_train,
batch_size=batch_size,
epochs=epochs,
 validation_data=(X_val, y_val))

 results = model.evaluate(X_test, y_test, verbose=2)
 print ('test loss: {0}, test acc: {1}'.format(results[0],results[1]))
 y_pred=model.predict_classes(X_test) 
 con_mat = tf.math.confusion_matrix(labels=y_test, predictions=y_pred)
print(con_mat.numpy())
 plot_graphs(history, 'accuracy')

fit_print_conv (X_train_zab, X_test_zab,np.array(y_train_zab), np.array(y_test_zab),2)
fit_print_conv (X_train, X_test, np.array(y_train)-1, np.array(y_test)-1,7) 

За основу для наведеної моделі я брав код звідси . Зверніть увагу, тут ми маємо приклад дещо іншої логіки у застосуванні вокабуляру токенів. Замість перетворювати текст у вектор розмірності вокабуляру із зазначенням наявності чи відсутності в цьому тексті вказаних у вокабулярі слів, ми замінюємо слова в тексті на їхні індекси у вокабулярі та приводимо отримані послідовності до вказаної довжини (за допомогою класу sequence.pad_sequences). Вісь результати.

Для класифікації за забудованістю ділянок:

Pad sequences (samples x time)
x_train shape: (2874, 400)
x_test shape: (1416, 400)
Build model...
Train on 2586 samples, validate on 288 samples
Epoch 1/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 0.4355 - accuracy: 0.8546 - val_loss: 0.3997 - val_accuracy: 0.8576
Epoch 2/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 0.2997 - accuracy: 0.8813 - val_loss: 0.2049 - val_accuracy: 0.9340
Epoch 3/6
2586/2586 [==============================] - 17s 7ms/sample - loss: 0.1293 - accuracy: 0.9509 - val_loss: 0.1607 - val_accuracy: 0.9410
Epoch 4/6
2586/2586 [==============================] - 17s 7ms/sample - loss: 0.0781 - accuracy: 0.9718 - val_loss: 0.2130 - val_accuracy: 0.9444
Epoch 5/6
2586/2586 [==============================] - 17s 7ms/sample - loss: 0.0625 - accuracy: 0.9811 - val_loss: 0.2324 - val_accuracy: 0.9306
Epoch 6/6
2586/2586 [==============================] - 17s 7ms/sample - loss: 0.0363 - accuracy: 0.9857 - val_loss: 0.2167 - val_accuracy: 0.9340
1416/1416 - 2s - loss: 0.2101 - accuracy: 0.9308
test loss: 0.21011744961563478, test acc: 0.9307909607887268
[[1163 50]
 [ 48 155]]

Для класифікації за типами ділянок:

Pad sequences (samples x time)
x_train shape: (2874, 400)
x_test shape: (1416, 400)
Build model...
Train on 2586 samples, validate on 288 samples
Epoch 1/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 1.1947 - accuracy: 0.6121 - val_loss: 0.7535 - val_accuracy: 0.6562
Epoch 2/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 0.6555 - accuracy: 0.8005 - val_loss: 0.5115 - val_accuracy: 0.8507
Epoch 3/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 0.4232 - accuracy: 0.8720 - val_loss: 0.3215 - val_accuracy: 0.9236
Epoch 4/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 0.2397 - accuracy: 0.9393 - val_loss: 0.2479 - val_accuracy: 0.9375
Epoch 5/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 0.1604 - accuracy: 0.9551 - val_loss: 0.2623 - val_accuracy: 0.9340
Epoch 6/6
2586/2586 [==============================] - 18s 7ms/sample - loss: 0.1293 - accuracy: 0.9640 - val_loss: 0.2831 - val_accuracy: 0.9340
1416/1416 - 2s - loss: 0.2571 - accuracy: 0.9350
test loss: 0.2570587779674153, test acc: 0.9350282549858093
[[857 2 8 5 0 0 1]
 [ 2 108 1 6 0 0 0]
 [ 24 4 262 0 0 0 0]
 [ 9 2 2 70 0 0 1]
 [ 10 0 0 3 0 0 0]
 [ 0 0 0 0 0 13 0]
 [ 8 4 0 0 0 0 14]]

Приклад рекурентної нейронної мережі

А тепер наведу приклад рекурентної нейронної мережі (recurrent neural networks , RNN). Ідея RNN полягає в послідовному використанні інформації. У традиційних нейронних мережах мається на увазі, що всі входи й виходи незалежні. Але для багатьох завдань це не підходить. Якщо ви хочете передбачити наступне слово в реченні, краще враховувати попередні слова. RNN називаються рекурентними, тому що вони виконують одну й ту ж задачу для шкірного елемента послідовності, причому вихід залежить від попередніх обчислень. Ще одна інтерпретація RNN: це мережі, в яких є «пам'ять», яка враховує попередню інформацію. Теоретично RNN можуть використовувати інформацію в доволі довгих послідовностях, але на практиці вони обмежені лише кількома кроками. Вісь код:

def fit_print_rnn(X_train, X_test, y_train, y_test,n_classes,units,epochs):
 # set parameters:
 max_features = 5000
 maxlen = 400
 batch_size = 32
 embedding_dims = 100
 kernel_size = 3
 hidden_dims = 250 


 t = Tokenizer(num_words=max_features)
 t.fit_on_texts(X_train) 
 X_train = t.texts_to_sequences(X_train)
 X_test = t.texts_to_sequences(X_test)


 print('Pad sequences (samples x time)')
 X_train = sequence.pad_sequences(X_train, maxlen=maxlen)
 X_test = sequence.pad_sequences(X_test, maxlen=maxlen)
 print('x_train shape:', X_train.shape)
 print('x_test shape:', X_test.shape)

 X_train, X_val, y_train, y_val=train_test_split(X_train,y_train,
stratify=y_train,
test_size=0.10,random_state=42)


 print('Build model...')
 model = Sequential()
model.add(Embedding(max_features,
embedding_dims,
input_length=maxlen
))
model.add(Dropout(0.2))

 model.add(tf.keras.layers.LSTM(units, return_sequences=True, dropout=0.4))
 model.add(tf.keras.layers.LSTM(units//2, dropout=0.4))
 model.add(Dense(n_classes, activation='sigmoid'))


model.compile(loss='sparse_categorical_crossentropy',
optimizer='adam',
metrics=['accuracy'])
 history = model.fit(X_train, y_train,
batch_size=batch_size,
epochs=epochs,
 validation_data=(X_val, y_val))

 results = model.evaluate(X_test, y_test, verbose=2)
 print ('test loss: {0}, test acc: {1}'.format(results[0],results[1]))
 y_pred=model.predict_classes(X_test) 
 con_mat = tf.math.confusion_matrix(labels=y_test, predictions=y_pred)
print(con_mat.numpy())
 plot_graphs(history, 'accuracy')
fit_print_rnn (X_train_zab, X_test_zab,np.array(y_train_zab), np.array(y_test_zab),2,64,5)
fit_print_rnn (X_train, X_test, np.array(y_train)-1, np.array(y_test)-1,7,196,10)

А вісь результати.

Для класифікації за забудованістю ділянок:

Pad sequences (samples x time)
x_train shape: (2874, 400)
x_test shape: (1416, 400)
Build model...
Train on 2586 samples, validate on 288 samples
Epoch 1/5
2586/2586 [==============================] - 24s 9ms/sample - loss: 0.4483 - accuracy: 0.8531 - val_loss: 0.4096 - val_accuracy: 0.8576
Epoch 2/5
2586/2586 [==============================] - 22s 9ms/sample - loss: 0.4058 - accuracy: 0.8561 - val_loss: 0.4192 - val_accuracy: 0.8576
Epoch 3/5
2586/2586 [==============================] - 22s 8ms/sample - loss: 0.2882 - accuracy: 0.8948 - val_loss: 0.2273 - val_accuracy: 0.9236
Epoch 4/5
2586/2586 [==============================] - 23s 9ms/sample - loss: 0.1716 - accuracy: 0.9490 - val_loss: 0.2336 - val_accuracy: 0.9167
Epoch 5/5
2586/2586 [==============================] - 22s 8ms/sample - loss: 0.1186 - accuracy: 0.9640 - val_loss: 0.1900 - val_accuracy: 0.9306
1416/1416 - 4s - loss: 0.1975 - accuracy: 0.9237
test loss: 0.19753522800523682, test acc: 0.9237288236618042
[[1142 71]
 [ 37 166]]

Для класифікації за типами ділянок:

Pad sequences (samples x time)
x_train shape: (2874, 400)
x_test shape: (1416, 400)
Build model...
Train on 2586 samples, validate on 288 samples
Epoch 1/10
2586/2586 [==============================] - 201s 78ms/sample - loss: 1.2284 - accuracy: 0.6114 - val_loss: 1.1348 - val_accuracy: 0.6181
Epoch 2/10
2586/2586 [==============================] - 202s 78ms/sample - loss: 0.9664 - accuracy: 0.6640 - val_loss: 0.6732 - val_accuracy: 0.7951
Epoch 3/10
2586/2586 [==============================] - 207s 80ms/sample - loss: 0.5501 - accuracy: 0.8534 - val_loss: 0.4591 - val_accuracy: 0.8542
Epoch 4/10
2586/2586 [==============================] - 206s 80ms/sample - loss: 0.4257 - accuracy: 0.8882 - val_loss: 0.4178 - val_accuracy: 0.8785
Epoch 5/10
2586/2586 [==============================] - 211s 82ms/sample - loss: 0.3595 - accuracy: 0.9033 - val_loss: 0.5131 - val_accuracy: 0.8715
Epoch 6/10
2586/2586 [==============================] - 212s 82ms/sample - loss: 0.2949 - accuracy: 0.9258 - val_loss: 0.4371 - val_accuracy: 0.8785
Epoch 7/10
2586/2586 [==============================] - 210s 81ms/sample - loss: 0.2411 - accuracy: 0.9331 - val_loss: 0.3719 - val_accuracy: 0.8993
Epoch 8/10
2586/2586 [==============================] - 212s 82ms/sample - loss: 0.2003 - accuracy: 0.9447 - val_loss: 0.3960 - val_accuracy: 0.8958
Epoch 9/10
2586/2586 [==============================] - 207s 80ms/sample - loss: 0.1740 - accuracy: 0.9466 - val_loss: 0.3882 - val_accuracy: 0.8993
Epoch 10/10
2586/2586 [==============================] - 213s 83ms/sample - loss: 0.1409 - accuracy: 0.9590 - val_loss: 0.4112 - val_accuracy: 0.9062
1416/1416 - 29s - loss: 0.3019 - accuracy: 0.9153
test loss: 0.3019262415983078, test acc: 0.9152542352676392
[[839 2 12 19 0 0 1]
 [ 3 102 7 0 0 4 1]
 [ 7 2 280 1 0 0 0]
 [ 10 2 1 70 0 1 0]
 [ 8 0 1 3 1 0 0]
 [ 0 11 0 0 0 2 0]
 [ 13 10 0 1 0 0 2]]

У попередньому прикладі я використав LSTM , одну із різновидів RNN. За основу я брав приклад звідси .

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

Модель word2vec

Як я вже згадав про нейронні мережі, необхідно наголосити, що існують інші моделі для векторного представлення слів, наприклад word2vec , а не лише bag-of-words. Я тестував реалізацію моделі word2vec за допомогою бібліотеки Gensim, але, очевидно, у мене банально надто мала вибірка — навіть модель, побудована на всіх наявних у мене даних, не дає більш-менш прийнятних результатів. Вісь реалізації:

def dataset_to_Word2Vec(X,model_dir='word2vec_gensim.bin',ua_stemmer=False):
print('Word2Vec')
try:
 model = Word2Vec.load(model_dir)
 except IOError:
 X=X. map(lambda x: ua_tokenizer(x,ua_stemmer=ua_stemmer))
 model = Word2Vec(X, size=1000, min_count=10, workers=-1) 
 model.train(X, total_examples=model.corpus_count, epochs=10000)
model.init_sims(replace=True)
model.save(model_dir)
finally:
 return model

model = dataset_to_Word2Vec(land_data['text'],model_dir='word2vec_gensim_all_corpus.bin')

А вісь результат пошуку «схожих» слів (у розумінні word2vec) для декількох заданих:

print ("Слово 'ділянка' - ", model.wv.most_similar('ділянка'))
print ("Слово 'земельна' - ",model.wv.most_similar('земельна')) 
print ("Слово 'будинку' - ",model.wv.most_similar('будинку')) 

Слово 'ділянка' - [('мрію', 0.12444563210010529), ('присвоєний', 0.1221877932548523), ('гори', 0.1125452071428299), ('господарства', 0.10289157181978226), ('неї', 0.10067432373762131), ('пилипець', 0.10029172897338867), ('зовсім', 0.09704037010669708), ('потічок', 0.09689418971538544), ('широка', 0.09640874713659286), ('проходити', 0.09575922787189484)]
Слово 'земельна' - [('увазі', 0.1161714568734169), ('хуст', 0.10643313825130463), ('зведення', 0.10264638066291809), ('початкова', 0.1005157008767128), ('зведено', 0.09715737402439117), ('гакадастровий', 0.095176562666893), ('тзов', 0.09422482550144196), ('колії', 0.09348714351654053), ('суховолі', 0.09305611252784729), ('електричка', 0.09153789281845093)]
Слово 'будинку' - [('різного', 0.11177098006010056), ('садочка', 0.10531207919120789), ('приватизований', 0.10071966052055359), ('облаштування', 0.0977017879486084), ('станція', 0.09768658876419067), ('плай', 0.09451328217983246), ('житловими', 0.08689279854297638), ('спарку', 0.08635914325714111), ('тихо', 0.08573484420776367), ('грушів', 0.0851108729839325)]

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

Висновки

У статті я навів приклади реалізації моделі «мішка слів» за допомогою трьох бібліотек: NLTK, scikit-learn і TensorFlow. На прикладі побудови моделі «мішка слів» для класифікації оголошень з продажу земельних ділянок в Україні видно, що кожна з бібліотек має свою специфіку, підводні камені й проблеми, на розв'язків язання яких я витратив досить багато часу. Сподіваюся, що наведені приклади й пояснення комусь і справді допоможуть.

А щодо мене, то я вирішив у своєму проєкті використати комбіновану модель машинного навчання на основі стекінгу (приклад у частині 2 ). Зрозуміло, що для задачі класифікації за типами ділянок, мої гіпотези підтвердились: використовуючи модель «мішка слів» на відносно малому вокабулярі унікальних токенів, можна побудувати модель із достатньою точністю, а для задачі класифікації за забудованістю отримана точність класифікаторів не така гарна. Цю проблему я ще спробую вирішити шляхом збільшення вибірки або зміною міри якості (нагадаю, у прикладах використовується F1 score), але це вже справа майбутнього.

Тут важливо розуміти предметну галузь і пов'язані з нею проблеми. По-перше, для роботи мені значно важливіше було підготувати якісну модель для класифікації за типами (чого я і досягнув), класифікацію за забудованістю я завжди розглядав як другорядне завдання. По-друге, треба розуміти, що довіряти текстом оголошення на 100% не можна в принципі, оскільки велика частина «продавців» самі не знають, що вони продають і «який там тип тої ділянки й чи вона забудована». Отож загалом я вважаю, що отримав хороші результати.

Приклад, який я навів, добре показує, що далеко не завжди методи, дієві для однієї задачі в вузькоспеціалізованій галузі, будуть так само ефективні для іншої, навіть подібної, задачі у цій же галузі. Тож постійно потрібно експериментувати, щоб отримати кращі результати. Усім дякую за увагу!

Список рекомендованих джерел:


Читайте також попередні частини циклу:

Опубліковано: 26/03/20 @ 08:00
Розділ Різне

Рекомендуємо:

Як ІТ-спеціалісти працюють віддалено на карантині. Фотоогляд
13 способів професійного розвитку для менеджерів і не тільки
PM дайджест #24: як мітинги підвищують продуктивність команди, список питань для зустрічей 1:1
Strategi bermain Poker online secara benar
Вчимося відчувати себе краще, або навіщо нам wellbeing