DanceDanceRevolution [^ DDR] ist eines der von KONAMI entwickelten Musikspiele. DanceDanceRevolution hat für jede Partitur einen Schwierigkeitsgrad [^], der zeigt, wie schwierig es ist, diese Partitur zu spielen.
Abgesehen davon gibt es einen Mechanismus namens Groove Radar, der die Tendenz der Musikpartitur zeigt. Jedes Element ist wie folgt.
Der Wert des Groove-Radars kann genau aus der Musikpartitur selbst berechnet werden. Die Formel wurde nicht veröffentlicht, aber von freiwilligen Spielern mit beträchtlicher Genauigkeit enthüllt.
Andererseits wird der Schwierigkeitsgrad von der Produktionsseite künstlich bestimmt. Daher kann der Schwierigkeitsgrad zum Zeitpunkt des Versions-Upgrades überprüft werden.
Ist es dann möglich, den Schwierigkeitsgrad aus dem numerischen Wert des Rillenradars abzuschätzen? Machen wir das.
[^ DDR]: Es ist lang und ich möchte es weglassen, aber ich hatte das Gefühl, dass das Weglassen mit Qiita ein Hindernis für Leute wäre, die etwas über das Gedächtnis herausfinden wollen, also werde ich es nicht weglassen. [^ level]: Da es einmal durch ein Fußsymbol angezeigt wurde, wird es als "Fuß 16" geschrieben.
Ich werde es importieren.
import math
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from pycm import ConfusionMatrix
from scipy.optimize import minimize, differential_evolution, Bounds
def ustd_coefficient(n):
try:
return math.sqrt(n / 2) * math.gamma((n - 1) / 2) / math.gamma(n / 2)
except OverflowError:
return math.sqrt(n / (n - 1.5))
def std_u(a):
return np.std(a) * ustd_coefficient(len(a))
oo = math.inf
Lesen Sie die Daten jeder Partitur. Als Daten wurden die Daten des alten und des neuen Songs von DanceDanceRevolution A20 aus dem damaligen BEMANI-Wiki auf CSV gesetzt. Platzieren Sie es unter hier. Dieses Mal werden wir das alte Lied als Trainingsdaten für die Anpassung und das neue Lied als Bewertungsdaten verwenden. Lesen wir es jetzt und machen es zu einem DataFrame.
old_csv = Path('./old.csv')
new_csv = Path('./new.csv')
train_df = pd.read_csv(old_csv)
test_df = pd.read_csv(new_csv)
display(train_df)
display(test_df)
VERSION | MUSIC | SEQUENCE | LEVEL | STREAM | VOLTAGE | AIR | FREEZE | CHAOS | |
---|---|---|---|---|---|---|---|---|---|
0 | DanceDanceRevolution A | Worte der Liebe | BEGINNER | 3 | 21 | 22 | 7 | 26 | 0 |
1 | DanceDanceRevolution A | Worte der Liebe | BASIC | 5 | 34 | 22 | 18 | 26 | 0 |
2 | DanceDanceRevolution A | Worte der Liebe | DIFFICULT | 7 | 43 | 34 | 23 | 26 | 7 |
3 | DanceDanceRevolution A | Worte der Liebe | EXPERT | 11 | 63 | 45 | 21 | 25 | 28 |
4 | DanceDanceRevolution A | Tenno schwach | BEGINNER | 3 | 20 | 25 | 0 | 0 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
3390 | DanceDanceRevolution 1st | PARANOiA | EXPERT | 11 | 67 | 52 | 25 | 0 | 17 |
3391 | DanceDanceRevolution 1st | TRIP MACHINE | BEGINNER | 3 | 25 | 26 | 5 | 0 | 0 |
3392 | DanceDanceRevolution 1st | TRIP MACHINE | BASIC | 8 | 47 | 40 | 14 | 0 | 4 |
3393 | DanceDanceRevolution 1st | TRIP MACHINE | DIFFICULT | 9 | 52 | 40 | 30 | 0 | 7 |
3394 | DanceDanceRevolution 1st | TRIP MACHINE | EXPERT | 10 | 56 | 53 | 36 | 0 | 12 |
3395 rows × 9 columns
VERSION | MUSIC | SEQUENCE | LEVEL | STREAM | VOLTAGE | AIR | FREEZE | CHAOS | |
---|---|---|---|---|---|---|---|---|---|
0 | DanceDanceRevolution A20 | In Ordnung! Schön! Schätzchen! Darin! | BEGINNER | 3 | 18 | 21 | 5 | 16 | 0 |
1 | DanceDanceRevolution A20 | In Ordnung! Schön! Schätzchen! Darin! | BASIC | 7 | 37 | 28 | 18 | 39 | 0 |
2 | DanceDanceRevolution A20 | In Ordnung! Schön! Schätzchen! Darin! | DIFFICULT | 12 | 60 | 56 | 54 | 55 | 21 |
3 | DanceDanceRevolution A20 | In Ordnung! Schön! Schätzchen! Darin! | EXPERT | 15 | 95 | 99 | 30 | 25 | 100 |
4 | DanceDanceRevolution A20 | Revolution leidenschaftlich | BEGINNER | 3 | 16 | 16 | 1 | 35 | 0 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
380 | DanceDanceRevolution A20 | 50th Memorial Songs -The BEMANI History- | EXPERT | 13 | 63 | 79 | 14 | 62 | 63 |
381 | DanceDanceRevolution A20 | 50th Memorial Songs -Wenn wir zwei sind ~ unter der Kirsche bl... | BEGINNER | 3 | 17 | 20 | 3 | 46 | 0 |
382 | DanceDanceRevolution A20 | 50th Memorial Songs -Wenn wir zwei sind ~ unter der Kirsche bl... | BASIC | 7 | 40 | 33 | 36 | 29 | 0 |
383 | DanceDanceRevolution A20 | 50th Memorial Songs -Wenn wir zwei sind ~ unter der Kirsche bl... | DIFFICULT | 9 | 50 | 46 | 47 | 3 | 6 |
384 | DanceDanceRevolution A20 | 50th Memorial Songs -Wenn wir zwei sind ~ unter der Kirsche bl... | EXPERT | 12 | 73 | 60 | 60 | 15 | 32 |
385 rows × 9 columns
Darüber hinaus werden wir die numerischen Werte jedes Rillenradars standardisieren. Stellen Sie sicher, dass der Durchschnitt der Trainingsdaten 0 und die Standardabweichung 1 ist, und führen Sie dieselbe Operation für die Bewertungsdaten aus.
grs = ['STREAM', 'VOLTAGE', 'AIR', 'FREEZE', 'CHAOS']
sgrs = ['S_{}'.format(gr) for gr in grs]
m = {}
s = {}
for gr, sgr in zip(grs, sgrs):
v = train_df.loc[:, gr].values
v_t = test_df.loc[:, gr].values
m[gr] = np.mean(v)
s[gr] = std_u(v)
train_df[sgr] = (v - m[gr]) / s[gr]
test_df[sgr] = (v_t - m[gr]) / s[gr]
display(train_df.loc[:, sgrs])
display(test_df.loc[:, sgrs])
S_STREAM | S_VOLTAGE | S_AIR | S_FREEZE | S_CHAOS | |
---|---|---|---|---|---|
0 | -0.981448 | -0.838977 | -0.636332 | 0.056063 | -0.661167 |
1 | -0.534364 | -0.838977 | -0.160513 | 0.056063 | -0.661167 |
2 | -0.224844 | -0.405051 | 0.055768 | 0.056063 | -0.441192 |
3 | 0.462978 | -0.007285 | -0.030744 | 0.014296 | 0.218735 |
4 | -1.015839 | -0.730495 | -0.939125 | -1.029883 | -0.661167 |
... | ... | ... | ... | ... | ... |
3390 | 0.600542 | 0.245838 | 0.142280 | -1.029883 | -0.126941 |
3391 | -0.843883 | -0.694335 | -0.722844 | -1.029883 | -0.661167 |
3392 | -0.087279 | -0.188088 | -0.333538 | -1.029883 | -0.535467 |
3393 | 0.084676 | -0.188088 | 0.358562 | -1.029883 | -0.441192 |
3394 | 0.222240 | 0.281999 | 0.618099 | -1.029883 | -0.284066 |
3395 rows × 5 columns
S_STREAM | S_VOLTAGE | S_AIR | S_FREEZE | S_CHAOS | |
---|---|---|---|---|---|
0 | -1.08462 | -0.87514 | -0.72284 | -0.36161 | -0.66117 |
1 | -0.43119 | -0.62201 | -0.16051 | 0.599036 | -0.66117 |
2 | 0.359805 | 0.39048 | 1.396711 | 1.26731 | -0.00124 |
3 | 1.563493 | 1.945381 | 0.358562 | 0.014296 | 2.481343 |
4 | -1.1534 | -1.05594 | -0.89587 | 0.431967 | -0.66117 |
... | ... | ... | ... | ... | ... |
380 | 0.462978 | 1.222171 | -0.33354 | 1.55968 | 1.318614 |
381 | -1.11901 | -0.9113 | -0.80936 | 0.891406 | -0.66117 |
382 | -0.32802 | -0.44121 | 0.618099 | 0.181364 | -0.66117 |
383 | 0.015894 | 0.028875 | 1.093917 | -0.90458 | -0.47262 |
384 | 0.806889 | 0.535122 | 1.656248 | -0.40338 | 0.344436 |
385 rows × 5 columns
Extrahieren Sie dann den Tensor im 2. Stock, der das Groove-Radar jeder Partitur anzeigt, und den Tensor im 1. Stock, der den Schwierigkeitsgrad jeder Partitur anzeigt.
train_sgr_arr = train_df.loc[:, sgrs].values
test_sgr_arr = test_df.loc[:, sgrs].values
train_level_arr = train_df.loc[:, 'LEVEL'].values
test_level_arr = test_df.loc[:, 'LEVEL'].values
Die multiple Regressionsanalyse basiert auf dem folgenden Konzept.
Es gibt eine erklärende Variablengruppe $ x_n $ und eine objektive Variable $ y
Definieren Sie zunächst die Funktion, die Sie minimieren möchten. Es ist $ e ^ 2 $.
def hadprosum(a, b):
return (a * b).sum(axis=1)
def estimate(x, sgr_arr):
x_const = x[0]
x_coef = x[1:]
return hadprosum(sgr_arr, x_coef) + x_const
def sqerr(x):
est = estimate(x, train_sgr_arr)
return ((est - train_level_arr) ** 2).sum()
Geben Sie dies der Funktion "differentielle_evolution" von SciPy. In Bezug auf den Suchbereich gebe ich einen Bereich an, der beim Ausprobieren verschiedener Dinge ausreichend zu sein scheint.
bounds = Bounds([0.] * 6, [10.] * 6)
result = differential_evolution(sqerr, bounds, seed=300)
print(result)
fun: 5170.056057917698
jac: array([-0.00236469, 0.14933903, 0.15834303, 0.07094059, 0.01737135,
0.1551598 ])
message: 'Optimization terminated successfully.'
nfev: 3546
nit: 37
success: True
x: array([8.04447683, 2.64586828, 0.58686288, 0.42785461, 0.45934494,
0.4635763 ])
Betrachtet man dieses Ergebnis, so scheint STREAM den größten Einfluss zu haben, gefolgt von VOLTAGE, CHAOS, FREEZE, AIR.
Lassen Sie uns nun anhand der tatsächlich erhaltenen Parameter bewerten.
Definieren Sie zunächst eine Funktion für die Vorhersage. Gibt einen Tensor der erwarteten Schwierigkeit unter Berücksichtigung der Parameter und des Tensors des Rillenradars an diese Funktion zurück.
def pred1(x, sgr_arr):
est = estimate(x, sgr_arr).clip(1., 19.)
return np.round(est).astype(int)
Indem Sie PyCMs "ConfusionMatrix" den Rückgabewert dieser Funktion und den tatsächlichen Schwierigkeitsgrad geben, wird ein Verwirrungsmatrixobjekt erstellt. Greifen Sie auf die Eigenschaften zu und finden Sie die richtige Antwortrate und den Makro-F-Wert.
train_pred1_arr = pred1(result.x, train_sgr_arr)
test_pred1_arr = pred1(result.x, test_sgr_arr)
train_cm1 = ConfusionMatrix(train_level_arr, train_pred1_arr)
test_cm1 = ConfusionMatrix(test_level_arr, test_pred1_arr)
print('====================')
print('Train Score')
print(' Accuracy: {}'.format(train_cm1.Overall_ACC))
print(' Fmeasure: {}'.format(train_cm1.F1_Macro))
print('====================')
print('Test Score')
print(' Accuracy: {}'.format(test_cm1.Overall_ACC))
print(' Fmeasure: {}'.format(test_cm1.F1_Macro))
print('====================')
====================
Train Score
Accuracy: 0.33431516936671574
Fmeasure: 0.2785969345790368
====================
Test Score
Accuracy: 0.3142857142857143
Fmeasure: 0.24415916194348555
====================
Die korrekte Rücklaufquote betrug 31,4%. Dies ist ein viel niedrigeres Ergebnis als ich erwartet hatte. Lassen Sie uns die Verwirrungsmatrix mit Seaborn abbilden.
plt.figure(figsize=(10, 10), dpi=200)
sns.heatmap(pd.DataFrame(test_cm1.table), annot=True, square=True, cmap='Blues')
plt.show()
Die horizontale Achse ist der tatsächliche Schwierigkeitsgrad und die vertikale Achse ist der vorhergesagte Schwierigkeitsgrad. Wenn man dies betrachtet, scheint es, dass Songs mit einem niedrigen Schwierigkeitsgrad hoch sind, diejenigen mit einem bestimmten Schwierigkeitsgrad als niedrig bewertet werden und diejenigen mit einem höheren Schwierigkeitsgrad überschätzt werden. Die Wärmekarte ist keine gerade Linie, sondern biegt sich nach innen, um eine Bogenform zu zeichnen.
Versuchen Sie als Nächstes die logistische Regressionsanalyse. Die logistische Regressionsanalyse eignet sich für Analysen, bei denen eine gewisse Wahrscheinlichkeit als Zielvariable verwendet wird. Betrachten Sie die folgende Formel.
Diesmal haben wir es nicht mit negativ oder positiv zu tun, sondern wo in der geordneten Klasse sie hingehören. Nehmen Sie in einem solchen Fall mehrere logistische Kurven an, die sich nur im konstanten Term $ k_0 $ unterscheiden, und betrachten Sie sie als "Wahrscheinlichkeit von Schwierigkeitsgrad 2 oder höher", "Wahrscheinlichkeit von Schwierigkeitsgrad 3 oder höher". "Wahrscheinlichkeit von Schwierigkeitsgrad 2" wird berechnet, indem "Wahrscheinlichkeit von Schwierigkeitsgrad 3 oder höher" von "Wahrscheinlichkeit von Schwierigkeitsgrad 2 oder höher" subtrahiert wird, sodass die Wahrscheinlichkeit daraus berechnet werden kann.
Konvertieren Sie zunächst den Schwierigkeitsgrad zur Berechnung in den Tensor im 2. Stock mit einem heißen Format.
train_level_onehot_arr = np.zeros(train_level_arr.shape + (19,))
for i, l in np.ndenumerate(train_level_arr):
train_level_onehot_arr[i, l - 1] = 1.
Geben Sie dann die zu minimierende Funktion an. Da es minimiert ist, definieren wir die obige Log-Wahrscheinlichkeit mit einem Minus.
def upperscore(x, sgr_arr):
x_const = np.append(np.append(oo, x[:18].copy()), -oo) #Fügen Sie an beiden Enden unendlich für Wahrscheinlichkeit 1 über 1 und Wahrscheinlichkeit 0 größer als 20 ein
x_coef = x[18:]
var = np.asarray([hadprosum(sgr_arr, x_coef)]).T
cons = np.asarray([x_const])
return 1 / (1 + np.exp(-(var + cons)))
def score(x, sgr_arr):
us = upperscore(x, sgr_arr)
us_2 = np.roll(us, -1)
return np.delete(us - us_2, -1, axis=1) #Verschieben und ziehen,Entfernen Sie das Ende, um die Wahrscheinlichkeit für jede Schwierigkeit zu ermitteln
def mloglh(x):
sc = score(x, train_sgr_arr)
ret = -(np.log((sc * train_level_onehot_arr).sum(axis=1).clip(1e-323, oo)).sum())
return ret
Führen Sie eine Suche durch. Bitte beachten Sie, dass es erheblich länger dauert als zuvor.
bounds = Bounds([-60.] * 18 + [0] * 5, [20.] * 18 + [10] * 5)
result = differential_evolution(mloglh, bounds, seed=300)
print(result)
fun: 4116.792196474322
jac: array([ 0.00272848, 0.00636646, -0.00090949, 0.00327418, -0.00563887,
-0.00291038, -0.00509317, 0.00045475, 0.00800355, 0.00536602,
-0.00673026, 0.00536602, 0.00782165, -0.01209628, 0.00154614,
-0.0003638 , 0.00218279, 0.00582077, 0.04783942, 0.03237801,
0.01400622, 0.00682121, 0.03601599])
message: 'Optimization terminated successfully.'
nfev: 218922
nit: 625
success: True
x: array([ 14.33053717, 12.20158703, 9.97549255, 8.1718939 ,
6.36190483, 4.58724228, 2.61478521, 0.66474105,
-1.46625252, -3.60065138, -6.27127806, -9.65032254,
-14.06390123, -18.287351 , -23.44011235, -28.39033479,
-32.35825176, -43.38390248, 6.13059504, 2.01974223,
0.64631137, 0.67555403, 2.44873606])
Diesmal STREAM> CHAOS> VOLTAGE> FREEZE> AIR, und Sie können sehen, dass CHAOS einen größeren Einfluss hat.
Lassen Sie uns dies auch tatsächlich bewerten.
def pred2(x, sgr_arr):
sc = score(x, sgr_arr)
return np.argmax(sc, axis=1) + 1
train_pred2_arr = pred2(result.x, train_sgr_arr)
test_pred2_arr = pred2(result.x, test_sgr_arr)
train_cm2 = ConfusionMatrix(train_level_arr, train_pred2_arr)
test_cm2 = ConfusionMatrix(test_level_arr, test_pred2_arr)
print('====================')
print('Train Score')
print(' Accuracy: {}'.format(train_cm2.Overall_ACC))
print(' Fmeasure: {}'.format(train_cm2.F1_Macro))
print('====================')
print('Test Score')
print(' Accuracy: {}'.format(test_cm2.Overall_ACC))
print(' Fmeasure: {}'.format(test_cm2.F1_Macro))
print('====================')
====================
Train Score
Accuracy: 0.4960235640648012
Fmeasure: 0.48246495009640167
====================
Test Score
Accuracy: 0.5454545454545454
Fmeasure: 0.5121482282311358
====================
Diesmal lag die korrekte Rücklaufquote bei 54,5%. Es ist viel besser als zuvor, aber es ist noch weit weg.
plt.figure(figsize=(10, 10), dpi=200)
sns.heatmap(pd.DataFrame(test_cm2.table), annot=True, square=True, cmap='Blues')
plt.show()
Dies ist im Allgemeinen auf einer geraden Linie, aber der niedrige und der hohe Schwierigkeitsgrad sind immer noch nicht gut.
Das Fazit lautet: "Sie können diese Art von Wert erhalten, aber es ist nicht praktisch." Ich habe diesen Versuch mit der Hoffnung gemacht, dass er als Referenz verwendet werden kann, um die Punktzahl von Step Mania zu erschweren, aber am Ende scheint es notwendig zu sein, zu spielen und sich anzupassen.
Andererseits wird bei der logistischen Regression jeder konstante Term natürlich durch die Größenbeziehung eingeschränkt, aber in diesem Code kann die Einschränkung in Abhängigkeit von der Zufallszahl möglicherweise nicht erfüllt werden, und ein abnormaler Wert kann zu einer Konvergenzbeurteilung führen [^ gebunden]. ]. Die Optimierungsfunktion von SciPy kann eine Einschränkung mit einer Ungleichung angeben, hat jedoch bei mir nicht funktioniert, da ein Fehler aufgetreten ist, als ob eine Einschränkung einer unbekannten Form übergeben wurde. Suchzeit wird auch verschwendet. Wenn jemand sie lösen kann, möchte ich einen Professor fragen.
[^ bound]: Ich bin einmal auf ein solches Phänomen gestoßen, als ich den tatsächlichen Bereich eingestellt habe.
Recommended Posts