☰
MATLAB实现生成对抗网络:从网络搭建到训练调参全攻略
2026/10/11 6:04:22 网站建设 项目流程

简介:面向深度学习初学者的GANs生成对抗网络MATLAB实践资源包。资源以生成器与判别器对抗训练为主线,涵盖网络结构搭建、损失函数选择、Adam/SGD优化器配置、两步梯度下降训练流程,以及模式崩溃、收敛不稳定等问题的常见处理思路,适合希望通过代码与文档结合方式快速上手GANs的研究者和学生。包内共有544个文件,整体约177.21MB。包含大量PNG/JPG图片,以及ZIP压缩包、Python脚本、Markdown、PDF、HTML/CSS和TXT等文件,覆盖代码、说明文档与可视化素材;另有无扩展名文件,可能对应生成数据或中间结果。目前已有269人学习下载。对照MATLAB深度学习工具箱,可完整体验从随机噪声到图像生成的实现过程,并通过示例了解Wasserstein距离、渐进式GAN等改进方向。压缩包内素材组织较为零散,但代码与文档互为补充,适合边看边调参、逐步拆解GAN训练细节的学习方式。

1. 生成对抗网络的MATLAB代码资源:先搞清楚这类资源能解决什么问题

生成对抗网络(GANs)的MATLAB代码资料,网上搜“gans matlab”“matlab 生成对抗网络”能翻出来不少,但真正下下来就能跑通的很少。这类资源包通常默认你已经装好了Deep Learning Toolbox、手上有可用的GPU、数据路径也对,偏偏这几条在文档里就写一行带过。结果就是新手满怀期待地打开主脚本,第一轮训练就翻车,损失值变NaN、生成图千篇一律,又回头去折腾另一份代码。这篇文章按我实际调过的套路,把这类资源包的落地路径拆开讲全:判别器和生成器怎么搭、训练循环的参数怎么设、损失函数怎么选、出了问题从哪查起。适合想在MATLAB里入手GANs的本科生、研究生和图像方向的工程师;完全没碰过MATLAB的初学者读起来会吃力一点。

2. 用MATLAB搭GANs的判别器和生成器:结构、输出层和损失函数的选型依据

2.1 判别器:卷积下采样与单节点线性输出

判别器在GAN里扮演裁判,输入一张图,输出一个“真或假”的分数。放到MATLAB里,最常用的结构就是几个卷积下采样层接一个全连接输出。我见过的大量GAN图片生成示例里,判别器长得都差不多:前两层用stride为2的卷积替代池化,逐步把28×28的图像压到7×7,最后展平接一个输出节点。代码一般是这个形态:

% 判别器网络:输入 28x28 单通道灰度图,输出 1 个分数节点 dlnetD = dlnetwork([ imageInputLayer([28 28 1], 'Normalization', 'none', 'Name', 'img_in') convolution2dLayer(5, 32, 'Stride', 2, 'Padding', 'same', 'Name', 'conv1') leakyReluLayer(0.2, 'Name', 'act1') convolution2dLayer(5, 64, 'Stride', 2, 'Padding', 'same', 'Name', 'conv2') leakyReluLayer(0.2, 'Name', 'act2') fullyConnectedLayer(1, 'Name', 'score_out') ]);

这里有两个选型理由要说明白。第一,下采样用stride 2而不是maxpooling层,因为步进卷积在反向传播时梯度路径更平滑,判别器更新更稳,训练中不太容易出现某层梯度直接断掉的情况。第二,最后一层没有接sigmoid,输出的是logits,真正的0到1概率在损失函数里用sigmoid函数现算。如果你在判别器末端接了sigmoid,又在损失里再算一次log(sigmoid(...)),数值上容易产生log(0)的NaN,这是很多资源包代码里的经典坑。输入层把Normalization设为none是因为GAN训练时图像像素已经被缩放到[-1,1],不需要再做z-score标准化。

关于批标准化,我一般默认不在判别器里加。判别器本身是个二分类器,加BN会让单张图的输出依赖同一个batch里的其他样本,小batch训练时统计量抖动很大,判别器的分数会忽高忽低。如果判别器收敛太慢,优先调学习率而不是加BN;如果图像分辨率高、判别器有四五层卷积,再加BN也不迟。

2.2 生成器:转置卷积上采样与tanh输出层

生成器拿一个低维噪声向量,输出一张图片。以MNIST的28×28灰度图为例,常见路线是把100维噪声先投影成7×7×64的特征图,再用两次转置卷积放大到28×28。整个过程相当于把判别器倒过来。参考结构如下:

% 生成器:输入 100 维噪声,输出 28x28x1 图像 numLatentInputs = 100; dlnetG = dlnetwork(... featureInputLayer(numLatentInputs, 'Normalization', 'none', 'Name', 'z_in') ... fullyConnectedLayer(7*7*64, 'Name', 'fc_proj') ... reluLayer('Name', 'relu_proj') ... functionLayer(@(x) reshape(x, 7, 7, 64, []), 'Name', 'reshape_proj') ... transposedConv2dLayer(5, 32, 'Stride', 2, 'Cropping', 'same', 'Name', 'tconv1') ... reluLayer('Name', 'relu_tconv1') ... transposedConv2dLayer(5, 1, 'Stride', 2, 'Cropping', 'same', 'Name', 'tconv2') ... tanhLayer('Name', 'tanh_out'));

代码里最值得注意的有三个点。一是reshape那一步:全连接层输出的是一维向量,要把它整理成7×7×64的特征图才能进转置卷积。不同版本的MATLAB处理方式不一样,有的用functionLayer,有的写成projectAndReshapeLayer自定义层。如果运行时报“functionLayer未被识别”,不要死磕这一层,换成自定义层或者直接按你MATLAB版本里GAN官方示例的写法来。

二是转置卷积的参数:两次stride 2的转置卷积把7×7放大到14×14,再到28×28。Cropping设为same是为了保持尺寸对齐。三是输出层用tanh,把生成图像的像素压到[-1,1],对应训练数据预处理时的范围。很多资源包在数据加载部分把像素除以255变成了[0,1],又在这里用tanh输出[-1,1],两头不配对,生成图普遍偏灰,这是最常见的生成质量差的原因之一。

2.3 生成对抗网络损失函数怎么选:从BCE到WGAN-GP的替换边界

生成对抗网络的损失函数是整个训练的灵魂。标准GAN用的是二元交叉熵,判别器想办法把真实图判成1、假图判成0,生成器想办法让假图被判别器判成1。在MATLAB自定义训练循环里,我一般不用内置的交叉熵分类层,而是用dlarray的sigmoid运算直接算:

function [lossG, lossD] = ganLosses(dReal, dFake) epsVal = 1e-8; lossD = mean(-log(sigmoid(dReal) + epsVal)) + ... mean(-log(1 - sigmoid(dFake) + epsVal)); lossG = mean(-log(sigmoid(dFake) + epsVal)); end

这里加epsVal是防止sigmoid输出恰好为0或1时log计算出NaN。把判别器输出先过sigmoid再取负对数,就等价于二分类交叉熵。真实图和假图的损失分开算,然后加起来就是判别器的总损失;生成器的损失只看假图那一项。这套逻辑是所有GAN变体的地基。

那什么时候该换成WGAN-GP呢?最常见的一个信号是模式崩塌——生成图千篇一律。标准GAN用的是JS散度的近似,当真实分布和生成分布重叠太少时,梯度会消失,生成器学不动。WGAN-GP把损失改成判别器输出分数的直接差值,再用梯度惩罚保证判别器不震荡,训练稳定性明显好一截。核心损失换成这样:

% WGAN-GP 的判别器损失和生成器损失(示意) lossD = mean(dFake) - mean(dReal) + lambda * gradPenalty; lossG = -mean(dFake);

但换损失不是只改两行公式就完事。判别器最后一层要保持线性输出,不能加sigmoid;训练轮数通常要翻倍;生成器学习率要相应调低,否则重建的损失曲线会抖得厉害。下面这张表是我在MATLAB里常用的选择依据:

损失类型适用场景主要优点注意点
标准GAN+BCEMNIST等简单数据集、快速验证实现简单,收敛快容易模式崩塌,对初始化敏感
LSGAN生成图像偏模糊时损失曲面更平滑,缓解梯度消失对异常值敏感,建议配合梯度裁剪
WGAN-GP彩色图、高分辨率、训练不稳稳定性最好,Wasserstein距离更有意义训练慢,梯度惩罚系数lambda通常取10

如果你拿到的资源包只提供标准GAN的损失版本,而你想换WGAN-GP,我的建议是先不要整体重写,只替换损失函数和判别器输出层,其余网络结构原样保留。改完以后先跑500个iteration看趋势,别一上来就调所有超参。

3. 把GAN代码资源跑起来的实操:环境检查、数据准备与训练循环的参数设置

3.1 环境检查:MATLAB版本、工具箱和GPU

开跑之前先把环境摸一遍,能省掉后面一半的排查时间。很多人在这一步翻车是因为拿到手的资源包代码用了较新的网络层API,而MATLAB版本太老。我见过最典型的情况是R2019a打开含dlnetwork的脚本直接报未定义函数。dlnetwork在R2019b才引入,dlgradient和dlfeval的完整支持要到R2020a以后才稳定。如果你下载的资源包里出现了functionLayer这种新一点的层,建议直接用R2021a之后的版本,省得和层定义较劲。

用下面这段代码做快速体检:

% 检查关键工具箱和GPU ver('deep'); % Deep Learning Toolbox ver('parallel'); % Parallel Computing Toolbox,GPU训练依赖它 gpuDeviceCount % 返回可用的GPU数量,0表示只有CPU

如果gpuDeviceCount返回0,还可以看是不是驱动问题。MATLAB的GPU支持列表里,NVIDIA的显卡兼容性最好,建议优先确认显卡驱动和CUDA版本是否匹配。很多时候不是代码问题,而是Toolbox没装全,Deep Learning Toolbox和Parallel Computing Toolbox两者都要装,只装前者跑GPU会报“找不到GPU设备”之类的错。

3.2 数据准备:MNIST到自定义图像数据的输入管线

数据准备是另一个容易踩坑的地方。以MNIST为例,常见资源包会自带mat文件,直接load就行。但像素范围的处理很容易出偏差:原始数据一般是[0,1]或[0,255],而生成器输出是tanh的范围[-1,1],两边必须对齐。我在代码里通常先把图像从[0,1]映射到[-1,1]:

% 加载MNIST并调整像素范围 mnistData = load('mnist.mat'); X = mnistData.XTrain; X = single(X); % 转成single类型,double会让显存占用直接翻倍 X = (X - 0.5) / 0.5; % [0,1] 映射到 [-1,1] X = dlarray(X, 'SSCB'); % 告诉dlarray:S=空间, C=通道, B=批

dlarray的标签顺序容易弄反,‘SSCB’表示空间-空间-通道-批,这是图像数据最常用的布局。如果你从imageDatastore读自己的数据集,记得在readall之后也统一做一次single转换和范围缩放。自定义数据还要强制resize到固定尺寸,比如64×64或128×128,判别器的输入层尺寸定了就不能随意改。很多资源包在文档里写“支持任意图像”,实际上网络结构写死了一个尺寸,换数据集就崩,你需要自己确认输入层到底是多少。

3.3 训练循环:dlnetwork、dlfeval和adamupdate的最小组合

GAN训练循环和普通分类网络的训练循环差别很大,因为它有两个网络、两组梯度、两组优化器状态。MATLAB里最简洁的写法是用dlfeval包住一个梯度函数,让自动微分同时算生成器和判别器的梯度,再用adamupdate分别更新两组参数。参考代码:

% 定义梯度计算函数 function [gradG, gradD, lossG, lossD] = modelGradients(dlnetG, dlnetD, Z, XReal) XFake = forward(dlnetG, Z); % 生成假样本 dReal = forward(dlnetD, XReal); % 判别器对真实图打分 dFake = forward(dlnetD, XFake); % 判别器对假图打分 epsVal = 1e-8; lossD = mean(-log(sigmoid(dReal)+epsVal)) + ... mean(-log(1-sigmoid(dFake)+epsVal)); lossG = mean(-log(sigmoid(dFake)+epsVal)); gradG = dlgradient(lossG, dlnetG.Learnables); gradD = dlgradient(lossD, dlnetD.Learnables); end % 主训练循环 for iter = 1:totalIterations idx = randi(size(X, 4), miniBatchSize, 1); % 随机抽一批真实图 XReal = X(:, :, :, idx); Z = dlarray(randn([numLatentInputs, miniBatchSize], 'single'), 'CB'); [gradG, gradD, lossG, lossD] = dlfeval(... @modelGradients, dlnetG, dlnetD, Z, XReal); [dlnetG, stateG] = adamupdate(dlnetG, gradG, stateG, lrG); [dlnetD, stateD] = adamupdate(dlnetD, gradD, stateD, lrD); if mod(iter, 100) == 0 fprintf('Iter %d, G loss %.3f, D loss %.3f\n', ... iter, extractdata(lossG), extractdata(lossD)); end end

一大群人以为dlfeval只是“调用函数”,其实它和dlgradient一起构成了自动微分的作用域。在modelGradients里调用forward得到的中间结果会被记录在计算图里,dlgradient沿着这个图反向求出对Learnables的梯度。所以千万不要试图在循环外用dlgradient去算梯度,那是算不出来的。

这里有个Excel的stateG和stateD,是Adam优化器的动量状态。第一次迭代时stateG设为空数组[],adamupdate会自动初始化。如果你用的是SGD或RMSProp,就没有这个状态参数,但GAN训练几乎不会用SGD,Adam是默认选择,因为模型复杂度和非凸性都要求更稳的一阶优化方法。

3.4 三个必调参数:学习率、batch size和噪声维度

GAN训练参数有三个是最先要调的:学习率、batch size和噪声维度。这三个参数相互作用,某个参数卡住不动时,整体训练都会表现出明显异常。我常用的起始值如下:

参数推荐范围起始值调参方向
生成器学习率1e-4 到 2e-31e-3生成图模糊就适当下调
判别器学习率生成器的0.5倍到1倍2e-4判别器损失掉到0就近下调
batch size16到12864显存爆了就降到32或16
噪声维度64到128100模式崩塌时适当加大

判别器学习率比生成器低是GAN训练里的常见惯例。先让判别器显得有些“迟钝”,生成器才有力气去追赶。如果两边学习率一样大,判别器往往两三个epoch就把真实图和假图分得清清楚楚,生成器梯度随之消失。batch size则直接影响梯度估计的稳定性,也和批标准化层挂钩:如果你在判别器里加了BN,batch size长期小于32,BN的统计量会抖得没法看。噪声维度不是越大越好,100维是很多官方示例的默认选择,加到200维很少带来明显提升,反而让生成器需要更多迭代才能把高维噪声映射到有意义的图像空间。

4. GANs训练中的常见问题排查:损失异常、模式崩塌、显存不足从哪查起

4.1 现象:判别器损失掉到0附近,生成器损失原地不动

训练几百步之后,判别器对真实图已经能给出非常高的分数,对假图能给出非常低的分数,于是dReal的损失趋近于0,dFake的损失也趋近于0,生成器的梯度几乎消失,损失曲线变成一条平坦的线。这个过程很多人都遇到过,本质原因是判别器学得太快,把真实分布和生成分布分得太开,导致生成器得到的梯度信号太小。

我一般先降低判别器学习率,把lrD从2e-4降到1e-4,同时给判别器的卷积层加dropout。在MATLAB里就是在卷积层后面插一个dropoutLayer(0.3)。dropout会让判别器抽取特征的确定性降低,不至于每次都给绝对判定的分数。还有一种做法是减少判别器的层数或通道数,比如把两层卷积各64通道改成各32通道,让判别器没有足够的容量去碾压生成器。改完之后不要立刻看最终效果,而是观察接下来200-500步里dFake的损失有没有重新浮动起来,如果还是一潭死水,再去动生成器的结构。

4.2 现象:生成图片千篇一律,换随机种子也不管用

模式崩塌是GAN训练最出名的坑。表现是:生成器无论拿到什么噪声向量,生成的图像都像同一个样本,可能是同一个数字,同一个偏旁,甚至同一块纯色区域。原因在于生成器找到了一条“安全路径”:只输出判别器最难辨认的某个样本,就能稳定骗过判别器,于是它放弃了学习真实分布。

排查时,我习惯先固定几个噪声向量,在训练过程中定期生成一组图像并保存下来。如果中途某次迭代开始,这几张图开始收敛到同一张脸,就说明模式崩塌开始了。处理手段按成本从低到高排序:把噪声维度从100加大到128,给生成器一点更多的变化空间;把生成器学习率降低到原来的1/3;把标准GAN损失换成WGAN-GP,这招对模式崩塌往往有奇效,但代价是训练时间变长。还有一个容易忽略的原因是数据没打乱,或者数据里某些类别的图像特别多,生成器学了一个“取巧”的分布。检查训练数据的类别均衡度,也是一种必要的做法。

4.3 现象:损失曲线出现NaN,训练直接中断

NaN问题几乎每个训练GAN的人都中过招。常见原因有三个:损失函数里log的输入变成0;学习率太大导致梯度爆炸;输入数据本身包含NaN或Inf。判别器输出的logits绝对值很大时,sigmoid会饱和到接近1或0,log函数在那里直接算出无穷值。

排查顺序也很固定。第一步,给sigmoid的输入加上eps保护,也就是在前面代码里出现的epsVal=1e-8,并在data里检查一遍是否有NaN值。第二步,降低学习率,尤其是生成器,因为生成器参数的爆炸会让下一轮生成图像的像素值飘到异常大。第三步,加上梯度裁剪,把梯度过大的分量压下去。在MATLAB里可以对dlgradient的返回值做一个全局裁剪:

% 梯度裁剪:限制总梯度的最大范数 grads = [gradG; gradD]; totalNorm = sqrt(sum(grads .^ 2)); maxNorm = 10; if totalNorm > maxNorm scale = maxNorm / totalNorm; grads = grads * scale; end

这只是示意写法,实际用时需要按网络和层拆分再套回去。如果NaN还是在迭代几百步后出现,还要考虑是不是batch里出现了一批特别难判别的图像,导致判别器输出极端值。把batch size调大一点能缓解这种单点极端情况。

4.4 现象:显存不足或CPU训练慢到怀疑人生

很多资源包默认你有一块8GB以上的GPU,但实际使用的人可能只有一台普通办公电脑。显存不足的直接表现是MATLAB报“Out of memory during computation”或“CUDA out of memory”。大部分时候是batch size或图像尺寸起的锅。一个64×64的彩色图,batch size取128,判别器前向和反向各存一份特征图,显存占用轻松超过4GB。

我的处置是先降batch size到16,把图像尺寸压到32×32,再把训练数据全部转成single类型。这三招几乎总能应付显存不足。如果还是不够,就削减判别器通道数,从64剪到32,生成器也同步改。CPU训练则更麻烦,dlnetwork在CPU上也能跑,但速度比GPU慢一个数量级。我的建议是CPU训练时只跑小规模数据集的验证,不要直接上完整训练计划;如果资源包里默认250个epoch,CPU上可以先只跑10个epoch确认梯度方向没有大问题,再找GPU环境继续。

5. 怎么判断GAN练好了没有:损失曲线、样本多样性和插值视频的验证套路

5.1 损失曲线不看下降,看的是对抗平衡

GAN训练里最误导人的一个习惯是把损失曲线当成普通分类任务的Loss来读:看到下降就安心,看到上升就焦虑。实际上,GAN的生成器损失和判别器损失是一对博弈对象,生成器损失上升不一定代表训练变差,可能是判别器暂时占了上风,而下一步生成器会追上来。我训练生成对抗网络时打印四个量:G loss、D loss、D对真实图的平均分、D对假图的平均分。不是只打印两个总损失。

判断训练是否正常的核心是看判别器的分数:D对真实图的分数应该长期高于对假图的分数,但差距不能过大。如果两者差距越来越大,说明判别器很容易分辨真假,生成器跟不上;如果两者一直交织震荡,反而是正常状态。生成器损失的值本身没有绝对好坏,震荡在0.4到0.9之间都是常见的。我习惯每500步把损失值和当前生成的样本图一起存下来,只看曲线容易产生“好像练好了”的错觉,样本图才是最终裁判。

5.2 用图像方差粗测多样性:不依赖FID的快速检查

FID是评估生成多样性的标准指标,但在MATLAB里算FID要额外准备Inception网络的权重,对很多环境来说太重了。一个朴素但有效的替代方法是统计生成样本的像素级方差。把一批生成图像拉平,计算每个像素位置上的标准差,再取平均值。如果这个平均值非常低,接近0,说明生成图像几乎一模一样,模式崩塌的苗头出现了。代码片断:

% 生成一批假图像,并统计像素级标准差 Z = dlarray(randn([numLatentInputs, 128], 'single'), 'CB'); fakeBatch = extractdata(forward(dlnetG, Z)); stdPerPixel = std(reshape(fakeBatch, [], size(fakeBatch, 4)), 0, 2); avgDiversity = mean(stdPerPixel(:)); fprintf('像素级标准差均值: %.4f\n', avgDiversity);

这个指标不能替代FID,但能快速发现明显异常。如果你看到MNIST生成样本的像素标准差从0.3逐渐跌到0.05,那基本可以断定生成器正在偷懒。配合观察样本的方式,可以做到每10分钟同步校验一次生成器状态。

5.3 噪声插值视频:验证生成器学到了连续性

最后一个验证技巧是噪声插值。取两个不同噪声向量z1和z2,让它们线性插值过渡,同时生成每一帧的图片并合成视频。如果生成器真正学到了数据流形,那么插值过程中的画面应该是连续的:数字从“1”慢慢变成“7”,背景逐渐变化。如果画面是跳跃的、撕裂的、突然从一个人脸跳到另一个人脸,说明生成器只是在记忆样本,没有学到平滑的潜在空间。MATLAB里合成视频的写法很简单:

% 生成噪声插值视频,验证潜在空间的连续性 z1 = dlarray(randn([numLatentInputs, 1], 'single'), 'CB'); z2 = dlarray(randn([numLatentInputs, 1], 'single'), 'CB'); v = VideoWriter('gan_interp.mp4', 'MPEG-4'); open(v); for t = 0:0.02:1 zt = z1 * (1 - t) + z2 * t; img = extractdata(forward(dlnetG, zt)); img = (img + 1) / 2; % 从 [-1,1] 映射回 [0,1] img = squeeze(img); % 去掉单通道和单样本维度 writeVideo(v, im2uint8(img)); end close(v);

这个视频会告诉你很多损失曲线说不清的信息。如果插值过程中画面平滑但变化幅度极小,说明生成器保守,输出接近均值;如果画面剧烈跳动,说明潜在空间被切成了互不相连的小区域。我一般会在训练的第三个epoch之后开始做这个测试,每500步生成一小段视频,可以直观看到生成器从“模糊团块”到“清晰数字”的演变。这个做法尤其适合需要向导师或同事展示训练效果的场景,比一张静止的生成样本图更有说服力。

6. 中途停训不翻车:checkpoint保存与恢复训练的代码套路

GAN训练动辄几个小时,中途停电、MATLAB崩溃、或者临时要开会关电脑,都是很现实的风险。更别提训练到一半发现学习率设太小,生成器几乎没变化,此时只想改完参数从头继续。很多人没有写checkpoint的习惯,我早期也是这样,直到一次训练到第4000个iteration,电脑重启,所有进度归零,才明白“训练前先写好保存逻辑”比调参更重要。

我现在的习惯是每隔N次迭代保存一次完整状态,包括两个网络的可学习参数、Adam优化器的stateG和stateD、当前迭代数,以及当前的学习率。以1000步为例,保存代码如下:

if mod(iter, 1000) == 0 filename = sprintf('gan_ckpt_iter%d.mat', iter); save(filename, 'dlnetG', 'dlnetD', 'stateG', 'stateD', ... 'iter', 'lrG', 'lrD'); end

恢复的时候用load把变量全部读回来,从iter+1继续跑:

ckpt = load('gan_ckpt_iter5000.mat'); dlnetG = ckpt.dlnetG; dlnetD = ckpt.dlnetD; stateG = ckpt.stateG; stateD = ckpt.stateD; % 学习率可能也要恢复,如果你动过它 lrG = ckpt.lrG; lrD = ckpt.lrD; for iter = ckpt.iter + 1 : totalIterations % 原有训练代码照常执行 end

这里有两个细节值得记住。第一,stateG和stateD必须保存,它们是Adam动量的内部状态。如果只保存网络参数,恢复后相当于Adam重新从零开始,前几百步的梯度方向会和正常训练明显出入,生成的图像质量会有短暂回退。第二,保存频率不能太低。每1000步保存一次,文件占用的磁盘空间可以忽略;每10000步保存一次,万一中途崩了,最多丢掉一万步的进度。如果只想保留一个最佳版本,可以每次保存前覆盖同一个文件名,写成gan_ckpt_latest.mat,占一份空间,省心。

这个做checkpoint的习惯我现在一直带着,哪怕只跑两千步的快速验证也会顺手写上保存逻辑。与其赌环境稳定,不如给自己留好后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询