MxNet для табличных данных

Недавно я работал над задачей построения модели двоичной классификации с использованием MxNet для табличных данных. Мой набор данных представлял собой смесь числовых и категориальных переменных, и я хотел предварительно обработать каждый тип отдельно, прежде чем передавать комбинацию в функцию потерь. Чтобы усложнить задачу, я хотел объединить табличные данные с текстовыми данными. Проще говоря, у меня было три типа входных данных для моей модели - числовой, категориальный и текстовый - каждый из которых подвергался собственному рабочему процессу предварительной обработки.

В Интернете доступно множество примеров, которые показывают, как создавать модели MxNet для распознавания изображений, генерации текста, классификации текста / изображений / видео. Все они очень полезны, чтобы дать толчок усилиям в этих областях, если мы хотим имитировать что-то подобное. MxNet-Gluon предоставляет множество объектов DataLoader, которые помогают загружать данные обучения / тестирования, обычно доступные в виде дампов корпусов. Однако ни один из них не подходит для табличных данных, особенно когда есть несколько входных данных, каждый из которых поступает из разных источников. Покопавшись во многих примерах, коде и документации, я нашел несколько интересных способов добиться всего, что хотел. Делюсь своими открытиями здесь -

Закладки:

  1. Gluon: BasicBlocks http://mxnet.incubator.apache.org/versions/master/tutorials/gluon/gluon.html
  2. Глюон: HybridBlocks https://mxnet.incubator.apache.org/versions/master/tutorials/gluon/hybrid.html
  3. Документация: https://mxnet.apache.org/versions/master/api/python/gluon/gluon.html

Типы данных:

  1. mx.ndarray: https://mxnet.incubator.apache.org/api/python/ndarray/ndarray.html
  2. NDArrayIter: https://mxnet.incubator.apache.org/api/python/io/io.html

Шаги, которые я опишу в этой статье, кратко изложены ниже.

  1. Загрузка (нескольких) входов в объекты NDArrayIter, которые передаются в модуль нейронной сети
  2. Создание модуля нейронной сети, обеспечивающего гибкость предварительной обработки различных сегментов входных данных.
  3. Обучение и оценка нейронной сети
  4. Прогноз

1. Загрузка данных в NDArrayIter

Данные читаются с использованием нескольких типов загрузчиков, каждый из которых может быть настроен для поддержки разных форматов. Для чтения данных из нескольких источников вы можете создать свой Итератор или использовать объект NDArrayIter. Я выбрал последнее.

Из всех параметров, которые используются при создании экземпляра объекта NDArrayIter, два - данные и метки - могут быть объектами словарей.
Допустим, вы загружаете числовые данные, категориальные данные и текстовые данные в три разных объекта mx.nd.array, тогда dict данных можно построить следующим образом

data =  {'numeric_data': numeric_data, 'categorical_data':categorical_data, 
'text_data': text_data}

где числовые_данные, категориальные_данные и текстовые_данные - это объекты mx.nd.array.

Я опишу процесс создания объекта NDArrayIter, используя фиктивный пример, который отражает все требования, которые у меня были в моей задаче ниже.

Предположим, что ваш ввод поступил форма файла Excel со следующим заголовком
labelnumeric1numeric2categorical1categorical2text label: двоичный 0/1
числовой *: числовые функции: может иметь много пропущенных значений.
категориальный *: категориальные особенности, у некоторых есть много категорий, а у некоторых - несколько. В этом примере предположим, что следующий
category1: имеет более 20000 категорий
category2: has ‹= 10 категорий
text: неструктурированный текст

Чтобы немного усложнить проблему интересно, допустим, этот набор данных несбалансирован и смещен в сторону класса 0.

Цель здесь - создать «хорошую» модель двоичной классификации, которая принимает на вход все эти типы переменных.

В моем конкретном примере я хотел бы сделать следующее

  1. У вас есть способ обрабатывать числовые функции, возможно, исследуйте этап конкатенации здесь
  2. Создавайте вложения для категориальных функций с большим количеством категорий
  3. Создавайте одно горячие вложения для категориальных функций с управляемым количеством категорий
  4. Создавать вложения текста
  5. Объедините все вышеперечисленное и перейдите на этап, оптимизирующий двоичную классификацию.

Этот Excel был загружен в фрейм данных pandas, и

числовые столбцы функций были изменены для обработки

  1. Отсутствующие значения - где использовалось среднее значение столбца
  2. numeric_data [‘numeric1’]. fillna ((numeric_data [‘numeric1’]. mean ()), inplace = True)
  3. Нормализованный - на основе среднего и стандартного отклонения
  4. numeric_data [‘numeric1’] = (numeric_data [‘numeric1’] - numeric_data [‘numeric1’]. mean ())
    / numeric_data [‘numeric1’]. std ()

категориальные особенности рассматривались отдельно

  1. были ли те, которые требовали одноразового встраивания, были предоставлены дополнительные столбцы
  2. one_hot_encodings = pd.get_dummies (small_categorical_data,
    columns = [‘category2’])
  3. все были факторизованы
  4. factorized_categorical1, levels = pd.factorize (category_data [‘category1’]. values)

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

Как правило, 70% данных используется для обучения и 30% для оценки. Вы можете использовать функцию sklean train_eval_split. Однако я предпочитаю делать это независимо и конвертировать в объекты массива данных mxnet. Это помогло мне лучше отладить. Убедитесь, что ваши ярлыки также разделены соответствующим образом.

numeric_train_data = mx.nd.array(numeric_train, dtype='float32')

Объект data dict, который предоставляется в качестве параметра для объекта NDArrayIter, теперь может быть сконструирован следующим образом

data_train = {'numeric_data': numeric_train_data, 'one_hot_data':one_hot_data, 
'categorical_data': categorical_data}
label_train = {'label':label_train}

Приведенный выше образец кода суммирует все, что вам нужно, если вы хотите использовать различные входные данные для своей модели. Значения объекта словаря data_train можно получить из любого источника, если вы убедитесь, что они нормализованы и разложены на множители (для категориальных значений) и относятся к типу mx.ndarray.

Сделайте то же самое и для данных оценки. - data_eval и label_eval.

Теперь данные читаются с помощью объекта NDArrayIter.

dataiter = mx.io.NDArrayIter(data_train, label_train, BATCH_SIZE, True, last_batch_handle='discard')
valdataiter = mx.io.NDArrayIter(data_eval, label_eval, BATCH_SIZE, True, last_batch_handle='discard')

2. Создание нейронной сети.

Как указывалось ранее, здесь требуется предварительно обрабатывать каждый тип данных по-разному и, наконец, объединить их и передать в функцию потерь для двоичной классификации. Архитектура выглядит следующим образом.


MxNet предоставляет эти замечательные блоки, называемые HybridBlocks, которые объединяют декларативное программирование и императивное программирование. Их легко отладить и понять. Чтобы узнать больше о том, как оптимизировать эти блоки, прочтите документацию по «гибридизации». Я не собираюсь говорить об этом здесь, вместо этого я сосредоточусь на том, как кодировать HybridBlock таким образом, чтобы он принимал несколько входных данных и по-разному обрабатывал их.

Основываясь на архитектуре, показанной на рис. 1, я сначала объяснит этап ConcatBlock.
Этап ConcatBlock определяется классом ConcatBlock следующим образом

class ConcatBlock(gluon.nn.HybridBlock):
    def __init__(self, input_dim1,
                 embedding_dim1,  
                 **kwargs):
        gluon.nn.HybridBlock.__init__(self, **kwargs) # attention here to pass kwargs to initialization of hybridblock
        with self.name_scope():
           
            #categorical embeddings
            self.embedding_stage = make_embedding_stage(kwargs.get('input_dim1',"default value"), 
                        kwargs.get('embedding_dim',"default value"))
           
            #embeddings postprocessing  
            self.embedding_post = make_post_processing_stage(32)
            
            #onehot preprocessing  
            self.onehot_stage = make_onehot_stage()
            
            #numeric processing
            self.numeric_stage = make_numeric_stage(32, 1)
            
            #concatenation processing
            self.concat_stage = make_post_processing_stage(16)
            
            #last layer
            self.binary_activation = nn.Dense(1, activation="sigmoid")
           
     def hybrid_forward(self, F, numeric_data, one_hot_data, categorical_data):
        
        out1 = self.embedding_stage(categorical_data)
        Y_embeddings = self.embedding_post(out1)
        Y_encoded = self.onehot_stage(one_hot_data)
        Y_numeric = self.numeric_stage(numeric_data)
        
        Y_concat = F.concat(Y_embedded, Y_numeric, Y_encoded)
        Y_all = self.concat_stage(Y_concat)
        Y = self.binary_activation(Y_all)
        
        return Y

Класс - это HybridBlock, который определяет различные этапы, которые входят в модель нейронной сети.
Каждый этап представляет собой блок HybridSequential, который дополнительно определяет этапы, которые выполняются на входе, переданном в этот последовательный блок. Что входит в каждый этап, определяется функцией hybrid_forward. Параметры этой функции - это, по сути, различные входные данные, на которые ссылаются при создании модели нейронной сети.

В определенном выше ConcatBlock я настроил четыре блока HybridSequential: make_embedding_stage, make_post_processing_stage, make_onehot_stage, make_numeric_stage - каждый работает с одним типом ввода. Пример для make_embedding_stage показан ниже.

class EmbeddingBlock(HybridBlock):
    def __init__(self, input_dim, embedding_dim, **kwargs):
        super(EmbeddingBlock, self).__init__(**kwargs)
        self.body = nn.HybridSequential()
        self.body.add(
                      nn.Embedding(input_dim, embedding_dim,
                                        weight_initializer=mx.init.Uniform(0.1)),
                      nn.BatchNorm(),
                      nn.Flatten(),
                      nn.Dropout(0.5)
                     )
       
    def hybrid_forward(self, F, x):
        x = self.body(x)
        return x
#note that in the below function you may not need a HybridSequential #stage if it contains only one block - showing it here in case one 
#would want to add more stages, which could be the case if you want #to repeat the EmbeddingBlock stage "n" times
def make_embedding_stage(input_dim, embedding_dim):
    stage = nn.HybridSequential()
    stage.add(EmbeddingBlock(input_dim=input_dim, embedding_dim=embedding_dim))
    return stage

Другие этапы также определяются аналогичным образом, когда настраиваемый блок, имеющий набор операций, добавляется как этап в блоке HybridSequential. Вы можете подумать о добавлении настраиваемого блока LSTM, который работает с текстовыми данными, и сделать из него стадию для добавления в ConcatBlock.

Теперь, когда мы определили нейронную сеть, мы можем создать экземпляр сети следующее

net = ConcatBlock("input_dim"=input_dim, "embedding_dim"=embedding_dim)

3. Обучение и оценка

Инициализация параметров модели выполняется обычным способом.

BATCH_SIZE = 32
LEARNING_R = 0.001
EPOCHS = 10
THRESHOLD = 0.5
#initialize framework net
ctx = mx.cpu()
net.collect_params().initialize(mx.init.Xavier(), force_reinit=True)
loss = gluon.loss.L2Loss()
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': LEARNING_R})
#set the metrics to evaluate
accuracy = mx.metric.Accuracy()
f1 = mx.metric.F1()

Вы можете попробовать разные скорости обучения, функции потерь (SigmoidCrossEntropyLoss) и разные оптимизаторы. Также обратите внимание, что я установил контекст на cpu. Вы можете перейти на gpu, но помните об изменениях, которые вам нужно сделать, чтобы сбалансировать классы. Я не говорю об этом здесь.

Процесс обучения и оценки модели показан ниже.

training_loss = []
validation_loss = []
dataiter = mx.io.NDArrayIter(data, label, BATCH_SIZE, True, last_batch_handle='discard')
valdataiter = mx.io.NDArrayIter(data_eval, label_eval, BATCH_SIZE, True, last_batch_handle='discard')
     
accuracy_lst = []
for e in range(EPOCHS):
    dataiter.reset()
    valdataiter.reset()
    
    all_val_labels = np.empty((0,1), int)
    all_val_predictions = np.empty((0,1), float)
    
    avg_train_loss = train_model(dataiter)
    avg_val_loss, all_val_labels, all_val_predictions  = validate_model(valdataiter, all_val_labels, all_val_predictions)
  
    print("Epoch: %s, Training loss: %.4f, Validation loss: %.4f, Validation accuracy: %.4f, F1 score: %.4f" %
         (e, avg_train_loss, avg_val_loss, accuracy.get()[1], f1.get()[1]))

Объекты итератора данных для обучения и проверки создаются, как показано в разделе 1, а затем сбрасываются в каждую эпоху. Для каждой эпохи вычисляются потери при обучении и проверке, а также вычисляется точность проверки и f1.

Функция train_model вызывает нейронную сеть следующим образом

def train_model(dataiter):
    cumulative_train_loss = 0.
    total_size = 0
    for batch in dataiter:
      
        with autograd.record(): # Start recording the derivatives
            # balance the classes
            sample_weight = generate_sample_weights(batch.label[0])
          
            y_hat = net(batch.data[0], batch.data[1], batch.data[2])
            l = loss(y_hat, batch.label[0], sample_weight)
        l.backward()
               
        trainer.step(batch.data[0].shape[0])
            
        # Provide stats on the improvement of the model over each epoch
        train_loss = nd.sum(l).asscalar()
        cumulative_train_loss += train_loss
        total_size += batch.data[0].shape[0]
        
    return cumulative_train_loss/total_size

Как упоминалось ранее, в моей задаче была проблема с данными о дисбалансе. MxNet предоставляет способ взвешивания классов, и это я реализовал в функции generate_sample_weights.

def generate_sample_weights( true_labels ):
    sample_weight = [ 3 if i == 1 else 1 for i in true_labels ]
    return mx.nd.array(sample_weight, dtype=float32)

Функция оценки для каждой эпохи показана ниже.

def validate_model(valdataiter, all_val_labels, all_val_predictions):
    cumulative_val_loss = 0.
    total_size = 0
    for batch in valdataiter:
        # Do forward pass on a batch of validation data
        output = net(batch.data[0], batch.data[1], batch.data[2])
        # Similar to cumulative training loss, calculate cumulative validation loss
        cumulative_val_loss += nd.sum(loss(output, batch.label[0])).asscalar()
        total_size += batch.data[0].shape[0]
        # Converting neuron outputs to classes
        predicted_classes = mx.nd.ceil(output - THRESHOLD)
        # Update validation accuracy
        accuracy.update(batch.label[0], predicted_classes)
      
        # calculate probabilities of belonging to different classes. F1 metric works only with this notation
        prediction = output.reshape(-1)
        probabilities = mx.nd.stack(1 - prediction, prediction, axis=1)
        f1.update(batch.label[0], probabilities)
        
        all_val_labels = np.append(all_val_labels, batch_numpy)
        all_val_predictions = np.append(all_val_predictions, pred_numpy)
        
    return cumulative_val_loss/total_size, all_val_labels, all_val_prediction

Теперь постройте ROC для набора для проверки, чтобы определить качество модели нейронной сети.

#plotting roc for validation
fpr_val, tpr_val, thresholds_val = metrics.roc_curve(all_val_labels, all_val_predictions)
roc_auc = metrics.auc(fpr_val, tpr_val)
# method I: plt
import matplotlib.pyplot as plt
plt.title('Receiver Operating Characteristic')
plt.plot(fpr_val, tpr_val, 'b', label = 'AUC = %0.2f' % roc_auc)
plt.legend(loc = 'lower right')
plt.plot([0, 1], [0, 1],'r--')
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()

В этом упражнении я не показал способы гибридизации модели и не показал, как сохранить промежуточную модель и перезапустить процесс обучения. Надеюсь, я смогу рассказать об этом в другой статье. Однако на данный момент я думаю, что приведенных здесь объяснений будет достаточно для тех, кто хочет запустить иерархическую модель MxNet на табличных данных.