5D Library › 心理物理学、韦伯-费希纳定律与多感官贝叶斯整合的数学模型 · English

第八章 实践与预注册试验

注

本章的核查表用于设计与评估学习材料、教学和产品,不是医疗建议。 它不能用来判断任何人的听力、视力、前庭、嗓音或神经功能;如有这方面的症状,请就医。试验方案包含持续的发声练习与前臂振动,方案已写明嗓音负荷与振动的安全条款,并把有嗓音疾病等情况的人排除在外。

第一部分 四步多感官精度核查表

设计一节技能课、一段演讲、一个教学视频或一件产品时,都可以依次问四个问题。

  1. 主通道噪声评估与高危单通道降级识别。 学习者或用户主要依赖哪一种感觉?在真实使用条件下(视觉杂乱、背景噪声、疲劳、分心),这种感觉的可靠性会下降多少?如果一个通道承担了绝大部分「权重」,而它恰好在关键时刻可能变差,就是一个高危的单通道设计,需要准备一个可靠的备份通道(第三章)。
  2. 微弱信号区的跨通道同步强化。 主通道的信号是否微弱到难以分辨?如果是,同时、同地加入第二种感觉的信号,预期收益可以按两个通道的精度合成来估计。不要假设「信号越弱,加第二通道的收益越大」:超加性主要见于单个神经元在弱刺激下的反应,行为上视听言语的增益在中等信噪比最大(第五章)。
  3. 视听时间同步窗口对齐。 声音与画面(或振动)之间的时间差是否在可接受的范围内?框架中写的是「约 ±100 毫秒」。核对后应改为不对称的窗口:声音比画面提前时更容易被察觉。
  4. 主动探索与被动接收审计。 在全部练习时间中,有多大比例是学习者自己发起动作、控制探索的,有多大比例只是看和听?主动产生的动作提供了已知的输入,是建立「动作—感觉」对应关系的条件(第六章)。

证据

声音与画面能错开多少。 国际电信联盟 1998 年的广播建议书根据在日本、瑞士与澳大利亚进行的观众主观评价,给出声音与画面时间差的阈值:可察觉阈约为声音提前 45 毫秒、延后 125 毫秒;可接受阈约为声音提前 90 毫秒、延后 185 毫秒 (ITU-R 1998)。在言语融合的实验中,声音提前约 30 毫秒到延后约 170 毫秒的范围内融合都很普遍(第四章)(Wassenhove et al. 2007)。

证据

时间窗口可以训练。 鲍尔斯、希洛克与华莱士 2009 年让受试者做带反馈的视听同时性判断训练,判断「同时」的时间窗口宽度缩小了约 40% (Powers et al. 2009)。这说明窗口不是固定的生理常数;也说明不同经验的人窗口宽度可能不同,核查表中的数值只能作为起点。

把核查表用于三个虚构的例子:一段在线发音教学视频,一辆车的车道偏离提示,以及一个动作技能学习应用。各通道的标准差与 d′d' 都是假设值;第一步中「一个通道超过 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。两个弱信号合起来 d′d' 从 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),取中位数,与目标音比较:

e=1200log⁡2f0ft,e = 1200\,\log_2\frac{f_0}{f_t},

其中 ftf_t 是目标音的基频,ee 以音分计,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 个试次音高误差绝对值 |e|\lvert e\rvert 的平均(音分),越低越好
次要结局 24 小时的元音 F1/F2 距离(半音);训练结束时的即时误差;7 天后的误差;同一目标音重复唱的一致性(有符号误差的标准差);未训练的目标音(舒适音高上下 1 个与 3 个半音)的误差;误差在 ±50 音分以内的试次比例;嗓音不适与发声费力的自评
分析 协方差分析:因变量为主要结局,自变量为分组,协变量为基线误差与分层变量;报告 B − A 的差及其 95% 置信区间,正值表示 A 组更准确;双侧 α=0.05\alpha = 0.05。音高误差的分布右偏,预先登记一项对数尺度上的敏感性分析,只作报告,不改变结论

选择「音分绝对误差的平均」作为唯一的主要结局,理由是:它是歌唱研究中常用的模仿准确度指标,用 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 小时误差的标准差与它和基线的相关。预试验的数据不进入主分析。第二步按预先写成代码的规则重新计算正式试验的样本量:

  1. 标准差取预试验合并组内标准差(自由度 18)的单侧 80% 置信上限 (Browne 1995)。西姆与刘易斯 2012 年也用置信上限的思路,但取 95% 上限,并主张按估计精度与总效率来定预试验的规模;他们的分析建议预试验总共约 55 人以上 (Sim and Lewis 2012)。本方案按作者的决定只做 20 人,代价是置信上限把标准差放大 1.183 倍,样本量放大约 1.40 倍。
  2. 与基线的相关取预试验的估计值,但不高于计划值 0.6、不低于 0。
  3. 效应量不由预试验估计。 最小关注差异事先固定为 20 音分(约五分之一个半音),不随预试验改变。每组 10 人时,两组差值的标准误约 27 音分,比最小关注差异还大;用这样的估计来定样本量,结果会随机地偏大或偏小,而且只在预试验「看起来有效」时才继续,会使后续研究系统地高估效应、低估所需人数 (Kraemer et al. 2006)。所以预试验的统计人员只报告合并标准差与相关这两个数,不计算、不报告两组的差。
  4. 最终每组可分析人数限定在 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 个标准差)。这三个数都是作者的假设,不是已发表的估计。协方差分析利用基线,使所需人数乘以 1−r21 – r^2 (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 名),判定规则写成代码,并有嗓音负荷与振动的安全条款,无论结果如何都发表。

参考文献

Borm, George F., Jaap Fransen, and Wim A. J. G. Lemmens. 2007. “A Simple Sample Size Formula for Analysis of Covariance in Randomized Clinical Trials.” Journal of Clinical Epidemiology 60 (12): 1234–38. https://doi.org/10.1016/j.jclinepi.2007.02.006.
Browne, Richard H. 1995. “On the Use of a Pilot Sample for Sample Size Determination.” Statistics in Medicine 14 (17): 1933–40. https://doi.org/10.1002/sim.4780141709.
Cheveigné, Alain de, and Hideki Kawahara. 2002. “YIN, a Fundamental Frequency Estimator for Speech and Music.” The Journal of the Acoustical Society of America 111 (4): 1917–30. https://doi.org/10.1121/1.1458024.
Demorest, Steven M., and Peter Q. Pfordresher. 2015. “Singing Accuracy Development from K-Adult: A Comparative Study.” Music Perception 32 (3): 293–302. https://doi.org/10.1525/mp.2015.32.3.293.
Fletcher, Mark D., Nour Thini, and Samuel W. Perry. 2020. “Enhanced Pitch Discrimination for Cochlear Implant Users with a New Haptic Neuroprosthetic.” Scientific Reports 10: 10354. https://doi.org/10.1038/s41598-020-67140-0.
Hutchins, Sean, Pauline Larrouy-Maestri, and Isabelle Peretz. 2014. “Singing Ability Is Rooted in Vocal-Motor Control of Pitch.” Attention, Perception, & Psychophysics 76 (8): 2522–30. https://doi.org/10.3758/s13414-014-0732-1.
ITU-R. 1998. Relative Timing of Sound and Vision for Broadcasting. Recommendation ITU-R BT.1359. International Telecommunication Union.
Kraemer, Helena Chmura, Jim Mintz, Art Noda, Jared Tinklenberg, and Jerome A. Yesavage. 2006. “Caution Regarding the Use of Pilot Studies to Guide Power Calculations for Study Proposals.” Archives of General Psychiatry 63 (5): 484–89. https://doi.org/10.1001/archpsyc.63.5.484.
Makhoul, John. 1975. “Linear Prediction: A Tutorial Review.” Proceedings of the IEEE 63 (4): 561–80. https://doi.org/10.1109/PROC.1975.9792.
Pfordresher, Peter Q., and Steven Brown. 2007. “Poor-Pitch Singing in the Absence of ‘Tone Deafness’.” Music Perception 25 (2): 95–115. https://doi.org/10.1525/mp.2007.25.2.95.
Powers, Albert R., Andrea R. Hillock, and Mark T. Wallace. 2009. “Perceptual Training Narrows the Temporal Window of Multisensory Binding.” Journal of Neuroscience 29 (39): 12265–74. https://doi.org/10.1523/JNEUROSCI.3501-09.2009.
Salmoni, Alan W., Richard A. Schmidt, and Charles B. Walter. 1984. “Knowledge of Results and Motor Learning: A Review and Critical Reappraisal.” Psychological Bulletin 95 (3): 355–86. https://doi.org/10.1037/0033-2909.95.3.355.
Sim, Julius, and Martyn Lewis. 2012. “The Size of a Pilot Study for a Clinical Trial Should Be Calculated in Relation to Considerations of Precision and Efficiency.” Journal of Clinical Epidemiology 65 (3): 301–8. https://doi.org/10.1016/j.jclinepi.2011.07.011.
Wassenhove, Virginie van, Ken W. Grant, and David Poeppel. 2007. “Temporal Window of Integration in Auditory-Visual Speech Perception.” Neuropsychologia 45 (3): 598–607. https://doi.org/10.1016/j.neuropsychologia.2006.01.001.

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).