音乐生成模型评测旋律连贯性、和声合理性与风格一致性续篇场景痛点团队训练了两个音乐生成模型。Model-A的MOS评分4.2Model-B的MOS评分3.8。选择Model-A上线。上线后用户反馈Model-A生成的音乐听起来不错但感觉空洞——旋律跳跃没有连贯性和弦搭配偶尔违反乐理规则风格在爵士和古典之间随机切换。MOS评分主观平均意见分只反映好不好听不反映是否正确。好听但不正确的音乐像AI生成的漂亮假话——表面光鲜、内在空洞。核心矛盾主观评测MOS不能替代客观评测旋律连贯性、和声合理性、风格一致性。需要结构化评测框架量化音乐的内在质量而非表面好听度。底层机制与原理剖析音乐生成模型评测有三个客观维度每个维度有明确的量化指标关键机制旋律连贯性Motif Coherence。一段旋律的好坏不取决于单个音符取决于音符之间的关系。衡量标准自相关系数相邻音符的音程变化是否平滑。跳跃式旋律音程变化7个半音的自相关系数低。动机重复率主题素材是否在后续发展中复现。好的旋律有动机重复AABA结构差的旋律每段都是新素材。和声合理性Harmonic Validity。和弦搭配是否符合乐理规则。衡量标准违规和弦比例不在当前调性音阶内的和弦占比。C大调中出现F#大三和弦违规。声部冲突率和弦内音程违反规则的比例平行五度、平行八度等禁止进行。风格一致性Style Consistency。整首曲子是否保持统一风格。衡量标准风格切换频率片段间风格标签变化的次数。一首30秒曲子风格切换5次风格混乱。节奏一致性BPM波动幅度。±5%是正常rubato±30%是风格混乱。生产级代码实现MusicEvaluator综合评测框架# evaluation/music_evaluator.py import numpy as np from typing import Dict, List, Tuple from dataclasses import dataclass dataclass class Note: pitch: int # MIDI pitch (0-127) velocity: int # MIDI velocity (0-127) start_time: float # 秒 duration: float # 秒 channel: int # MIDI channel dataclass class EvalResult: melody_coherence: float # 旋律连贯性 (0~1) harmonic_validity: float # 和声合理性 (0~1) style_consistency: float # 风格一致性 (0~1) composite_score: float # 综合评分 (0~1) details: Dict # 详细指标数据 class MusicEvaluator: 音乐生成模型客观评测框架 # 调性音阶定义半音偏移 # 为什么需要调性定义和声合理性检查需要知道当前调性的合法音符集合 # 调外音符构成的和弦是违规和弦 KEY_SCALES { C_major: [0, 2, 4, 5, 7, 9, 11], # C大调 C_minor: [0, 2, 3, 5, 7, 8, 10], # C小调 D_major: [2, 4, 6, 7, 9, 11, 13], # D大调偏移2 } # 和弦进行规则简化版 # 为什么需要和弦进行规则乐理中和弦不是随机组合的 # I→IV→V→I是经典进行I→III→VI→II是违规进行 VALID_CHORD_PROGRESSIONS { major: [ (I, IV), (I, V), (IV, V), (V, I), (I, vi), (vi, IV), (IV, I), (V, vi), ], minor: [ (i, iv), (i, V), (iv, V), (V, i), (i, VI), (VI, iv), ] } def evaluate(self, notes: List[Note], duration: float, key: str C_major) - EvalResult: 综合评测一段音乐 # 旋律连贯性评测 melody_result self.evaluate_melody_coherence(notes, duration) # 和声合理性评测 harmony_result self.evaluate_harmonic_validity(notes, key) # 风格一致性评测 style_result self.evaluate_style_consistency(notes, duration) # 综合评分加权计算 # 为什么旋律权重0.4、和声0.3、风格0.3旋律是音乐最核心的维度 # 听众对旋律连贯性的容忍度最低。和声和风格同等重要 composite ( melody_result[score] * 0.4 harmony_result[score] * 0.3 style_result[score] * 0.3 ) return EvalResult( melody_coherencemelody_result[score], harmonic_validityharmony_result[score], style_consistencystyle_result[score], composite_scorecomposite, details{ melody: melody_result, harmony: harmony_result, style: style_result } ) def evaluate_melody_coherence(self, notes: List[Note], duration: float) - Dict: 旋律连贯性评测 # 按时间排序音符 sorted_notes sorted(notes, keylambda n: n.start_time) # 1. 音程连续性自相关系数 # 计算相邻音符的音程变化序列 intervals [] for i in range(1, len(sorted_notes)): interval sorted_notes[i].pitch - sorted_notes[i-1].pitch intervals.append(abs(interval)) # 自相关系数相邻interval的变化是否平滑 # 为什么看相邻音程的变化而非音程本身音程大不等于不连贯 # 大跳后回跳如C→G→D→C是合理的旋律模式。 # 不连贯的是无规律的随机跳跃C→G→A→E→B interval_changes [abs(intervals[i1] - intervals[i]) for i in range(len(intervals) - 1)] if len(interval_changes) 0: # 理想值interval_changes的平均值3音程变化不超过3个半音 # 为什么阈值33个半音约1.5个音级是旋律自然流动的范围 avg_change np.mean(interval_changes) # 归一化到0~1avg_change0→完美连贯(1.0)avg_change12→极端跳跃(0.0) smoothness max(0, 1.0 - avg_change / 12.0) else: smoothness 1.0 # 只有一个音符默认连贯 # 2. 动机重复率 # 检测旋律片段是否在后续发展中复现 # 为什么需要动机重复好的旋律有主题发展动机→变奏→展开 # 没有动机重复的旋律像随机音符堆砌 motif_repetition self._detect_motif_repetition(sorted_notes) # 综合旋律连贯性评分 # 为什么smoothness权重更高音程连续性是旋律最基本的连贯要求 # 动机重复是更高层次的结构连贯 melody_score smoothness * 0.6 motif_repetition * 0.4 return { score: melody_score, smoothness: smoothness, avg_interval_change: np.mean(interval_changes) if interval_changes else 0, motif_repetition: motif_repetition, max_interval: max(intervals) if intervals else 0, } def evaluate_harmonic_validity(self, notes: List[Note], key: str) - Dict: 和声合理性评测 # 1. 调内音符比例 # 当前调性的合法音符集合 scale self.KEY_SCALES.get(key, self.KEY_SCALES[C_major]) # 归一化到0~11范围不管八度 scale_pitches set(s % 12 for s in scale) # 检查每个音符是否在调内 in_scale_count 0 out_of_scale_pitches [] for note in notes: pitch_class note.pitch % 12 if pitch_class in scale_pitches: in_scale_count 1 else: out_of_scale_pitches.append(pitch_class) in_scale_ratio in_scale_count / max(1, len(notes)) # 为什么in_scale_ratio高不代表好全部在调内100%但可能单调 # 适度调外音符5~10%是正常的表现手法如blue note。 # 但超过20%的调外音符和声混乱 # 2. 违规和弦检测 # 将同时发声的音符组合为和弦 # 为什么需要检测和弦而非只看音符单个调外音符可能是经过音 # 但和弦级别的调外音符是和声违规 chords self._extract_chords(notes) violation_count 0 for chord in chords: # 检查和弦内是否有声部冲突 # 平行五度连续两个和弦都有纯五度音程且根音同方向移动 # 为什么禁止平行五度传统乐理中平行五度导致声部独立性丧失 # 现代音乐中这个规则有所放松但仍然是衡量和声质量的参考 if self._has_parallel_fifths(chords, chords.index(chord)): violation_count 1 violation_ratio violation_count / max(1, len(chords)) # 和声合理性评分 # 调内音符比例权重低允许少量调外音符违规和弦比例权重高不可容忍 harmony_score in_scale_ratio * 0.3 (1 - violation_ratio) * 0.7 return { score: harmony_score, in_scale_ratio: in_scale_ratio, out_of_scale_pitches: out_of_scale_pitches[:5], # 只展示前5个违规音 violation_ratio: violation_ratio, chord_count: len(chords), violation_count: violation_count, } def evaluate_style_consistency(self, notes: List[Note], duration: float) - Dict: 风格一致性评测 # 1. 节奏一致性BPM波动 # 计算每个音符间距→推算瞬时BPM sorted_notes sorted(notes, keylambda n: n.start_time) bpms [] for i in range(1, len(sorted_notes)): interval_seconds sorted_notes[i].start_time - sorted_notes[i-1].start_time if interval_seconds 0: # 一个beat的间隔对应BPM # 为什么估算而非直接测量MIDI文件没有BPM信息时 # 需要从音符间距推算。推算误差约5%对一致性判断足够 bpm 60.0 / interval_seconds bpms.append(bpm) # BPM波动幅度 if len(bpms) 1: bpm_std np.std(bpms) bpm_mean np.mean(bpms) bpm_variation bpm_std / max(1, bpm_mean) # 标准差/均值变异系数 else: bpm_variation 0 # BPM一致性评分变异系数0.05非常稳定(1.0)0.3极度混乱(0.0) # 为什么0.05阈值5%的BPM波动是正常rubato音乐表达 # 超过30%的波动是风格混乱不像任何真实音乐风格 bpm_consistency max(0, 1.0 - bpm_variation / 0.3) # 2. 速度区间分布 # 音乐的速度倾向快/中/慢应该保持一致 # 为什么需要检查速度倾向一段音乐从慢速突然变快速又回到慢速 # 虽然BPM变异系数可能不高因为平均了但风格感受上是混乱的 tempo_buckets {slow: 0, medium: 0, fast: 0} for bpm in bpms: if bpm 80: tempo_buckets[slow] 1 elif bpm 140: tempo_buckets[medium] 1 else: tempo_buckets[fast] 1 # 主速度区间占比 total sum(tempo_buckets.values()) dominant_tempo_ratio max(tempo_buckets.values()) / max(1, total) # 为什么dominant_tempo_ratio高代表风格一致 # 一首曲子70%的音符在medium速度区间→风格统一中速曲。 # 三个区间各占33%→风格混乱 # 综合风格一致性评分 style_score bpm_consistency * 0.5 dominant_tempo_ratio * 0.5 return { score: style_score, bpm_variation: bpm_variation, bpm_mean: np.mean(bpms) if bpms else 0, bpm_std: np.std(bpms) if len(bpms) 1 else 0, tempo_distribution: tempo_buckets, dominant_tempo_ratio: dominant_tempo_ratio, } def _detect_motif_repetition(self, notes: List[Note]) - float: 检测旋律动机重复率 # 将音符序列转化为音程模式忽略绝对音高只看相对变化 # 为什么只看相对变化而非绝对音高动机重复包括变奏音高移位 # 绝对音高不同但音程模式相同变奏式动机重复 patterns [] window_size 4 # 4音符窗口作为动机单元 # 为什么4音符窗口4音符约1小节是动机的最小有意义单位 for i in range(len(notes) - window_size 1): window notes[i:i window_size] pattern tuple( window[j1].pitch - window[j].pitch for j in range(len(window) - 1) ) patterns.append(pattern) # 计算模式重复率有多少模式出现了至少2次 pattern_counts {} for p in patterns: pattern_counts[p] pattern_counts.get(p, 0) 1 repeated_patterns sum(1 for count in pattern_counts.values() if count 2) total_patterns len(patterns) repetition_rate repeated_patterns / max(1, total_patterns) # 为什么重复率0.3~0.6是理想范围太低(0.1)没有动机发展 # 太高(0.8)过度重复单调。0.3~0.6表示有适度的发展与回归 # 归一化repetition_rate 0.3→1.0理想0→0.5太少0.8→0.5太多 if repetition_rate 0.3: return 0.5 repetition_rate / 0.3 * 0.5 elif repetition_rate 0.6: return 1.0 else: return max(0.5, 1.0 - (repetition_rate - 0.6) / 0.4 * 0.5) def _extract_chords(self, notes: List[Note]) - List[List[Note]]: 提取同时发声的音符组合为和弦 chords [] # 按时间分组同时发声的音符时间差0.05秒 sorted_notes sorted(notes, keylambda n: n.start_time) current_chord [sorted_notes[0]] if sorted_notes else [] for i in range(1, len(sorted_notes)): time_diff sorted_notes[i].start_time - sorted_notes[i-1].start_time if time_diff 0.05: # 50ms以内视为同时发声 current_chord.append(sorted_notes[i]) else: if len(current_chord) 2: # 至少2个音符才算和弦 chords.append(current_chord) current_chord [sorted_notes[i]] if len(current_chord) 2: chords.append(current_chord) return chords def _has_parallel_fifths(self, chords: List[List[Note]], chord_index: int) - bool: 检测平行五度 if chord_index len(chords) - 1: return False chord1 chords[chord_index] chord2 chords[chord_index 1] # 检查两个和弦是否都有纯五度音程7个半音 has_fifth_in_chord1 any( abs(n1.pitch - n2.pitch) 7 for n1 in chord1 for n2 in chord1 if n1.pitch ! n2.pitch ) has_fifth_in_chord2 any( abs(n1.pitch - n2.pitch) 7 for n1 in chord2 for n2 in chord2 if n1.pitch ! n2.pitch ) if not (has_fifth_in_chord1 and has_fifth_in_chord2): return False # 检查根音是否同方向移动 root1 min(n.pitch for n in chord1) root2 min(n.pitch for n in chord2) prev_root min(n.pitch for n in chords[chord_index - 1]) if chord_index 0 else root1 same_direction (root2 root1 and root1 prev_root) or \ (root2 root1 and root1 prev_root) return same_direction批量评测与模型对比# evaluation/batch_evaluator.py import json from typing import List, Dict from music_evaluator import MusicEvaluator, Note class BatchEvaluator: 批量评测多个模型的生成结果 def __init__(self): self.evaluator MusicEvaluator() def evaluate_models( self, model_outputs: Dict[str, List[List[Note]]], # model_name → 多段音乐的notes key: str C_major ) - ComparisonReport: 对比评测多个模型 results: Dict[str, List[EvalResult]] {} for model_name, outputs in model_outputs.items(): model_results [] for notes in outputs: result self.evaluator.evaluate(notes, duration30, keykey) model_results.append(result) results[model_name] model_results # 计算每个模型的平均分数 averages {} for model_name, model_results in results.items(): averages[model_name] { melody: np.mean([r.melody_coherence for r in model_results]), harmony: np.mean([r.harmonic_validity for r in model_results]), style: np.mean([r.style_consistency for r in model_results]), composite: np.mean([r.composite_score for r in model_results]), # 分数标准差反映模型输出的稳定性 # 为什么看标准差平均分高但标准差大模型输出不稳定 # 有时很好有时很差。平均分中等但标准差小模型输出稳定可靠 std: np.std([r.composite_score for r in model_results]), sample_count: len(model_results), } # 找到最佳模型 best_model max(averages.items(), keylambda x: x[1][composite]) return { model_results: averages, best_model: best_model[0], best_score: best_model[1][composite], recommendation: self._generate_recommendation(averages), } def _generate_recommendation(self, averages: Dict) - str: 生成评测建议 lines [] # 模型排序 ranked sorted(averages.items(), keylambda x: x[1][composite], reverseTrue) lines.append(模型排名综合评分) for model, scores in ranked: lines.append(f {model}: {scores[composite]:.3f} f(旋律{scores[melody]:.3f}, f和声{scores[harmony]:.3f}, f风格{scores[style]:.3f}, f稳定性{1-scores[std]:.3f})) # 弱项分析 worst_melody min(averages.items(), keylambda x: x[1][melody]) worst_harmony min(averages.items(), keylambda x: x[1][harmony]) worst_style min(averages.items(), keylambda x: x[1][style]) if worst_melody[1][melody] 0.5: lines.append(f\n⚠️ {worst_melody[0]}旋律连贯性不足({worst_melody[1][melody]:.3f}) f建议改进动机发展机制) if worst_harmony[1][harmony] 0.5: lines.append(f\n⚠️ {worst_harmony[0]}和声合理性不足({worst_harmony[1][harmony]:.3f}) f建议添加乐理规则约束) if worst_style[1][style] 0.5: lines.append(f\n⚠️ {worst_style[0]}风格一致性不足({worst_style[1][style]:.3f}) f建议强化风格condition信号) return \n.join(lines)评测结果可视化# evaluation/visualizer.py import matplotlib.pyplot as plt import numpy as np def plot_evaluation_radar(models: Dict[str, Dict]): 雷达图展示各模型的三个评测维度 categories [旋律连贯性, 和声合理性, 风格一致性, 输出稳定性] N len(categories) angles [n / float(N) * 2 * np.pi for n in range(N)] angles angles[:1] # 闭合图形 fig, ax plt.subplots(figsize(8, 8), subplot_kwdict(polarTrue)) for model_name, scores in models.items(): values [ scores[melody], scores[harmony], scores[style], 1 - scores[std] # 稳定性1-标准差 ] values values[:1] ax.plot(angles, values, o-, linewidth2, labelmodel_name) ax.fill(angles, values, alpha0.25) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories) ax.set_ylim(0, 1) ax.legend(locupper right, bbox_to_anchor(1.3, 1.0)) ax.set_title(音乐生成模型评测雷达图) plt.tight_layout() plt.savefig(evaluation_radar.png, dpi150)边界分析与架构权衡客观评测与主观评测的关系客观评测量化音乐是否正确。主观评测量化音乐是否好听。两者不是替代关系客观评测高、主观评测低音乐乐理正确但不好听。常见于规则约束过强的模型生成的音乐像教科书范例。客观评测低、主观评测高音乐好听但不正确。常见于大模型自由生成MOS评分高但风格混乱。生产场景客观评测是底线门槛低于0.5不允许上线主观评测是最终选择两个模型客观评测都达标后选主观评测更高的。调性检测的准确性评测和声合理性时需要知道当前调性。但AI生成的音乐调性可能模糊混合调式或频繁切换。解决方案自动调性检测。用Krumhansl-Schmuckler算法从音符分布推算最可能的调性。推算准确率约85%——15%的模糊调性音乐会被误判。误判的影响把大调音乐误判为小调→和声合理性评分偏低。缓解对模糊调性的音乐降低和声评测权重从0.3降到0.2增加旋律和风格权重。动机重复率的窗口大小4音符窗口检测动机重复。4音符太小可能检测出伪重复巧合相似的短片段。8音符窗口更精确但漏检短动机。解决方案多尺度检测。同时用3音符、4音符、6音符窗口检测取加权平均。3音符权重低容易巧合6音符权重高真正的动机重复。风格标签缺失的问题风格一致性评测需要风格标签爵士/古典/电子等。AI生成的音乐没有标签——需要分类器推断。风格分类器可用预训练模型如MusicBERT。但分类器本身有误判率。误判导致风格切换频率的计算不准确。缓解不用分类器用音乐特征分布的一致性替代。不判断是什么风格判断风格特征是否一致。具体做法计算音程分布、节奏模式、速度分布的JS散度。散度低风格一致散度高风格混乱。不依赖分类器不依赖风格标签。评测数据集的选择评测模型需要标准的音乐数据集作为参考基线。不能只评测AI生成的音乐——需要与真实音乐对比。基线数据集选择训练数据集中的真实音乐最直接的基线。AI模型应该至少达到训练数据集的平均质量。人工创作的参考曲目同一风格/调性的人工音乐作为上限目标。随机生成的音乐作为下限基线。AI模型应该显著超过随机生成。评测维度 | 训练数据平均 | 人工参考上限 | 随机生成下限旋律连贯性 | 0.7 | 0.9 | 0.2和声合理性 | 0.75 | 0.95 | 0.1风格一致性 | 0.65 | 0.85 | 0.15AI模型的目标各维度超过训练数据平均0.1。低于训练数据平均模型没有学到结构。评测的统计显著性评测50段音乐样本。每段音乐30秒。总评测时长25分钟。50个样本的平均分数是否有统计显著性需要至少30个样本才能达到95%置信区间±0.05。50个样本达到±0.03。推荐评测100段样本——置信区间±0.02足以区分模型间的微小差异。总结音乐生成模型评测不能只靠MOS评分。结构化评测框架量化三个客观维度旋律连贯性音程连续性自相关系数动机重复率。权重0.6和0.4。平滑变化随机跳跃。适度重复没有发展或过度重复。和声合理性调内音符比例违规和弦比例平行五度等。违规和弦权重高0.7调外音符容忍度低权重0.3。风格一致性BPM波动幅度变异系数主速度区间占比。变异系数5%正常30%混乱。综合评分加权旋律0.4和声0.3风格0.3。旋律权重最高——听众对旋律混乱的容忍度最低。客观评测是底线门槛0.5不上线主观评测是最终选择。两者互补不替代。批量评测至少50段样本推荐100段。标准差反映输出稳定性——高分高标准差不可靠的模型。模糊调性音乐降低和声评测权重风格标签缺失时用特征分布一致性替代。MOS评分4.2的模型可能旋律跳跃、和声违规、风格混乱。0.7的综合客观评分才能上线——好听不是正确的替代品。资料说明本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论不应视为行业事实。可参考 0731 资料来源索引并在发布前将具体来源贴到对应断言之后。
音乐生成模型评测:旋律连贯性、和声合理性与风格一致性(续篇)
音乐生成模型评测旋律连贯性、和声合理性与风格一致性续篇场景痛点团队训练了两个音乐生成模型。Model-A的MOS评分4.2Model-B的MOS评分3.8。选择Model-A上线。上线后用户反馈Model-A生成的音乐听起来不错但感觉空洞——旋律跳跃没有连贯性和弦搭配偶尔违反乐理规则风格在爵士和古典之间随机切换。MOS评分主观平均意见分只反映好不好听不反映是否正确。好听但不正确的音乐像AI生成的漂亮假话——表面光鲜、内在空洞。核心矛盾主观评测MOS不能替代客观评测旋律连贯性、和声合理性、风格一致性。需要结构化评测框架量化音乐的内在质量而非表面好听度。底层机制与原理剖析音乐生成模型评测有三个客观维度每个维度有明确的量化指标关键机制旋律连贯性Motif Coherence。一段旋律的好坏不取决于单个音符取决于音符之间的关系。衡量标准自相关系数相邻音符的音程变化是否平滑。跳跃式旋律音程变化7个半音的自相关系数低。动机重复率主题素材是否在后续发展中复现。好的旋律有动机重复AABA结构差的旋律每段都是新素材。和声合理性Harmonic Validity。和弦搭配是否符合乐理规则。衡量标准违规和弦比例不在当前调性音阶内的和弦占比。C大调中出现F#大三和弦违规。声部冲突率和弦内音程违反规则的比例平行五度、平行八度等禁止进行。风格一致性Style Consistency。整首曲子是否保持统一风格。衡量标准风格切换频率片段间风格标签变化的次数。一首30秒曲子风格切换5次风格混乱。节奏一致性BPM波动幅度。±5%是正常rubato±30%是风格混乱。生产级代码实现MusicEvaluator综合评测框架# evaluation/music_evaluator.py import numpy as np from typing import Dict, List, Tuple from dataclasses import dataclass dataclass class Note: pitch: int # MIDI pitch (0-127) velocity: int # MIDI velocity (0-127) start_time: float # 秒 duration: float # 秒 channel: int # MIDI channel dataclass class EvalResult: melody_coherence: float # 旋律连贯性 (0~1) harmonic_validity: float # 和声合理性 (0~1) style_consistency: float # 风格一致性 (0~1) composite_score: float # 综合评分 (0~1) details: Dict # 详细指标数据 class MusicEvaluator: 音乐生成模型客观评测框架 # 调性音阶定义半音偏移 # 为什么需要调性定义和声合理性检查需要知道当前调性的合法音符集合 # 调外音符构成的和弦是违规和弦 KEY_SCALES { C_major: [0, 2, 4, 5, 7, 9, 11], # C大调 C_minor: [0, 2, 3, 5, 7, 8, 10], # C小调 D_major: [2, 4, 6, 7, 9, 11, 13], # D大调偏移2 } # 和弦进行规则简化版 # 为什么需要和弦进行规则乐理中和弦不是随机组合的 # I→IV→V→I是经典进行I→III→VI→II是违规进行 VALID_CHORD_PROGRESSIONS { major: [ (I, IV), (I, V), (IV, V), (V, I), (I, vi), (vi, IV), (IV, I), (V, vi), ], minor: [ (i, iv), (i, V), (iv, V), (V, i), (i, VI), (VI, iv), ] } def evaluate(self, notes: List[Note], duration: float, key: str C_major) - EvalResult: 综合评测一段音乐 # 旋律连贯性评测 melody_result self.evaluate_melody_coherence(notes, duration) # 和声合理性评测 harmony_result self.evaluate_harmonic_validity(notes, key) # 风格一致性评测 style_result self.evaluate_style_consistency(notes, duration) # 综合评分加权计算 # 为什么旋律权重0.4、和声0.3、风格0.3旋律是音乐最核心的维度 # 听众对旋律连贯性的容忍度最低。和声和风格同等重要 composite ( melody_result[score] * 0.4 harmony_result[score] * 0.3 style_result[score] * 0.3 ) return EvalResult( melody_coherencemelody_result[score], harmonic_validityharmony_result[score], style_consistencystyle_result[score], composite_scorecomposite, details{ melody: melody_result, harmony: harmony_result, style: style_result } ) def evaluate_melody_coherence(self, notes: List[Note], duration: float) - Dict: 旋律连贯性评测 # 按时间排序音符 sorted_notes sorted(notes, keylambda n: n.start_time) # 1. 音程连续性自相关系数 # 计算相邻音符的音程变化序列 intervals [] for i in range(1, len(sorted_notes)): interval sorted_notes[i].pitch - sorted_notes[i-1].pitch intervals.append(abs(interval)) # 自相关系数相邻interval的变化是否平滑 # 为什么看相邻音程的变化而非音程本身音程大不等于不连贯 # 大跳后回跳如C→G→D→C是合理的旋律模式。 # 不连贯的是无规律的随机跳跃C→G→A→E→B interval_changes [abs(intervals[i1] - intervals[i]) for i in range(len(intervals) - 1)] if len(interval_changes) 0: # 理想值interval_changes的平均值3音程变化不超过3个半音 # 为什么阈值33个半音约1.5个音级是旋律自然流动的范围 avg_change np.mean(interval_changes) # 归一化到0~1avg_change0→完美连贯(1.0)avg_change12→极端跳跃(0.0) smoothness max(0, 1.0 - avg_change / 12.0) else: smoothness 1.0 # 只有一个音符默认连贯 # 2. 动机重复率 # 检测旋律片段是否在后续发展中复现 # 为什么需要动机重复好的旋律有主题发展动机→变奏→展开 # 没有动机重复的旋律像随机音符堆砌 motif_repetition self._detect_motif_repetition(sorted_notes) # 综合旋律连贯性评分 # 为什么smoothness权重更高音程连续性是旋律最基本的连贯要求 # 动机重复是更高层次的结构连贯 melody_score smoothness * 0.6 motif_repetition * 0.4 return { score: melody_score, smoothness: smoothness, avg_interval_change: np.mean(interval_changes) if interval_changes else 0, motif_repetition: motif_repetition, max_interval: max(intervals) if intervals else 0, } def evaluate_harmonic_validity(self, notes: List[Note], key: str) - Dict: 和声合理性评测 # 1. 调内音符比例 # 当前调性的合法音符集合 scale self.KEY_SCALES.get(key, self.KEY_SCALES[C_major]) # 归一化到0~11范围不管八度 scale_pitches set(s % 12 for s in scale) # 检查每个音符是否在调内 in_scale_count 0 out_of_scale_pitches [] for note in notes: pitch_class note.pitch % 12 if pitch_class in scale_pitches: in_scale_count 1 else: out_of_scale_pitches.append(pitch_class) in_scale_ratio in_scale_count / max(1, len(notes)) # 为什么in_scale_ratio高不代表好全部在调内100%但可能单调 # 适度调外音符5~10%是正常的表现手法如blue note。 # 但超过20%的调外音符和声混乱 # 2. 违规和弦检测 # 将同时发声的音符组合为和弦 # 为什么需要检测和弦而非只看音符单个调外音符可能是经过音 # 但和弦级别的调外音符是和声违规 chords self._extract_chords(notes) violation_count 0 for chord in chords: # 检查和弦内是否有声部冲突 # 平行五度连续两个和弦都有纯五度音程且根音同方向移动 # 为什么禁止平行五度传统乐理中平行五度导致声部独立性丧失 # 现代音乐中这个规则有所放松但仍然是衡量和声质量的参考 if self._has_parallel_fifths(chords, chords.index(chord)): violation_count 1 violation_ratio violation_count / max(1, len(chords)) # 和声合理性评分 # 调内音符比例权重低允许少量调外音符违规和弦比例权重高不可容忍 harmony_score in_scale_ratio * 0.3 (1 - violation_ratio) * 0.7 return { score: harmony_score, in_scale_ratio: in_scale_ratio, out_of_scale_pitches: out_of_scale_pitches[:5], # 只展示前5个违规音 violation_ratio: violation_ratio, chord_count: len(chords), violation_count: violation_count, } def evaluate_style_consistency(self, notes: List[Note], duration: float) - Dict: 风格一致性评测 # 1. 节奏一致性BPM波动 # 计算每个音符间距→推算瞬时BPM sorted_notes sorted(notes, keylambda n: n.start_time) bpms [] for i in range(1, len(sorted_notes)): interval_seconds sorted_notes[i].start_time - sorted_notes[i-1].start_time if interval_seconds 0: # 一个beat的间隔对应BPM # 为什么估算而非直接测量MIDI文件没有BPM信息时 # 需要从音符间距推算。推算误差约5%对一致性判断足够 bpm 60.0 / interval_seconds bpms.append(bpm) # BPM波动幅度 if len(bpms) 1: bpm_std np.std(bpms) bpm_mean np.mean(bpms) bpm_variation bpm_std / max(1, bpm_mean) # 标准差/均值变异系数 else: bpm_variation 0 # BPM一致性评分变异系数0.05非常稳定(1.0)0.3极度混乱(0.0) # 为什么0.05阈值5%的BPM波动是正常rubato音乐表达 # 超过30%的波动是风格混乱不像任何真实音乐风格 bpm_consistency max(0, 1.0 - bpm_variation / 0.3) # 2. 速度区间分布 # 音乐的速度倾向快/中/慢应该保持一致 # 为什么需要检查速度倾向一段音乐从慢速突然变快速又回到慢速 # 虽然BPM变异系数可能不高因为平均了但风格感受上是混乱的 tempo_buckets {slow: 0, medium: 0, fast: 0} for bpm in bpms: if bpm 80: tempo_buckets[slow] 1 elif bpm 140: tempo_buckets[medium] 1 else: tempo_buckets[fast] 1 # 主速度区间占比 total sum(tempo_buckets.values()) dominant_tempo_ratio max(tempo_buckets.values()) / max(1, total) # 为什么dominant_tempo_ratio高代表风格一致 # 一首曲子70%的音符在medium速度区间→风格统一中速曲。 # 三个区间各占33%→风格混乱 # 综合风格一致性评分 style_score bpm_consistency * 0.5 dominant_tempo_ratio * 0.5 return { score: style_score, bpm_variation: bpm_variation, bpm_mean: np.mean(bpms) if bpms else 0, bpm_std: np.std(bpms) if len(bpms) 1 else 0, tempo_distribution: tempo_buckets, dominant_tempo_ratio: dominant_tempo_ratio, } def _detect_motif_repetition(self, notes: List[Note]) - float: 检测旋律动机重复率 # 将音符序列转化为音程模式忽略绝对音高只看相对变化 # 为什么只看相对变化而非绝对音高动机重复包括变奏音高移位 # 绝对音高不同但音程模式相同变奏式动机重复 patterns [] window_size 4 # 4音符窗口作为动机单元 # 为什么4音符窗口4音符约1小节是动机的最小有意义单位 for i in range(len(notes) - window_size 1): window notes[i:i window_size] pattern tuple( window[j1].pitch - window[j].pitch for j in range(len(window) - 1) ) patterns.append(pattern) # 计算模式重复率有多少模式出现了至少2次 pattern_counts {} for p in patterns: pattern_counts[p] pattern_counts.get(p, 0) 1 repeated_patterns sum(1 for count in pattern_counts.values() if count 2) total_patterns len(patterns) repetition_rate repeated_patterns / max(1, total_patterns) # 为什么重复率0.3~0.6是理想范围太低(0.1)没有动机发展 # 太高(0.8)过度重复单调。0.3~0.6表示有适度的发展与回归 # 归一化repetition_rate 0.3→1.0理想0→0.5太少0.8→0.5太多 if repetition_rate 0.3: return 0.5 repetition_rate / 0.3 * 0.5 elif repetition_rate 0.6: return 1.0 else: return max(0.5, 1.0 - (repetition_rate - 0.6) / 0.4 * 0.5) def _extract_chords(self, notes: List[Note]) - List[List[Note]]: 提取同时发声的音符组合为和弦 chords [] # 按时间分组同时发声的音符时间差0.05秒 sorted_notes sorted(notes, keylambda n: n.start_time) current_chord [sorted_notes[0]] if sorted_notes else [] for i in range(1, len(sorted_notes)): time_diff sorted_notes[i].start_time - sorted_notes[i-1].start_time if time_diff 0.05: # 50ms以内视为同时发声 current_chord.append(sorted_notes[i]) else: if len(current_chord) 2: # 至少2个音符才算和弦 chords.append(current_chord) current_chord [sorted_notes[i]] if len(current_chord) 2: chords.append(current_chord) return chords def _has_parallel_fifths(self, chords: List[List[Note]], chord_index: int) - bool: 检测平行五度 if chord_index len(chords) - 1: return False chord1 chords[chord_index] chord2 chords[chord_index 1] # 检查两个和弦是否都有纯五度音程7个半音 has_fifth_in_chord1 any( abs(n1.pitch - n2.pitch) 7 for n1 in chord1 for n2 in chord1 if n1.pitch ! n2.pitch ) has_fifth_in_chord2 any( abs(n1.pitch - n2.pitch) 7 for n1 in chord2 for n2 in chord2 if n1.pitch ! n2.pitch ) if not (has_fifth_in_chord1 and has_fifth_in_chord2): return False # 检查根音是否同方向移动 root1 min(n.pitch for n in chord1) root2 min(n.pitch for n in chord2) prev_root min(n.pitch for n in chords[chord_index - 1]) if chord_index 0 else root1 same_direction (root2 root1 and root1 prev_root) or \ (root2 root1 and root1 prev_root) return same_direction批量评测与模型对比# evaluation/batch_evaluator.py import json from typing import List, Dict from music_evaluator import MusicEvaluator, Note class BatchEvaluator: 批量评测多个模型的生成结果 def __init__(self): self.evaluator MusicEvaluator() def evaluate_models( self, model_outputs: Dict[str, List[List[Note]]], # model_name → 多段音乐的notes key: str C_major ) - ComparisonReport: 对比评测多个模型 results: Dict[str, List[EvalResult]] {} for model_name, outputs in model_outputs.items(): model_results [] for notes in outputs: result self.evaluator.evaluate(notes, duration30, keykey) model_results.append(result) results[model_name] model_results # 计算每个模型的平均分数 averages {} for model_name, model_results in results.items(): averages[model_name] { melody: np.mean([r.melody_coherence for r in model_results]), harmony: np.mean([r.harmonic_validity for r in model_results]), style: np.mean([r.style_consistency for r in model_results]), composite: np.mean([r.composite_score for r in model_results]), # 分数标准差反映模型输出的稳定性 # 为什么看标准差平均分高但标准差大模型输出不稳定 # 有时很好有时很差。平均分中等但标准差小模型输出稳定可靠 std: np.std([r.composite_score for r in model_results]), sample_count: len(model_results), } # 找到最佳模型 best_model max(averages.items(), keylambda x: x[1][composite]) return { model_results: averages, best_model: best_model[0], best_score: best_model[1][composite], recommendation: self._generate_recommendation(averages), } def _generate_recommendation(self, averages: Dict) - str: 生成评测建议 lines [] # 模型排序 ranked sorted(averages.items(), keylambda x: x[1][composite], reverseTrue) lines.append(模型排名综合评分) for model, scores in ranked: lines.append(f {model}: {scores[composite]:.3f} f(旋律{scores[melody]:.3f}, f和声{scores[harmony]:.3f}, f风格{scores[style]:.3f}, f稳定性{1-scores[std]:.3f})) # 弱项分析 worst_melody min(averages.items(), keylambda x: x[1][melody]) worst_harmony min(averages.items(), keylambda x: x[1][harmony]) worst_style min(averages.items(), keylambda x: x[1][style]) if worst_melody[1][melody] 0.5: lines.append(f\n⚠️ {worst_melody[0]}旋律连贯性不足({worst_melody[1][melody]:.3f}) f建议改进动机发展机制) if worst_harmony[1][harmony] 0.5: lines.append(f\n⚠️ {worst_harmony[0]}和声合理性不足({worst_harmony[1][harmony]:.3f}) f建议添加乐理规则约束) if worst_style[1][style] 0.5: lines.append(f\n⚠️ {worst_style[0]}风格一致性不足({worst_style[1][style]:.3f}) f建议强化风格condition信号) return \n.join(lines)评测结果可视化# evaluation/visualizer.py import matplotlib.pyplot as plt import numpy as np def plot_evaluation_radar(models: Dict[str, Dict]): 雷达图展示各模型的三个评测维度 categories [旋律连贯性, 和声合理性, 风格一致性, 输出稳定性] N len(categories) angles [n / float(N) * 2 * np.pi for n in range(N)] angles angles[:1] # 闭合图形 fig, ax plt.subplots(figsize(8, 8), subplot_kwdict(polarTrue)) for model_name, scores in models.items(): values [ scores[melody], scores[harmony], scores[style], 1 - scores[std] # 稳定性1-标准差 ] values values[:1] ax.plot(angles, values, o-, linewidth2, labelmodel_name) ax.fill(angles, values, alpha0.25) ax.set_xticks(angles[:-1]) ax.set_xticklabels(categories) ax.set_ylim(0, 1) ax.legend(locupper right, bbox_to_anchor(1.3, 1.0)) ax.set_title(音乐生成模型评测雷达图) plt.tight_layout() plt.savefig(evaluation_radar.png, dpi150)边界分析与架构权衡客观评测与主观评测的关系客观评测量化音乐是否正确。主观评测量化音乐是否好听。两者不是替代关系客观评测高、主观评测低音乐乐理正确但不好听。常见于规则约束过强的模型生成的音乐像教科书范例。客观评测低、主观评测高音乐好听但不正确。常见于大模型自由生成MOS评分高但风格混乱。生产场景客观评测是底线门槛低于0.5不允许上线主观评测是最终选择两个模型客观评测都达标后选主观评测更高的。调性检测的准确性评测和声合理性时需要知道当前调性。但AI生成的音乐调性可能模糊混合调式或频繁切换。解决方案自动调性检测。用Krumhansl-Schmuckler算法从音符分布推算最可能的调性。推算准确率约85%——15%的模糊调性音乐会被误判。误判的影响把大调音乐误判为小调→和声合理性评分偏低。缓解对模糊调性的音乐降低和声评测权重从0.3降到0.2增加旋律和风格权重。动机重复率的窗口大小4音符窗口检测动机重复。4音符太小可能检测出伪重复巧合相似的短片段。8音符窗口更精确但漏检短动机。解决方案多尺度检测。同时用3音符、4音符、6音符窗口检测取加权平均。3音符权重低容易巧合6音符权重高真正的动机重复。风格标签缺失的问题风格一致性评测需要风格标签爵士/古典/电子等。AI生成的音乐没有标签——需要分类器推断。风格分类器可用预训练模型如MusicBERT。但分类器本身有误判率。误判导致风格切换频率的计算不准确。缓解不用分类器用音乐特征分布的一致性替代。不判断是什么风格判断风格特征是否一致。具体做法计算音程分布、节奏模式、速度分布的JS散度。散度低风格一致散度高风格混乱。不依赖分类器不依赖风格标签。评测数据集的选择评测模型需要标准的音乐数据集作为参考基线。不能只评测AI生成的音乐——需要与真实音乐对比。基线数据集选择训练数据集中的真实音乐最直接的基线。AI模型应该至少达到训练数据集的平均质量。人工创作的参考曲目同一风格/调性的人工音乐作为上限目标。随机生成的音乐作为下限基线。AI模型应该显著超过随机生成。评测维度 | 训练数据平均 | 人工参考上限 | 随机生成下限旋律连贯性 | 0.7 | 0.9 | 0.2和声合理性 | 0.75 | 0.95 | 0.1风格一致性 | 0.65 | 0.85 | 0.15AI模型的目标各维度超过训练数据平均0.1。低于训练数据平均模型没有学到结构。评测的统计显著性评测50段音乐样本。每段音乐30秒。总评测时长25分钟。50个样本的平均分数是否有统计显著性需要至少30个样本才能达到95%置信区间±0.05。50个样本达到±0.03。推荐评测100段样本——置信区间±0.02足以区分模型间的微小差异。总结音乐生成模型评测不能只靠MOS评分。结构化评测框架量化三个客观维度旋律连贯性音程连续性自相关系数动机重复率。权重0.6和0.4。平滑变化随机跳跃。适度重复没有发展或过度重复。和声合理性调内音符比例违规和弦比例平行五度等。违规和弦权重高0.7调外音符容忍度低权重0.3。风格一致性BPM波动幅度变异系数主速度区间占比。变异系数5%正常30%混乱。综合评分加权旋律0.4和声0.3风格0.3。旋律权重最高——听众对旋律混乱的容忍度最低。客观评测是底线门槛0.5不上线主观评测是最终选择。两者互补不替代。批量评测至少50段样本推荐100段。标准差反映输出稳定性——高分高标准差不可靠的模型。模糊调性音乐降低和声评测权重风格标签缺失时用特征分布一致性替代。MOS评分4.2的模型可能旋律跳跃、和声违规、风格混乱。0.7的综合客观评分才能上线——好听不是正确的替代品。资料说明本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论不应视为行业事实。可参考 0731 资料来源索引并在发布前将具体来源贴到对应断言之后。