Я аналитик данных, а не специалист по данным, но в последнее время я читал о теории алгоритмов машинного обучения и ее реализации в R, и мне захотелось попробовать. Затем… Я попытался построить модель машинного обучения, чтобы предсказать вероятность победы моей сестры и меня в Brawlhalla Ranked 2v2.

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



Преобразование данных

Исходные данные те же, что и в предыдущей статье.

Цель состоит в том, чтобы прогнозировать на основе информации в предматчевых характеристиках (персонажи и цвет, выбранные нами, персонажи сцены и враги); нокауты и удары не применяются, потому что это информация после матча. Целевая переменная довольно сбалансирована: 0 = 318 и 1 = 320.

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

Последнее преобразование состоит в объединении Enemy_1 и Enemy_2, но оно может быть таким же, как team, потому что здесь не имеет значения порядок, тогда я использовал целевую функцию кодирования для каждого столбца и вычислил среднее гармоническое.

set.seed(123)
brawlhalla_clean <- brawlhalla %>% 
  mutate(team = paste0(Ch_D, "-", Ch_S),
         team = fct_lump_n(team, 7, other_level = "Other"),
         Rating = ifelse(Rating <= 1389, "Silver", "Gold"),
         Color = fct_lump_min(Color, 30, other_level = "Other"),
         Stage = fct_lump_min(Stage, 32, other_level = "Other"),
         k = sample(1:5, nrow(brawlhalla), replace = TRUE)) %>% 
  select(-starts_with(c("KO", "Hit", "Ch")))
#Creating a dataframe of enemies and lumping below 2%
most_freq_enemies2 <- data.frame(
  enemy = c(brawlhalla_clean$Enemy_1,
            brawlhalla_clean$Enemy_2),
  win = rep(brawlhalla_clean$Win, 2),
  k = rep(brawlhalla_clean$k, 2)
) %>% 
  mutate(enemy = fct_lump_prop(enemy, 0.02,
                               other_level = "Other_ch"))
#Target encoding from enemies
tar_enc_enemies <- target_enc(data = most_freq_enemies2,
                              enc_col = "enemy", tar_col = "win",
                              k_col = "k", kmin = 1, kmax = 5)
brawlhalla_clean <- brawlhalla_clean %>% 
  mutate(Enemy_1 = ifelse(Enemy_1 %in% tar_enc_enemies$enemy,
                          Enemy_1, "Other_ch"),
         Enemy_2 = ifelse(Enemy_2 %in% tar_enc_enemies$enemy,
                          Enemy_2, "Other_ch"))
#Joining the enemies to final dataset for ML
brawlhalla_ML <- brawlhalla_clean %>%
  left_join(tar_enc_enemies, by = c("Enemy_1" = "enemy")) %>% 
  rename(EnemyEnemy_1tar = tar_enc_enemy) %>% 
  left_join(tar_enc_enemies, by = c("Enemy_2" = "enemy")) %>% 
  rename(EnemyEnemy_2tar = tar_enc_enemy) %>% 
  mutate(Enemy_mean = 
    (2 * EnemyEnemy_1tar * EnemyEnemy_2tar) / (EnemyEnemy_1tar + EnemyEnemy_2tar)
  ) %>% 
  select(-c(EnemyEnemy_1tar, EnemyEnemy_2tar, 
            Enemy_1, Enemy_2,
            k)) %>% 
  mutate(Rating = factor(Rating),
         Win = factor(Win))

Разделение набора данных

После начального разделения (обучение 75%) я добавляю некоторый шум в набор данных Enemy_mean on training, чтобы избежать переобучения из-за утечки данных.

set.seed(684)
brawlhalla_split <- initial_split(brawlhalla_ML, strata = Win)
brawlhalla_train <- training(brawlhalla_split)
brawlhalla_test <- testing(brawlhalla_split)
Enemy_train_vect <- brawlhalla_train %>% 
  pull(Enemy_mean)
noise_limit <- (max(Enemy_train_vect) - min(Enemy_train_vect)) * 0.3
set.seed(1919)
brawlhalla_train <- brawlhalla_train %>% 
  mutate(noise = runif(nrow(.), -noise_limit, noise_limit),
         Enemy_mean = Enemy_mean + noise) %>% 
  select(-noise)

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

Я использовал случайный лес, который я знаю лучше других, более сложных, таких как «машина опорных векторов» или «нейронные сети». В наборе данных проверки я использовал технику начальной загрузки с 30 сгибами.

Были настроены параметры mtry (количество объектов, случайно выбранных алгоритмом для использования в деревьях) и min_n (количество точек данных, необходимых в узле перед разделением). Количество trees зафиксировано на 500. Первая сетка была общей с использованием 20 комбинаций.

set.seed(1313)
brawlhalla_folds <- bootstraps(brawlhalla_train,
                               strata = Win, times = 30)
#recipe definition
brawlhalla_rec <- recipe(Win ~., data = brawlhalla_train) %>% 
  step_dummy(all_nominal_predictors())
#model definition
rand_spec <- rand_forest(mtry = tune(),
                         min_n = tune(),
                         trees = 500) %>% 
  set_mode("classification") %>% 
  set_engine("ranger")
#workflow definition
brawlhalla_wf <- workflow() %>% 
  add_recipe(brawlhalla_rec) %>% 
  add_model(rand_spec)
doParallel::registerDoParallel()
set.seed(797)
rand_rs <- tune_grid(
  brawlhalla_wf,
  resamples = brawlhalla_folds,
  grid = 20,
  metrics = metric_set(accuracy, roc_auc)
)

Как видите, точность довольно низкая, поэтому в поисках небольшой оптимизации была построена другая сетка, учитывая, что 20 ≤ min_n ≤ 35 и 1 ≤ mtry ≤ 5 имеют большие средние значения.

rf_grid <- grid_regular(mtry(range = c(1, 5)),
             min_n(range = c(20, 35)),
             levels = 6)
doParallel::registerDoParallel()
set.seed(694)
regular_rs <- brawlhalla_wf %>%  
  tune_grid(resamples = brawlhalla_folds,
            grid = rf_grid)

Точность немного выросла. Гиперпараметр mtry = 2 и min_n = 20 дает максимальную точность, но низкий ROC-AUC; Я выбрал mtry = 1 и min_n = 32, потому что это баланс между двумя показателями.

best_tune <- tibble(mtry = 1, min_n = 32)
final_rf <- finalize_model(
  rand_spec,
  best_tune
)

Тестирование модели

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

final_wf <- workflow() %>% 
  add_recipe(brawlhalla_rec) %>%
  add_model(final_rf)
set.seed(857)
final_res <- final_wf %>% 
  last_fit(brawlhalla_split)
final_res %>% 
  collect_metrics()

Ну, могло быть и хуже, я прав? На приведенном ниже графике мы можем видеть вероятность, заданную моделью для контрольных точек по оси x и категорий color и team по оси y.

Модель с трудом классифицирует, когда color является Charged OG или Skyforged, и team в целом. Это может быть связано со многими точками, которые имеют эти переменные, но разница между выигрышами и проигрышами минимальна, и это может запутать модель. Серый color, например, имеет довольно большую разницу в выигрыше-проигрыше (-10), и предсказание модели почти правильное.

Кроме того, я думаю, что модель искажена из-за малого количества точек. Например, team Zariel-Val является лучшим с коэффициентом выигрыша 70%, но у него всего 26 очков во всем наборе данных; затем модель предсказала все вероятности для этой команды > 0,5; Потому что в тренировочных данных было 14 побед и 4 поражения.

Наконец, важность переменной (крутая штука случайного леса). Стадион «Тандергард» и «Энигма» — наши лучшие Stage, а Кинг-Пасс — худшие, и все они здесь; Зариэль-Вал — наш лучший team, а Goldenforged and Home team — наш лучший color. Я думаю, что модель имеет правильное направление и определяет некоторые закономерности, но ей нужно немного подтолкнуть мои данные (то есть больше данных).

set.seed(1912)
final_rf %>%
  set_engine("ranger", importance = "permutation") %>%
  fit(Win ~.,
      data = juice(prep(brawlhalla_rec))) %>% 
  vip(geom = "point") + theme_light()

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