Перші кроки в 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 filters of 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% не можна в принципі, оскільки велика частина «продавців» самі не знають, що вони продають і «який там тип тої ділянки й чи вона забудована». Отож загалом я вважаю, що отримав хороші результати.

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

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


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

Похожие статьи:
Цього разу героєм рубрики, що присвячена технічним співбесідам, є Golang (або просто Go). Як завжди, ми з’ясували у реальних СТО та інших...
В п’ятницю, 10 березня, президент України Володимир Зеленський ввів у дію рішення Ради національної безпеки та оборони України...
В Україні — найбільша кількість розробників штучного інтелекту, тож доречно їх усіх обʼєднати в межах системи Дія City, — про...
Фахівці з Opendatabot підготували рейтинг топових підприємств України. Під час дослідження враховували як фінансові показники...
Современный IT-рынок очень напоминает борьбу королевств за железный трон в «Игре престолов». Но сейчас не время распрей!...
Яндекс.Метрика