AIGC实战——变分自编码器(Variational Autoencoder, VAE)

AIGC实战——变分自编码器

    • 0. 前言
    • 1. 变分自编码器
      • 1.1 基本原理
      • 1.2 编码器
    • 2. 构建VAE编码器
      • 2.1 Sampling 层
      • 2.2 编码器
      • 2.3 损失函数
      • 2.4 训练变分自编码器
    • 3. 变分自编码器分析
    • 小结
    • 系列链接

0. 前言

我们已经学习了如何实现自编码器,并了解了自编码器无法在潜空间中的空白位置处生成逼真的图像,且空间分布并不均匀,为了解决这些问题,我们需要将自编码器 (Autoencoder, AE) 改进为变分自编码器 (Variational Autoencoder, VAE)。在本节中,我们将学习变分自编码器的基本原理,并使用 Keras 实现变分自编码器模型。

1. 变分自编码器

1.1 基本原理

变分自编码器 (Variational Autoencoder, VAE) 是一种结合了自编码器和概率图模型的生成模型,通过学习输入数据的潜分布来进行无监督学习并生成新样本。
与传统的自编码器不同,变分自编码器引入了概率编码器和概率解码器。编码器将输入数据映射到潜在空间的概率分布,而解码器将从潜在空间中的样本重构为原始输入的分布。通过学习这两个概率分布,VAE 可以学习到输入数据的概率表示,并且能够通过从潜在空间中采样生成新的样本。
在训练过程中,VAE 使用最大似然估计来最小化观测数据与重构数据之间的差异,并通过最小化编码器和解码器之间的 KL 散度来约束潜在空间的分布与先验分布之间的差异。这使得 VAE 能够学习到连续且平滑的潜在表示,使得在潜在空间中的相邻点对应于语义上相似的样本。
接下来,我们从数学角度介绍我们需要哪些改动,才能将自编码器转换为变分自编码器,从而使其成为一个更复杂的生成模型。实际上,相比于自编码器,我们仅需要改变两部分:编码器和损失函数。

1.2 编码器

在自编码器中,每个图像直接映射为潜空间中的一个点。在变分自编码器中,每个图像映射为潜在空间中某一点周围的多元正态分布:

编码器

多元正态分布 (Multivariate Normal Distribution) 是一种概率分布,也称为高斯分布 (Gaussian Distribution)。其曲线图呈独特的钟形,一维的正态分布由两个变量确定:均值 ( μ μ μ) 和方差 ( σ 2 σ^2 σ2),标准差 ( σ σ σ) 是方差的平方根。在一维空间中,正态分布的概率密度函数为:
f ( x ∣ μ , σ 2 ) = 1 2 π σ 2 e − ( x − μ ) 2 2 σ 2 f(x|μ,σ^2) = \frac 1 {\sqrt{2πσ^2}}e^{-\frac{(x-μ)^2}{2σ^2}} f(xμ,σ2)=2πσ2 1e2σ2(xμ)2
下图展示了不同均值和方差的一维正态分布,红色曲线表示标准正态分布 (Standard Normal Distribution) N ( 0 , 1 ) \mathcal N(0,1) N(0,1),即均值为 0,方差为 1 的正态分布。

正态分布

可以使用以下公式从均值为 μ μ μ,标准差为 σ σ σ 的正态分布中采样一个点 z z z
z = μ + σ ϵ z = μ + σϵ z=μ+σϵ
其中 ϵ ϵ ϵ 是从标准正态分布中采样得到的。
正态分布的概念可以扩展到多维空间,具有均值向量 μ μ μ 和对称协方差矩阵 Σ Σ Σ k k k 维多元正态分布(或多元高斯分布) N ( μ , Σ ) \mathcal N(\mu,Σ) N(μΣ)的概率密度函数如下:
f ( x 1 , . . . , x k ) = e x p ( − 1 2 ( x − μ ) T Σ − 1 ( x − μ ) ) ( 2 π ) k ∣ Σ ∣ f(x_1,...,x_k) = \frac {exp(-\frac 12 (x-μ)^T Σ^{-1} (x-μ)) } {\sqrt{(2π)^k |Σ|}} f(x1,...,xk)=(2π)k∣Σ∣ exp(21(xμ)TΣ1(xμ))
我们通常使用各向同性的多元正态分布,其中协方差矩阵是对角矩阵。这意味着在每个维度上分布都是独立的,即我们可以采样一个向量,其中每个元素都服从独立的均值和方差的正态分布。
多元标准正态分布 (multivariate standard normal distribution) N ( 0 , I ) \mathcal N(0,I) N(0,I) 是一个多元分布,具有零值均值向量和单位协方差矩阵。
在多数情况下,正态分布与高斯分布通常可以互换使用,并且通常隐含着各向同性和多元性。
变分自编码器假设潜空间中的维度之间没有相关性,因此协方差矩阵是对角矩阵。编码器只需将每个输入映射到一个均值向量和一个方差向量,而不需要考虑维度之间的协方差。
此外,我们选择映射为方差的对数,因为对数可以是 ( − ∞ , ∞ ) (-∞, ∞) (,) 范围内的任意实数,与神经网络单元的自然输出范围—致,而方差值始终为正。这样,我们可以使用神经网络作为编码器,将输入图像映射到均值和对数方差向量。
总之,编码器将每个输入图像编码为两个向量 z_meanlog_var,两者共同定义了潜空间中的多元正态分布:

  • z_mean:分布的均值
  • z_log_var:各个维度上方差的对数

我们可以使用以下公式从由上述值定义的分布中采样一个点 z z z
z = z _ m e a n + z _ s i g m a ∗ e p s i l o n z = z\_mean + z\_sigma * epsilon z=z_mean+z_sigmaepsilon
其中:
z _ s i g m a = e x p ( z l o g v a r ∗ 0.5 ) e p s i l o n ∼ N ( 0 , I ) z\_sigma = exp(z_log_var * 0.5)\\ epsilon \sim \mathcal N(0,I) z_sigma=exp(zlogvar0.5)epsilonN(0,I)
z _ s i g m a ( σ ) z\_sigma(σ) z_sigma(σ) z _ l o g _ v a r ( l o g ( σ 2 ) ) z\_log\_var(log(σ^2)) z_log_var(log(σ2)) 之间的关系推导如下:
σ = e x p ( l o g ( σ ) ) = e x p ( 2 l o g ( σ ) / 2 ) = e x p ( l o g ( σ 2 ) / 2 ) σ = exp(log(σ)) = exp(2log(σ)/2) = exp(log(σ^2)/2) σ=exp(log(σ))=exp(2log(σ)/2)=exp(log(σ2)/2)
变分自编码器的解码器与普通自编码器的解码器相同,整体结构如下所示:

变分自编码器架构

为什么对编码器进行这样小的改变会有所帮助呢?在自编码器中,潜空间并不要求是连续的,即使点 (-2,2) 可以解码为完好的凉鞋图像,也不能保证点 (-2.1,2.1) 有类似的特征。而在变分自编码器中,由于我们从z_mean周围的区域中采样一个随机点,解码器必须确保当对同一邻域中的所有点进行解码时都能产生非常相似的图像,以便确保重构损失相对较小。这一属性确保了即使我们在隐空间中选择了一个解码器从未见过的点,编码器也可以将其解码为完好的图像。

2. 构建VAE编码器

接下来,我们利用 Keras 构建变分自编码器。

2.1 Sampling 层

首先,我们需要创建一种新层,称为 Sampling 层,用于从由 z_meanz_log_var 定义的分布中采样:

# 通过继承 Keras 基础 Layer 类来创建一个新层
class Sampling(layers.Layer):
    def call(self, inputs):
        z_mean, z_log_var = inputs
        batch = tf.shape(z_mean)[0]
        dim = tf.shape(z_mean)[1]
        epsilon = K.random_normal(shape=(batch, dim))
        # 使用重参数化技巧构建一个从由 z_mean 和 z_log_var 参数化的正态分布中采样的样本
        return z_mean + tf.exp(0.5 * z_log_var) * epsilon

子类化 Layer 类
我们可以通过子类化 Layer 类并定义 call 方法在 Keras 中创建新的层,call 方法描述了如何通过该层转换张量。
例如,在变分自编码器中,可以创建一个 Sampling 层,处理从由 z_meanz_log_var 定义的参数化正态分布中对 z 进行采样。子类化 Layer 类可以对一个张量应用不包含在 Keras 内置层类型中的变换。

重参数化技巧
变分自编码并不直接从由 z_meanz_log_var 参数化的正态分布中进行采样,而是从标准正态分布中采样 epsilon,然后调整采样以得到正确的均值和方差,这称为重参数化技巧 (Reparameterization Trick),使用此技术后,梯度可以自由地通过该层进行反向传播。通过将层的所有随机性都包含在变量 epsilon 中,可以证明该层输出相对于其输入的偏导数是确定性的(即与随机的 epsilon 无关),这对于反向传播通过该层是至关重要的。

2.2 编码器

编码器的完整代码如下:

# 编码器
encoder_input = layers.Input(
    shape=(IMAGE_SIZE, IMAGE_SIZE, 1), name="encoder_input"
)
x = layers.Conv2D(32, (3, 3), strides=2, activation="relu", padding="same")(
    encoder_input
)
x = layers.Conv2D(64, (3, 3), strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(128, (3, 3), strides=2, activation="relu", padding="same")(x)
shape_before_flattening = K.int_shape(x)[1:]  

x = layers.Flatten()(x)
# 将 Flatten 层连接到 z_mean 和 z_log_var 层,而不是直接连接到 2D 潜空间中
z_mean = layers.Dense(EMBEDDING_DIM, name="z_mean")(x)
z_log_var = layers.Dense(EMBEDDING_DIM, name="z_log_var")(x)
# Sampling 层从由 z_mean 和 z_log_var 参数定义的正态分布中对潜空间中的点 z 进行采样
z = Sampling()([z_mean, z_log_var])
# 定义编码器,接受一张输入图像,并输出 z_mean、z_log_var 以及从由这些参数定义的正态分布中采样得到的点 z
encoder = models.Model(encoder_input, [z_mean, z_log_var, z], name="encoder")
print(encoder.summary())

编码器架构摘要信息输出如下:

编码器架构

2.3 损失函数

在自编码器中,损失函数只包括经过编码器和解码器传递后的图像与其原始图像之间的重建损失。在变分自编码器中,除了重建损失外,还增加了一个额外的组成部分:KL 散度 (Kullback-Leibler divergence)。
KL 散度是衡量两个概率分布之间差异程度的方法。在 VAE 中,我们希望衡量由参数 z_meanz_log_var 构成的正态分布与标准正态分布之间的差异程度。在这种情况下,可以证明 KL 散度有以下解析解:
k l _ l o s s = − 0.5 ∗ s u m ( 1 + z _ l o g _ v a r − z _ m e a n 2 − e x p ( z _ l o g _ v a r ) ) kl\_loss = -0.5 * sum(1 + z\_log\_var - z\_mean ^ 2 - exp(z\_log\_var)) kl_loss=0.5sum(1+z_log_varz_mean2exp(z_log_var))
用数学符号表示如下:
D K L [ N ( μ , σ ) ∣ ∣ N ( 0 , 1 ) ] = − 1 2 ∑ ( 1 + l o g ( σ 2 ) − μ 2 − σ 2 ) D_{KL}[\mathcal N(\mu, \sigma)||\mathcal N(0,1)]=-\frac 12\sum(1+log(\sigma^2)-\mu^2-\sigma^2) DKL[N(μ,σ)∣∣N(0,1)]=21(1+log(σ2)μ2σ2)
求和是在潜空间的所有维度上进行的。当 z_mean = 0z_log_var = 0 时,kl_loss 会得到最小值 0。当这两个项开始与 0 的差距增大时,kl_loss 也会随之增加。
总之,KL 散度在编码观测样本时,如果 z_meanz_log_var 变量与标准正态分布参数(即 z_mean = 0z_log_var = 0 )出现显著不同,对网络施加的惩罚。
在损失函数中添加 KL 散度后,就有一个明确定义的分布(即标准正态分布),可以用于选择潜空间中的点,从该分布中采样更有可能得到 VAE 已经见过的范围内的点。其次,由于 KL 散度试图将所有编码分布强制趋近于标准正态分布,因此点簇之间形成较大间隙的可能性较小。相反,编码器将尝试以对称的方式高效利用原点周围的空白区域。
VAE 论文中,VAE 的损失函数只是重建损失和 KL 散度损失项之和。我们通过 r_loss_factor 为重建权重添加权重因子,用于平衡 KL 散度损失和重建损失。如果重建损失权重过大,KL 损失将无法产生预期的调节效果,就会遇到与普通自编码器相同的问题。如果 KL 散度项的权重过大,KL 散度损失将占主导地位,重建图像的质量将较差。权重因子是训练 VAE 时需要调整的超参数之一。

2.4 训练变分自编码器

将整个 VAE 模型构建为 Keras Model 类的子类,以便在自定义的 train_step 方法中计算损失函数的 KL 散度项。

class VAE(models.Model):
    def __init__(self, encoder, decoder, **kwargs):
        super(VAE, self).__init__(**kwargs)
        self.encoder = encoder
        self.decoder = decoder
        self.total_loss_tracker = metrics.Mean(name="total_loss")
        self.reconstruction_loss_tracker = metrics.Mean(
            name="reconstruction_loss"
        )
        self.kl_loss_tracker = metrics.Mean(name="kl_loss")

    @property
    def metrics(self):
        return [
            self.total_loss_tracker,
            self.reconstruction_loss_tracker,
            self.kl_loss_tracker,
        ]
    # 变分自编码器前向计算
    def call(self, inputs):
        """Call the model on a particular input."""
        z_mean, z_log_var, z = encoder(inputs)
        reconstruction = decoder(z)
        return z_mean, z_log_var, reconstruction
    # VAE训练过程,包括损失函数的计算
    def train_step(self, data):
        """Step run during training."""
        with tf.GradientTape() as tape:
            z_mean, z_log_var, reconstruction = self(data)
            reconstruction_loss = tf.reduce_mean(
                BETA
                * losses.binary_crossentropy(
                    data, reconstruction
                )
            )
            kl_loss = tf.reduce_mean(
                tf.reduce_sum(
                    -0.5
                    * (1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var)),
                    axis=1,
                )
            )
            total_loss = reconstruction_loss + kl_loss
        grads = tape.gradient(total_loss, self.trainable_weights)
        self.optimizer.apply_gradients(zip(grads, self.trainable_weights))
        self.total_loss_tracker.update_state(total_loss)
        self.reconstruction_loss_tracker.update_state(reconstruction_loss)
        self.kl_loss_tracker.update_state(kl_loss)

        return {m.name: m.result() for m in self.metrics}

    def test_step(self, data):
        """Step run during validation."""
        if isinstance(data, tuple):
            data = data[0]
        z_mean, z_log_var, reconstruction = self(data)
        # 重建损失权重因子 beta 为 500
        reconstruction_loss = tf.reduce_mean(
            BETA
            * losses.binary_crossentropy(data, reconstruction)
        )
        kl_loss = tf.reduce_mean(
            tf.reduce_sum(
                -0.5 * (1 + z_log_var - tf.square(z_mean) - tf.exp(z_log_var)),
                axis=1,
            )
        )
        # 总损失是重建损失和KL散度损失之和
        total_loss = reconstruction_loss + kl_loss

        return {
            "loss": total_loss,
            "reconstruction_loss": reconstruction_loss,
            "kl_loss": kl_loss,
        }

# 创建变分自编码器
vae = VAE(encoder, decoder)
# 编译变分自编码器
optimizer = optimizers.Adam(learning_rate=0.0005)
vae.compile(optimizer=optimizer)
# 模型训练
vae.fit(
    x_train,
    epochs=EPOCHS,
    batch_size=BATCH_SIZE,
    shuffle=True,
    validation_data=(x_test, x_test),
)

梯度记录器 (Gradient Tape)
TensorFlowGradient Tape 用于在模型的前向传播过程中计算操作的梯度。为了使用梯度记录器,需要将需要求导的操作的代码包装在 tf.GradientTape() 上下文中。一旦记录了这些操作,就可以通过调用 tape.gradient() 计算损失函数相对于某些变量的梯度,然后使用优化器利用这些梯度更新网络权重。
这一机制对于计算自定义损失函数的梯度非常有用,也适用于创建自定义的训练流程。

3. 变分自编码器分析

训练好 VAE 后,我们使用编码器对测试集中的图像进行编码,并在潜在空间中绘制 z_mean 值。我们还可以从标准正态分布中采样,生成潜空间中的点,并使用解码器将这些点解码回像素空间,以测试 VAE 的性能。
下图展示了新的潜空间的结构,以及采样点及其解码后的图像。我们可以看到潜空间的组织方式发生了 一些变化。

变分自编码器潜空间

首先,KL 散度损失项确保编码图像的 z_meanz_log_var 值不会偏离标准正态分布太远。其次,由于现在编码器是随机的而不是确定性的,因此,潜空间更加连续,所以不会生成过多不合理的图像,潜在空间现在更加连续。
最后,如下图所示,通过按图像类型对潜在空间中的点进行着色,可以看到没有任何一种类型有倾向性。右图显示了将潜空间转换为 p 值的情况,可以看到每种颜色占据的区域大致相等。需要强调的是,在训练过程中并没有使用标签,VAE 通过学习掌握了各种 Fashion-MNIST 图像的形式,以最小化重建损失。

潜空间

小结

变分自编码器通过在模型中引入随机性,并限制潜空间中的点的分布来解决自编码器存在的问题。只需进行一些微小的调整,就可以将自编码器转换为变分自编码器,从而使其成为真正的生成模型。在本节中,我们介绍了变分自编码器的基本原理,并使用 Keras 实现了一个变分自编码器用于生成 Fashion-MNIST 图像。

系列链接

AIGC实战——生成模型简介
AIGC实战——深度学习 (Deep Learning, DL)
AIGC实战——卷积神经网络(Convolutional Neural Network, CNN)
AIGC实战——自编码器(Autoencoder)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/151334.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

轻量封装WebGPU渲染系统示例<33>- 单精度浮点纹理(源码)

在WebGPU中创建纹理使用纹理很方便,只是js中只有Float32Array而默认不支持Float16Array,所以略微费点事。不过网上的大神多的是,摇摇小手就能获得解决方案。 废话多了容易挨胖揍,看代码。 js中float16单精度float数值转换: // …

[C/C++] 数据结构 链表OJ题:相交链表(寻找两个链表的相交起始结点)

题目描述: 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。 图示两个链表在节点 c1 开始相交: 题目数据 保证 整个链式结构中不存在环。 注意,函数返…

JSP 购物商城系统eclipse定制开发mysql数据库BS模式java编程servlet

一、源码特点 java 购物商城系统是一套完善的web设计系统 系统采用serlvetdaobean 模式开发,对理解JSP java编程开发语言有帮助,系统具有完整的源代码和数据库,系统主要采用B/S模 式开发。开发环境为TOMCAT7.0,eclipse开发,数…

CentOS停更在即,国内厂商该如何应对?KeyarchOS X2Keyarch 迁移体验

一、CentOS 停更危机二、关于浪潮信息KeyarchOS三、浪潮信息 KeyarchOS License 应用迁移实践第一步:迁移前验证第二步:迁移第三步:迁移后验证 四、写在最后 一、CentOS 停更危机 自 1993 年开始,红帽 Linux 已经陪伴开发者们走过…

荧光量子效率积分球检测薄膜需要注意什么

荧光量子效率积分球是一种特殊的积分球,它可以用于测量荧光材料在特定波长下的荧光量子效率。它由一个具有高朗伯特性的漫反射材料制成,具有高达99%的反射率和朗伯特性。荧光量子效率积分球的使用方法包括将样品放置在积分球的样品口中,调整激…

【C++高阶(二)】熟悉STL中的map和set --了解KV模型和pair结构

💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:C从入门到精通⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你学习C   🔝🔝 map和set 1. 前言2. map和set介绍3. pair结构介…

JWT登录认证(1登录)

JwtUtil package com.lin.springboot01.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import java.util.Date; import java.util.Map;public class JwtUtil {private static final String KEY "liner2332";//接受业务数据&#xf…

Python in Visual Studio Code 2023年11月发布

排版:Alan Wang 我们很高兴地宣布 Visual Studio Code 的 Python 和 Jupyter 扩展将于 2023 年 11 月发布! 此版本包括以下公告: 改进了使用 Shift Enter 在终端中运行当前行弃用内置 linting 和格式设置功能对 Python linting 扩展的改进重…

linux安装nginx并配置服务的详细步骤

文章目录 依赖安装安装gcc环境安装 pcre安装zlib安装openssl 安装Nginx在nginx官网下载安装包将安装包上传linux解压文件手动创建用户和用户组编译目录编译源码并安装启动查看进程 设置nginx服务并开机自启 依赖安装 nginx安装前需要一些依赖,如果已经安装了则忽略…

大数据Doris(二十三):取消导入与其他导入案例参考

文章目录 取消导入与其他导入案例参考 一、取消导入

Django(七、模型层)

文章目录 模型层模型层前期准备使用django ORM要注意 代码演示:切换MySQL数据库如何查看django ORM 底层原理? 单表操作模型层之ORM常见关键字基础的增删改查常用的关键字 常见的十几种查询基于双下滑线的查询 模型层 模型层前期准备 使用django ORM要…

【Qt之QWizardPage】使用

介绍 QWizardPage类是向导页面的基类。 QWizard表示一个向导。每个页面都是一个QWizardPage。当创建自己的向导时,可以直接使用QWizardPage,也可以子类化它以获得更多控制。 页面具有以下属性,由QWizard呈现:a title,…

【数据结构】别跟我讲你不会冒泡排序

👦个人主页:Weraphael ✍🏻作者简介:目前正在学习c和算法 ✈️专栏:数据结构 🐋 希望大家多多支持,咱一起进步!😁 如果文章有啥瑕疵 希望大佬指点一二 如果文章对你有帮助…

【Python 千题 —— 基础篇】列表的最大值与最小值(for 循环版)

题目描述 题目描述 输出列表的最大值与最小值。题中有一个包含数字的列表 [11, 39, 100, 48, 392, 10, 9],使用 for 循环输出这个列表的最大值与最小值。 输入描述 无输入。 输出描述 输出列表的最大值与最小值。 示例 示例 ① 输出: 列表的最…

如何在Ubuntu 23.10部署KVM并创建虚拟机?

正文共:1114 字 21 图,预估阅读时间:2 分钟 我们之前对OpenStack醉过一次简单介绍(什么是OpenStack?),OpenStack本身是一个云管理平台,它本身并不提供虚拟化功能,而是依赖…

UE基础必学系列:UMG

一、教程: 官方教程: 官方文档: 创建和显示UI 二、理解知识点: 2.1 RemoveFromParent 从视口中删除,但仍保留在内存中,并且变量仍然存在有效的 2.2 3D交互组件测试

webstorm/idea配置leetcode刷题

File -> settings -> Plugins -> 搜索leetcode 安装插件(截图显示我已经安装过了),安装完成后点击OK操作,在编辑器四个边角就会出现一个leetcode的插件 File -> settings -> Tools-> Leetcode plugin 点击…

表单校验wed第十九章

常见的表单验证 一。表单选择器 属性过滤选择器 :selected 选中所有的下拉元素 :checked:选项元素 :disabled 不可用元素 :enable 所有可用元素 二。字符串演示 1.判断非空 isNaN(j) :判断是否为数字 2.表…

C语言—字符串连接、拷贝和比较函数

strcpy_s&#xff1a;拷贝整个字符串 #include <stdio.h> #include <string.h>int main() {char str1[] "first stringiiii";char str2[] "second string";char str3[100];strcpy_s(str1, sizeof(str1) / sizeof(str1[0]), str2);strcpy_s(…

docker安装MongoDB数据库,并且进行密码配置

很美的一首小诗> 我在外面流浪&#xff0c;回来时 故乡瘦了一圈—— 墩子叔走了&#xff0c;门前的池水 干了一半。 屋后驼背的柳树 头发散落了一地&#xff0c; 老房子蹲在坟边&#xff0c;屋顶的白云 仍在风中奔跑。 安装配置 要在Docker中安装MongoDB并启用远程连接&…
最新文章