本教程使用 TensorFlow 搭建一个深度卷积生成对抗网络(DCGAN),在 CelebA 人脸数据集上训练,实现从随机噪声生成人脸图像。
1. 环境准备 #
导入所需的库:
import tensorflow as tf
import glob
import imageio
import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
from tensorflow.keras import layers
import time
from IPython import display
import pathlib
import random
import seaborn as sns
import gdown
sns.set()2. 数据集下载 #
从 Google Drive 下载 CelebA 人脸数据集(约 20 万张人脸图片)。
!test ! -d /content/GAN_face && mkdir /content/GAN_face
%cd /content/GAN_face/content/GAN_faceurl = "https://drive.google.com/uc?id=1O7m1010EJjLE5QxLZiM9Fpjs7Oj6e684"
gdown.download(url, "dataset.zip", quiet=True)'dataset.zip'解压数据集:
!test ! -d dataset && mkdir dataset
!unzip -q -d dataset dataset.zipdataset_path = pathlib.Path.cwd()/'dataset/img_align_celeba'查看前 10 张图片的文件名和原始尺寸:
!ls dataset/img_align_celeba/ | head -n 10
img = tf.io.read_file('dataset/img_align_celeba/000001.jpg')
img = tf.image.decode_image(img)
print(img.shape)
plt.figure()
plt.imshow(img)
plt.axis('off')000001.jpg
000002.jpg
000003.jpg
000004.jpg
000005.jpg
000006.jpg
000007.jpg
000008.jpg
000009.jpg
000010.jpg
(218, 178, 3)
(-0.5, 177.5, 217.5, -0.5)
数据集总量:
len(list(pathlib.Path.cwd().glob('dataset/*/*.jpg')))202599共约 202,599 张人脸图片,原始尺寸为 218×178 像素。
3. 数据清洗 #
检查并移除损坏图片:
from pathlib import Path
from tensorflow.io import read_file
from tensorflow.image import decode_image, decode_jpeg
def check(data_dir, channels):
cnt = 0
for image in sorted(data_dir.glob('*')):
cnt += 1
try:
img = read_file(str(image))
img = decode_jpeg(img)
if img.ndim != 3:
raise Exception('invalid')
if img.shape[-1] != channels:
raise Exception('invalid')
except Exception as e:
if str(e) == 'invalid':
print('removed bad file: ', str(image))
image.unlink()
print('checked {} files'.format(cnt))removed bad file: ...
checked 4117 files4. 数据预处理与加载 #
将图片统一缩放到 56×56 像素,归一化到 [0,1] 区间。
4.1 参数设置 #
BUFFER_SIZE = 6000
BATCH_SIZE = 256
IMG_WIDTH = 56
IMG_HEIGHT = 564.2 图片加载函数 #
def get_image(path, width, height, channels):
img = tf.io.read_file(str(path))
img = tf.io.decode_jpeg(img, channels)
img = tf.image.resize(img, [width, height], method=tf.image.ResizeMethod.BILINEAR)
img = img / 255.0
return img4.3 自定义数据提供器 #
使用 generator 逐张读取图片,训练时随机打乱顺序:
class ImageProvider:
def __init__(self, path, width, height, channels, format, training=False):
self.path = path
self.format = format
self.training = training
self.width = width
self.height = height
self.channels = channels
def __call__(self):
files = sorted(list(self.path.glob(f'*.{self.format}')))
if self.training:
random.shuffle(files)
for f in files:
yield get_image(f, self.width, self.height, self.channels)4.4 构建 tf.data 流水线 #
利用 cache、shuffle、prefetch、batch 优化数据加载性能:
train_ds_ = tf.data.Dataset.from_generator(
ImageProvider(dataset_path, IMG_WIDTH, IMG_HEIGHT, 3, 'jpg', training=True),
output_signature=(tf.TensorSpec(shape=(IMG_WIDTH, IMG_HEIGHT, 3), dtype=tf.float32))
)
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds_.cache().shuffle(BUFFER_SIZE).prefetch(buffer_size=AUTOTUNE).batch(BATCH_SIZE)验证数据形状:
for images in train_ds.take(1):
print(images.shape)
plt.figure()
plt.imshow(images[0])
plt.axis('off')(256, 56, 56, 3)每个 batch 包含 256 张 56×56×3 的图片。
5. 构建生成器(Generator) #
生成器的作用是接收随机噪声向量,通过转置卷积(反卷积)逐步上采样,最终输出一张 56×56×3 的图片。
5.1 噪声维度设定 #
noise_dim = 100使用 100 维的随机正态分布向量作为输入。
5.2 生成器网络结构 #
def make_generator_model():
model = tf.keras.Sequential()
model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(noise_dim, )))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Reshape((7, 7, 256)))
model.add(layers.Conv2DTranspose(128, kernel_size=5, strides=1, padding='same', use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(64, kernel_size=5, strides=2, padding='same', use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(128, kernel_size=5, strides=2, padding='same', use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
model.add(layers.Conv2DTranspose(3, kernel_size=5, strides=2, padding='same', use_bias=False, activation='tanh'))
return model5.3 备选生成器结构 #
另一个更简洁的生成器变体,使用更小的卷积核(3×3):
def make_generator_model_2():
model = tf.keras.Sequential()
model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(noise_dim, ), activation='relu'))
model.add(layers.Reshape((7, 7, 256)))
model.add(layers.Conv2DTranspose(256, kernel_size=3, strides=2, padding='same'))
model.add(layers.BatchNormalization(momentum=0.8))
model.add(layers.ReLU())
model.add(layers.Conv2DTranspose(256, kernel_size=3, strides=2, padding='same'))
model.add(layers.BatchNormalization(momentum=0.8))
model.add(layers.ReLU())
model.add(layers.Conv2DTranspose(3, kernel_size=3, strides=2, padding='same', activation='tanh'))
return model5.4 测试生成器 #
用未训练的生成器测试一次前向传播,验证输出形状:
noise = tf.random.normal([1, noise_dim])
generator = make_generator_model()
generated_image = generator(noise, training=False)
print(generated_image[0].shape)
plt.imshow(generated_image[0, :, :, 0])
plt.axis('off')(56, 56, 3)输出 56×56×3 的图片,与预处理后的真实图片尺寸一致。
6. 构建判别器(Discriminator) #
判别器是一个二分类卷积网络,接收 56×56×3 的图片,输出一个标量 logit,表示图片为真实图片的置信度。
def make_discriminator_model():
model = tf.keras.Sequential()
model.add(layers.Input([IMG_WIDTH, IMG_HEIGHT, 3]))
model.add(layers.Conv2D(64, kernel_size=5, strides=2, padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(128, kernel_size=5, strides=2, padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(256, kernel_size=5, strides=2, padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(128, kernel_size=5, strides=2, padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Conv2D(64, kernel_size=5, strides=2, padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
model.add(layers.Flatten())
model.add(layers.Dense(1))
return model测试未训练的判别器对生成图片的判断:
discriminator = make_discriminator_model()
decision = discriminator(generated_image)
print(decision)tf.Tensor([[-0.00022056]], shape=(1, 1), dtype=float32)由于模型未经训练,输出接近于 0(即对真假判断不明确)。
7. 损失函数与优化器 #
7.1 损失函数 #
使用 BinaryCrossentropy(from_logits=True),将判别器的输出直接作为 logits 计算损失:
- 判别器损失:
real_loss:真实图片被误判为假的损失(标签=1)fake_loss:生成图片被误判为真的损失(标签=0)- 总损失 = real_loss + fake_loss
- 生成器损失:生成图片被判别器判为真的能力(标签=1)
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
def discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output)
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss
return total_loss
def generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output)7.2 优化器 #
生成器和判别器均使用 Adam 优化器,学习率 1e-4:
generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)7.3 检查点 #
设置模型保存路径,每轮训练后可恢复:
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, 'ckpt')
checkpoint = tf.train.Checkpoint(
generator_optimizer=generator_optimizer,
discriminator_optimizer=discriminator_optimizer,
generator=generator,
discriminator=discriminator
)8. 训练循环 #
8.1 参数设置 #
训练 500 个 epoch,每次生成 16 张样例图片:
EPOCHS = 500
num_examples_to_generate = 16
seed = tf.random.normal([num_examples_to_generate, noise_dim])8.2 单步训练 #
每个 batch 的步骤如下:
- 采样随机噪声 → 生成器生成假图
- 真实图片和假图分别送入判别器
- 计算生成器损失和判别器损失
- 反向传播更新两个网络的参数
@tf.function
def train_step(images):
noise = tf.random.normal([BATCH_SIZE, noise_dim])
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True)
real_output = discriminator(images, training=True)
fake_output = discriminator(generated_images, training=True)
gen_loss = generator_loss(fake_output)
disc_loss = discriminator_loss(real_output, fake_output)
gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))8.3 图片保存函数 #
每个 epoch 结束后生成并保存当前生成器的输出图片:
!test ! -d generated_images && mkdir generated_images
def generate_and_save_images(model, epoch, test_input):
predictions = model(test_input, training=False)
fig = plt.figure()
generated_image = predictions * 255
generated_image = generated_image.numpy()
plt.imshow(generated_image[0].astype("uint8"))
plt.axis('off')
plt.savefig('./generated_images/image_at_epoch_{:04d}.png'.format(epoch))
plt.show()8.4 完整训练流程 #
def train(dataset, epochs):
for epoch in range(epochs):
print('epoch: ', epoch)
start = time.time()
batch_idx = 0
for image_batch in dataset:
train_step(image_batch)
batch_idx += 1
display.clear_output(wait=True)
generate_and_save_images(generator, epoch+1, seed)
print(f'Time for epoch {epoch+1} is {time.time()-start} sec')
checkpoint.save(file_prefix=checkpoint_prefix)
display.clear_output(wait=True)
generate_and_save_images(generator, epochs, seed)启动训练:
train(train_ds, EPOCHS)8.5 检查点文件 #
训练完成后,模型权重保存在检查点文件中:
total 53M
-rw-r--r-- 1 root root 69 Aug 27 01:41 checkpoint
-rw-r--r-- 1 root root 53M Aug 27 01:41 ckpt-1.data-00000-of-00001
-rw-r--r-- 1 root root 5.6K Aug 27 01:41 ckpt-1.index检查点文件约 53MB,包含生成器和判别器的全部权重。
9. 最终生成 #
训练完成后,使用生成器批量生成 18 张人脸图片:
!test ! -d final_generated && mkdir final_generated
num_img = 18
def Potrait_Generator():
Generated_Paintings = []
seed = tf.random.normal([num_img, noise_dim])
generated_image = generator(seed)
generated_image *= 255
generated_image = generated_image.numpy()
for i in range(num_img):
img = tf.keras.preprocessing.image.array_to_img(generated_image[i])
Generated_Paintings.append(img)
img.save("./final_generated/image{:02d}.png".format(i))
return
def Show_Img(data):
plt.figure(figsize=(15, 15))
for images in data.take(1):
for i in range(18):
ax = plt.subplot(6, 6, i + 1)
ax.imshow(images[i].numpy().astype("uint8"))
ax.axis("off")
Images = Potrait_Generator()加载并显示生成的 18 张人脸:
Generated_path = "./final_generated/"
Potraits_generated = tf.keras.preprocessing.image_dataset_from_directory(Generated_path, label_mode=None)
Show_Img(Potraits_generated)Found 18 files belonging to 1 classes.
10. 总结 #
| 项目 | 说明 |
|---|---|
| 模型类型 | DCGAN(深度卷积生成对抗网络) |
| 数据集 | CelebA — 约 20 万张人脸图片 |
| 输入 | 100 维随机正态噪声 |
| 输出尺寸 | 56×56×3(RGB 人脸) |
| 生成器结构 | Dense + Conv2DTranspose + BatchNorm + LeakyReLU |
| 判别器结构 | Conv2D + LeakyReLU + Dropout 堆叠,最终输出单标量 |
| 损失函数 | BinaryCrossentropy (from_logits=True) |
| 优化器 | Adam (lr=1e-4) |
| 训练轮数 | 500 epochs |
| Batch size | 256 |
| 总参数量(检查点) | 53MB |
| 最终生成数量 | 18 张 |
本教程完整实现了从数据下载、预处理、模型构建到训练和生成的全流程。你可以通过调整网络结构、超参数或训练更长时间来进一步提升生成质量。