Обучение полносвязной многослойной нейросети на исправленных данных
В данном примере будет рассмотрена обработка данных и обучение на их основе нейросетевой модели. Будет продемонстрирован метод скользящего окна - для разделения обучающей и тестовой выборки на наборы данных для обучения, а также будут определены параметры модели, для получения наиболее точных прогнозных значений.
Запуск необходимых библиотек:
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 с помощью метода скользящего окна.
Метод скользящего окна:
где 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="Функция потерь")
Получение прогнозных значений:
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)
Так как исходный датасет имеет участки, в которых пропущенные значения были заменены линейной интерполяцией, то сложно оценить работу обученной нейросетевой модели на прямой.
Для этого были загружены реальные данные без пропусков:
real_data = DataFrame(CSV.File("$(@__DIR__)/real_data.csv"));
Построение графика зависимости температуры от времени по реальным и спрогнозированным данным:
plot(real_data[1:261,2])
plot!(y_pred)
Проверим взаимосвязь полученных величин с помощью корреляции Пирсона, таким образом оценив точность полученной модели:
corr_T = cor(y_pred,real_data[1:261,2])
0.9131755711843572
Коэффициент корреляции Пирсона может принимать значения от -1 до 1, где 0 будет означать отсутствие связи между переменными, а -1 и 1 - тесную связь (обратная и прямая зависимость соответственно).
Выводы
В данном примере были предобработаны данные температурных наблюдений за последние пять лет, а также определена архитектура нейросети, параметры оптимизатора и функция потерь. Модель была обучена и показала достаточно высокую, но не идеальную сходимость предсказанных значений с реальными данными. Для улучшения качества прогноза нейросеть может быть модифицирована путём изменения архитектуры слоёв и увеличения обучающей выборки.