Java – темный случайный лес
Про решающие деревья уже писали и на JAVA их показывали. При этом все знают деревья сами по себе работают, мягко говоря, не очень. Поэтому, зачем нужно одно плохое дерево, если есть отличный лес. Про лес мы сегодня и расскажем. Конечно, тут может быть ремарка, что «про это уже сто раз рассказывали» – да, но это будет рассказ про лес на JAVA.
Достоинства случайного леса перечислять глупо – любой, кто работает в DS больше одного дня и решивший хоть что-либо сложнее «Титаника» про него знает. Но, все-таки, давайте укажем на именно самые критичные:
— Лес годно отрабатывает пропуски, а пропуски данных в нашей практике — это очень частое дело.
— Он хорошо отрабатывает большие массивы данных. И, в нашем случае, это тоже критично, т.к. наши датасеты с ярдом-другим строк могут легко подвесить любую другую модель этак на месяц-другой и без гарантии на развешивание.
В рамках проведения аудита одной модели нам нужно было проверить насколько адекватно считаются показатели при принятии решения. В ходе проверки мы увидели наличие отклонения от нормального значения данных. Но, чтобы это проверить, нужно произвести воспроизведение модели.
Так у нас возникла задача построить модель на основе случайного леса. При этом в Python из-за размера обрабатываемых данных нам это быстро сделать не удалось, скорость работы оставляла желать лучшего — датасет на 1,5 миллиарда строк после работы скрипта в течение нескольких дней был обработан где-то на 17-20%. И здесь нам на помощь приходит JAVA — тестовый датасет около 500 тысяч строк отработал в 6 раз быстрее Python. Забегая немного вперед скажем, что полная обработка датасета из 1,5 миллиарда строк на JAVA заняла почти 9 часов.
Вот образец датасета, который нам пришлось анализировать в рамках проверки данных по модели (целиком он выглядит достаточно страшно):
id | factor1 | factor2 | … | factor499 | factor500 |
1 | 2 | 64564 | … | 11 | NULL |
2 | NULL | … | 4 | 324 | |
… | … | … | … | … | … |
1064005100 | 0 | 5456492 | … | NULL |
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. В нашей работе мы все чаще сталкиваемся с датасетами очень большого размера, и скорость обработки данных становится критичной, в результате приходится искать программные решения, которые позволят эту скорость обеспечить.