Время прочтения: 4 мин.

Про решающие деревья уже писали и на JAVA их показывали. При этом все знают деревья сами по себе работают, мягко говоря, не очень. Поэтому, зачем нужно одно плохое дерево, если есть отличный лес. Про лес мы сегодня и расскажем. Конечно, тут может быть ремарка, что «про это уже сто раз рассказывали» – да, но это будет рассказ про лес на JAVA.

Достоинства случайного леса перечислять глупо – любой, кто работает в DS больше одного дня и решивший хоть что-либо сложнее «Титаника» про него знает. Но, все-таки, давайте укажем на именно самые критичные:

— Лес годно отрабатывает пропуски, а пропуски данных в нашей практике — это очень частое дело.

— Он хорошо отрабатывает большие массивы данных. И, в нашем случае, это тоже критично, т.к. наши датасеты с ярдом-другим строк могут легко подвесить любую другую модель этак на месяц-другой и без гарантии на развешивание.

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

Так у нас возникла задача построить модель на основе случайного леса. При этом в Python из-за размера обрабатываемых данных нам это быстро сделать не удалось, скорость работы оставляла желать лучшего — датасет на 1,5 миллиарда строк после работы скрипта в течение нескольких дней был обработан где-то на 17-20%. И здесь нам на помощь приходит JAVA — тестовый датасет около 500 тысяч строк отработал в 6 раз быстрее Python.  Забегая немного вперед скажем, что полная обработка датасета из 1,5 миллиарда строк на JAVA заняла почти 9 часов.

Вот образец датасета, который нам пришлось анализировать в рамках проверки данных по модели (целиком он выглядит достаточно страшно):

idfactor1factor2factor499factor500
126456411NULL
2NULL4324
106400510005456492NULL

Random Forest отработает и регрессию, и классификацию, нам как раз нужна классификация – поэтому импортируем нужное, а именно собственно сам классификатор и классы для работы с вводом выводом.

import java.io.File;
import java.io.IOException;

import weka.classifiers.Classifier;
import weka.classifiers.Evaluation;
import weka.classifiers.trees.RandomForest;
import weka.core.Instances;
import weka.core.converters.ArffLoader;
import weka.filters.Filter;
import weka.filters.unsupervised.attribute.StringToWordVector;

Весь лес у нас будет размещен в классе RandomForestExample, в котором мы сразу определим нужные поля и методы. Поля нам укажут расположение файлов с нужными нам данными для формирования датасета. Методы произведут загрузку данных из файлов, построение леса и отработку dataset на построенной модели. В общем виде класс с лесом может выглядеть вот так:

public class RandomForestExample {
    // Поля с адресом и названием файла dataset
    public static final String DATA_SET_TRAIN_FILE="randomforest-train.arff";
     public static final String DATA_SET_TEST_FILE="randomforest-test.arff";

     public static Instances loadDataSet(String dataSetName) throws IOException {
           // Метод для загрузки dataset.
     }

     public static void makeRandomForest() throws Exception {
         // Метод для работы с моделью.
      }

     public static void printResult() throws Exception {
         // Метод для отображения результатов отработки модели.
      }

}

Разберем методы подробнее. Для начала производим загрузку dataset из файла с данными. Здесь не особо критична внутренняя реализация – это может быть загрузка из файла или загрузка из базы, из облака и т.п. На выходе получаем датасет в формате пригодном для отработки моделью RandomForest.

public static Instances loadDataSet (String dataSetName) throws IOException {
    int classIndex = 1;
    
    ArffLoader loaderDataSet = new ArffLoader();
    loaderDataSet.setSource(RandomForestExample.class.getResourceAsStream("/" + fileName));
    Instances dataSet = loader.getDataSet();
    dataSet.setClassIndex(classIndex);

    return dataSet;
}

Далее с помощью метода makeRandomForest можно построить модель леса на полученном датасете. Первым делом производится загрузка dataset из файла с данными, после чего строится модель и отрабатывает на тренировочных и тестовых данных.

public static RandomForest makeRandomForest () throws Exception {

    Instances trainingDataSet = getDataSet(DATA_SET_TRAIN_FILE);
    Instances testingDataSet = getDataSet(DATA_SET_TEST_FILE);
    RandomForest randomForest=new RandomForest();

    randomForest.setNumTrees(20);
    randomForest.buildClassifier(trainingDataSet);

    Evaluation eval = new Evaluation(trainingDataSet);
    eval.evaluateModel(randomForest, testingDataSet);

    return randomForest;
}

После построения и тренировки модели, посчитаем основные статистики и отобразим их для оценки результата работы. Ниже приведен пример метода для вывода результатов отработки модели.

public static void result (RandomForest randomForest) throws Exception {
    System.out.println("Результаты анализа");
    System.out.println(eval.toSummaryString());
    System.out.print("Дополнительная информация по алгоритму");
    System.out.println(randomForest);
    System.out.println(eval.toMatrixString());
    System.out.println(eval.toClassDetailsString());
}

Основные статистики результата работы модели будут выглядеть примерно вот так:

Итак, результаты отработки модели получены. Мы считываем исходные данные, преобразуем их в формат, пригодный для отработки моделью RandomForest и можно строить модель. Комбинация методов мощного языка JAVA позволила нам осуществить построение модели случайного леса на очень большом датасете. При этом построенная модель достаточно оперативно производит обработку данных. Модель с лесом отработает намного медленнее, чем модель с решающими деревьями, но это всё равно несопоставимо быстрее Python. В нашей работе мы все чаще сталкиваемся с датасетами очень большого размера, и скорость обработки данных становится критичной, в результате приходится искать программные решения, которые позволят эту скорость обеспечить.