GAN的工程化用例

摘要

在深度学习的过程中,数据的采集和标注是非常头疼和重要的一个部分。通过纯工人的方式来做费时费力,因此,如果在条件允许的情况下,我们可以通过市面上一些已经存在的识别软件来尝试对数据做标注(可以通过调整置信率来提高标注的准确度),甚至可以采取不同识别软件/模型做交叉验证,进一步确保标注的准确度。
但是,收集数据本身也是一个资源受限的问题,在实际过程中,我们想到了通过使用DCGAN来学习以标注的数据,然后生成出一些具有相同标签(分布)的伪数据,从而达到拓展标注数据集的目的。
最后,利用原始数据+伪数据再去进行模型训练,就可以获得一些“比较好”的预期了。
本文会基于数字识别这样一个场景,给出一个整套的工程化操作过程。

流程步骤

  1. 采集原始数据
  2. 使用已有的识别软件/模型,进行数据的一次标注
  3. (可选)采用其它软件/模型,进行数据二次标注(交叉验证)
  4. 构造DCGAN(深度卷积对抗生成网络),训练生成器和辨识器,从而获得伪数据
  5. 对伪数据进行2-3步骤的标注,过滤出比较好的数据并保留下来
  6. 保留3,5步骤中的数据,作为真实的训练数据代入模型进行训练

Example:数字识别

我们使用王者荣耀中的Kill,Defence,Assistant数值来作为初始数据。数据集的基数为0-9,10个数字各500个,数据样本总共是5000。对于想要训练一个识别数字的神经网络来说,这个训练的数据量远远是不够的,另外截取出来的数字都是没有进行过标注的,所以也无法识别使用。
因此,通过上面的流程步骤,我们需要使用一些软件来做一次识别标注,这边我们使用Tesseract

步骤2,使用Tesseract对数字做一次识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for file in tqdm(os.listdir(input_path)):
recoginze_digits = ''
img = Image.open(os.path.join(input_path,file))
img = img.resize(size=(28,28))
# 2. the first recognize
first_recognize = pytesseract.image_to_string(img,config="-psm 7 digits")
for digit in first_recognize:
if digit >= '0' and digit <= '9':
recoginze_digits = recoginze_digits + digit
else:
recoginze_digits='unrecognize'
break
#if the recognize is unknown or unrecognize , just skip this file
if recoginze_digits == '' or recoginze_digits == 'unrecognize':
continue
  • 先遍历文件夹中的所有文件,并把这些文件做一次resize,这个是一个归一化的动作
  • 调用python版本的tesseract,对传入的图片做一次识别

    当然,由于我们事先知道图片肯定是数字,因此,我们限定特定的字库digits,避免出现类似abcd的字母,造成干扰

  • 对于识别的结果,我们做一次过滤,对于出现类似1.3小数的识别结果,我们就直接pass掉
  • 因此,在line 18的continue,就帮我们过滤掉了这类无效结果

步骤3,交叉验证

1
2
3
4
5
6
7
8
9
10
11
12
# 3. convert the same file to white/black and recognize again
img = convertToBinary(img)
second_recognize = pytesseract.image_to_string(img,config="-psm 7 digits")
# if the result of 1st and 2nd are the same , then we save the file
# into a separate folder
if second_recognize == first_recognize :
if not os.path.exists(os.path.join(output_path,second_recognize)):
os.mkdir(os.path.join(output_path,second_recognize))
if not os.path.exists(os.path.join(output_path,second_recognize,file)):
shutil.copy(os.path.join(input_path,file),
os.path.join(output_path,second_recognize,file))

理论上,我们应该使用其他的识别软件/模型来做一次交叉验证,比如可以用训练好的minst模型来做预测。但我们这边偷了一次懒,因此直接对原始的数据做一次二值化(大津法获取阈值),然后再做一次识别,如果识别的结果跟第一次一致,那我们就认为这个图片是“好”的,需要保留。

  • line 2,先对原始的图片做一次二值化
  • line 3,对二值化之后的图片调用Tesseract再做一次识别
  • line 7,如果两次识别结果一致,那么我们就把文件保存到特定的地方(line 11

步骤4,构造DCGAN,使用标注过的数据来生成更多的伪数据

关于DCGAN,CSDN的这篇文章不错《深度卷积对抗生成网络(DCGAN)》
知乎上也有一篇文章不错,《用DCGAN生成女朋友》
GAN网络的核心,是生成器(generator)和辨识器(discriminator)的对抗,在对抗的过程中不断优化G与D,举一个不恰当的比喻,生成器(G)是一个制假卖假的犯罪团伙,辨识器(D)是相关的市场监管部门,在刚开始的时候,G的技术比较差劲,D总可以非常简单的识别出来,把G绳之以法,但是随着G的技术不断提升,D越来越难识别出G的产品,因此,最后G就可以做出“以假乱真”的产品。
一个DCGAN的generator网络结构:

针对上述网络,解析如下:输入一个100维的随机向量z

  • 对这100维的向量,做一个全连接层,连接层的大小为4x4x1024个节点
  • 对这个全连接层做reshape,tensor shape=(4,4,1024)
  • 做一次反卷积,其中kernel size=5,filters=512,stride=2
    因此,经过这一次反卷积之后得到的shape=(8, 8, 512)
  • 继续做反卷积,其中kernel size=5,filters=256,stride=2
    经过这一次反卷积之后得到的shape=(16, 16, 256)
  • 第三次反卷积,其中kernel size=5,filters=128,stride=2
    经过这一次反卷积之后得到的shape=(32, 32, 128)
  • 第四次反卷积,其中kernel size=5,filters=3,stride=2
    经过这一次反卷积之后得到的shape=(64, 64, 3),也就是64x64的RGB图片

所以,一个100维的向量通过这个模型,就成为了一副64x64的RGB图片。
对应的,辨识器D,其实就是一个逆向过程,而D的输出结果,最后就是0,1的False与True,也就是鉴定结果。

Implement of G

注意,这里跟之前的示例图片略有出入,因为实际输出的图片是28x28。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def generator(z, out_channel_dim=3 , is_train=True):
alpha=0.2
with tf.variable_scope('generator',reuse=not(is_train)):
# Reshape it to start the convolutional stack
fc = tf.layers.dense(z, 4*4*512)
x = tf.reshape(fc, (-1, 4, 4, 512))
x = tf.layers.batch_normalization(x, training=is_train)
relu_x = tf.maximum(alpha * x, x)
# 4x4x512 now
conv2d_1_t = tf.layers.conv2d_transpose(relu_x, 256,4, strides=1, padding='valid')
conv2d_1_t_bn = tf.layers.batch_normalization(conv2d_1_t, training=is_train)
relu1 = tf.maximum(alpha * conv2d_1_t_bn, conv2d_1_t_bn)
# 7x7x256 now
conv2d_2_t = tf.layers.conv2d_transpose(relu1, 128, 4, strides=2, padding='same')
conv2d_2_t_bn = tf.layers.batch_normalization(conv2d_2_t, training=is_train)
relu2 = tf.maximum(alpha * conv2d_2_t_bn, conv2d_2_t_bn)
# 14x14x128
# Output layer
logits = tf.layers.conv2d_transpose(relu2, out_channel_dim, 4, strides=2, padding='same')
# 28x28x3 now
out = tf.tanh(logits)
return out

解释一下:

  • 对这100维的向量,做一个全连接层,连接层的大小为4x4x512个节点
  • 对这个全连接层做reshape,tensor shape=(4,4,512)
  • 做一次反卷积,其中kernel size=4,filters=256,stride=1
    因此,经过这一次反卷积之后得到的shape=(7, 7, 256)
  • 继续做反卷积,其中kernel size=4,filters=128,stride=2
    经过这一次反卷积之后得到的shape=(14, 14, 128)
  • 第三次反卷积,其中kernel size=4,filters=128,stride=2
    经过这一次反卷积之后得到的shape=(28, 28, out_channel_dim)
  • 最后,对28x28xout_channel_dim做一次tanh,转换到(-1,1)方便做(0,255)的映射

这里有两个地方,一个是在D中,一般不直接使用ReLu,而是使用LeakyRelu,也就是tf.maximum(alpha * x, x)
另外一个,就是针对反卷积后的结果,做batch_normalization

有一些看的懂的文章,解释了为什么要用LeakyRelu知乎链接,以及为什么要用batch_normalization知乎链接

Implement of D

相对于G,D就比较简单了,辨识器的主要任务是辨识当前的图片,是否是真的,所以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def discriminator(images, reuse=False):
alpha=0.2
with tf.variable_scope("discriminator",reuse=reuse):
conv_1 = tf.layers.conv2d(images, 64, 4, strides=2, padding='same')
conv_1_bn = tf.layers.batch_normalization(conv_1, training=True)
relu1 = tf.maximum(alpha * conv_1_bn, conv_1_bn)
# 14x14x64
conv_2 = tf.layers.conv2d(relu1, 128, 4, strides=2, padding='same')
conv_2_bn = tf.layers.batch_normalization(conv_2, training=True)
relu2 = tf.maximum(alpha * conv_2_bn, conv_2_bn)
# 7x7x128
conv_3 = tf.layers.conv2d(relu2, 256, 4, strides=2, padding='same')
conv_3_bn = tf.layers.batch_normalization(conv_3, training=True)
relu3 = tf.maximum(alpha * conv_3_bn, conv_3_bn)
# 4x4x256
flat = tf.reshape(relu3, (-1, 4*4*256))
logits = tf.layers.dense(flat, 1)
out = tf.sigmoid(logits)
return out, logits

其实我们只要对于传入的图片做一些卷积,卷积,卷积,再接一个全连接层,最后链到一个神经元,输出sigmoid。
对于“真实”的图片,我们认为它的label(即ground truth)为1,对于G生成的,我们认为它是0。
所以,训练的目的就是似的G于D的loss都收敛,当然了,我们主要还是要训练的是G,毕竟要靠它来产生假数据,而随着G的loss下降,D的loss势必会上升(因为G太强了,导致D无法准确辨识“真”“假”)

Implement of Loss

基于上述事实,我们来看看GAN模型的Loss是怎么回事

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def model_loss(input_real, input_z, out_channel_dim):
g_model = generator(input_z, out_channel_dim)
d_model_real, d_logits_real = discriminator(input_real)
d_model_fake, d_logits_fake = discriminator(g_model, reuse=True)
d_loss_real = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_real, labels=tf.ones_like(d_logits_real)*(1 - 0.1)))
d_loss_fake = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake, labels=tf.zeros_like(d_logits_fake)))
g_loss = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=d_logits_fake, labels=tf.ones_like(d_logits_fake)))
d_loss = d_loss_real + d_loss_fake
return d_loss, g_loss

这里比较复杂,我们一步一步拆解了看,整个函数有3个input,函数内部定义了9个变量。

  • 三个input
    • input_real:真实的图片
    • input_z:随机生成的100维数组
    • out_channel_dim:可以认为是图片的颜色维度,一般都是RGB,也就是3
  • 九个内部变量
    • g_model:G生成的图片tensorflow
    • d_model_real:对于真实图片,生成的sigmod数值
    • d_logits_real:对于真实图片,生成的logits
    • d_model_fake:对于G生成的假图片,最后的sigmod数值

      这里要注意reuse为True,因为对于真实图片和G生成的假图片而言,D的参数是同一套

    • d_logits_fake:对于G生成的假图片,生成的logits
    • d_loss_real:D上对真实图片的loss,这边引入了单边标签平滑,也就是标注真实数据的label为0.9,做交叉熵
    • d_loss_fake:D上对G生成图片的loss,因为是fake图片,所以标注为0,做交叉熵
    • g_loss:G的loss,我们需要让G生成的图片往真(1)的方向走,因此需要与1做交叉熵
    • d_loss:D的整体loss,也就是对真的图片的loss,加上,对G生成的假图片的loss

Implement of Optimization

1
2
3
4
5
6
7
8
9
10
11
def model_opt(d_loss, g_loss, learning_rate, beta1):
t_vars = tf.trainable_variables()
d_vars = [var for var in t_vars if var.name.startswith('discriminator')]
g_vars = [var for var in t_vars if var.name.startswith('generator')]
# Optimize
with tf.control_dependencies(tf.get_collection(tf.GraphKeys.UPDATE_OPS)):
d_train_opt = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(d_loss, var_list=d_vars)
g_train_opt = tf.train.AdamOptimizer(learning_rate, beta1=beta1).minimize(g_loss, var_list=g_vars)
return d_train_opt, g_train_opt

因为G和D的内部变量都是独立的,所以优化器需要针对不同的变量分别做优化,恰好,我们在建立模型的时候增加了前缀。

Implement of Train model

最后,就是整个训练过程了。

1
2
3
4
5
6
7
d_loss, g_loss = model_loss(input_real,input_z,out_channel_dim)
d_train_opt, g_train_opt = model_opt(d_loss,g_loss,lr,beta1)
batch_z = np.random.normal(0, 0.02, size=(batch_size, z_dim))
_ = sess.run(d_train_opt, feed_dict={input_real: batch_images,
input_z: batch_z, lr :learning_rate})
_ = sess.run(g_train_opt, feed_dict={input_real: batch_images,
input_z: batch_z, lr :learning_rate})

其中batch_z就是一个100维的随机输入向量。
为了看看训练的具体效果,我们设定每过100步,就输出一下生成的图片们,同时如果训练效果足够好了,那么我们就直接用G生成图片,结束本次训练。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if steps % 100 == 0 :
end = time.clock()
train_loss_d = d_loss.eval({input_real: batch_images, input_z: batch_z})
train_loss_g = g_loss.eval({input_z: batch_z})
print("Time: {:.4f}s ..Epoch {}/{}.setp:{}...".format((end - start),
epoch_i+1, epoch_count,steps),
"Discriminator Loss: {:.4f}...".format(train_loss_d),
"Generator Loss: {:.4f}...".format(train_loss_g),
"patience: {:2d}".format(patience))
start = time.clock()
if steps > 5000 and train_loss_g < train_loss_d and epoch_i > 5:
patience = patience - 1
if (patience == 0):
for i in range(int(20000/100)):
save_output(sess,100,input_z,out_channel_dim,
data_image_mode,output_path,output_prefix)
return
else:
patience = 10
if steps % 100 == 0:
show_generator_output(sess,4,input_z,out_channel_dim,data_image_mode)
if int(time.clock() - totoal_start) > 1800:
for i in range(20000/100):
save_output(sess,100,input_z,out_channel_dim,
data_image_mode,output_path,output_prefix)

步骤5,呵呵哒,请参考2-3,也就是针对4生成的图片继续做过滤

步骤6,自己玩

实验代码

目前已经上传到github,大家可以去拿下来玩:
https://github.com/Howie-hxu/blogs_demo

这部分简单解释一下:

  • 2-3.tesseract_recognize.ipynb:步骤2-3的过程,默认会根据datas中的文件,分类出labeled文件夹,其中子文件名就是标注的结果
  • 4.DCGAN_Model.ipynb:DCGAN的模型以及训练过程
  • datas.tar.gz:需要解压缩,里面包含了5000张未标注的0-9,每个数字各10张