В начале этого года я прослушал знаменитый курс машинного обучения на Coursera (Стэнфордского университета) и иногда мне было немного сложно одновременно учить октаву. Поскольку это не мой удобный язык программирования, я хотел поэкспериментировать с тем, что я изучил с помощью Java или Kotlin. Наконец, я решил использовать Kotlin, так как гораздо проще реализовать матрицу и ее функции, в основном из-за поддержки оператора overlord и инфиксной функции.

Итак, я собираюсь начать с более экономичной регрессии, такой же, как в курсе Couseara, а позже, надеюсь, я перейду к логистической регрессии, нейронной сети, SVM и т. д.… и сделаю то же самое с Kotlin.

Начнем с функции стоимости.

Что такое функция стоимости (функция потерь или функция квадратичной ошибки)

Здесь я не буду объяснять определение, но то, что оно делает, — это количественная оценка ошибки между предсказанными значениями и ожидаемыми значениями. Итак, как только мы вычислим ошибку (расстояние на графике) и суммируем все это, мы получим потерю нашего прогноза.

Приступаем к кодированию.

Сначала определите матрицу

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

Например, если матрица 2 x 3 [0,0] = 1-й элемент, [0,3] = 3-й элемент, [1,0] = 4-й элемент, [1,3] = 6-й элемент

Я планирую получить все функции и результаты в виде списков. Поэтому нужен метод для преобразования списка в эту матрицу.

Здесь он легко может преобразовать его в матрицу по заданному номеру столбца. Убедитесь, что размер списка должен быть совместим с количеством столбцов.

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

Функция стоимости представляет собой прямое уравнение. Что это делает, так это получить сумму всех ошибок, а затем взять среднее значение.

Примечание. Для дальнейшего понимания я рекомендую ознакомиться с подробным руководством.

И это гипотеза для многофункционального набора данных.

Ну, что, черт возьми, такое «Х х тета» (Х, умноженное на тета)? Да, это метрическое умножение, и в этом прелесть инфиксной функции Kotlin. Давайте это реализуем.

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

Всегда столбцы первой матрицы и строки второй матрицы должны быть равны.

Так что в основном это все. Теперь вы можете рассчитать стоимость для заданных тета.

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

Выше я добавил обе реализации, чтобы вы могли проверить разницу.

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

Хорошо, мы закончили с реализацией функции стоимости. Давайте загрузим некоторый набор данных и найдем стоимость некоторых угаданных тета.

Здесь я использовал JMathPlot для построения набора данных.

Давайте угадаем для удовольствия. 😂 😂

После некоторых предположений моим лучшим предположением было тета [-4,0,1,2]. (Поскольку это набор данных с одной переменной), и давайте нарисуем это.

Примечание. Чтобы учесть термин перехвата (theta0), мы добавляем дополнительный столбец в качестве первого индекса для X и устанавливаем для него все единицы (1 * theta0). так что мы можем легко сделать умножение и найти гипотезу

И нарисуйте линию в сюжете.

Теперь давайте запустим нашу реализацию и узнаем цену этого предположения.

4.478453220857733 (from non vectorized function)
4.478453220857733 (from vectorized function)

Таким образом, я получил 4,4785 в качестве стоимости. По сути, нам нужно убедиться, что стоимость минимальна, чтобы сказать, что нарисованная линия лучше всего подходит для данного набора данных.

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

Другими словами..

Прежде чем реализовать алгоритм градиентного спуска, мне действительно нужно знать, правильное мое предположение или нет 😉. Итак, давайте сделаем быстрый скрипт на Python и посмотрим.

Intercept : -3.8957808783118537
Feature's coefficients are: [1.19303364]
So calculated cost is: 4.476971375975176

хахааа, так близко 😇 😎.

Давайте теперь напишем код градиентного спуска.

Нам нужно реализовать еще несколько матричных функций и перегрузок операторов. (транспонирование и матрица * двойное)

Итак, мы закончили ✌️. Давайте запустим тот же пример и посмотрим, получим ли мы тот же результат, что и скрипт Python.

With learning rate = 0.01 and iteration = 1000 i've got
Intercept: -3.241402144274422
Coefficients: [1.1272942024281842]
Cost: 4.515955503078912

Вау… это очень плохо 😱. Даже мое предположение лучше, чем это. Так что не так? Давайте построим конвергенцию и посмотрим, что происходит.

Это ясно, скорость обучения справедлива, но для большей сходимости требуется больше итераций. Так что давайте увеличим итерацию до 5000 и посмотрим.

With learning rate = 0.01 and iteration = 5000 i've got
Intercept: -3.895300510657168
Coefficients: [1.1929853860482198]
Cost: 4.47697139698281

и график сходимости тоже хорош.

ура….. 😇 😇. Это сработало. Теперь зафиксируем результат.

Достаточно справедливо, верно? Теперь я хотел визуализировать, как значения тета изменяются на каждой итерации. Поэтому я собираюсь сохранить все тета-значения в списке и вернуть его. А затем распечатайте каждый с помощью thread.sleep()

Теперь давайте получим еще один набор данных от Kaggle и оценим нашу реализацию.

Набор данных — https://www.kaggle.com/andonians/random-linear-regression

С нашей реализацией

скорость обучения = 0,0005 и итерация = 10 000

intercept: -0.08157245168346036
coefficients: [1.0001999306062792]
Cost: 3.944586551695178

скорость обучения = 0,0005 и итерация = 100 000

intercept: -0.12015504504010248
coefficients: [1.0007782407846593]
Cost: 3.944399934788149

Просто из любопытства давайте нарисуем только первые 100 итераций (увеличив масштаб) и посмотрим, как они себя поведут.

Кажется, что изначально он боролся. Однако вскоре выздоровел. Первоначальные трудности указывают на то, что скорость обучения немного выше, чем мы думаем, поэтому просто уменьшите скорость обучения до 0,0002 и посмотрите, что произойдет.

Теперь это кажется справедливым, но за это приходится платить количеством итераций. Это означает время, необходимое для обучения. Поэтому, поскольку он хорошо восстанавливается, я думаю, что лучше оставить значение 0,0005.

Или я должен увеличить немного больше и посмотреть? Хорошо, давайте увеличим до 0,0006 и посмотрим, как оно себя поведет.

Хаа хааа Это слишком высоко😬 😬, Не будем туда 😉.

Градиентный спуск для нескольких переменных

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

Примечание: две особенности, размер дома (квадратные футы) и количество спален. И мы прогнозируем цену дома.

Во-первых, вот код Python для расчета коэффициентов перехвата и признаков.

Сделал прогноз для 2000 квадратных футов и 3 спален, и ответ: 341805,200.

А теперь запустим нашу реализацию и посмотрим, получим ли мы такие же результаты.

1st attempt
Learning Rate = 0.0000002
Iterations = 5000000
2nd attempt
Learning Rate = 0.0000002
Iterations = 10000000
3rd attempt
Learning Rate = 0.0000002
Iterations = 100000000

Все 3 попытки показывают, что итерации недостаточно для минимизации тета. Вы можете видеть это на графиках ниже.

Итак, в моей 4-й попытке я увеличил количество итераций в 4 раза, и будем надеяться, что это даст мне некоторые оптимальные значения.

4th attempt
Learning Rate = 0.0000002
Iterations = 400000000

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

Ну, это заняло около 145 минут. Но тогда это не на 100% точно. но пока я могу справиться с этими результатами. А еще мне хватило 🙏. Давайте сделаем наш прогноз с результатами, которые я получил в этой последней попытке.

5th attempt
Learning Rate = 0.0000002
Iterations = 550000000
Intercept: 89211.91357108342
coefficients: [139.20934164431515, -8621.386066273524]]
Cost: 2.0432840334191492E9

Итак, если мы предскажем то же самое, что и с питоном (2000 квадратных футов и 3 спальни).

89211.916 + (2000 * 139.209) + (3 * -8621.386) = 341765.758

прогнозируемое значение равно 341 765,758. Довольно близко, не так ли? 👍

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

Ответ: мы не масштабировали функции. Посмотрим на набор данных.

Характеристики, которые мы получаем, - это размеры дома (квадратные футы) и количество спален. Проблема в том, что масштабы этих признаков очень разные и не находятся в одинаковом диапазоне значений. И это делает все медленным.

Итак, давайте сначала масштабируем функции. Идея состоит в том, чтобы получить все данные о функциях примерно между -1 и +1.

Это называется нормализацией среднего. По сути, значение признака вычитается из среднего значения того же признака, а затем делится на диапазон. Для диапазона мы можем использовать минимальное-максимальное значение или стандартное отклонение. (вероятно, вы можете найти лучший ответ здесь https://sebastianraschka.com/Articles/2014_about_feature_scaling.html)

Поскольку в курсе «Coursera» использовалось стандартное отклонение, я тоже собираюсь использовать его.

Итак, мы реализуем все необходимые функции. Теперь давайте соберем их все вместе и реализуем метод «featureNormalize».

Что ж, наш градиентный спуск выполняется за секунду 🤘.

Intercept: 340412.659
coefficients: [109447.796,  -6578.354]]
Cost: 2.043280050602829E9

Чтобы выполнить наш прогноз для 2000 квадратных футов и 3 комнат, нам нужно нормализовать эти значения в соответствии с соответствующим средним значением и стандартным отклонением. Для этого нам нужно получить такое же среднее значение и стандартное отклонение от нормализованного метода.

Mean : [2000.680, 3.170])
Standed diviation : [786.203, 0.753]) 
Sq feet = (2000–2000.680) /786.203 = -0.000865
Rooms = (3 - 3.170)/0.753 = -0.226
340412.659 + (109447.796 * -0.000865) + (-0.226 * -6578.354)
= 341804.694

Результаты такие же. Тем не менее, мы получили результат намного быстрее после масштабирования признаков.

Вот и все, мы закончили. 😇

Весь приведенный выше код вы можете найти в моем репозитории GITHUB.

до скорого .. :)