Перші кроки в 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% не можна в принципі, оскільки велика частина «продавців» самі не знають, що вони продають і «який там тип тої ділянки й чи вона забудована». Отож загалом я вважаю, що отримав хороші результати.
Приклад, який я навів, добре показує, що далеко не завжди методи, дієві для однієї задачі в вузькоспеціалізованій галузі, будуть так само ефективні для іншої, навіть подібної, задачі у цій же галузі. Тож постійно потрібно експериментувати, щоб отримати кращі результати. Усім дякую за увагу!
Список рекомендованих джерел:
- Hands-On Machine Learning with Scikit-Learn and TensorFlow by Aurelien Geron — O’Reilly;
- Python Data Science Handbook by Jake VanderPlas — O’Reilly;
- Deep Learning with Keras by Antonio Gulli, Sujit Pal — Packt;
- scikit-learn.org/stable;
- keras.io;
- tensorflow.org.
Читайте також попередні частини циклу:
- Перші кроки в NLP: розглядаємо Python-бібліотеку NLTK в реальному завданні
- Перші кроки в NLP: розглядаємо Python-бібліотеку scikit-learn в реальному завданні