Julia в машинном обучение: база

a47b88439b7af401207eaf70cfa96fd4.png

Привет, Хабр!

Julia зародилась в 2012 году, благодаря усилиям четырех энтузиастов-разработчиков: Джефф Безансон, Стефан Карпински, Вирал Би Шах, и Алан Эдельман. Они стремились создать язык, который сочетал бы легкость Python, скорость C, динамичность Ruby, лингвистическую чистоту Lisp и возможности математических систем вроде Matlab. Им удалось! Julia — это слияние простоты и мощи.

Благодаря JIT-компиляции, код Julia может выполняться с скоростью, сопоставимой с кодом, написанным на C или Fortran.

Основные особенности Julia

JIT компиляция

JIT компиляция — это процесс, при котором исходный код программы компилируется в машинный код непосредственно во время ее выполнения, а не заранее.

Когда запускается прога на Julia, она сначала работает как интерпретируемый язык. Код читается и исполняется построчно.

Когда Julia встречает функцию, она компилирует эту функцию в машинный код для конкретной архитектуры процессора. Это происходит в первый раз, когда функция вызывается.

Скомпилированный машинный код затем кэшируется. Если функция будет вызвана снова, Julia будет использовать уже скомпилированный код, а не интерпретировать или компилировать ее заново.

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

Типизация

Как и во многих высокоуровневых языках, в Julia по умолчанию используется динамическая типизация — типы переменных определяются во время выполнения программы

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

function add(a, b)
    return a + b
end

println(add(10, 20))  # вывод: 30
println(add(10.5, 20.3))  # вывод: 30.8

add функция может принимать любые типы, которые поддерживают операцию сложения.

function add(a::Int, b::Int)
    return a + b
end

println(add(10, 20))  # вывод: 30
println(add(10.5, 20.3))  # вызовет ошибку, так как аргументы не являются Int

add строго ограничена целочисленными аргументами.

function add{T <: Number}(a::T, b::T)
    return a + b
end

println(add(10, 20))  # вывод: 30
println(add(10.5, 20.3))  # вывод: 30.8

add принимает аргументы одного типа, но этот тип может быть любым подтипом Number.

Многопоточность

Julia предоставляет встроенную поддержку многопоточности, что позволяет распределять вычислительные задачи между различными потоками. Можно управлять количеством потоков, используемых их программами, и распределять задачи между ними.

Базовая многопоточность выглядит так:

using Base.Threads

function threaded_sum(arr)
    nthreads = nthreads()  # получаем количество потоков
    sums = zeros(nthreads)  # массив для сумм по каждому потоку
    @threads for i in 1:nthreads
        # Распределяем итерации цикла по потокам
        for j in i:nthreads:length(arr)
            sums[i] += arr[j]
        end
    end
    return sum(sums)  # суммируем результаты всех потоков
end

arr = rand(1:100, 10^6)  # большой массив случайных чисел
println(threaded_sum(arr))  # использование многопоточности для подсчета суммы

С атомарными операциями:

using Base.Threads

function threaded_increment(n)
    count = Atomic{Int64}(0)  # атомарная переменная
    @threads for i in 1:n
        atomic_add!(count, 1)  # безопасное увеличение счетчика
    end
    return count[]
end

println(threaded_increment(10^6))  # увеличиваем счетчик многократно в многопоточном режиме

Библиотеки для ML

Для julia на гите есть юпитер.

Flux.jl

Flux.jl — это самая основная и базированная библиотека в машинном обучение для джулии.

Flux предоставляет множество предопределенных слоев, которые являются строительными блоками для нейронных сетей.

Dense layer: основной слой для создания полносвязных сетей:

dense = Dense(10, 5, σ)  # 10 входов, 5 выходов, сигмоидная функция активации

Convolutional layer: для создания сверточных сетей:

conv = Conv((3, 3), 1=>16, σ)  # 3x3 фильтры, 1 входной канал, 16 выходных каналов

Flux имеет функции активации, такие как σ, relu, tanh:

relu_layer = Dense(10, 5, relu)

Оптимизаторы используются для обновления весов модели в процессе обучения. Flux поддерживает различные оптимизаторы, такие как SGD, Adam, RMSprop:

Пример с SGD:

opt = Descent(0.01)  # SGD с шагом обучения 0.01

Функции потерь как кросс-энтропия или MSE используются для оценки производительности модели, к примеру кросс-энтропия может выглядеть так:

loss(x, y) = Flux.Losses.crossentropy(model(x), y)

Flux предоставляет утилиты для упрощения процесса обучения, такие как train!:

data = [(x, y), ...]  # набор данных
Flux.train!(loss, params(model), data, opt)

Создадим простую полносвязную нейронную сеть для классификации изображений из набора данных MNIST:

using Flux, Flux.Data.MNIST, Statistics
using Flux: onehotbatch, onecold, crossentropy, throttle
using Base.Iterators: repeated

# так выгдядит загрузка 
images = float.(reshape(hcat(float.(MNIST.images())...), 28 * 28, :))
labels = onehotbatch(MNIST.labels(), 0:9)

# делим данные на обучающую тестовую выборку
train_indices = 1:60000
x_train, y_train = images[:, train_indices], labels[:, train_indices]

test_indices = 60001:70000
x_test, y_test = images[:, test_indices], labels[:, test_indices]

# объявляем модель
model = Chain(
  Dense(28*28, 64, relu),
  Dense(64, 10),
  softmax
)

# функция потерь и оптимизатор
loss(x, y) = crossentropy(model(x), y)
opt = ADAM()

# само обучение
dataset = repeated((x_train, y_train), 200)
evalcb = () -> @show(loss(x_train, y_train))
Flux.train!(loss, params(model), dataset, opt, cb = throttle(evalcb, 10))

MLJ

MLJ (Machine learning in Julia) предоставляет унифицированный интерфейс для доступа к широкому спектру моделей машинного обучения.

MLJ упрощает процесс загрузки и предварительной обработки данных.

Загрузка данных:

using MLJ
X, y = @load_iris

Выглядит весьма интересно.

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

Выбор модели:

tree_model = @load DecisionTreeClassifier

Настройка гиперпараметров:

tree = tree_model(max_depth=5)

С MLJ, можно обучать модели и делать предсказания с помощью простых функций:

Обучение модели:

mach = machine(tree, X, y)
fit!(mach)

Предсказание:

yhat = predict(mach, Xnew)

Оценки производительности моделей естественно есть в этой библио, к примеру оценка с помощью кросс-энтропии выглядит так:

cv = cross_validation(mach, K=5)
cv_measure = mean(cv.measurements)

Применим MLJ для классификации ирисов, используя дерево решений:

using MLJ

# грузим набор
X, y = @load_iris

# делим данные на обучающую и тестовую выборки
train, test = partition(eachindex(y), 0.7, shuffle=true)

# выбираем модель и настраиваем гиперпараметров
tree_model = @load DecisionTreeClassifier
tree = tree_model(max_depth=5)

# создаем и обучаем
mach = machine(tree, X, y)
fit!(mach, rows=train)

# предсказание для тестовой выборки
yhat = predict(mach, rows=test)

ScikitLearn.jl:

Для тех, кто привык к Scikit-Learn в питоне, ScikitLearn.jl предлагает похожий опыт, но с Julia.

ScikitLearn.jl позволяет легко загружать стандартные наборы данных для тестирования и обучения моделей:

using ScikitLearn
@sk_import datasets: load_iris
iris = load_iris()

Предварительная обработка данных включает функции для нормализации, масштабирования и преобразования данных:

@sk_import preprocessing: StandardScaler
scaler = StandardScaler()
X_scaled = fit_transform(scaler, iris["data"])

Функция для разделения данных на обучающую и тестовую выборку:

@sk_import model_selection: train_test_split
X_train, X_test, y_train, y_test = train_test_split(iris["data"], iris["target"], test_size=0.4)

ScikitLearn.jl имеет много алгоритмов, включая линейные модели, деревья решений и т.д:

@sk_import linear_model: LogisticRegression
model = LogisticRegression()

Процесс обучения модели на подготовленных данных:

fit!(model, X_train, y_train)

Использование обученной модели для предсказания результатов на новых данных:

predictions = predict(model, X_test)

Оценка качества модели с помощью различных метрик:

@sk_import metrics: accuracy_score
acc = accuracy_score(y_test, predictions)

Классификации видов ирисов:

using ScikitLearn

@sk_import datasets: load_iris
@sk_import model_selection: train_test_split
@sk_import preprocessing: StandardScaler
@sk_import linear_model: LogisticRegression
@sk_import metrics: accuracy_score
# да, такие вот импорты ;)

iris = load_iris()
X, y = iris["data"], iris["target"]

scaler = StandardScaler()
X_scaled = fit_transform(scaler, X)

X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.3)

model = LogisticRegression()
fit!(model, X_train, y_train)

predictions = predict(model, X_test)

Оптимизация и автоматическое дифференцирование

Zygote — это библиотека для автоматического дифференцирования в Julia, позволяет легко вычислять градиенты функций:

using Zygote

f(x) = 3x^2 + 2x + 1
df = gradient(f, 2)  # вычисляет градиент f в точке x=2
println(df)  # вывод: (14,)
g(x, y) = sin(x) + cos(y)
dg = gradient(g, π/2, π)  # вычисление градиента по x и y
println(dg)  # вывод: (-1.0, 0.0)
# функция потерь
loss(x, y) = sum((x - y).^2)

# градиент функции потерь
x, y = [1, 2, 3], [3, 2, 1]
grad_loss = gradient(() -> loss(x, y), params([x, y]))
println(grad_loss)

Также есть Optim, которая позволяет для оптимизации функций:

using Optim

f(x) = (x[1] - 2)^2 + (x[2] - 3)^2
res = optimize(f, [0.0, 0.0])  # начальная точка [0.0, 0.0]
println(Optim.minimizer(res))  # ввод: близко к [2, 3]

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

f(x) = (x[1] - 1)^2 + (x[2] - 2)^2
∇f(x) = gradient(f, x)[1]

res = optimize(f, ∇f, [0.0, 0.0], BFGS())  # BFGS метод
println(Optim.minimizer(res))  # близко к [1, 2]
# функция потерь
loss(x) = (x[1] - 3)^2 + (x[2] - 4)^2
∇loss(x) = gradient(loss, x)[1]

initial_params = [0.0, 0.0]
res = optimize(loss, ∇loss, initial_params, LBFGS())
println(Optim.minimizer(res))  # близко к [3, 4]

Plots.jl

С помощью plost.jl можно визуализировать данные и т.п.

using Plots

# линейный графика
plot([1, 2, 3, 4, 5], [2, 4, 1, 3, 7], label="линия", linewidth=2)
# гистограмма
histogram(randn(1000), bins=15, label="частоты")
# график рассеяния
scatter(randn(100), randn(100), color=:blue, label="точки")

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

Несмотря на то, что Julia все еще молодой язык и его сообщество не так велико, как у Python или R, он стремительно развивается, обогащается новыми библиотеками и возможностями.

Больше интересного из мира машинного обучения эксперты OTUS рассказывают в рамках онлайн-курсов. А в календаре мероприятий все желающие могут зарегистрироваться на заинтересовавший бесплатный вебинар.

© Habrahabr.ru