블로그 디자인

현재 블로그 디자인과 테마를 어떻게 할까 고민 중에 있습니다.

일단 지금은 landscape 테마를 적절히 고쳐 쓸 생각입니다.

유비트 서열표 생성하기

몇 달 전에 유비트 생성기 서열표를 만들었는데, 객관적으로 곡 점수 분포만을 가지고 곡 사이의 서열을 컴퓨터가 매길 수 있는지를 확인하기 위해 만들었습니다. 지금까지 서열표 생성기가 만들어 낸 서열표를 보면 제법 쓸만한 결과가 나옵니다.

유비트 서열표 생성기가 생성한 서열표. Towards the TOWER의 위치가 비정상적으로 높다.

이 서열표를 만들 때 오직 점수만을 사용하기 때문에 최근 곡이나 인기 없는 곡의 난이도가 (초견에 점수가 낮게 나오는 것의 영향을 받아) 높게 평가된다던지, 사람 간의 개인차를 반영하기가 힘들다던지 하는 문제가 있고, 또한 지금까지 점수 데이터가 부족하기 때문에 정확한 결과를 얻기는 힘들지만, 그럼에도 불구하고 나름 결과가 괜찮아서 만족하고 있습니다.


서열표는 매일 임의로 정한 일정한 시각에 DB에 저장된 $(user, song, score)$ 데이터를 가져온 다음, 다음과 같은 과정을 거칩니다.

  1. 점수 normalization
  2. 점수를 기반으로 곡의 난이도 결정
  3. 곡의 난이도를 기반으로 곡의 서열표 상의 레벨 결정

우선 서열표는 보통 특정한 기준(트리플 (98만점), 더블 (95만점), …)에 맞춰 작성되는데, 이는 단 하나의 서열표를 만들기에는 곡별로 성향이 상당히 다르고, 플레이어간에도 목표로 하는 것이 다르기 때문입니다.

예를 들어, AIR RAID FROM THA UNDAGROUND와 Confiserie EXT를 비교해 봅시다. 콩피 익스는 긁기가 주 요소이고, 에레기는 어려운 반복적인 패턴이 주 요소이기 때문에, 더블까지는 에레기가 콩피 익스보다 어렵거나 비슷한 수준입니다. 하지만 에레기는 일단 익숙해지거나 어느 정도 실력이 되면, 정확하면서도 빠르게 긁거나 손을 움직여야 되는 콩피 익스보다는 스코어링이 더 수월하기 때문에, 트리플 난이도는 현재 유비트 삼대장 중 하나이기도 한 콩피 익스에 밀리는 모습을 볼 수 있습니다. 이러한 요소를 1차원상으로 표현하기는 불가능하고, 또한 더블작을 하는 사람과 트리플작을 하는 사람간에 두 곡중 어느 곡을 먼저 파는게 좋을지에 대한 차이가 있기 때문에, 서열표도 여러 개로 나눠놓는 것이 좋습니다.

트리플 서열표를 만드는 것을 생각 해 봤을때, 이 서열표의 기준은 “특정 곡을 트리플찍기 얼마나 쉬운가”, 즉, “특정 곡의 점수를 980000점 이상으로 만드는 것이 얼마나 쉬운가”여야 합니다. 따라서 서열표를 만드는 제일 간단한 기준은 “특정 곡을 트리플찍은 사람의 비율”이라고 할 수 있습니다.

특정 플레이어가 특정 곡을 플레이했을 때의 점수가 트리플 이상이면 +1의 “normalized된 점수”를, 트리플 미만이면 -1을 각 곡별로 부여한 다음, 곡별로 이 normalized된 점수를 평균내어 이것으로 곡의 난이도를 결정하는 것이죠. 아슬아슬하게 역보더, 보더인 경우에 이것을 그대로 -1, +1로 부여하는 것은 너무 극단적일 수 있으니, 적절히 $(score - 980000) / scale$을 sigmoid 함수에 넣은 결과값으로 이를 대체할 수 있습니다. $scale$값은 서열표가 요구하는 점수대에서의 점수 편차가 어느정도인지를 짐작해서 대입합니다. (사실 정확히 하려면 곡별 최대 콤보 수 같은 정보를 이용하는 것이 좋지만, 이 데이터가 주어지지 않았을 때에는 어쩔 수 없습니다.) Sigmoid 함수의 경우에는, tanh, trim (-1과 1 사이에서는 항등함수, 그 밖에서는 -1이나 1) 등등의 함수를 처리 속도를 고려하여 정해 씁니다. 이것이 점수 normalization 과정이고, 이 과정을 거친 뒤에는 어떤 종류의 서열표를 만드는가를 고려할 필요가 없이 점수의 평균만을 고려하면 되는 것입니다.

평균을 이용한 예전 서열표. 그럭저럭 쓸 만 한 것 같다.

하지만 그냥 평균을 내서 서열표를 만들면 몇 가지 문제가 있습니다.

제일 큰 문제는 바로 잘 하는 사람일수록 유비트를 자주하기 때문에, 더 많은 곡을 플레이하고, 또한 신곡을 더 빨리 플레이한다는 것입니다. 따라서 초보자와 고수가 자주 하는 인기 있는 곡이라면 모를까, 신곡이라던가 인기가 많이 없는 곡이면 고수들이 더 많이 플레이하기 때문에 곡의 난이도가 실제보다 저평가될 수 있습니다. 반대의 경우도 생길 수 있는데, 전곡 순회를 한 플레이어가 부족한 상태에서 어떤 초보가 한 곡을 플레이한 유일한 플레이어라면 그 곡의 난이도는 실제보다 고평가될 수 있습니다. 이 외에도 여러가지 문제점이 있는데, 현재 주어진 데이터만으로 해결할 수 없는 문제(구곡은 공략이 많이 되어있어서 난이도가 낮게 나옴, 특정 곡을 플레이 한 사람이 적으면 데이터가 튈 수 있음, 플레이 횟수에 따라 특정 곡에 익숙해짐)를 제외하면 단순히 비율을 따지는 것의 제일 큰 문제는 플레이어간의 개인차를 고려하지 않음이라고 할 수 있습니다.

따라서 플레이어간의 개인차를 고려할 수 있는 방법을 이용해 두 번째 단계에서 곡별 난이도를 계산해야 합니다.


플레이어간의 개인차를 고려할 수 있는 간단한 방법으로는 각 플레이어에 대해 그 플레이어가 플레이 한 곡의 점수를 평균내는 것인데, 이것은 각 플레이별로 플레이한 곡의 난이도(… 이걸 알기 위해 플레이어의 실력을 계산하려는 건데)에 따라 달라지므로 별로 좋은 방법은 아닙니다. 선형대수학적인 방법을 쓰거나 하면 이 쪽으로 접근하는 방식이 나을 수도 있지만 일단 깊게 생각해 보고 있지는 않습니다.

그 다음으로 생각할 수 있는 것은 한 플레이어 안에서 곡별 서열표를 이용해 전체 플레이어에 대한 곡별 서열표를 만드는 것입니다.

$$\text{score_diff}(p, s_i, s_j) : Player \times Song^2 \rightarrow \mathbb{R} = \text{norm_score}(p, s_i) - \text{norm_score}(p, s_j) \\ \text{score_avr}(p, s) : Player \times Song \rightarrow \mathbb{R} = average_{\,s' \in Song \,\wedge\, p \text{ played } s'}(\text{score_diff}(p, s, s')) \\ \text{score_avr}(s) : Song \rightarrow \mathbb{R} = average_{\,p \in Player \,\wedge\, p \text{ played } s}(\text{score_avr}(p, s))$$

이렇게 정의하면, $\text{score_avr}(player, song)$은 특정한 한 플레이어에 대해 특정한 한 곡이 상대적으로 어느 정도로 어려운지를 나타내며, 따라서 이것을 모든 플레이어에 대해 평균 낸 $\text{score_avr}(song)$은 플레이어간의 실력 차이에 상관 없이 그 곡의 난이도가 어느 정도인지를 알 수 있습니다.

참고로 모든 플레이어가 모든 곡을 플레이했다면 식은 이렇게 간소화될 수 있습니다.
$$\begin{aligned} \text{score_avr}(s) &= average_{\,p \in Player}(average_{\,s' \in Song}(\text{score_diff}(p, s, s'))) \\ &= \frac{1}{|Player| \times |Song|} \sum_{p \in Player} \sum_{s' \in Song} \text{norm_score}(p, s) - \text{norm_score}(p, s') \\ &= \frac{1}{|Player|} \sum_{p \in Player} \text{norm_score}(p, s) - C_p \\ &= average_{\,p \in Player}(\text{norm_score}(p, s)) - C \end{aligned}$$

다시 말하자면 전체 곡의 (normalized된) score의 평균만큼의 값을 더하면 위에서 언급한, 단순히 평균을 이용해 곡별 난이도를 매기는 것과 같아집니다. 평균 점수를 이용한 방법과의 차이는 모든 플레이어가 모든 곡을 플레이하지 않았다는 데에서 생기는 것이죠.

현재 유비트 서열표 생성기는 이 식을 이용하여 곡별 난이도를 매기고 있습니다. 다만 이 방법도 문제가 있습니다. 바로 “XX마”의 존재이죠. 만약 한 사람이 거의 모든 곡에 대해 트리플을 찍었다면, 그 곡들간의 점수 차이는 0에 가까워지는데, 이것은 그 곡들의 점수를 0에 가까워지게 만듭니다. 그러므로 그 사람이 만약 10곡을 트리플찍고 다른 곡을 플레이하지 않았다면, 이상적으로는 트리플 서열표에 아무런 영향을 끼치면 안 되는데, 그 10곡이 ±0에 가까워지는, 원하지 않았던 효과가 생겨버리죠.


마지막으로, 서열표 상의 레벨 결정은 서열표의 품질을 결정짓는 제일 중요한 요소는 아니기 때문에, 현재는 단순히 곡별 난이도의 분포의 평균과 표준편차를 이용해 레벨을 결정짓고 있습니다. 추후에 군집화 알고리즘 등을 써서 개선할 수는 있지만, 일단 현재 결과도 그리 나쁘지는 않기 때문에 그냥 두고 있습니다.

만약 이 부분을 개선한다면, 다음과 같은 사항을 고려해야 할 것입니다.

  • 한 구간의 곡의 갯수가 너무 많지 않음
  • 특정 구간(최상단, 최하단)을 제외하고는, 한 구간의 곡의 갯수가 너무 적지도 않음
  • + 구간과 - 구간의 곡의 갯수가 비슷함
  • 한 레벨 안의 곡들끼리는 점수를 달성하는데 필요한 실력이 비슷하고, 레벨이 달라지면 필요한 실력도 차이가 남