简介:这份资源是DCGAN在TensorFlow框架下的完整实现,面向希望将生成对抗网络用于图像增强与扩充的开发者与研究者,尤其适合医学影像、人脸识别等小样本场景。代码已跑通,替换data文件夹中的图像数据集即可直接使用,对X射线图像增强的尝试也提供了可借鉴的思路。压缩包共235个文件,约164.07MB,包含6个py脚本与2组checkpoint模型文件,另有162张png、9张jpg及gif、mp4等图像与演示素材,以及js、css、html等前端展示文件和xml配置,结构完整便于复现与二次开发。目前已有1463人学习下载,说明该实现具备一定参考价值。读者可获得可运行的DCGAN训练与生成流程、预训练模型权重、数据组织方式及前端可视化页面,既能快速验证生成效果,也能在此基础上替换数据集开展自己的增强实验,遇到问题还可与作者交流排错。
1. DCGAN 做数据增强:为什么它比旋转裁剪更值得投入
手里有一批标注数据,几百张到几千张,类别不均衡,少数类样本少得可怜。常规做法是旋转、翻转、裁剪、加噪声,但这些几何变换本质上没有引入新信息,模型看多了照样过拟合。DCGAN 这类生成对抗网络的价值在于:它学的是数据分布本身,能凭空"造"出没见过但统计特征一致的样本。标题里这个 DCGAN-tensorflow-master 方案,核心就是一套能跑通的深度卷积生成对抗网络实现,替换数据集目录就能直接用于扩充训练集。它适合谁?适合手头有图像分类或检测任务、标注成本高、少数类样本不足的从业者。不适合谁?不适合数据量已经上万且分布均衡的场景,也不适合对生成样本真实性要求极高的医疗影像等强监管领域——生成图只能做增强,不能当真实标注用。这一章先把"为什么值得做"讲清楚,后面几章拆实现、参数和坑。
2. DCGAN 的卷积结构为什么比全连接 GAN 稳:从判别器到生成器的选型逻辑
DCGAN 全称 Deep Convolutional Generative Adversarial Networks,2015 年提出后一直是图像生成入门的基线方案。它最大的贡献不是生成质量,而是把 GAN 的训练稳定性往前推了一大步。原始 GAN 用全连接层堆叠,生成器和判别器都是 MLP,训练时梯度震荡严重,模式崩溃(mode collapse)几乎必然发生。DCGAN 用卷积层替换全连接层,配合几条规定动作,让训练过程从"玄学"变成"可调"。
2.1 四条架构约束:哪些层能用、哪些不能用
DCGAN 论文里给了四条硬约束,这套 tensorflow 实现基本遵循了:
第一,判别器用步长卷积(strided convolution)替代池化层。池化会丢失空间信息,步长卷积让网络自己学下采样方式。第二,生成器用转置卷积(transposed convolution)做上采样,从噪声向量逐步放大到目标分辨率。第三,生成器和判别器都加 Batch Normalization,但生成器输出层和判别器输入层不加——输出层加 BN 会让生成的像素值被强行归一化,颜色失真;输入层加 BN 会破坏原始图像分布。第四,生成器输出层用 tanh,把像素值压到 [-1, 1];判别器统一用 LeakyReLU,斜率 0.2,避免 ReLU 在负区梯度为零导致的"神经元死亡"。
这四条不是拍脑袋定的,每一条都对应一个具体的训练失败模式。我见过有人把判别器输入层加了 BN,结果判别器对真实图和生成图的区分能力直接崩掉,loss 曲线变成一条直线。
2.2 生成器与判别器的张量流:从 100 维噪声到 64×64 图像
以 64×64 输出为例,生成器的张量变化路径是:
输入噪声 z,形状 [batch, 100]。经过第一个全连接层映射到 [batch, 4, 4, 1024],reshape 成 4×4×1024 的特征图。然后连续四次转置卷积:4×4 → 8×8 → 16×16 → 32×32 → 64×64。每次转置卷积把空间尺寸翻倍,通道数减半:1024 → 512 → 256 → 128 → 3(RGB 三通道)。最后一层用 tanh 激活。
判别器反过来:输入 [batch, 64, 64, 3],经过四次步长卷积,每次空间尺寸减半、通道数翻倍:64×64 → 32×32 → 16×16 → 8×8 → 4×4,通道 3 → 64 → 128 → 256 → 512。最后 flatten 后接一个全连接层输出单个标量,表示"这张图是真的概率"。
这套结构的关键参数是卷积核大小 5×5、步长 2、padding 用 same。5×5 比 3×3 感受野大,比 7×7 参数少,是 DCGAN 的默认选择。步长 2 保证每次精确翻倍或减半,不会出现尺寸对不齐的问题。
2.3 替换数据集前必须确认的三件事
标题说"替换数据集可直接用",但直接替换之前有三件事必须确认,否则跑起来就是一堆报错或者生成一堆噪声。
第一,图像尺寸。这套代码默认 64×64。如果你的原始图像是 256×256,要么改网络结构加两层转置卷积,要么在数据预处理阶段统一 resize 到 64×64。改网络结构意味着生成器和判别器的层数要同步调整,改不好就训崩。我一般建议新手先 resize,跑通之后再考虑改结构。
第二,图像通道数。默认是 RGB 三通道。如果你的数据是灰度图,生成器输出层通道改成 1,判别器输入层通道也改成 1。改一个不改另一个,张量维度对不上,直接报错。
第三,数据目录结构。这套代码通常要求数据放在一个目录下,代码自动读取目录内所有图片。如果你的数据分多个子目录,要么写个脚本把所有图片复制到一个目录,要么改数据读取部分的代码。我一般会先跑一个统计脚本,确认图片总数、尺寸分布、通道数分布,再动手改代码。
import os from PIL import Image from collections import Counter data_dir = "./data/my_dataset" sizes = Counter() modes = Counter() count = 0 for fname in os.listdir(data_dir): if not fname.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")): continue path = os.path.join(data_dir, fname) try: img = Image.open(path) sizes[img.size] += 1 modes[img.mode] += 1 count += 1 except Exception as e: print(f"读取失败: {fname}, 原因: {e}") print(f"图片总数: {count}") print(f"尺寸分布: {sizes.most_common(5)}") print(f"通道模式分布: {modes.most_common(5)}")这段脚本遍历数据目录,统计图片总数、尺寸分布和通道模式。sizes用 Counter 记录每种 (宽, 高) 组合出现的次数,modes记录 RGB、L、RGBA 等模式。跑完之后如果发现尺寸五花八门,就需要在数据加载阶段统一 resize;如果发现大量 RGBA 四通道图,需要先转成 RGB,否则读进来是四通道,和网络期望的三通道不匹配。这一步花两分钟,能省掉后面半小时的 debug。
3. 把 DCGAN 跑起来:环境、数据管道与训练循环的最小闭环
这一章是实操核心。目标是从零把训练跑起来,看到 loss 曲线和生成的样本图。不追求生成质量多高,先追求"能跑通、能出图、能判断训练是否正常"。
3.1 环境依赖与版本选择:TensorFlow 1.x 还是 2.x
这套代码名字里带 tensorflow,但没有版本号。实际拿到手第一件事是确认它用的是 TF1 还是 TF2。判断方法很简单:看代码里有没有tf.placeholder、tf.Session。有就是 TF1,没有就是 TF2 或者用了tf.compat.v1。
TF1 和 TF2 的 API 差异很大。TF1 用静态图,先定义计算图再sess.run;TF2 默认动态图,直接像 NumPy 一样运算。如果你环境里装的是 TF2 但代码是 TF1 写的,跑起来会报AttributeError: module 'tensorflow' has no attribute 'placeholder'。
我的建议是:如果代码是 TF1 写的,直接建一个 TF1 的虚拟环境,别折腾迁移。迁移 TF1 到 TF2 的工作量不比重写小,而且容易引入新 bug。建环境用 conda:
conda create -n dcgan_tf1 python=3.7 conda activate dcgan_tf1 pip install tensorflow==1.15.0 pip install pillow numpy scipy matplotlibPython 3.7 是 TF1.15 支持的最后几个版本之一,再高就不兼容了。pillow用于图像读写,numpy和scipy用于数值计算,matplotlib用于可视化生成结果。这四个是基础依赖,缺哪个补哪个。
如果代码是 TF2 写的,环境更简单:
conda create -n dcgan_tf2 python=3.9 conda activate dcgan_tf2 pip install tensorflow==2.10.0 pip install pillow numpy matplotlibTF2.10 是最后一个支持 Windows 原生 GPU 的版本,如果你在 Windows 上跑且想用 GPU,选这个版本。Linux 上可以用更新的版本。
注意:不要盲目装最新版 TensorFlow。新版本可能改了某些 API 的默认行为,导致老代码跑出不一样的结果。按代码实际需要的版本装,是最稳的做法。
3.2 数据管道:把任意目录的图片转成 64×64 的 batch
数据管道负责三件事:读图片、统一尺寸、归一化到 [-1, 1]。归一化到 [-1, 1] 是因为生成器输出层用 tanh,输出范围就是 [-1, 1],判别器输入也必须在这个范围,否则判别器看到的真实图和生成图分布不一致,训练必然失败。
import tensorflow as tf import os def load_image(path, image_size=64, channels=3): raw = tf.io.read_file(path) image = tf.image.decode_image(raw, channels=channels) image = tf.image.resize(image, [image_size, image_size]) image = tf.cast(image, tf.float32) # 归一化到 [-1, 1],与生成器 tanh 输出对齐 image = (image / 127.5) - 1.0 return image def build_dataset(data_dir, batch_size=64, image_size=64, channels=3): paths = [] for fname in os.listdir(data_dir): if fname.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")): paths.append(os.path.join(data_dir, fname)) dataset = tf.data.Dataset.from_tensor_slices(paths) dataset = dataset.map( lambda p: load_image(p, image_size, channels), num_parallel_calls=tf.data.AUTOTUNE ) dataset = dataset.shuffle(buffer_size=min(len(paths), 5000)) dataset = dataset.batch(batch_size, drop_remainder=True) dataset = dataset.prefetch(tf.data.AUTOTUNE) return datasetload_image里decode_image的channels参数强制把图片转成指定通道数,灰度图传 3 会自动复制成三通道,RGBA 传 3 会丢掉 alpha 通道。resize统一到 64×64。归一化公式(image / 127.5) - 1.0把 [0, 255] 映射到 [-1, 1]。
build_dataset里drop_remainder=True很重要。如果最后一个 batch 不满 batch_size,判别器计算 Batch Normalization 时统计量会有偏差,训练不稳定。丢掉最后一个不满的 batch,保证每个 batch 大小一致。shuffle的 buffer_size 取数据量和 5000 的较小值,数据量小的时候全量打乱,数据量大时用 5000 的滑动窗口打乱,兼顾内存和随机性。
3.3 训练循环:两个优化器、两个 loss、一次交替更新
DCGAN 的训练循环是交替更新:先更新判别器一次,再更新生成器一次。判别器的目标是最大化区分真实图和生成图的准确率,生成器的目标是让判别器把生成图判为真实。
import tensorflow as tf cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True) def discriminator_loss(real_output, fake_output): # 真实图标签为 1,生成图标签为 0 real_loss = cross_entropy(tf.ones_like(real_output), real_output) fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output) return real_loss + fake_loss def generator_loss(fake_output): # 生成器希望判别器把生成图判为 1 return cross_entropy(tf.ones_like(fake_output), fake_output) generator_optimizer = tf.keras.optimizers.Adam(1e-4, beta_1=0.5) discriminator_optimizer = tf.keras.optimizers.Adam(1e-4, beta_1=0.5) @tf.function def train_step(images, generator, discriminator): noise = tf.random.normal([images.shape[0], 100]) with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: generated = generator(noise, training=True) real_output = discriminator(images, training=True) fake_output = discriminator(generated, training=True) gen_loss = generator_loss(fake_output) disc_loss = discriminator_loss(real_output, fake_output) gen_grads = gen_tape.gradient(gen_loss, generator.trainable_variables) disc_grads = disc_tape.gradient(disc_loss, discriminator.trainable_variables) generator_optimizer.apply_gradients(zip(gen_grads, generator.trainable_variables)) discriminator_optimizer.apply_gradients(zip(disc_grads, discriminator.trainable_variables)) return gen_loss, disc_loss关键参数说明:Adam 的学习率设 1e-4,这是 DCGAN 的默认值。beta_1设 0.5 而不是默认的 0.9,这是 DCGAN 论文特别强调的——0.9 的动量会让训练初期震荡太大,0.5 更稳。from_logits=True表示 loss 函数内部会做 sigmoid,所以判别器最后一层不要加 sigmoid 激活,直接输出 logits。
tf.function装饰器把 train_step 编译成图执行,速度比 eager 模式快很多。tf.GradientTape分别记录生成器和判别器的梯度,注意两个 tape 要分开,不能共用一个,否则梯度会互相污染。
训练循环主体:
EPOCHS = 200 noise_dim = 100 num_examples_to_generate = 16 seed = tf.random.normal([num_examples_to_generate, noise_dim]) for epoch in range(EPOCHS): for image_batch in dataset: gen_loss, disc_loss = train_step(image_batch, generator, discriminator) if (epoch + 1) % 10 == 0: print(f"Epoch {epoch+1}, Gen Loss: {gen_loss:.4f}, Disc Loss: {disc_loss:.4f}") predictions = generator(seed, training=False) save_images(predictions, f"output/epoch_{epoch+1}.png")每 10 个 epoch 打印一次 loss 并保存一组生成图。seed是固定的噪声向量,这样不同 epoch 生成的图有可比性,能直观看到生成质量的变化。如果每次都用随机噪声,你没法判断是模型变好了还是噪声变了。
3.4 怎么判断训练是否正常:loss 曲线的三种典型形态
DCGAN 的 loss 曲线不像分类任务那样单调下降,它的正常形态是两条曲线在某个区间内震荡,判别器 loss 略低于生成器 loss。以下三种形态对应三种问题:
第一种,判别器 loss 趋近于 0,生成器 loss 持续上升。这是判别器太强,生成器学不动。解决办法是降低判别器学习率,或者给判别器加 dropout,或者减少判别器更新频率(比如每更新两次生成器才更新一次判别器)。
第二种,判别器 loss 和生成器 loss 都在 0.69 附近震荡(0.69 是 ln2,随机猜测的交叉熵)。这是判别器太弱,分不清真假,生成器也没有压力去提升。解决办法是提高判别器学习率,或者增加判别器层数。
第三种,loss 曲线周期性大幅震荡,生成图在不同 epoch 之间风格突变。这是学习率太大或者 batch size 太小。解决办法是把学习率降到 1e-4 以下,或者把 batch size 加到 128。
我一般会在训练脚本里加一个每隔 100 个 step 记录 loss 的逻辑,存成 CSV,训练完用 matplotlib 画出来。光看终端打印的数字,很难判断趋势。
4. 替换数据集后翻车的五个典型场景:从报错到生成噪声
这一章专门讲替换数据集之后最容易踩的坑。每个坑按"现象 → 原因 → 解决"写,都是我或者身边人实际遇到过的。
4.1 现象:训练一开始就报张量维度不匹配
原因:生成器输出通道数和数据集通道数不一致。比如数据集是灰度图,生成器输出 3 通道,判别器输入期望 1 通道,或者反过来。报错信息通常是ValueError: Dimensions must be equal或者InvalidArgumentError: Incompatible shapes。
解决:先确认数据集通道数。用第 2 章那个统计脚本看modes分布。如果是灰度图,把生成器最后一层 Conv2DTranspose 的 filters 改成 1,判别器第一层 Conv2D 的 input_shape 改成(64, 64, 1)。两个地方必须同时改,改一个不改另一个照样报错。
4.2 现象:训练能跑,但生成图全是灰色噪声,loss 不下降
原因:数据归一化没做对。常见错误是只除了 255 没减 0.5,或者用了 ImageNet 的均值方差归一化。DCGAN 的生成器输出是 tanh,范围 [-1, 1],如果真实图归一化到 [0, 1],判别器看到的真实图和生成图分布差了一倍,判别器很容易区分,生成器梯度消失。
解决:确认归一化公式是(image / 127.5) - 1.0。跑之前打印一个 batch 的 min 和 max,确认在 [-1, 1] 附近。如果 min 是 0、max 是 1,说明归一化写错了。
4.3 现象:训练到一半生成图突然全部变成同一张脸
原因:模式崩溃(mode collapse)。生成器发现某一张图能稳定骗过判别器,就只生成那一张,不再探索其他模式。数据量越小、类别越不均衡,越容易发生。
解决:三个手段组合用。第一,给判别器加 dropout 或者谱归一化(spectral normalization),降低判别器对单一模式的过拟合。第二,生成器 loss 里加一个多样性正则项,比如鼓励不同噪声生成不同图像。第三,最简单粗暴的——减少判别器的更新频率,比如每更新 3 次生成器才更新 1 次判别器。我一般先用第三个手段,改一行代码就能试。
4.4 现象:训练 loss 正常,但生成图分辨率明显低于 64×64
原因:数据预处理阶段 resize 之后又做了 center crop,或者 resize 的插值方式用了最近邻(nearest),导致图像块状化。还有一种可能是生成器最后一层转置卷积的 stride 和 kernel 组合不对,实际输出尺寸小于预期。
解决:检查数据管道里 resize 之后有没有多余的裁剪操作。插值方式用bilinear或bicubic,别用nearest。然后打印生成器输出张量的 shape,确认是(batch, 64, 64, 3)。如果小于 64,检查转置卷积的 stride 和 padding 设置。
4.5 现象:GPU 显存溢出,batch size 降到 1 还是 OOM
原因:生成器或判别器的参数量太大,或者输入分辨率太高。64×64 的 DCGAN 参数量在百万级别,正常 4GB 显存够用。如果 OOM,很可能是误把输入分辨率设成了 256×256 或者更高,参数量翻了十几倍。
解决:先确认 image_size 是 64。如果确实是 64 还 OOM,检查有没有在训练循环里累积梯度而没有释放,比如在循环内反复创建新的模型实例。还有一种可能是 TensorFlow 默认占满所有显存,加一行配置让它按需分配:
gpus = tf.config.experimental.list_physical_devices('GPU') if gpus: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True)这段代码让 TensorFlow 按实际需要申请显存,而不是一次性占满。放在所有 TensorFlow 操作之前执行。
5. 生成样本怎么用才算有效增强:筛选、配比与验证的实操技巧
生成图造出来只是第一步,直接混进训练集很可能起反作用。这一章讲怎么筛选生成样本、怎么配比、怎么验证增强是否真的有效。
5.1 用判别器打分做初筛:保留哪些、丢掉哪些
训练完之后,判别器就是一个现成的"真实性打分器"。把生成图喂给判别器,输出 logits 越高,说明判别器越认为它像真实图。我一般会生成 5 到 10 倍于目标数量的图,然后用判别器打分,保留分数最高的那部分。
import numpy as np def score_generated_images(generator, discriminator, num_samples=1000, batch_size=64): scores = [] images = [] for i in range(0, num_samples, batch_size): noise = tf.random.normal([batch_size, 100]) gen_images = generator(noise, training=False) logits = discriminator(gen_images, training=False) scores.extend(logits.numpy().flatten()) images.append(gen_images.numpy()) images = np.concatenate(images, axis=0) scores = np.array(scores) # 按分数从高到低排序,返回排序后的索引 sorted_idx = np.argsort(scores)[::-1] return images[sorted_idx], scores[sorted_idx]num_samples是生成总数,batch_size控制显存占用。返回的images和scores都按分数降序排列。实际使用时,取前 30% 到 50% 就够了。判别器分数特别低的生成图,往往有明显的伪影或结构错误,混进去只会教坏分类器。
注意:判别器打分不是绝对标准。判别器本身也是在训练中逐渐变强的,早期 epoch 的判别器打分参考价值有限。建议用训练后期(最后 20% epoch)的判别器来打分。
5.2 真实样本与生成样本的配比:从 1:1 到 1:3 的梯度实验
生成样本和真实样本的比例没有固定答案,取决于真实数据量和任务难度。我的经验是:真实数据越少,生成样本比例可以越高,但上限是 1:3(真实:生成)。超过 1:3 之后,分类器会开始学到生成图的伪影特征,在真实测试集上反而掉点。
具体操作:先按 1:1 配比训一个分类器,记录验证集准确率。然后按 1:2、1:3 各训一个,对比。如果 1:2 比 1:1 好,1:3 又比 1:2 差,那 1:2 就是当前数据下的最优配比。这个实验成本不高,但能避免拍脑袋定比例。
还有一个细节:生成样本不要和真实样本混在一起做数据增强(比如对生成图再做旋转翻转)。生成图本身已经有分布偏移,再叠加几何变换会让偏移更大。生成图直接用,几何变换只对真实图做。
5.3 验证增强效果:用固定测试集对比增强前后的分类指标
验证增强是否有效,唯一可靠的方法是固定测试集上的对比实验。测试集必须全部是真实图,不能混入任何生成图。对比三组:只用真实数据训练、真实+生成 1:1、真实+生成 1:2。每组跑三次不同随机种子,取平均。
# 伪代码示意,实际用你的分类器训练脚本 configs = [ {"name": "real_only", "real_ratio": 1.0, "fake_ratio": 0.0}, {"name": "real_fake_1_1", "real_ratio": 1.0, "fake_ratio": 1.0}, {"name": "real_fake_1_2", "real_ratio": 1.0, "fake_ratio": 2.0}, ] for cfg in configs: for seed in [42, 123, 2024]: acc = train_and_eval(cfg, seed) print(f"{cfg['name']} seed={seed} acc={acc:.4f}")如果增强后的准确率比 real_only 高出 2 个点以上,说明增强有效。如果持平或者下降,说明生成图质量不够,需要回到第 3 章调训练参数,或者减少生成图比例。
5.4 一个容易被忽略的细节:生成图的标签噪声
生成图虽然来自少数类,但它和真实少数类样本之间存在分布差异。如果直接把生成图标成少数类标签,分类器可能会学到"这类图的边界比真实边界更宽",导致在真实测试集上对少数类的判定过于激进。
我的做法是给生成图加一个小的标签平滑(label smoothing),比如真实少数类标签是 1.0,生成图标签用 0.9。这样分类器不会完全信任生成图,保留一定的区分能力。这个技巧在数据量极少(每类几十张)的时候效果尤其明显。
5.5 训练日志与生成图存档:出问题时能回看的后悔药
DCGAN 训练一次动辄几小时,如果中途崩了或者生成质量不达标,没有日志就只能重跑。我习惯在训练脚本里做三件事:每 10 个 epoch 保存一次模型权重、每 10 个 epoch 保存一组固定噪声的生成图、每个 step 记录 loss 到 CSV。
模型权重用tf.train.Checkpoint或者model.save_weights保存,文件名带 epoch 数。生成图按 epoch 编号存成 PNG,方便对比不同阶段的生成质量。loss CSV 用csv模块追加写入,字段包括 epoch、step、gen_loss、disc_loss。
这套存档机制看起来繁琐,但当你需要回滚到某个中间 checkpoint 做微调,或者需要向别人解释"训练到第 80 epoch 时生成质量最好"的时候,它就是后悔药。我吃过没存日志的亏,重跑一次多花了大半天,希望你别踩同样的坑。
最后一句话是我自己的习惯:每次替换新数据集,先跑 10 个 epoch 看 loss 曲线和生成图,确认方向对了再跑完整训练。方向不对,跑 200 个 epoch 也是白跑。希望帮到你。
本文还有配套的精品资源,点击获取