This commit is contained in:
2026-04-25 02:32:22 +09:00
parent 52aaa4d1f6
commit 8a8c0c9713
38 changed files with 1033 additions and 38 deletions

View File

View File

@@ -8,6 +8,7 @@
"tja-parser": "^0.2.9",
},
"devDependencies": {
"@types/bun": "^1.3.13",
"@types/iconv-lite": "^0.0.1",
"@types/node": "^25.6.0",
"typescript": "^6.0.3",
@@ -17,10 +18,14 @@
"packages": {
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/iconv-lite": ["@types/iconv-lite@0.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-SsRBQxGw7/2/NxYJfBdiUx5a7Ms/voaUhOO9u2y9FTeTNBO1PXohzE4i3JfD8q2Te42HLTn5pyZtDf8j1bPKgQ=="],
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"complex.js": ["complex.js@2.4.3", "", {}, "sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ=="],
"csv-parse": ["csv-parse@6.2.1", "", {}, "sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ=="],

12
docs/1. tja factorize.md Normal file
View File

@@ -0,0 +1,12 @@
# TJA factorize
TJA를 DNN에 사용하기 위해 factorize한다.
각 노트를 다음과 같이 변환한다.
- [Note type, BPM, Scroll, Timing]
- Note type은 0과 1만 사용한다.
- Don -> 0
- Ka -> 1
- BPM은 100을 나누어 사용한다.
- Scroll은 5를 나누어 사용한다.
- Timing은 이전 노트와의 시간 차이를 사용한다. (단위 s)

40
docs/2. features.md Normal file
View File

@@ -0,0 +1,40 @@
# Feature
DNN 분석을 위해 채보의 특성을 추출한다.
## Average density
평균 밀도를 나타낸다.
$\frac{노트 수}{마지막 노트 타이밍 - 첫 노트 타이밍}$
## Peak density
최고 밀도를 나타낸다.
어떤 노트를 기준으로 앞으로 1초내에 있는 노트들의 개수 중 최대값으로 구한다.
## Average BPM
평균 BPM을 나타낸다.
$\frac{\Sigma BPM}{노트 수}$
## Average BPM 2
평균 BPM을 나타내나, 다른 방식으로 구한다.
극소수의 노트만 BPM이 다를 경우 평균 BPM에 미치는 영향이 클 수 있기 때문에, BPM의 제곱을 총합하여 평균을 구한다.
$\sqrt{\frac{\Sigma BPM^2}{노트수}}$
## BPM Change
BPM 변화 횟수를 나타낸다. BPM 흔들림을 제외하기 위해, 이전 노트와의 BPM차이가 1.5 이상일 떄 1 증가한다.
## Scroll Change
스크롤 변화 횟수를 나타낸다. 노트의 $BPM \times Scroll$의 값이 이전 노트와 1.5 이상 차이날 때 1 증가한다.
## Rhythm Complexity
i번째 노트와 i-1번쨰 노트의 간격이 i-1번쨰 노트와 i-2번째 노트의 간격의 비율이 2의 거듭제곱이 아닐 때 1 증가한다.
## Color Complexity
i번째 노트와 i-2번쨰 노트가 다른 종류일 때 증가하며, 간격의 제곱의 역수에 비례한다.
$\frac{\mathrm{color\ changed\ ?\ 1\ :\ 0}}{\Delta t^2}$
## Note Count
노트의 개수

View File

@@ -5,6 +5,7 @@
"tja-parser": "^0.2.9"
},
"devDependencies": {
"@types/bun": "^1.3.13",
"@types/iconv-lite": "^0.0.1",
"@types/node": "^25.6.0",
"typescript": "^6.0.3"

155
predict/predict_xgboost.py Normal file
View File

@@ -0,0 +1,155 @@
import argparse
import json
import math
import os
import joblib
import numpy as np
# =========================================================
# 파일명
# =========================================================
FEATURES_FILENAME = "features.json"
MODEL_FILENAME = "model.pkl"
SCALER_FILENAME = "scaler.pkl"
FEATURE_NAMES_FILENAME = "features.txt"
# =========================================================
# safe float
# =========================================================
def safe_float(value):
if value is None:
return 0.0
x = float(value)
if not math.isfinite(x):
return 0.0
return x
# =========================================================
# 예측 함수
# =========================================================
def predict(
working_dir: str,
songno: str
):
# =====================================================
# 경로
# =====================================================
features_path = os.path.join(
working_dir,
FEATURES_FILENAME
)
model_path = os.path.join(
working_dir,
MODEL_FILENAME
)
scaler_path = os.path.join(
working_dir,
SCALER_FILENAME
)
feature_names_path = os.path.join(
working_dir,
FEATURE_NAMES_FILENAME
)
# =====================================================
# 모델 로드
# =====================================================
model = joblib.load(model_path)
scaler = joblib.load(scaler_path)
# =====================================================
# feature 이름 로드
# =====================================================
with open(feature_names_path, "r", encoding="utf-8") as f:
feature_names = [
line.strip()
for line in f.readlines()
if line.strip()
]
# =====================================================
# features.json 로드
# =====================================================
with open(features_path, "r", encoding="utf-8") as f:
data = json.load(f)
# =====================================================
# target 찾기
# =====================================================
targets = []
for item in data:
if str(item["songno"]) == str(songno):
targets.append(item)
if len(targets) == 0:
raise ValueError(f"Chart not found: songno={songno}")
# =====================================================
# feature vector 생성
# =====================================================
row = []
for target in targets:
row = []
for k in feature_names:
value = target.get(k, 0)
row.append(safe_float(value))
X = np.array([row], dtype=np.float32)
X = scaler.transform(X)
pred = model.predict(X)[0]
diff = target.get("difficulty", "unknown")
print(
f"{diff:10} "
f"{pred:.1f}"
)
# =========================================================
# main
# =========================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--workingDir",
required=True
)
parser.add_argument(
"--songno",
required=True
)
args = parser.parse_args()
predict(
args.workingDir,
args.songno
)

18
preprocess/factorize.ts Normal file
View File

@@ -0,0 +1,18 @@
import { Course } from "tja-parser";
import { filterHitNotes } from "./util";
export type NoteFactor = [type: 0 | 1, bpm: number, scroll: number, delta: number]
export function factorize(course: Course) {
const hitNotes = filterHitNotes(course);
const factors: NoteFactor[] = [];
for (let i = 0; i < hitNotes.length; i++) {
const note = hitNotes[i];
factors.push([
(note.type === 1 || note.type === 3) ? 0 : 1,
note.getBPM() / 100,
note.getScroll() / 5,
i === 0 ? 0 : (note.getTimingMS() - hitNotes[i - 1].getTimingMS()) / 1000
])
}
return factors;
}

View File

@@ -1,6 +1,6 @@
import { Course, Bar, Note, HitNote } from 'tja-parser';
export namespace Factor {
export namespace Feature {
export function filterHitNotes(course: Course) {
const notes: HitNote[] = [];
course.noteGroups.forEach((g) => {
@@ -31,6 +31,9 @@ export namespace Factor {
// 밀도 관련
export function getAverageDensity(notes: HitNote[]) {
if (notes.length === 0) {
return 0;
}
return notes.length / (notes[notes.length - 1].getTimingMS() - notes[0].getTimingMS()) * 1000
}
@@ -50,7 +53,7 @@ export namespace Factor {
// BPM 관련
export function getAverageBPM(notes: HitNote[]) {
if(notes.length === 0) return 0;
const averageBPM = notes.reduce((p, note) => p + note.getBPM().valueOf(), 0) / notes.length;
return averageBPM;
}
@@ -65,29 +68,6 @@ export namespace Factor {
return bpmChange;
}
// 복잡성
export function getComplexity(notes: HitNote[]) {
let complexity = 0;
for (let i = 2; i < notes.length; i++) {
let localComplexity = 0;
// ddk 또는 dkk류면 1, 아니면 0.5
if (
(notes[i].type % 2 === notes[i - 1].type % 2 && notes[i - 1].type % 2 !== notes[i - 2].type % 2) ||
(notes[i].type % 2 !== notes[i - 1].type % 2 && notes[i - 1].type % 2 === notes[i - 2].type % 2)
) {
localComplexity = 1;
} else {
localComplexity = 0.5
}
// 시간 차가 짧을 수록 complexity 증가
localComplexity *= (1 / (notes[i].getTimingMS() - notes[i - 1].getTimingMS()) + 1 / (notes[i - 1].getTimingMS() - notes[i - 2].getTimingMS()))
complexity += localComplexity;
}
return complexity / notes.length;
}
// 스크롤 변화
export function getScrollChange(notes: HitNote[]) {
let bpmChange = 0;
@@ -98,17 +78,44 @@ export namespace Factor {
};
return bpmChange;
}
export function getRhythmComplexity(notes: HitNote[]) {
let complexity = 0;
for (let i = 2; i < notes.length; i++) {
const d1 = notes[i].getTimingMS() - notes[i - 1].getTimingMS()
const d2 = notes[i - 1].getTimingMS() - notes[i - 2].getTimingMS()
const ratio = d1 / d2;
const log = Math.log2(ratio);
if (Math.abs(log - Math.round(log)) < 1e-3) {
complexity++;
}
}
return complexity;
}
export function factorize(course: Course) {
const notes = Factor.filterHitNotes(course)
export function getColorComplexity(notes: HitNote[]) {
let complexity = 0;
for (let i = 2; i < notes.length; i++) {
if (notes[i].type % 2 != notes[i - 2].type % 2) {
complexity += 1 / ((notes[i].getTimingMS() - notes[i - 2].getTimingMS()) ** 2)
}
}
return complexity;
}
}
export function featurize(course: Course) {
const notes = Feature.filterHitNotes(course)
return {
note_count: notes.length,
density_avg: Factor.getAverageDensity(notes),
density_peak: Factor.getPeakDensity(notes),
bpm_avg: Factor.getAverageBPM(notes),
bpm_change: Factor.getBpmChange(notes),
complexity: Factor.getComplexity(notes),
scroll_change: Factor.getScrollChange(notes)
density_avg: Feature.getAverageDensity(notes),
density_peak: Feature.getPeakDensity(notes),
bpm_avg: Feature.getAverageBPM(notes),
bpm_change: Feature.getBpmChange(notes),
scroll_change: Feature.getScrollChange(notes),
rhythm_complexity: Feature.getRhythmComplexity(notes),
color_complexity: Feature.getColorComplexity(notes)
};
}

15
preprocess/util.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Bar, Course, HitNote } from "tja-parser";
export function filterHitNotes(course: Course): HitNote[] {
const notes: HitNote[] = [];
course.noteGroups.forEach((g) => {
if (g instanceof Bar) {
for (const note of g.getNotes()) {
if (note instanceof HitNote) {
notes.push(note);
}
}
}
});
return notes;
}

65
script/preprocess.ts Normal file
View File

@@ -0,0 +1,65 @@
import Bun from 'bun';
import path from 'node:path';
import { parseArgs } from 'node:util';
import fs, { mkdirSync } from 'node:fs';
import { Song } from 'tja-parser';
import { featurize } from '../preprocess/featurize';
import { parseTja } from '../preprocess/parse'
const { values } = parseArgs({
args: Bun.argv,
options: {
outputDir: {
type: "string"
},
dataDir: {
type: "string"
}
},
strict: true,
allowPositionals: true,
})
if (!values.dataDir || !values.outputDir) {
console.error("--outputDir --dataDir");
process.exit(1);
}
const outputDir = values.outputDir ?? '';
if (!fs.existsSync(outputDir)) mkdirSync(outputDir)
const dataDir = values.dataDir ?? '';
const tjaDir = path.join(dataDir, 'tja');
const files = fs.readdirSync(tjaDir);
const features: ({ songno: string, difficulty: 'oni' | 'ura' } & {})[] = [];
for (const file of files) {
const tja = fs.readFileSync(path.join(tjaDir, file), 'utf-8');
const songno = path.basename(file, '.tja');
try {
const parsed = parseTja(tja);
const oni = parsed?.oni;
const edit = parsed?.edit;
if (oni) {
features.push({
songno,
difficulty: 'oni',
...featurize(oni)
})
}
if (edit) {
features.push({
songno,
difficulty: 'ura',
...featurize(edit)
})
}
}
catch (err) {
console.error(err);
console.error(file);
}
}
const featurePath = path.join(outputDir, 'features.json');
fs.writeFileSync(featurePath, JSON.stringify(features, null, 2), 'utf-8');

313
train/train_xgboost.py Normal file
View File

@@ -0,0 +1,313 @@
import argparse
import csv
import json
import math
import os
import joblib
import numpy as np
from xgboost import XGBRegressor
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error
# =========================================================
# Hyper Parameters
# =========================================================
TEST_SIZE = 0.2
RANDOM_STATE = 42
N_ESTIMATORS = 500
MAX_DEPTH = 6
LEARNING_RATE = 0.05
SUBSAMPLE = 0.8
COLSAMPLE_BYTREE = 0.8
CONTINUE_TRAINING = True
# 예측 성공으로 간주할 허용 오차
ERROR_TOLERANCE = 0.2
# =========================================================
# 파일명
# =========================================================
FEATURES_FILENAME = "features.json"
MEASURE_FILENAME = "measure.csv"
MODEL_FILENAME = "model.pkl"
SCALER_FILENAME = "scaler.pkl"
FEATURE_NAMES_FILENAME = "features.txt"
# =========================================================
# 무시할 key
# =========================================================
IGNORE_KEYS = {
"songno",
"difficulty"
}
def safe_float(value):
if value is None:
return 0.0
x = float(value)
if not math.isfinite(x):
return 0.0
return x
def train_model(
working_dir: str,
data_dir: str
):
# =====================================================
# path
# =====================================================
features_path = os.path.join(
working_dir,
FEATURES_FILENAME
)
measure_path = os.path.join(
data_dir,
MEASURE_FILENAME
)
model_path = os.path.join(
working_dir,
MODEL_FILENAME
)
scaler_path = os.path.join(
working_dir,
SCALER_FILENAME
)
feature_names_path = os.path.join(
working_dir,
FEATURE_NAMES_FILENAME
)
# =====================================================
# features.json
# =====================================================
with open(features_path, "r", encoding="utf-8") as f:
feature_data = json.load(f)
if len(feature_data) == 0:
raise ValueError("features.json is empty")
# =====================================================
# feature map
# =====================================================
feature_map = {}
for item in feature_data:
key = (
str(item["songno"]),
str(item["difficulty"])
)
feature_map[key] = item
# =====================================================
# feature names
# =====================================================
feature_names = sorted([
k for k in feature_data[0].keys()
if k not in IGNORE_KEYS
])
# =====================================================
# measure.csv
# =====================================================
X = []
y = []
with open(measure_path, "r", encoding="utf-8") as f:
reader = csv.reader(f)
next(reader, None)
for row in reader:
if len(row) < 3:
continue
measure = safe_float(row[0])
songno = str(row[1])
diff = str(row[2])
key = (songno, diff)
if key not in feature_map:
print(
f"[WARN] feature not found: "
f"{songno} {diff}"
)
continue
feature_item = feature_map[key]
features = [
safe_float(feature_item.get(k, 0))
for k in feature_names
]
X.append(features)
y.append(measure)
if len(X) == 0:
raise ValueError("No training data")
X = np.array(X, dtype=np.float32)
y = np.array(y, dtype=np.float32)
print(f"Dataset Size: {len(X)}")
print(f"Feature Count: {len(feature_names)}")
# =====================================================
# split
# =====================================================
X_train, X_valid, y_train, y_valid = train_test_split(
X,
y,
test_size=TEST_SIZE,
random_state=RANDOM_STATE
)
# =====================================================
# scaler
# =====================================================
if CONTINUE_TRAINING and os.path.exists(scaler_path):
print("Loading existing scaler...")
scaler = joblib.load(scaler_path)
else:
print("Creating new scaler...")
scaler = StandardScaler()
scaler.fit(X_train)
X_train = scaler.transform(X_train)
X_valid = scaler.transform(X_valid)
# =====================================================
# model
# =====================================================
if CONTINUE_TRAINING and os.path.exists(model_path):
print("Loading existing model...")
model = joblib.load(model_path)
previous_booster = model.get_booster()
model.fit(
X_train,
y_train,
xgb_model=previous_booster
)
else:
print("Creating new model...")
model = XGBRegressor(
n_estimators=N_ESTIMATORS,
max_depth=MAX_DEPTH,
learning_rate=LEARNING_RATE,
subsample=SUBSAMPLE,
colsample_bytree=COLSAMPLE_BYTREE,
objective="reg:squarederror",
random_state=RANDOM_STATE
)
model.fit(X_train, y_train)
# =====================================================
# evaluate
# =====================================================
pred = model.predict(X_valid)
mae = mean_absolute_error(y_valid, pred)
correct = np.sum(
np.abs(pred - y_valid) <= ERROR_TOLERANCE
)
accuracy = correct / len(y_valid)
print(f"\nMAE: {mae:.4f}")
print(
f"Accuracy "
f"{ERROR_TOLERANCE}): "
f"{accuracy:.4f}"
)
# =====================================================
# feature importance
# =====================================================
print("\nFeature Importance:")
importance = model.feature_importances_
pairs = list(zip(feature_names, importance))
pairs.sort(key=lambda x: x[1], reverse=True)
for name, score in pairs:
print(f"{name:25} {score:.6f}")
# =====================================================
# save
# =====================================================
joblib.dump(model, model_path)
joblib.dump(scaler, scaler_path)
with open(feature_names_path, "w", encoding="utf-8") as f:
for name in feature_names:
f.write(name + "\n")
print("\nSaved:")
print(model_path)
print(scaler_path)
print(feature_names_path)
# =========================================================
# main
# =========================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--workingDir",
required=True
)
parser.add_argument(
"--dataDir",
required=True
)
args = parser.parse_args()
train_model(
args.workingDir,
args.dataDir
)

369
train/train_xgboost_pick.py Normal file
View File

@@ -0,0 +1,369 @@
import argparse
import csv
import json
import math
import os
import random
import joblib
import numpy as np
from xgboost import XGBRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_absolute_error
# =========================================================
# Hyper Parameters
# =========================================================
TRAIN_SIZE = 0
VALID_SIZE = 0
RANDOM_STATE = 42
N_ESTIMATORS = 500
MAX_DEPTH = 6
LEARNING_RATE = 0.05
SUBSAMPLE = 0.8
COLSAMPLE_BYTREE = 0.8
CONTINUE_TRAINING = True
# 예측 성공으로 간주할 허용 오차
ERROR_TOLERANCE = 0.5
# =========================================================
# 파일명
# =========================================================
FEATURES_FILENAME = "features.json"
MEASURE_FILENAME = "measure.csv"
MODEL_FILENAME = "model.pkl"
SCALER_FILENAME = "scaler.pkl"
FEATURE_NAMES_FILENAME = "features.txt"
# =========================================================
# 무시할 key
# =========================================================
IGNORE_KEYS = {
"songno",
"difficulty"
}
# =========================================================
# safe float
# =========================================================
def safe_float(value):
if value is None:
return 0.0
x = float(value)
if not math.isfinite(x):
return 0.0
return x
# =========================================================
# train
# =========================================================
def train_model(
working_dir: str,
data_dir: str
):
random.seed(RANDOM_STATE)
# =====================================================
# path
# =====================================================
features_path = os.path.join(
working_dir,
FEATURES_FILENAME
)
measure_path = os.path.join(
data_dir,
MEASURE_FILENAME
)
model_path = os.path.join(
working_dir,
MODEL_FILENAME
)
scaler_path = os.path.join(
working_dir,
SCALER_FILENAME
)
feature_names_path = os.path.join(
working_dir,
FEATURE_NAMES_FILENAME
)
# =====================================================
# features.json
# =====================================================
with open(features_path, "r", encoding="utf-8") as f:
feature_data = json.load(f)
if len(feature_data) == 0:
raise ValueError("features.json is empty")
# =====================================================
# feature map
# =====================================================
feature_map = {}
for item in feature_data:
key = (
str(item["songno"]),
str(item["difficulty"])
)
feature_map[key] = item
# =====================================================
# feature names
# =====================================================
feature_names = sorted([
k for k in feature_data[0].keys()
if k not in IGNORE_KEYS
])
# =====================================================
# dataset build
# =====================================================
dataset = []
with open(measure_path, "r", encoding="utf-8") as f:
reader = csv.reader(f)
next(reader, None)
for row in reader:
if len(row) < 3:
continue
measure = safe_float(row[0])
songno = str(row[1])
diff = str(row[2])
key = (songno, diff)
if key not in feature_map:
print(
f"[WARN] feature not found: "
f"{songno} {diff}"
)
continue
feature_item = feature_map[key]
features = [
safe_float(feature_item.get(k, 0))
for k in feature_names
]
dataset.append((
features,
measure
))
# =====================================================
# shuffle
# =====================================================
random.shuffle(dataset)
required_size = TRAIN_SIZE + VALID_SIZE
if len(dataset) < required_size:
raise ValueError(
f"Not enough dataset "
f"({len(dataset)} < {required_size})"
)
# =====================================================
# split
# =====================================================
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
)
print(f"Train Size: {len(X_train)}")
print(f"Valid Size: {len(X_valid)}")
print(f"Feature Count: {len(feature_names)}")
# =====================================================
# scaler
# =====================================================
if CONTINUE_TRAINING and os.path.exists(scaler_path):
print("Loading existing scaler...")
scaler = joblib.load(scaler_path)
else:
print("Creating new scaler...")
scaler = StandardScaler()
scaler.fit(X_train)
X_train = scaler.transform(X_train)
X_valid = scaler.transform(X_valid)
# =====================================================
# model
# =====================================================
if CONTINUE_TRAINING and os.path.exists(model_path):
print("Loading existing model...")
model = joblib.load(model_path)
previous_booster = model.get_booster()
model.fit(
X_train,
y_train,
xgb_model=previous_booster
)
else:
print("Creating new model...")
model = XGBRegressor(
n_estimators=N_ESTIMATORS,
max_depth=MAX_DEPTH,
learning_rate=LEARNING_RATE,
subsample=SUBSAMPLE,
colsample_bytree=COLSAMPLE_BYTREE,
objective="reg:squarederror",
random_state=RANDOM_STATE
)
model.fit(X_train, y_train)
# =====================================================
# evaluate
# =====================================================
pred = model.predict(X_valid)
mae = mean_absolute_error(y_valid, pred)
correct = np.sum(
np.abs(pred - y_valid) <= ERROR_TOLERANCE
)
accuracy = correct / len(y_valid)
print(f"\nMAE: {mae:.4f}")
print(
f"Accuracy "
f"{ERROR_TOLERANCE}): "
f"{accuracy:.4f}"
)
# =====================================================
# feature importance
# =====================================================
print("\nFeature Importance:")
importance = model.feature_importances_
pairs = list(zip(feature_names, importance))
pairs.sort(key=lambda x: x[1], reverse=True)
for name, score in pairs:
print(f"{name:25} {score:.6f}")
# =====================================================
# save
# =====================================================
joblib.dump(model, model_path)
joblib.dump(scaler, scaler_path)
with open(feature_names_path, "w", encoding="utf-8") as f:
for name in feature_names:
f.write(name + "\n")
print("\nSaved:")
print(model_path)
print(scaler_path)
print(feature_names_path)
# =========================================================
# main
# =========================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--workingDir",
required=True
)
parser.add_argument(
"--dataDir",
required=True
)
parser.add_argument(
"--trainSize",
required=True
)
parser.add_argument(
"--validSize",
required=True
)
args = parser.parse_args()
TRAIN_SIZE = int(args.trainSize)
VALID_SIZE = int(args.validSize)
train_model(
args.workingDir,
args.dataDir
)

View File

@@ -1,5 +0,0 @@
{
"compilerOptions": {
"lib": []
}
}