Машинное обучение для Java-разработчиков, часть 1
Оценка целевой функции
Напомним, целевая функцияhθ
, она же — функция предсказаний, является результатом процесса подготовки или тренировки. Математически сложность состоит в том, чтобы найти функцию, которая получает на вход переменную х
и возвращает предсказанное значение у
.
В машинном обучении функция стоимости
(J(θ))
используется для вычисления ошибки значения или «стоимости» заданной целевой функции.
Функция стоимости показывает, насколько точно модель соответствует тренировочным данным. Для определения стоимости целевой функции, показанной выше, необходимо рассчитать квадратичную ошибку каждого примера дома
(i)
. Ошибка – расстояние между расчетным значением у
и настоящим значением y
дома из примера i
.
Например, реальная цена дома площадью 1330 = 6,500,000 €. А отличие предсказанной цены дома обученной целевой функцией составляет 7,032,478 €: разница (или ошибка) равна 532,478 €. Вы также можете увидеть эту разницу на графике выше. Разница (или ошибка) показана в виде вертикальных пунктирных красных линий для каждой тренировочной пары цена – площадь.
Высчитав стоимость обученной целевой функции, нужно просуммировать квадраты ошибки для каждого дома в примере и рассчитать основное значение. Чем меньше значение цены
(J(θ))
, тем точнее будут предсказания нашей целевой функции.
В листинге-3, приведена простая реализация на Java функции стоимости, принимающей на вход целевую функцию, список тренировочных данных, и метки связанные с ними. Значения предсказаний будут вычисляться в цикле, и ошибка будет вычисляться вычитанием реального значения цены (взятого из метки).
Позже квадрат ошибок будет просуммирован и значение ошибки будет рассчитано. Стоимость будет возвращена как значение типа double
:
Листинг-3
public static double cost(Function<ltDouble[], Double> targetFunction,
List<ltDouble[]> dataset,
List<ltDouble> labels) {
int m = dataset.size();
double sumSquaredErrors = 0;
// рассчет квадрата ошибки («разницы») для каждого тренировочного примера и //добавление его к сумме
for (int i = 0; i < m; i++) {
// получаем вектор признаков из текущего примера
Double[] featureVector = dataset.get(i);
// предсказываем значение и вычисляем ошибку базируясь на реальном
//значении (метка)
double predicted = targetFunction.apply(featureVector);
double label = labels.get(i);
double gap = predicted - label;
sumSquaredErrors += Math.pow(gap, 2);
}
// Вычисляем и возращаем значение ошибки (чем меньше тем лучше)
return (1.0 / (2 * m)) * sumSquaredErrors;
}
Интересно читать о Java? Вступайте в группу Java Developer! |
Обучение целевой функции
Несмотря на то, что функция стоимости помогает оценить качество целевой функции и тета-параметров, вы все же надо найти самые подходящие параметры тета. Вы можете использовать для этого алгоритм градиентного спуска.Градиентный спуск
Градиентный спуск минимизирует функцию стоимости. Это значит, что он используется для поиска параметров тета, которые имеют минимальную стоимость(J(θ))
на основе тренировочных данных.
Вот упрощенный алгоритм вычисления новых, более подходящих значений тета:
Так вот, параметры вектора тета будут улучшаться с каждой итерацией алгоритма. Коэффициент обучения α задает количество вычислений на каждой итерации. Эти вычисления можно проводить, пока не найдены «хорошие» значения тета. Для примера, функция линейной регрессии ниже имеет три параметра тета:
На каждой итерации будет вычислено новое значение для каждого из параметров тета:
θ0
, θ1
, и θ2
. После каждой итерации, можно создать новую, более соответствующую реализацию LinearRegressionFunction
используя новый тета вектор {θ0, θ1, θ2}.
В листинге-4 приведен Java-код алгоритма градиентного спада. Тета для функции регрессии будут обучены с использованием тренировочных данных, данных маркеров, коэффициента обучения (α)
. Результатом будет улучшенная целевая функция, использующая параметры тета. Метод train()
будет вызываться снова и снова, и передавать новую целевую функцию и новые параметры тета из предыдущих вычислений. И эти вызовы будут повторяться, пока настроенная целевая функция не достигнет плато минимума:
Листинг-4
public static LinearRegressionFunction train(LinearRegressionFunction targetFunction,
List<ltDouble[]> dataset,
List<ltDouble> labels,
double alpha) {
int m = dataset.size();
double[] thetaVector = targetFunction.getThetas();
double[] newThetaVector = new double[thetaVector.length];
// вычисление нового значения тета для каждого элемента тета массива
for (int j = 0; j < thetaVector.length; j++) {
// сумируем разницу ошибки * признак
double sumErrors = 0;
for (int i = 0; i < m; i++) {
Double[] featureVector = dataset.get(i);
double error = targetFunction.apply(featureVector) - labels.get(i);
sumErrors += error * featureVector[j];
}
//вычисляем новые значения тета
double gradient = (1.0 / m) * sumErrors;
newThetaVector[j] = thetaVector[j] - alpha * gradient;
}
return new LinearRegressionFunction(newThetaVector);
}
Чтобы убедиться, что стоимость постоянно уменьшается, можно запускать функцию стоимости J(θ)
на исполнение после каждого шага обучения. После каждой итерации стоимость должна уменьшаться. Если этого не происходит, это значит, что значение коэффициента обучения слишком большое и алгоритм просто проскочил минимальное значение. В таком случае алгоритм градиентного спада терпит неудачу.
Графики ниже демонстрируют целевую функцию, использующую новые, вычисленные, тета-параметры, начинающиеся со стартового тета-вектора {1.0, 1.0}
.
Левая колонка показывает график функции предсказания после 50 повторений; средняя колонка после 200 повторений; и правая колонка после 1000 повторений.
Из них видно, что цена уменьшается после каждой итерации, и новая целевая функция соответствует все лучше и лучше. После 500-600 повторений тета-параметры больше существенно не меняются, и цена достигает стабильного «плато». После этого точность целевой функции улучшить таким способом не получится.
В таком случае, несмотря на то, что стоимость больше значительно не уменьшается после 500-600 итераций, целевая функция все еще не оптимальна. Это свидетельствует о несоответствии. В машинном обучении термин «несоответствие» используется для обозначения того что алгоритм обучения не находит основные тенденции данных.
Если обратиться к реальному опыту, вполне вероятно ожидать уменьшения цены за квадратный метр для бОльших владений. Отсюда мы можем сделать вывод, что модель, использованная для процесса обучения целевой функции, не соответствует данным в достаточной мере.
Несоответствие часто связанно с чрезмерным упрощением модели. Так произошло и в нашем случае, целевая функция слишком простая, и для анализа использует единственный параметр — площадь дома. Только вот этой информации недостаточно для точного предсказания цены дома.
Добавление признаков и их масштабирование
Если вы обнаружили что ваша целевая функция не соответствует проблеме, которую вы пытаетесь решить, её нужно подкорректировать. Распространенный способ корректировки несоответствия — добавление дополнительных признаков в вектор признаков. В примере с ценой дома, можно добавить такие характеристики, как количество комнат или возраст дома. То есть вместо использования вектора с одним значением признака{size}
для описания дома, можно использовать вектор с несколькими значениями, например, {size, number-of-rooms, age}.
В некоторых случаях количества признаков в доступных тренировочных данных не хватает. Тогда стоит попробовать применить полиномиальные признаки, которые вычисляются с использованием существующих.
Например, вы имеете возможность расширить целевую функцию определения цены дома таким образом, чтобы она включала вычисляемый признак квадратных метров (x2):
Использование нескольких признаков требует масштабирования признаков, которое используется для стандартизации диапазона для разных признаков. Так, диапазон значений признака size2 значительно больше диапазона значений признака size. Без масштабирования признаков, size2 будет чрезмерно влиять на функцию стоимости. Ошибка, вносимая признаком size2, будет значительно больше ошибки, вносимой признаком size. Простой алгоритм масштабирования признаков приведен ниже:
Этот алгоритм реализован в классе
FeaturesScaling
в коде примера ниже. Класс FeaturesScaling
представляет промышленный метод для создания функции масштабирования подстраиваемой на тренировочных данных. Внутри экземпляры тренировочных данных используются для вычисления среднего, минимального и максимального значений.
Результирующая функция использует вектор признаков и производит новый с отмасштабированными признаками. Масштабирование признаков необходимо как для процесса обучения, так и для процесса предсказания, как показано ниже:
// создание массива данных
List<ltDouble[]> dataset = new ArrayList<>();
dataset.add(new Double[] { 1.0, 90.0, 8100.0 }); // feature vector of house#1
dataset.add(new Double[] { 1.0, 101.0, 10201.0 }); // feature vector of house#2
dataset.add(new Double[] { 1.0, 103.0, 10609.0 }); // ...
//...
// создание меток
List<ltDouble> labels = new ArrayList<>();
labels.add(249.0); // price label of house#1
labels.add(338.0); // price label of house#2
labels.add(304.0); // ...
//...
// создание расширенного списка признаков
Function<ltDouble[], Double[]> scalingFunc = FeaturesScaling.createFunction(dataset);
List<ltDouble[]> scaledDataset = dataset.stream().map(scalingFunc).collect(Collectors.toList());
// создаем функцию которая инициализирует теты и осуществляет обучение //используя коэффициент обучения 0.1
LinearRegressionFunction targetFunction = new LinearRegressionFunction(new double[] { 1.0, 1.0, 1.0 });
for (int i = 0; i < 10000; i++) {
targetFunction = Learner.train(targetFunction, scaledDataset, labels, 0.1);
}
// делаем предсказание стоимости дома с площадью 600 m2
Double[] scaledFeatureVector = scalingFunc.apply(new Double[] { 1.0, 600.0, 360000.0 });
double predictedPrice = targetFunction.apply(scaledFeatureVector);
С добавлением всё большего и большего количества признаков, становится заметен рост соответствия целевой функции, однако будьте осторожны. Если вы зайдете слишком далеко и добавите слишком много признаков, вы можете в результате поучить целевую функцию, которая сверхсоответствует.
Сверхсоответствие и перекрестные проверки
Сверхсоответствие возникает тогда, когда целевая функция или модель соответствует тренировочным данным слишком хорошо, настолько, что захватывает шум или случайные отклонения в тренировочных данных. Пример сверхсоответствия приведен на крайнем с права графике ниже:
Как бы там ни было, сверхсоответствующая модель очень хорошо показывает себя на тренировочных данных, но при этом будет показывать плохие результаты на реальных неизвестных данных. Существует несколько путей избежать сверхсоответствия.
- Использовать больший массив данных для тренировки.
- Использовать меньше признаков как показано на графиках выше.
- Использовать улучшенный алгоритм машинного обучения, принимающий во внимание регуляризацию.
- тренировочные данные;
- проверочные данные;
- тестовые данные.
Инструменты машинного обучения и фреймворк Weka
Большинство фреймворков и библиотек предоставляют собой обширную коллекцию алгоритмов машинного обучения. Кроме этого они предоставляют удобный высокоуровневый интерфейс к обучению, проверке и обработке моделей данных. Weka один из популярнейших фреймворков для JVM. Weka — это Java-библиотека для практического применения, которая содержит графические тесты для проверки моделей. В примере ниже библиотека Weka используется для создания набора тренировочных данных, который содержит признаки и метки. МетодsetClassIndex()
— для маркировки. В Weka метка определена как класс:
// определяем атрибуты для признаков и меток
ArrayList<ltAttribute> attributes = new ArrayList<>();
Attribute sizeAttribute = new Attribute("sizeFeature");
attributes.add(sizeAttribute);
Attribute squaredSizeAttribute = new Attribute("squaredSizeFeature");
attributes.add(squaredSizeAttribute);
Attribute priceAttribute = new Attribute("priceLabel");
attributes.add(priceAttribute);
// создаем и заполняем список признаков 5000 примеров
Instances trainingDataset = new Instances("trainData", attributes, 5000);
trainingDataset.setClassIndex(trainingSet.numAttributes() - 1);
Instance instance = new DenseInstance(3);
instance.setValue(sizeAttribute, 90.0);
instance.setValue(squaredSizeAttribute, Math.pow(90.0, 2));
instance.setValue(priceAttribute, 249.0);
trainingDataset.add(instance);
Instance instance = new DenseInstance(3);
instance.setValue(sizeAttribute, 101.0);
...
Набор данных и Образец объекта может быть сохранен и загружен из файла. Weka использует ARFF (Attribute Relation File Format) который поддерживается графическими тестами Weka. Этот набор данных используется для тренировки целевой функции, известной как классификатор в Weka.
Прежде всего вы должны определить целевую функцию. В коде ниже экземпляр классификатора LinearRegression
будет создан. Этот классификатор будет обучен с помощью вызова buildClassifier()
. Метод buildClassifier()
подбирает тета параметры базируясь на тренировочных данных в поисках наилучшей целевой модели. Используя Weka, вам не придется волноваться об установке коэффициента обучения или количества итераций. Так же Weka выполняет масштабирование признаков самостоятельно.
Classifier targetFunction = new LinearRegression();
targetFunction.buildClassifier(trainingDataset);
После того, как выполнены эти установки, целевая функция может быть использована для предсказания цены дома, как показано ниже:
Instances unlabeledInstances = new Instances("predictionset", attributes, 1);
unlabeledInstances.setClassIndex(trainingSet.numAttributes() - 1);
Instance unlabeled = new DenseInstance(3);
unlabeled.setValue(sizeAttribute, 1330.0);
unlabeled.setValue(squaredSizeAttribute, Math.pow(1330.0, 2));
unlabeledInstances.add(unlabeled);
double prediction = targetFunction.classifyInstance(unlabeledInstances.get(0));
Weka предоставляет класс Evaluation
класс для проверки обученного классификатора или модели. В коде ниже, выбранный массив проверочных данных используется, чтобы избежать ошибочных результатов.
Результаты измерений (цена ошибки) будут выводиться на консоль. Как правило, результаты оценки используются для сравнения моделей, которые были обучены с использованием разных алгоритмов машинного обучения, или вариаций такого рода:
Evaluation evaluation = new Evaluation(trainingDataset);
evaluation.evaluateModel(targetFunction, validationDataset);
System.out.println(evaluation.toSummaryString("Results", false));
Пример выше использует линейную регрессию, которая предсказывает численные значения, такие, как цена дома, базируясь на входных значениях. Линейная регрессия поддерживает предсказание непрерывных числовых значений. Для предсказания бинарных значений («Да» и «Нет») нужно использовать другие алгоритмы машинного обучения. Например, дерево решений, нейронные сети или логистическую регрессию.
// использование логистической регрессии
Classifier targetFunction = new Logistic();
targetFunction.buildClassifier(trainingSet);
Вы можете использовать один из этих алгоритмов, например, для предсказания того, является ли почтовое сообщение спамом, или предсказания погоды, или предсказания хорошо ли будет продаваться дом. Если вы хотите научить ваш алгоритм предсказывать погоду или на сколько быстро будет продан дом вам нужен другой набор данных, например topseller:
// использование атрибута маркера topseller вместо атрибута маркера цена
ArrayList<string> classVal = new ArrayList<>();
classVal.add("true");
classVal.add("false");
Attribute topsellerAttribute = new Attribute("topsellerLabel", classVal);
attributes.add(topsellerAttribute);
Этот набор данных будет использован для тренировки нового классификатора topseller
. После того как он будет обучен, вызов предсказания должен возвращать индекс класса маркера, который можно использовать для получения предсказанного значения.
int idx = (int) targetFunction.classifyInstance(unlabeledInstances.get(0));
String prediction = classVal.get(idx);
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ