有不少介绍扩散模型的资料,其中"Understanding Diffusion Models: A Unified Perspective"论文是我读到的解释最详细也是最易于理解的一个。
数学符号
- 用字母x表示可观测到变量,用字母z表示隐变量
- 用qϕ(z|x)表示从观测变量x到隐变量z编码模型,模型参数用ϕ表示,这个参数即可以是训练得到的也可以是人为设定的
证据下界 ELBO
假设观测到的变量x和其对应的隐变量z的联合概率分布用p(x,z)表示。回想有一种生成模型方法,称之为“基于似然”,该方法通过最大化观测的变量x的似然来构建模型。有两种方法来从联合概率分布p(x,z)得到仅仅是关于观测变量x的似然。
p(x)=∫p(x,z)dz(1)
p(x)=p(x,z)p(z|x)(2)
从上述两个公式出发直接计算和最大化似然p(x)都是很困难的。因为公式(1)中对于复杂的模型,关于隐变量z求积分/边际分布是很困难的;公式(2)中求解真实的隐变量编码器p(z|x)也是很困难的。但是使用这两个公式可以推导出Evidence Lower Bound (ELBO)证据下界。这里的证据,是指观测到数据x的似然值对数log p(x)。下面来推导证据下界公式
推导一
log p(x)=log∫p(x,z)dz=log∫p(x,z)qϕ(z|x)qϕ(z|x)dz=log Eqϕ(z|x)p(x,z)qϕ(z|x)≥Eqϕ(z|x)logp(x,z)qϕ(z|x)(3)
其中,用qϕ(z|x)表示从观测变量x到隐变量z编码模型,模型参数用ϕ表示,该编码模型学习估计一个概率分布,来近似在给定观测变量x下隐变量z的真实后验概率分布p(z|x)。公式首先利用公式(1)以及分子分母同时乘以qϕ(z|x),将积分转化为期望。最重要的是利用Jensen不等式,交换期望与对数运算。
推导二
log p(x)=log p(x)∫qϕ(z|x)dz=∫log p(x)qϕ(z|x)dz=∫log p(x,z)p(z|x)qϕ(z|x)dz=∫log p(x,z)qϕ(z|x)p(z|x)qϕ(z|x)qϕ(z|x)dz=∫[log p(x,z)qϕ(z|x)+log qϕ(z|x)p(z|x)]qϕ(z|x)dz=∫log p(x,z)qϕ(z|x)qϕ(z|x)dz+∫log qϕ(z|x)p(z|x)qϕ(z|x)dz=Eqϕ(z|x)logp(x,z)qϕ(z|x)+DKL(qϕ(z|x)||p(z|x))≥Eqϕ(z|x)logp(x,z)qϕ(z|x)(4)
推导二中,关键的一点是利用条件概率qϕ(z|x)的积分等于1,然后再使用公式(2)替换概率p(x),再分子分母同乘以qϕ(z|x),拆分得到KL散度项DKL(qϕ(z|x)||p(z|x))和Eqϕ(z|x)logp(x,z)qϕ(z|x)。
- 上述公式中,DKL(qϕ(z|x)||p(z|x)),表示近似的后验概率分布qϕ(z|x)与真实的后验概率分布p(z|x)之间的KL散度
- 上述公式中,Eqϕ(z|x)logp(x,z)qϕ(z|x),可以从形式看出,如果qϕ(z|x)是真实的后验概率分布p(z|x),那么就刚好等于log p(x),如果不等于那么log p(x)就大于关于任意条件概率qϕ(z|x)的logp(x,z)qϕ(z|x)的期望。称 Eqϕ(z|x)logp(x,z)qϕ(z|x)为证据下界。
总结,证据=证据下界+近似后验与真实后验间的KL散度
为什么最大化ELBO可以代替最大化 log p(x)
- 由于证据和ELBO之间就相差一个KL散度,KL散度本身是非负的,因此ELBO是证据的下界
- 引入想要建模的隐变量z,目标就是学习能够描述观测的数据的根本隐藏的内在结构。换句话说,就是优化近似后验概率qϕ(z|x)的参数,以达到精确匹配真实的后验概率p(z|x)。而这种匹配关系恰好通过最小化KL散度来实现。理想情况下KL散度值为0. 不幸的是,由于无法获取真实的后验概率分布p(z|x),因此是很难直接最小化KL散度。 但是注意到推导二中,观测到数据的似然对数即证据关于参数ϕ是一个常量,因为它是通过对联合概率分布p(x,z)关于所有隐变量z求边际分布而不依赖参数ϕ。因此,此处最小化KL散度等价于最大化ELBO,因此最大化ELBO,可以用来指导如何完美建模真实后验概率分布。此外,一旦训练完成,ELBO就可以用来估计观测到的或者生成数据的似然,因为它已经可以近似模型证据log p(x).
变分自编码器 VAE
在变分自编码器VAE任务中,是直接最大化证据下界ELBO。之所以用“变分”修饰,是由于求解的结果是一个的函数而非一个值。
传统的AE模型,先是经过中间瓶颈表示步骤,将输入数据转化为与之对应的隐变量取值(encoder),然后再逆变换将隐变量取值转化为原始输入(decoder)。这里的AE模型特点是,输入数据与隐变量取值是一一对应的,并未设计成,给定一个输入数据x,而与之对应的隐变量取值服从某个分布p(z|x)。因此AE模型无法截取decoder部分从中取样用于生成近似训练集分布的数据。
而变分自编码VAE中,encoder部分是将输入数据转化为与之对应所有可能隐变量取值,不同取值都具有不同的概率值。这样encoder部分输入是向量x,输出是概率分布p(z|x),是一个函数。接下来decoder部分,学习一个确定性的函数pθ(x|z),给定隐变量取值,得到观察值x。
下面对ELBO进行进一步的拆解
Eqϕ(z|x)logp(x,z)qϕ(z|x)=Eqϕ(z|x)logpθ(x|z)p(z)qϕ(z|x)=Eqϕ(z|x)log[pθ(x|z)]+Eqϕ(z|x)log[p(z)qϕ(z|x)]=Eqϕ(z|x)log[pθ(x|z)]−DKL(qϕ(z|x)||p(z))](5)
- 拆解的第1项表示,从encoder变分分布qϕ(z|x)中取样,经过decoder合成部分pθ(x|z)重建出x的可能性。这一项保证了,学习到有效的概率分布,使得从中取样的隐变量取值能重建回对应的原始数据。
- 拆解的第2项表示,学习的变分分布与先验分布的相似性。最小化该项,则是鼓励encoder实际上是学习一个分布而不是退化为Diracδ函数。如果这样,就有点变成传统AE模型的意思了~
分级变分自编码器 HVAE
HVAE可以看成是VAE的一种扩展,将VAE由单个隐变量扩展到多个分级的隐变量。假设HVAE有T级/层隐变量,每个隐变量都可以条件依赖之前的所有隐变量,为了简化模型复杂度和便于计算,我们仅考虑条件依赖相邻之前的一个隐变量,称之为马尔科夫HVAE(MHVAE)。如图所示,在MHVAE中,encoder过程中,隐变量zt仅条件依赖zt−1;在decoder过程中,隐变量zt仅条件依赖zt+1。接下来按照公式(1)和(2)推导MHVAE的ELBO,因此要先写出联合概率分布和后验概率分布的公式。
- 从decoder过程推导联合概率分布,看图上方由右到左容易得到
p(x,z1:T)=p(x|z1)p(z1|z2)⋯p(zT−1|zT)p(zT)=p(x|z1)p(zT)T−1∏t=1p(zt|zt+1)(6)
- 从encoder过程推导联合概率分布,看图下方由左到右容易得到
p(x,z1:T)=q(x)q(z1|x)q(z2|z1)⋯p(zT|zT−1)=q(x)q(z1|x)T−1∏t=1q(zt+1|zt)(7)
- 从encoder过程推导后验概率分布,看图容易得到
q(z1:T|x)=q(z1|x)q(z2|z1)⋯p(zT|zT−1)=q(z1|x)T−1∏t=1q(zt+1|zt)(8)
上面是理论推导,而encoder和decoder都用某种方式进行建模,encoder部分建模参数用ϕ表示,decoder建模参数用θ表示。于是,MHVAE的联合概率分布和后验概率分布可以表示为
p(x,z1:T)=pθ(x|z1)pθ(z1|z2)⋯pθ(zT−1|zT)p(zT)=p(zT)pθ(x|z1)T−1∏t=1pθ(zt|zt+1)(9)
qϕ(z1:T|x)=qϕ(z1|x)qϕ(z2|z1)⋯pϕ(zT|zT−1)=qϕ(z1|x)T−1∏t=1qϕ(zt+1|zt)(10)
直接应用公式(3),使用z1:T替换其中的z,便可以得到[注1]
log p(x)≥Eqϕ(z1:T|x)logp(x,z1:T)qϕ(z1:T|x)(11)
log p(x)≥Eqϕ(z1:T|x)logp(zT)pθ(x|z1)∏T−1t=1pθ(zt|zt+1)qϕ(z1|x)∏T−1t=1qϕ(zt+1|zt)(12)

变分扩散模型 VDM
VDM可以看成是MHVAE的一种,相比MHVAE具有下面3个特点:
- 隐变量的维度与原始数据维度保持一致
- encocder过程,每个时间步的条件概率分布不是训练学习到的,而是预先设定的高斯分布
- 从初始数据出发,T个时间步后,得到的数据服从高斯分布
推导一
基于上述公式,继续对ELBO进行拆解,还是习惯于去掉∏符号,展开写更清晰些
log p(x)≥Eqϕ(z1:T|x)logp(zT)pθ(x|z1)∏T−1t=1pθ(zt|zt+1)qϕ(z1|x)∏T−1t=1qϕ(zt+1|zt)=Eqϕ(z1:T|x)logp(zT)pθ(x|z1)pθ(z1|z2)⋯pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|x)qϕ(z2|z1)⋯qϕ(zt+1|zt)⋯pϕ(zT|zT−1)(13)
参考VAE中的拆解方式,需要将重建项和先验匹配项拆解出来,就对应此处的log pθ(x|z1)和log p(zT)qϕ(zT|zT−1),于是对上述公式进行改写
log p(x)≥Eqϕ(z1:T|x)logp(zT)pθ(x|z1)∏T−1t=1pθ(zt|zt+1)qϕ(z1|x)∏T−1t=1qϕ(zt+1|zt)=Eqϕ(z1:T|x)logp(zT)pθ(x|z1)pθ(z1|z2)⋯pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|x)qϕ(z2|z1)⋯qϕ(zt+1|zt)⋯pϕ(zT|zT−1)=Eqϕ(z1:T|x)[log pθ(x|z1)]+Eqϕ(z1:T|x)[p(zT)qϕ(zT|zT−1)]+Eqϕ(z1:T|x)logpθ(z1|z2)⋯pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|x)qϕ(z2|z1)⋯qϕ(zt+1|zt)⋯pϕ(zT−1|zT−2)(14)
现在继续对上述公式最后一项进行进一步拆解,结合下图理解,粉色线和绿色线从两个不同方向对隐变量zt进行条件概率分布建模
Eqϕ(z1:T|x)logpθ(z1|z2)⋯pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|x)qϕ(z2|z1)⋯qϕ(zt+1|zt)⋯pϕ(zT−1|zT−2)=Eqϕ(z1:T|x)logpθ(z1|z2)qϕ(z1|x)+Eqϕ(z1:T|x)logpθ(z2|z3)qϕ(z2|z1)+⋯+Eqϕ(z1:T|x)logpθ(zt|zt+1)qϕ(zt|zt−1)+⋯+Eqϕ(z1:T|x)logpθ(zT−1|zT)qϕ(zT−1|zT−2)(15)

现在回到VDM的假设,初始数据x与隐变量zt具有相同的维度,再加之后续公式表达的便捷,于是设置z0=x,而隐变量zt其中t∈[1,T]。于是VDM的ELBO可以写成为如下形式:
ELBOVDM=Eqϕ(z1:T|z0)[log pθ(z0|z1)]+Eqϕ(z1:T|z0)[log p(zT)qϕ(zT|zT−1)]+Eqϕ(z1:T|z0)logpθ(z1|z2)qϕ(z1|z0)+Eqϕ(z1:T|z0)logpθ(z2|z3)qϕ(z2|z1)+⋯+Eqϕ(z1:T|z0)logpθ(zt|zt+1)qϕ(zt|zt−1)+⋯+Eqϕ(z1:T|z0)logpθ(zT−1|zT)qϕ(zT−1|zT−2)(16)
简化形式为
ELBOVDM=Eqϕ(z1:T|z0)[log pθ(z0|z1)]+Eqϕ(z1:T|z0)[log p(zT)qϕ(zT|zT−1)]+T−1∑t=1Eqϕ(z1:T|z0)[log pθ(zt|zt+1)qϕ(zt|zt−1)](17)

进一步化简,公式(17)中的期望Eqϕ(z1:T|z0) 是关于T个变量z1,z2,⋯,zT的期望。由于马尔科夫假设,因此可将qϕ(z1:T|z0)进行拆解即注2
qϕ(z1:T|z0)=qϕ(z1|z0)qϕ(z2|z1)⋯qϕ(zt+1|zt)⋯pϕ(zT|zT−1)(18)
- 公式(17)中第1项 期望求解项仅仅涉及变量z1,z0,因此其余项可以省略即
Eqϕ(z1:T|z0)[log pθ(z0|z1)]=Eqϕ(z1|z0)[log pθ(z0|z1)](19)
- 公式(17)中第2项 期望求解项仅仅涉及变量zT−1,zT,因此其余项可以省略即
Eqϕ(z1:T|z0)[log p(zT)qϕ(zT|zT−1)]=Eqϕ(zT−1,zT|z0)[log p(zT)qϕ(zT|zT−1)](20)
- 公式(17)中第3项中 期望求解项仅仅涉及变量zt−1,zt,zt+1,因此其余项可以省略即
Eqϕ(z1:T|z0)[log pθ(zt|zt+1)qϕ(zt|zt−1)]=Eqϕ(zt−1,zt,zt+1|z0)[log pθ(zt|zt+1)qϕ(zt|zt−1)](21)
又由于qϕ(zT−1,zT|z0)=qϕ(zT−1|z0)qϕ(zT|zT−1), 因此
Eqϕ(zT−1,zT|z0)[log p(zT)qϕ(zT|zT−1)]=Eqϕ(zT−1|z0)[qϕ(zT|zT−1)log p(zT)qϕ(zT|zT−1)]=−Eqϕ(zT−1|z0)[DKL(qϕ(zT|zT−1)||p(zT))](22)
又由于qϕ(zt−1,zt,zt+1|z0)=qϕ(zt−1,zt+1|z0)qϕ(zt|zt−1), 因此
Eqϕ(zt−1,zt,zt+1|z0)[log pθ(zt|zt+1)qϕ(zt|zt−1)]=Eqϕ(zt−1,zt+1|z0)[qϕ(zt|zt−1)log pθ(zt|zt+1)qϕ(zt|zt−1)]=−Eqϕ(zt−1,zt+1|z0)DKL(qϕ(zt|zt−1)||pθ(zt|zt+1))(23)
最终得到VDM的变分下界
ELBOVDM=Eqϕ(z1|z0)[log pθ(z0|z1)]−Eqϕ(zT−1|z0)DKL(qϕ(zT|zT−1)||p(zT))−T−1∑t=1Eqϕ(zt−1,zt+1|z0)DKL(qϕ(zt|zt−1)||pθ(zt|zt+1))(24)
VDM变分下界各个子项解释
- Eqϕ(z1|z0)[log pθ(z0|z1)] 看作重建项,表示在给定第1步隐变量取值后,重建/得到原始数据的对数概率。
- Eqϕ(zT−1|z0)DKL(qϕ(zT|zT−1)||p(zT)) 看作先验匹配项,表示最后一步的隐变量的分布与高斯先验分布之间的KL散度,最小化即要求这两个分布要相等或者匹配。由于隐变量的概率分布的特殊设计,这一点是可以保证的,不需要训练。
- Eqϕ(zt−1,zt+1|z0)DKL(qϕ(zt|zt−1)||pθ(zt|zt+1)) 看作一致性项,表示对于每一个隐变量zt,从encoder方向编码出隐变量zt概率分布与从decoder方向解码出zt概率分布是相等的/一致的。
推导二
VDM优化主要集中在一致性项上面,因为是关于时间步进行求和共计T−1项。VDM的变分下界ELBO都是期望,因此可以使用蒙特卡罗估计方法进行近似计算。然而基于这些项进行求解优化并不是最优的,因为一致性项求期望是关于两个随机变量zt−1,zt+1进行计算的,使用蒙特卡罗估计的方差比基于单个随机变量的方差要大。而且T−1个一致性项加在一起,最终的方差可能更大。
因此就需要想办法减少期望中的随机变量的个数,此处的关键是将encoder过程中每个时间步变换q(zt|zt−1)改写为q(zt|zt−1,z0)。这是因为马尔科夫特性仅条件依赖相邻的前一项,因此q(zt|zt−1,z0)中z0添加上或者省略都不影响这个变换。因此
q(zt|zt−1)=q(zt|zt−1,z0)(25)
再由贝叶斯公式改写每个时间步变换得到
q(zt|zt−1,z0)=q(zt−1|zt,z0)q(zt|z0)q(zt−1|z0)(26)
之所以如此变换,就是为了反转条件概率中变量的顺序,不再像上述推导中从encoder和decoder两个不同方向去保证时间步t时刻的隐变量zt的条件概率分布一致,也就不会出现了期望是关于两个隐变量zt−1,zt+1。如此变换后,从encoder和decoder部分得到的隐变量zt的条件概率分布都仅条件依赖zt+1,从而实现了求期望过程中减少了随机变量的个数,从理论上减少了求解过程中的方差。
基于此,再对ELBO进行推导
log p(x)≥Eqϕ(z1:T|z0)logp(zT)∏T−1t=0pθ(zt|zt+1)∏T−1t=0qϕ(zt+1|zt)=Eqϕ(z1:T|z0)logp(zT)pθ(z0|z1)pθ(z1|z2)⋯pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|z0)qϕ(z2|z1)⋯qϕ(zt+1|zt)⋯pϕ(zT|zT−1)=Eqϕ(z1:T|z0)logp(zT)pθ(z0|z1)pθ(z1|z2)⋯pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|z0)qϕ(z2|z1,z0)qϕ(z3|z2,z0)⋯qϕ(zt|zt−1,z0)qϕ(zt+1|zt,z0)⋯pϕ(zT|zT−1,z0)=Eqϕ(z1:T|z0)logp(zT)pθ(z0|z1)pθ(z1|z2)pθ(z2|z3)⋯pθ(zt−1|zt)pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|z0)qϕ(z1|z2,z0)qϕ(z2|z0)qϕ(z1|z0)qϕ(z2|z3,z0)qϕ(z3|z0)qϕ(z2|z0)⋯q(zt−2|zt−1,z0)q(zt−1|z0)q(zt−2|z0)q(zt−1|zt,z0)q(zt|z0)q(zt−1|z0)⋯pϕ(zT−1|zT,z0)qϕ(zT|z0)qϕ(zT|z0)=Eqϕ(z1:T|z0)logp(zT)pθ(z0|z1)pθ(z1|z2)⋯pθ(zt|zt+1)⋯pθ(zT−1|zT)qϕ(z1|z2,z0)qϕ(z2|z3,z0)⋯qϕ(zt−2|zt−1,z0)qϕ(zt−1|zt,z0)⋯qϕ(zT−1|zT,z0)qϕ(zT|z0)=Eqϕ(z1:T|z0)[log pθ(z0|z1)+log p(zT)qϕ(zT|z0)+T∑t=2log pθ(zt−1|zt)qϕ(zt−1|zt,z0)]=Eqϕ(z1:T|z0)[log pθ(z0|z1)]+Eqϕ(z1:T|z0)[log p(zT)qϕ(zT|z0)]+Eqϕ(z1:T|z0)[T∑t=2log pθ(zt−1|zt)qϕ(zt−1|zt,z0)]=Eqϕ(z1|z0)[log pθ(z0|z1)]+Eqϕ(zT|z0)[log p(zT)qϕ(zT|z0)]+T∑t=2Eqϕ(zt−1,zt|z0)[log pθ(zt−1|zt)qϕ(zt−1|zt,z0)]=Eqϕ(z1|z0)[log pθ(z0|z1)]−DKL(qϕ(zT|z0)||p(zT))−T∑t=2Eqϕ(zt|z0)[DKL(qϕ(zt−1|zt,z0)||pθ(zt−1|zt))](27)
- 同样地,Eqϕ(z1|z0)[log pθ(z0|z1)] 看作重建项,表示在给定第1步隐变量取值后,重建/得到原始数据的对数概率。
- DKL(qϕ(zT|z0)||p(zT)) 看作先验匹配项,表示最后一步的隐变量的分布与高斯先验分布之间的KL散度,最小化即要求这两个分布要相等或者匹配。由于隐变量的概率分布的特殊设计,这一点是可以保证的,不需要训练。
- Eqϕ(zt|z0)[DKL(qϕ(zt−1|zt,z0)||pθ(zt−1|zt))] 这项并没有称之为一致性项,而是去噪匹配项。通过上述变化得到了zt处的真实去噪转换概率分布,而模型学习的是pθ(zt−1|zt)。因此希望模型学习到的概率分布和真实的概率分布是相等的,刚好这个KL散度就可以衡量。
注意公式(27)中的各个项与公式(24)的异同。
推导三
再回顾关于VDM对encoder部分每个时间步的高斯分布假设,设
zt=αtzt−1+βtϵt,α2t+β2t=1,αt,βt>0,ϵt∼N(0,I)
换一种表达形式,就是zt∼N(zt;αtzt−1,β2tI) 或者q(zt|zt−1)=N(zt;αtzt−1,β2tI)
然后可以推导出
zt=(αt⋯α1)记为¯αtz0+√1−(αt⋯α1)2记为¯βt¯ϵt,¯ϵt∼N(0,I)
换一种表达形式,就是zt∼N(zt;¯αtz0,¯β2tI),即q(zt|zt−1)=N(zt;¯αtz0,¯β2tI)。
现在要对公式(27)中第3项中的DKL(qϕ(zt−1|zt,z0)||pθ(zt−1|zt)) 进行进一步变换。由公式(26)可以得到
q(zt−1|zt,z0)=q(zt|zt−1,z0)q(zt−1|z0)q(zt|z0)=q(zt|zt−1)q(zt−1|z0)q(zt|z0)(28)
再结合上面的公式,可以得到
q(zt−1|zt,z0)=q(zt|zt−1)q(zt−1|z0)q(zt|z0)=N(zt;αtzt−1,β2tI)N(zt−1;¯αt−1z0,¯β2t−1I)N(zt;¯αtz0,¯β2tI)∝exp{−[(zt−αtzt−1)22β2t+(zt−1−¯αt−1z0)22¯β2t−1−(zt−¯αtz0)22¯β2t]}=exp{−12[z2t−2αtztzt−1+α2tz2t−1β2t+z2t−1−2¯αt−1zt−1z0+¯α2t−1z20¯β2t−1−(zt−¯αtz0)22¯β2t]}=exp{−12[−2αtztzt−1+α2tz2t−1β2t+z2t−1−2¯αt−1zt−1z0¯β2t−1+C(zt,z0)]}∝exp{−12[(α2tβ2t+1¯β2t−1)z2t−1−2(αtztβ2t+¯αt−1z0¯β2t−1α2tβ2t+1¯β2t−1)zt−1]}∝exp{−12(11α2tβ2t+1¯β2t−1)(zt−1−αtztβ2t+¯αt−1z0¯β2t−1α2tβ2t+1¯β2t−1)2}=N(zt−1;αtztβ2t+¯αt−1z0¯β2t−1α2tβ2t+1¯β2t−1,1α2tβ2t+1¯β2t−1)(xx)
令
μq(zt,z0)=αtztβ2t+¯αt−1z0¯β2t−1α2tβ2t+1¯β2t−1
Σq(t)=1α2tβ2t+1¯β2t−1
由于 β2t=1−α2t,¯α2t=(α1⋯αt)2,¯β2t=1−¯α2t,因此可以很容易得到
μq(zt,z0)=αt(1−¯α2t−1)zt+¯αt−1(1−α2t)z01−¯α2t
Σq(t)=(1−α2t)(1−¯α2t−1)1−¯α2t
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律