• [技术干货] DDPM、DDIM与Score-based模型的区别与联系
    DDPM、DDIM与Score-based模型的区别与联系扩散模型家族中,DDPM(Denoising Diffusion Probabilistic Model)、DDIM(Denoising Diffusion Implicit Model)和Score-based模型是三个核心代表,它们在理论上密切相关,但在采样机制、速度与生成质量上各具特点。理解它们之间的关系,有助于深入掌握现代生成模型的发展脉络。DDPM 是扩散模型的基础形式,首次系统提出通过逐步加噪和反向去噪的方式生成高质量图像。其训练目标是预测每一步添加的噪声,使用均值为模型预测、方差预设的高斯分布进行反向采样。虽然图像质量出色,但采样过程通常需要上百甚至上千步,生成速度较慢。为了解决这一问题,DDIM 应运而生。DDIM与DDPM共享相同的训练框架,但在采样阶段采用了确定性推理路径。通过改写反向过程中的随机项,DDIM能够大幅减少采样步数(如从1000步降至50步以内),同时保持图像质量。这使得DDIM成为扩散模型在实际部署中的优选方案之一。Score-based模型(又称为Score Matching with Langevin Dynamics或SDE-based模型)则是从另一个角度理解扩散过程的生成原理。它直接学习数据分布的梯度(score function),使用随机微分方程(SDE)建模噪声演化,通过反向SDE或概率流ODE实现采样。其理论基础更接近连续时间建模,具备数学上的严谨性和灵活性。三者之间存在深刻联系:DDPM与Score-based模型可以看作是不同时间建模下的扩散等价形式,而DDIM则是DDPM采样过程的一种非随机变体。实际上,Score-based模型也可以转化为对应的离散形式,与DDPM形成统一框架。总结来说,DDPM奠定基础,DDIM优化采样效率,而Score-based模型提供更广泛的数学视角。三者共同构成了现代扩散模型理论的核心支柱。
  • [技术干货] 扩散概率模型与生成对抗网络(GAN)的对比研究
    扩散概率模型与生成对抗网络(GAN)的对比研究在人工智能生成模型的快速发展中,**扩散概率模型(Diffusion Probabilistic Models, DPM)和生成对抗网络(Generative Adversarial Networks, GAN)**作为两大主流图像生成技术,各自展现出强大的能力。它们的核心思想与实现路径大相径庭,本文将从生成机制、训练稳定性、图像质量与灵活性等角度进行简要对比。首先,从生成机制看,GAN采用博弈论思想,由一个生成器与一个判别器组成。生成器尝试欺骗判别器,判别器则学习分辨真实与伪造图像,二者在对抗中不断提升性能。而扩散模型的基本流程则是**“逐步加噪+逐步去噪”**。其前向过程向图像加入高斯噪声,后向过程利用深度神经网络学习如何从噪声中还原图像。从训练稳定性上,GAN较难训练,常常出现模式崩溃(mode collapse)或不收敛的问题,因为判别器与生成器在对抗中可能不平衡。而扩散模型采用最大似然估计(MLE)或噪声预测,训练过程更稳定,容易收敛。在图像质量方面,过去GAN因其高效率与清晰度常占优势,但近年来扩散模型如Stable Diffusion、Imagen、DALL·E 3等,生成图像的细节与多样性甚至已超越GAN,特别在复杂场景与语义控制上更为出色。此外,扩散模型在可控生成与**跨模态任务(如文本生成图像)**中表现尤为出色,而GAN更多用于图像增强、风格迁移等领域。总之,GAN凭借高效生成仍具优势,适合对实时性要求高的任务;而扩散模型则在高保真生成和多样性方面优势明显,尤其适用于AIGC等精细内容创作场景。未来,二者的融合与互补,可能催生出新一代更强大的生成模型。
  • [技术干货] 什么是扩散模型?一文看懂Diffusion Models的基本架构与流程
    什么是扩散模型?一文看懂Diffusion Models的基本架构与流程扩散模型(Diffusion Models)是一种生成式模型,通过模拟数据逐步被噪声污染,再反向还原出清晰数据的过程,实现图像、音频甚至文本的生成。近年来,扩散模型因其在图像生成中的高质量表现,迅速成为AIGC(生成式人工智能)的热门技术,如著名的Stable Diffusion和DALL·E 2等都采用了该技术。基本原理扩散模型的核心思想可以概括为“正向扰动 + 反向去噪”。在**正向过程(Forward Process)**中,模型将真实图像逐步添加高斯噪声,直到变成接近纯噪声的图像。这个过程通常由一个马尔可夫链定义,每一步的图像都被加入一小部分噪声。在**反向过程(Reverse Process)**中,模型训练一个神经网络来一步步去除噪声,逐步还原出原始图像。训练目标是最小化预测噪声与真实噪声之间的差异。模型架构主流扩散模型多采用U-Net结构作为噪声预测器,结合时间步嵌入(Timestep Embedding)与条件输入(如文本或标签),使模型具备良好的表达能力。为了提高采样速度,常会引入改进机制如DDIM(去噪扩散隐变量模型)或Latent Diffusion(潜空间扩散)等。流程总结数据准备:输入图像添加逐步噪声,形成训练对(噪声图,原图)。模型训练:学习如何从噪声图中预测噪声或原始数据。图像生成:从随机噪声开始,逐步去噪还原为高质量图像。应用领域扩散模型广泛应用于图像生成、图像修复、风格迁移、文本生成图像(如文生图)等领域,是当前AI创作中最具前景的技术之一。简而言之,扩散模型就像“教会AI如何擦掉噪声”,它不是直接画图,而是一步步“还原”图像,令人惊叹。
  • [技术干货] 散模型原理详解:从随机噪声到图像生成的奇妙旅程
    扩散模型原理详解:从随机噪声到图像生成的奇妙旅程扩散模型(Diffusion Models)是一类近年来在图像生成任务中大放异彩的生成模型,其核心思想源自物理中的扩散过程。与传统的生成对抗网络(GAN)不同,扩散模型以其稳定的训练过程和惊艳的生成质量,逐步成为AIGC领域的主力军。本文将简要讲解其原理及生成机制。扩散模型的生成过程可分为两个阶段:前向扩散(Forward Diffusion)和反向去噪(Reverse Denoising)。在前向过程里,一张真实图像会逐步被加入高斯噪声,最终变成一张近似纯噪声的图像。这个过程是有序且可控的,通常分为T步。在每一步中,图像都按照某个噪声调度函数加上一些随机扰动,使其信息逐渐丢失。而在反向过程中,模型的任务则是学习如何一步步从纯噪声中恢复原始图像。这个去噪的过程通过一个神经网络(通常是U-Net结构)来建模。模型学习在每一个时间步t,如何将当前的噪声图像“还原”回前一步。最终,当反向过程完成,就生成了一张高质量的图像。可以将其比作“蒙上一层又一层灰尘的玻璃”,前向过程是不断增加灰尘,而反向过程则是逐步擦去灰尘,直至玻璃变得清晰可见。在训练阶段,模型并不直接恢复整张图像,而是学习预测每一步的噪声成分。损失函数通常使用MSE来衡量模型预测噪声与真实添加噪声之间的差异。扩散模型的优势在于生成质量极高、细节丰富,且不易产生模式崩溃(mode collapse)问题。它已广泛应用于图像生成(如Stable Diffusion)、图像修复、超分辨率等任务,成为AIGC时代的核心技术之一。总的来说,扩散模型用“加噪-去噪”的思维方式,完成了从混沌中生出艺术的奇妙旅程。
  • [技术干货] NICEGAN原理解析笔记【全干货 | 读对抗生成网络论文源码记录】
    NICEGANNICEGAN(Neural Image Compression Generative Adversarial Network)是一种基于生成对抗网络(GAN)的方法,用于高效的图像压缩与重建。它通过生成器学习从潜在空间中生成压缩图像,并利用判别器优化生成图像的质量,采用无监督学习的方式进行端到端训练。与传统压缩算法(如JPEG)相比,NICEGAN能在较低比特率下保持更高的图像质量,尤其在压缩比和重建效果上具有优势。NICEGAN的应用场景包括图像存储、传输、实时图像压缩以及高质量图像生成,适合网络带宽受限或对图像质量要求较高的领域。ResnetGenerator 类解析【生成器】ResnetGenerator 类是一个图像生成器,通常用于生成对抗网络(GAN)中。它使用了残差网络(ResNet)架构,并结合了自适应实例归一化(AdaIN)模块,能够从潜在空间 z 中生成图像。这个类的实现中包含了上采样和残差块的设计,以及参数 gamma 和 beta 来实现风格的控制。以下是 ResnetGenerator 类的重点解析:构造函数:__init__def __init__(self, input_nc, output_nc, ngf=64, n_blocks=6, img_size=256, light=False): input_nc:输入图像的通道数(例如,彩色图像为3,灰度图像为1)。output_nc:输出图像的通道数(通常是3,代表RGB图像)。ngf:生成器中每个卷积层的基本通道数(通常为64)。n_blocks:生成器中包含的残差块的数量。img_size:输入图像的尺寸(例如,256x256)。light:控制是否使用轻量化的全连接层设计。网络层构建:1. UpBlock0(初始卷积块)UpBlock0 = [nn.ReflectionPad2d(1), nn.Conv2d(int(ngf * mult / 2), ngf * mult, kernel_size=3, stride=1, padding=0, bias=True), ILN(ngf * mult), nn.ReLU(True)] ReflectionPad2d(1):反射填充,用于避免卷积操作带来的边界效应。nn.Conv2d:卷积层,将输入特征图的通道数从 ngf * mult / 2 变为 ngf * mult。ILN(ngf * mult):自定义的实例归一化(Instance Normalization)层。它将归一化参数传递给后续的层。nn.ReLU(True):ReLU激活函数。这个块的作用是初始化卷积处理,并通过实例归一化层来增强训练稳定性。2. FC(全连接层,用于计算gamma和beta)if self.light: FC = [nn.Linear(ngf * mult, ngf * mult, bias=False), nn.ReLU(True), nn.Linear(ngf * mult, ngf * mult, bias=False), nn.ReLU(True)] else: FC = [nn.Linear(img_size // mult * img_size // mult * ngf * mult, ngf * mult, bias=False), nn.ReLU(True), nn.Linear(ngf * mult, ngf * mult, bias=False), nn.ReLU(True)] 如果 light=True,则使用简单的全连接层结构,将输入大小映射到 ngf * mult,适合用于轻量化生成器。否则,采用更复杂的全连接结构,输入大小是经过多次卷积后得到的特征图大小。3. gamma 和 betaself.gamma = nn.Linear(ngf * mult, ngf * mult, bias=False) self.beta = nn.Linear(ngf * mult, ngf * mult, bias=False) 这两个参数 (gamma 和 beta) 是用于自适应实例归一化(AdaIN)的重要参数。它们控制生成图像的风格。4. 残差块:UpBlock1(n_blocks个残差块)for i in range(n_blocks): setattr(self, 'UpBlock1_' + str(i+1), ResnetAdaILNBlock(ngf * mult, use_bias=False)) 这些残差块使用了 ResnetAdaILNBlock,它们结合了残差连接和自适应实例归一化(AdaIN)。每个残差块在图像生成中起到了提取特征、保持信息和提升生成质量的作用。5. UpBlock2(上采样块)UpBlock2 = [] for i in range(n_downsampling): mult = 2**(n_downsampling - i) UpBlock2 += [nn.ReflectionPad2d(1), nn.Conv2d(ngf * mult, int(ngf * mult / 2), kernel_size=3, stride=1, padding=0, bias=False), ILN(int(ngf * mult / 2)), nn.ReLU(True), nn.Conv2d(int(ngf * mult / 2), int(ngf * mult / 2)*4, kernel_size=1, stride=1, bias=True), nn.PixelShuffle(2), ILN(int(ngf * mult / 2)), nn.ReLU(True)] 这个部分包含了上采样操作,用于增加图像的分辨率。使用了 PixelShuffle 技术,这是一个高效的上采样方法,能够提高生成图像的分辨率,同时减少计算量。在每次上采样后,采用卷积和激活函数进一步提取特征。6. 输出层UpBlock2 += [nn.ReflectionPad2d(3), nn.Conv2d(ngf, output_nc, kernel_size=7, stride=1, padding=0, bias=False), nn.Tanh()] ReflectionPad2d(3):再次使用反射填充,确保卷积后图像尺寸正确。nn.Conv2d:将特征图通道数从 ngf 转换为 output_nc,即生成最终输出图像。nn.Tanh():Tanh 激活函数将输出图像的像素值限制在 [-1, 1] 范围,适用于图像生成任务。前向传播:forwarddef forward(self, z): x = z x = self.UpBlock0(x) if self.light: x_ = torch.nn.functional.adaptive_avg_pool2d(x, 1) x_ = self.FC(x_.view(x_.shape[0], -1)) else: x_ = self.FC(x.view(x.shape[0], -1)) gamma, beta = self.gamma(x_), self.beta(x_) for i in range(self.n_blocks): x = getattr(self, 'UpBlock1_' + str(i+1))(x, gamma, beta) out = self.UpBlock2(x) return out输入:接受一个潜在向量 z,这是生成器的输入。UpBlock0:首先通过初始的卷积块进行处理。gamma 和 beta:计算 gamma 和 beta,这些参数控制生成图像的风格。残差块:通过循环应用 n_blocks 个残差块(UpBlock1_1 到 UpBlock1_n_blocks)。UpBlock2:最后通过上采样块(UpBlock2)生成最终的图像。输出:返回生成的图像。总结ResnetGenerator 类的核心在于通过逐步上采样和残差块来生成图像,同时使用 AdaIN 来控制风格。其结构包括:初始的卷积和归一化层多个残差块上采样操作和输出生成的图像这种设计适用于生成任务,尤其是在风格迁移和图像生成对抗网络(GAN)中。ResnetBlock 类解析【残差网络块】ResnetBlock 类是一个基本的残差网络块,典型地用于解决深层网络中的梯度消失或爆炸问题。它通过跳跃连接(skip connection)允许梯度直接通过网络进行反向传播,保持信息流通。构造函数:__init__def __init__(self, dim, use_bias): super(ResnetBlock, self).__init__() conv_block = [] conv_block += [nn.ReflectionPad2d(1), nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias), nn.InstanceNorm2d(dim), nn.ReLU(True)] conv_block += [nn.ReflectionPad2d(1), nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias), nn.InstanceNorm2d(dim)] self.conv_block = nn.Sequential(*conv_block) dim:表示输入和输出的通道数。通常情况下,输入和输出通道数是相同的(例如在卷积操作后,通道数不改变)。use_bias:一个布尔值,表示是否使用偏置项。在卷积操作中,可以选择是否使用偏置,通常在使用批归一化(Batch Normalization)或实例归一化(Instance Normalization)时,会关闭偏置。构建卷积块:nn.ReflectionPad2d(1):反射填充:在卷积操作前对输入进行填充。填充方式为反射填充,这有助于减小边界效应,提高边缘的特征表示能力。填充大小为1,即为每个边缘补充1个像素的反射填充。nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias):这是一个卷积层,使用的卷积核大小为3x3,步幅为1。通道数保持不变,输入和输出的通道数为 dim。padding=0 表示没有额外的零填充。bias=use_bias:偏置项根据构造函数中的参数决定。nn.InstanceNorm2d(dim):实例归一化:对每个样本的每个通道进行归一化。它有助于去除样本间的差异,且不依赖于批次的统计信息(与 BatchNorm 不同)。归一化会计算每个通道的均值和方差,然后将其标准化。nn.ReLU(True):ReLU激活函数:激活函数用于非线性变换,True 参数表示使用原地操作,直接修改输入。第二个卷积块:与第一个卷积块非常类似,但是第二个卷积块中去除了 ReLU 激活函数,因为在 ResNet 结构中,两个卷积操作后通常只有最后一层有激活,而中间不加激活函数。conv_blockconv_block 是一个由上面两层卷积层、归一化和激活函数堆叠而成的 nn.Sequential 容器。这样做的好处是便于管理多个层次,并且使得代码简洁。前向传播:forwarddef forward(self, x): out = x + self.conv_block(x) return outx:输入数据。x + self.conv_block(x):这是典型的残差连接(skip connection),它的作用是将输入数据 x 与经过卷积操作和归一化的特征图相加。这样的操作帮助解决了深层网络中的梯度消失或爆炸问题。最终,返回经过跳跃连接的输出 out。总结:残差连接:最重要的特点是将输入数据 x 与卷积网络输出相加,这样可以更好地传递梯度,避免网络过深时梯度消失。实例归一化:帮助提升训练的稳定性,特别是在图像生成任务中。卷积和归一化:保持特征的表达能力,并通过激活函数引入非线性特征。ResnetBlock 主要用于构建更深的神经网络,它确保每一层学习到的信息可以有效地流动,而不会被前面的层过滤掉。通过这种方式,网络可以学习到更加复杂的特征表示。ResnetAdaILNBlock 类解析【结合了残差学习和自适应实例归一化】ResnetAdaILNBlock 是一个结合了残差学习和自适应实例归一化(AdaILN)的模块。这个类继承自 nn.Module,用于构建网络中的一个模块块,通常用于生成任务中,以增强模型的表达能力和稳定性。构造函数:__init__def __init__(self, dim, use_bias): super(ResnetAdaILNBlock, self).__init__() self.pad1 = nn.ReflectionPad2d(1) self.conv1 = nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias) self.norm1 = adaILN(dim) self.relu1 = nn.ReLU(True) self.pad2 = nn.ReflectionPad2d(1) self.conv2 = nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias) self.norm2 = adaILN(dim) dim:表示输入和输出的通道数。在此模块中,dim 保持一致,即输入和输出的通道数不变。use_bias:是否在卷积操作中使用偏置项,通常和归一化操作一起使用时会关闭偏置项。模块内部操作:self.pad1 = nn.ReflectionPad2d(1) 和 self.pad2 = nn.ReflectionPad2d(1):反射填充:这两个操作对输入的张量进行反射填充,填充大小为1。这有助于减少卷积操作中可能出现的边界效应。self.conv1 = nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias) 和 self.conv2 = nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias):卷积操作:这两个卷积层分别使用大小为 3x3 的卷积核,步幅为1。输入和输出通道数均为 dim,即输入与输出的通道数保持一致。self.norm1 = adaILN(dim) 和 self.norm2 = adaILN(dim):自适应实例归一化(AdaILN):这是一个定制化的归一化方法,能够根据每一层的输入信息调整归一化的参数。adaILN 类的实现可以根据训练中的动态信息自适应地调整归一化过程的影响。这里对每一层输出进行归一化。self.relu1 = nn.ReLU(True):ReLU激活函数:用于非线性激活。True 表示使用原地操作(in-place),即直接修改输入。前向传播:forwarddef forward(self, x, gamma, beta): out = self.pad1(x) out = self.conv1(out) out = self.norm1(out, gamma, beta) out = self.relu1(out) out = self.pad2(out) out = self.conv2(out) out = self.norm2(out, gamma, beta) return out + x输入:x:输入特征图(tensor)。gamma 和 beta:这些是通过自适应归一化方法(AdaILN)进行调整的参数,通常来自于网络中的其他部分(例如网络的其他层)。操作步骤:填充:对输入 x 使用反射填充,增加周围的像素边缘,避免卷积时出现边界效应。第一层卷积:将填充后的输入通过卷积层 conv1 进行处理。归一化:通过 norm1(即 adaILN)对卷积后的特征图进行归一化,gamma 和 beta 用于调整归一化的尺度和偏移。ReLU激活:对归一化后的特征图应用 ReLU 激活函数。第二层卷积:将经过激活后的特征图进行第二次卷积处理(卷积核为3x3,步幅为1)。归一化:同样通过 norm2(即 adaILN)对第二次卷积的输出进行归一化。跳跃连接:将处理后的输出与输入 x 相加(残差连接)。这是典型的 ResNet 结构,旨在缓解深层网络中的梯度消失问题。总结ResnetAdaILNBlock 是一个基于残差学习(ResNet)和自适应实例归一化(AdaILN)方法的模块,它的设计目标是通过结合残差连接和动态归一化来提升神经网络的表现,尤其是在图像生成任务中。关键步骤包括:通过残差连接确保梯度在深层网络中的流动。自适应归一化(AdaILN)根据输入特征动态调整归一化过程,提高模型的表达能力。使用反射填充和卷积操作进一步提取和处理特征。Discriminator 类解析【判别器】该类是一个生成对抗网络(GAN)中的判别器(Discriminator),用于判定输入图像是来自真实数据分布还是生成的假数据(即生成器生成的图像)。该网络采用了多个卷积层,并结合了自适应池化、分类激活映射(CAM)等技术,提升了模型对图像特征的判别能力。以下是对该类代码的详细解析:1. 构造函数 __init__def __init__(self, input_nc, ndf=64, n_layers=7): super(Discriminator, self).__init__() input_nc: 输入图像的通道数,通常为 3(RGB图像)。ndf: 基础通道数,即最初的卷积核数量,默认为 64。该值随着层数增加而翻倍。n_layers: 网络的层数。默认值为 7,表示卷积网络的深度。2. 网络结构初始卷积层和 LeakyReLU 激活函数model = [nn.ReflectionPad2d(1), nn.utils.spectral_norm( nn.Conv2d(input_nc, ndf, kernel_size=4, stride=2, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] nn.ReflectionPad2d(1): 对输入进行反射填充,填充大小为 1,防止卷积操作时丢失边缘信息。nn.utils.spectral_norm: 使用谱归一化对卷积层进行正则化,防止梯度爆炸,提升稳定性。nn.Conv2d: 卷积操作,input_nc 为输入通道数,ndf 为输出通道数(卷积核数量)。使用步幅 stride=2 进行下采样。nn.LeakyReLU(0.2, True): LeakyReLU 激活函数,负部分斜率设置为 0.2。第二层卷积for i in range(1, 2): mult = 2 ** (i - 1) model += [nn.ReflectionPad2d(1), nn.utils.spectral_norm( nn.Conv2d(ndf * mult, ndf * mult * 2, kernel_size=4, stride=2, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] 该块用于构建第二层卷积,输出通道数为上一层的两倍(ndf * mult * 2)。stride=2 继续进行下采样,每经过一次卷积,特征图大小会减半。类激活映射(CAM)模块self.fc = nn.utils.spectral_norm(nn.Linear(ndf * mult * 2, 1, bias=False)) self.conv1x1 = nn.Conv2d(ndf * mult * 2, ndf * mult, kernel_size=1, stride=1, bias=True) self.leaky_relu = nn.LeakyReLU(0.2, True) self.lamda = nn.Parameter(torch.zeros(1)) fc: 通过全连接层计算类激活映射(Class Activation Map, CAM)特征,输出大小为 1。conv1x1: 1x1 卷积操作,进一步处理特征图。lamda: 一个训练过程中可学习的参数,用于调整模型的加权和。额外的卷积层和处理层Dis0_0 = [] for i in range(2, n_layers - 4): mult = 2 ** (i - 1) Dis0_0 += [nn.ReflectionPad2d(1), nn.utils.spectral_norm( nn.Conv2d(ndf * mult, ndf * mult * 2, kernel_size=4, stride=2, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] Dis0_0: 更多的卷积层,每经过一层,通道数将翻倍,进一步提取图像特征。最终的卷积层self.conv0 = nn.utils.spectral_norm( nn.Conv2d(ndf * mult, 1, kernel_size=4, stride=1, padding=0, bias=False)) 最后的卷积层,用于将特征图转换为最终的判别结果,输出单通道(1),用于输出真假图像的判断。3. 前向传播:forwarddef forward(self, input): x = self.model(input) x_0 = x gap = torch.nn.functional.adaptive_avg_pool2d(x, 1) gmp = torch.nn.functional.adaptive_max_pool2d(x, 1) x = torch.cat([x, x], 1) cam_logit = torch.cat([gap, gmp], 1) cam_logit = self.fc(cam_logit.view(cam_logit.shape[0], -1)) 卷积操作:首先通过 self.model 提取输入图像的特征。池化操作:gap: 自适应平均池化,输出形状为 (batch_size, ndf * mult * 2, 1, 1)。gmp: 自适应最大池化,输出形状也为 (batch_size, ndf * mult * 2, 1, 1)。特征拼接:将 gap 和 gmp 拼接在一起,得到最终的 cam_logit,并通过全连接层 self.fc 得到类激活映射。处理和加权x = x * weight.unsqueeze(2).unsqueeze(3) x = self.conv1x1(x) x = self.lamda * x + x_0加权处理:通过 weight 对特征图进行加权。self.lamda * x + x_0: 在 x_0 和加权后的 x 之间做线性加权。lamda 是网络在训练过程中学到的可调参数。生成热图和最终输出heatmap = torch.sum(x, dim=1, keepdim=True) heatmap: 通过对 x 进行求和得到热图,表示每个像素的贡献。x0 = self.Dis0_0(x) x1 = self.Dis1_0(x0) x0 = self.Dis0_1(x0) x1 = self.Dis1_1(x1) x0 = self.pad(x0) x1 = self.pad(x1) out0 = self.conv0(x0) out1 = self.conv1(x1) Dis0_0, Dis1_0, Dis0_1, Dis1_1: 分别经过多个卷积层,进一步提取特征。conv0, conv1: 最终的卷积层,输出判别器的判断结果,通常为单通道的真假判断值。4. 返回的结果return out0, out1, cam_logit, heatmap, zout0, out1: 判别器的最终输出,表示输入图像为真实或假的概率。cam_logit: 类激活映射(Class Activation Map),指示图像哪些区域对判别有更大贡献。heatmap: 热图,展示图像中不同区域的重要性。z: 用于后续操作的中间特征。总结Discriminator 类是一个复杂的卷积神经网络,通过多个卷积层、池化层、LeakyReLU 激活以及自适应池化等技术,构建了一个强大的图像判别器。在生成对抗网络中,判别器的作用是判定输入图像是否为真实数据,或者是生成器生成的假数据。该网络特别注重特征提取和加权,通过类激活映射(CAM)进一步提升其判别能力。核心网络源码【完整】import torch import torch.nn as nn from torch.nn.parameter import Parameter class ResnetGenerator(nn.Module): def __init__(self, input_nc, output_nc, ngf=64, n_blocks=6, img_size=256, light=False): assert(n_blocks >= 0) super(ResnetGenerator, self).__init__() self.input_nc = input_nc self.output_nc = output_nc self.ngf = ngf self.n_blocks = n_blocks self.img_size = img_size self.light = light n_downsampling = 2 mult = 2**n_downsampling UpBlock0 = [nn.ReflectionPad2d(1), nn.Conv2d(int(ngf * mult / 2), ngf * mult, kernel_size=3, stride=1, padding=0, bias=True), ILN(ngf * mult), nn.ReLU(True)] self.relu = nn.ReLU(True) # Gamma, Beta block if self.light: FC = [nn.Linear(ngf * mult, ngf * mult, bias=False), nn.ReLU(True), nn.Linear(ngf * mult, ngf * mult, bias=False), nn.ReLU(True)] else: FC = [nn.Linear(img_size // mult * img_size // mult * ngf * mult, ngf * mult, bias=False), nn.ReLU(True), nn.Linear(ngf * mult, ngf * mult, bias=False), nn.ReLU(True)] self.gamma = nn.Linear(ngf * mult, ngf * mult, bias=False) self.beta = nn.Linear(ngf * mult, ngf * mult, bias=False) # Up-Sampling Bottleneck for i in range(n_blocks): setattr(self, 'UpBlock1_' + str(i+1), ResnetAdaILNBlock(ngf * mult, use_bias=False)) # Up-Sampling UpBlock2 = [] for i in range(n_downsampling): mult = 2**(n_downsampling - i) # Experiments show that the performance of Up-sample and Sub-pixel is similar, # although theoretically Sub-pixel has more parameters and less FLOPs. # UpBlock2 += [nn.Upsample(scale_factor=2, mode='nearest'), # nn.ReflectionPad2d(1), # nn.Conv2d(ngf * mult, int(ngf * mult / 2), kernel_size=3, stride=1, padding=0, bias=False), # ILN(int(ngf * mult / 2)), # nn.ReLU(True)] UpBlock2 += [nn.ReflectionPad2d(1), nn.Conv2d(ngf * mult, int(ngf * mult / 2), kernel_size=3, stride=1, padding=0, bias=False), ILN(int(ngf * mult / 2)), nn.ReLU(True), nn.Conv2d(int(ngf * mult / 2), int(ngf * mult / 2)*4, kernel_size=1, stride=1, bias=True), nn.PixelShuffle(2), ILN(int(ngf * mult / 2)), nn.ReLU(True) ] UpBlock2 += [nn.ReflectionPad2d(3), nn.Conv2d(ngf, output_nc, kernel_size=7, stride=1, padding=0, bias=False), nn.Tanh()] self.FC = nn.Sequential(*FC) self.UpBlock0 = nn.Sequential(*UpBlock0) self.UpBlock2 = nn.Sequential(*UpBlock2) def forward(self, z): x = z x = self.UpBlock0(x) if self.light: x_ = torch.nn.functional.adaptive_avg_pool2d(x, 1) x_ = self.FC(x_.view(x_.shape[0], -1)) else: x_ = self.FC(x.view(x.shape[0], -1)) gamma, beta = self.gamma(x_), self.beta(x_) for i in range(self.n_blocks): x = getattr(self, 'UpBlock1_' + str(i+1))(x, gamma, beta) out = self.UpBlock2(x) return out class ResnetBlock(nn.Module): def __init__(self, dim, use_bias): super(ResnetBlock, self).__init__() conv_block = [] conv_block += [nn.ReflectionPad2d(1), nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias), nn.InstanceNorm2d(dim), nn.ReLU(True)] conv_block += [nn.ReflectionPad2d(1), nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias), nn.InstanceNorm2d(dim)] self.conv_block = nn.Sequential(*conv_block) def forward(self, x): out = x + self.conv_block(x) return out class ResnetAdaILNBlock(nn.Module): def __init__(self, dim, use_bias): super(ResnetAdaILNBlock, self).__init__() self.pad1 = nn.ReflectionPad2d(1) self.conv1 = nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias) self.norm1 = adaILN(dim) self.relu1 = nn.ReLU(True) self.pad2 = nn.ReflectionPad2d(1) self.conv2 = nn.Conv2d(dim, dim, kernel_size=3, stride=1, padding=0, bias=use_bias) self.norm2 = adaILN(dim) def forward(self, x, gamma, beta): out = self.pad1(x) out = self.conv1(out) out = self.norm1(out, gamma, beta) out = self.relu1(out) out = self.pad2(out) out = self.conv2(out) out = self.norm2(out, gamma, beta) return out + x class adaILN(nn.Module): def __init__(self, num_features, eps=1e-5, momentum=0.9, using_moving_average=True, using_bn=False): super(adaILN, self).__init__() self.eps = eps self.momentum = momentum self.using_moving_average = using_moving_average self.using_bn = using_bn self.num_features = num_features if self.using_bn: self.rho = Parameter(torch.Tensor(1, num_features, 3)) self.rho[:,:,0].data.fill_(3) self.rho[:,:,1].data.fill_(1) self.rho[:,:,2].data.fill_(1) self.register_buffer('running_mean', torch.zeros(1, num_features, 1,1)) self.register_buffer('running_var', torch.zeros(1, num_features, 1,1)) self.running_mean.zero_() self.running_var.zero_() else: self.rho = Parameter(torch.Tensor(1, num_features, 2)) self.rho[:,:,0].data.fill_(3.2) self.rho[:,:,1].data.fill_(1) def forward(self, input, gamma, beta): in_mean, in_var = torch.mean(input, dim=[2, 3], keepdim=True), torch.var(input, dim=[2, 3], keepdim=True) out_in = (input - in_mean) / torch.sqrt(in_var + self.eps) ln_mean, ln_var = torch.mean(input, dim=[1, 2, 3], keepdim=True), torch.var(input, dim=[1, 2, 3], keepdim=True) out_ln = (input - ln_mean) / torch.sqrt(ln_var + self.eps) softmax = nn.Softmax(2) rho = softmax(self.rho) if self.using_bn: if self.training: bn_mean, bn_var = torch.mean(input, dim=[0, 2, 3], keepdim=True), torch.var(input, dim=[0, 2, 3], keepdim=True) if self.using_moving_average: self.running_mean.mul_(self.momentum) self.running_mean.add_((1 - self.momentum) * bn_mean.data) self.running_var.mul_(self.momentum) self.running_var.add_((1 - self.momentum) * bn_var.data) else: self.running_mean.add_(bn_mean.data) self.running_var.add_(bn_mean.data ** 2 + bn_var.data) else: bn_mean = torch.autograd.Variable(self.running_mean) bn_var = torch.autograd.Variable(self.running_var) out_bn = (input - bn_mean) / torch.sqrt(bn_var + self.eps) rho_0 = rho[:,:,0] rho_1 = rho[:,:,1] rho_2 = rho[:,:,2] rho_0 = rho_0.view(1, self.num_features, 1,1) rho_1 = rho_1.view(1, self.num_features, 1,1) rho_2 = rho_2.view(1, self.num_features, 1,1) rho_0 = rho_0.expand(input.shape[0], -1, -1, -1) rho_1 = rho_1.expand(input.shape[0], -1, -1, -1) rho_2 = rho_2.expand(input.shape[0], -1, -1, -1) out = rho_0 * out_in + rho_1 * out_ln + rho_2 * out_bn else: rho_0 = rho[:,:,0] rho_1 = rho[:,:,1] rho_0 = rho_0.view(1, self.num_features, 1,1) rho_1 = rho_1.view(1, self.num_features, 1,1) rho_0 = rho_0.expand(input.shape[0], -1, -1, -1) rho_1 = rho_1.expand(input.shape[0], -1, -1, -1) out = rho_0 * out_in + rho_1 * out_ln out = out * gamma.unsqueeze(2).unsqueeze(3) + beta.unsqueeze(2).unsqueeze(3) return out class ILN(nn.Module): def __init__(self, num_features, eps=1e-5, momentum=0.9, using_moving_average=True, using_bn=False): super(ILN, self).__init__() self.eps = eps self.momentum = momentum self.using_moving_average = using_moving_average self.using_bn = using_bn self.num_features = num_features if self.using_bn: self.rho = Parameter(torch.Tensor(1, num_features, 3)) self.rho[:,:,0].data.fill_(1) self.rho[:,:,1].data.fill_(3) self.rho[:,:,2].data.fill_(3) self.register_buffer('running_mean', torch.zeros(1, num_features, 1,1)) self.register_buffer('running_var', torch.zeros(1, num_features, 1,1)) self.running_mean.zero_() self.running_var.zero_() else: self.rho = Parameter(torch.Tensor(1, num_features, 2)) self.rho[:,:,0].data.fill_(1) self.rho[:,:,1].data.fill_(3.2) self.gamma = Parameter(torch.Tensor(1, num_features, 1, 1)) self.beta = Parameter(torch.Tensor(1, num_features, 1, 1)) self.gamma.data.fill_(1.0) self.beta.data.fill_(0.0) def forward(self, input): in_mean, in_var = torch.mean(input, dim=[2, 3], keepdim=True), torch.var(input, dim=[2, 3], keepdim=True) out_in = (input - in_mean) / torch.sqrt(in_var + self.eps) ln_mean, ln_var = torch.mean(input, dim=[1, 2, 3], keepdim=True), torch.var(input, dim=[1, 2, 3], keepdim=True) out_ln = (input - ln_mean) / torch.sqrt(ln_var + self.eps) softmax = nn.Softmax(2) rho = softmax(self.rho) if self.using_bn: if self.training: bn_mean, bn_var = torch.mean(input, dim=[0, 2, 3], keepdim=True), torch.var(input, dim=[0, 2, 3], keepdim=True) if self.using_moving_average: self.running_mean.mul_(self.momentum) self.running_mean.add_((1 - self.momentum) * bn_mean.data) self.running_var.mul_(self.momentum) self.running_var.add_((1 - self.momentum) * bn_var.data) else: self.running_mean.add_(bn_mean.data) self.running_var.add_(bn_mean.data ** 2 + bn_var.data) else: bn_mean = torch.autograd.Variable(self.running_mean) bn_var = torch.autograd.Variable(self.running_var) out_bn = (input - bn_mean) / torch.sqrt(bn_var + self.eps) rho_0 = rho[:,:,0] rho_1 = rho[:,:,1] rho_2 = rho[:,:,2] rho_0 = rho_0.view(1, self.num_features, 1,1) rho_1 = rho_1.view(1, self.num_features, 1,1) rho_2 = rho_2.view(1, self.num_features, 1,1) rho_0 = rho_0.expand(input.shape[0], -1, -1, -1) rho_1 = rho_1.expand(input.shape[0], -1, -1, -1) rho_2 = rho_2.expand(input.shape[0], -1, -1, -1) out = rho_0 * out_in + rho_1 * out_ln + rho_2 * out_bn else: rho_0 = rho[:,:,0] rho_1 = rho[:,:,1] rho_0 = rho_0.view(1, self.num_features, 1,1) rho_1 = rho_1.view(1, self.num_features, 1,1) rho_0 = rho_0.expand(input.shape[0], -1, -1, -1) rho_1 = rho_1.expand(input.shape[0], -1, -1, -1) out = rho_0 * out_in + rho_1 * out_ln out = out * self.gamma.expand(input.shape[0], -1, -1, -1) + self.beta.expand(input.shape[0], -1, -1, -1) return out class Discriminator(nn.Module): def __init__(self, input_nc, ndf=64, n_layers=7): super(Discriminator, self).__init__() model = [nn.ReflectionPad2d(1), nn.utils.spectral_norm( nn.Conv2d(input_nc, ndf, kernel_size=4, stride=2, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] #1+3*2^0 =4 for i in range(1, 2): #1+3*2^0 + 3*2^1 =10 mult = 2 ** (i - 1) model += [nn.ReflectionPad2d(1), nn.utils.spectral_norm( nn.Conv2d(ndf * mult, ndf * mult * 2, kernel_size=4, stride=2, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] # Class Activation Map mult = 2 ** (1) self.fc = nn.utils.spectral_norm(nn.Linear(ndf * mult * 2, 1, bias=False)) self.conv1x1 = nn.Conv2d(ndf * mult * 2, ndf * mult, kernel_size=1, stride=1, bias=True) self.leaky_relu = nn.LeakyReLU(0.2, True) self.lamda = nn.Parameter(torch.zeros(1)) Dis0_0 = [] for i in range(2, n_layers - 4): # 1+3*2^0 + 3*2^1 + 3*2^2 =22 mult = 2 ** (i - 1) Dis0_0 += [nn.ReflectionPad2d(1), nn.utils.spectral_norm( nn.Conv2d(ndf * mult, ndf * mult * 2, kernel_size=4, stride=2, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] mult = 2 ** (n_layers - 4 - 1) Dis0_1 = [nn.ReflectionPad2d(1), #1+3*2^0 + 3*2^1 + 3*2^2 +3*2^3 = 46 nn.utils.spectral_norm( nn.Conv2d(ndf * mult, ndf * mult * 2, kernel_size=4, stride=1, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] mult = 2 ** (n_layers - 4) self.conv0 = nn.utils.spectral_norm( #1+3*2^0 + 3*2^1 + 3*2^2 +3*2^3 + 3*2^3= 70 nn.Conv2d(ndf * mult, 1, kernel_size=4, stride=1, padding=0, bias=False)) Dis1_0 = [] for i in range(n_layers - 4, n_layers - 2): # 1+3*2^0 + 3*2^1 + 3*2^2 + 3*2^3=46, 1+3*2^0 + 3*2^1 + 3*2^2 +3*2^3 +3*2^4 = 94 mult = 2 ** (i - 1) Dis1_0 += [nn.ReflectionPad2d(1), nn.utils.spectral_norm( nn.Conv2d(ndf * mult, ndf * mult * 2, kernel_size=4, stride=2, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] mult = 2 ** (n_layers - 2 - 1) Dis1_1 = [nn.ReflectionPad2d(1), #1+3*2^0 + 3*2^1 + 3*2^2 +3*2^3 +3*2^4 + 3*2^5= 94 + 96 = 190 nn.utils.spectral_norm( nn.Conv2d(ndf * mult, ndf * mult * 2, kernel_size=4, stride=1, padding=0, bias=True)), nn.LeakyReLU(0.2, True)] mult = 2 ** (n_layers - 2) self.conv1 = nn.utils.spectral_norm( #1+3*2^0 + 3*2^1 + 3*2^2 +3*2^3 +3*2^4 + 3*2^5 + 3*2^5 = 286 nn.Conv2d(ndf * mult, 1, kernel_size=4, stride=1, padding=0, bias=False)) # self.attn = Self_Attn( ndf * mult) self.pad = nn.ReflectionPad2d(1) self.model = nn.Sequential(*model) self.Dis0_0 = nn.Sequential(*Dis0_0) self.Dis0_1 = nn.Sequential(*Dis0_1) self.Dis1_0 = nn.Sequential(*Dis1_0) self.Dis1_1 = nn.Sequential(*Dis1_1) def forward(self, input): x = self.model(input) x_0 = x gap = torch.nn.functional.adaptive_avg_pool2d(x, 1) gmp = torch.nn.functional.adaptive_max_pool2d(x, 1) x = torch.cat([x, x], 1) cam_logit = torch.cat([gap, gmp], 1) cam_logit = self.fc(cam_logit.view(cam_logit.shape[0], -1)) weight = list(self.fc.parameters())[0] x = x * weight.unsqueeze(2).unsqueeze(3) x = self.conv1x1(x) x = self.lamda*x + x_0 # print("lamda:",self.lamda) x = self.leaky_relu(x) heatmap = torch.sum(x, dim=1, keepdim=True) z = x x0 = self.Dis0_0(x) x1 = self.Dis1_0(x0) x0 = self.Dis0_1(x0) x1 = self.Dis1_1(x1) x0 = self.pad(x0) x1 = self.pad(x1) out0 = self.conv0(x0) out1 = self.conv1(x1) return out0, out1, cam_logit, heatmap, z
  • [技术干货] 损失突刺(Loss Spike)的产生和处理
    一、Loss Spike常见吗?小型模型/简单任务:较罕见(数据量小、结构简单,容易稳定训练)大型模型(如LLM、扩散模型):相对常见(训练复杂度高,需数周甚至数月)例如:GPT-3/ChatGPT训练中,团队会专门设计防御机制应对spike技术报告中常会提到"无spike"作为训练稳定的重要成就二、为什么会产生Loss Spike?1. 梯度相关原因(主要凶手)梯度爆炸(Gradient Explosion)当反向传播时梯度值指数级增长 → 参数更新幅度过大 → 模型"跑飞" ,就好像骑自行车下坡时刹车失灵,速度失控典型场景:RNN/LSTM、深层Transformer的初期训练阶段梯度消失(Gradient Vanishing)梯度变得极小 → 参数几乎不更新 → 突然某个层"苏醒"引发连锁反应,有点像水管长期堵塞后突然爆开2. 学习率问题学习率(LR)过高:参数更新步伐太大,"跳过"最优解即使初始LR合适,动态学习率策略(如Adam)也可能在特定情况下突变3. 数据层面的异常脏数据/标注错误:突然出现大量错误样本(如文本中乱码、图像全黑)。曾经有团队因数据管道故障混入未处理的HTML标签导致spike数据分布突变:训练中途切换数据源(如从新闻语料突然切换到医学文献)4. 硬件/数值不稳定浮点数溢出:当数值超过计算精度限制(如FP16训练时容易发生)经典场景:softmax计算中未做数值稳定化处理5. 模型架构缺陷残差连接缺失:深层网络容易出现信号传递中断初始化不当:参数初始值过大/过小引发训练初期不稳定三、实际案例Google的PaLM模型(5400亿参数)技术报告提到:初期spike频发 → 通过梯度裁剪(Gradient Clipping) + 学习率预热(LR Warmup) 解决Stable Diffusion训练因图像数据质量不均,需动态过滤"有毒样本"避免spike四、如何检测和应对?现象应急措施长期预防Loss突然上升暂停训练,回滚到上一个checkpoint启用梯度裁剪(如阈值设为1.0)权重出现NaN值检查混合精度训练配置添加数值稳定项(如+1e-7)连续多个batch异常验证数据加载管道数据预处理流水线自动化测试关键工具:TensorBoard/WandB实时监控loss曲线梯度直方图可视化(发现异常分布)总结Loss spike虽然常见但暴露了系统潜在问题。现代深度学习框架(如PyTorch)已内置许多防护机制(如自动梯度裁剪),但真正稳定的训练仍需工程师对数据、模型、硬件的深度理解。
  • [技术干货] 基于Llama词表扩展Tokenizer的做法介绍
    一、初步分析预训练词表的优势:Llama的tokenizer基于BPE算法,其词表(通常32k tokens)已经过大规模语料优化,覆盖了常见词汇、子词和字符级表示使用现有词表可继承其语言建模能力,避免从头训练的计算开销扩展的必要性:领域适应:添加领域专用术语(如医学/法律词汇)多语言支持:扩展非拉丁语系字符(如中文/日文字符)特殊符号:适配新任务所需的特殊标记(如[USER]、[SYSTEM])实现方法:增量训练:在原有BPE基础上继续训练,保持原有合并规则的同时学习新合并对混合词表:人工添加特定token后重新归一化(需调整嵌入层维度)冻结原词表:仅对新token进行训练(避免破坏原有表征)二、可行性分析业界普遍实践:GPT系列、Bloom等模型都采用类似策略例如ChatGPT扩展了表情符号和编程语言token技术经济性:从头训练BPE需要数百GB文本和大量计算资源扩展词表仅需目标领域数据(可降低90%+成本)研究支持:论文《How Good Is Your Tokenizer?》(ACL 2021)显示预训练词表迁移的有效性实验表明扩展词表比替换词表更少破坏原有语言模型性能潜在问题:子词冲突风险:新添加token可能破坏原有子词组合规则嵌入对齐挑战:新token的初始向量需要与原有语义空间协调词表膨胀:过度扩展会降低推理效率三、实践建议扩展策略:# 典型扩展示例(使用HuggingFace实现) from tokenizers import ByteLevelBPETokenizer tokenizer = ByteLevelBPETokenizer.from_file("llama_tokenizer.json") tokenizer.train([new_data.txt], vocab_size=34000) # 扩展2k tokens 效果验证指标:压缩率(Compression Ratio):应接近原模型水平未知token率(UNK Rate):目标领域应<0.1%下游任务性能变化:差异应<±2%替代方案对比:方法训练成本领域适应性通用性保持词表扩展低中高优完全重训极高最优需重新验证多tokenizer集成中高中四、总结基于Llama词表扩展Tokenizer的做法符合当前NLP领域的主流实践。但需要注意:扩展规模建议控制在原词表的5-10%以内需进行严格的词表覆盖率和OOV测试应配合嵌入层初始化策略(如均值初始化)这种方法的合理性已在实际应用中得到验证,包括LLaMA-2的中文扩展版本(如Chinese-LLaMA)等成功案例。
  • [技术干货] 位置编码/bias/归一化等模型改造方法介绍
    1. 位置编码:从“绝对位置”到“旋转位置(RoPE)问题:为什么需要位置编码?语言模型中,单词的顺序很重要。比如“猫追狗”和“狗追猫”意思完全不同。但模型本身(如Transformer)没有内置的顺序感知能力,需要额外告诉它单词的位置。原始方法:绝对位置编码做法:给每个位置(如第1个词、第2个词……)分配一个固定的数字编码,直接加到词向量上。缺点:模型难以理解“相对位置”(比如“距离3个词”这种关系)。长文本时性能下降(比如处理1000个词的文本时,位置数字太大,模型容易混乱)。改进方法:RoPE(旋转位置编码)核心思想:用“旋转”的方式表示位置。比如:每个词的位置像钟表的指针,旋转角度代表它的位置。词之间的相对距离可以通过旋转角度差计算(比如30°和60°相差30°)。优势:更好处理长文本(旋转是周期性的,不会爆炸)。更擅长捕捉相对位置关系(比如“A距离B 5个词”)。类比:绝对位置:用“第几排第几列”描述座位,换场地就失效。RoPE:用“面向舞台的角度”描述座位,无论场地大小都适用。2. 去掉Bias(偏置项)什么是Bias?在神经网络中,每个神经元计算时会有:输出 = 权重 * 输入 + 偏置(bias)。Bias的作用是给模型一个“默认偏移量”,比如让某些神经元更容易激活或更不容易激活。为什么要去掉Bias?简化模型:Bias是额外的参数,去掉后减少计算量。避免过拟合:Bias可能让模型过度依赖某些固定偏移,影响泛化能力。与归一化层(如RMSNorm)配合更好:归一化层已经能调整数据分布,Bias变得冗余。现代趋势:GPT、LLaMA等大模型都去掉了Bias。类比:骑自行车时,如果车把本身有偏向(Bias),你需要额外用力纠正。去掉Bias就像把车把调正,骑行更稳定。3. 切换为RMSNorm(均方根归一化)什么是归一化?神经网络中,每层的输入数据可能分布不稳定(比如数值忽大忽小),导致训练困难。归一化(Normalization)的作用是将数据调整到稳定范围内(比如均值为0、方差为1)。传统方法:LayerNorm对每个样本的所有特征计算均值和方差,然后标准化。计算量较大(需要求均值和方差)。改进方法:RMSNorm只计算均方根(Root Mean Square),忽略均值(假设均值接近0)。公式:RMS = sqrt( (x₁² + x₂² + ... + xₙ²) / n )然后对输入除以RMS。优势:计算更快(少一步均值计算)。实际效果与LayerNorm相近,但更省资源。为什么适合大模型?:参数越多,计算效率越重要,RMSNorm能减少计算量。总结改进项解决的问题实际效果RoPE长文本位置关系建模难更好的长文本理解能力去掉Bias参数冗余、过拟合风险模型更简洁,训练更稳定RMSNorm归一化计算效率低提速,适合大规模模型
  • [行业动态] 【话题讨论】Vibe Coding能否真正提升开发效率和创造力?你有什么看法?
    【话题讨论】Vibe Coding能否真正提升开发效率和创造力?你有什么看法?
  • [技术干货] Tokenizer编码效率低下带来的问题
    一、Tokenizer的核心作用与设计目标Tokenizer的核心任务是将原始文本转换为模型可处理的token序列,其设计需平衡:词汇覆盖率:尽可能覆盖多种语言和符号编码效率:用最少token表示常见文本语义一致性:避免拆分有语义的单元(如单词、短语)计算效率:分词速度影响训练/推理吞吐量当这些目标冲突时,早期大模型往往优先考虑通用性而牺牲效率。二、低效Tokenizer的具体表现字符级分词(Char-level)倾向:每个ASCII符号(如!@#$)、数字、空格都分配独立token汉字按单字分词(而非词或子词),例如"人工智能"→[“人”,“工”,“智”,“能”]词汇表(Vocab)构建问题:未充分合并高频子词(如"ing"、"tion"等英语后缀)对数字处理粗糙("123"→[“1”,“2”,“3”]而非[“123”])未针对多语言优化(中英混合文本效率骤降)三、导致的负面影响计算资源浪费:相同文本生成更多token → 注意力层计算复杂度呈平方级增长(O(n²))示例:用字符级Tokenizer处理代码时,token数量可能是子词方法的5-10倍模型性能下降:长序列导致信息碎片化,模型更难捕捉长期依赖上下文窗口被低效占用(如2048 token的窗口实际表达内容更少)训练效率降低:更多token意味着需要更多训练步数才能收敛梯度更新信号分散(同一概念被拆分为多个token)四、根源分析Byte Pair Encoding (BPE)的早期实现缺陷:初始BPE算法未考虑符号/数字的特殊性合并策略过于保守,优先保留单字符token(为避免OOV问题)多语言处理的妥协:为支持中文/日文等非拉丁语系,被迫保留单字分词混合语料训练时,高频语言(如英语)挤占其他语言的合并机会训练数据偏差:若训练语料中代码/数学符号占比较低,BPE不会为其生成高效合并规则历史局限性:2018-2020年的模型更关注通用性而非垂直优化当时硬件(如A100/V100)对长序列容忍度更高五、改进方案对比方案代表模型核心改进效果提升更智能的BPEGPT-4数字/符号预合并,动态词汇表代码效率提升Unigram分词T5概率化分词,淘汰低效token中文token减少字节级BPELLaMA 2放弃UTF-8解码,直接操作bytes支持所有字符无OOV词汇表压缩Qwen-72B移除低频单字符token推理速度提升六、最佳实践符号预处理:将连续空格合并为单个token为数学符号(如∫∏∑)分配专用token数字编码优化:# 旧方法(低效) "123.45" → ["1","2","3",".","4","5"] (6 tokens) # 新方法(高效) "123.45" → ["123", ".", "45"] (3 tokens) 中文分词改进:采用基于统计的子词合并(如"人工智能"→[“人工”,“智能”])引入专有名词识别(避免拆分"比特币"等术语)七、对模型能力的深层影响低效Tokenizer不仅影响计算性能,还会导致:数学推理能力弱:数字分拆破坏数值连续性代码生成差:符号密集场景(如正则表达式)消耗过多上下文多语言混输混乱:中英交替时token分配失衡现代模型(如DeepSeek-V3)通过动态分词和领域自适应词汇表已显著改善这些问题,这也是当前千亿参数模型能达到更优效果的关键因素之一。
  • [技术干货] MoE(Mixture of Experts)架构与大模型Dense模型的发展
    早期概念(1990s-2000s)MoE起源于1991年(Jacobs等提出),核心思想:将任务分解,由多个“专家”子模型(神经网络)处理,通过门控机制动态选择专家。早期受限于算力和数据,主要用于小规模场景(如分类任务)。Dense模型主导(2010s)2012年后,深度学习兴起,Dense模型(全连接、参数密集)成为主流,如ResNet、Transformer。特点:所有参数参与计算,表现力强,但计算成本随规模线性增长。MoE复兴(2017后)算力与数据增长推动MoE在大模型中的应用。关键里程碑:2017年,Google提出稀疏MoE层,首次用于大规模语言模型。2021年,Google推出Switch Transformer(万亿参数),MoE成为扩展模型规模的关键技术。优势:仅激活部分专家,计算高效,模型总参数可极大增加(如万亿级)。当前趋势(2020s)Dense模型:仍主导中等规模场景(如GPT-3的密集版本),平衡性能与工程复杂度。MoE模型:成为超大规模模型首选(如Google的GLaM、DeepSeek-MoE),支持更稀疏、更高效的训练与推理。混合方向:探索MoE与Dense的结合(如部分密集+部分稀疏专家),优化性能与成本。核心差异:Dense:所有参数参与计算,适合均衡任务。MoE:动态稀疏性,适合扩展参数量而不显著增加计算成本。
  • [技术干货] 仅支持FP16训练的局限性和改进
    在使用某早期芯片进行AI训练时,面临算力有限且仅支持FP16(16位浮点数)的情况,其训练稳定性不如后续支持BF16(Brain Floating Point 16)的硬件。1. 数值精度与动态范围FP16(Half Precision):格式:1符号位 + 5指数位 + 10尾数位。动态范围:约10−510^{-5}10−5到10410^4104(指数范围−14-14−14到+15+15+15)。问题:指数范围较窄,在训练中容易发生数值溢出(梯度爆炸)或下溢(梯度消失),尤其是深层网络或大梯度更新时。BF16(Brain Float 16):格式:1符号位 + 8指数位 + 7尾数位。动态范围:与FP32(单精度)相同,约10−3810^{-38}10−38到103810^{38}1038(指数范围−126-126−126到+127+127+127)。优势:更大的指数范围避免了溢出/下溢问题,适合处理极端梯度值。牺牲尾数精度(7位)换取动态范围,但对训练稳定性影响较小(神经网络对指数范围更敏感)。2. 训练稳定性的关键影响梯度更新问题:FP16在反向传播时,梯度可能因范围不足被截断为0(下溢)或无限大(溢出),导致参数更新失效。BF16的宽动态范围可保留梯度数值,即使损失一些尾数精度,也能稳定更新。损失函数收敛:FP16的窄范围可能导致损失函数剧烈震荡(如出现NaN),而BF16的数值特性使收敛更平滑。3. 硬件与计算效率算力限制:昇腾早期芯片的FP16算力虽高,但缺乏BF16支持,需依赖软件优化(如梯度裁剪、损失缩放)弥补稳定性问题,增加了计算开销。后续硬件通过原生BF16加速,既保持FP16的速度优势,又避免数值问题。混合精度训练:现代框架(如PyTorch AMP)通常结合FP16/BF16(前向/反向)与FP32(主权重),但纯FP16需更多手工调参(如动态损失缩放)。总结FP16的局限性在于窄动态范围和高精度需求的矛盾,而BF16通过牺牲冗余尾数精度换取指数范围,更契合神经网络训练的数值特性。硬件对BF16的原生支持(如昇腾后续版本、NVIDIA Ampere架构)进一步提升了训练效率和稳定性,减少了早期FP16时代的调参负担。
  • [技术干货] 大模型和小模型
    在人工智能领域,"大模型"和"小模型"是根据模型参数规模、计算需求和适用场景进行的分类,这种区分源于深度学习技术的发展与实际应用需求的演变。1. 基本概念大模型:通常指参数规模在数亿到数万亿之间的深度学习模型(如GPT-4有1700亿参数),依赖海量数据和强大算力训练,擅长复杂任务(如自然语言生成、多模态理解)。小模型:参数规模从几千到几亿(如Phi-3-mini仅3.8亿参数),训练数据量小,计算需求低,专精于特定任务(如移动端图像分类、语音助手)。2. 什么时候开始区分大小的?技术发展:早期AI模型规模有限,2017年Transformer架构出现后,模型参数爆炸式增长(如BERT、GPT系列),催生了"大模型"概念。应用需求:大模型虽通用性强,但成本高、难部署;小模型因轻量化、低延迟(如手机端运行)成为资源受限场景的刚需。经济因素:大模型训练成本可达数百万美元(如GPT-4),而小模型成本仅为零头,推动企业根据预算选择。3. 核心差异性能:大模型泛化能力强,小模型在特定任务上效率更高。资源:大模型需GPU集群训练,小模型可在单台服务器甚至手机运行。数据:大模型依赖TB级数据,小模型仅需GB级。4. 可能的未来演变大模型轻量化,如"轻量级大模型"(如GPT-4o mini),通过模型压缩技术平衡性能与效率,从"单纯求大"转向"大小协同"。
  • [问题求助] 在训练CNN时,如何调节学习率以提高模型性能?
    在训练CNN时,如何调节学习率以提高模型性能?
  • [问题求助] 卷积神经网络(CNN)与传统的机器学习方法相比,有哪些优势?
    卷积神经网络(CNN)与传统的机器学习方法相比,有哪些优势?