[PYTHON] [Dance Dance Revolution] Ist es möglich, den Schwierigkeitsgrad (Fuß) aus dem Wert des Groove-Radars vorherzusagen?

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.

STREAM
durchschnittliche Dichte . Je höher die Anzahl der Objekte im Song ist, desto höher ist der Wert.
VOLTAGE
höchste Dichte . Je mehr Objekte vorhanden sind, desto höher ist die Anzahl der Objekte in 4 Schlägen.
AIR
Sprungfrequenz . Je mehr Objekte Sie nicht gleichzeitig betreten oder betreten sollten, desto höher ist der Preis.
FREEZE
Einschränkung . Je mehr Beats Sie auf ein Panel treten, desto höher wird es.
CHAOS
Unregelmäßigkeit . Je feiner Rhythmen und Verschiebungen, desto höher der Preis.

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.

Umgebung

  • Python3 + JupyterLab
    • Matplotlib
    • NumPy
    • Pandas
    • PyCM
    • SciPy
    • Seaborn

Vorbereitung

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

Multiple Regressionsanalyse nach der Methode der kleinsten Quadrate

Die multiple Regressionsanalyse basiert auf dem folgenden Konzept.

Es gibt eine erklärende Variablengruppe $ x_n $ und eine objektive Variable $ y . In diesem Fall sind die erklärenden Variablen die Werte des Rillenradars. Die Zielvariable ist der Schwierigkeitsgrad. In diesem Moment, $ y' = k_0 + k_1x_1 + k_2x_2 + \cdots + k_nx_n $$ Unter Berücksichtigung der Koeffizientengruppen $ k_n $ und $ y '$ ergibt sich der quadratische Fehler von $ m $ data $ e ^ 2: = \ sum_ {i = 1} ^ m \ left (y'_i --y_i \ right) ) $ K_n $ wird durchsucht, so dass ^ 2 $ das kleinste ist. Dieses Mal werden wir ein solches Optimierungsproblem durch die differentielle Evolutionsmethode unter Verwendung von SciPy finden.

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()

01.png

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.

Logistische Regressionsanalyse nach wahrscheinlichster Schätzmethode (Reihenfolge)

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. $ y' = \frac{1}{1 + \exp\left[-\left(k_0 + k_1x_1 + k_2x_2 + \cdots + k_nx_n\right)\right]} $ Dieses $ y '$ nimmt einen Wert von 0 bis 1 an. Wenn die Zielvariable $ y $ in $ m $ -Daten durch 0 oder 1 bestimmt wird, ist die Wahrscheinlichkeit $ l = \ prod_ {i = 1} ^ m y_iy_i '+ (1 --y_i) (1 --y_i' ) Erwägen Sie, $ zu maximieren. Es ist schwer zu verstehen, ob es in einer mathematischen Formel geschrieben ist, aber $ y '$ ist die Wahrscheinlichkeit, positiv zu sein, und wenn $ y $ 1 positiv ist, dann ist $ y' $ so wie es ist, und wenn $ y $ 0 ist, das heißt negativ. Es bedeutet, die Wahrscheinlichkeit zu verwenden, dh den Wert, der durch Subtrahieren von $ y '$ von 1 erhalten wird. Wenn Sie $ y '$ multiplizieren, dh das Intervall von $ (0, 1) $, wird der Wert zu klein und für den Computer schwierig zu handhaben. Protokollieren Sie daher die Wahrscheinlichkeit $ \ log l = \ sum_ {i = 1} ^ m \ log \ left [y_iy_i '+ (1 --y_i) (1 --y_i') \ right] Es ist normal, $ zu maximieren.

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()

02.png

Dies ist im Allgemeinen auf einer geraden Linie, aber der niedrige und der hohe Schwierigkeitsgrad sind immer noch nicht gut.

Fazit

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