Spaces:
Runtime error
Runtime error
import os | |
import random | |
import subprocess | |
import pandas as pd | |
from datetime import datetime | |
from huggingface_hub import HfApi, Repository | |
DATASET_REPO_URL = "https://huggingface.co/datasets/huggingface-projects/bot-fight-data" | |
DATASET_TEMP_REPO_URL = "https://huggingface.co/datasets/huggingface-projects/temp-match-results" | |
ELO_FILENAME = "soccer_elo.csv" | |
TEMP_FILENAME = "results.csv" | |
ELO_DIR = "soccer_elo" | |
TEMP_DIR = "temp" | |
HF_TOKEN = os.environ.get("HF_TOKEN") | |
repo = Repository( | |
local_dir=ELO_DIR, clone_from=DATASET_REPO_URL, use_auth_token=HF_TOKEN | |
) | |
repo_temp = Repository( | |
local_dir=TEMP_DIR, clone_from=DATASET_TEMP_REPO_URL, use_auth_token=HF_TOKEN | |
) | |
api = HfApi() | |
os.chmod('./SoccerTows.x86_64', 0o755) | |
class Model: | |
""" | |
Class containing the info of a model. | |
:param name: Name of the model | |
:param elo: Elo rating of the model | |
:param games_played: Number of games played by the model (useful if we implement sigma uncertainty) | |
""" | |
def __init__(self, author, name, elo=1200, games_played=0): | |
self.author = author | |
self.name = name | |
self.elo = elo | |
self.games_played = games_played | |
class Matchmaking: | |
""" | |
Class managing the matchmaking between the models. | |
:param models: List of models | |
:param queue: Temporary list of models used for the matching process | |
:param k: Dev coefficient | |
:param max_diff: Maximum difference considered between two models' elo | |
:param matches: Dictionary containing the match history (to later upload as CSV) | |
""" | |
def __init__(self, models): | |
self.models = models | |
self.queue = self.models.copy() | |
self.k = 20 | |
self.max_diff = 500 | |
self.matches = { | |
"model1": [], | |
"model2": [], | |
"result": [], | |
"timestamp": [], | |
"env": [] | |
} | |
def run(self): | |
""" | |
Run the matchmaking process. | |
Add models to the queue, shuffle it, and match the models one by one to models with close ratings. | |
Compute the new elo for each model after each match and add the match to the match history. | |
""" | |
self.queue = self.models.copy() | |
random.shuffle(self.queue) | |
while len(self.queue) > 1: | |
print(f"Queue length: {len(self.queue)}") | |
model1 = self.queue.pop(0) | |
model2 = self.queue.pop(self.find_n_closest_indexes(model1, 10)) | |
match(model1, model2) | |
self.load_results() | |
def load_results(self): | |
""" Load the match history from the hub. """ | |
repo.git_pull() | |
results = pd.read_csv( | |
"https://huggingface.co/datasets/huggingface-projects/temp-match-results/raw/main/results.csv" | |
) | |
# while len(results) < len(self.matches["model1"]): | |
# time.sleep(60) | |
# results = pd.read_csv( | |
# "https://huggingface.co/datasets/huggingface-projects/temp-match-results/raw/main/results.csv" | |
# ) | |
for i, row in results.iterrows(): | |
model1 = row["model1"].split("/") | |
model2 = row["model2"].split("/") | |
model1 = self.find_model(model1[0], model1[1]) | |
model2 = self.find_model(model2[0], model2[1]) | |
result = row["result"] | |
if model1 is not None or model2 is not None: | |
self.compute_elo(model1, model2, row["result"]) | |
self.matches["model1"].append(model1.name) | |
self.matches["model2"].append(model2.name) | |
self.matches["result"].append(result) | |
self.matches["timestamp"].append(row["timestamp"]) | |
data_dict = {"model1": [], "model2": [], "timestamp": [], "result": []} | |
df = pd.DataFrame(data_dict) | |
print(df.head()) | |
repo_temp.git_pull() | |
df.to_csv(os.path.join(TEMP_DIR, TEMP_FILENAME), index=False) | |
repo_temp.push_to_hub(commit_message="Reset results.csv") | |
def find_model(self, author, name): | |
""" Find a model in the models list. """ | |
for model in self.models: | |
if model.author == author and model.name == name: | |
return model | |
return None | |
def compute_elo(self, model1, model2, result): | |
""" Compute the new elo for each model based on a match result. """ | |
delta = model1.elo - model2.elo | |
win_probability = 1 / (1 + 10 ** (-delta / 500)) | |
model1.elo += self.k * (result - win_probability) | |
model2.elo -= self.k * (result - win_probability) | |
def find_n_closest_indexes(self, model, n) -> int: | |
""" | |
Get a model index with a fairly close rating. If no model is found, return the last model in the queue. | |
We don't always pick the closest rating to add variety to the matchups. | |
:param model: Model to compare | |
:param n: Number of close models from which to pick a candidate | |
:return: id of the chosen candidate | |
""" | |
if len(self.queue) == 1: | |
return 0 | |
indexes = [] | |
closest_diffs = [9999999] * n | |
for i, m in enumerate(self.queue): | |
if m.name == model.name: | |
continue | |
diff = abs(m.elo - model.elo) | |
if diff < max(closest_diffs): | |
closest_diffs.append(diff) | |
closest_diffs.sort() | |
closest_diffs.pop() | |
indexes.append(i) | |
random.shuffle(indexes) | |
return indexes[0] | |
def to_csv(self): | |
""" Save the match history as a CSV file to the hub. """ | |
data_dict = {"rank": [], "author": [], "model": [], "elo": [], "games_played": []} | |
sorted_models = sorted(self.models, key=lambda x: x.elo, reverse=True) | |
for i, model in enumerate(sorted_models): | |
data_dict["rank"].append(i + 1) | |
data_dict["author"].append(model.author) | |
data_dict["model"].append(model.name) | |
data_dict["elo"].append(model.elo) | |
data_dict["games_played"].append(model.games_played) | |
df = pd.DataFrame(data_dict) | |
print(df.head()) | |
repo.git_pull() | |
df.to_csv(os.path.join(ELO_DIR, ELO_FILENAME), index=False) | |
repo.push_to_hub(commit_message="Update ELO") | |
def match(model1, model2): | |
""" | |
Simulate a match between two models using the Unity environment. | |
:param model1: First Model object | |
:param model2: Second Model object | |
:return: match result (0: model1 lost, 0.5: draw, 1: model1 won) | |
""" | |
model1_id = model1.author + "/" + model1.name | |
model2_id = model2.author + "/" + model2.name | |
subprocess.run(["./SoccerTows.x86_64", "-model1", model1_id, "-model2", model2_id, "-nographics", "-batchmode"]) | |
print(f"Match {model1_id} against {model2_id} ended.") | |
model1.games_played += 1 | |
model2.games_played += 1 | |
def get_models_list() -> list: | |
""" | |
Get the list of models from the hub and the ELO file. | |
:return: list of Model objects | |
""" | |
models = [] | |
models_ids = [] | |
data = pd.read_csv(os.path.join(DATASET_REPO_URL, "resolve", "main", ELO_FILENAME)) | |
models_on_hub = api.list_models(filter=["reinforcement-learning", "ml-agents", "ML-Agents-SoccerTwos"]) | |
for i, row in data.iterrows(): | |
models.append(Model(row["author"], row["model"], row["elo"], row["games_played"])) | |
models_ids.append(row["author"] + "/" + row["model"]) | |
for model in models_on_hub: | |
author, name = model.modelId.split("/")[0], model.modelId.split("/")[1] | |
if model.modelId not in models_ids: | |
models.append(Model(author, name)) | |
print("New model found: ", author, "-", name) | |
return models | |
def get_elo_data() -> pd.DataFrame: | |
""" | |
Get the ELO data from the hub for all the models that have played at least one game. | |
:return: ELO data as a pandas DataFrame | |
""" | |
repo.git_pull() | |
data = pd.read_csv(os.path.join(DATASET_REPO_URL, "resolve", "main", ELO_FILENAME)) | |
return data | |
def init_matchmaking(): | |
""" | |
Run the matchmaking algorithm and save the results to the hub. | |
1. Get the list of models from the hub and the ELO data | |
2. Match models together based on their ELO rating | |
3. Simulate the matches using Unity to get the match result | |
4. Compute the new ELO rating for each model | |
5. Save the results to the hub | |
""" | |
models = get_models_list() | |
matchmaking = Matchmaking(models) | |
matchmaking.run() | |
matchmaking.to_csv() | |
print("Matchmaking done --", datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")) | |