Створення алгоритму прогнозування результатів матчів у Dota 2: Мій шлях та досвід
«Я не геній, я не знаю майбутнього. Але мені подобається ідея, що розумні люди використовують технології та дані, щоб допомогти нам вдосконалюватися. Ми використовуємо аналітику, і вона нам дуже допомагає». — Юрген Клопп.
Як завзяті геймери, ми стежимо за непрогнозованістю через непередбачувану природу багатокористувацьких онлайн-ігор (MOBA). Dota 2 — одна з найскладніших MOBA. Існує багато факторів — герої, предмети, навички та стратегії гравців — які роблять передбачення результату турніру складним завданням. Оскільки люди, які люблять програмування і Dota 2, можуть визначати результат матчів Dota 2, цей проект розпочався як подарунок моєму брату, а незабаром перетворився на можливість вивчати Data Science, Машинне навчання та кіберспортивну галузь.
У цій статті я проведу вас через шлях створення предиктора для Dota 2: від збору та обробки даних до навчання моделей машинного навчання і, зрештою, прогнозування результатів. Якщо ви фанат Dota 2, програміст або просто цікавитеся процесом, я сподіваюся, що ця стаття буде для вас корисною. Вона дасть вам розуміння того, як побудована система та які виклики з нею пов’язані.
Чому я створив Dota 2 Predictor
Як фанат Dota 2, я часто задаюся питанням, які команди виграють або програють. Чи бувають збіги у виборі героїв? Можливо впливають особисті навички гравців? Чи які предмети вони купують? Натхненний цими питаннями та захопленням мого брата грою, я поставив собі за мету відповісти на ці питання, використовуючи дані та машинне навчання.
Структура набору даних і дизайн системи прогнозування
З чітким розумінням того, які класи нам потрібні — герої, гравці, команди, матчі, турніри — настав час зануритися глибше в те, як вони обробляються і використовуються для прогнозування результатів матчів. Ось як працює кожна категорія і як перетворити ці дані на ключові характеристики для наших моделей прогнозування.
Клас Hero
Клас Hero показує детальну інформацію про конкретного героя, включаючи відсоток перемог у професійних іграх. За допомогою методу get_hero_features() клас отримує таку інформацію, як ім’я героя, його піки та перемоги. Ця інформація важлива, тому що гра героя є ключовим фактором у результаті матчу.
class Hero: def __init__(self, hero_id): self.hero_id = hero_id self.features = self.get_hero_features() self.name = self.features["name"] if self.features else "Unknown Hero" if self.features and self.features["pro_pick"] > 0: self.winrate = self.features["pro_win"] / self.features["pro_pick"] else: self.winrate = 0 def get_hero_features(self): url = f"https://api.opendota.com/api/heroStats?api_key={opendota_key}" response = requests.get(url) if response.status_code == 200: heroes = response.json() for hero in heroes: if hero["id"] == self.hero_id: return { "hero_id": hero["id"], "name": hero["localized_name"], "pro_win": hero.get("pro_win", 0), "pro_pick": hero.get("pro_pick", 0), } else: print(f"Error fetching data: {response.status_code}") return None def __repr__(self): return f"Hero(ID: {self.hero_id}, Name: {self.name}, Features: {self.features}
Клас Player
Клас Player представляє окремих гравців і записує їхню статистику, таку як вбивства, смерті, передачі, отримане золото та досвід за хвилину. Цю статистику можна отримати за допомогою методів get_player_total_data() та get_player_data(), обидва з яких покладаються на OpenDоta API.
class Player: def __init__(self, account_id, name, hero_id, team): self.account_id = account_id self.name = name self.team = team self.hero = Hero(hero_id) self.player_data = self.get_player_data() player_data = self.get_player_total_data() kills = find_dict_in_list(player_data, "field", "kills") self.kills = kills["sum"] / kills["n"] if kills["n"] > 0 else 0 deaths = find_dict_in_list(player_data, "field", "deaths") self.deaths = deaths["sum"] / deaths["n"] if deaths["n"] > 0 else 0 assists = find_dict_in_list(player_data, "field", "assists") self.assists = assists["sum"] / assists["n"] if assists["n"] > 0 else 0 gold_per_min = find_dict_in_list(player_data, "field", "gold_per_min") self.gold_per_min = ( gold_per_min["sum"] / gold_per_min["n"] if gold_per_min["n"] > 0 else 0 ) xp_per_min = find_dict_in_list(player_data, "field", "xp_per_min") self.xp_per_min = ( xp_per_min["sum"] / xp_per_min["n"] if xp_per_min["n"] > 0 else 0 ) last_hits = find_dict_in_list(player_data, "field", "last_hits") self.last_hits = last_hits["sum"] / last_hits["n"] if last_hits["n"] > 0 else 0 denies = find_dict_in_list(player_data, "field", "denies") self.denies = denies["sum"] / denies["n"] if denies["n"] > 0 else 0 def get_player_total_data(self): """Fetch player total data with indefinite retries until success.""" url = f"https://api.opendota.com/api/players/{self.account_id}/totals?api_key={opendota_key}&hero_id={self.hero.hero_id}&limit=30" while True: # Retry loop try: response = requests.get(url) if response.status_code == 200: return response.json() # Successful response, exit loop else: print( f"Error fetching player data: {response.status_code}. Retrying..." ) except requests.exceptions.RequestException as e: print(f"Request failed: {e}. Retrying...") sleep(2) def get_player_data(self): # Fetch general win/loss data url = f"https://api.opendota.com/api/players/{self.account_id}/wl?api_key={opendota_key}" response = requests.get(url) if response.status_code == 200: data = response.json() player_stats = { "win_rate": ( data.get("win") / (data.get("win") + data.get("lose")) if (data.get("win") + data.get("lose")) > 0 else 0 ), } hero_url = f"https://api.opendota.com/api/players/{self.account_id}/heroes?api_key={opendota_key}&limit=30" hero_response = requests.get(hero_url) if hero_response.status_code == 200: hero_data = hero_response.json() for hero in hero_data: if hero["hero_id"] == self.hero.hero_id: # Calculate the hero's win rate if hero["games"] > 0: self.hero_win_rate = hero["win"] / hero["games"] else: self.hero_win_rate = 0 break else: print(f"Error fetching hero data: {hero_response.status_code}") return player_stats else: print(f"Error fetching player data: {response.status_code}") return None def __repr__(self): return f"Player({self.name}, Hero : {self.hero.name}, Team: {self.team}, Data: {self.player_data})"
На додаток до загальної інформації про перемоги/поразки, клас Player відстежує показники героя лише тоді, коли гравець грає цим героєм. Ця комбінація статистики кожного героя формує основу нашої системи прогнозування, поєднуючи навички гравців у виборі героя та минулі результати в поєднанні з оперативною інформацією.
Team клас
Клас Team об’єднує гравців у команди. Кожен об’єкт команди може додати об’єкт Player за допомогою методу add_player(). Ця конструкція важлива для відтворення реальних змагальних ситуацій, коли координація між членами команди та вибір героя може вплинути на результат матчу.
class Team: def __init__(self, team_name: str, team_id: int): self.team_name = team_name self.team_id = team_id self.players = [] def add_player(self, player): self.players.append(player) def __repr__(self): return f"Team({self.team_name}, ID: {self.team_id}, Players: {self.players})"
Клас Match
Клас Match зберігає всю важливу статистику матчу, включаючи обидві команди, їхніх гравців та кінцевий результат матчу. Метод get_match_data() заповнює цю інформацію, витягуючи деталі матчу з OpenDota API, що дозволяє нам розбити вплив кожного гравця на матч.
class Match: def __init__( self, match_id: int, radiant_team_id: int, dire_team_id: int, league_id: int, radiant_win=None, ): self.match_id = match_id self.radiant_team_id = radiant_team_id self.dire_team_id = dire_team_id self.radiant_team = None self.dire_team = None self.league_id = league_id self.radiant_win = radiant_win def get_match_data(self): url = f"https://api.opendota.com/api/matches/{self.match_id}?api_key={opendota_key}" response = requests.get(url) if response.status_code == 200: match_info = response.json() radiant_team = Team( match_info["radiant_name"], match_info["radiant_team_id"] ) dire_team = Team(match_info["dire_name"], match_info["dire_team_id"]) self.radiant_win = match_info["radiant_win"] for player in match_info["players"]: if player["isRadiant"]: player = Player( player["account_id"], player["name"], player["hero_id"], radiant_team.team_name, ) radiant_team.add_player(player) else: player = Player( player["account_id"], player["name"], player["hero_id"], dire_team.team_name, ) dire_team.add_player(player) self.radiant_team = radiant_team self.dire_team = dire_team def get_match_data_for_prediction(self): if len(self.radiant_team.players) == 5 and len(self.dire_team.players) == 5: # Create a single row with match and player data match_data = { "match_id": self.match_id, "radiant_team_id": self.radiant_team.team_id, "radiant_team_name": self.radiant_team.team_name, "dire_team_id": self.dire_team.team_id, "dire_team_name": self.dire_team.team_name, } # Add radiant team player data (5 players) for i, player in enumerate(self.radiant_team.players): match_data[f"radiant_player_{i + 1}_id"] = player.account_id match_data[f"radiant_player_{i + 1}_name"] = player.name match_data[f"radiant_player_{i + 1}_hero_id"] = player.hero.hero_id match_data[f"radiant_player_{i + 1}_hero_name"] = player.hero.name match_data[f"radiant_player_{i + 1}_hero_winrate"] = player.hero.winrate match_data[f"radiant_player_{i + 1}_winrate"] = player.player_data[ "win_rate" ] match_data[f"radiant_player_{i + 1}_kills"] = player.kills match_data[f"radiant_player_{i + 1}_deaths"] = player.deaths match_data[f"radiant_player_{i + 1}_assists"] = player.assists match_data[f"radiant_player_{i + 1}_gold_per_min"] = player.gold_per_min match_data[f"radiant_player_{i + 1}_xp_per_min"] = player.xp_per_min # Add dire team player data (5 players) for i, player in enumerate(self.dire_team.players): match_data[f"dire_player_{i + 1}_id"] = player.account_id match_data[f"dire_player_{i + 1}_name"] = player.name match_data[f"dire_player_{i + 1}_hero_id"] = player.hero.hero_id match_data[f"dire_player_{i + 1}_hero_name"] = player.hero.name match_data[f"dire_player_{i + 1}_hero_winrate"] = player.hero.winrate match_data[f"dire_player_{i + 1}_winrate"] = player.player_data[ "win_rate" ] match_data[f"dire_player_{i + 1}_kills"] = player.kills match_data[f"dire_player_{i + 1}_deaths"] = player.deaths match_data[f"dire_player_{i + 1}_assists"] = player.assists match_data[f"dire_player_{i + 1}_gold_per_min"] = player.gold_per_min match_data[f"dire_player_{i + 1}_xp_per_min"] = player.xp_per_min df = pd.DataFrame([match_data]) df = prepare_data(df) top_features = df.columns.tolist() return df, top_features def __repr__(self): # Prepare the Radiant team players radiant_players = "\n".join( [ f" Player: {player.name} (Hero : {player.hero.name})" for player in self.radiant_team.players ] ) # Prepare the Dire team players dire_players = "\n".join( [ f" Player: {player.name} (Hero : {player.hero.name})" for player in self.dire_team.players ] ) # Format the result return ( f"Match ID: {self.match_id}\n" f"League ID: {self.league_id}\n" f"Radiant Team: {self.radiant_team.team_name}\n" f"Radiant Players:\n{radiant_players}\n" f"Dire Team: {self.dire_team.team_name}\n" f"Dire Players:\n{dire_players}\n" f"Radiant Win: {'Yes' if self.radiant_win else 'No'}" )
Метод get_match_data_for_prediction() збирає дані від обох команд, які потім форматуються для введення в модель прогнозування. Це забезпечує відповідну структуру набору даних для машинного навчання, відокремлюючи пов’язану статистику.
Клас Tournament
Нарешті, клас Tournament збирає всі матчі турніру і зберігає їх у вигляді списку. Це особливо корисно для аналізу великих груп матчів і виділення загальних тенденцій.
class Tournament: def __init__(self, league_id: int, name: str): self.league_id = league_id self.name = name self.matches = [] def add_match(self, match): self.matches.append(match) def get_league_matches(self): url = f"https://api.opendota.com/api/leagues/{self.league_id}/matches?api_key={opendota_key}" response = requests.get(url) if response.status_code == 200: for match_info in response.json(): match_id = match_info["match_id"] radiant_team_id = match_info["radiant_team_id"] dire_team_id = match_info["dire_team_id"] radiant_win = match_info["radiant_win"] match = Match( match_id, radiant_team_id, dire_team_id, self.league_id, radiant_win ) match.get_match_data() self.add_match(match) else: print( f"Error fetching matches for league {self.league_id}: {response.status_code}" ) def __repr__(self): return f"Tournament({self.name}, ID: {self.league_id})"
Генерація даних для професійних ліг Dota 2
Прогностична здатність моделі машинного навчання значною мірою залежить від якості даних, які вона обробляє. Для Dota 2 створення повного набору даних є надзвичайно важливим. У цьому розділі описано, як ми отримуємо історичні дані на рівні матчів. Зосереджуючись на турнірах преміум-класу з використанням OpenDota API, цей набір даних використовується для навчання моделі, яка прогнозує результати турніру на основі комбінації історичних показників і даних прямих трансляцій матчів.
1.Історичні дані про гравців
Одним з найважливіших джерел інформації є інформація з профілю гравця, яка містить метрики, що відображають загальну продуктивність у попередній грі. Вона дає уявлення про наступні питання:
- Стабільність гравця: Гравці, які мають високі показники вбивств, асистувань або GPM (золото за хвилину), є більш ефективними та надійними.
- Специфічні показники героя: Такі статистичні дані, як відсоток перемог гравців з певним героєм, показують, наскільки добре вони володіють цим персонажем.
Наприклад, за допомогою кінцевих посилань OpenDota /totals або /heroes ви можете збирати дані:
- Вбивства за гру: Середню кількість вбивств, яку гравець заробив у всіх або останніх іграх.
- Відсоток перемог для певного героя: Як часто гравці перемагають, використовуючи конкретних героїв.
- GPM і XPM (досвід за хвилину): Вимірює, наскільки ефективно гравці добувають золото та отримують досвід з плином часу.
Ця статистика допомагає передбачити, як гравець, ймовірно, виступить у майбутніх матчах. В основному, історичні дані розкривають закономірності щодо загального рівня майстерності гравців, тенденцій та сильних сторін конкретного героя.
2. Використання історичних даних для прогнозування
Це тому, що історичні дані не прив’язані до конкретного змагання. Таким чином, вони слугують комплексним показником ефективності. Ось як ці показники впливають на модель:
- Характеристики гравців: Такі показники, як вбивства, смерті, передачі, GPM і XPM використовуються як предиктори поведінки гравця в грі.
- Усереднені історичні дані: Наприклад, середня кількість вбивств, відсоток перемог і GPM гравця в останніх N іграх дає більш стабільну оцінку його загального потенціалу.
Приклади історичних характеристик гравців можуть включати:
- average_kills: Середня кількість вбивств гравця за останні N ігор.
- win_rate: Відсоток перемог гравця в останніх N іграх.
- hero_win_rate: Відсоток перемог гравця з певним героєм за останні N ігор.
- gold_per_min: середній GPM гравця за останні N ігор.
Ці характеристики дозволяють моделі оцінити ймовірний вплив кожного гравця в конкретному матчі на основі його минулих результатів.
3. Характеристики героїв
На додаток до статистики гравців, важливу роль відіграють також дані про героїв. Ця статистика не відноситься до поточної гри, а є середнім значенням з усіх матчів, де був обраний герой. Це допомагає оцінити силу героїв у поточній меті:
- Частота вибору: Як часто героя вибирають у професійних матчах.
- Процент перемог: Відсоток перемог героя в професійних іграх.
Ці характеристики героя дають моделі розуміння ширшої ефективності даного героя в професійній грі, що має вирішальне значення при складанні прогнозів.
4. Поєднання історичних даних з даними матчів у реальному часі
Для моделі прогнозування результатів матчів важливими є як історичні дані, так і дані з живих матчів. Ідея полягає в тому, щоб об’єднати їх:
- Історичні дані гравців: Таку інформацію, як вбивства, асисти, коефіцієнт перемог і GPM, які показують, як гравці виступали в минулому.
- Дані про ефективність героя: Статистичні дані, такі як відсоток перемог героя та кількість обирань, які відображають загальну силу героя в професійних матчах.
Це поєднання історичних і реальних даних формує повний набір вхідних даних для моделей машинного навчання. Незважаючи на те, що історичні дані не відображають події конкретного матчу, вони слугують потужним індикатором потенціалу гравця та героя, який можна використовувати для прогнозування результатів.
5. Здобування даних про матчі за допомогою OpenDota API
З таким розумінням історичних даних та даних про героїв, наступним кроком є вилучення даних рівня матчу з профі-ліг Dota 2. Використовуючи API OpenDota, ми збираємо детальну інформацію про матчі з таких престижних турнірів, як The International та ESL One.
Ось скрипт на Python, який демонструє, як згенерувати набір даних з цих ліг, включно з даними на рівні матчів та гравців:
from structure.struct import Tournament from structure.opendota import OpenDotaApi def generate_dataset(): api = OpenDotaApi() dataset = [] premium_leagues = api.set_premium_leagues() last_big_leagues = [ "ESL One Kuala Lumpur powered by Intel" "BetBoom Dacha Dubai 2024", "DreamLeague Season 22 powered by Intel", "Elite League Season 2 Main Event – presented by ESB", "ESL One Birmingham 2024 Powered by Intel", "DreamLeague Season 23 powered by Intel", "Riyadh Masters 2024 at Esports World Cup", "Clavision DOTA League S1 : Snow-Ruyi", "The International 2024", "PGL Wallachia 2024 Season 1", ] for premium_league in premium_leagues: league_id = premium_league["leagueid"] league_name = premium_league["name"] if league_name in last_big_leagues: tournament = Tournament(league_id=league_id, name=league_name) tournament.get_league_matches() # Load matches for the tournament # Extract data from each match in the tournament for match in tournament.matches: radiant_team = match.radiant_team dire_team = match.dire_team # Ensure we have 5 players on each team if len(radiant_team.players) == 5 and len(dire_team.players) == 5: # Create a single row with match and player data match_data = { "match_id": match.match_id, "radiant_team_id": radiant_team.team_id, "radiant_team_name": radiant_team.team_name, "dire_team_id": dire_team.team_id, "dire_team_name": dire_team.team_name, "radiant_win": match.radiant_win, # True/False if Radiant team won } # Add radiant team player data (5 players) for i, player in enumerate(radiant_team.players): match_data[f"radiant_player_{i + 1}_id"] = player.account_id match_data[f"radiant_player_{i + 1}_name"] = player.name match_data[f"radiant_player_{i + 1}_hero_id"] = ( player.hero.hero_id ) match_data[f"radiant_player_{i + 1}_hero_name"] = ( player.hero.name ) match_data[f"radiant_player_{i + 1}_hero_winrate"] = ( player.hero.winrate ) match_data[f"radiant_player_{i + 1}_winrate"] = ( player.player_data["win_rate"] ) match_data[f"radiant_player_{i + 1}_kills"] = player.kills match_data[f"radiant_player_{i + 1}_deaths"] = player.deaths match_data[f"radiant_player_{i + 1}_assists"] = player.assists match_data[f"radiant_player_{i + 1}_gold_per_min"] = ( player.gold_per_min ) match_data[f"radiant_player_{i + 1}_xp_per_min"] = ( player.xp_per_min ) # Add dire team player data (5 players) for i, player in enumerate(dire_team.players): match_data[f"dire_player_{i + 1}_id"] = player.account_id match_data[f"dire_player_{i + 1}_name"] = player.name match_data[f"dire_player_{i + 1}_hero_id"] = player.hero.hero_id match_data[f"dire_player_{i + 1}_hero_name"] = player.hero.name match_data[f"dire_player_{i + 1}_hero_winrate"] = ( player.hero.winrate ) match_data[f"dire_player_{i + 1}_winrate"] = player.player_data[ "win_rate" ] match_data[f"dire_player_{i + 1}_kills"] = player.kills match_data[f"dire_player_{i + 1}_deaths"] = player.deaths match_data[f"dire_player_{i + 1}_assists"] = player.assists match_data[f"dire_player_{i + 1}_gold_per_min"] = ( player.gold_per_min ) match_data[f"dire_player_{i + 1}_xp_per_min"] = ( player.xp_per_min ) print(match_data) # Append match data to dataset dataset.append(match_data) df = pd.DataFrame(dataset) # Write DataFrame to a CSV file df.to_csv("premium_league_matches.csv", index=False) print("Match dataset has been generated and saved to 'premium_league_matches.csv'.") generate_dataset()
6. Структурування та збереження набору даних
Остаточний набір даних зберігається у файлі CSV. Кожен рядок представляє один матч, фіксуючи дані як на рівні матчу, так і на рівні гравця, включаючи історичну статистику гравця та ефективність героя. Цей набір даних забезпечує вхідні дані для наших моделей машинного навчання для прогнозування результатів матчів.
Підготовка даних
Підготовка даних — один з найважливіших етапів у робочому процесі машинного навчання, оскільки він суттєво впливає на продуктивність і точність предиктивних моделей. У цьому розділі я розповім, як необроблені дані про матчі були перетворені на структурований набір даних, готовий до аналізу машинного навчання.
1. Інженерія ознак
Інженерія ознак передбачає створення нових ознак на основі наявних даних для покращення інсайтів моделі. Для цього проекту ми розробили функції на основі команд, які підсумовують ключові показники ефективності для команд Radiant і Dire:
- Середній відсоток перемог героя: Ми розрахували середній відсоток перемог героїв, обраних гравцями в кожній команді. Це дає уявлення про те, наскільки історично успішним є склад героїв кожної команди.
- Середній відсоток перемог гравців: Подібно до відсотка перемог героїв, ця функція відображає середній відсоток перемог гравців у команді, пропонуючи погляд на силу гравців команди.
- Загальна кількість вбивств, смертей та асистів: Підсумовуючи кількість вбивств, смертей та асистів для всіх гравців у кожній команді, ми отримуємо загальну картину ефективності команди.
- Середня кількість золота за хвилину (GPM) та досвіду за хвилину (XPM): Розрахунок середнього GPM і XPM для кожної команди дає змогу оцінити рівень накопичення ресурсів, що має вирішальне значення для розуміння того, наскільки добре команда розвивається і підвищує свій рівень.
Ці командні характеристики були створені за допомогою функції calculate_team_features(), яка обробляє DataFrame і обчислює нові метрики на основі заданого префікса команди («radiant» або «dire»). Після створення нових характеристик оригінальні стовпці, що використовувалися для їхнього обчислення, було вилучено, щоб уникнути надмірності.
def calculate_team_features(df, team_prefix): """ Function to calculate team-based features for a given prefix (radiant or dire). """ # Team Hero Win Rate: Average win rate of the heroes for the team hero_winrate_cols = [f"{team_prefix}_player_{i}_hero_winrate" for i in range(1, 6)] df[f"{team_prefix}_avg_hero_winrate"] = df[hero_winrate_cols].mean(axis=1) # Team Player Win Rate: Average win rate of the players for the team player_winrate_cols = [f"{team_prefix}_player_{i}_winrate" for i in range(1, 6)] df[f"{team_prefix}_avg_player_winrate"] = df[player_winrate_cols].mean(axis=1) # Team Kills, Deaths, Assists kills_cols = [f"{team_prefix}_player_{i}_kills" for i in range(1, 6)] deaths_cols = [f"{team_prefix}_player_{i}_deaths" for i in range(1, 6)] assists_cols = [f"{team_prefix}_player_{i}_assists" for i in range(1, 6)] df[f"{team_prefix}_total_kills"] = df[kills_cols].sum(axis=1) df[f"{team_prefix}_total_deaths"] = df[deaths_cols].sum(axis=1) df[f"{team_prefix}_total_assists"] = df[assists_cols].sum(axis=1) # Team GPM and XPM: Average GPM and XPM per team gpm_cols = [f"{team_prefix}_player_{i}_gold_per_min" for i in range(1, 6)] xpm_cols = [f"{team_prefix}_player_{i}_xp_per_min" for i in range(1, 6)] df[f"{team_prefix}_avg_gpm"] = df[gpm_cols].mean(axis=1) df[f"{team_prefix}_avg_xpm"] = df[xpm_cols].mean(axis=1) # Drop the original columns used to create these features df.drop( columns=hero_winrate_cols + player_winrate_cols + gpm_cols + xpm_cols, inplace=True, ) return df
На додаток до командних характеристик, ми вирахували співвідношення «вбивство-смерть-асист» (KDA) для кожного гравця. KDA — це широко використовуваний показник у змагальних іграх, який допомагає оцінити індивідуальну ефективність гравців. Використовувана формула така:
Це дозволяє уникнути ділення на нуль, забезпечуючи надійність розрахунків. Після обчислення KDA для всіх гравців, оригінальні стовпці для вбивств, смертей та асистів були видалені.
Ось реалізація функції calculate_player_kda:
def calculate_player_kda(df, team_prefix): """ Function to calculate KDA (Kill-Death-Assist ratio) for each player. """ for i in range(1, 6): df[f"{team_prefix}_player_{i}_kda"] = ( df[f"{team_prefix}_player_{i}_kills"] + df[f"{team_prefix}_player_{i}_assists"] ) / df[f"{team_prefix}_player_{i}_deaths"].replace( 0, 1 ) # Avoid division by zero # Drop kills, deaths, and assists for each player df.drop( columns=[ f"{team_prefix}_player_{i}_kills", f"{team_prefix}_player_{i}_deaths", f"{team_prefix}_player_{i}_assists", ], inplace=True, ) return df
2. Створення цільової змінної
Щоб перетворити набір даних для бінарної класифікації, ми перетворили стовпець radiant_win у цілочисельний формат, де:
- 1 означає «Radiant victory»
- 0 означає «Dire victory
Цей крок є важливим для моделі керованого навчання, де передбачення команди-переможця (Radiant або Dire) є основною метою.
3. Очищення даних
Очищення даних було виконано для видалення непотрібних стовпців, які не впливають на продуктивність моделі. Зокрема, ми видалили:
- Ідентифікатори, специфічні для збігів: Такі як match_id, radiant_team_id, dire_team_id тощо, оскільки вони не є прогностичними ознаками.
- Ідентифікатори гравців: Імена та ідентифікатори гравців були видалені, щоб анонімізувати дані і зосередитися виключно на показниках ефективності.
Ці кроки допомагають зменшити надмірне припасування і гарантують, що модель добре узагальнює невидимі дані.
4. Нормалізація
Щоб гарантувати, що всі ознаки однаково впливають на модель машинного навчання, ми застосували Min-Max нормалізацію для масштабування ознак між 0 і 1. Це особливо важливо для таких алгоритмів, як логістична регресія і нейронні мережі, які чутливі до величини вхідних ознак.
Ми використали MinMaxScaler для нормалізації всіх числових стовпців, окрім radiant_win (нашої цільової змінної). Нормалізація гарантує, що такі характеристики, як кількість вбивств, GPM та відсоток перемог героя, мають однакову шкалу.
def prepare_data(df): # Apply feature engineering for both Radiant and Dire teams df = calculate_team_features(df, "radiant") df = calculate_team_features(df, "dire") # Calculate KDA for each player (for both teams) df = calculate_player_kda(df, "radiant") df = calculate_player_kda(df, "dire") # Create a new column for the match target: 1 if radiant_win is True, else 0 try: df["radiant_win"] = df["radiant_win"].astype(int) except KeyError: pass df.drop( columns=[ "match_id", "radiant_team_id", "radiant_team_name", "dire_team_id", "dire_team_name", # Drop player names to anonymize data *[f"radiant_player_{i}_name" for i in range(1, 6)], *[f"radiant_player_{i}_id" for i in range(1, 6)], *[f"radiant_player_{i}_hero_name" for i in range(1, 6)], *[f"dire_player_{i}_name" for i in range(1, 6)], *[f"dire_player_{i}_id" for i in range(1, 6)], *[f"dire_player_{i}_hero_name" for i in range(1, 6)], ], inplace=True, ) columns_to_normalize = df.columns.difference(["match_id", "radiant_win"]) # Initialize the MinMaxScaler scaler = MinMaxScaler() # Apply Min-Max normalization df[columns_to_normalize] = scaler.fit_transform(df[columns_to_normalize]) return df
Розробка моделі машинного навчання
Для нашої першої ітерації розробки моделі ми обрали XGBoost як основний алгоритм. XGBoost, скорочення від Extreme Gradient Boosting, відомий своєю ефективністю та високою продуктивністю, особливо зі структурованими/табличними даними, що робить його популярним вибором на змаганнях з машинного навчання та в реальних додатках.
Чому XGBoost?
Є кілька причин, чому XGBoost було обрано для цієї початкової ітерації:
- Продуктивність на невеликих наборах даних: XGBoost є високоефективним навіть з обмеженими даними, що відповідає нашому відносно невеликому розміру набору даних. Він може забезпечити конкурентні результати, не вимагаючи великої кількості даних для навчання.
- Ефективність: XGBoost оптимізовано для швидкості та продуктивності. Його реалізація розроблена для ефективної роботи з великими наборами даних, але він також чудово справляється і з меншими наборами даних завдяки швидкому навчанню та здатності швидко ітерації над гіперпараметрами.
- Надійність: XGBoost за своєю суттю знижує ризик надмірного пристосування завдяки вбудованим методам регуляризації, таким як L1 (Лассо) і L2 (Ridge). Це робить його більш надійним при роботі з зашумленими або складними наборами даних.
Враховуючи ці переваги, ми використали можливості класифікації XGBoost для прогнозування результату матчу (промениста перемога або поразка). Наш робочий процес складався з кількох ключових кроків:
1. Поділ даних
Спочатку ми розділили наш набір даних на:
- Характеристики: Сюди входила статистика гравців (наприклад, вбивства, смерті, передачі), вибір героїв та інші показники ефективності команди.
- Цільова змінна: Цільовою змінною є результат матчу (radiant_win), який вказує на те, чи виграла команда Radiant, чи програла.
Далі ми розділили дані на навчальні та тестові набори, використовуючи розподіл
2. Навчання моделі
Для початкової ітерації ми використовували оригінальні параметри XGBoost, щоб створити базову модель. Хоча налаштування гіперпараметрів може покращити продуктивність, початок з усталених параметрів дозволяє нам оцінити базову продуктивність і визначити області для покращення.
3. Оцінка моделі
Після навчання моделі ми оцінили її продуктивність на тестовому наборі, використовуючи ключові метрики класифікації:
- Точність: Вимірює загальну правильність прогнозів.
- Точність, Recall та F1-Score: Ці метрики дають нам глибше розуміння того, наскільки добре модель розрізняє класи, особливо в сценаріях, де розподіл класів може бути незбалансованим.
- Матриця плутанини: Надає детальну розбивку істинно позитивних, хибнопозитивних, істинних негативних і хибнонегативних спрацьовувань.
4. Збереження моделі
Після успішного навчання модель було збережено за допомогою бібліотеки joblib. Це дозволяє нам повторно використовувати модель для майбутніх прогнозів без необхідності щоразу її перенавчати.
5. Прогнозування на нових даних
З навченою моделлю ми можемо робити прогнози на нових, ще не бачених даних. Ця можливість дозволяє нам прогнозувати результати матчів на основі схожих наборів ознак, надаючи уявлення про потенційні результати гри.
Реалізація
Нижче наведено реалізацію пайплайну машинного навчання на Python, інкапсульовану в клас MainML:
from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix from xgboost import XGBClassifier import joblib class MainML: """ Main class that orchestrates model training, evaluation, and prediction. """ def __init__(self, df, model_path): self.df = df self.model_path = model_path self.xgb_model = XGBClassifier(random_state=42) def train_and_save_model(self, features, target): """ Trains the XGBoost model and saves it to the specified path. """ # Split the dataset into features (X) and target (y) X = self.df[features] y = self.df[target] # Split data into training and testing sets X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # Train the model self.xgb_model.fit(X_train, y_train) # Save the model joblib.dump(self.xgb_model, self.model_path) print(f"Model saved to {self.model_path}") # Evaluate the model on the test set self.evaluate_model(X_test, y_test) def evaluate_model(self, X_test, y_test): """ Evaluates the model on the test data and prints the classification report and confusion matrix. """ # Make predictions on the test set y_pred = self.xgb_model.predict(X_test) # Print classification report print("XGBoost Classification Report:") print(classification_report(y_test, y_pred)) # Print confusion matrix print("XGBoost Confusion Matrix:") print(confusion_matrix(y_test, y_pred)) def load_model(self): """ Loads the model from the specified path. """ self.xgb_model = joblib.load(self.model_path) print(f"Model loaded from {self.model_path}") def predict(self, new_data): """ Predicts the class for the new data point. """ # Ensure that the new_data has the same features as the training set prediction = self.xgb_model.predict(new_data) return prediction
Навчання моделі та прогнозування
У цьому розділі ми зосередимося на практичних кроках з навчання нашої моделі машинного навчання за допомогою класифікатора XGBoost, а потім використаємо навчену модель для прогнозування. Робочий процес включає завантаження набору даних, підготовку даних, навчання моделі, збереження її для подальшого використання і, зрештою, створення прогнозів на нових даних.
Ось покрокова інструкція:
1. Завантаження та підготовка набору даних
Ми починаємо з завантаження набору даних з файлу CSV. Сирі дані містять різноманітну статистику на рівні матчів та гравців, яку ми готуємо до машинного навчання, застосовуючи функціональну інженерію та нормалізацію. Функція prepare_data(), яку ми реалізували раніше, виконує ці завдання, перетворюючи сирі дані у формат, придатний для навчання моделі.
import os import pandas as pd from ml.model import MainML from structure.helpers import prepare_data # Define the file path to the dataset file_path = os.path.join("..", "dataset", "train_data", "all_data.csv") # Load the dataset into a DataFrame df = pd.read_csv(file_path) # Prepare the dataset for model training df = prepare_data(df)
У функції prepare_data() ми застосували методи функціональної інженерії для створення метрик на основі команд та гравців, нормалізували характеристики та видалили непотрібні стовпці. Це підготувало дані для ефективного навчання моделі та забезпечило узгодженість.
2. Визначення ознак і мети
Далі ми визначаємо ознаки (вхідні змінні) та ціль (вихідна змінна). У нашому випадку метою є radiant_win, бінарна змінна, яка вказує, чи виграла команда Radiant (1), чи програла (0) матч. Характеристики складаються з усіх інших стовпців у наборі даних, які описують різні показники ефективності команди та гравців.
# Specify the target column (the variable we want to predict) target = "radiant_win" # Specify the features (all columns except the target) features = df.columns.drop(target).tolist
Цей крок гарантує, що модель знає, які ознаки слід враховувати при прогнозуванні, і який стовпчик представляє результат матчу.
3. Ініціалізація та навчання моделі
Ми створюємо екземпляр класу MainML, який інкапсулює модель XGBoost, і переходимо до навчання моделі. Процес навчання включає в себе поділ даних на навчальні та тестові набори, настройку моделі на навчальних даних та оцінку її продуктивності на тестових даних.
# Path to save the trained model model_path = "../xgb_model.pkl" # Create an instance of MainML with the dataset and model path main_ml = MainML(df, model_path) # Train the model using the features and target, and save the model main_ml.train_and_save_model(features, targe
На цьому кроці
- Метод train_and_save_model() виконує весь процес навчання.
- Після навчання модель зберігається у файл (xgb_model.pkl) за допомогою бібліотеки joblib, щоб її можна було перезавантажити для майбутніх прогнозів без перенавчання.
4. Завантаження збереженої моделі
Після того, як модель збережена, ми можемо завантажити її в будь-який час, щоб зробити прогнози на нових даних. Завантаження моделі з диска дозволяє уникнути перенавчання, що робить її більш ефективною для прогнозування в реальному часі або повторних прогнозів.
# Load the previously saved model main_ml.load_model()
Цей рядок перезавантажує збережену модель XGBoost зі шляху model_path, роблячи її доступною для задач прогнозування.
5. Прогнозування на нових даних
Нарешті, ми готуємо вибірку нових даних, щоб продемонструвати, як модель можна використовувати для прогнозування. Тут ми моделюємо нові дані, взявши останні п’ять рядків нашого набору даних (за винятком цільового стовпчика). На практиці це можуть бути дані матчів у реальному часі або раніше невідомі дані.
# Prepare new data for prediction (excluding the target column) new_data = df.tail(5).drop(columns=[target]) # Example: Using the last 5 rows as new data # Make predictions using the loaded model prediction = main_ml.predict(new_data) # Output the predictions print(f"Prediction for new data: {prediction}")
У цьому прикладі
- Ми використовуємо метод .tail(5), щоб вибрати останні п’ять рядків даних і опустити стовпець radiant_win, оскільки ми робимо прогнози на основі цих невидимих даних.
- Метод predict() з класу MainML виводить прогнозований результат матчу (1 для перемоги Radiant, 0 для поразки Radiant).
Як запустити проект Dota 2 Predictor
Цей розділ містить покрокову інструкцію з налаштування та запуску проекту Dota 2 Predictor на вашому комп’ютері.
1. Клонування репозиторію
Для початку завантажте код проекту, клонувавши репозиторій GitHub. Відкрийте термінал і виконайте наступні команди:
git clone https://github.com/masterhood13/dota2predictor.git cd dota2predictor git checkout tags/1.0.1
Це створить локальну копію сховища і перемістить вас в каталог проекту.
2. Встановлення залежностей
Переконайтеся, що у вашій системі встановлено Python 3.9 або новішої версії. Далі вам потрібно буде встановити необхідні бібліотеки Python, виконавши
pip install -r requirements.txt
Ця команда встановить всі необхідні пакети, такі як pandas, xgboost, scikit-learn та інші, необхідні для обробки даних і завдань машинного навчання.
3. Налаштування змінних середовища
Вам знадобляться ключі API для OpenDota, Steam і Telegram, щоб увімкнути зчитування даних і функціонал бота. Створіть файл .env у каталозі проекту та додайте до нього ключі API наступним чином:
OPENDOTA_KEY=your_api_key STEAM_API_KEY=your_steam_api_key TELEGRAM_KEY=your_telegram_bot_token
Замініть your_opendota_api_key, your_steam_api_key та your_telegram_bot_token на ваші фактичні ключі API.
4. Запуск проекту
Після того, як все налаштовано, ви можете запустити бота, виконавши наступну команду:
python start.py
Це запустить бота-предиктора, що дозволить йому отримувати дані та взаємодіяти з API Dota 2 для прогнозування результатів матчів на основі навченої моделі машинного навчання.
Взаємодія з Telegram-ботом для прогнозування матчів Dota 2
Після того, як модель навчена і бот запущений, виконайте наступні кроки, щоб почати використовувати Telegram-бот для прогнозування матчів:
1. Отримайте доступ до Telegram-бота
Після налаштування бота в попередніх кроках відкрийте Telegram на своєму пристрої. Знайдіть свого бота за його іменем користувача або скористайтеся посиланням на бота (якщо ви поділилися ним із собою або іншими).
2. Почніть розмову
Надішліть боту будь-яке повідомлення, щоб почати взаємодію. Бот відповість меню з опціями на вибір.
3. Отримуйте прогнози для поточних матчів Dota 2
Щоб отримати прогнози на всі поточні матчі Dota 2, просто натисніть відповідну кнопку в меню бота (наприклад, «Отримати прогнози на матчі»). Після цього бот звернеться до API Dota 2 і покаже прогнози для кожної активної гри.
4. Перегляд результатів прогнозування
Для кожного активного матчу бот повертатиме прогнозованого переможця — команду Radiant або Dire — на основі оцінки даних матчу моделлю машинного навчання. Замість ймовірності перемоги ви отримаєте чіткий прогноз, який вказує, яка команда з більшою ймовірністю виграє матч, забезпечуючи розуміння поточної гри в реальному часі.
Ось приклад результату:
Високорівнева архітектура системи
Архітектура нашого бота-прогнозиста Dota 2 демонструє, як різні компоненти працюють разом, щоб надавати прогнози результатів матчів кінцевому користувачеві через Telegram-бот. Потік починається із запиту користувача і проходить через різні етапи збору даних, попередньої обробки, прогнозування моделі і, нарешті, надання відповіді.
Ось покрокова розбивка робочого процесу системи:
1. Взаємодія зкористувачем:
- Процес починається з того, що користувач надсилає запит боту через Telegram. Це може бути прохання передбачити результат поточного матчу з Dota 2.
2. Телеграм-бот:
- Телеграм-бот виступає інтерфейсом між користувачем і бекенд-системою. Отримавши запит, бот пересилає його до бекенду для подальшої обробки.
3. Бекенд-система:
- Бекенд-сервер організовує основну логіку процесу прогнозування. Він збирає необхідні дані про матчі та гравців із зовнішніх API, таких як OpenDota та Steam.
4. Отримання даних із зовнішніх API:
- Внутрішня система запитує дані про матчі та гравців з API OpenDota та, за необхідності, з API Steam, щоб збагатити дані про гравців додатковим контекстом.
5. Попередня обробка даних:
- Перед тим, як завантажити дані в модель прогнозування, рушій попередньої обробки перетворює необроблені дані у формат, придатний для аналізу. Це включає очищення, розробку функцій і нормалізацію, щоб забезпечити відповідність даних вимогам навченої моделі машинного навчання.
6. Прогнозування моделі XGBoost:
- Оброблені дані передаються в модель XGBoost, яка була навчена прогнозувати, чи переможе команда Radiant або Dire. Модель приймає рішення на основі історичних даних та поточного контексту матчу.
7. Повернення результатів:
- Після того, як модель надає свій прогноз, бекенд обробляє результат і надсилає його назад у Telegram-бот, який потім показує користувачеві прогнозованого переможця (Radiant або Dire).
Блок-схема системи
Ця архітектура забезпечує плавний, автоматизований потік від взаємодії з користувачем до прогнозування фінального матчу, дозволяючи користувачам легко отримати доступ до прогнозів для поточних ігор Dota 2. Кожен компонент працює незалежно, але робить свій внесок у загальну функціональність системи прогнозування.
Висновок. I що далі?
На першому етапі ми успішно побудували модель машинного навчання для прогнозування результатів матчів Dota 2, досягнувши базової точності близько 60%. Це багатообіцяючий результат, але він залишає багато можливостей для вдосконалення. Складність Dota 2 означає, що більш досконалі дані та інженерія функцій можуть забезпечити ще кращі прогнози.
Але це тільки початок.
У другій частині ми заглибимося в інженерію функцій, введемо нові метрики для кращого відображення динаміки команди та продуктивності гравців. Ми також інтегруємо додаткові джерела даних, включаючи статистику матчів у реальному часі від нашого Telegram-бота. Найцікавіше, що ми розробимо та навчимо спеціальну кастомну модель глибокого навчання, адаптовану спеціально для цього збагаченого набору даних.
Залишайтеся з нами — майбутні кроки вже не за горами!
Щоб дізнатися більше деталей та ознайомитися з проектом в цілому, відвідайте Dota 2 Predictor GitHub Repository.
27 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів