Update app.py
Browse files
app.py
CHANGED
|
@@ -5,45 +5,50 @@ import random
|
|
| 5 |
import torch
|
| 6 |
import threading
|
| 7 |
import time
|
|
|
|
|
|
|
| 8 |
import gradio as gr
|
| 9 |
import pandas as pd
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
-
import
|
| 12 |
-
from huggingface_hub import hf_hub_download, HfApi
|
| 13 |
|
| 14 |
from riichienv import RiichiEnv, GameRule
|
| 15 |
from model3pLOCAL import load_model
|
| 16 |
|
| 17 |
# ==========================================
|
| 18 |
-
# 0. 云端持久化
|
| 19 |
# ==========================================
|
| 20 |
REPO_ID = "ffzeroHua/mj-eval-results" # ⚠️ 请务必替换为你在 HF 创建的 Dataset 名称
|
| 21 |
-
REPORT_FILE = "eval_report.txt"
|
| 22 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 23 |
-
api = HfApi()
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
EVAL_RUNNING = True
|
| 26 |
|
| 27 |
def sync_from_hub():
|
| 28 |
-
"""启动时从数据集下载
|
| 29 |
if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
|
| 30 |
try:
|
| 31 |
-
print(f"🔄 正在从 Hub
|
| 32 |
-
|
| 33 |
repo_id=REPO_ID,
|
| 34 |
-
filename=REPORT_FILE,
|
| 35 |
repo_type="dataset",
|
|
|
|
|
|
|
| 36 |
token=HF_TOKEN
|
| 37 |
)
|
| 38 |
-
|
| 39 |
-
print("✅ 下载完成,已恢复历史数据。")
|
| 40 |
except Exception as e:
|
| 41 |
-
print(f"⚠️
|
| 42 |
else:
|
| 43 |
-
print("⚠️ 未配置 HF_TOKEN 或 REPO_ID,跳过云端拉取。")
|
| 44 |
|
| 45 |
def sync_to_hub():
|
| 46 |
-
"""将
|
| 47 |
if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
|
| 48 |
try:
|
| 49 |
api.upload_file(
|
|
@@ -53,7 +58,7 @@ def sync_to_hub():
|
|
| 53 |
repo_type="dataset",
|
| 54 |
token=HF_TOKEN
|
| 55 |
)
|
| 56 |
-
print(f"☁️ 战绩已成功同步至 Hub: {time.strftime('%H:%M:%S')}")
|
| 57 |
except Exception as e:
|
| 58 |
print(f"❌ 同步失败: {e}")
|
| 59 |
|
|
@@ -134,11 +139,12 @@ def play_one_game(game_index):
|
|
| 134 |
# 3. 后台独立评估线程
|
| 135 |
# ==========================================
|
| 136 |
def background_eval_loop():
|
| 137 |
-
sync_from_hub() # 启动时拉取历史数据
|
| 138 |
|
| 139 |
NUM_WORKERS = 1 # Docker/云端免费 CPU 推荐设为 1
|
| 140 |
-
print("🚀 后台对战线程已启动...")
|
| 141 |
|
|
|
|
| 142 |
if not os.path.exists(REPORT_FILE):
|
| 143 |
open(REPORT_FILE, 'w').close()
|
| 144 |
|
|
@@ -161,42 +167,44 @@ def background_eval_loop():
|
|
| 161 |
f.flush()
|
| 162 |
games_completed += 1
|
| 163 |
games_since_last_sync += 1
|
| 164 |
-
print(f"完成 {games_completed} 局: 顺位 {rank}, 得点 {score}")
|
| 165 |
except Exception as e:
|
| 166 |
print(f"对局异常: {e}")
|
| 167 |
|
| 168 |
if EVAL_RUNNING:
|
| 169 |
futures.add(executor.submit(play_one_game, games_completed))
|
| 170 |
|
| 171 |
-
# 每完成 5 局,自动向云端备份一次
|
| 172 |
if games_since_last_sync >= 5:
|
| 173 |
sync_to_hub()
|
| 174 |
games_since_last_sync = 0
|
| 175 |
|
| 176 |
# ==========================================
|
| 177 |
-
# 4. 前端 Gradio 实时展示面板
|
| 178 |
# ==========================================
|
| 179 |
def read_and_analyze():
|
| 180 |
-
"""读取战绩文件,并生成统计文本和图表"""
|
| 181 |
-
|
|
|
|
| 182 |
return "⏳ 等待模型完成第一局对战...", None
|
| 183 |
|
|
|
|
| 184 |
try:
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
total = len(ranks)
|
| 196 |
if total == 0:
|
| 197 |
return "⏳ 正在进行第一局对战...", None
|
| 198 |
|
| 199 |
-
# 计算数据
|
| 200 |
avg_rank = sum(ranks) / total
|
| 201 |
avg_score = sum(scores) / total
|
| 202 |
rank1_rate = ranks.count(1) / total * 100
|
|
@@ -207,16 +215,17 @@ def read_and_analyze():
|
|
| 207 |
|
| 208 |
# 格式化 Markdown 文本
|
| 209 |
md_text = f"""
|
| 210 |
-
### 📊
|
| 211 |
-
- **总对局数:** {total} 局
|
| 212 |
-
- **平均顺位:** {avg_rank:.3f}
|
| 213 |
-
- **平均得点:** {avg_score:.0f}
|
| 214 |
---
|
| 215 |
- 🥇 **一位率:** {rank1_rate:.1f}%
|
| 216 |
- 🥈 **二位率:** {rank2_rate:.1f}%
|
| 217 |
- 🥉 **三位率:** {rank3_rate:.1f}%
|
| 218 |
---
|
| 219 |
-
-
|
|
|
|
| 220 |
"""
|
| 221 |
|
| 222 |
# 绘制图表
|
|
@@ -224,7 +233,7 @@ def read_and_analyze():
|
|
| 224 |
|
| 225 |
ax1 = fig.add_subplot(121)
|
| 226 |
ax1.bar(['1st', '2nd', '3rd'], [rank1_rate, rank2_rate, rank3_rate], color=['#FFD700', '#C0C0C0', '#CD7F32'])
|
| 227 |
-
ax1.set_title('Rank Distribution (%)')
|
| 228 |
ax1.set_ylim(0, max(100, max([rank1_rate, rank2_rate, rank3_rate] + [0]) + 10))
|
| 229 |
for i, v in enumerate([rank1_rate, rank2_rate, rank3_rate]):
|
| 230 |
ax1.text(i, v + 2, f"{v:.1f}%", ha='center')
|
|
@@ -234,7 +243,7 @@ def read_and_analyze():
|
|
| 234 |
df['ma'] = df['score'].rolling(window=min(10, max(1, len(df))), min_periods=1).mean()
|
| 235 |
ax2.plot(df['score'], alpha=0.3, color='gray', label='Raw Score')
|
| 236 |
ax2.plot(df['ma'], color='blue', linewidth=2, label='Moving Avg (10)')
|
| 237 |
-
ax2.set_title('Score Trend')
|
| 238 |
ax2.legend()
|
| 239 |
|
| 240 |
plt.tight_layout()
|
|
@@ -245,27 +254,23 @@ def read_and_analyze():
|
|
| 245 |
return f"❌ 数据解析出错: {e}", None
|
| 246 |
|
| 247 |
# ==========================================
|
| 248 |
-
# 5. 启动 Gradio 应用
|
| 249 |
-
# ==========================================
|
| 250 |
-
# ==========================================
|
| 251 |
-
# 5. 启动 Gradio 应用
|
| 252 |
# ==========================================
|
| 253 |
-
# 修复 1: 移除这里的 theme 参数
|
| 254 |
with gr.Blocks() as demo:
|
| 255 |
-
gr.Markdown("# 🀄 Mahjong AI
|
| 256 |
-
gr.Markdown("新模型正在后台随机座次与两名旧模型进行战斗。
|
| 257 |
|
| 258 |
with gr.Row():
|
| 259 |
with gr.Column(scale=1):
|
| 260 |
stats_output = gr.Markdown("🚀 正在启动后台环境...")
|
| 261 |
-
refresh_btn = gr.Button("🔄 手动刷新战绩")
|
| 262 |
with gr.Column(scale=2):
|
| 263 |
plot_output = gr.Plot()
|
| 264 |
|
| 265 |
-
#
|
| 266 |
demo.load(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
|
| 267 |
|
| 268 |
-
#
|
| 269 |
timer = gr.Timer(15)
|
| 270 |
timer.tick(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
|
| 271 |
|
|
@@ -276,6 +281,5 @@ if __name__ == "__main__":
|
|
| 276 |
t = threading.Thread(target=background_eval_loop, daemon=True)
|
| 277 |
t.start()
|
| 278 |
|
| 279 |
-
# 2. 启动前端网页
|
| 280 |
-
# 修复 4: 将 theme 参数转移到 launch() 里面
|
| 281 |
demo.queue().launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())
|
|
|
|
| 5 |
import torch
|
| 6 |
import threading
|
| 7 |
import time
|
| 8 |
+
import uuid
|
| 9 |
+
import glob
|
| 10 |
import gradio as gr
|
| 11 |
import pandas as pd
|
| 12 |
import matplotlib.pyplot as plt
|
| 13 |
+
from huggingface_hub import snapshot_download, HfApi
|
|
|
|
| 14 |
|
| 15 |
from riichienv import RiichiEnv, GameRule
|
| 16 |
from model3pLOCAL import load_model
|
| 17 |
|
| 18 |
# ==========================================
|
| 19 |
+
# 0. 分布式多开与云端持久化配置
|
| 20 |
# ==========================================
|
| 21 |
REPO_ID = "ffzeroHua/mj-eval-results" # ⚠️ 请务必替换为你在 HF 创建的 Dataset 名称
|
|
|
|
| 22 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
|
|
|
| 23 |
|
| 24 |
+
# 为当前 Space 节点生成唯一的 ID (如果在本地跑,也会生成唯一 ID)
|
| 25 |
+
WORKER_ID = os.getenv("WORKER_ID", str(uuid.uuid4())[:6])
|
| 26 |
+
REPORT_FILE_PREFIX = 'eval_report'
|
| 27 |
+
REPORT_FILE = f"{REPORT_FILE_PREFIX}_{WORKER_ID}.txt"
|
| 28 |
+
|
| 29 |
+
api = HfApi()
|
| 30 |
EVAL_RUNNING = True
|
| 31 |
|
| 32 |
def sync_from_hub():
|
| 33 |
+
"""启动时从数据集下载所有节点的战绩分片文件"""
|
| 34 |
if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
|
| 35 |
try:
|
| 36 |
+
print(f"🔄 正在从 Hub 拉取全局历史数据...")
|
| 37 |
+
snapshot_download(
|
| 38 |
repo_id=REPO_ID,
|
|
|
|
| 39 |
repo_type="dataset",
|
| 40 |
+
local_dir=".",
|
| 41 |
+
allow_patterns=REPORT_FILE_PREFIX + "_*.txt", # 只下载战绩文件
|
| 42 |
token=HF_TOKEN
|
| 43 |
)
|
| 44 |
+
print("✅ 历史数据拉取完成。")
|
|
|
|
| 45 |
except Exception as e:
|
| 46 |
+
print(f"⚠️ 拉取历史战绩失败 (可能是首次运行或尚无数据): {e}")
|
| 47 |
else:
|
| 48 |
+
print("⚠️ 未配置 HF_TOKEN 或 REPO_ID 未修改,跳过云端拉取。")
|
| 49 |
|
| 50 |
def sync_to_hub():
|
| 51 |
+
"""将当前节点的战绩文件备份到数据集"""
|
| 52 |
if HF_TOKEN and REPO_ID != "你的用户名/mj-eval-results":
|
| 53 |
try:
|
| 54 |
api.upload_file(
|
|
|
|
| 58 |
repo_type="dataset",
|
| 59 |
token=HF_TOKEN
|
| 60 |
)
|
| 61 |
+
print(f"☁️ 节点 {WORKER_ID} 战绩已成功同步至 Hub: {time.strftime('%H:%M:%S')}")
|
| 62 |
except Exception as e:
|
| 63 |
print(f"❌ 同步失败: {e}")
|
| 64 |
|
|
|
|
| 139 |
# 3. 后台独立评估线程
|
| 140 |
# ==========================================
|
| 141 |
def background_eval_loop():
|
| 142 |
+
sync_from_hub() # 启动时拉取全局历史数据
|
| 143 |
|
| 144 |
NUM_WORKERS = 1 # Docker/云端免费 CPU 推荐设为 1
|
| 145 |
+
print(f"🚀 节点 [{WORKER_ID}] 后台对战线程已启动...")
|
| 146 |
|
| 147 |
+
# 确保当前节点的文件存在
|
| 148 |
if not os.path.exists(REPORT_FILE):
|
| 149 |
open(REPORT_FILE, 'w').close()
|
| 150 |
|
|
|
|
| 167 |
f.flush()
|
| 168 |
games_completed += 1
|
| 169 |
games_since_last_sync += 1
|
| 170 |
+
print(f"[节点 {WORKER_ID}] 完成 {games_completed} 局: 顺位 {rank}, 得点 {score}")
|
| 171 |
except Exception as e:
|
| 172 |
print(f"对局异常: {e}")
|
| 173 |
|
| 174 |
if EVAL_RUNNING:
|
| 175 |
futures.add(executor.submit(play_one_game, games_completed))
|
| 176 |
|
| 177 |
+
# 每完成 5 局,自动向云端备份一次当前节点的文件
|
| 178 |
if games_since_last_sync >= 5:
|
| 179 |
sync_to_hub()
|
| 180 |
games_since_last_sync = 0
|
| 181 |
|
| 182 |
# ==========================================
|
| 183 |
+
# 4. 前端 Gradio 实时展示面板 (全局汇总)
|
| 184 |
# ==========================================
|
| 185 |
def read_and_analyze():
|
| 186 |
+
"""读取所有节点的战绩文件,并生成全局汇总统计文本和图表"""
|
| 187 |
+
all_files = glob.glob(f"{REPORT_FILE_PREFIX}_*.txt")
|
| 188 |
+
if not all_files:
|
| 189 |
return "⏳ 等待模型完成第一局对战...", None
|
| 190 |
|
| 191 |
+
ranks, scores = [], []
|
| 192 |
try:
|
| 193 |
+
# 遍历汇总所有节点数据
|
| 194 |
+
for file in all_files:
|
| 195 |
+
with open(file, "r") as f:
|
| 196 |
+
lines = f.readlines()
|
| 197 |
+
for line in lines:
|
| 198 |
+
parts = line.strip().split()
|
| 199 |
+
if len(parts) == 2:
|
| 200 |
+
ranks.append(int(float(parts[0])))
|
| 201 |
+
scores.append(float(parts[1]))
|
| 202 |
+
|
| 203 |
total = len(ranks)
|
| 204 |
if total == 0:
|
| 205 |
return "⏳ 正在进行第一局对战...", None
|
| 206 |
|
| 207 |
+
# 计算全局数据
|
| 208 |
avg_rank = sum(ranks) / total
|
| 209 |
avg_score = sum(scores) / total
|
| 210 |
rank1_rate = ranks.count(1) / total * 100
|
|
|
|
| 215 |
|
| 216 |
# 格式化 Markdown 文本
|
| 217 |
md_text = f"""
|
| 218 |
+
### 📊 分布式集群全局对抗统计 (新模型 1v2 旧模型)
|
| 219 |
+
- **总汇集对局数:** {total} 局 (来自 {len(all_files)} 个活跃/历史节点)
|
| 220 |
+
- **全局平均顺位:** {avg_rank:.3f}
|
| 221 |
+
- **全局平均得点:** {avg_score:.0f}
|
| 222 |
---
|
| 223 |
- 🥇 **一位率:** {rank1_rate:.1f}%
|
| 224 |
- 🥈 **二位率:** {rank2_rate:.1f}%
|
| 225 |
- 🥉 **三位率:** {rank3_rate:.1f}%
|
| 226 |
---
|
| 227 |
+
- 🌐 **当前节点 ID:** `{WORKER_ID}`
|
| 228 |
+
- 🕒 **页面刷新时间:** {last_update}
|
| 229 |
"""
|
| 230 |
|
| 231 |
# 绘制图表
|
|
|
|
| 233 |
|
| 234 |
ax1 = fig.add_subplot(121)
|
| 235 |
ax1.bar(['1st', '2nd', '3rd'], [rank1_rate, rank2_rate, rank3_rate], color=['#FFD700', '#C0C0C0', '#CD7F32'])
|
| 236 |
+
ax1.set_title('Global Rank Distribution (%)')
|
| 237 |
ax1.set_ylim(0, max(100, max([rank1_rate, rank2_rate, rank3_rate] + [0]) + 10))
|
| 238 |
for i, v in enumerate([rank1_rate, rank2_rate, rank3_rate]):
|
| 239 |
ax1.text(i, v + 2, f"{v:.1f}%", ha='center')
|
|
|
|
| 243 |
df['ma'] = df['score'].rolling(window=min(10, max(1, len(df))), min_periods=1).mean()
|
| 244 |
ax2.plot(df['score'], alpha=0.3, color='gray', label='Raw Score')
|
| 245 |
ax2.plot(df['ma'], color='blue', linewidth=2, label='Moving Avg (10)')
|
| 246 |
+
ax2.set_title('Global Score Trend')
|
| 247 |
ax2.legend()
|
| 248 |
|
| 249 |
plt.tight_layout()
|
|
|
|
| 254 |
return f"❌ 数据解析出错: {e}", None
|
| 255 |
|
| 256 |
# ==========================================
|
| 257 |
+
# 5. 启动 Gradio 应用 (兼容 Gradio 6.0+)
|
|
|
|
|
|
|
|
|
|
| 258 |
# ==========================================
|
|
|
|
| 259 |
with gr.Blocks() as demo:
|
| 260 |
+
gr.Markdown("# 🀄 Mahjong AI 分布式强度评估舱")
|
| 261 |
+
gr.Markdown("新模型正在后台随机座次与两名旧模型进行战斗。战绩会自动通过数据分片同步至 Hugging Face Dataset,页面展示为集群全局汇总数据。")
|
| 262 |
|
| 263 |
with gr.Row():
|
| 264 |
with gr.Column(scale=1):
|
| 265 |
stats_output = gr.Markdown("🚀 正在启动后台环境...")
|
| 266 |
+
refresh_btn = gr.Button("🔄 手动刷新全局战绩")
|
| 267 |
with gr.Column(scale=2):
|
| 268 |
plot_output = gr.Plot()
|
| 269 |
|
| 270 |
+
# 首次加载渲染
|
| 271 |
demo.load(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
|
| 272 |
|
| 273 |
+
# 兼容新版 Gradio 的定时器 (每 15 秒刷新一次前端)
|
| 274 |
timer = gr.Timer(15)
|
| 275 |
timer.tick(fn=read_and_analyze, inputs=None, outputs=[stats_output, plot_output])
|
| 276 |
|
|
|
|
| 281 |
t = threading.Thread(target=background_eval_loop, daemon=True)
|
| 282 |
t.start()
|
| 283 |
|
| 284 |
+
# 2. 启动前端网页 (主题参数移至此处)
|
|
|
|
| 285 |
demo.queue().launch(server_name="0.0.0.0", server_port=7860, theme=gr.themes.Soft())
|