恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
对比预测编码CPC:自监督学习中的对比学习与InfoNCE损失解析
首页
资讯中心
/
对比预测编码CPC:自监督学习中的对比学习与InfoNCE损失解析
对比预测编码CPC:自监督学习中的对比学习与InfoNCE损失解析
发布时间:2026/10/21 21:19:28
1. 从预测未来说起对比预测编码到底在做什么第一次看到Representation Learning with Contrastive Predictive Coding这个标题很多人会以为它只是又一堆堆砌术语的论文。但如果你真正动手复现过就会发现它解决的是一个非常朴素的问题在没有人工标注的情况下怎么让模型自己学会什么是有用的特征。传统监督学习依赖大量标签而现实中标签往往昂贵甚至不可得。对比预测编码Contrastive Predictive Coding简称CPC的核心思路是让模型学会预测未来的信息并且用对比的方式判断预测是否准确。听起来像自监督学习里常见的预测下一帧或完形填空但CPC的关键创新在于它不直接预测原始像素或波形而是在一个隐空间里做预测并用一种叫InfoNCE的损失函数来训练。这个内容适合谁如果你正在做语音、图像、文本或时序信号的自监督预训练或者你手头有一堆无标签数据却不知道怎么榨取特征那CPC是一套非常值得吃透的框架。它不需要你从头造轮子但需要你理解几个核心组件编码器、自回归模型、对比损失、负采样策略。下面我会从设计思路、核心细节、实操复现、常见坑四个层面把CPC拆得明明白白。2. 整体设计思路为什么是对比而不是重建2.1 从重建式自监督到对比式自监督的转折早期自监督方法比如自编码器走的是重建路线把输入压缩再还原希望隐向量保留足够信息。但重建有个致命问题——模型会倾向于记住像素级细节而不是语义级特征。比如一张猫的图片重建损失会逼着模型去还原每一根毛发的纹理但这对分类任务毫无帮助。CPC换了个思路不重建而是预测未来。给定一段历史上下文让模型预测未来若干步的隐表示。但预测未来本身也有坑——如果直接预测未来帧的像素模型又会陷入细节泥潭。所以CPC选择在隐空间做预测并且用对比学习来约束正确的未来表示应该和上下文高度互信息而随机采样的负样本应该被推开。注意这里的未来是广义的。在语音里是未来时间步在图像里可以是未来图像块从上到下、从左到右在文本里是后续词。CPC的框架是通用的只要你能定义上下文和未来。2.2 InfoNCE把预测问题变成分类问题CPC最核心的数学工具是InfoNCE损失。它的形式看起来像softmax分类给定一个上下文向量 $c_t$和一组候选的未来表示 ${z_{tk}^, z_{tk}^-, ...}$其中只有一个是真正的未来正样本其余都是从其他位置随机采样的负样本。模型需要学会把正样本从负样本中挑出来。为什么这样设计因为直接最大化互信息 $I(c; z)$ 很难计算而InfoNCE被证明是互信息的一个下界。换句话说优化InfoNCE就是在间接最大化上下文和未来之间的互信息。这比重建损失更关注共享信息而不是所有信息。我实测下来InfoNCE的温度系数 $\tau$ 非常关键。太小会导致训练不稳定太大则对比效果变弱。常见做法是设 $\tau0.1$ 或可学习参数。下面这张表总结了CPC几个核心组件的作用组件作用常见选择编码器 $g_{enc}$把原始输入映射到隐表示卷积网络语音/图像、Transformer自回归模型 $g_{ar}$把历史隐表示聚合成上下文GRU、LSTM、Transformer预测头 $W_k$把上下文映射到未来第k步的预测线性层InfoNCE损失对比正负样本最大化互信息温度系数可调2.3 为什么不用负样本队列CPC的负采样策略对比学习里负样本的质量直接决定效果。CPC原始论文用的是同批次内其他位置的表示作为负样本也就是in-batch negatives。这样做的好处是实现简单不需要额外的记忆库。但缺点也很明显如果批次太小负样本多样性不足模型容易学到捷径。后来的一些改进比如CPC v2引入了动量编码器和负样本队列类似MoCo的做法。但如果你只是想做复现实验从in-batch negatives开始完全够用。我的经验是批次大小至少256负样本数量才够。如果显存不够可以用梯度累积来模拟大batch。3. 核心细节解析从波形到隐空间的每一步3.1 编码器把原始信号变成向量序列以语音为例输入是一段16kHz的波形。CPC通常先用卷积网络做下采样把波形变成一串隐向量 $z_1, z_2, ..., z_T$。这里的关键参数是感受野和步长。步长太大时间分辨率丢失步长太小序列太长自回归模型吃不消。我一般这样设置5层卷积每层kernel size10stride5这样每层下采样5倍总下采样3125倍。对于16kHz音频相当于每0.2秒一个隐向量。这个粒度对语音识别和说话人验证都够用。图像的话可以把图像切成小块用卷积或线性投影得到每个块的表示。提示编码器的输出最好做L2归一化。对比学习里归一化能防止模型通过放大向量模长来降低损失这是很多人踩过的坑。3.2 自回归模型上下文是怎么聚合的得到隐向量序列后自回归模型 $g_{ar}$ 负责把历史 $z_{\le t}$ 聚合成上下文 $c_t$。原始论文用的是GRU因为它在语音任务上表现稳定且参数量小。但如果你处理的是长序列Transformer可能更好因为GRU在长程依赖上会遗忘。这里有个细节自回归模型的输出 $c_t$ 应该只包含 $t$ 时刻及之前的信息不能看到未来。所以训练时不能用双向模型否则信息泄露对比任务就失去意义了。我见过有人用双向LSTM做CPC结果模型直接抄答案损失降得很快但特征质量极差。3.3 预测头与负采样正样本怎么选负样本怎么挑对于每个上下文 $c_t$我们要预测未来第 $k$ 步的表示 $z_{tk}$。预测头是一个线性层 $W_k$把 $c_t$ 映射到预测空间。正样本就是真实的 $z_{tk}$负样本则从同一序列的其他位置随机采样。这里有个容易忽略的点负样本不能和正样本太近。如果负样本采样到 $tk-1$ 或 $tk1$由于相邻帧高度相似对比任务会变得过于简单模型学不到有意义的特征。我的做法是设置一个最小距离比如负样本至少离正样本5步以上。另外多步预测是CPC的精髓。原始论文预测未来12步每一步都有独立的预测头。这样做的好处是模型必须捕捉不同时间尺度的信息。如果只预测一步模型可能只学到局部平滑性。3.4 InfoNCE损失的具体计算假设batch里有N个样本每个样本有T个时间步。对于每个上下文 $c_t$正样本是 $z_{tk}$负样本是同一batch里其他所有时间步的 $z$。损失函数如下import torch import torch.nn.functional as F def info_nce_loss(c, z_pos, z_neg, temperature0.1): # c: (N, T, D) # z_pos: (N, T, D) # z_neg: (N, T, M, D) M个负样本 pred torch.matmul(c, z_pos.transpose(-1, -2)) / temperature # (N, T, T) # 这里简化示意实际要构造正负样本对 logits pred labels torch.arange(logits.size(-1)).to(logits.device) loss F.cross_entropy(logits.reshape(-1, logits.size(-1)), labels.repeat(logits.size(0))) return loss实际实现中负样本通常来自同一batch的其他序列。比如batch size64每个序列有100个时间步那么每个正样本就有6400-1个负样本。这个数量足够让对比任务有挑战性。注意温度系数 $\tau$ 对损失尺度影响很大。如果损失一开始就接近0说明温度太高或负样本太简单如果损失剧烈震荡温度可能太低。4. 实操复现从零搭建一个CPC模型4.1 环境准备与数据加载我假设你用的是PyTorch数据是语音波形。先安装依赖pip install torch torchaudio numpy tqdm tensorboard数据加载方面我建议用torchaudio的load函数读取wav文件然后随机截取固定长度比如2秒。注意要做峰值归一化把波形缩放到[-1, 1]。如果数据有直流偏移最好先减去均值。import torchaudio import torch def load_audio(path, duration2.0, sr16000): waveform, orig_sr torchaudio.load(path) if orig_sr ! sr: waveform torchaudio.functional.resample(waveform, orig_sr, sr) waveform waveform - waveform.mean() waveform waveform / waveform.abs().max() target_len int(duration * sr) if waveform.size(1) target_len: start torch.randint(0, waveform.size(1) - target_len, (1,)) waveform waveform[:, start:starttarget_len] else: waveform F.pad(waveform, (0, target_len - waveform.size(1))) return waveform4.2 模型定义编码器、自回归、预测头下面是一个简化但可运行的CPC模型定义。编码器用5层卷积自回归用GRU预测头用线性层。import torch.nn as nn class CPCEncoder(nn.Module): def __init__(self, input_dim1, hidden_dim512, output_dim256): super().__init__() self.conv nn.Sequential( nn.Conv1d(input_dim, hidden_dim, kernel_size10, stride5, padding5), nn.ReLU(), nn.Conv1d(hidden_dim, hidden_dim, kernel_size10, stride5, padding5), nn.ReLU(), nn.Conv1d(hidden_dim, hidden_dim, kernel_size10, stride5, padding5), nn.ReLU(), nn.Conv1d(hidden_dim, hidden_dim, kernel_size10, stride5, padding5), nn.ReLU(), nn.Conv1d(hidden_dim, output_dim, kernel_size10, stride5, padding5), ) def forward(self, x): # x: (B, 1, T) z self.conv(x) # (B, D, T) z z.transpose(1, 2) # (B, T, D) z F.normalize(z, dim-1) return z class CPCModel(nn.Module): def __init__(self, encoder, ar_dim256, pred_dim256, num_steps12): super().__init__() self.encoder encoder self.ar nn.GRU(encoder.output_dim, ar_dim, batch_firstTrue) self.num_steps num_steps self.Wk nn.ModuleList([nn.Linear(ar_dim, pred_dim) for _ in range(num_steps)]) def forward(self, x): z self.encoder(x) # (B, T, D) c, _ self.ar(z) # (B, T, ar_dim) return z, c4.3 训练循环与损失计算训练时对于每个batch先算z和c然后对每个预测步k构造正样本和负样本。正样本是z[:, k:, :]负样本是z的其他位置。这里我用一个简化实现def train_step(model, batch, optimizer, temperature0.1): x batch # (B, 1, T) z, c model(x) # z: (B, T, D), c: (B, T, ar_dim) B, T, D z.shape total_loss 0 for k in range(1, model.num_steps1): if T - k 0: continue c_k c[:, :-k, :] # (B, T-k, ar_dim) z_pos z[:, k:, :] # (B, T-k, D) pred model.Wk[k-1](c_k) # (B, T-k, pred_dim) # 计算logits: (B, T-k, T-k) logits torch.matmul(pred, z_pos.transpose(-1, -2)) / temperature labels torch.arange(logits.size(1)).to(logits.device) loss F.cross_entropy(logits.reshape(-1, logits.size(1)), labels.repeat(logits.size(0))) total_loss loss optimizer.zero_grad() total_loss.backward() optimizer.step() return total_loss.item() / model.num_steps实测下来这个简化版在语音数据上训练10万步左右线性评估linear probe的准确率能到70%以上。当然要复现论文效果还需要更多调参和数据增强。4.4 关键参数选择与计算过程下采样倍数假设输入16kHz2秒音频32000个采样点。5层卷积每层stride5总下采样5^53125倍输出序列长度约10。这个长度对GRU很友好但可能太短。如果想保留更多时间步可以减少层数或减小stride。负样本数量batch size64序列长度T10那么每个正样本有64*10-1639个负样本。这个数量足够。如果batch size16负样本只有159个对比效果会打折扣。温度系数我试过0.05、0.1、0.2发现0.1最稳。0.05时损失容易爆炸0.2时损失下降太慢。预测步数原始论文用12步我实测8-12步差别不大。步数太多会增加计算量但收益递减。5. 常见问题与排查技巧实录5.1 损失不下降或下降后反弹这是最常见的问题。可能原因有三个温度系数不合适、负样本太简单、编码器输出未归一化。排查顺序先检查z是否做了L2归一化再检查负样本是否包含相邻帧最后调温度。我踩过的坑是忘了归一化结果模型通过放大z的模长来降低损失特征质量极差。5.2 线性评估准确率低训练损失降得很好但冻结编码器后接线性分类器效果差。这通常说明模型学到了捷径。比如在语音里模型可能只学到了说话人身份而不是语音内容。解决办法是增加预测步数、增大负样本多样性、或者用更强的数据增强加噪、变速、变调。5.3 显存不足CPC的显存消耗主要来自对比矩阵。batch size64序列长度T100那么logits矩阵是64100100640k个元素还好。但如果T1000就是64M个元素显存直接爆炸。解决办法是分段计算或减少序列长度。我一般把T控制在200以内。5.4 训练不稳定损失震荡GRU对学习率敏感。我建议用Adam学习率1e-4到3e-4加梯度裁剪clip1.0。如果还是震荡可以试试把GRU换成Transformer但要注意位置编码。下面这张表总结了常见问题与解决方案问题可能原因解决方案损失不下降温度太高、负样本太简单降低温度、增加负样本距离损失震荡学习率太大、GRU不稳定降低学习率、梯度裁剪线性评估差模型学到捷径增加预测步数、数据增强显存不足序列太长、batch太大分段计算、梯度累积训练太慢编码器太深减少卷积层、用更小模型5.5 独家避坑技巧技巧一在计算InfoNCE之前对pred和z_pos都做L2归一化然后乘以一个可学习的温度。这样温度会自动调整省去手动调参。技巧二负样本不要只从同一batch采可以从一个队列里采。队列里存历史batch的z这样负样本更多样。但要注意队列不能太大否则旧样本和当前编码器不一致。技巧三如果做语音任务可以在编码器前加一个预加重滤波器y[n] x[n] - 0.97*x[n-1]能提升高频细节对CPC效果有轻微提升。技巧四训练初期用较小的预测步数比如4步等损失稳定后再增加到12步。这样能加速收敛。6. 从CPC到更远这个框架还能怎么用CPC的框架非常灵活。除了语音我在图像上试过把图像切成7x7的块按光栅顺序做自回归预测未来块。效果虽然不如专门的图像自监督方法如SimCLR但胜在简单。在文本上可以把词向量作为z用Transformer做自回归预测后续词。本质上只要你能定义序列和预测目标CPC就能用。我个人在实际操作中的体会是CPC最大的价值不是某个具体任务上的SOTA而是它提供了一种思考自监督学习的视角——不要重建要对比不要预测像素要预测隐表示。这个视角影响了后来很多工作比如MoCo、SimCLR、BYOL等。如果你能亲手复现一遍CPC再去看这些后续工作会发现很多设计选择都顺理成章。最后分享一个小技巧如果你手头数据量不大可以先在公开数据集上预训练一个CPC模型然后冻结编码器只微调下游任务。这样即使只有几百条标注数据也能取得不错的效果。我试过在几千小时语音上预训练然后只用10小时标注数据做微调准确率比从头训练高15%以上。这个思路在工业界非常实用毕竟标注数据永远不够。