docs
This commit is contained in:
BIN
abs_error_analysis.png
Normal file
BIN
abs_error_analysis.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
41
docs/3-2.lightgbm.md
Normal file
41
docs/3-2.lightgbm.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# LightGBM 기반 난이도 상수 예측 모델
|
||||
|
||||
이 문서는 프로젝트의 또 다른 학습 엔진인 LightGBM 모델의 구조, 하이퍼파라미터 및 특징을 설명합니다.
|
||||
|
||||
## 1. 모델 개요
|
||||
**LightGBM (Light Gradient Boosting Machine)**은 트리 기반 학습 알고리즘으로, XGBoost와 유사하지만 'Leaf-wise' 트리 성장 방식을 사용하여 더 빠르고 메모리 효율적인 학습이 가능합니다. 특히 대규모 데이터셋에서 높은 성능을 발휘하며, 본 프로젝트에서는 XGBoost와의 비교 및 앙상블 가능성을 열어두기 위해 도입되었습니다.
|
||||
|
||||
## 2. 하이퍼파라미터 설정
|
||||
`train/train_lightgbm.py`에 정의된 주요 설정값은 다음과 같습니다. XGBoost보다 더 깊고 복잡한 트리를 형성하도록 설정되어 있습니다.
|
||||
|
||||
| 파라미터 | 설정값 | 설명 |
|
||||
| :--- | :--- | :--- |
|
||||
| `learning_rate` | 0.02 | 학습률. XGBoost(0.05)보다 낮게 설정하여 더 정밀하게 수렴 |
|
||||
| `num_leaves` | 63 | 하나의 트리가 가질 수 있는 최대 잎(Leaf) 수. 복잡한 패턴 학습에 유리 |
|
||||
| `n_estimators` | 3000 | 결정 트리의 개수. 충분한 학습을 위해 크게 설정 |
|
||||
| `feature_fraction`| 0.9 | 각 트리 학습 시 사용할 피처 비율 (과적합 방지) |
|
||||
| `bagging_fraction`| 0.8 | 데이터 샘플링 비율 |
|
||||
| `bagging_freq` | 5 | 배깅 수행 빈도 |
|
||||
| `early_stopping` | 100 | 검증 오차가 개선되지 않을 경우 학습을 조기 종료하는 라운드 수 |
|
||||
|
||||
## 3. LightGBM 모델의 특징
|
||||
|
||||
### A. Leaf-wise 성장 방식
|
||||
- 대부분의 boosting 알고리즘이 Level-wise(수평 성장) 방식을 사용하는 것과 달리, LightGBM은 **Leaf-wise(수직 성장)** 방식을 사용합니다.
|
||||
- 이 방식은 손실(Loss)을 가장 많이 줄일 수 있는 잎 노드를 계속 분할하므로, 동일한 분할 횟수에서 Level-wise보다 더 낮은 손실을 달성할 수 있습니다.
|
||||
|
||||
### B. 실행 속도 및 효율성
|
||||
- XGBoost 대비 학습 속도가 매우 빠르며 메모리 사용량이 적습니다.
|
||||
- 많은 양의 데이터를 처리할 때 이점이 큽니다.
|
||||
|
||||
## 4. 학습 및 검증 프로세스
|
||||
XGBoost와 동일한 파이프라인을 따르며, 결과물은 다음과 같이 구분되어 저장됩니다.
|
||||
|
||||
- **모델 파일**: `model_lgbm.pkl`
|
||||
- **스케일러**: `scaler_lgbm.pkl`
|
||||
- **피처 목록**: `features_lgbm.txt`
|
||||
- **검증 결과**: `validate.json`, `validate.png`
|
||||
|
||||
## 5. 모델 평가
|
||||
- LightGBM은 하이퍼파라미터 변화에 민감하므로 과적합(Overfitting) 여부를 `validate.png`를 통해 상시 모니터링해야 합니다.
|
||||
- 현재 설정은 높은 `n_estimators`와 낮은 `learning_rate`를 통해 아주 미세한 채보의 차이까지 학습하는 것을 목표로 하고 있습니다.
|
||||
67
docs/4. train script.md
Normal file
67
docs/4. train script.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 4. 학습 스크립트 (Training Script) 가이드
|
||||
|
||||
이 문서는 `train/` 폴더 내의 학습 스크립트(`train_xgboost.py`, `train_lightgbm.py`)의 구조와 실행 과정을 설명합니다.
|
||||
|
||||
## 1. 실행 구조 및 파라미터
|
||||
|
||||
학습 스크립트는 명령행 인자(CLI Arguments)를 통해 제어됩니다.
|
||||
|
||||
### 실행 예시
|
||||
```bash
|
||||
python3 train/train_xgboost.py \
|
||||
--workingDir ./output/xgboost \
|
||||
--dataDir ./datas \
|
||||
--trainSize 1000 \
|
||||
--validSize 200
|
||||
```
|
||||
|
||||
### 파라미터 상세
|
||||
- `--workingDir`: 학습 결과물(`model.pkl`, `validate.json` 등)이 저장될 경로입니다.
|
||||
- `--dataDir`: 정답 데이터(`measure.csv`)가 위치한 경로입니다.
|
||||
- `--trainSize`: 전체 데이터셋 중 학습에 사용할 샘플 수입니다.
|
||||
- `--validSize`: 학습에 참여하지 않고 모델 평가(검증)에만 사용할 샘플 수입니다.
|
||||
|
||||
## 2. 데이터 처리 및 학습 파이프라인
|
||||
|
||||
스크립트 내부의 `train_model` 함수는 다음 순서로 동작합니다.
|
||||
|
||||
### A. 데이터 로드 및 매칭
|
||||
1. `workingDir`에서 `features.json`(전처리된 피처)을 읽어옵니다.
|
||||
2. `dataDir`에서 `measure.csv`(정답 난이도)를 읽어옵니다.
|
||||
3. `(songno, difficulty)`를 키로 사용하여 두 데이터를 매칭하고 하나의 데이터셋으로 결합합니다.
|
||||
|
||||
### B. 데이터셋 분리 (Train/Valid Split)
|
||||
1. 결합된 데이터셋을 `RANDOM_STATE(42)`를 기반으로 무작위로 섞습니다(Shuffle).
|
||||
2. 앞에서부터 `TRAIN_SIZE` 만큼을 학습 데이터로, 그 뒤의 `VALID_SIZE` 만큼을 검증 데이터로 엄격히 분리하여 모델의 일반화 성능을 보장합니다.
|
||||
|
||||
### C. 정규화 (Scaling)
|
||||
1. `StandardScaler`를 사용하여 피처의 평균을 0, 분산을 1로 조정합니다.
|
||||
2. `CONTINUE_TRAINING` 옵션이 켜져 있고 기존 `scaler.pkl`이 있다면 이를 로드하여 일관성을 유지합니다.
|
||||
|
||||
### D. 모델 학습 및 지속 학습 (Warm Start)
|
||||
1. 모델 객체를 생성하거나, 기존 `model.pkl`이 있다면 이를 로드합니다.
|
||||
2. 기존 모델이 있는 경우 이전 학습 상태를 유지한 채 새로운 데이터로 가중치를 미세 조정(Fine-tuning)합니다.
|
||||
|
||||
## 3. 검증 및 결과 시각화
|
||||
|
||||
학습 완료 직후, 모델이 학습 중에 보지 못한 검증 데이터셋(`X_valid`)을 사용하여 성능을 평가합니다.
|
||||
|
||||
1. **지표 계산**: MAE(Mean Absolute Error)와 오차 범위 ±0.1 이내의 정확도를 산출합니다.
|
||||
2. **validate.json 저장**: 검증된 각 곡의 실제값, 예측값, 오차를 에러 절댓값 내림차순으로 저장합니다.
|
||||
3. **validate.png 생성**: 에러의 분포를 한눈에 볼 수 있도록 산점도 그래프를 생성합니다. (X축: 에러 크기 순, Y축: 에러 절댓값)
|
||||
|
||||
## 4. 주요 산출물 (Outputs)
|
||||
|
||||
학습이 성공하면 `workingDir` 폴더에 다음 파일들이 생성/업데이트됩니다.
|
||||
|
||||
| 파일명 | 내용 |
|
||||
| :--- | :--- |
|
||||
| `model.pkl` | 학습된 모델 바이너리 |
|
||||
| `scaler.pkl` | 피처 정규화를 위한 Scaler 객체 |
|
||||
| `features.txt` | 학습에 사용된 피처 이름 목록 및 순서 |
|
||||
| `validate.json` | 검증 데이터셋에 대한 상세 예측 결과 (JSON) |
|
||||
| `validate.png` | 검증 에러 분포 시각화 그래프 (PNG) |
|
||||
|
||||
## 5. 주의사항
|
||||
- `features.json`이 먼저 생성되어 있어야 학습이 가능합니다 (`preprocess.ts` 선행 필요).
|
||||
- `TRAIN_SIZE + VALID_SIZE`가 전체 가용 데이터 수보다 크면 오류가 발생합니다.
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
bpm_avg
|
||||
bpm_change
|
||||
color_complexity
|
||||
density_avg
|
||||
density_peak
|
||||
note_count
|
||||
rhythm_complexity
|
||||
scroll_change
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
bpm_avg
|
||||
bpm_change
|
||||
color_complexity
|
||||
density_avg
|
||||
density_peak
|
||||
note_count
|
||||
rhythm_complexity
|
||||
scroll_change
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,74 +0,0 @@
|
||||
[
|
||||
{
|
||||
"songno": "XODUS",
|
||||
"difficulty": "oni",
|
||||
"note_count": 1007,
|
||||
"density_avg": 8.14959935897436,
|
||||
"density_peak": 17,
|
||||
"bpm_avg": 202,
|
||||
"bpm_change": 0,
|
||||
"scroll_change": 14,
|
||||
"rhythm_complexity": 868,
|
||||
"color_complexity": 0.012565274045963932
|
||||
},
|
||||
{
|
||||
"songno": "Destructive Little Sister",
|
||||
"difficulty": "oni",
|
||||
"note_count": 1119,
|
||||
"density_avg": 7.917452830188679,
|
||||
"density_peak": 21,
|
||||
"bpm_avg": 247.5,
|
||||
"bpm_change": 0,
|
||||
"scroll_change": 17,
|
||||
"rhythm_complexity": 1079,
|
||||
"color_complexity": 0.016291673920775917
|
||||
},
|
||||
{
|
||||
"songno": "7 Wonders",
|
||||
"difficulty": "oni",
|
||||
"note_count": 794,
|
||||
"density_avg": 5.571929824561403,
|
||||
"density_peak": 12,
|
||||
"bpm_avg": 168,
|
||||
"bpm_change": 0,
|
||||
"scroll_change": 0,
|
||||
"rhythm_complexity": 726,
|
||||
"color_complexity": 0.006656416024691356
|
||||
},
|
||||
{
|
||||
"songno": "7 Wonders",
|
||||
"difficulty": "ura",
|
||||
"note_count": 1207,
|
||||
"density_avg": 8.47017543859649,
|
||||
"density_peak": 15,
|
||||
"bpm_avg": 168,
|
||||
"bpm_change": 0,
|
||||
"scroll_change": 0,
|
||||
"rhythm_complexity": 965,
|
||||
"color_complexity": 0.01463965596985236
|
||||
},
|
||||
{
|
||||
"songno": "Destruction 3 2 1",
|
||||
"difficulty": "oni",
|
||||
"note_count": 1160,
|
||||
"density_avg": 7.46659375,
|
||||
"density_peak": 15,
|
||||
"bpm_avg": 321.3209999999973,
|
||||
"bpm_change": 0,
|
||||
"scroll_change": 5,
|
||||
"rhythm_complexity": 1101,
|
||||
"color_complexity": 0.012682773678672012
|
||||
},
|
||||
{
|
||||
"songno": "Destruction 3 2 1",
|
||||
"difficulty": "ura",
|
||||
"note_count": 1491,
|
||||
"density_avg": 9.59714765625,
|
||||
"density_peak": 20,
|
||||
"bpm_avg": 321.32099999999707,
|
||||
"bpm_change": 0,
|
||||
"scroll_change": 8,
|
||||
"rhythm_complexity": 1417,
|
||||
"color_complexity": 0.028926671611070473
|
||||
}
|
||||
]
|
||||
@@ -15,7 +15,7 @@ const { values } = parseArgs({
|
||||
dataDir: {
|
||||
type: "string"
|
||||
},
|
||||
trainScript: {
|
||||
script: {
|
||||
type: "string"
|
||||
},
|
||||
trainSize: {
|
||||
@@ -28,13 +28,13 @@ const { values } = parseArgs({
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (!values.dataDir || !values.workingDir || !values.trainScript) {
|
||||
if (!values.dataDir || !values.workingDir || !values.script) {
|
||||
console.error("--workingDir --dataDir --trainDir");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
generateFeatures();
|
||||
const child = spawn("python3", [values.trainScript,
|
||||
const child = spawn("python3", [values.script,
|
||||
"--workingDir", values.workingDir,
|
||||
"--dataDir", values.dataDir,
|
||||
"--trainSize", (Number(values.trainSize) || 500).toString(),
|
||||
50
script/visualize_abs_errors.py
Normal file
50
script/visualize_abs_errors.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import os
|
||||
import json
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
import seaborn as sns
|
||||
|
||||
output_dir = 'output'
|
||||
model_dirs = [d for d in os.listdir(output_dir) if os.path.isdir(os.path.join(output_dir, d)) and os.path.exists(os.path.join(output_dir, d, 'compare.json'))]
|
||||
model_dirs.sort()
|
||||
|
||||
if not model_dirs:
|
||||
print("데이터를 찾을 수 없습니다.")
|
||||
exit()
|
||||
|
||||
fig, axes = plt.subplots(len(model_dirs), 1, figsize=(15, 6 * len(model_dirs)), sharex=False)
|
||||
if len(model_dirs) == 1:
|
||||
axes = [axes]
|
||||
|
||||
for i, model in enumerate(model_dirs):
|
||||
json_path = os.path.join(output_dir, model, 'compare.json')
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
df = pd.DataFrame(data.get('details', []))
|
||||
|
||||
# 에러 절댓값 계산 및 내림차순 정렬
|
||||
df['abs_error'] = df['error'].abs()
|
||||
df = df.sort_values('abs_error', ascending=False).reset_index(drop=True)
|
||||
|
||||
ax = axes[i]
|
||||
# y축에 abs_error를 사용하여 양수 영역만 표시
|
||||
sns.scatterplot(data=df, x=df.index, y='abs_error', ax=ax, alpha=0.6, s=20, color='darkorange')
|
||||
|
||||
ax.set_title(f'Model: {model} (Sorted by Absolute Error)', fontsize=15, fontweight='bold')
|
||||
ax.set_ylabel('Absolute Error (|Actual - Predicted|)', fontsize=12)
|
||||
ax.set_xlabel(f'Songs (Ordered by Error Magnitude)', fontsize=12)
|
||||
|
||||
# 가이드 라인 (오차 0.2, 0.5, 1.0 단위)
|
||||
ax.axhline(0.2, color='green', linestyle='--', linewidth=0.8, alpha=0.5, label='Target (0.2)')
|
||||
ax.axhline(0.5, color='blue', linestyle='--', linewidth=0.8, alpha=0.5)
|
||||
ax.axhline(1.0, color='red', linestyle='--', linewidth=0.8, alpha=0.5)
|
||||
|
||||
# y축을 0부터 시작하도록 설정
|
||||
ax.set_ylim(0, 3.5)
|
||||
ax.set_xticks([]) # x축 라벨 제거
|
||||
ax.grid(True, axis='y', alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
output_image = 'abs_error_analysis.png'
|
||||
plt.savefig(output_image)
|
||||
print(f"절댓값 에러 정렬 그래프가 {output_image}에 저장되었습니다.")
|
||||
@@ -7,6 +7,9 @@ import random
|
||||
import joblib
|
||||
import numpy as np
|
||||
import lightgbm as lgb
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.metrics import mean_absolute_error
|
||||
|
||||
@@ -82,7 +85,7 @@ def train_model(working_dir: str, data_dir: str):
|
||||
key = (songno, diff)
|
||||
if key in feature_map:
|
||||
features = [safe_float(feature_map[key].get(k, 0)) for k in feature_names]
|
||||
dataset.append((features, measure))
|
||||
dataset.append((features, measure, songno, diff))
|
||||
|
||||
random.shuffle(dataset)
|
||||
if len(dataset) < (TRAIN_SIZE + VALID_SIZE):
|
||||
@@ -91,10 +94,11 @@ def train_model(working_dir: str, data_dir: str):
|
||||
train_dataset = dataset[:TRAIN_SIZE]
|
||||
valid_dataset = dataset[TRAIN_SIZE:TRAIN_SIZE + VALID_SIZE]
|
||||
|
||||
X_train = np.array([x for x, _ in train_dataset], dtype=np.float32)
|
||||
y_train = np.array([y for _, y in train_dataset], dtype=np.float32)
|
||||
X_valid = np.array([x for x, _ in valid_dataset], dtype=np.float32)
|
||||
y_valid = np.array([y for _, y in valid_dataset], dtype=np.float32)
|
||||
X_train = np.array([x for x, _, _, _ in train_dataset], dtype=np.float32)
|
||||
y_train = np.array([y for _, y, _, _ in train_dataset], dtype=np.float32)
|
||||
X_valid = np.array([x for x, _, _, _ in valid_dataset], dtype=np.float32)
|
||||
y_valid = np.array([y for _, y, _, _ in valid_dataset], dtype=np.float32)
|
||||
valid_info = [(s, d) for _, _, s, d in valid_dataset]
|
||||
|
||||
print(f"Train Size: {len(X_train)} | Valid Size: {len(X_valid)} | Features: {len(feature_names)}")
|
||||
|
||||
@@ -133,6 +137,66 @@ def train_model(working_dir: str, data_dir: str):
|
||||
|
||||
print(f"\nMAE: {mae:.4f} | Accuracy (±{ERROR_TOLERANCE}): {accuracy:.4f}")
|
||||
|
||||
# =====================================================
|
||||
# save validate.json
|
||||
# =====================================================
|
||||
validate_details = []
|
||||
for i in range(len(y_valid)):
|
||||
actual = float(y_valid[i])
|
||||
predicted = float(pred[i])
|
||||
songno, diff = valid_info[i]
|
||||
validate_details.append({
|
||||
"songno": songno,
|
||||
"diff": diff,
|
||||
"actual": actual,
|
||||
"predicted": predicted,
|
||||
"error": actual - predicted
|
||||
})
|
||||
|
||||
validate_details.sort(key=lambda x: abs(x["error"]), reverse=True)
|
||||
validate_result = {
|
||||
"summary": {
|
||||
"total_compared": len(y_valid),
|
||||
"average_absolute_error": float(mae),
|
||||
"accuracy": float(accuracy),
|
||||
"timestamp": "now",
|
||||
"script_used": "train/train_lightgbm.py"
|
||||
},
|
||||
"details": validate_details
|
||||
}
|
||||
|
||||
validate_path = os.path.join(working_dir, "validate.json")
|
||||
with open(validate_path, "w", encoding="utf-8") as f:
|
||||
json.dump(validate_result, f, indent=2, ensure_ascii=False)
|
||||
print(f"Validation result saved: {validate_path}")
|
||||
|
||||
# =====================================================
|
||||
# save validate.png
|
||||
# =====================================================
|
||||
try:
|
||||
plt.switch_backend('Agg')
|
||||
df_plot = pd.DataFrame(validate_details)
|
||||
df_plot['abs_error'] = df_plot['error'].abs()
|
||||
df_plot = df_plot.sort_values('abs_error', ascending=False).reset_index(drop=True)
|
||||
|
||||
plt.figure(figsize=(12, 6))
|
||||
sns.scatterplot(data=df_plot, x=df_plot.index, y='abs_error', alpha=0.6, s=20, color='darkorange')
|
||||
plt.axhline(0.2, color='green', linestyle='--', linewidth=0.8, alpha=0.5, label='Target (0.2)')
|
||||
plt.axhline(0.5, color='blue', linestyle='--', linewidth=0.8, alpha=0.5)
|
||||
plt.axhline(1.0, color='red', linestyle='--', linewidth=0.8, alpha=0.5)
|
||||
plt.ylim(0, max(3.5, df_plot['abs_error'].max() + 0.5))
|
||||
plt.title(f'Validation Absolute Error - {os.path.basename(working_dir)}', fontsize=14)
|
||||
plt.xlabel('Samples (Sorted by Error Magnitude)', fontsize=12)
|
||||
plt.ylabel('Absolute Error', fontsize=12)
|
||||
plt.grid(True, axis='y', alpha=0.3)
|
||||
|
||||
plot_path = os.path.join(working_dir, "validate.png")
|
||||
plt.savefig(plot_path)
|
||||
plt.close()
|
||||
print(f"Validation plot saved: {plot_path}")
|
||||
except Exception as e:
|
||||
print(f"[WARN] Failed to create validation plot: {e}")
|
||||
|
||||
joblib.dump(model, model_path)
|
||||
joblib.dump(scaler, scaler_path)
|
||||
with open(feature_names_path, "w", encoding="utf-8") as f:
|
||||
|
||||
@@ -6,6 +6,9 @@ import os
|
||||
import random
|
||||
import joblib
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
|
||||
from xgboost import XGBRegressor
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
@@ -177,7 +180,9 @@ def train_model(
|
||||
|
||||
dataset.append((
|
||||
features,
|
||||
measure
|
||||
measure,
|
||||
songno,
|
||||
diff
|
||||
))
|
||||
|
||||
# =====================================================
|
||||
@@ -205,25 +210,29 @@ def train_model(
|
||||
]
|
||||
|
||||
X_train = np.array(
|
||||
[x for x, _ in train_dataset],
|
||||
[x for x, _, _, _ in train_dataset],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
y_train = np.array(
|
||||
[y for _, y in train_dataset],
|
||||
[y for _, y, _, _ in train_dataset],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
X_valid = np.array(
|
||||
[x for x, _ in valid_dataset],
|
||||
[x for x, _, _, _ in valid_dataset],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
y_valid = np.array(
|
||||
[y for _, y in valid_dataset],
|
||||
[y for _, y, _, _ in valid_dataset],
|
||||
dtype=np.float32
|
||||
)
|
||||
|
||||
valid_info = [
|
||||
(s, d) for _, _, s, d in valid_dataset
|
||||
]
|
||||
|
||||
print(f"Train Size: {len(X_train)}")
|
||||
print(f"Valid Size: {len(X_valid)}")
|
||||
print(f"Feature Count: {len(feature_names)}")
|
||||
@@ -314,6 +323,73 @@ def train_model(
|
||||
for name, score in pairs:
|
||||
print(f"{name:25} {score:.6f}")
|
||||
|
||||
# =====================================================
|
||||
# save validate.json
|
||||
# =====================================================
|
||||
|
||||
validate_details = []
|
||||
for i in range(len(y_valid)):
|
||||
actual = float(y_valid[i])
|
||||
predicted = float(pred[i])
|
||||
songno, diff = valid_info[i]
|
||||
|
||||
validate_details.append({
|
||||
"songno": songno,
|
||||
"diff": diff,
|
||||
"actual": actual,
|
||||
"predicted": predicted,
|
||||
"error": actual - predicted
|
||||
})
|
||||
|
||||
# 에러 절댓값 기준 정렬
|
||||
validate_details.sort(key=lambda x: abs(x["error"]), reverse=True)
|
||||
|
||||
validate_result = {
|
||||
"summary": {
|
||||
"total_compared": len(y_valid),
|
||||
"average_absolute_error": float(mae),
|
||||
"accuracy": float(accuracy),
|
||||
"timestamp": "now",
|
||||
"script_used": "train/train_xgboost.py"
|
||||
},
|
||||
"details": validate_details
|
||||
}
|
||||
|
||||
validate_path = os.path.join(working_dir, "validate.json")
|
||||
with open(validate_path, "w", encoding="utf-8") as f:
|
||||
json.dump(validate_result, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"Validation result saved: {validate_path}")
|
||||
|
||||
# =====================================================
|
||||
# save validate.png
|
||||
# =====================================================
|
||||
try:
|
||||
plt.switch_backend('Agg') # GUI 없는 환경 대응
|
||||
df_plot = pd.DataFrame(validate_details)
|
||||
df_plot['abs_error'] = df_plot['error'].abs()
|
||||
df_plot = df_plot.sort_values('abs_error', ascending=False).reset_index(drop=True)
|
||||
|
||||
plt.figure(figsize=(12, 6))
|
||||
sns.scatterplot(data=df_plot, x=df_plot.index, y='abs_error', alpha=0.6, s=20, color='darkorange')
|
||||
|
||||
plt.axhline(0.2, color='green', linestyle='--', linewidth=0.8, alpha=0.5, label='Target (0.2)')
|
||||
plt.axhline(0.5, color='blue', linestyle='--', linewidth=0.8, alpha=0.5)
|
||||
plt.axhline(1.0, color='red', linestyle='--', linewidth=0.8, alpha=0.5)
|
||||
|
||||
plt.ylim(0, max(3.5, df_plot['abs_error'].max() + 0.5))
|
||||
plt.title(f'Validation Absolute Error - {os.path.basename(working_dir)}', fontsize=14)
|
||||
plt.xlabel('Samples (Sorted by Error Magnitude)', fontsize=12)
|
||||
plt.ylabel('Absolute Error', fontsize=12)
|
||||
plt.grid(True, axis='y', alpha=0.3)
|
||||
|
||||
plot_path = os.path.join(working_dir, "validate.png")
|
||||
plt.savefig(plot_path)
|
||||
plt.close()
|
||||
print(f"Validation plot saved: {plot_path}")
|
||||
except Exception as e:
|
||||
print(f"[WARN] Failed to create validation plot: {e}")
|
||||
|
||||
# =====================================================
|
||||
# save
|
||||
# =====================================================
|
||||
@@ -366,4 +442,4 @@ if __name__ == "__main__":
|
||||
train_model(
|
||||
args.workingDir,
|
||||
args.dataDir
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user