作为一名学音乐的艺术生,为什么平均律现行的是12个音而不是其他的选择,我们需要一个合理的解释。后现代解构主义喜欢把所有的锅都甩给“文化上下文”跟“历史的偶然性”,这是很不负责任的表现。
本文的目的是通过简单的数学和python可视化工具来解释为什么平均律是12个音。以下是正文。
任何数学工具都要从人的需求开始,因此我们先从音乐的角度说人话。
从听感的角度,我们将音程分为两种:
这里我们有了第一个需求:协和音程比不协和音程有更高的优先级,也就是说我们优先考虑让协和音程尽量协和。
因为我们的生理构造以及各种神秘的原因,协和音程对应的都是这种形式的比例:
例如:
from enum import Enum
class INTERVALS(Enum):
P8 = 2/1
P5 = 3/2
P4 = 4/3
M3 = 5/4
m3 = 6/5
因此我们想要一个n-平均律,使得这一平均律最好地照顾到这些协和音程。
本文的目的就是解释为什么当的时候是最优解。
怎么样算“照顾到这些协和音程”?我们需要找到一种衡量n-平均律跟这些协和音程之间的“距离”,来判定n-平均律的优劣。
当我们定义了一个n-平均律,我们将八度分为了n等份,同时也生成了n个音程:
我们想要的就是:对于任何一个协和音程,存在一个n平均律中的音程,使得这两个音程之间的距离尽量小。
例如,我们熟悉的12平均律(),对于纯五度,我们可以找到12平均律中的第7个音程:
而这跟理想中的纯五度
非常接近。
import numpy as np
np.abs(2**(7/12) - 1.5)
0.0016929231233184794
因此定义损失函数非常简单:遍历上文枚举出的协和音程,对于任何一个协和音程,找出在n-平均律中离这一协和音程“距离”最近的一个音程然后计算“距离”。
既然上文提到了“距离”,我们可以很直观地直接用欧氏距离(毕竟从几何到统计学的MSE都在用这个):
也就是说
def vanilla_loss(n):
loss = 0
for interval in INTERVALS:
x = 2**(np.arange(0,n + 1) / n) - interval.value
x = np.abs(x)
x = np.min(x)
x = np.square(x)
loss = loss + x
return np.sqrt(loss)
我们现在来看一下效果。
import matplotlib.pyplot as plt
import pandas as pd
def plot(loss,title):
x = np.arange(2,25)
y = np.array(list(map(loss,x)))
data = pd.DataFrame({'n':x,'loss':y})
fig, ax = plt.subplots()
ax.set_xticks(data['n'].head(20))
ax.bar(data['n'].head(20),data['loss'].head(20))
ax.set(xlabel='n', ylabel='loss',
title=title)
ax.grid()
plt.show()
plot(vanilla_loss,'n-equal temperament, vanilla loss')
从上面已经可以看出来12对应着一个极小值,这也解释了为什么我们会选择12。
然而美中不足的是:19一样也很小,也就是说,19这一竞争者的存在让我们看不到12独特的光辉。
接下来我们可以用一个邪恶的招数:通过修改“比赛规则”让12成为内定的冠军。
我们可以提出第二个(伪)需求:不同的协和音程重要程度也不一样。例如,或许我们觉得顾及纯五度比顾及小三度更加重要,也就是说我们更需要P5精准而不是m3。
从数学的角度来说,我们需要的是加入权重。
权重该如何取值?一个很heuristic也是最为自然的办法就是直接用音程自身的值,因为我们发现协和音程的大小跟重要性呈正比。
例如纯五度比小三度重要,恰好纯五度对应的比小三度对应的要大。
因此公式就成了
def weighted_loss(n):
loss = 0
for interval in INTERVALS:
x = 2**(np.arange(0,n + 1) / n) - interval.value
x = np.abs(x)
x = np.min(x)
x = interval.value * np.square(x)
loss = loss + x
return np.sqrt(loss)
看一下效果:
plot(weighted_loss,'n-equal temperament, weighted loss')
另外一个直觉则是,n-平均律,n越大越不现实,毕竟作曲家和听众都会懵逼:比如如果一个八度有100万个音,那么我们大家都会集体懵逼。因此从regularization的角度来讲,我们可以加入对n的penalty:我们想要的是小的损失函数,但是n也最好不要大。
所以一个最简单的方式就是把这个对n的regularization penalty放到损失函数中,n越大损失越大。
一个最简单且自然的选择就是选择n本身当做系数相乘
def regularized_weighted_loss(n):
loss = 0
for interval in INTERVALS:
x = 2**(np.arange(0,n + 1) / n) - interval.value
x = np.abs(x)
x = np.min(x)
x = interval.value * np.square(x)
loss = loss + x
return n * np.sqrt(loss)
看一下效果:
plot(regularized_weighted_loss,'n-equal temperament, weighted and regularized')
如上,12这个数字成了唯一的最优解。
上文我们考虑了三种情况的损失函数:
剩下的最后一种情况:
也就是
长得是这个样子:
def regularized_unweighted_loss(n):
loss = 0
for interval in INTERVALS:
x = 2**(np.arange(0,n + 1) / n) - interval.value
x = np.abs(x)
x = np.min(x)
x = np.square(x)
loss = loss + x
return n * np.sqrt(loss)
plot(regularized_unweighted_loss,'n-equal temperament, unweighted with regularization')