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.
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.
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
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
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()
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.
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.
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()
C'est généralement en ligne droite, mais les niveaux de difficulté faible et élevé ne sont toujours pas bons.
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.