5D Library › 心理物理学、韦伯-费希纳定律与多感官贝叶斯整合的数学模型 · English
第八章 实践与预注册试验
注
本章的核查表用于设计与评估学习材料、教学和产品,不是医疗建议。 它不能用来判断任何人的听力、视力、前庭、嗓音或神经功能;如有这方面的症状,请就医。试验方案包含持续的发声练习与前臂振动,方案已写明嗓音负荷与振动的安全条款,并把有嗓音疾病等情况的人排除在外。
第一部分 四步多感官精度核查表
设计一节技能课、一段演讲、一个教学视频或一件产品时,都可以依次问四个问题。
- 主通道噪声评估与高危单通道降级识别。 学习者或用户主要依赖哪一种感觉?在真实使用条件下(视觉杂乱、背景噪声、疲劳、分心),这种感觉的可靠性会下降多少?如果一个通道承担了绝大部分「权重」,而它恰好在关键时刻可能变差,就是一个高危的单通道设计,需要准备一个可靠的备份通道(第三章)。
- 微弱信号区的跨通道同步强化。 主通道的信号是否微弱到难以分辨?如果是,同时、同地加入第二种感觉的信号,预期收益可以按两个通道的精度合成来估计。不要假设「信号越弱,加第二通道的收益越大」:超加性主要见于单个神经元在弱刺激下的反应,行为上视听言语的增益在中等信噪比最大(第五章)。
- 视听时间同步窗口对齐。 声音与画面(或振动)之间的时间差是否在可接受的范围内?框架中写的是「约 ±100 毫秒」。核对后应改为不对称的窗口:声音比画面提前时更容易被察觉。
- 主动探索与被动接收审计。 在全部练习时间中,有多大比例是学习者自己发起动作、控制探索的,有多大比例只是看和听?主动产生的动作提供了已知的输入,是建立「动作—感觉」对应关系的条件(第六章)。
证据
声音与画面能错开多少。 国际电信联盟 1998 年的广播建议书根据在日本、瑞士与澳大利亚进行的观众主观评价,给出声音与画面时间差的阈值:可察觉阈约为声音提前 45 毫秒、延后 125 毫秒;可接受阈约为声音提前 90 毫秒、延后 185 毫秒 (ITU-R 1998)。在言语融合的实验中,声音提前约 30 毫秒到延后约 170 毫秒的范围内融合都很普遍(第四章)(Wassenhove et al. 2007)。
证据
时间窗口可以训练。 鲍尔斯、希洛克与华莱士 2009 年让受试者做带反馈的视听同时性判断训练,判断「同时」的时间窗口宽度缩小了约 40% (Powers et al. 2009)。这说明窗口不是固定的生理常数;也说明不同经验的人窗口宽度可能不同,核查表中的数值只能作为起点。
把核查表用于三个虚构的例子:一段在线发音教学视频,一辆车的车道偏离提示,以及一个动作技能学习应用。各通道的标准差与 都是假设值;第一步中「一个通道超过 80% 的权重」与第四步中「主动练习少于 50%」两条标线是作者为示例设定的,不是已发表的标准。
推导
# Four-step multisensory precision checklist applied to three FICTIONAL cases.
# Step 1: reliability weights (MLE); flag a channel carrying > 80 % of the weight, and the
# loss if it is degraded 3-fold (e.g. vision under visual clutter).
# Step 2: weak signals: combined d' = sqrt(d1^2 + d2^2) for independent channels.
# Step 3: sound-picture offset against ITU-R BT.1359 detectability (sound early <= 45 ms,
# late <= 125 ms); for speech, fusion window about -30 to +170 ms (van Wassenhove 2007).
# Step 4: share of practice the learner steers. The 50 % line is an ILLUSTRATIVE choice.
import math
def mle(sds):
w = [1 / s ** 2 for s in sds.values()]
return {k: v / sum(w) for k, v in zip(sds, w)}, 1 / math.sqrt(sum(w))
CASES = {
"A online pronunciation video": dict(
sds={"hearing": 1.0, "lips": 1.5}, dprime={"hearing": 0.8, "lips": 0.6},
offset=+160, speech=True, active=0.2),
"B dashboard lane alert": dict(
sds={"icon": 1.0}, dprime={"icon": 1.2}, offset=None, speech=False, active=None),
"C motor-skill app": dict(
sds={"video": 1.2, "hearing": 1.5, "wrist buzz": 2.0},
dprime={"video": 2.0, "hearing": 1.5, "wrist buzz": 1.0},
offset=-40, speech=False, active=0.6),
}
for name, c in CASES.items():
print(name)
w, sd = mle(c["sds"])
top = max(w, key=w.get)
worse = dict(c["sds"]); worse[top] *= 3
flag = "FLAG single channel" if w[top] > 0.8 else "ok"
print(f" 1 weights {', '.join(f'{k} {v:.2f}' for k, v in w.items())}: {flag}")
print(f" combined sd {sd:.2f}; if {top} gets 3x noisier: {mle(worse)[1]:.2f}")
ds = list(c["dprime"].values())
best, comb = max(ds), math.sqrt(sum(d * d for d in ds))
print(f" 2 best single d' {best:.1f}, combined {comb:.2f}"
+ (" (no second channel)" if len(ds) == 1 else f" (x{comb / best:.2f})"))
off = c["offset"]
if off is None:
print(" 3 one channel: no timing to check")
else:
ok = -45 <= off <= 125
print(f" 3 sound {'late' if off > 0 else 'early'} by {abs(off)} ms:"
f" {'within' if ok else 'OUTSIDE'} the -45/+125 ms detectability limits")
if c["speech"]:
fuse = -30 <= off <= 170
where = "inside" if fuse else "outside"
print(f" speech fusion window -30/+170 ms: {where}")
a = c["active"]
if a is None:
print(" 4 not applicable")
else:
print(f" 4 learner steers {a:.0%} of practice:"
f" {'ok' if a >= 0.5 else 'FLAG mostly passive'}")
# A online pronunciation video
# 1 weights hearing 0.69, lips 0.31: ok
# combined sd 0.83; if hearing gets 3x noisier: 1.34
# 2 best single d' 0.8, combined 1.00 (x1.25)
# 3 sound late by 160 ms: OUTSIDE the -45/+125 ms detectability limits
# speech fusion window -30/+170 ms: inside
# 4 learner steers 20% of practice: FLAG mostly passive
# B dashboard lane alert
# 1 weights icon 1.00: FLAG single channel
# combined sd 1.00; if icon gets 3x noisier: 3.00
# 2 best single d' 1.2, combined 1.20 (no second channel)
# 3 one channel: no timing to check
# 4 not applicable
# C motor-skill app
# 1 weights video 0.50, hearing 0.32, wrist buzz 0.18: ok
# combined sd 0.85; if video gets 3x noisier: 1.14
# 2 best single d' 2.0, combined 2.69 (x1.35)
# 3 sound early by 40 ms: within the -45/+125 ms detectability limits
# 4 learner steers 60% of practice: ok发音教学视频:听觉与看嘴型的权重为 0.69 与 0.31,不是单通道设计;但如果学习者在嘈杂环境中观看,听觉变差三倍,合成的标准差从 0.83 升到 1.34。两个弱信号合起来 从 0.8 升到 1.00。问题在第三步:蓝牙耳机等造成声音晚到 160 毫秒,超出了可察觉阈,观众会注意到口型对不上;它仍在言语融合的窗口之内,所以不一定破坏融合,但会分散注意力,应当在制作或播放端校正。第四步:学习者只有 20% 的时间在自己发音,其余都在看和听,被标记为过于被动。
车道偏离提示:只有一个视觉图标,权重 1.00,被标记为高危单通道。驾驶时视觉负荷高,图标的可靠性下降三倍时,没有任何备份。加一个声音或座椅振动提示,就是第一步与第二步共同建议的修改。
动作技能应用:视频、声音与手腕振动的权重为 0.50、0.32 与 0.18;视频变差三倍时,合成的标准差只从 0.85 升到 1.14,有「平稳降级」的能力。声音比画面早 40 毫秒,在可察觉阈之内;学习者主动练习的比例为 60%。
第二部分 预注册随机对照试验方案
问题
成人初学者练习模仿音高与元音时,在相同的视听条件下加上「把实时音高或共振峰误差编码为振动」的触觉反馈,与佩戴同款但不振动的设备相比,能否降低 24 小时后、没有任何反馈时模仿音高的误差?
框架中的问题是「复杂发音/动作模仿学习」。本方案选择发音:音高(基频)与元音的共振峰都可以从录音中客观、自动地测量,测量方法有成熟的算法(见下面的程序)。发音任务与作者的声乐训练业务更接近,所以本方案加强了独立性的安排(见伦理节)。
主要结局定在撤去反馈之后,是有意的。运动学习研究中的「指导假说」指出:练习时频繁、即时的外加反馈可以提高练习当时的表现,却可能使学习者依赖它,撤去反馈后的保持反而变差 (Salmoni et al. 1984)。所以本试验不看训练当时唱得准不准,而看 24 小时后不戴设备、不给反馈时的表现。
这项试验检验第三章假说框中的想法,与第 10 本(发声的生物力学)、第 23 本(刻意练习)、第 29 本(听觉—运动反馈)的主题相邻,只作交叉引用,不重复那几本的内容。
测量方法
每个试次,参与者先从耳机听到一个约 1.5 秒的目标音(与本人性别相应音区的合成人声,元音 /a/),提示音后用 /a/ 唱出同一个音高,持续约 2 秒。取每个试次中间稳定的 1 秒,用 YIN 算法估计基频 (Cheveigné and Kawahara 2002),取中位数,与目标音比较:
其中 是目标音的基频, 以音分计,100 音分是一个半音。这一测法(取稳定的中段、取中位数、换算为音分、再取绝对值的平均)与第 10 本第八章的音准评估相同,那里用 Praat 读取基频;本书不重复,只补上基频与共振峰估计算法本身的示意。共振峰用线性预测(LPC)包络的峰估计 (Makhoul 1975),元音误差为 F1 与 F2 各自以半音计的偏差的欧氏距离。下面的程序只用标准库,演示这两步的原理:先合成一个已知基频与共振峰的元音,再把它们估计回来。
推导
# How the Chapter 8 outcomes are measured: a SIMPLIFIED sketch, not lab software.
# A vowel is synthesised (pulse train -> glottal low-pass -> 4 formant resonators,
# sampling rate 10 kHz); f0 is recovered with YIN (de Cheveigne & Kawahara 2002:
# difference function, cumulative-mean normalisation, threshold 0.1, parabolic
# interpolation) and F1/F2 from the peaks of an order-12 LPC envelope (autocorrelation
# method, Levinson-Durbin; Makhoul 1975). Pitch error in cents = 1200 log2(f0 / target).
# Formant distance = Euclidean distance of F1 and F2 in semitones (st). Formant values and
# the "sung" errors are ILLUSTRATIVE; a real trial would use validated software.
import cmath, math, random, statistics
FS = 10000
def vowel(f0, formants, rng, dur=0.25):
n = int(FS * dur); x = [0.0] * n; t = 0.0
while t < n:
x[int(t)] = 1.0; t += FS / f0
for _ in range(2): # glottal roll-off
y = 0.0
for i in range(n):
y = 0.9 * y + x[i]; x[i] = y
for f, bw in formants: # cascade of resonators
r = math.exp(-math.pi * bw / FS); c = 2 * r * math.cos(2 * math.pi * f / FS)
y1 = y2 = 0.0
for i in range(n):
y = x[i] + c * y1 - r * r * y2; y2, y1 = y1, y; x[i] = y
peak = max(abs(v) for v in x)
return [v / peak + rng.gauss(0, 0.002) for v in x]
def yin(x, start, fmin=70, fmax=600, w=300, thr=0.1):
tmin, tmax = int(FS / fmax), int(FS / fmin) + 1
d = [0.0] + [sum((x[start + j] - x[start + j + t]) ** 2 for j in range(w))
for t in range(1, tmax + 2)]
cm, run = [1.0], 0.0
for t in range(1, len(d)):
run += d[t]; cm.append(d[t] * t / run)
t = next((t for t in range(tmin, tmax) if cm[t] < thr), None)
if t is None:
t = min(range(tmin, tmax), key=lambda k: cm[k])
while t + 1 < tmax and cm[t + 1] < cm[t]:
t += 1
a, b, c = cm[t - 1], cm[t], cm[t + 1]
return FS / (t + 0.5 * (a - c) / (a - 2 * b + c))
def lpc_formants(x, start, p=12, w=300):
s = [x[start + i] - 0.97 * x[start + i - 1] for i in range(w)]
s = [v * (0.54 - 0.46 * math.cos(2 * math.pi * i / (w - 1))) for i, v in enumerate(s)]
r = [sum(s[i] * s[i + k] for i in range(w - k)) for k in range(p + 1)]
a, e = [1.0] + [0.0] * p, r[0]
for i in range(1, p + 1): # Levinson-Durbin recursion
k = -sum(a[j] * r[i - j] for j in range(i)) / e
a = [a[j] + k * a[i - j] if 0 < j < i else a[j] for j in range(p + 1)]
a[i] = k; e *= 1 - k * k
def gain(f):
w0 = -2j * math.pi * f / FS
return 1 / abs(sum(aj * cmath.exp(w0 * j) for j, aj in enumerate(a)))
fs_ = list(range(150, 4000, 5)); g = [gain(f) for f in fs_]
peaks = [fs_[i] for i in range(1, len(g) - 1) if g[i - 1] < g[i] > g[i + 1]]
return peaks[0], peaks[1]
def semis(a, b):
return 12 * math.log2(a / b)
if __name__ == "__main__":
rng = random.Random(34)
A = [(700, 80), (1200, 90), (2500, 120), (3500, 150)] # illustrative /a/-like vowel
comfort = 150.0 # participant's comfortable f0
sung_err = [+35, -20, +60, -45, +10] # illustrative "true" errors
print("target steps true err YIN f0 measured err (cents)")
meas = []
for st, err in zip((-4, -2, 0, 2, 4), sung_err):
target = comfort * 2 ** (st / 12); sung = target * 2 ** (err / 1200)
x = vowel(sung, A, rng)
f0 = statistics.median(yin(x, s) for s in (800, 1100, 1400)) # stable middle
meas.append(1200 * math.log2(f0 / target))
print(f"{target:6.1f} {st:+4d} {err:+8d} {f0:6.1f} {meas[-1]:+8.1f}")
print(f"mean absolute error: true {statistics.mean(map(abs, sung_err)):.1f},"
f" measured {statistics.mean(map(abs, meas)):.1f} cents")
print("formants of the same vowel (true F1 700, F2 1200) as f0 rises:")
for f0 in (110, 220, 330, 440):
f1, f2 = lpc_formants(vowel(f0, A, rng), 1200)
dist = math.hypot(semis(f1, 700), semis(f2, 1200))
print(f" f0 {f0:3d} Hz: LPC F1 {f1:4d}, F2 {f2:4d}; distance {dist:4.2f} st")
# target steps true err YIN f0 measured err (cents)
# 119.1 -4 +35 121.6 +36.1
# 133.6 -2 -20 132.0 -21.0
# 150.0 +0 +60 155.4 +61.6
# 168.4 +2 -45 164.0 -46.0
# 189.0 +4 +10 189.9 +8.1
# mean absolute error: true 34.0, measured 34.6 cents
# formants of the same vowel (true F1 700, F2 1200) as f0 rises:
# f0 110 Hz: LPC F1 690, F2 1200; distance 0.25 st
# f0 220 Hz: LPC F1 665, F2 1175; distance 0.96 st
# f0 330 Hz: LPC F1 665, F2 1225; distance 0.96 st
# f0 440 Hz: LPC F1 455, F2 895; distance 9.02 st程序中的五个目标音设在本人舒适音高(示例为 150 赫兹)上下 2 个与 4 个半音,「唱出」的误差是设定的示意值。YIN 估计的误差与设定值相差 1.0 至 1.9 音分(来自合成与估计两方面的简化,例如合成时脉冲只能落在整数采样点上),五个试次的平均绝对误差为 34.6 对真实的 34.0 音分,远小于下文的最小关注差异 20 音分。共振峰则不同:基频为 110 赫兹时,LPC 估计的 F1/F2 只偏 0.25 个半音;基频升到 440 赫兹时,谐波稀疏,包络的峰被谐波「吸走」(F1 455、F2 895 赫兹,接近 440 与 880 赫兹的谐波),距离达 9.02 个半音。所以本方案把共振峰只作为次要结局,元音试次一律在本人舒适音高唱,并预先规定估计值由不知分组的人员对照语图抽查。正式试验使用经过验证的语音分析软件,并在入组前公开全部参数。
设计
| 要素 | 规定 |
|---|---|
| 参与者 | 18 至 45 岁成人,经与作者无关的渠道招募,不从作者的学员、社群、客户或牌卡用户中招募;排除:两年以上系统的声乐或嗓音训练、职业歌手或职业嗓音使用者、筛查发现的听力损失、目前的嗓音疾病或持续两周以上的声音嘶哑、喉部手术史、神经或运动障碍、前臂或腕部皮肤破损或感觉障碍、植入式电子医疗设备;测试当天有急性上呼吸道感染者改期 |
| 任务 | 音高试次:五个目标音,设在本人舒适音高上下 2 个与 4 个半音,用元音 /a/ 模仿;元音试次:两个参与者母语中没有的元音,目标 F1/F2 取同性别示范者的录音值,并按本人基线时 /i/、/a/、/u/ 的共振峰作个体缩放(方法入组前登记);所有参与者训练前先完成一次无反馈的基线测验 |
| 随机分组 | 个体按 1:1 随机分配,按性别与基线音高误差是否达到 100 音分分层;分配序列由与招募无关的人员用计算机生成,在完成基线测验后才显示 |
| 两组相同的条件 | 同一副耳机播放同样的目标音,参与者听到自己的自然声音(不加掩蔽或变调);屏幕只显示目标符号与倒计时,两组都不显示实时音高曲线,训练中与训练后都不给分数;双侧前臂靠近手腕处各戴一条同款振动带,设备通电、指示灯相同 |
| A 组(触觉误差反馈) | 音高试次中,设备实时估计基频:偏低时左侧振动、偏高时右侧振动,误差在 ±15 音分以内不振动,超出后脉冲频率随误差增大(100 音分及以上为最高档);元音试次中,两侧同时振动,脉冲频率随 F1/F2 距离增大,1 个半音以内不振动;设备日志记录从声音到振动的延迟 |
| B 组(对照) | 佩戴同款振动带,设备通电但不振动;其余一切与 A 组相同 |
| 佩戴部位 | 选前臂靠近手腕处,理由:双手与面部不受妨碍;不放在颈部,以免与发声时喉部自身的振动混在一起,也避免压迫颈部或被话筒拾取;已有研究把实时音高信息编码为前臂上的振动,在模拟人工耳蜗听觉的正常听力者中提高了音高辨别 (Fletcher et al. 2020) |
| 训练量 | 同一天 3 段,每段 10 分钟,段间休息 5 分钟;每段音高试次与元音试次的数目、顺序两组相同 |
| 盲法 | 参与者无法对是否振动设盲,只被告知研究比较两种腕带设置,不被告知研究假设;24 小时测试时取下振动带,测试程序完全相同,由不知分组的人员主持;结局由预先写好的程序自动计算;分析者在锁定数据与代码前不知分组 |
| 主要结局 | 训练后 24 小时(±2 小时),不戴设备、不给任何反馈,五个目标音各唱 4 次(共 20 个试次,随机顺序);结局为 20 个试次音高误差绝对值 的平均(音分),越低越好 |
| 次要结局 | 24 小时的元音 F1/F2 距离(半音);训练结束时的即时误差;7 天后的误差;同一目标音重复唱的一致性(有符号误差的标准差);未训练的目标音(舒适音高上下 1 个与 3 个半音)的误差;误差在 ±50 音分以内的试次比例;嗓音不适与发声费力的自评 |
| 分析 | 协方差分析:因变量为主要结局,自变量为分组,协变量为基线误差与分层变量;报告 B − A 的差及其 95% 置信区间,正值表示 A 组更准确;双侧 。音高误差的分布右偏,预先登记一项对数尺度上的敏感性分析,只作报告,不改变结论 |
选择「音分绝对误差的平均」作为唯一的主要结局,理由是:它是歌唱研究中常用的模仿准确度指标,用 YIN 等算法自动计算,不需要评分者 (Hutchins et al. 2014);它是连续量,比「±50 音分以内的比例」这类二分指标保留更多信息,对已经唱得比较准的人也不会碰到天花板。共振峰的测量受基频影响(见上面的程序),所以放在次要结局。
预试验与样本量重估
作者没有找到可以直接借用的「振动反馈训练音高」的效应量,人与人之间误差的标准差也只能从已发表研究的平均值粗略推算。已发表研究报告的大多是平均值:78 名成人模仿单个音高的平均绝对误差约 66 音分 (Demorest and Pfordresher 2015);普福德雷舍与布朗 2007 年的第二个实验中,45 人里有 7 人(16%)属于唱不准的人,他们的平均绝对误差为 229 音分,配对的 7 名唱得准的人约 30 音分 (Pfordresher and Brown 2007)。另一项研究中,22 名受试者用嗓音模仿五个目标音的平均绝对误差约 38 音分 (Hutchins et al. 2014)。这样的分布严重右偏:少数唱不准的人把平均值与标准差都拉高了。第 10 本第八章的试验用的是同一种结局(五个目标音的平均绝对音高误差),当时假设个体间标准差为 20 音分,并计划由预试验核对;按上面这些数字,在未经挑选的成年初学者中,这个假设很可能偏低,所以本书取较保守的计划值(见下),并交给预试验重估。
所以方案分两步。第一步是一项 n = 20 的小型外部预试验(每组 10 人,方案与正式试验相同),用来检验流程,并估计 24 小时误差的标准差与它和基线的相关。预试验的数据不进入主分析。第二步按预先写成代码的规则重新计算正式试验的样本量:
- 标准差取预试验合并组内标准差(自由度 18)的单侧 80% 置信上限 (Browne 1995)。西姆与刘易斯 2012 年也用置信上限的思路,但取 95% 上限,并主张按估计精度与总效率来定预试验的规模;他们的分析建议预试验总共约 55 人以上 (Sim and Lewis 2012)。本方案按作者的决定只做 20 人,代价是置信上限把标准差放大 1.183 倍,样本量放大约 1.40 倍。
- 与基线的相关取预试验的估计值,但不高于计划值 0.6、不低于 0。
- 效应量不由预试验估计。 最小关注差异事先固定为 20 音分(约五分之一个半音),不随预试验改变。每组 10 人时,两组差值的标准误约 27 音分,比最小关注差异还大;用这样的估计来定样本量,结果会随机地偏大或偏小,而且只在预试验「看起来有效」时才继续,会使后续研究系统地高估效应、低估所需人数 (Kraemer et al. 2006)。所以预试验的统计人员只报告合并标准差与相关这两个数,不计算、不报告两组的差。
- 最终每组可分析人数限定在 150 至 350 之间:下限防止一个偶然偏小的预试验标准差使试验规模过小;上限是可行性的约束。如果规则给出的数超过上限,试验按 350 人进行,并在登记文件中写明在预试验数值下实际的把握度低于 90%。
推导
# Sample size for the Chapter 8 trial, with a pre-registered re-estimation rule.
# Primary outcome: mean absolute pitch error (cents) when imitating 5 target notes,
# 24 h after training, without feedback. Planning values, all ASSUMED:
# SD between people 75 cents (published means only; see the check below),
# correlation with baseline r 0.6, smallest effect of interest (SESOI) 20 cents,
# two-sided alpha 0.05, 90 % power, 15 % loss. ANCOVA on the baseline shrinks the
# variance by (1 - r^2) (Borm, Fransen & Lemmens 2007).
# Pilot rule: external pilot, n = 20 (10 per arm), never in the main analysis. An
# independent statistician reports only the pooled within-arm SD (df 18) and r.
# SD used = one-sided 80 % upper confidence limit (Browne 1995); r used = pilot r, but
# never above the planned 0.6 nor below 0. SESOI stays 20: the pilot never estimates
# the effect. Final n is clamped to [150, 350] analysable per arm.
import math, random
from statistics import NormalDist
N = NormalDist()
SESOI, LOSS, N_MIN, N_MAX = 20.0, 0.15, 150, 350
def per_arm(sd, delta, r, alpha=0.05, power=0.90):
z = N.inv_cdf(1 - alpha / 2) + N.inv_cdf(power) # +1 below ~ t correction z_a^2/4
return math.ceil(2 * z ** 2 * sd ** 2 * (1 - r ** 2) / delta ** 2) + 1
def chi2_cdf(x, k): # regularised lower gamma P(k/2, x/2), series
a, y = k / 2, x / 2
term = total = 1 / a
for i in range(1, 500):
term *= y / (a + i); total += term
return total * math.exp(-y + a * math.log(y) - math.lgamma(a))
def chi2_ppf(p, k):
lo, hi = 1e-9, 10 * k + 100
for _ in range(100):
mid = (lo + hi) / 2
lo, hi = (mid, hi) if chi2_cdf(mid, k) < p else (lo, mid)
return (lo + hi) / 2
def reestimate(s, r, df=18, conf=0.80):
s_up = s * math.sqrt(df / chi2_ppf(1 - conf, df))
r_use = min(max(r, 0.0), 0.6)
n = per_arm(s_up, SESOI, r_use)
return s_up, r_use, n, min(max(n, N_MIN), N_MAX)
def simulate(rng, n, true_diff, sd=75.0, r=0.6, sims=3000):
benefit = strongest = 0
for _ in range(sims):
arms = []
for shift in (0.0, -true_diff): # arm A lowers the error
xs = [rng.gauss(0, sd) for _ in range(n)]
ys = [r * x + rng.gauss(shift, sd * math.sqrt(1 - r * r)) for x in xs]
arms.append((xs, ys))
cx = [[x - sum(xs) / n for x in xs] for xs, _ in arms] # centre within arms
cy = [[y - sum(ys) / n for y in ys] for _, ys in arms]
b = sum(x * y for a, c in zip(cx, cy) for x, y in zip(a, c)) \
/ sum(x * x for a in cx for x in a)
adj = [sum(y - b * x for x, y in zip(xs, ys)) / n for xs, ys in arms]
d = adj[0] - adj[1] # B minus A: positive = A better
rss = sum((y - b * x) ** 2 for a, c in zip(cx, cy) for x, y in zip(a, c))
lo = d - 1.96 * math.sqrt(rss / (2 * n - 3) * 2 / n)
benefit += lo > 0; strongest += lo >= SESOI
return benefit / sims, strongest / sims
if __name__ == "__main__":
p, good, poor = 7 / 45, 30.0, 229.0 # Pfordresher & Brown 2007, Exp. 2
print(f"check: {p:.0%} poor-pitch singers (229 c), rest 30 c -> mean"
f" {p * poor + (1 - p) * good:.0f} c; split alone gives SD"
f" {math.sqrt(p * (1 - p)) * (poor - good):.0f} c")
n = per_arm(75, SESOI, 0.6); rec = math.ceil(n / (1 - LOSS))
print(f"plan: SD 75, r 0.6, SESOI 20 (d = {20 / 75:.2f}), 90 % power: {n} per arm,"
f" recruit {rec} ({2 * rec})")
print("analysable per arm SESOI 15 20 30")
for sd in (60, 75, 90):
for r in (0.4, 0.6):
print(f" SD {sd}, r {r:.1f} {'':8s}"
+ "".join(f"{per_arm(sd, dl, r):7d}" for dl in (15, 20, 30)))
se = 75 * math.sqrt(2 / 10) * math.sqrt(1 - 0.36)
print(f"pilot of 10 per arm: SE of an arm difference about {se:.0f} c (SESOI 20)")
k = math.sqrt(18 / chi2_ppf(0.20, 18))
print(f"80 % upper limit with df 18: SD x {k:.3f}, so n x {k * k:.2f}")
print("pilot SD, r -> SD used, r used -> n per arm -> clamped -> recruit (all)")
for s, r in ((50, 0.65), (60, 0.70), (75, 0.60), (90, 0.45)):
s_up, r_lo, raw, n2 = reestimate(s, r)
rec2 = math.ceil(n2 / (1 - LOSS))
print(f" {s:4d}, {r:.2f} -> {s_up:5.1f}, {r_lo:.2f} -> {raw:4d} -> {n2:3d}"
f" -> {rec2} ({2 * rec2})")
rng = random.Random(2026)
for true in (0.0, 20.0, 40.0):
p_any, p_top = simulate(rng, n, true)
print(f"true benefit {true:4.1f}: P(some benefit verdict) {p_any:.3f},"
f" P(benefit >= SESOI shown) {p_top:.3f}")
# check: 16% poor-pitch singers (229 c), rest 30 c -> mean 61 c; split alone gives SD 72 c
# plan: SD 75, r 0.6, SESOI 20 (d = 0.27), 90 % power: 191 per arm, recruit 225 (450)
# analysable per arm SESOI 15 20 30
# SD 60, r 0.4 284 160 72
# SD 60, r 0.6 217 123 55
# SD 75, r 0.4 443 250 112
# SD 75, r 0.6 338 191 86
# SD 90, r 0.4 637 359 160
# SD 90, r 0.6 486 274 123
# pilot of 10 per arm: SE of an arm difference about 27 c (SESOI 20)
# 80 % upper limit with df 18: SD x 1.183, so n x 1.40
# pilot SD, r -> SD used, r used -> n per arm -> clamped -> recruit (all)
# 50, 0.65 -> 59.2, 0.60 -> 119 -> 150 -> 177 (354)
# 60, 0.70 -> 71.0, 0.60 -> 171 -> 171 -> 202 (404)
# 75, 0.60 -> 88.7, 0.60 -> 266 -> 266 -> 313 (626)
# 90, 0.45 -> 106.5, 0.45 -> 477 -> 350 -> 412 (824)
# true benefit 0.0: P(some benefit verdict) 0.028, P(benefit >= SESOI shown) 0.000
# true benefit 20.0: P(some benefit verdict) 0.908, P(benefit >= SESOI shown) 0.026
# true benefit 40.0: P(some benefit verdict) 1.000, P(benefit >= SESOI shown) 0.894程序第一行用普福德雷舍与布朗的数字做一个粗略的核对:16% 的人平均误差 229 音分、其余 30 音分时,总体平均约 61 音分(与德莫雷斯特与普福德雷舍报告的 66 音分同一量级),仅这两类人之间的差别就带来约 72 音分的标准差。据此,本书取计划值:标准差 75 音分、与基线的相关 0.6、最小关注差异 20 音分(约 0.27 个标准差)。这三个数都是作者的假设,不是已发表的估计。协方差分析利用基线,使所需人数乘以 (Borm et al. 2007)。在这些假设下,每组需要 191 名可分析的参与者;考虑约 15% 的流失与排除,每组招募 225 人,共 450 人。敏感性表显示,标准差为 90、相关为 0.4 时每组需要 359 人;标准差为 60、相关为 0.6 时只需 123 人。
重估示例中,如果预试验得到的标准差恰好是 75、相关 0.6,规则给出每组 266 人(招募 313 人,共 626 人),比计划值多,因为置信上限有意偏保守;预试验标准差为 50 时,规则给出 119 人,按下限取 150 人;为 90、相关 0.45 时,规则给出 477 人,按上限取 350 人(招募 412 人,共 824 人)。
程序最后三行用 3000 次模拟检查判定规则的统计性质,与第 33 本的结论一致:「90% 把握度」指的是在真实效应等于最小关注效应时得到某种获益结论的概率(模拟为 0.908);在这种情况下,最强的结论「至少达到最小关注效应」出现的概率只有约 2.5%(模拟为 0.026,蒙特卡洛误差约 0.003);要以约 90% 的把握得到最强结论,真实效应需要约为最小关注效应的两倍(模拟 40 音分时为 0.894)。真实效应为零时,错误地得出获益结论的概率为 0.028,接近名义上的 2.5%。模拟假定误差服从正态分布;真实数据右偏,这正是要做预试验与敏感性分析的原因之一。
判定规则
推导
# Decision rule fixed before enrolment. D = arm B minus arm A in 24-h pitch error (cents),
# adjusted for baseline and stratum; 95 % CI; positive = arm A (vibrotactile error
# feedback) more accurate. SESOI 20 cents. Gates: more than 25 % of randomised people
# excluded, or a fidelity check fails (median voice-to-vibration latency above 60 ms in
# the device log, or under 90 % of planned training minutes delivered in either arm)
# -> NOT INTERPRETABLE. Secondary outcomes never decide the verdict.
SESOI = 20
def verdict(d, lo, hi, excluded=0.10, fidelity_ok=True):
if excluded > 0.25:
return f"D = {d:+.0f}: NOT INTERPRETABLE - {excluded:.0%} excluded (limit 25 %)"
if not fidelity_ok:
return f"D = {d:+.0f}: NOT INTERPRETABLE - fidelity check failed"
if lo >= SESOI: v = "A beats B by at least the SESOI (20 cents)"
elif lo > 0: v = "A beats B; size not shown to reach 20 cents"
elif hi < 0: v = "A is WORSE than B"
elif lo >= -SESOI and hi <= SESOI: v = "no difference worth acting on"
else: v = "unclear: the interval is too wide to decide"
return f"D = {d:+.0f} ({lo:+.0f} to {hi:+.0f}): {v}"
for case in ((36, 24, 48), (19, 7, 31), (2, -10, 14), (-18, -30, -6),
(10, -22, 42), (30, 18, 42, 0.31), (25, 13, 37, 0.12, False)):
print(verdict(*case))
# D = +36 (+24 to +48): A beats B by at least the SESOI (20 cents)
# D = +19 (+7 to +31): A beats B; size not shown to reach 20 cents
# D = +2 (-10 to +14): no difference worth acting on
# D = -18 (-30 to -6): A is WORSE than B
# D = +10 (-22 to +42): unclear: the interval is too wide to decide
# D = +30: NOT INTERPRETABLE - 31% excluded (limit 25 %)
# D = +25: NOT INTERPRETABLE - fidelity check failed示例结果是编造的。如果被随机分配者中超过 25% 被排除,或者保真度检查不合格(设备日志显示从声音到振动的延迟中位数超过 60 毫秒,或者任一组实际完成的训练时间不足计划的 90%),无论主要结局如何,报告都写「不可解释」。只有当差值的置信区间下限不低于 20 音分时,报告才写「A 组至少好出最小关注的幅度」。次要结局只报告估计值与置信区间,不改变结论。
这项试验不能说明什么
第一,它比较的是「有振动的误差反馈」与「不振动的同款设备」,其余条件相同,所以能回答「加上这种触觉反馈有没有用」;但它不能与视觉音高曲线、老师的指导等其他反馈方式比较,也不能分开是振动所携带的误差信息起作用,还是「有东西在振动」本身带来的注意或期望起作用;要回答后一个问题,需要增加一组接受与误差无关的振动的对照。第二,五个舒适音区内的持续元音与两个陌生元音,不能代表唱歌、言语语调或外语发音的全部。第三,实验室中 24 小时与 7 天的保持,不能说明数月后的水平。第四,参与者是没有系统声乐训练的成人,结果不能推到儿童、专业歌手、有嗓音疾病或听力障碍的人(包括人工耳蜗使用者)。第五,参与者知道自己是否感到振动,期望效应无法完全排除。第六,无论结果如何,它都不能用来支持或反对任何具体的课程、设备或作者的教学方法。
伦理与利益冲突
试验风险很低,但仍须经独立的伦理委员会批准,参与者签署知情同意。
嗓音负荷。 每段训练不超过 10 分钟,每天的训练合计不超过 30 分钟,段间休息 5 分钟,现场提供饮用水,休息时鼓励饮水;所有目标音都在本人舒适音高上下 4 个半音以内,以舒适的音量唱,不要求大声或用力;有嗓音疾病、持续声音嘶哑或喉部手术史者不参加,测试当天有急性上呼吸道感染者改期;参与者出现喉咙疼痛、声音嘶哑、咳嗽或任何不适时随时停止,不受影响;第二天仍有声音嘶哑者由研究人员随访,并建议就医。
振动安全。 振动带采用与手机、智能手表触觉提示同类的低功率振动元件,脉冲短促,累计振动时间只占 30 分钟训练的一小部分,远低于手臂振动职业暴露限值所针对的水平(欧盟 2002/44/EC 指令以 8 小时等效加速度 2.5 米/秒² 为行动值);前臂或腕部皮肤破损、感觉障碍或植入式电子医疗设备者不参加;参与者感到任何不适可随时取下振动带或退出,不受影响。
独立性。 由于发音任务与作者的声乐训练业务更接近,训练材料不使用作者课程的任何内容,不从作者的学员中招募,试验由独立研究者实施和分析。方案、目标音与元音材料、设备的编码程序与分析代码在第一位参与者入组前公开登记;如有可能,以注册报告的形式投稿,使发表与否不取决于结果。试验结束后向 B 组参与者提供 A 组的训练方式。录音与数据匿名存储。
利益披露
作者在 vocal.guru 和 singingasia.com 教授声乐,创立了「耳根圆通法」听觉反馈训练体系,并开发相关的感知训练课程,因此与本书主题及本章试验的结果有直接的商业利益关系。此外,作者以 5D Library 的著作(包括本书)作为其社群课程、升维牌卡(5D Ascension Cards,Dimensional Shift Card Decks)与 Yafu AI 知识库的方法论基础,因此与本书主题有商业利益关系。本书不推销任何课程、牌卡或服务;本章的试验设计为由独立于作者企业的研究人员实施和分析,不从作者的学员、社群或客户中招募。由于本章试验的发音任务与作者的声乐训练业务更接近,训练材料不使用作者课程的任何内容,试验不从作者的学员中招募,由独立研究者实施和分析。书中所有代码与研究方案均公开,供任何人检验。为限制利益的影响:训练材料、测试程序与分析代码在入组前公开;无论结果如何,结果、数据和代码都将公开。
本章小结
四步核查表依次检查:是否依赖一个可能失效的单一通道、微弱信号是否有第二通道的同步支持、视听时间差是否在不对称的窗口之内(声音提前不超过约 45 毫秒、延后不超过约 125 毫秒),以及学习者有多少时间在主动探索。预注册试验在相同的视听条件下,比较「把实时音高或共振峰误差编码为前臂振动」与「佩戴同款但不振动的设备」两种训练对 24 小时后无反馈音高模仿误差的影响:唯一的主要结局以音分计,先做 20 人的外部预试验,再按写成代码的规则(标准差取 80% 置信上限、最小关注差异固定为 20 音分、每组 150 至 350 人)确定样本量,计划值为每组 191 名可分析(招募 225 名),判定规则写成代码,并有嗓音负荷与振动的安全条款,无论结果如何都发表。
参考文献
Fan, Y. (2026). Psychophysics, the Weber-Fechner Law and Optimal Bayesian Multisensory Integration: From Stevens's Power Law and the McGurk Effect to Sensory Substitution, Read Beside Classical Chinese Thought on the Six Senses [心理物理学、韦伯-费希纳定律与多感官贝叶斯整合的数学模型] (Version 1.0.0). 5D Library. https://doi.org/10.5281/zenodo.23788548 · Licensed under Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International (CC BY-NC-ND 4.0).