ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Performance-Timed Music Tokens:让LLM生成音乐更有“人味”

Performance-Timed Music Tokens:让LLM生成音乐更有“人味” 前阵子在研究 LLM 生成音乐时我一直在纠结一个现象让大模型写一段钢琴曲音符、和弦、调式全对但导成 MIDI 一听总觉得“太机械”。后来意识到问题几乎不在音高上而在时间上。人类演奏时乐谱上写着 1 秒的音符实际可能弹了 0.97 秒下一个音可能晚了 30 毫秒才落下。这种微小的、有表现意图的时间偏移就是我们常说的“音乐表情”。而传统符号音乐生成方法把这些偏差全部抹平了。最近读到 Agogic 这个方向的工作提出用 Performance-Timed Music Tokens 来做 LLM-Native 的文本到符号音乐生成。简单来说就是把“乐谱时间”和“演奏时间”一起建模让 LLM 不只是生成正确的音符序列还能生成带有真实表演节奏的音乐。今天这篇文章我会从概念拆解、Token 设计、数据准备、模型训练到推理验证完整梳理这套思路并给出可运行的代码示例。无论是做 AIGC 音乐工具还是在研究 LLM 在符号序列生成上的应用这篇文章都值得收藏。1. 背景为什么 LLM 生成的音乐总缺“人味”1.1 符号音乐生成与 LLM 的结合符号音乐生成Symbolic Music Generation和音频生成有一个本质区别它不直接生成波形而是生成一种符号表示。常见的符号表示包括MIDI 文件记录音符的 pitch、velocity、start time、duration。ABC 记谱法一种基于文本的乐谱表示。MusicXML、MEI结构化的乐谱标记语言。各种 Token 序列把 MIDI 事件映射成离散 token供 Transformer 类模型学习。LLM 天然擅长处理离散序列所以把音乐转成 token 序列后就可以用标准的自回归语言模型来生成。这也是近年 MusicLM、MusicGen 等模型在音频层面流行之前符号音乐生成的主要路线。在符号音乐生成中最常用的 Token 化策略来自 Oore 等人在 2018 年提出的 Performance RNN以及后续的 REMI、CP 等表示法。这类方法把 MIDI 事件拆成Note-On 事件Note-Off 事件Time-Shift 事件表示时间推进Velocity 事件。LLM 的任务就是根据前面的 token预测下一个 token。只要训练数据足够多模型就能学到音符之间的统计关系包括旋律走向、和声功能、节奏型等。1.2 问题的本质谱面时间与演奏时间是两回事这是理解 Agogic 设计动机的关键。乐谱上的“四分之一音符”只是一个量化后的符号。真实演奏中这个音符的时长取决于演奏者的处理。在音乐术语里这种有意偏离严格节拍的现象叫Agogic中文常译为“缓急法”或“弹性速度”。它强调的不是节奏上的“错”而是时间上的“动力”。如果我们把一首钢琴曲的 MIDI 导出后和原始乐谱对齐会发现两种时间Score Time谱面时间理论上每个音符应当发生的时刻。Performance Time演奏时间演奏时实际发生的时刻。两者之间的差值在音乐信息检索MIR领域通常叫作 timing deviation。对于人类听觉来说这种 deviation 恰恰是“音乐感”的来源之一。同一个乐谱专业钢琴家演奏时旋律声部的音符可能稍微提前几毫秒伴奏声部稍微靠后让旋律在听觉上更突出。如果把这些偏差全部去掉变成严格的量化 MIDI音乐就变成了节拍器。传统符号音乐生成往往把 token 建立在固定的时间网格上比如每个 time-shift 代表 10ms 或 30ms。这样确实能表达一定的时值精度但它有两个问题网格量化会丢失亚网格级别的 timing 信息token 序列会把“谱面时间”和“演奏时间”混在一起模型很难区分“这个音应该落在第几拍”和“这个音实际落在第几拍”。因此生成的音乐在结构上正确但听感上缺乏律动和呼吸感。1.3 Agogic 的核心思路Agogic 的切入点就是把演奏时间从谱面时间中分离出来显式编码成一种独立的信息。按照题目中的描述可以理解为设计一种Performance-Timed Music Token把音符事件与实际的演奏时间戳绑定。例如不再只记录“第 10 个 time-shift 处有一个 C4 音符”而是记录“C4 音符在第 2.173 秒处开始持续 0.836 秒”。这样LLM 学习到的就不是一个量化后的离散节奏型而是人类演奏中真实的时间分布。模型在生成时不仅预测音高还预测每个音符的时间偏移和实际时长。这种做法带来的好处非常明显生成的 MIDI 可以直接带着表情信息不需要后处理“拟人化”。训练目标更贴近真实演奏数据。可以通过控制 timing token让模型生成“机器人模式”或“情感模式”的演奏。下面我们把这个思路落到具体实现上。2. 环境准备与版本说明本文的示例代码以 Python 为核心环境重点演示 Token 构造、数据预处理和生成推理。因为是偏研究方向的代码所以不依赖某个具体的大模型框架而是以 PyTorch 为例构建一个最小可运行的 Transformer decoder。2.1 环境依赖python3.10 torch2.0 mido1.3.0 pretty_midi0.2.10 numpy tqdm其中mido和pretty_midi用于读取 MIDI 文件提取音符事件和真实时间戳。torch用于搭建生成模型。安装命令pip install mido pretty_midi numpy torch tqdm如果你已经安装了更高版本的 PyTorch可以保留现有版本不影响本文示例。2.2 示例项目结构我建议把实验代码按下面的结构组织agogic_demo/ ├── tokenizer.py # Performance-Timed Token 的构造与解析 ├── dataset.py # MIDI 预处理与训练数据集 ├── model.py # 基于 Transformer 的最小生成模型 ├── generate.py # 推理与采样 ├── data/ │ └── example.mid # 示例 MIDI 文件 └── output/ └── generated.mid # 生成结果版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示设计思路。如果你在安装依赖时遇到冲突建议使用虚拟环境隔离。3. 核心设计Performance-Timed Music Token3.1 传统 Token 方案存在什么问题以 REMI 风格为例子一个音符序列可能长这样Bar_Start Pitch_C4 Time_Shift_1 Note_On如果时间精度为 10ms一个持续 250ms 的音符会被编码成 25 个 time-shift token。在长乐曲中这种 token 序列会非常长。而且time-shift 只能表达 10ms 的整数倍真实演奏中“第 237ms 开始”这种信息会被四舍五入。更重要的是谱面时间应该落在第几拍和表演时间实际在哪个 ms 出现被混在同一个 time-shift 维度中。模型既要用它表达节拍结构又要用它表达微小的 timing 偏差学习负担明显增加。3.2 把“表演时间”拆成独立的 Token 维度Agogic 风格的 Token 设计可以抽象成下面几个事件类型事件类型含义示例Pitch音高Pitch_C4Onset-Time实际起始时间秒Onset_2.173Duration实际持续时长秒Dur_0.836Velocity力度Vel_68Bar小节信息可选Bar_StartTrack声部信息可选Track_Piano在具体实现时时间值不一定要用浮点数直接作为 token。因为浮点数空间是连续的不适合直接作为离散 token。更合理的做法是采用一个高精度时间网格但网格粒度比传统方案细得多并且在 token 中明确区分“谱面时间”和“表演时间”。例如可以定义两种时间推进 tokenBeatTimeShift推进谱面节拍单位为四分音符。PerfTimeShift推进真实时间单位为 1ms 或 5ms。模型生成时先通过 BeatTimeShift 维持小节和拍号结构再通过 PerfTimeShift 表达音符真实的 onset 偏移。这样结构信息和表演信息各司其职。3.3 一个最小 Tokenizer 实现下面我们写一个极简版的 Performance-Timed Tokenizer。它会把一条音符列表转换成 token 序列。# 文件路径agogic_demo/tokenizer.py import numpy as np PITCH_TOKENS [fPitch_{i} for i in range(0, 128)] VEL_TOKENS [fVel_{i} for i in range(0, 128)] # 真实时间精度5ms 一个单位 PERF_TIME_UNIT 0.005 class PerformanceTimedTokenizer: def __init__(self, time_resolutionPERF_TIME_UNIT): self.time_resolution time_resolution def encode_time(self, seconds: float) - str: 把秒为单位的时间戳量化成 token 下标。 idx int(round(seconds / self.time_resolution)) return fTime_{idx} def decode_time(self, token: str) - float: 把 Time token 还原成秒。 idx int(token.split(_)[1]) return idx * self.time_resolution def encode(self, notes): notes: list of dict, 每个元素包含 pitch: int velocity: int onset: float, 演奏起始时间(秒) duration: float, 演奏持续时长(秒) 返回: list[str] tokens [] # 假设所有音符已经按 onset 排序 prev_onset 0.0 for note in notes: # 1. 输出当前音符与上一个音符之间的时间偏移 gap note[onset] - prev_onset tokens.append(self.encode_time(gap)) # 2. 输出音高、力度、时长 tokens.append(PITCH_TOKENS[note[pitch]]) tokens.append(VEL_TOKENS[note[velocity]]) tokens.append(self.encode_time(note[duration])) prev_onset note[onset] tokens.append(EOS) return tokens def decode(self, tokens): 从 token 序列恢复音符列表用于后续转 MIDI。 notes [] current_time 0.0 i 0 while i len(tokens): tok tokens[i] if tok EOS: break if tok.startswith(Time_): delta self.decode_time(tok) current_time delta i 1 continue if tok.startswith(Pitch_): pitch int(tok.split(_)[1]) velocity_token tokens[i 1] duration_token tokens[i 2] velocity int(velocity_token.split(_)[1]) duration self.decode_time(duration_token) notes.append({ pitch: pitch, velocity: velocity, onset: current_time, duration: duration, }) i 3 continue i 1 return notes这个 tokenizer 的特点是每个音符前面的 Time token 表示“距离上一个音符的真实时间差”没有任何量化到节拍网格的过程。因此它能保留训练数据中真实的演奏时间信息。我们简单测试一下notes_example [ {pitch: 60, velocity: 72, onset: 0.0, duration: 0.5}, {pitch: 64, velocity: 70, onset: 0.52, duration: 0.48}, {pitch: 67, velocity: 74, onset: 1.05, duration: 1.2}, ] tk PerformanceTimedTokenizer() tokens tk.encode(notes_example) print(tokens) # 输出示例: [Time_0, Pitch_60, Vel_72, Time_100, Time_104, Pitch_64, ...]注意Time_100表示 100 × 0.005 0.5 秒。从这段代码可以看到token 序列本身并不复杂复杂的在于训练数据如何从 MIDI 中提取真实时间戳以及模型如何在生成时决定输出的时间分布。3.4 与标准 LLM 生成框架的融合Performance-Timed Music Token 本质上还是一个离散 token 序列所以完全可以套用标准的 LLM 训练范式用 BPE 或 WordPiece 对 token 序列做 subword 切分用 decoder-only Transformer 做语言建模训练目标是最小化下一个 token 的交叉熵损失推理时用 temperature sampling、top-k、top-p 等策略。这一点很关键。它意味着 Agogic 不是一个全新的模型架构而是对训练目标、数据表示和 token 词典的重新设计。任何熟悉 LLM 训练的团队都可以在现有框架上接入这种 token 方案。4. 数据准备从 MIDI 到 Token 序列4.1 需要什么样的训练数据既然要学习“演奏时间”训练数据就不能是单纯的乐谱 MIDI而必须是带有真实演奏信息的 MIDI。常见的数据集包括MAESTRO包含超过 200 小时的专业钢琴演奏通过 Yamaha Disklavier 录制MIDI 和音频严格对齐。ASAP包含流行钢琴曲的乐谱与演奏 MIDI 对齐数据。Bach Cello Suites 等公开演奏 MIDI适合专项风格训练。如果数据集中只包含量化后的 MIDI例如从乐谱软件直接导出的 MIDI那么 Performance-Timed Token 就退化为普通 token学不到真实的 agogic 信息。因此数据质量是整个方案的基础。4.2 用 pretty_midi 提取真实时间戳下面是一个加载 MIDI 并提取音符列表的脚本。# 文件路径agogic_demo/dataset.py import pretty_midi def midi_to_notes(midi_file: str): 从 MIDI 文件中提取音符并排序。 midi pretty_midi.PrettyMIDI(midi_file) notes [] for instrument in midi.instruments: if instrument.is_drum: continue for note in instrument.notes: notes.append({ pitch: note.pitch, velocity: note.velocity, onset: note.start, duration: note.end - note.start, instrument: instrument.program, }) notes.sort(keylambda x: x[onset]) return notes这段代码会为每个音符记录真实的start和end。pretty_midi读取 MIDI 时会保留文件中的真实时间信息如果源 MIDI 不是量化生成的这些时间戳通常带有丰富的演奏 timing。在实际处理中还需要做几件事去除重叠音符。LLM 生成时自回归方式更容易处理“同一时刻多个音符”的序列化形式。多轨数据需要决定如何混音或分轨。如果训练目标是钢琴独奏可以只保留钢琴轨。过滤过长或过短的音符避免异常值干扰 token 分布。4.3 构建训练样本假设我们已经有了大量 MIDI 文件可以按下面方式构建模型输入。# 文件路径agogic_demo/dataset.py import torch from torch.utils.data import Dataset class TokenDataset(Dataset): def __init__(self, midi_files, tokenizer, seq_len512): self.samples [] for midi_file in midi_files: notes midi_to_notes(midi_file) if len(notes) 10: continue tokens tokenizer.encode(notes) for i in range(0, len(tokens) - seq_len, seq_len // 2): self.samples.append(tokens[i:i seq_len]) self.tokenizer tokenizer self.seq_len seq_len def __len__(self): return len(self.samples) def __getitem__(self, idx): sample self.samples[idx] ids self.tokenizer.tokens_to_ids(sample) return torch.tensor(ids, dtypetorch.long)这只是一个示例思路。实际项目中tokens_to_ids需要维护一个完整的词表映射把Pitch_60、Time_12这类字符串映射成整型 id。更高效的做法是直接让 tokenizer 返回整型 id 序列而不是字符串序列。此外训练时建议使用滑动窗口采样让模型看到不同位置的上下文推理时则保留完整的 token 序列逐步生成。5. 模型搭建与生成推理5.1 最小 Transformer Decoder我们用一个非常小的 Transformer decoder 来演示。模型接收 token id 序列输出下一个 token 的分布。# 文件路径agogic_demo/model.py import torch import torch.nn as nn import math class MusicTransformer(nn.Module): def __init__(self, vocab_size, d_model256, nhead4, num_layers4, max_len1024): super().__init__() self.token_embedding nn.Embedding(vocab_size, d_model) self.position_embedding nn.Embedding(max_len, d_model) self.dropout nn.Dropout(0.1) decoder_layer nn.TransformerDecoderLayer( d_modeld_model, nheadnhead, dim_feedforward512, batch_firstTrue, ) self.decoder nn.TransformerDecoder(decoder_layer, num_layersnum_layers) self.lm_head nn.Linear(d_model, vocab_size) self.max_len max_len self.d_model d_model def forward(self, x): # x: [batch, seq_len] seq_len x.size(1) positions torch.arange(seq_len, devicex.device).unsqueeze(0) x self.dropout(self.token_embedding(x) self.position_embedding(positions)) tgt_mask self._generate_square_subsequent_mask(seq_len, x.device) x self.decoder(x, x, tgt_masktgt_mask) logits self.lm_head(x) return logits def _generate_square_subsequent_mask(self, size, device): mask torch.triu(torch.ones(size, size, devicedevice), diagonal1) mask mask.masked_fill(mask 1, float(-inf)) return mask这里用了 TransformerDecoder并传入tgt_mask保证自回归特性。position_embedding使用可学习的位置编码。对于简单的演示这样已经足够。如果要更贴近现代 LLM 的实践可以替换为 GPT 风格的 decoder-only 结构使用因果自注意力。上面的代码只做流程演示不是性能最优方案。5.2 训练循环训练时需要把 token 序列分成输入和标签。输入是tokens[:-1]标签是tokens[1:]。# 文件路径agogic_demo/train.py核心片段 import torch from torch.utils.data import DataLoader def train_one_epoch(model, dataloader, optimizer, criterion, device): model.train() total_loss 0.0 for batch in dataloader: # batch: [batch, seq_len] src batch[:, :-1].to(device) tgt batch[:, 1:].to(device) logits model(src) # [batch, seq_len, vocab_size] loss criterion(logits.reshape(-1, logits.size(-1)), tgt.reshape(-1)) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / max(len(dataloader), 1)这是一个比较标准的语言模型训练循环。如果你的训练数据量很大还可以加入梯度累积、学习率预热、混合精度训练等策略。这里要特别说明因为我们的 token 序列里Time token 和 Pitch token 是交替出现的模型需要学习“演奏时间何时推进、推进多少”的分布。训练数据里真实演奏的时间分布越丰富模型生成的音乐就越有“人味”。5.3 推理与采样生成时我们从起始 token 开始反复把已生成的序列输入模型采样下一个 token直到生成 EOS 或达到最大长度。# 文件路径agogic_demo/generate.py import torch import torch.nn.functional as F torch.no_grad() def generate(model, tokenizer, start_tokens, max_len512, temperature1.0, top_k40, top_p0.9): model.eval() device next(model.parameters()).device ids tokenizer.tokens_to_ids(start_tokens) generated ids[:] for _ in range(max_len): input_ids torch.tensor([generated[-model.max_len:]], devicedevice) logits model(input_ids)[0, -1, :] / temperature # top-k 过滤 if top_k 0: values, _ torch.topk(logits, top_k) logits[logits values[-1]] float(-inf) # top-p 过滤 if top_p 1.0: sorted_logits, sorted_indices torch.sort(logits, descendingTrue) cumulative_probs torch.cumsum(F.softmax(sorted_logits, dim-1), dim-1) remove_idx cumulative_probs top_p remove_idx[1:] remove_idx[:-1].clone() remove_idx[0] False logits[sorted_indices[remove_idx]] float(-inf) probs F.softmax(logits, dim-1) next_id torch.multinomial(probs, num_samples1).item() generated.append(next_id) if tokenizer.ids_to_token(next_id) EOS: break return generated采样时temperature控制随机性。temperature 越小生成越保守越大越可能有意外但有趣的节奏变化。实际项目里可以针对 Time token 和 Pitch token 设置不同的 temperature例如Pitch token 使用较低 temperature保证音高结构的稳定性Time token 使用较高温度让演奏时间的分布更多样。这种“按 token 类型控制采样参数”的方法在符号音乐生成中很实用。5.4 把生成结果转回 MIDI生成 token 序列后调用 tokenizer 的decode方法还原音符列表再用pretty_midi输出 MIDI。import pretty_midi def save_notes_to_midi(notes, output_path): midi pretty_midi.PrettyMIDI() piano pretty_midi.Instrument(program0) for note in notes: midi_note pretty_midi.Note( velocitynote[velocity], pitchnote[pitch], startnote[onset], endnote[onset] note[duration], ) piano.notes.append(midi_note) midi.instruments.append(piano) midi.write(output_path)这样生成出来的 MIDI 文件天然就带有演奏时间信息。导入 DAW 或采样器后无需再做 humanize 处理就能直接听出“呼吸感”。6. 评估如何判断生成结果是否更好6.1 传统评估指标符号音乐生成常用的自动指标包括Token Accuracy生成 token 与真实 token 的匹配程度适合判断模型是否收敛。Pitch Accuracy预测音高是否正确。Duration Accuracy预测时值是否正确。Valid Rate生成序列是否符合语法规则例如事件顺序是否正确。这些指标能反映“结构正确性”但无法反映“表演时间是否自然”。6.2 面向表演时间的评估指标针对 Performance-Timed Token需要增加以下指标Onset Deviation生成音符的 onset 与谱面 onset 的偏差分布。如果偏差接近训练集中人类演奏的分布就说明模型学到了 agogic 特征。Duration Deviation生成音符时长与谱面时长的偏差。Timing Consistency同一主题或乐句重复出现时时间偏移是否具有一致性。Rubato Curve把每一拍的实际时间间隔画成曲线观察是否有符合人类演奏习惯的渐快、渐慢。这些指标可以帮助我们筛选更好的模型 checkpoint也可以用来分析模型是否“过度随机”或“过于机械”。下面是一个简单示例计算生成的音符序列中onset 差值的标准差。import numpy as np def compute_onset_jitter(notes): 计算相邻音符 onset 间隔的标准差。 onsets [note[onset] for note in notes] intervals np.diff(onsets) return float(np.std(intervals))如果你比较传统量化 token 方案和 Performance-Timed 方案生成的 MIDI会发现后者在onset_jitter、时长分布上更接近真实演奏数据。7. 常见问题与排查思路7.1 生成的音乐时间混乱节拍感丢失问题现象常见原因解决思路生成的音乐听起来没有稳定节拍Time token 和 Beat token 混在一起模型没有学到节拍层级引入显式的 Bar token 或 Beat token强化节拍结构音符之间停顿过长时间 token 的分布过于均匀模型生成大间隔的概率被放大调整 Time token 的采样 temperature加长训练数据中的时间上下文音高正确但节奏怪异训练数据中真实演奏的偏差过大模型把异常值当成常规分布清洗数据集去除极端 timing outlier在实践中最有效的做法是在 token 序列里同时加入“小节线”和“拍号”信息。LLM 首先学会这个小节内有多少拍再预测每个音符落在哪一拍附近。否则模型很容易迷失在大量 Time token 中。7.2 训练时 loss 下降慢问题现象常见原因解决思路Token 序列过长模型难以收敛5ms 精度的 time 分辨率导致序列长度爆炸先用 10ms 或 20ms 分辨率起步后期再提升精度模型总是预测常见 token词表不平衡Pitch 和 Time token 数量差距过大对 token 类型分组计算 loss或使用类别加权生成结果重复自回归模型陷入重复循环增加 no-repeat-n-gram 限制或调整采样 top-p时间分辨率的设定需要权衡太粗会丢失微妙的 agogic 信息太细则序列过长、训练成本高。建议先做一个 10ms 精度的 baseline再测试 5ms 或 2ms 精度的效果差异。7.3 推理时时间累积误差自回归生成时模型通过 Time token 累加当前时间。如果模型预测的时间 token 稍微偏大误差会不断累积导致一首曲子越到后面越快或越慢。解决方案有三种在解码阶段加一个时间状态校准模块如果累计时间超过当前小节长度自动纠正到下一小节起点采用绝对时间戳 token而不是相对时间差 token把小节视为独立的生成单位在每个小节开始时重置时间状态。第一种和第三种在当前论文和开源项目中都比较常见推荐新手先尝试第三种。8. 最佳实践与工程建议8.1 数据质量优先于模型规模Performance-Timed Token 的价值完全依赖于训练数据中是否包含真实的演奏时间。如果数据本身就是量化过的 MIDI那么模型学不到任何微妙的 agojic 信息无论模型多大都没有意义。因此在开始训练之前建议先做一次数据可视化。随机抽取一批 MIDI打印音符的 onset 间隔分布看看是不是像节拍器一样整齐还是带有自然波动。只有后者才适合训练 Performance-Timed 方案。8.2 把时间 token 独立对待在实现中我建议把 token 词表分成几个类型组Pitch、Velocity、Time、Bar等。这样做的原因是可以针对不同类型 token 设计不同的采样策略可以分组计算 loss观察模型在哪类 token 上表现最差可以为时间 token 设置更高的分辨率为 pitch token 保持较小的词表。例如在训练时可以只对 Time token 计算额外的损失权重让模型把更多 capacity 花在学习时间分布上。8.3 安全与授权问题如果你要使用公开数据集务必确认数据集的许可协议。MAESTRO 数据集有明确的许可要求ASAP 等数据集也需要参照作者的使用条款。如果是企业项目建议先用已授权或自采的数据进行实验再考虑对外发布模型权重。同时训练和评估过程中涉及的模型权重、数据文件、生成 MIDI 都属于项目资产建议做好版本管理。模型文件较大时可以使用 Git LFS 或对象存储避免仓库膨胀。8.4 生产环境的推理设计如果要把这种生成能力集成到实际产品中需要考虑以下几点生成延迟LLM 自回归生成 token 是一个 token 一个 token 来的长音乐可能需要较多时间。可以考虑蒸馏成小模型或用 prefetch 机制提前生成片段。可控性用户可能希望指定调号、BPM、风格。可以在输入中加入条件 token例如Tempo_120、Style_Jazz让模型按照条件生成。失败兜底生成结果可能不稳定。建议让产品层提供“重新生成”“强度调节”等功能而不是把所有结果都直接出给用户。日志与监控记录生成请求的时间、模型版本、采样参数、生成结果长度便于线上问题回溯。8.5 可解释性与人工评估自动指标再丰富最后还是要回归听觉。建议在项目中期引入人工评估让熟悉音乐的人对“节奏自然度”“情感表达”“结构清晰度”打分。很多时候模型生成的 token 序列在指标上很漂亮但听起来并不自然。这种时候优先检查时间偏差的分布是否过度集中在几个离散值上。如果生成结果听起来太“碎”可以把 Time 分辨率调粗一点如果听起来太“死”可以增加 Time token 的采样温度或者调整训练数据中演奏风格的配比。9. 总结与实践建议这篇文章从 Agogic 的角度完整拆解了如何为 LLM 原生文本到符号音乐生成设计 Performance-Timed Music Tokens。核心思想并不复杂把谱面时间与演奏时间分开建模让 token 序列承载真实演奏中的微小时间偏移。你需要掌握的要点包括传统符号音乐生成的 token 化方法无法充分建模人类演奏中的 agogic 时间偏差Performance-Timed Token 通过高分辨率时间 token显式保留演奏时间信息它可以无缝接入标准 LLM 训练框架不需要发明新的模型架构数据质量决定了这个方案的上限训练数据必须是真实演奏 MIDI评估时除了 pitch accuracy还要关注 onset deviation、duration deviation 等时间相关指标。下一步建议你先用一个小数据集跑通整个流程读 MIDI → 提取真实时间 → 构造 token → 训练小模型 → 生成 MIDI → 转成音频试听。在试听过程中你可以对比量化方案和 Performance-Timed 方案的差异。只要数据集里真的有“人味”差别会非常明显。实际项目中优先关注数据采集和数据清洗然后才是模型调参。如果时间 token 设计不合理后面的训练和推理都会遇到问题。希望这篇文章能帮你在 LLM 音乐生成这条路上少踩一些坑。
返回列表