Документация Engee

Обучение полносвязной многослойной нейросети на исправленных данных

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

Запуск необходимых библиотек:

using Statistics
using CSV
using DataFrames
using Flux
using Plots
using Flux: train!

Подготовка обучающей и тестовой выборки

Загрузка данных для обучения модели:

df = DataFrame(CSV.File("$(@__DIR__)/data.csv"));

Данные были сохранены после выполнения примера /start/examples/data_analysis/data_processing.ipynb.

Формирование обучающего набора данных

Весь датасет был разделён на обучающую и тестовую выборку. Обучающая выборка составила 0,8 от всего датасета, а тестовая 0,2.

T = df[1:1460,3]; # определение обучающего набора данных, весь датасет 1825 строк
first(df, 5)
5×3 DataFrame
 Row │ date   P        T
     │ Int64  Float64  Float64
─────┼─────────────────────────
   1 │     1    747.7     19.7
   2 │     2    744.2     22.1
   3 │     3    748.6     23.0
   4 │     4    754.5     23.4
   5 │     5    754.6     21.9

Разделение вектора T на батчи длиной в 100 наблюдений:

batch_starts = 1:1:1360 # определение диапазона для цикла

weather_batches = [] # определение пустого массива для записи результатов выполнения цикла
for start in batch_starts
    dop = T[start:start+99] # батч на текущем временном шаге
    weather_batches = vcat(weather_batches, dop) # запись батча в массив
end

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

Метод скользящего окна:

windowing_method.svg

где x - наблюдение, а y1 - прогнозное значение.

Преобразование полученного набора в вектор-строку:

weather_batches = weather_batches'
1×136000 adjoint(::Vector{Any}) with eltype Any:
 19.7  22.1  23.0  23.4  21.9  23.35  …  26.4  18.8  19.7  16.3  16.8  20.5

Изменение формы массива для соответствия длине батча, указанной выше:

weather_batches = reshape(weather_batches, (100,:))
100×1360 reshape(adjoint(::Vector{Any}), 100, 1360) with eltype Any:
 19.7   22.1   23.0   23.4   21.9   23.35  …  -4.4     -2.9  -4.0  -4.7  -4.2
 22.1   23.0   23.4   21.9   23.35  24.8      -2.9     -4.0  -4.7  -4.2  -7.8
 23.0   23.4   21.9   23.35  24.8   26.25     -4.0     -4.7  -4.2  -7.8   1.7
 23.4   21.9   23.35  24.8   26.25  27.7      -4.7     -4.2  -7.8   1.7   2.8
 21.9   23.35  24.8   26.25  27.7   28.0      -4.2     -7.8   1.7   2.8   2.9
 23.35  24.8   26.25  27.7   28.0   27.4   …  -7.8      1.7   2.8   2.9   5.8
 24.8   26.25  27.7   28.0   27.4   25.1       1.7      2.8   2.9   5.8   3.1
 26.25  27.7   28.0   27.4   25.1   25.6       2.8      2.9   5.8   3.1   4.1
 27.7   28.0   27.4   25.1   25.6   24.5       2.9      5.8   3.1   4.1   5.1
 28.0   27.4   25.1   25.6   24.5   21.9       5.8      3.1   4.1   5.1   4.4
 27.4   25.1   25.6   24.5   21.9   15.5   …   3.1      4.1   5.1   4.4   4.3
 25.1   25.6   24.5   21.9   15.5   22.7       4.1      5.1   4.4   4.3   7.5
 25.6   24.5   21.9   15.5   22.7   23.1       5.1      4.4   4.3   7.5   6.9
  ⋮                                  ⋮     ⋱   ⋮
 22.1   18.9   17.9   15.5   20.9   20.3      19.9917  19.7  15.3  20.5  19.5
 18.9   17.9   15.5   20.9   20.3   16.7      19.7     15.3  20.5  19.5  19.3
 17.9   15.5   20.9   20.3   16.7   15.5   …  15.3     20.5  19.5  19.3  21.6
 15.5   20.9   20.3   16.7   15.5   12.7      20.5     19.5  19.3  21.6  21.1
 20.9   20.3   16.7   15.5   12.7    9.7      19.5     19.3  21.6  21.1  23.8
 20.3   16.7   15.5   12.7    9.7    6.7      19.3     21.6  21.1  23.8  23.6
 16.7   15.5   12.7    9.7    6.7    4.3      21.6     21.1  23.8  23.6  26.4
 15.5   12.7    9.7    6.7    4.3    5.6   …  21.1     23.8  23.6  26.4  18.8
 12.7    9.7    6.7    4.3    5.6   12.2      23.8     23.6  26.4  18.8  19.7
  9.7    6.7    4.3    5.6   12.2   12.8      23.6     26.4  18.8  19.7  16.3
  6.7    4.3    5.6   12.2   12.8   12.3      26.4     18.8  19.7  16.3  16.8
  4.3    5.6   12.2   12.8   12.3    9.8      18.8     19.7  16.3  16.8  20.5
X = weather_batches # переприсвоение
100×1360 reshape(adjoint(::Vector{Any}), 100, 1360) with eltype Any:
 19.7   22.1   23.0   23.4   21.9   23.35  …  -4.4     -2.9  -4.0  -4.7  -4.2
 22.1   23.0   23.4   21.9   23.35  24.8      -2.9     -4.0  -4.7  -4.2  -7.8
 23.0   23.4   21.9   23.35  24.8   26.25     -4.0     -4.7  -4.2  -7.8   1.7
 23.4   21.9   23.35  24.8   26.25  27.7      -4.7     -4.2  -7.8   1.7   2.8
 21.9   23.35  24.8   26.25  27.7   28.0      -4.2     -7.8   1.7   2.8   2.9
 23.35  24.8   26.25  27.7   28.0   27.4   …  -7.8      1.7   2.8   2.9   5.8
 24.8   26.25  27.7   28.0   27.4   25.1       1.7      2.8   2.9   5.8   3.1
 26.25  27.7   28.0   27.4   25.1   25.6       2.8      2.9   5.8   3.1   4.1
 27.7   28.0   27.4   25.1   25.6   24.5       2.9      5.8   3.1   4.1   5.1
 28.0   27.4   25.1   25.6   24.5   21.9       5.8      3.1   4.1   5.1   4.4
 27.4   25.1   25.6   24.5   21.9   15.5   …   3.1      4.1   5.1   4.4   4.3
 25.1   25.6   24.5   21.9   15.5   22.7       4.1      5.1   4.4   4.3   7.5
 25.6   24.5   21.9   15.5   22.7   23.1       5.1      4.4   4.3   7.5   6.9
  ⋮                                  ⋮     ⋱   ⋮
 22.1   18.9   17.9   15.5   20.9   20.3      19.9917  19.7  15.3  20.5  19.5
 18.9   17.9   15.5   20.9   20.3   16.7      19.7     15.3  20.5  19.5  19.3
 17.9   15.5   20.9   20.3   16.7   15.5   …  15.3     20.5  19.5  19.3  21.6
 15.5   20.9   20.3   16.7   15.5   12.7      20.5     19.5  19.3  21.6  21.1
 20.9   20.3   16.7   15.5   12.7    9.7      19.5     19.3  21.6  21.1  23.8
 20.3   16.7   15.5   12.7    9.7    6.7      19.3     21.6  21.1  23.8  23.6
 16.7   15.5   12.7    9.7    6.7    4.3      21.6     21.1  23.8  23.6  26.4
 15.5   12.7    9.7    6.7    4.3    5.6   …  21.1     23.8  23.6  26.4  18.8
 12.7    9.7    6.7    4.3    5.6   12.2      23.8     23.6  26.4  18.8  19.7
  9.7    6.7    4.3    5.6   12.2   12.8      23.6     26.4  18.8  19.7  16.3
  6.7    4.3    5.6   12.2   12.8   12.3      26.4     18.8  19.7  16.3  16.8
  4.3    5.6   12.2   12.8   12.3    9.8      18.8     19.7  16.3  16.8  20.5

Определение массива целевых значений:

Y = (T[101:1460]) # отсчёт начинается с 101, так как предыдущие 100 наблюдений используются в качестве исходных данных
Y = Y'
1×1360 adjoint(::Vector{Float64}) with eltype Float64:
 5.6  12.2  12.8  12.3  9.8  11.0  8.7  …  18.8  19.7  16.3  16.8  20.5  19.2

Преобразование в формат приемлемый для обработки нейросетью:

X = convert(Array{Float32}, X)
Y = convert(Array{Float32}, Y)
1×1360 Matrix{Float32}:
 5.6  12.2  12.8  12.3  9.8  11.0  8.7  …  18.8  19.7  16.3  16.8  20.5  19.2

Формирование тестового набора данных

Разделение тестовой выборки на батчи длиной 100 наблюдений:

X_test = df[1461:1820, 3] # определение тестового набора данных
batch_starts_test = 1:1:261  # определение диапазона для цикла

test_batches = [] # определение пустого массива для записи результатов выполнения цикла
for start in batch_starts_test
    dop = X_test[start:start+99] # батч на текущем временном шаге
    test_batches = vcat(test_batches, dop) # запись батча в массив
end
test_batches = reshape(test_batches, (100,:)) # изменение формы массива для соответствия длине батча, указанной выше:

X_test = convert(Array{Float32}, test_batches) # преобразование в формат приемлимый для обработки нейросетью
100×261 Matrix{Float32}:
 23.1  18.9  17.2  12.4  15.0  23.3  …  -9.7  -8.8  -7.4  -5.2  -3.1  -2.0
 18.9  17.2  12.4  15.0  23.3  20.7     -8.8  -7.4  -5.2  -3.1  -2.0  -1.3
 17.2  12.4  15.0  23.3  20.7  15.0     -7.4  -5.2  -3.1  -2.0  -1.3  -0.5
 12.4  15.0  23.3  20.7  15.0  13.2     -5.2  -3.1  -2.0  -1.3  -0.5  -2.4
 15.0  23.3  20.7  15.0  13.2  11.2     -3.1  -2.0  -1.3  -0.5  -2.4  -0.9
 23.3  20.7  15.0  13.2  11.2  15.5  …  -2.0  -1.3  -0.5  -2.4  -0.9  -0.2
 20.7  15.0  13.2  11.2  15.5  13.4     -1.3  -0.5  -2.4  -0.9  -0.2  -3.9
 15.0  13.2  11.2  15.5  13.4  14.1     -0.5  -2.4  -0.9  -0.2  -3.9   2.0
 13.2  11.2  15.5  13.4  14.1  10.9     -2.4  -0.9  -0.2  -3.9   2.0   1.3
 11.2  15.5  13.4  14.1  10.9  14.5     -0.9  -0.2  -3.9   2.0   1.3   1.0
 15.5  13.4  14.1  10.9  14.5  15.2  …  -0.2  -3.9   2.0   1.3   1.0   0.3
 13.4  14.1  10.9  14.5  15.2  25.0     -3.9   2.0   1.3   1.0   0.3   1.4
 14.1  10.9  14.5  15.2  25.0  26.5      2.0   1.3   1.0   0.3   1.4  -0.5
  ⋮                             ⋮    ⋱   ⋮                             ⋮
 16.7  16.4  21.4  17.1  17.1  20.0     21.5  22.2  23.3  21.8  22.4  26.3
 16.4  21.4  17.1  17.1  20.0  18.0     22.2  23.3  21.8  22.4  26.3  28.0
 21.4  17.1  17.1  20.0  18.0  24.2  …  23.3  21.8  22.4  26.3  28.0  27.9
 17.1  17.1  20.0  18.0  24.2  14.7     21.8  22.4  26.3  28.0  27.9  27.7
 17.1  20.0  18.0  24.2  14.7  16.0     22.4  26.3  28.0  27.9  27.7  26.6
 20.0  18.0  24.2  14.7  16.0  24.6     26.3  28.0  27.9  27.7  26.6  25.1
 18.0  24.2  14.7  16.0  24.6  23.3     28.0  27.9  27.7  26.6  25.1  21.0
 24.2  14.7  16.0  24.6  23.3  19.4  …  27.9  27.7  26.6  25.1  21.0  18.7
 14.7  16.0  24.6  23.3  19.4  11.6     27.7  26.6  25.1  21.0  18.7  17.8
 16.0  24.6  23.3  19.4  11.6  13.7     26.6  25.1  21.0  18.7  17.8  21.3
 24.6  23.3  19.4  11.6  13.7   8.3     25.1  21.0  18.7  17.8  21.3  21.6
 23.3  19.4  11.6  13.7   8.3  13.9     21.0  18.7  17.8  21.3  21.6  21.9

Построение и обучение нейросети

Определение архитектуры нейросети:

model = Flux.Chain(
        Dense(100, 50,elu),
        Dense(50, 25,elu),
        Dense(25, 5,elu),
        Dense(5, 1)
        )
Chain(
  Dense(100 => 50, elu),                # 5_050 parameters
  Dense(50 => 25, elu),                 # 1_275 parameters
  Dense(25 => 5, elu),                  # 130 parameters
  Dense(5 => 1),                        # 6 parameters
)                   # Total: 8 arrays, 6_461 parameters, 25.738 KiB.

Определение параметров обучения:

loss(x, y) = Flux.mse(model(x), y) # функция потерь

ps = Flux.params(model) # указание на параметры корректируемые в процессе обучения

learning_rate = Float32(0.01) # определение скорости обучения

opt = Adam(learning_rate) # выбор оптимизатора
Adam(0.009999999776482582, (0.9, 0.999), 1.0e-8, IdDict{Any, Any}())

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

loss_history = [] # определение пустого массива для записи функции потерь

epochs = 100 # определение количества шагов обучения модели

for epoch in 1:epochs # повторение обучения модели на каждом шаге
    train!(loss, ps, [(X, Y)], opt) # обучающая функция, корректирующая параметры модели
    train_loss = loss(X, Y) # расчёт функции потерь на текущем шаге
    push!(loss_history, train_loss) # запись функции потерь
    if epoch == 1 # условие для отображения функции потерь на каждом 500-м шаге обучения
        println("Epoch = $epoch : Training Loss = $train_loss");
    elseif epoch % 10 == 0
        println("Epoch = $epoch : Training Loss = $train_loss");
    end
end
# время обучения модели может измеряться минутами или десятками минут
# в зависимости от выбранных параметров (в основном, от количества эпох и шага обучения)
Epoch = 1 : Training Loss = 226.48419
Epoch = 10 : Training Loss = 152.66289
Epoch = 20 : Training Loss = 22.490475
Epoch = 30 : Training Loss = 20.869888
Epoch = 40 : Training Loss = 18.231764
Epoch = 50 : Training Loss = 15.668477
Epoch = 60 : Training Loss = 14.022537
Epoch = 70 : Training Loss = 12.5214615
Epoch = 80 : Training Loss = 11.397099
Epoch = 90 : Training Loss = 10.560633
Epoch = 100 : Training Loss = 10.030463

Визуализация изменения функции потерь:

plot((1:epochs), loss_history, title="Изменение функции потерь", xlabel="Эпоха", ylabel="Функция потерь")

interactive-scripts/images/data_analysis_neural_net_learning/469b88d61fb8ba6de9406fe44c8a87c153e1d4c6

Получение прогнозных значений:

y_hat_raw = model(X_test) # загрузка тестовой выборки в модель, получение прогноза
y_pred = y_hat_raw'
y_pred = y_pred[:,1]
y_pred = convert(Vector{Float64}, y_pred)
first(y_pred, 5)
5-element Vector{Float64}:
 20.30321502685547
 20.370384216308594
 18.007482528686523
 15.8691987991333
 13.052804946899414

Визуализация предсказанных значений

days = df[:,1] # формирование массива дней, начиная с первого наблюдения
first(days, 5)
5-element Vector{Int64}:
 1
 2
 3
 4
 5

Подключение бэкэнда - метода отображения графики:

plotlyjs()
Plots.PlotlyJSBackend()

Формирование набора данных из начального датасета для сравнения:

df_T = df[:, 3]#df[1471:1820, 3]
first(df_T, 5)
5-element Vector{Float64}:
 19.7
 22.1
 23.0
 23.4
 21.9

Построение графика зависимости температуры от времени по исходным и спрогнозированным данным:

plot(days, df_T)#plot(days, T[11:end]) #T[11:end]
plot!(days[1560:1820], y_pred)

interactive-scripts/images/data_analysis_neural_net_learning/498631a748a2b47b8cc8bd79bbda4ce32e4e1c23

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

Для этого были загружены реальные данные без пропусков:

real_data = DataFrame(CSV.File("$(@__DIR__)/real_data.csv"));

Построение графика зависимости температуры от времени по реальным и спрогнозированным данным:

plot(real_data[1:261,2])
plot!(y_pred)

interactive-scripts/images/data_analysis_neural_net_learning/8d4305d91e81a078f9fd9da794cd526ab9214848

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

corr_T = cor(y_pred,real_data[1:261,2])
0.9131755711843572

Коэффициент корреляции Пирсона может принимать значения от -1 до 1, где 0 будет означать отсутствие связи между переменными, а -1 и 1 - тесную связь (обратная и прямая зависимость соответственно).

Выводы

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