chordotomy는 녹음을 듣고 박자마다 코드를 적고, 로마 숫자로 그 코드의 역할을 붙여 준다. 이 글은 그 정확도를 한 단계씩, 측정하면서 올린 이야기다.
어떻게 쟀나
공개 데이터셋 두 개를 mir_eval로 채점했다.
- Tiny AAM: 믹스 음원 20곡. 메이저와 마이너로만 주석이 달려 있다.
- GuitarSet: 솔로 기타 반주 테이크 180개. 세븐스 코드와 재즈 보이싱이 가득하다.
핵심 숫자는 majmin이다. 분석기가 메이저/마이너 3화음을 맞힌 시간의 비율이다. 아래에서 %p는 이 점수의 0.01이다. 이렇게 올라갔다.
| Tiny AAM | GuitarSet | |
|---|---|---|
| 출발점: DSP만 | 0.738 | 0.519 |
| 1. 화이트닝 | 0.788 | 0.619 |
| 2. 어휘 확장 | 0.793 | 0.661 |
| 3. 사전 학습 모델 | 0.913 | 0.787 |
| 4. 베이스 | 0.934 | 0.872 |
| 5. 박자 | 0.950 | 0.900 |
각주 하나. 3단계 뒤에 내 채점 로직에서 버그를 찾았다. Tiny AAM의 마지막 코드를 파일 끝의 무음 구간까지 늘려서 채점하고 있었다. 고치자 분석기는 그대로인데도 Tiny AAM의 코드 점수가 전부 1%p쯤 올랐다. 4단계부터의 Tiny AAM 숫자는 고친 채점 기준이다.
출발점: “코드 없음”이 너무 자주 이겼다
첫 버전은 교과서적인 신호 처리였다. 스펙트럼을 12음 크로마로 접고, 코드마다 템플릿과 비교하고, “코드 없음”용으로 평평한 템플릿을 하나 더 뒀다.
실제 믹스 음원에서는 “코드 없음”이 너무 자주 이겼다. Tiny AAM에서 시간의 12%를 “코드 없음”이라고 했는데, 주석에는 1.5%뿐이었다. 한 곡은 77%였다. 꽉 찬 믹스의 크로마는 3화음 템플릿이 평평한 선을 이길 만큼 깔끔해지지 않는다.
1. 화이트닝: 0.519 → 0.619
해법은 Mauch와 Dixon의 2010년 논문에서 왔다. 스펙트럼의 각 칸을 주변 한 옥타브의 평균 수준과 비교해 보정해서(화이트닝), 음이 주변의 뭉개진 소리에서 도드라지게 한다. 그들의 플러그인은 GPL이라 논문을 보고 직접 구현했다. “코드 없음”도 더는 템플릿이 아니다. 코드가 일정한 기준을 넘어야 인정되고, 시끄러운 박자보다 훨씬 조용한 박자는 “코드 없음”이 된다. 베이스 음역에도 따로 크로마를 두어, 그 음들을 구성음으로 가진 코드에 표를 주게 했다.
이 단계에서 이후에도 계속 쓴 원칙이 생겼다. 개선에는 대가가 있고, 그 대가를 적어 둔다. 합성한 테스트 클립은 반드시 통과해야 해서, 두 박자짜리 코드 변화와 1전위 코드가 살아남아야 한다. 이 제약이 없었다면 Tiny AAM은 0.823 정도까지 갈 수 있었다. 제약을 지키면 0.788이다. 나는 0.788을 골랐다.
2. 어휘 확장: 0.619 → 0.661
여기까지 분석기가 아는 코드 종류는 메이저, 마이너, 도미넌트 세븐스 셋뿐이었다. 나머지는 그중 가까운 것으로 불러야 했고, 자주 틀렸다. 메이저/마이너 세븐스, 마이너 식스, 하프 디미니시드 세븐스, 디미니시드 세븐스, sus4를 추가했다. 템플릿에는 각 음의 배음도 넣었다. 옥타브, 12도, 두 옥타브 위의 음이다. 그래서 3화음 자체의 배음을 세븐스로 착각하지 않게 됐고, 이것만으로 GuitarSet에서 2~3%p를 얻었다. GuitarSet의 majmin은 0.661이 됐고, 세븐스 점수는 0.492에서 0.531로 올랐다.
대가는 Tiny AAM이 치렀다. 메이저와 마이너로만 주석이 달려 있어서, sus4는 틀린 3화음으로, 세븐스는 틀린 세븐스 코드로 채점된다. 근음 정확도가 0.9%p, 세븐스 점수가 1.2%p 내려갔다. 이때부터 두 데이터셋은 역할을 나눴다. GuitarSet의 세븐스 점수는 끌어올릴 목표가 됐고, Tiny AAM의 하한선은 디코더가 새 어휘를 얼마나 쓸 수 있는지의 한계가 됐다.
3. 사전 학습 모델: 0.661 → 0.787
다음으로 DSP를 사전 학습된 코드 인식 모델 lv-chordia(Jiang, Chen, Li, Xia, ISMIR 2019)와 정면으로 비교했다. 두 데이터셋의 모든 지표에서 DSP보다 높았고, 이 표에서 가장 큰 도약이 여기서 나왔다.
받아들일 수 있었던 이유는 두 가지다.
- 라이선스. 가중치가 소스 코드와 같은 저장소에 MIT로 올라가 있다. 가중치의 사용 조건을 밝힐 수 없는 모델은 넣지 않는다.
- 용량. torch만 500MB가 넘는다. 그래서 모델은 선택 설치(extra)로 두고, 기본 설치와 DSP 경로에서는 torch를 아예 불러오지 않는다. 모델 없이
pip install chordotomy만 해도 돌아간다.
README에 모델이 무엇으로 학습됐는지 적어 두어서, 쓰고 싶지 않은 사람은 빼고 쓸 수 있다.
4. 베이스: 0.787 → 0.872
chordotomy는 코드마다 베이스도 읽어서, C/E 같은 슬래시 코드를 그대로 적는다. 그 베이스는 단순한 DSP 규칙으로 골랐는데, 음이 몰리는 구간에서는 엉뚱한 저음을 집었다. 코드 밖의 베이스는 그 코드에 음 하나가 더 붙은 것으로 채점되니, 가짜 슬래시 코드가 몰래 코드 정확도를 깎고 있었다.
두 가지를 바꿨다. 코드의 박자들 사이에서 베이스 투표가 비기면 이제 코드 구성음을 고른다. 그리고 알고 보니 lv-chordia에는 베이스만 따로 내는 출력이 있었는데, 내 구현은 그걸 계산만 하고 읽지 않았다. 이제 코드 밖의 베이스는 그 출력으로 판정한다.
GuitarSet의 majmin은 0.872로, 세븐스 점수는 0.819로 올랐지만, 이건 코드를 더 잘 들어서가 아니다. 코드 이름은 그대로고, 잘못 붙던 음이 빠졌을 뿐이다. 베이스 자체는 실제로 나아졌다. 베이스가 맞은 박자의 비율이 0.49에서 0.61로 올랐다. 대가는 진짜 전위 일부였다. 전위 재현율이 최대 10분의 1까지만 떨어지도록 허용했고, 베이스 출력을 쓰자 0.227에서 0.208로 떨어졌다. 이 버그는 곡 하나로 찾아서 따로 글을 썼다. 모델은 이미 알고 있었다.
5. 박자: 0.872 → 0.900
lv-chordia는 짧은 프레임마다 라벨을 붙이고, chordotomy는 그 라벨을 박자에 맞춘다. 그런데 박자에 맞춘 결과가 모델의 원래 프레임 출력보다 Tiny AAM에서 1.3%p, GuitarSet에서 2.1%p 낮았다. 박자에 맞추는 것 자체가 문제일까?
아니었다. 같은 방식으로 데이터셋에 적힌 정답 박자에 맞추면 오히려 원래 모델보다 높았다. 손해는 librosa 박자 추적기에서 나왔고, 이 추적기는 GuitarSet 180개 테이크 중 72개만 박자를 제대로 잡았다.
Beat This!(Foscarin, Schlüter, Widmer, ISMIR 2024)는 박자를 훨씬 잘 잡지만, 자체적으로 찍은 박자는 간격이 고르지 않다. 한 마디에 한 번 치는 코드 위에서는 박자 사이가 최대 1.84초까지 벌어진다. 그래서 템포를 일정하게 유지하는 librosa의 추적기는 그대로 두고, librosa의 onset 강도 대신 Beat This!의 박자 확률을 넣었다.
envelope = beats.activation(y) # Beat This!'s beat probability per frame
tempo, frames = librosa.beat.beat_track(
onset_envelope=envelope, sr=SR, hop_length=HOP, trim=False
)
GuitarSet의 박자 정확도(beat F)는 0.517에서 0.919로 올랐다. 이제 박자에 맞춘 코드가 두 데이터셋 모두에서 원래 모델을 앞선다. Tiny AAM에서 0.947 대비 0.950, GuitarSet에서 0.893 대비 0.900이다.
이것도 공짜는 아니었다. 180개 테이크 중 21개는 majmin이 내려갔는데, 가장 크게 내려간 건 Beat This!가 절반 템포로 잡은 빠른 재즈 테이크들이다. 전위 재현율도 0.191로, 4단계에서 정한 하한선 아래로 내려갔다. 원인을 추적해 보니 박자가 정답 박자 쪽으로 수십 밀리초씩 옮겨진 탓이라, 받아들였다.
공정성 확인 하나. Beat This!는 GuitarSet으로 학습했기 때문에, 여기 GuitarSet 숫자는 채점하는 테이크로 학습하지 않은 체크포인트로 잰 값이다. 다만 그 체크포인트도 같은 곡의 다른 테이크는 들어 본 적이 있다. 실제로 배포하는 모델은 180개 테이크 전부로 학습했고, 0.902가 나온다.
무엇이 통했나
- 고치기 전에 잰다. 박자 단계는 “박자에 맞추는 게 얼마나 손해인가?”라는 질문에서 시작했고, 답은 전혀 다른 곳을 가리켰다.
- 대가를 적어 둔다. 모든 단계가 무언가를 내줬다. 0.823 대신 0.788, Tiny AAM의 근음 대신 GuitarSet의 세븐스, 전위 일부 대신 가짜 슬래시 감소. 거래마다 하한선과 이유를 문서에 남겼다.
- 점수가 아닌 조건. 합성 테스트 클립에는 절대 깨지면 안 되는 것들을 담았다. 네 박자 동안 유지되는 진짜 슬래시 코드 같은 것.
- 데이터셋은 거의 없는 걸 보여주지 못한다. GuitarSet에는 7sus4가 시간의 0.07%뿐이고 Tiny AAM에는 아예 없어서, 모델이 거기서 7sus4를 한 번도 내지 않았다. 그래서 7sus4를 전부 sus4로 바꾸던 버그는 점수를 하나도 움직이지 않았다. 실제 곡이 몇 분 만에 찾아냈다.
모델을 쓰고 싶지 않은 사람을 위해 DSP 엔진도 그대로 있다. 점수는 Tiny AAM 0.803, GuitarSet 0.696이다. 두 엔진 모두 PyPI의 한 패키지에 들어 있고(uv tool install "chordotomy[model]"), 뷰어에서 녹음을 코드와 함께 재생해 볼 수 있다.