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

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

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


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

Похожие статьи:
Володимир вступив на бюджет до медичного університету, але кинув його, щоб піти на фронт. Був серед перших десятків бійців «Азову» —...
Для чого потрібні Chrome DevTools? Буває так, що написаний код невідомо чому відмовляється працювати. Щоб швидко з’ясувати, у чому проблема...
Компания «Связной» объявила о возможности приобрести браслеты Xiaomi теперь и в магазинах розничной сети, а также о старте продаж...
Статья написана в соавторстве с Мэри Ротарь, CEO IAMPM. Привет, меня зовут Кирилл Белявский, уже больше 5 лет работаю Lead BA в Softserve....
Цього року Україна вдруге представить власний павільйон на технологічній конференції Web Summit, що відбудеться...
Яндекс.Метрика