Files
2026-04-25 02:32:22 +09:00

152 lines
6.0 KiB
Python

import pandas as pd
import json, os, sys
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.preprocessing import StandardScaler
MAX_MARGIN_LIMIT = 10
class DifficultyNet(nn.Module):
def __init__(self, input_dim):
super(DifficultyNet, self).__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, 128),
nn.BatchNorm1d(128),
nn.ReLU(),
nn.Dropout(0.4),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 1),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x)
def train(script_dir, working_dir, data_dir, train_count, val_count, margin, val_iterations):
device = torch.device("mps" if torch.backends.mps.is_available() else "cpu")
print(f"🚀 Using Device: {device}")
# 데이터 로드 및 병합
factors_path = os.path.join(working_dir, 'factors.json')
if not os.path.exists(factors_path):
os.system(f"bun run {os.path.join(script_dir, 'factorize.ts')} {data_dir} {working_dir}")
df_feat = pd.read_json(factors_path)
df_meas = pd.read_csv(os.path.join(data_dir, 'measure.csv'))
df_meas['diff'] = df_meas['diff'].replace('ura', 'edit')
df_feat['songno'], df_meas['songno'] = df_feat['songno'].astype(int), df_meas['songno'].astype(int)
df = pd.merge(df_feat, df_meas, on=['songno', 'diff'])
exclude_cols = ['songno', 'diff', 'title', 'course', '상수', 'predicted_measure', 'error']
X_cols = [c for c in df_feat.columns if c not in exclude_cols]
scaler = StandardScaler()
df[X_cols] = scaler.fit_transform(df[X_cols])
with open(os.path.join(working_dir, 'scaler.json'), 'w') as f:
json.dump({'mean': scaler.mean_.tolist(), 'std': scaler.scale_.tolist(), 'cols': X_cols}, f)
model_path = os.path.join(working_dir, 'model.pth')
margin = float(margin)
val_iterations = int(val_iterations)
model = DifficultyNet(len(X_cols)).to(device)
attempt = 1
while True:
print(f"\n[Attempt {attempt}] " + "-"*40)
if os.path.exists(model_path):
try: model.load_state_dict(torch.load(model_path, map_location=device))
except: pass
train_df = df.sample(n=min(int(train_count), len(df)))
X_train = torch.FloatTensor(train_df[X_cols].values).to(device)
y_train = torch.FloatTensor(((train_df['상수'].values - 1) / 11)).unsqueeze(1).to(device)
base_lr = 0.001
optimizer = optim.Adam(model.parameters(), lr=base_lr, weight_decay=1e-5)
criterion = nn.L1Loss()
model.train()
epoch = 0
while True:
optimizer.zero_grad()
preds_train = model(X_train)
loss = criterion(preds_train, y_train) # MSELoss 권장
loss.backward()
optimizer.step()
# 현재 학습 데이터에 대한 오차 분석
with torch.no_grad():
diff = torch.abs((preds_train * 12 + 0.5) - (y_train * 12 + 0.5))
train_mae = torch.mean(diff).item()
train_max = torch.max(diff).item()
if epoch % 1000 == 0:
print(f" - Ep {epoch:5d} | MAE: {train_mae:.4f} | MAX: {train_max:.4f}")
# ✅ 조건 강화: MAE뿐만 아니라 MAX도 어느 정도 잡혔을 때만 검증으로 이동
if train_mae < margin and train_max < (margin * MAX_MARGIN_LIMIT):
print(f" ✅ Train Goal Reached (MAE: {train_mae} < {margin:.4f}, MAX: {train_max:.4f} < {(margin * MAX_MARGIN_LIMIT):.4f}). Moving to Val.")
break
epoch += 1
if epoch > 50000:
print(" ⚠️ Timed out. Resampling...")
break
if train_mae >= margin:
torch.save(model.state_dict(), model_path)
attempt += 1
continue
# 3. 검증 단계
print(f"3. Validating {val_iterations} iterations...")
model.eval()
all_passed = True
with torch.no_grad():
for i in range(1, val_iterations + 1):
val_df = df.sample(n=min(int(val_count), len(df)))
X_val = torch.FloatTensor(val_df[X_cols].values).to(device)
y_val_raw = val_df['상수'].values
preds = model(X_val) * 12 + 0.5
y_val_tensor = torch.FloatTensor(y_val_raw).unsqueeze(1).to(device)
diff_tensor = torch.abs(preds - y_val_tensor)
mae = torch.mean(diff_tensor).item()
max_error = torch.max(diff_tensor).item()
# csv 저장
# preds를 CPU 넘파이로 변환하고 1차원으로 펴주는 과정이 포함되어야 합니다.
preds_np = preds.detach().cpu().numpy().flatten() if torch.is_tensor(preds) else preds
res_df = val_df[['songno', 'diff', '상수']].copy()
res_df['predicted_measure'] = preds_np
res_df['error'] = np.abs(preds_np - y_val_raw)
output_file = os.path.join(working_dir, f'validate_result_{i}.csv')
res_df.to_csv(output_file, index=False)
if mae <= margin and max_error <= (margin * MAX_MARGIN_LIMIT):
print(f" [Iter {i}] ✅ PASS (MAE: {mae:.4f}, MAX: {max_error:.4f})")
else:
print(f" [Iter {i}] ❌ FAIL (MAE: {mae:.4f} > {margin} || MAX: {max_error:.4f} > {margin * MAX_MARGIN_LIMIT})")
all_passed = False
break
if all_passed:
print(f"\n✨ Final Success!")
torch.save(model.state_dict(), model_path)
return
else:
torch.save(model.state_dict(), model_path)
attempt += 1
if __name__ == "__main__":
if len(sys.argv) < 8: sys.exit(1)
train(*sys.argv[1:8])