跳过正文
  1. AI 相关内容/

GAN 模型实践-黑暗之魂游戏画面生成

·804 字·4 分钟
目录

本教程使用 TensorFlow 搭建深度卷积生成对抗网络(DCGAN),在 FromSoftware 魂系游戏(Dark Souls)截图数据集上训练,实现从随机噪声生成黑暗之魂的游戏画面。

1. 环境准备
#

导入所需的库:

import tensorflow as tf
from tensorflow.keras import layers
from pathlib import Path
import matplotlib.pyplot as plt
from IPython import display
import random
import os
import time
from datetime import datetime
from dateutil import tz
import seaborn as sns
sns.set()
time_zone = tz.gettz('Asia/Shanghai')

2. 数据集下载
#

数据集来自 Kaggle 的 FromSoftware 魂系游戏图片集:

URL = 'https://www.kaggle.com/api/v1/datasets/download/fraxle/images-from-fromsoftware-soulslikes?datasetVersionNumber=1'

设置工作目录:

!test ! -d /content/GAN_DS1 && mkdir /content/GAN_DS1
%cd /content/GAN_DS1
/content/GAN_DS1

数据集包含 24,218 张 PNG 图片,均为 Dark Souls 等魂系游戏的截图:

dataset_path = Path.cwd()/'../datasets/FromSoftwareImages/dark souls'
len(list(dataset_path.glob('*.png')))
24218

3. 数据清洗
#

检查并移除损坏文件。运行过程中因数据量过大被中断,未完成全部检查。

def check(file_dir, format, channels):
  cnt = 0
  bad_files =[]
  for file_path in sorted(file_dir.glob(f'*.{format}')):
    cnt += 1
    try:
      img = tf.io.read_file(str(file_path))
      img = tf.image.decode_image(img)
      if img.ndim != 3:
        raise Exception('invalid')
      if img.shape[-1] != channels:
        raise Exception('invalid')
    except Exception as e:
      if str(e) == 'invalid':
        bad_files.append(str(file_path))
  print(f'checked {cnt} files, found {len(bad_files)} bad files')
  return bad_files

bad_files = check(dataset_path, 'png', 3)

4. 数据预览与预处理
#

4.1 查看样本
#

原始图片尺寸为 360×640 像素:

for file_path in sorted(dataset_path.glob('*.png'))[:10]:
  img = tf.io.read_file(str(file_path))
  img = tf.image.decode_image(img)
  print(img.shape)
  plt.figure()
  plt.imshow(img)
  plt.axis('off')
  break
(360, 640, 3)

4.2 参数设置
#

将所有图片缩放到 128×256

BUFFER_SIZE = 24218
BATCH_SIZE = 256

# 640 360
IMG_WIDTH = 128
IMG_HEIGHT = 256

4.3 图片加载函数
#

def get_image(path, width, height):
  img = tf.io.read_file(str(path))
  img = tf.image.decode_image(img)
  img = tf.image.resize(img, [width, height])
  img = img / 255.0
  return img

4.4 自定义数据提供器
#

支持限制读取数量,训练时随机打乱顺序:

class ImageProvider:
  def __init__(self, path, nums, width, height, format, training=False):
    self.path = path
    self.nums = nums
    self.format = format
    self.width = width
    self.height = height
    self.training = training

  def __call__(self):
    files = sorted(list(self.path.glob(f'*.{self.format}'))[:self.nums])
    if self.training:
      random.shuffle(files)

    for f in files:
      yield get_image(f, self.width, self.height)

验证数据提供器:

image_provider = ImageProvider(dataset_path, 10, IMG_WIDTH, IMG_HEIGHT, 'png')
img = next(image_provider())
print(img.shape)
plt.imshow(img)
plt.axis('off')
(128, 256, 3)

4.5 构建 tf.data 流水线
#

train_ds = tf.data.Dataset.from_generator(
    ImageProvider(dataset_path, BUFFER_SIZE, IMG_WIDTH, IMG_HEIGHT, 'png', 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.imshow(images[0])
  plt.axis('off')
(256, 128, 256, 3)

每个 batch 包含 256 张 128×256×3 的图片。

5. 构建生成器(Generator)
#

5.1 噪声维度
#

seed_dim = 100

5.2 生成器网络结构
#

接收 100 维随机噪声,通过多层转置卷积(Conv2DTranspose)逐步上采样到 128×256×3 的输出:

# 128 256
# 64 128
# 32 64
# 16 32
# 8 16

def make_generator_model():
  model = tf.keras.Sequential()
  model.add(layers.Input(shape=(seed_dim, )))
  model.add(layers.Dense(8*16*256, use_bias=False))
  model.add(layers.BatchNormalization())
  model.add(layers.LeakyReLU())

  model.add(layers.Reshape((8, 16, 256)))

  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(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 model

5.3 测试生成器
#

seed = tf.random.normal([1, seed_dim])
generator = make_generator_model()
generated_images = generator(seed, training=False)
print(generated_images.shape)
plt.imshow(generated_images[0, :, :, 0])
plt.axis('off')
(1, 128, 256, 3)

输出形状为 (1, 128, 256, 3),与预处理后的真实图片尺寸一致。

6. 构建判别器(Discriminator)
#

判别器接收 128×256×3 的图片,通过多层卷积逐步下采样,最终输出一个标量 logit:

def make_discriminator_model():
  model = tf.keras.Sequential()
  model.add(layers.Input(shape = (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_images)
print(decision)
tf.Tensor([[-0.00015949]], shape=(1, 1), dtype=float32)

输出接近 0,表示模型未经训练时对图片真假判断不明确。

7. 损失函数与优化器
#

7.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)
  return real_loss + fake_loss

def generator_loss(fake_output):
  return cross_entropy(tf.ones_like(fake_output), fake_output)
  • 判别器损失:希望真实图片被判为真(标签=1),生成图片被判为假(标签=0)
  • 生成器损失:希望生成图片被判别器判为真(标签=1)

7.2 优化器与训练参数
#

EPOCHS = 800

generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)

800 个 epoch,两个网络均使用 Adam 优化器,学习率 1e-4

8. 训练循环
#

8.1 单步训练
#

每个 batch 的对抗训练步骤:

@tf.function()
def train_step(images):
  seed = tf.random.normal([BATCH_SIZE, seed_dim])

  with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
    generated_images = generator(seed, 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.2 图片保存
#

每个 epoch 结束后保存当前生成器输出的图片:

!test ! -d generated_images && mkdir generated_images

def generate_and_save_images(model, epoch, test_input):
  predictions = model(test_input, training=False)
  generated_images = predictions * 255
  generated_images = generated_images.numpy()

  plt.figure()
  plt.imshow(generated_images[0].astype('uint8'))
  plt.axis('off')
  plt.savefig('./generated_images/image_at_epoch_{:04d}.png'.format(epoch))

8.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
)

seed = tf.random.normal([BATCH_SIZE, seed_dim])

def train(dataset, epochs):
  for epoch in range(epochs):
    print('epoch: ', epoch)
    start_time = 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_time} sec')
  checkpoint.save(file_prefix = checkpoint_prefix)

启动训练:

train(train_ds, EPOCHS)

9. 最终生成
#

训练完成后,使用生成器生成并保存一张带有时间戳的游戏画面:

!test ! -d final_generated && mkdir final_generated

time_str = datetime.now().astimezone(time_zone).strftime('%Y-%m-%d_%H:%M:%S')

seed = tf.random.normal([1, seed_dim])
generated_image = generator(seed)[0]
generated_image *= 255
generated_image = generated_image.numpy()
generated_image = tf.keras.preprocessing.image.array_to_img(generated_image)
generated_image.save(f'./final_generated/image_{time_str}.png')
plt.imshow(generated_image)
plt.axis('off')

10. 总结
#

项目 说明
模型类型 DCGAN(深度卷积生成对抗网络)
数据集 FromSoftware 魂系游戏截图 — 24,218 张 PNG
输入 100 维随机正态噪声
输出尺寸 128×256×3(宽幅游戏画面)
生成器结构 Dense + Conv2DTranspose + BatchNorm + LeakyReLU
判别器结构 Conv2D + LeakyReLU + Dropout 堆叠,最终输出单标量
损失函数 BinaryCrossentropy (from_logits=True)
优化器 Adam (lr=1e-4)
训练轮数 800 epochs
Batch size 256
原始图片尺寸 360×640 像素

本教程从数据下载、预处理到模型构建和训练,完整实现了 DCGAN 在魂系游戏画面生成任务中的应用。