Сообщество Engee

GUI для обучения нейросети

作者
avatar-nkapyrinnkapyrin
Notebook

Настройка процесса обучения нейросети

В этом примере мы создадим окружение для обучения нейросети, управляемый при помощи удобной графической формы.

network_training.gif

Описание задачи

Разработка нейросетей давно перестала быть уделом только лишь исследователей с многолетним опытом в написании кода. Сегодня это инструмент инженера, аналитика и продуктолога. Однако часто порог входа всё ещё высок: даже для простой полносвязной сети с тремя слоями нужно помнить синтаксис библиотек вроде Flux.jl и правильно загрузить датасет из CSV и вывести правильные графики... Именно здесь на помощь приходят графические интерфейсы (GUI). Они позволяют сосредоточиться на сути задачи — работе над качеством данных и интерпретацией результата, а не на банальных ошибках, которые все делают на первом этапе.

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

Подготовительные операции включают установку библиотеки для обучения нейросетей:

In [ ]:
# Компиляция библиотеки Flux может занять минуту
Pkg.add(["Flux", "Interpolations"])
using Flux, CSV, DataFrames, Random
using Flux: mse
using Statistics, Interpolations
gr()

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

Предположения: в папке со скриптом лежат файлы train.csv и test.csv. Первый столбец каждого файла содержит прогнозируемую переменную, остальные столбцы содержат признаки. Мы решаем задачу регрессии.

In [ ]:
# Переместимся в директорию с этим скриптом
cd(@__DIR__)

train_filename = "train.csv" # @param {type:"string"}
test_filename = "test.csv" # @param {type:"string"}

train_df = CSV.read(train_filename, DataFrame, types=Float32)
test_df = CSV.read(test_filename, DataFrame, types=Float32)

# Предполагаем, что первый столбец содержит прогнозируемые значения, а остальные -- признаки
X_train = Matrix(train_df[:, 2:end])'  # (n_features, n_samples)
X_test = Matrix(test_df[:, 2:end])'    # (n_features, n_samples)
y_train_raw = reshape(train_df[:, 1], 1, :)  # (1, n_samples)
y_test_raw = reshape(test_df[:, 1], 1, :)    # (1, n_samples)

# Нормализация признаков
X_train_mean = mean(X_train, dims=2)
X_train_std = std(X_train, dims=2)

# Нормализуем train и test с параметрами train-выборки
X_train_norm = (X_train .- X_train_mean) ./ X_train_std
X_test_norm = (X_test .- X_train_mean) ./ X_train_std

# Нормализуем целевые значения
y_train_mean = mean(y_train_raw)
y_train_std = std(y_train_raw)

y_train_norm = (y_train_raw .- y_train_mean) ./ y_train_std
y_test_norm = (y_test_raw .- y_train_mean) ./ y_train_std

features_count = size(X_train_norm, 1)
l1_neurons = 30 # @param {type:"slider",min:1,max:30,step:1}
l2_neurons = 28 # @param {type:"slider",min:1,max:30,step:1}
l3_neurons = 25 # @param {type:"slider",min:1,max:30,step:1}
l4_neurons = 13 # @param {type:"slider",min:1,max:30,step:1}
l5_neurons = 1 # @param {type:"slider",min:1,max:30,step:1}
n_epochs = 499 # @param {type:"slider",min:1,max:500,step:1}

# Архитектура для регрессии
model = Chain(
    Dense(features_count => l1_neurons, relu),
    Dense(l1_neurons => l2_neurons, relu),
    Dense(l2_neurons => l3_neurons, relu),
    Dense(l3_neurons => l4_neurons, relu),
    Dense(l4_neurons => l5_neurons, relu),
    Dense(l5_neurons => 1)
)

# Обучение
loss(m, x, y) = mse(m(x), y)

opt_state = Flux.setup(Adam(0.001), model)
data = [(X_train_norm, y_train_norm)]
train_losses = []
test_losses = []

# Начинаем обучение
for epoch in 1:n_epochs
    Flux.train!(loss, model, data, opt_state)
    push!(train_losses, loss(model, X_train_norm, y_train_norm))
    push!(test_losses, loss(model, X_test_norm, y_test_norm))
end

# Функции для преобразования между масштабами
denormalize_y(y_norm) = y_norm .* y_train_std .+ y_train_mean
denormalize_X(X_norm, dim) = X_norm .* X_train_std[dim] .+ X_train_mean[dim]

test_loss = loss(model, X_test_norm, y_test_norm)
println("\nMSE на тесте (в нормализованном масштабе): $(round(test_loss, digits=6))")
println("RMSE на тесте (в исходных единицах): $(round(sqrt(test_loss) * y_train_std, digits=2))")

# Пример предсказания для первого объекта из теста
prediction_norm = model(X_test_norm[:, 1:1])
actual_norm = y_test_norm[:, 1:1]

prediction = denormalize_y(prediction_norm)
actual = denormalize_y(actual_norm)

println("\nПример (нормализованный): предсказано $(round.(prediction_norm, digits=3)), реально $(round.(actual_norm, digits=3))")
println("Пример (исходный масштаб): предсказано $(round.(prediction, digits=2)), реально $(round.(actual, digits=2))")

# График 1: Кривые обучения
p1 = plot(1:n_epochs, train_losses, label="Train Loss", lw=2, marker=:circle, markersize=2)
plot!(p1, 1:n_epochs, test_losses, label="Test Loss", lw=2, marker=:square, markersize=2)
title!(p1, "Динамика обучения (нормализованный MSE)")
xlabel!(p1, "Эпоха")
ylabel!(p1, "MSE")

# График 2: 3D поверхность
if features_count >= 2
    n_points = 30
    
    # Диапазоны в нормализованном пространстве
    x1_range_norm = range(extrema(X_train_norm[1, :])..., length=n_points)
    x2_range_norm = range(extrema(X_train_norm[2, :])..., length=n_points)
    
    # Создаем сетку в нормализованном пространстве
    grid_x1_norm = repeat(x1_range_norm', n_points, 1)
    grid_x2_norm = repeat(x2_range_norm, 1, n_points)
    
    # Для остальных признаков берем средние значения (в нормализованном пространстве)
    grid_other_norm = zeros(features_count-2, n_points, n_points)
    for i in 3:features_count
        grid_other_norm[i-2, :, :] .= mean(X_train_norm[i, :])
    end
    
    # Формируем полный набор признаков для сетки (в нормализованном пространстве)
    grid_points_norm = vcat(
        reshape(grid_x1_norm, 1, n_points, n_points),
        reshape(grid_x2_norm, 1, n_points, n_points),
        grid_other_norm
    )
    grid_points_flat_norm = reshape(grid_points_norm, features_count, n_points * n_points)
    
    # Предсказания модели на сетке (в нормализованном масштабе)
    predictions_norm = model(grid_points_flat_norm)
    predictions_2d_norm = reshape(predictions_norm, n_points, n_points)
    
    # Преобразуем предсказания в исходный масштаб для визуализации
    predictions_2d_original = denormalize_y(predictions_2d_norm)
    
    # Преобразуем координаты сетки в исходный масштаб для визуализации
    x1_range_original = denormalize_X(x1_range_norm, 1)
    x2_range_original = denormalize_X(x2_range_norm, 2)
    
    # Исходные точки данных (train) в исходном масштабе
    X1_train_original = denormalize_X(X_train_norm[1, :], 1)
    X2_train_original = denormalize_X(X_train_norm[2, :], 2)
    y_train_original = vec(denormalize_y(y_train_norm))
    
    # Тестовые точки данных в исходном масштабе
    X1_test_original = denormalize_X(X_test_norm[1, :], 1)
    X2_test_original = denormalize_X(X_test_norm[2, :], 2)
    y_test_original = vec(denormalize_y(y_test_norm))
    
    # Поверхность предсказаний в исходном масштабе
    p2 = surface(x1_range_original, x2_range_original, predictions_2d_original, 
                 alpha=0.6, label="Прогноз нейросети",
                 camera=(30, 30), color=:reds)
    
    # Добавляем тренировочные точки (синие)
    scatter!(p2, X1_train_original, X2_train_original, y_train_original, 
             label="Train данные", 
             color=:blue, 
             markersize=4,
             alpha=0.7,
             markeralpha=0.6)
    
    # Добавляем тестовые точки (зеленые)
    scatter!(p2, X1_test_original, X2_test_original, y_test_original, 
             label="Test данные", 
             color=:green, 
             markersize=4,
             alpha=0.7,
             markeralpha=0.6,
             marker=:square)
    
    title!(p2, "Прогноз (красный) vs Train (синий) vs Test (зеленый)")
    xlabel!(p2, "Признак 1")
    ylabel!(p2, "Признак 2")
    zlabel!(p2, "Целевая переменная")
    
    # # Выводим статистику для проверки
    # println("\nДиапазоны для визуализации:")
    # println("x1: [$(round(minimum(x1_range_original), digits=2)), $(round(maximum(x1_range_original), digits=2))]")
    # println("x2: [$(round(minimum(x2_range_original), digits=2)), $(round(maximum(x2_range_original), digits=2))]")
    # println("y (предсказания): [$(round(minimum(predictions_2d_original), digits=2)), $(round(maximum(predictions_2d_original), digits=2))]")
    # println("y (train): [$(round(minimum(y_train_original), digits=2)), $(round(maximum(y_train_original), digits=2))]")
    # println("y (test): [$(round(minimum(y_test_original), digits=2)), $(round(maximum(y_test_original), digits=2))]")
    
    plot(p1, p2, layout=(1, 2), titlefont=font(9), guidesfont=font(7), size=(1100,500))
else
    plot(p1, titlefont=font(9), guidesfont=font(7), size=(800,300))
end
MSE на тесте (в нормализованном масштабе): 0.076717
RMSE на тесте (в исходных единицах): 351.24

Пример (нормализованный): предсказано Float32[-0.478;;], реально Float32[-0.961;;]
Пример (исходный масштаб): предсказано Float32[764.79;;], реально Float32[153.06;;]
Out[0]:
No description has been provided for this image

Скройте код, щёлкнув дважды на форме для ввода параметров.

Проверьте что автовыполнение ячейки включено (кнопка должна быть зеленой).

И разместите вывод справа от ячейки, если он находится снизу. Ваш инструментарий готов!

Заключение

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

Это самый быстрый способ, дающий возможность обучить первые 5-10 нейросетей для неизвестных данных и посмотреть, подходит ли этот тип моделей для ваших задач.