[PYTHON] [Dance Dance Revolution] Est-il possible de prédire le niveau de difficulté (pied) à partir de la valeur du radar groove?

DanceDanceRevolution [^ DDR] est l'un des jeux musicaux développés par KONAMI. DanceDanceRevolution a un niveau de difficulté pour chaque partition musicale [^ level], ce qui montre à quel point il est difficile de jouer cette partition musicale.

En dehors de cela, il existe un mécanisme appelé groove radar, qui montre la tendance de la partition musicale. Chaque élément est le suivant.

STREAM
densité moyenne . Plus le nombre d'objets dans le morceau est élevé, plus la valeur est élevée.
VOLTAGE
densité la plus élevée . Plus il y a d'objets, plus le nombre d'objets en 4 temps est élevé.
AIR
Fréquence de saut . Plus il y a d'objets sur lesquels vous ne devez pas marcher ou marcher en même temps, plus le prix est élevé.
FREEZE
contrainte . Plus vous continuez à marcher sur un panneau, plus il monte.
CHAOS
irrégularité . Plus les rythmes et les changements sont fins, plus le prix est élevé.

La valeur du radar groove peut être calculée exactement à partir de la partition musicale elle-même. La formule n'a pas été publiée, mais elle a été révélée avec une précision considérable par des joueurs bénévoles.

D'un autre côté, la valeur de difficulté est déterminée artificiellement par le côté production. Par conséquent, le niveau de difficulté peut être revu au moment de la mise à niveau de la version.

Ensuite, est-il possible d'estimer le niveau de difficulté à partir de la valeur numérique du radar groove? Faisons le.

[^ DDR]: C'est long et je veux l'omettre, mais j'ai senti que l'omettre avec Qiita serait un obstacle pour les gens qui veulent en savoir plus sur la mémoire, donc je ne vais pas l'omettre. [^ level]: Puisqu'il était autrefois indiqué par une icône de pied, il s'écrit «pied 16» et ainsi de suite.

environnement

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

Préparation

Je vais l'importer.

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

Lisez les données de chaque partition musicale. En tant que données, les données de l'ancienne chanson et de la nouvelle chanson de DanceDanceRevolution A20 du wiki BEMANI à l'époque étaient définies en CSV. Placez-le sur ici. Cette fois, nous utiliserons l'ancien morceau comme données d'entraînement pour l'ajustement et le nouveau morceau comme données d'évaluation. Maintenant, lisons-le et faisons-en un 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 les mots d'amour BEGINNER 3 21 22 7 26 0
1 DanceDanceRevolution A les mots d'amour BASIC 5 34 22 18 26 0
2 DanceDanceRevolution A les mots d'amour DIFFICULT 7 43 34 23 26 7
3 DanceDanceRevolution A les mots d'amour EXPERT 11 63 45 21 25 28
4 DanceDanceRevolution A Tenno faible 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 D'accord! Charmant! Ma chérie! Darin! BEGINNER 3 18 21 5 16 0
1 DanceDanceRevolution A20 D'accord! Charmant! Ma chérie! Darin! BASIC 7 37 28 18 39 0
2 DanceDanceRevolution A20 D'accord! Charmant! Ma chérie! Darin! DIFFICULT 12 60 56 54 55 21
3 DanceDanceRevolution A20 D'accord! Charmant! Ma chérie! Darin! EXPERT 15 95 99 30 25 100
4 DanceDanceRevolution A20 Révolution passionnée 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 -Quand nous sommes deux ~ sous la cerise bl... BEGINNER 3 17 20 3 46 0
382 DanceDanceRevolution A20 50th Memorial Songs -Quand nous sommes deux ~ sous la cerise bl... BASIC 7 40 33 36 29 0
383 DanceDanceRevolution A20 50th Memorial Songs -Quand nous sommes deux ~ sous la cerise bl... DIFFICULT 9 50 46 47 3 6
384 DanceDanceRevolution A20 50th Memorial Songs -Quand nous sommes deux ~ sous la cerise bl... EXPERT 12 73 60 60 15 32
385 rows × 9 columns

De plus, nous standardiserons les valeurs numériques de chaque radar rainuré. Assurez-vous que la moyenne des données d'apprentissage est 0 et que l'écart type est 1 et effectuez la même opération pour les données d'évaluation.

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

Ensuite, extrayez le tenseur du 2ème étage qui montre le radar de groove de chaque partition musicale et le tenseur du 1er étage qui montre le niveau de difficulté de chaque partition musicale.

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

Analyse de régression multiple par la méthode des moindres carrés

L'analyse de régression multiple est basée sur le concept suivant.

Il existe un groupe de variables explicatives $ x_n $ et une variable objective $ y . Dans ce cas, les variables explicatives sont les valeurs du radar à rainure. La variable objective est le niveau de difficulté. En ce moment, $ y' = k_0 + k_1x_1 + k_2x_2 + \cdots + k_nx_n $$ En considérant les groupes de coefficients $ k_n $ et $ y '$, l'erreur carrée de $ m $ data $ e ^ 2: = \ sum_ {i = 1} ^ m \ left (y'_i --y_i \ right) ) $ K_n $ sera recherché de sorte que ^ 2 $ soit le plus petit. Cette fois, nous retrouverons un tel problème d'optimisation par la méthode d'évolution différentielle utilisant SciPy.

Tout d'abord, définissez la fonction que vous souhaitez minimiser. C'est $ 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()

Donnez ceci à la fonction difference_evolution de SciPy. En ce qui concerne la plage de recherche, je donne une plage qui semble suffisante en essayant diverses choses.

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

En regardant ce résultat, il semble que STREAM ait le plus d'influence, suivi de VOLTAGE, CHAOS, FREEZE, AIR.

Maintenant, évaluons en utilisant les paramètres réellement obtenus.

Tout d'abord, définissez une fonction de prédiction. Retourne un tenseur de la difficulté attendue compte tenu des paramètres et du tenseur du radar de rainure à cette fonction.

def pred1(x, sgr_arr):
    est = estimate(x, sgr_arr).clip(1., 19.)
    return np.round(est).astype(int)

En donnant la valeur de retour de cette fonction et la difficulté réelle à «ConfusionMatrix» de PyCM, un objet de matrice de confusion est créé. Accédez aux propriétés de celui-ci et trouvez le taux de réponse correct et la valeur de la macro F.

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
====================

Le taux de réponse correcte était de 31,4%. C'est un résultat bien inférieur à ce à quoi je m'attendais. Cartographions thermiquement la matrice de confusion avec Seaborn.

plt.figure(figsize=(10, 10), dpi=200)
sns.heatmap(pd.DataFrame(test_cm1.table), annot=True, square=True, cmap='Blues')
plt.show()

01.png

L'axe horizontal est le niveau de difficulté réel et l'axe vertical est le niveau de difficulté prévu. En regardant cela, il semble que les chansons avec un niveau de difficulté faible sont élevées, celles avec un certain niveau de difficulté sont évaluées comme faibles et celles avec un niveau de difficulté plus élevé sont surestimées. La carte thermique n'est pas une ligne droite mais se penche vers l'intérieur pour dessiner une forme d'arc.

Analyse de régression logistique par la méthode d'estimation la plus probable (ordre)

Ensuite, essayez l'analyse de régression logistique. L'analyse de régression logistique convient à l'analyse qui prend une certaine probabilité comme variable objective. Considérez la formule suivante. $ y' = \frac{1}{1 + \exp\left[-\left(k_0 + k_1x_1 + k_2x_2 + \cdots + k_nx_n\right)\right]} $ Ce $ y '$ prend une valeur de 0 à 1. Lorsque la variable objectif $ y $ est déterminée par 0 ou 1 dans $ m $ data, la vraisemblance $ l = \ prod_ {i = 1} ^ m y_iy_i '+ (1 --y_i) (1 --y_i' ) Envisagez de maximiser $. Il est difficile de comprendre s'il est écrit dans une formule mathématique, mais $ y '$ est la probabilité d'être positif, et si $ y $ est positif à 1, alors $ y' $ est tel quel, et si $ y $ vaut 0, c'est-à-dire négatif. Cela signifie utiliser la probabilité, c'est-à-dire la valeur obtenue en soustrayant $ y '$ de 1. Si vous multipliez $ y '$, qui est l'intervalle de $ (0, 1) $, la valeur deviendra trop petite et difficile à gérer pour l'ordinateur, donc enregistrez la probabilité $ \ log l = \ sum_ {i = 1} ^ m \ log \ left [y_iy_i '+ (1 --y_i) (1 --y_i') \ right] Il est normal de maximiser $.

Cette fois, nous ne traitons pas de négatifs ou de positifs, mais de leur appartenance dans la classe ordonnée. Dans un tel cas, supposons plusieurs courbes logistiques qui ne diffèrent que par le terme constant $ k_0 $, et considérez-les respectivement comme «probabilité de niveau de difficulté 2 ou supérieur», «probabilité de niveau de difficulté 3 ou supérieur», ..., respectivement. La «probabilité de niveau de difficulté 2» est calculée en soustrayant «probabilité de niveau de difficulté 3 ou supérieur» de «probabilité de niveau de difficulté 2 ou supérieur», de sorte que la probabilité peut être calculée à partir de cela.

Commencez par convertir le niveau de difficulté au tenseur du 2e étage au format one-hot pour le calcul.

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.

Donnez ensuite la fonction à minimiser. Puisqu'elle est minimisée, nous définissons la probabilité logarithmique ci-dessus avec un signe moins.

def upperscore(x, sgr_arr):
    x_const = np.append(np.append(oo, x[:18].copy()), -oo) #Insérez l'infini aux deux extrémités pour la probabilité 1 supérieure à 1 et la probabilité 0 supérieure à 20
    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) #Shift et tirer,Retirez la fin pour obtenir la probabilité de chaque difficulté

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

Effectuez une recherche. Veuillez noter que cela prendra beaucoup plus de temps qu'auparavant.

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

Cette fois, STREAM> CHAOS> VOLTAGE> FREEZE> AIR, et vous pouvez voir que le CHAOS a une plus grande influence.

Évaluons cela également.

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
====================

Cette fois, le taux de réponse correcte était de 54,5%. C'est beaucoup mieux qu'avant, mais c'est encore loin.

plt.figure(figsize=(10, 10), dpi=200)
sns.heatmap(pd.DataFrame(test_cm2.table), annot=True, square=True, cmap='Blues')
plt.show()

02.png

C'est généralement en ligne droite, mais les niveaux de difficulté faible et élevé ne sont toujours pas bons.

Conclusion

L'essentiel est: "Vous pouvez obtenir ce genre de valeur, mais ce n'est pas pratique." J'ai fait cette tentative avec l'espoir qu'elle puisse servir de référence pour ajouter de la difficulté à la partition de Step Mania, mais au final il semble qu'il faudra jouer et ajuster.

Une autre chose est que dans la régression logistique, chaque terme constant est naturellement limité par la relation de grandeur, mais dans ce code, en fonction du nombre aléatoire, la contrainte peut ne pas être satisfaite, et une valeur anormale peut entraîner un jugement de convergence [^ lié]. ]. La fonction d'optimisation de SciPy peut donner une contrainte avec une inégalité, mais cela n'a pas fonctionné pour moi car j'ai eu une erreur comme une contrainte d'une forme inconnue a été passée. Le temps de recherche est également perdu, donc si quelqu'un peut le résoudre, j'aimerais demander à un professeur.

[^ lié]: J'ai rencontré un tel phénomène une fois au stade du réglage de la portée réelle.

Recommended Posts