生成模型-流模型(Flow)

前言

  • Flow-based模型的不同之处
    从去年GLOW提出之后,我就一直对基于流(flow)的生成模型是如何实现的充满好奇,但一直没有彻底弄明白,直到最近观看了李宏毅老师的教程之后,很多细节都讲解地比较清楚,就想好好写篇笔记来梳理一下流模型的运作原理。
    首先来简单介绍一下流模型,它是一种比较独特的生成模型——它选择直接直面生成模型的概率计算,也就是把分布转换的积分式( p G ( x ) ) = ∫ z p ( x ∣ z ) p ( z ) d z p_G(x))= \int_{z}p(x|z)p(z)dz pG(x))=zp(xz)p(z)dz)给硬算出来。要知道现阶段其他较火的生成模型,要么采用优化上界或采用对抗训练的方式去避开概率计算,从而寻找近似逼近真实分布的方法,但是流模型选择了一条硬路(主要是通过变换Jacobian行列式)来求解,在后文会详细介绍。

    流模型有一个非常与众不同的特点是,它的转换通常是可逆的。也就是说,流模型不仅能找到从A分布变化到B分布的网络通路,并且该通路也能让B变化到A,简言之流模型找到的是一条A、B分布间的双工通路。当然,这样的可逆性是具有代价的——A、B的数据维度必须是一致的。
    A、B分布间的转换并不是轻易能做到的,流模型为实现这一点经历了三个步骤:最初的NICE实现了从A分布到高斯分布的可逆求解;后来RealNVP实现了从A分布到条件非高斯分布的可逆求解;而最新的GLOW,实现了从A分布到B分布的可逆求解,其中B分布可以是与A分布同样复杂的分布,这意味着给定两堆图片,GLOW能够实现这两堆图片间的任意转换。
    下面就是流模型学习笔记的正文,尽可能较简明地讲解清楚流模型的运行机制。

1.Flow-Based Model的建模思维

首先来回顾一下生成模型要解决的问题:
在这里插入图片描述

如上图所示,给定两组数据 z z z x x x,其中 z z z服从已知的简单先验分布 π ( z ) π(z) π(z)(通常是高斯分布), x x x服从复杂的分布 p ( x ) p(x) p(x)(即训练数据代表的分布),现在我们想要找到一个变换函数 f f f,它能建立一种 z z z x x x的映射 f : z → x f:z \to x f:zx,使得每对于 π ( z ) π(z) π(z)中的一个采样点 z ′ {z}' z,都能在 p ( x ) p(x) p(x)中有一个(新)样本点 x ′ {x}' x与之对应。
如果这个变换函数能找到的话,那么我们就实现了一个生成模型的构造。因为, p ( x ) p(x) p(x)中的每一个样本点都代表一张具体的图片,如果我们希望机器画出新图片的话,只需要从 π ( z ) π(z) π(z)中随机采样一个点,然后通过 f : z → x f:z \to x f:zx,得到新样本点 x x x,也就是对应的生成的具体图片。
所以,接下来的关键在于,这个变换函数f如何找呢?我们先来看一个最简单的例子。
在这里插入图片描述

如上图所示,假设z和x都是一维分布,其中z满足简单的均匀分布: π ( z ) = 1 ( z ∈ [ 0 , 1 ] ) \pi(z) = 1(z \in [0,1]) π(z)=1(z[0,1]),x也满足简单均匀分布: p ( x ) = 0.5 ( x ∈ [ 1 , 3 ] ) p(x) = 0.5(x \in [1,3]) p(x)=0.5(x[1,3])

在这里插入图片描述

那么构建z与x之间的变换关系只需要构造一个线性函数即可: x = f ( z ) = 2 z + 1 x=f(z)=2z+1 x=f(z)=2z+1
下面再考虑非均匀分布的更复杂的情况:
在这里插入图片描述

如上图所示, π ( z ) π(z) π(z) p ( x ) p(x) p(x)都是较为复杂的分布,为了实现二者的转化,我们可以考虑在很短的间隔上将二者视为简单均匀分布,然后应用前边方法计算小段上的 f △ f_{\triangle } f,最后将每个小段变换累加起来(每个小段实际对应一个采样样本)就得到最终的完整变换式 f f f
在这里插入图片描述

如上图所示,假设在 [ z ′ , z ′ + △ z ] [{z}',{z}′+{\triangle z}] [z,z+z] π ( z ) π(z) π(z)近似服从均匀分布,在 [ x ′ , x ′ + △ x ] [x′,x′+\triangle x] [x,x+x] p ( x ) p(x) p(x)也近似服从均匀分布,于是有 p ( x ′ ) △ x = π ( z ′ ) △ z p({x}′) \triangle x=\pi ({z}′) \triangle z p(x)x=π(z)z(因为变换前后的面积/即采样概率是一致的),当 △ x \triangle x x △ z \triangle z z极小时,有:
p ( x ′ ) = π ( z ′ ) d z d x p({x}') = \pi({z}') \frac{dz}{dx} p(x)=π(z)dxdz
又考虑到 d z d x \frac{dz}{dx} dxdz有可能是负值(如下图所示),而 p ( x ′ ) p({x}') p(x) π ( z ′ ) \pi({z}') π(z)都为非负,所以 p ( x ′ ) p({x}') p(x) π ( z ′ ) \pi({z}') π(z)的实际关系为: p ( x ′ ) p({x}') p(x) = π ( z ′ ) ∣ d z d x \pi({z}')|\frac{dz}{dx} π(z)dxdz
在这里插入图片描述

下面进一步地做推广,我们考虑z与x都是二维分布的情形。
在这里插入图片描述

如上图所示, z z z x x x都是二维分布,左图中浅蓝色区域表示初始点 z ′ {z}' z z 1 z_1 z1方向上移动 ∆ z 1 ∆z_1 z1,在 z 2 z_2 z2方向上移动 ∆ z 2 ∆z_2 z2所形成的区域,这一区域通过 f : z → x f:z \to x f:zx映射,形成右图所示 x x x域上的浅绿色菱形区域。其中,二维分布 π ( z ) π(z) π(z) p ( x ) p(x) p(x)均服从简单均匀分布,其高度在图中未画出(垂直纸面向外)。如上图所示, z z z x x x都是二维分布,左图中浅蓝色区域表示初始点在方向上移动 Δ Δ Δ,在方向上移动 Δ Δ Δ所形成的区域,这一区域通过映射,形成右图所示 x x x域上的浅绿色菱形区域。其中,二维分布 π ( z ) π(z) π(z) p ( x ) p(x) p(x)均服从简单均匀分布,其高度在图中未画出(垂直纸面向外)。

因为蓝色区域与绿色区域具有相同的体积,所以有:
p ( x ′ ) ∣ d e t [ △ x 11 △ x 12 △ x 21 △ x 22 ] ∣ = π ( z ′ ) △ z 1 △ z 2 p({x}')|det \begin{bmatrix} \triangle x_{11}&\triangle x_{12} \\ \triangle x_{21}&\triangle x_{22} \end{bmatrix} | = \pi(z')\triangle z_1 \triangle z_2 p(x)det[x11x21x12x22]=π(z)z1z2
其中代表行列式计算
d e t [ △ x 11 △ x 12 △ x 21 △ x 22 ] det \begin{bmatrix} \triangle x_{11}&\triangle x_{12} \\ \triangle x_{21}&\triangle x_{22} \end{bmatrix} det[x11x21x12x22]
,它的计算结果等于上图中浅绿色区域的面积(行列式的定义)。下面我们将 ∆ z 1 ∆ z 2 ∆z_1∆z_2 z1z2移至左侧,得到:
p ( x ′ ) ∣ 1 △ z 1 △ z 2 [ △ x 11 △ x 12 △ x 21 △ x 22 ] ∣ = π ( z ′ ) p({x}')|\frac{1}{\triangle z_1 \triangle z_2} \begin{bmatrix} \triangle x_{11}&\triangle x_{12} \\ \triangle x_{21}&\triangle x_{22} \end{bmatrix} | = \pi(z') p(x)z1z21[x11x21x12x22]=π(z)
即:
p ( x ′ ) ∣ d e t [ △ x 11 △ z 1 △ x 12 △ z 2 △ x 21 △ z 1 △ x 22 △ z 2 ] ∣ = π ( z ′ ) p({x}')|det \begin{bmatrix} \triangle \frac{x_{11}}{\triangle z_1}&\frac{\triangle x_{12}}{\triangle z_2}\\ \triangle \frac{x_{21}}{\triangle z_1}&\frac{\triangle x_{22}}{\triangle z_2} \end{bmatrix} | = \pi(z') p(x)det[z1x11z1x21z2x12z2x22]=π(z)
△ z 1 △ z 2 \triangle z_1 \triangle z_2 z1z2很小时,有:
p ( x ′ ) = π ( z ′ ) ∣ d e t ( J f − 1 ) ∣ p(x') = \pi(z') |det(J_{f^{-1}})| p(x)=π(z)det(Jf1)

其中 J f − 1 J_{f^{-1}} Jf1代表从 x x x变换为 z z z的变换式,即: z = f − 1 ( x ) z = \text{f}^{-1}(x) z=f1(x)。至此,我们得到了一个比较重要的结论:如果 z z z x x x分别满足两种分布,并且 z z z通过函数f能够转变为 x x x,那么 z z z x x x中的任意一组对应采样点 z ′ z' z x ′ x' x之间的关系为:
{ π ( z ′ ) = p ( x ′ ) ∣ d e t ( J f ) ∣ p ( x ′ ) = π ( z ′ ) ∣ d e t ( J f − 1 ) ∣ \left\{\begin{matrix} \pi(z') = p(x')|det(J_f)| \\ p(x') = \pi(z')|det(J_{f^{-1}})| \end{matrix}\right. {π(z)=p(x)det(Jf)p(x)=π(z)det(Jf1)

那么基于这一结论,再带回到生成模型要解决的问题当中,我们就得到了Flow-based Model(流模型)的初步建模思维。
在这里插入图片描述

如上图所示,为了实现 z   π ( z ) z~\pi(z) z π(z) x = G ( z )   p G ( x ) x = G(z)~p_{G}(x) x=G(z) pG(x)间的转化,待求解的生成器G的表达式为:
G ∗ = a r g max ⁡ G ∑ i = 1 m l o g p G ( x i ) G^{*} = arg\max_{G} \sum_{i=1}^{m}logp_{G}(x^i) G=argGmaxi=1mlogpG(xi)
基于前面推导,我们有 p G ( x ) p_{G}(x) pG(x)中的样本点与 π ( z ) \pi(z) π(z)中的样本点间的关系为:
p G ( x i ) = π ( z i ) ( ∣ d e t ( J G ) ∣ ) − 1 p_{G}(x_i) = \pi(z^i)(|det(J_G)|)^{-1} pG(xi)=π(zi)(det(JG))1
其中 z i = G − 1 ( x i ) z_i = G^{-1}(x^i) zi=G1(xi)
所以,如果 G ∗ G^{*} G的目标式能够通过上述关系式求解出来,那么我们就实现了一个完整的生成模型的求解。Flow-based Model就是基于这一思维进行理论推导和模型构建,下面将会详细解释Flow-based Model的求解过程。

2.Flow-based Model的理论推导&架构设计

我们关注一下上一章中引出的式子:
p G ( x i ) = π ( z i ) ( ∣ d e t ( J G ) ∣ ) − 1 , z i = G − 1 ( x i ) p_{G}(x_i) = \pi(z^i)(|det(J_G)|)^{-1}, z^{i} = G^{-1}(x^i) pG(xi)=π(zi)(det(JG))1,zi=G1(xi)
将其取log,得到:
l o g ( p G ( x i ) ) = l o g ( π ( z i ) ) + l o g ( ( ∣ d e t ( J G ) ∣ ) − 1 ) log(p_{G}(x_i)) = log(\pi(z^i)) + log((|det(J_G)|)^{-1}) log(pG(xi))=log(π(zi))+log((det(JG))1)

现在,如果想直接求解这个式子有两方面的困难。第一个困难是, d e t ( J G − 1 ) det(J_{G^{-1}}) det(JG1)是不好计算的——由于 G − 1 G^{-1} G1的Jacobian矩阵一般维度不低(譬如256 ⋆ \star 256矩阵),其行列式的计算量是异常巨大的,所以在实际计算中,我们必须对 G − 1 G^{-1} G1的Jacobian行列式做一定优化,使其能够在计算上变得简洁高效。第二个困难是,表达式中出现了 G − 1 G^{-1} G1,这意味着我们要知道 G − 1 G^{-1} G1长什么样子,而我们的目标是求 G G G,所以这需要巧妙地设计 G G G的结构使得 G − 1 G^{-1} G1也是好计算的。

下面我们来逐步设计 G G G的结构,首先从最基本的架构开始构思。考虑到 G − 1 G^{-1} G1必须是存在的且能被算出,这意味着 G G G的输入和输出的维度必须是一致的并且 G G G的行列式不能为0。然后,既然 G − 1 G^{-1} G1可以计算出来,而 l o g ( p G ( x i ) ) log(p_G(x^i)) log(pG(xi))的目标表达式只与 G − 1 G^{-1} G1有关,所以在实际训练中我们可以训练 G − 1 G^{-1} G1对应的网络,然后想办法算出 G G G来并且在测试时改用 G G G做图像生成。
在这里插入图片描述

如上图所示,在训练时我们从真实分布 p d a t a ( x ) p_{data}(x) pdata(x)中采样出 x i x^{i} xi,然后去训练 G − 1 G^{-1} G1,使得通过 G − 1 G^{-1} G1生成的 z i = G − 1 ( x i ) z^i = G^{-1}(x^i) zi=G1(xi)满足特定的先验分布;接下来在测试时,我们从 z z z中采样出一个点 z j z^{j} zj,然后通过 G G G生成的样本 x j = G ( z j ) x^j = G(z^j) xj=G(zj)就是新的生成图像。
接下来开始具体考虑 G G G的内部设计,为了让 G − 1 G^{-1} G1可以计算并且 G G G的Jacobian行列式也易于计算,Flow-based Model采用了一种称为耦合层(Coupling Layer)的设计来实现。
在这里插入图片描述

如上图所示, z z z x x x都会被拆分成两个部分,分别是前 1   d 1~d 1 d维和后 d + 1   D d+1~D d+1 D维。从 z z z变化为 x x x的计算式为: z z z 1   d 1~d 1 d维直接复制(copy)给 x x x 1   d 1~d 1 d维; z z z d + 1   D d+1~D d+1 D维分别通过 F F F H H H两个函数变换为和,然后 x i = β i z i + γ i ( i = d + 1 , . . . , D ) x_i = \beta_i z_i+\gamma_i(i=d+1,...,D) xi=βizi+γi(i=d+1,...,D)通过的仿射计算(affine)传递给 x x x。综上,由 z z z传给 x x x的计算式可以写为:
{ x i = z i , i ≤ d x i = β i z i + γ i , i > d \left\{\begin{matrix} x_i = z_i,i \le d\\ x_i = \beta_i z_i + \gamma_i, i>d \end{matrix}\right. {xi=zi,idxi=βizi+γi,i>d

其逆运算的计算式,即由x传给z的计算式,可以非常方便地推导出来为:
{ z i = x i , i ≤ d z i = x i − γ i β i , i > d \left\{\begin{matrix} z_i = x_i,i \le d\\ z_i = \frac{x_i - \gamma_i}{\beta_i} , i>d \end{matrix}\right. {zi=xi,idzi=βixiγi,i>d

上面我们说明了,这样设计的耦合层能快速计算出 G − 1 G^{-1} G1,下面我们来说明,其在 G G G的Jacobian行列式的计算上也是非常简便。
在这里插入图片描述

上图展示了 G G G的Jacobian行列式的计算矩阵。首先由于 z i . . . d z_{i...d} zi...d直接传递给 x i . . . d x_{i...d} xi...d所以Jacobian矩阵的左上角区域是单位矩阵 I I I,然后 x i . . . d x_{i...d} xi...d完全不受 z d + 1... D z_{d+1...D} zd+1...D影响,所以Jacobian矩阵的右上角区域是零矩阵O,这导致Jacobian矩阵的左下角区域的值对Jacobian矩阵行列式的计算没有影响,也就无需考虑。最后我们关注Jacobian矩阵的右下角区域,由于 x i = β i z i + γ i ( i > d ) x_i = \beta_i z_i + \gamma_i (i>d) xi=βizi+γi(i>d),所以只有在 i = j i=j i=j的情况下 ∂ x i ∂ z j ≠ 0 \frac{\partial x_i}{\partial z_j} \ne 0 zjxi=0,而在 i ≠ j i \ne j i=j ∂ x i ∂ z j = 0 \frac{\partial x_i}{\partial z_j} = 0 zjxi=0,所以Jacobian矩阵的右下角区域是一个对角矩阵。

最终,该 G G G的Jacobian的行列式计算式就表示为:
d e t ( J G ) = ∂ x d + 1 ∂ z d + 1 ∂ x d + 2 ∂ z d + 2 ∂ x D ∂ z D = β d + 1 β d + 2 ⋅ ⋅ ⋅ β D det(J_G) = \frac{\partial x_{d+1}}{\partial z_{d+1}} \frac{\partial x_{d+2}}{\partial z_{d+2}} \frac{\partial x_{D}}{\partial z_{D}} = \beta_{d+1} \beta_{d+2}···\beta_{D} det(JG)=zd+1xd+1zd+2xd+2zDxD=βd+1βd+2⋅⋅⋅βD
这确实是一个易于计算的简单表达式。接下来可以考虑,由于上述措施对 G G G做了诸多限制,导致 G G G的变换能力有限,所以我们可以堆叠多个 G G G,去增强模型的变换拟合能力。
在这里插入图片描述

如上图所示,我们将多个耦合层堆叠在一起,从而形成一个更完整的生成器。但是这样会有一个新问题,就是最终生成数据的前d维与初始数据的前d维是一致的,这会导致生成数据中总有一片区域看起来像是固定的图样(实际上它代表着来自初始高斯噪音的一个部分),我们可以通过将复制模块(copy)与仿射模块(affine)交换顺序的方式去解决这一问题。
在这里插入图片描述

如上图所示,通过将某些耦合层的copy与affine模块进行位置上的互换,使得每一部分数据都能走向copy->affine->copy->affine的交替变换通道,这样最终的生成图像就不会包含完全copy自初始图像的部分。值得说明的是,在图像生成当中,这种copy与affine模块互换的方式有很多种,下面举两个例子来说明:
在这里插入图片描述
上图展示了两种按照不同的数据划分方式做copy与affine的交替变换。左图代表的是在像素维度上做划分,即将横纵坐标之和为偶数的划分为一类,和为奇数的划分为另外一类,然后两类分别交替做copy和affine变换(两两交替);右图代表的是在通道维度上做划分,通常图像会有三通道,那么在每一次耦合变换中按顺序选择一个通道做copy,其他通道做affine(三个轮换交替),从而最终变换出我们需要的生成图形出来。
在这里插入图片描述
更进一步地,如何进行copy和affine的变换能够让生成模型学习地更好,这是一个可以由机器来学习的部分,所以我们引入W矩阵,帮我们决定按什么样的顺序做copy和affine变换,这种方法叫做1×1 convolution(被用于知名的GLOW当中)。1×1 convolution只需要让机器决定在每次仿射计算前对图片哪些区域实行像素对调,而保持copy和affine模块的顺序不变,这实际上和对调copy和affine模块顺序产生的效果是一致的。
在这里插入图片描述
这种对调的原理非常简单,如上图所示举例,假设我们需要将(3,1,2)向量替换成(1,2,3)向量,只需要将w矩阵定义为图中所示矩阵即可。下面我们看一下,将w引入flow模型之后,对于原始的Jacobian行列式的计算是否会有影响。

对于每一个3*3维划分上的仿射操作来说,由 x = f ( x ) = W z x= f(x) = W_z x=f(x)=Wz我们可以得到f的Jacobian行列式的计算结果为:
J ( f ) = [ ∂ x 1 ∂ z 1 ∂ x 1 ∂ z 2 ∂ x 1 ∂ z 3 ∂ x 2 ∂ z 1 ∂ x 2 ∂ z 2 ∂ x 2 ∂ z 3 ∂ x 3 ∂ z 1 ∂ x 3 ∂ z 2 ∂ x 3 ∂ z 3 ] = [ w 11 w 12 w 13 w 21 w 22 w 23 w 31 w 32 w 33 ] = W J(f) = \begin{bmatrix} \frac{\partial x_{1}}{\partial z_{1}}&\frac{\partial x_{1}}{\partial z_{2}}&\frac{\partial x_{1}}{\partial z_{3}} \\ \frac{\partial x_{2}}{\partial z_{1}}&\frac{\partial x_{2}}{\partial z_{2}}&\frac{\partial x_{2}}{\partial z_{3}} \\ \frac{\partial x_{3}}{\partial z_{1}}&\frac{\partial x_{3}}{\partial z_{2}}&\frac{\partial x_{3}}{\partial z_{3}} \\ \end{bmatrix} = \begin{bmatrix} w_{11}&w_{12}&w_{13} \\ w_{21}&w_{22}&w_{23} \\ w_{31}&w_{32}&w_{33} \\ \end{bmatrix}= W J(f)= z1x1z1x2z1x3z2x1z2x2z2x3z3x1z3x2z3x3 = w11w21w31w12w22w32w13w23w33 =W

代入到整个含有d个3*3维的仿射变换矩阵当中,得到最终的Jacobian行列式的计算结果就为:
( d e t ( W ) ) d ∗ d (det(W))^{d*d} (det(W))dd,如下图所示:
在这里插入图片描述
综上,关于Flow-based Model的理论讲解和架构分析就全部结束了,它通过巧妙地构造仿射变换的方式实现不同分布间的拟合,并实现了可逆计算和简化雅各比行列式计算的功能和优点,最终我们可以通过堆叠多个这样的耦合层去拟合更复杂的分布变化(如上图所示),从而达到生成模型需要的效果。

参考

https://www.gwylab.com/note-flow_based_model.html

  • 23
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一个简单的 flow 生成模型Python 代码示例: ```python import numpy as np import tensorflow as tf # 定义模型参数 num_layers = 2 hidden_size = 256 batch_size = 64 seq_length = 50 learning_rate = 0.001 num_epochs = 100 # 加载数据 data = open('input.txt', 'r').read() chars = list(set(data)) data_size, vocab_size = len(data), len(chars) char_to_ix = { ch:i for i,ch in enumerate(chars) } ix_to_char = { i:ch for i,ch in enumerate(chars) } # 定义输入和输出 inputs = tf.placeholder(tf.int32, shape=[batch_size, seq_length]) targets = tf.placeholder(tf.int32, shape=[batch_size, seq_length]) # 定义模型 cell = tf.contrib.rnn.BasicLSTMCell(hidden_size) cell = tf.contrib.rnn.MultiRNNCell([cell] * num_layers) initial_state = cell.zero_state(batch_size, tf.float32) embedding = tf.get_variable('embedding', [vocab_size, hidden_size]) inputs_embedded = tf.nn.embedding_lookup(embedding, inputs) outputs, final_state = tf.nn.dynamic_rnn(cell, inputs_embedded, initial_state=initial_state) # 定义损失函数和优化器 loss = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(labels=targets, logits=outputs)) optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(loss) # 训练模型 with tf.Session() as sess: sess.run(tf.global_variables_initializer()) for epoch in range(num_epochs): state = sess.run(initial_state) for i in range(0, data_size - seq_length - 1, seq_length): inputs_batch = np.zeros((batch_size, seq_length)) targets_batch = np.zeros((batch_size, seq_length)) for j in range(batch_size): inputs_batch[j] = [char_to_ix[ch] for ch in data[i+j:i+j+seq_length]] targets_batch[j] = [char_to_ix[ch] for ch in data[i+j+1:i+j+seq_length+1]] feed_dict = {inputs: inputs_batch, targets: targets_batch, initial_state: state} _, state, loss_val = sess.run([optimizer, final_state, loss], feed_dict=feed_dict) print('Epoch %d, Loss: %.3f' % (epoch+1, loss_val)) ``` 这段代码使用 TensorFlow 实现了一个基本的两层 LSTM 神经网络,用于生成文本数据。它将文本数据分成多个序列,每个序列包含固定数量的字符。模型的输入是一个序列的字符,目标是预测下一个字符。在训练过程中,模型使用随机梯度下降来最小化损失函数,并生成新的文本数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

alstonlou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值