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 = inputsbatch = 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 = encoderself.decoder = decoderself.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")@propertydef 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_lossgrads = 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 为 500reconstruction_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_lossreturn {"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.mzph.cn/news/144287.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

React-Router源码分析-History库

history源码 history 在 v5 之前使用单独的包, v6 之后再 router 包中单独实现。 history源码 Action 路由切换的动作类型,包含三种类型: POPREPLACEPUSH Action 枚举: export enum Action {Pop "POP",Push &quo…

OCR转换技巧:如何避免图片转Word时出现多余的换行?

在将图片中的文字识别转换为Word文档时,我们很多时候时会遇到识别内容的一个自然段还没结束就换行的问题,这些就是我们常说的多余换行的问题。为什么会产生这个问题呢?主要是由于OCR返回的识别结果是按图片上的文字换行而换行,而不…

算法通关村第十一关|白银|位运算高频算法题【持续更新】

1.位移 1.1 位1的个数 原题:力扣191. 挨个判断是不是 1 的话需要对整个长度进行遍历,但是采用技巧可以只寻找为 1 的位,然后将其数量记录下来。 public int hammingWeight(int n) {int count 0;while (n ! 0) {n n & (n - 1);count…

高并发架构设计(三大利器:缓存、限流和降级)

引言 高并发背景 互联网行业迅速发展,用户量剧增,系统面临巨大的并发请求压力。 软件系统有三个追求:高性能、高并发、高可用,俗称三高。三者既有区别也有联系,门门道道很多,全面讨论需要三天三夜&#…

web3资讯及远程工作

各位如果想了解区块链相关的消息可以通过如下网址了解,里面还会有相关职位招聘(包括远程工作),还可以在里面进行发帖,进入即可获得1000积分,后期可以兑换一些礼品Cryptosquare

如何检查 Docker 和 Kubernetes 是否可以访问外部网络,特别是用于拉取镜像的仓库?

要检查 Docker 和 Kubernetes 是否可以访问外部网络,尤其是用于拉取容器镜像的仓库,您可以按照以下步骤进行: 1. 检查节点的网络连接 首先,您需要确保 Kubernetes 节点能够访问外部网络。这可以通过在节点上执行 ping 命令来测试…

git 简单入门

git init touch test.txt git add test.txt git commit -m 初始化 仓库 git log //查找日志 git checkout -b dev //创建并切换dev分支 git branch // 查找分支 此时有master 和 dev分支, 此时在dev分支 dev分支也有test.txt文件 vim test.txt //写入dev …

2023 年 数维杯(B题)国际大学生数学建模挑战赛 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时,你是否曾经感到茫然无措?作为2021年美国大学生数学建模比赛的O奖得主,我为大家提供了一套优秀的解题思路,让你轻松应对各种难题。 让我们来看看数维杯(B题)! …

LeetCode617. Merge Two Binary Trees

文章目录 一、题目二、题解 一、题目 You are given two binary trees root1 and root2. Imagine that when you put one of them to cover the other, some nodes of the two trees are overlapped while the others are not. You need to merge the two trees into a new b…

一文看懂Spark中Cache和CheckPoint的区别

目录 循循渐进理解使用Cache或者PersistCheckPoint缓存和CheckPoint的区别 循循渐进理解 wc.txt数据 hello java spark hadoop flume kafka hbase kafka flume hadoop看下面代码会打印多少条-------------------------(RDD2) import org.apache.spark.rdd.RDD import org.ap…

PHP调用调用API接口的方法及实现

随着互联网、云计算和大数据时代的到来,越来越多的应用程序需要调用第三方的API接口来获取数据,实现数据互通和协同工作。PHP作为一种常用的服务器端语言,也可以通过调用API接口来实现不同系统的数据交互和整合。本文将介绍PHP调用电商API接口…

Jmeter- Beanshell语法和常用内置对象(网络整理)

在利用jmeter进行接口测试或者性能测试的时候,我们需要处理一些复杂的请求,此时就需要利用beanshell脚本了,BeanShell是一种完全符合Java语法规范的脚本语言,并且又拥有自己的一些语法和方法,所以它和java是可以无缝衔接的。beans…

RK3588平台开发系列讲解(摄像头篇)USB摄像头驱动分析

🚀返回专栏总目录 文章目录 一. USB摄像头基本知识1.1 内部逻辑结构1.2 描述符实例解析二. UVC驱动框架2.1、设备枚举过程2.2、数据传输过程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 USB摄像头驱动位于 drivers\media\usb\uvc\uvc_driver.c ,我们本篇重点看下…

MacOS设置JAVA_HOME环境变量

首先先查看一下,系统当前使用的java是谁,可以使用/usr/libexec/java_home命令 % /usr/libexec/java_home /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home检查一下这个路径下的文件,发现这是一个jre的目录。加上-V参数看…

Redis篇---第二篇

系列文章目录 文章目录 系列文章目录一、为什么 使用 Redis 而不是用 Memcache 呢?二、为什么 Redis 单线程模型效率也能那么高?三、说说 Redis 的线程模型一、为什么 使用 Redis 而不是用 Memcache 呢? 这时候肯定想到的就是做一个 Memcache 与 Redis 区别。 Redis 和 Mem…

移动机器人路径规划(二)--- 图搜索基础,Dijkstra,A*,JPS

目录 1 图搜索基础 1.1 机器人规划的配置空间 Configuration Space 1.2 图搜索算法的基本概念 1.3 启发式的搜索算法 Heuristic search 2 A* Dijkstra算法 2.1 Dijkstra算法 2.2 A*&&Weighted A*算法 2.3 A* 算法的工程实践中的应用 3 JPS 1 图搜索基础 1.1…

AD教程 (十六)常用PCB封装的直接调用

AD教程 (十六)常用PCB封装的直接调用 打开已经做好的PCB文件 点击设计,生成PCB库,会自动把PCB里所用到的所有封装,全部自动生成 CtrlA 将所有元器件的封装全部选中(或者只选中所需要的)&#x…

TikTok与心灵成长:娱乐与启发并重

社交媒体已成为我们生活的一部分,其中TikTok以其短视频内容和创新性而闻名。然而,TikTok不仅仅是一个娱乐平台,它还具有潜力成为心灵成长的有力工具。本文将探讨TikTok如何在娱乐与启发之间取得平衡,以促进心灵成长和积极影响。 娱…

利用curl测试WSS连接的建立

记录下: curl -vvv --include --no-buffer \ --header "Connection: Upgrade" \ --header "Upgrade: websocket" \ --header "Host: transcribestreaming.us-east-1.amazonaws.com:8443" \ --header "Origin: http://localhost…

IntelliJ IDEA启动一个普通的java web项目的配置

原创/朱季谦 这是我很久以前刚开始用IntelliJ IDEA时记录的笔记,应该是五年前的一篇笔记了。正好赶上最近离职了,可以有比较多的时间把以前的记录整理一下,可以让刚接触到IntelliJ IDEA的童鞋学习如何在IntelliJ IDEA引入一个单机版的jar形式…