Skip to content

Latest commit

 

History

History
723 lines (527 loc) · 38.1 KB

File metadata and controls

723 lines (527 loc) · 38.1 KB

十、深度学习视角下的验证码

术语验证码完全自动化的公共图灵测试的首字母缩写,用来区分计算机和人类。这是一个旨在区分人类用户和机器或机器人的计算机程序,通常作为防止垃圾邮件和数据滥用的安全措施。验证码的概念早在 1997 年就被引入,当时互联网搜索公司 AltaVista 试图阻止自动向平台提交网址,这扭曲了他们的搜索引擎算法。为了解决这个问题,AltaVista 的首席科学家 Andrei Broder 想出了一种算法,可以随机生成文本图像,这些图像很容易被人类识别,但不会被机器人识别。后来,在 2003 年,路易斯·冯·安恩、曼努埃尔·布卢姆、尼古拉斯·J·霍珀和约翰·兰福德完善了这项技术,并将其称为验证码。最常见的验证码形式需要用户识别失真图像中的字母和数字。这项测试的目的是希望人类能够很容易地区分扭曲图像中的字符,而自动程序或机器人将无法区分它们。验证码测试有时被称为反向图灵测试,因为它是由计算机而不是人类执行的。

直到最近,验证码已经开始发挥比防止机器人欺诈更大的作用。例如,谷歌在将《纽约时报》的档案和谷歌图书中的一些书籍数字化时,使用了验证码及其变体之一 reCAPTCHA。这通常是通过要求用户正确输入多个验证码的字符来完成的。实际上只有一个验证码被标记并用于验证用户是否是人类。

其余的验证码由用户标记。目前,谷歌使用基于图像的验证码来帮助标记其自动驾驶汽车数据集,如下图所示:

Figure 10.1: Some common CAPTCHAs on various websites

在本章中,我们将涵盖以下主题:

  • 什么是验证码
  • 使用深度学习破解验证码,暴露其漏洞
  • 利用对抗性学习生成验证码

技术要求

你将需要具备 Python 3、TensorFlow、Keras 和 OpenCV 的基本知识。

本章代码文件可在 GitHub: https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 10上找到

查看以下视频,查看正在运行的代码: http://bit.ly/2SgwR6P

用深度学习打破藩篱

随着卷积神经网络 ( CNNs )最近在计算机视觉任务中的成功,在几分钟内打破基本的验证码是一项相对容易的任务。因此,验证码需要比过去更加进化。在本章的第一部分,我们将通过深度学习框架,揭示使用僵尸工具自动检测基本验证码的漏洞。我们将通过利用 GAN 创建更难被机器人检测到的验证码来跟进此事。

生成基本验证码

验证码可以使用 Python 中的Claptcha包生成。我们用它来生成由数字和文本组成的四个字符的验证码图像。因此,每个字符可以是26字母和10数字中的任何一个。以下代码可用于生成随机选择字母和数字的验证码:

alphabets = 'abcdefghijklmnopqrstuvwxyz'
alphabets = alphabets.upper()
font = "/home/santanu/Android/Sdk/platforms/android-28/data/fonts/DancingScript-Regular.ttf"
# For each of the 4 characters determine randomly whether its a digit or alphabet
char_num_ind = list(np.random.randint(0,2,4))
text = ''
for ind in char_num_ind:
    if ind == 1:
    # for indicator 1 select character else number 
        loc = np.random.randint(0,26,1)
        text = text + alphabets[np.random.randint(0,26,1)[0]]

    else:
        text = text + str(np.random.randint(0,10,1)[0])

c = Claptcha(text,font)
text,image = c.image
plt.imshow(image)

下面的截图(图 10.2 )是前面代码生成的随机验证码:

Figure 10.2: Random CAPTCHA with the characters 26UR

除了文本之外,Claptcha工具还要求输入打印文本的字体。正如我们所看到的,它以水平轴上有点扭曲的线条的形式给图像添加了噪声。

为训练验证码生成数据

在本节中,我们将使用Claptcha工具生成几个验证码来训练一个 CNN 模型。CNN 模型将通过监督训练来学习识别验证码中的字符。我们将生成一个训练和验证集,用于训练 CNN 模型。除此之外,我们还将生成一个单独的测试集来评估它概括未知数据的能力。CaptchaGenerator.py脚本可以编码如下,生成验证码数据:

from claptcha import Claptcha
import os
import numpy as np
import cv2
import fire 
from elapsedtimer import ElasedTimer

def generate_captcha(outdir,font,num_captchas=20000):
    alphabets = 'abcdefghijklmnopqrstuvwxyz'
    alphabets = alphabets.upper()
    try:
        os.mkdir(outdir)
    except:
        'Directory already present,writing captchas to the same'
    #rint(char_num_ind)
    # select one alphabet if indicator 1 else number 
    for i in range(num_captchas):
        char_num_ind = list(np.random.randint(0,2,4))
        text = ''
        for ind in char_num_ind:
            if ind == 1:
                loc = np.random.randint(0,26,1)
                text = text + alphabets[np.random.randint(0,26,1)[0]]
            else:
                text = text + str(np.random.randint(0,10,1)[0])
        c = Claptcha(text,font)
        text,image = c.image
        image.save(outdir + text + '.png')

def main_process(outdir_train,num_captchas_train,
                 outdir_val,num_captchas_val,
                 outdir_test,num_captchas_test,
                font):

    generate_captcha(outdir_train,font,num_captchas_train)
    generate_captcha(outdir_val,font,num_captchas_val)
    generate_captcha(outdir_test,font,num_captchas_test)

if __name__ == '__main__':
    with ElasedTimer('main_process'):
        fire.Fire(main_process)

需要注意的一点是,大多数验证码生成器使用ttf文件来获取验证码的字体模式。

我们可以使用CaptchaGenerator.py脚本生成大小为1600040004000的训练集、验证集和测试集,如下所示:

python CaptchaGenerator.py --outdir_train '/home/santanu/Downloads/Captcha Generation/captcha_train/' --num_captchas_train 16000 --outdir_val '/home/santanu/Downloads/Captcha Generation/captcha_val/' --num_captchas_val 4000 
--outdir_test '/home/santanu/Downloads/Captcha Generation/captcha_test/' --num_captchas_test 4000 --font "/home/santanu/Android/Sdk/platforms/android-28/data/fonts/DancingScript-Regular.ttf"

脚本用3.328 mins生成16000训练验证码、4000验证验证码、4000测试验证码,从下面的脚本日志可以看到:

3.328 min: main_process

在下一节中,我们将讨论验证码破解者的卷积神经网络架构。

验证码破解 CNN 架构

我们将使用美国有线电视新闻网的架构来识别验证码中的字符。美国有线电视新闻网在密集层之前会有两对卷积和汇集。我们不是将整个验证码输入网络,而是将验证码分成四个字符,并分别输入模型。这需要 CNN 的最终输出层预测属于26字母和10数字的36类之一。

通过函数_model_,可以定义如下代码所示的模型:

def _model_(n_classes):
    # Build the neural network
    input_ = Input(shape=(40,25,1)) 

    # First convolutional layer with max pooling
    x = Conv2D(20, (5, 5), padding="same",activation="relu")(input_)
    x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2))(x)
    x = Dropout(0.2)(x)
    # Second convolutional layer with max pooling
    x = Conv2D(50, (5, 5), padding="same", activation="relu")(x)
    x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2))(x)
    x = Dropout(0.2)(x)
    # Hidden layer with 1024 nodes
    x = Flatten()(x)
    x = Dense(1024, activation="relu")(x)
    # Output layer with 36 nodes (one for each possible alphabet/digit we predict)
    out = Dense(n_classes,activation='softmax')(x)
    model = Model(inputs=[input_],outputs=out)

    model.compile(loss="sparse_categorical_crossentropy", optimizer="adam", metrics=
    ["accuracy"])
    return model 

验证码破解者 CNN 模型可以如下图所示进行图形化描述(图 10.3 ):

Figure 10.3: The CAPTCHA breaker CNN architecture

预处理验证码图像

图像的原始像素与美国有线电视新闻网的架构不太匹配。为了让 CNN 更快地收敛,对图像进行归一化总是一个好主意。通常用作归一化方案的两种方法是平均像素减法或通过将像素值除以255将像素缩放至位于[0,1]范围内。对于我们的美国有线电视新闻网,我们将正常化图像,使其位于[0,1]的范围内。我们还将处理验证码的灰度图像,这意味着我们将只处理一个颜色通道。load_img功能可用于加载和预处理验证码图片,如以下代码所示:

def load_img(path,dim=(100,40)):
    img = cv2.imread(path,cv2.IMREAD_GRAYSCALE)
    img = cv2.resize(img,dim)
    img = img.reshape((dim[1],dim[0],1))
    #print(img.shape)
    return img/255.

将验证码字符转换为类

验证码的原始字符需要转换成数字类来进行训练。create_dict_char_to_index功能可用于将原始字符转换为类别标签:

def create_dict_char_to_index():
    chars = 'abcdefghijklmnopqrstuvwxyz0123456789'.upper()
    chars = list(chars)
    index = np.arange(len(chars))
    char_to_index_dict,index_to_char_dict = {},{}
    for v,k in zip(index,chars):
        char_to_index_dict[k] = v 
        index_to_char_dict[v] = k 

    return char_to_index_dict,index_to_char_dict

数据生成程序

动态生成成批的训练和验证数据对于以高效的方式训练 CNN 是很重要的。在训练开始之前将所有数据加载到内存中可能会导致数据存储问题,因此在训练期间动态读取验证码和构建批处理是有意义的。这导致资源的最佳利用。

我们将使用一个数据生成器,它可以用于构建训练和验证批次。生成器将在初始化期间存储验证码文件位置,并在每个时期动态构建批处理。文件的顺序在每个之后被随机打乱,使得验证码图像在每个时期不以相同的顺序遍历。这通常可以确保模型在训练过程中不会陷入糟糕的局部极小值。数据生成器类可以编码如下:

class DataGenerator(keras.utils.Sequence):
    'Generates data for Keras'
    def __init__(self,dest,char_to_index_dict,batch_size=32,n_classes=36,dim=(40,100,1),shuffle=True):
        'Initialization'
        self.dest = dest
        self.files = os.listdir(self.dest)
        self.char_to_index_dict = char_to_index_dict
        self.batch_size = batch_size
        self.n_classes = n_classes
        self.dim = (40,100)
        self.shuffle = shuffle
        self.on_epoch_end()

    def __len__(self):
        'Denotes the number of batches per epoch'
        return int(np.floor(len(self.files) / self.batch_size))

    def __getitem__(self, index):
        'Generate one batch of data'
        # Generate indexes of the batch
        indexes = self.indexes[index*self.batch_size:(index+1)*self.batch_size]

        # Find list of files to be processed in the batch
        list_files = [self.files[k] for k in indexes]

        # Generate data
        X, y = self.__data_generation(list_files)

        return X, y

    def on_epoch_end(self):
        'Updates indexes after each epoch'
        self.indexes = np.arange(len(self.files))
        if self.shuffle == True:
            np.random.shuffle(self.indexes)

    def __data_generation(self,list_files):
        'Generates data containing batch_size samples' # X : 
         (n_samples, *dim, n_channels)
        # Initialization
        dim_h = dim[0]
        dim_w = dim[1]//4
        channels = dim[2]
        X = np.empty((4*len(list_files),dim_h,dim_w,channels))
        y = np.empty((4*len(list_files)),dtype=int)
       # print(X.shape,y.shape)

        # Generate data
        k = -1
        for f in list_files:
            target = list(f.split('.')[0])
            target = [self.char_to_index_dict[c] for c in target]
            img = load_img(self.dest + f)
            img_h,img_w = img.shape[0],img.shape[1]
            crop_w = img.shape[1]//4
            for i in range(4):
                img_crop = img[:,i*crop_w:(i+1)*crop_w]
                k+=1
                X[k,] = img_crop
                y[k] = int(target[i])

        return X,y

训练验证码破解者

验证码破解模型可以通过调用train函数进行训练,如下所示:

def train(dest_train,dest_val,outdir,batch_size,n_classes,dim,shuffle,epochs,lr):
    char_to_index_dict,index_to_char_dict = create_dict_char_to_index()
    model = _model_(n_classes)
    train_generator =  DataGenerator(dest_train,char_to_index_dict,batch_size,n_classes,dim,shuffle)
    val_generator =  DataGenerator(dest_val,char_to_index_dict,batch_size,n_classes,dim,shuffle)
    model.fit_generator(train_generator,epochs=epochs,validation_data=val_generator)
    model.save(outdir + 'captcha_breaker.h5')

对于批处理中的验证码,所有四个字符都被考虑用于训练。我们使用DataGenerator类定义train_generatorval_generator对象。这些数据生成器动态地为训练和验证提供批处理。

可以通过运行带有如下train参数的captcha_solver.py脚本来调用训练:

python captcha_solver.py train --dest_train '/home/santanu/Downloads/Captcha Generation/captcha_train/' --dest_val '/home/santanu/Downloads/Captcha Generation/captcha_val/' --outdir '/home/santanu/ML_DS_Catalog-/captcha/model/' --batch_size 16 --lr 1e-3 --epochs 20 --n_classes 36 --shuffle True --dim '(40,100,1)'

仅在20训练阶段,该模型就实现了验证码每个字符级别约 98.3%的验证准确率,如以下输出日志所示:

Epoch 17/20
1954/1954 [==============================] - 14s 7ms/step - loss: 0.0340 - acc: 0.9896 - val_loss: 0.0781 - val_acc: 0.9835
Epoch 18/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0310 - acc: 0.9904 - val_loss: 0.0679 - val_acc: 0.9851
Epoch 19/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0315 - acc: 0.9904 - val_loss: 0.0813 - val_acc: 0.9822
Epoch 20/20
1954/1954 [==============================] - 13s 7ms/step - loss: 0.0297 - acc: 0.9910 - val_loss: 0.0824 - val_acc: 0.9832
4.412 min: captcha_solver

The training time for 20 epochs with roughly 16000 98.3s (that is, 64000 CAPTCHA characters) is around 4.412 min using a GeForce GTX 1070 GPU. Readers are advised to use a GPU based machine for faster training.

测试数据集的准确性

测试数据的推断可以通过调用evaluate函数来运行。evaluate功能说明如下,供参考。请注意,评估应该从整体验证码的角度来看准确性,而不是验证码的字符级别。因此,只有当验证码目标的所有四个字符都与预测匹配时,我们才能将验证码标记为被美国有线电视新闻网正确识别。

在测试验证码上运行推理的evaluate功能可以编码如下:

def evaluate(model_path,eval_dest,outdir,fetch_target=True):
    char_to_index_dict,index_to_char_dict = create_dict_char_to_index()
    files = os.listdir(eval_dest)
    model = keras.models.load_model(model_path)
    predictions,targets = [],[]

    for f in files:
        if fetch_target == True:
            target = list(f.split('.')[0])
            targets.append(target)

        pred = []
        img = load_img(eval_dest + f)
        img_h,img_w = img.shape[0],img.shape[1]
        crop_w = img.shape[1]//4
        for i in range(4):
            img_crop = img[:,i*crop_w:(i+1)*crop_w]
            img_crop = img_crop[np.newaxis,:]
            pred_index  = np.argmax(model.predict(img_crop),axis=1)
            #print(pred_index)
            pred_char   = index_to_char_dict[pred_index[0]]
            pred.append(pred_char)
        predictions.append(pred)

    df = pd.DataFrame()
    df['files'] = files
    df['predictions'] = predictions

    if fetch_target == True:
        match = []

        df['targets'] = targets

        accuracy_count = 0 
        for i in range(len(files)):
            if targets[i] == predictions[i]:
                accuracy_count+= 1
                match.append(1)
            else:
                match.append(0)
        print(f'Accuracy: {accuracy_count/float(len(files))} ')

        eval_file = outdir + 'evaluation.csv'
        df['match'] = match
        df.to_csv(eval_file,index=False)
        print(f'Evaluation file written at: {eval_file} ')

可以运行以下命令来调用captcha_solver.py脚本的evaluate功能进行推理:

python captcha_solver.py evaluate  --model_path  /home/santanu/ML_DS_Catalog-/captcha/model/captcha_breaker.h5 --eval_dest '/home/santanu/Downloads/Captcha Generation/captcha_test/' --outdir /home/santanu/ML_DS_Catalog-/captcha/ --fetch_target True 

4000验证码测试数据集上实现的准确率在 93%左右。运行evaluate功能的输出如下:

Accuracy: 0.9320972187421699 
Evaluation file written at: /home/santanu/ML_DS_Catalog-/captcha/evaluation.csv 
13.564 s: captcha_solver

我们还可以看到对那些4000验证码的推断花费了大约 14 秒,评估的输出被写入/home/santanu/ML_DS_Catalog-/captcha/evaluation.csv文件。

我们将在下面的截图中查看模型没有做好的一些目标和预测(图 10.4 ):

Figure 10.4: CAPTCHAs where the CAPTCHA solver model failed

通过对抗性学习生成验证码

在本节中,我们将通过生成性对抗网络创建验证码。我们将生成类似于街景门牌号数据集( SVHN 数据集)中的图像。这个想法是使用这些 GAN 生成的图像作为验证码。只有当我们训练了氮化镓,它们才容易从噪声分布中采样。这将减轻通过更复杂的方法创建验证码的需要。它还将为验证码中使用的 SVHN 街道号码提供一些变化。

SVHN 是一个真实世界的数据集,由于其在对象识别算法中的应用,在机器学习和深度学习领域非常受欢迎。顾名思义,该数据集包含从谷歌街景图像获得的门牌号真实图像。数据集可从以下链接下载:http://ufldl.stanford.edu/housenumbers/

我们将使用调整后的门牌号数据集,其中图像已经调整到尺寸(32,32)。我们感兴趣的数据集是train_32x32.mat

通过这个生成对抗网络 ( GAN )我们将从随机噪声中生成门牌号图像,生成的图像将非常类似于 SVHN 数据集中的图像。

简单回顾一下,在一个 GAN 中,我们有一个生成器( G )和一个鉴别器( D ),它们根据损失函数相互进行零和极小极大游戏。随着时间的推移,发生器和鉴别器都在工作中变得更好,直到我们达到一个稳定的点,两者都不能进一步改善。这个稳定点是损失函数的鞍点。对于我们的应用,发电机 G 将把噪声 z 从给定的分布 P(z) 转换成门牌号图像 x ,使得 x = G(z)

该生成的图像通过鉴别器 D ,该鉴别器试图将该生成的图像 x 检测为假的,并且将来自 SVHN 数据集的真实门牌号图像检测为真实的。同时,生成器将尝试创建图像 x = G(z) ,使得鉴别器发现图像是真实的。如果我们将真实图像标记为1,将生成器生成的假图像标记为0,那么鉴别器将尝试最小化二进制交叉熵损失,作为给定两个类别的分类器网络。鉴频器 D 最小化的损耗可以写成:

在前面的表达式 *D(。)*是鉴别器函数,其输出表示将图像标记为真实的概率。 P z (z) 表示随机变量噪声 *z、*的分布,而 P X (x) 表示真实门牌号图像的分布。 *G(。)*和 *D(。)*分别表示发生器网络功能和鉴别器网络功能。这些将由网络的权重参数化,我们已经方便地跳过了杂乱的符号。如果我们用表示发电机网络权重的参数,用表示鉴别器网络的参数,那么鉴别器将学会相对于最小化 (1) 中的损耗,而发电机将致力于相对于最大化 (1) 中的相同损耗。我们可以将 (1) 中优化的损耗称为发生器和鉴别器都在优化其参数的效用函数。效用函数 U 可以写成发生器和鉴别器参数的函数,如下所示:

从博弈论的角度来看,生成器 G 和鉴别器 D 用效用函数相互进行零和极小极大博弈,极小极大博弈的优化问题可以表示为:

在参数空间中的一个点上,如果一个函数对于某些参数是局部最大值,对于其余参数是局部最小值,那么这个点被称为鞍点。因此,给出的点将是效用函数的鞍点。这个鞍点是极小极大零和博弈的纳什均衡,并且参数对于生成器和鉴别器正在优化的效用来说是最优的。就眼前的问题而言,生成器 G 会产生最难识别的验证码,以为参数进行检测。同样,鉴别器最适合检测以为参数的假验证码。

有鞍点的函数中最简单的是 x 2 - y 2 ,鞍点就是原点:(0,0)

优化氮化镓损耗

在前一节中,我们已经看到,发生器和鉴别器相对于其各自网络参数的最佳状态由下式给出:

为了使目标函数最大化,我们通常使用梯度上升,而为了使成本函数最小化,我们使用梯度下降。前面的优化问题可以分解为两部分:生成器和鉴别器分别通过梯度上升和梯度下降轮流优化效用函数。在优化期间的任何步骤 t 中,鉴别器将通过如下最小化效用来尝试移动到新的状态:

或者,发电机将尝试最大化相同的效用。由于鉴别器 D 没有发电机的任何参数,效用的第二项不影响发电机的优化。同样的情况可以表述如下:

我们已经将生成器和鉴别器优化目标转换为最小化问题。鉴别器和生成器的优化使用梯度下降执行,直到我们到达目标函数的鞍点。

发电机网络

生成器网络将接收随机噪声,并尝试输出类似于 SVHN 图像的图像作为输出。随机噪声是一个100维输入向量。每个维度都是遵循标准正态分布的随机变量,平均值为0,标准差为1

最初的致密层有8192个单位,被重塑为一个 4×4×512 形状的三维张量。张量可以被认为是带有512滤镜的 4×4 图像。为了增加张量的空间维度,我们进行了一系列的转置 2D 卷积,步长为2,核滤波器大小为 5×5。步幅大小决定转置卷积的缩放比例。例如,2 倍于输入图像的每个空间维度的步幅,随后是转置卷积,通常伴随着批量归一化,以便更好地收敛。除激活层外,网络使用LeakyReLU作为激活功能。网络的最终输出是 32 x 32 x 3 维的图像。

在最终层中使用tanh激活,以便在[-1,1]范围内归一化图像像素值。

发生器可以编码如下:

def generator(input_dim,alpha=0.2):
    model = Sequential()
    model.add(Dense(input_dim=input_dim, output_dim=4*4*512))
    model.add(Reshape(target_shape=(4,4,512)))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha))
    model.add(Conv2DTranspose(256, kernel_size=5, strides=2,
                              padding='same'))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha))
    model.add(Conv2DTranspose(128, kernel_size=5, strides=2, 
                              padding='same')) 
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha))
    model.add(Conv2DTranspose(3, kernel_size=5, strides=2,
                              padding='same')) 
    model.add(Activation('tanh'))
    return model

发电机的网络架构如下图所示(图 10.5 )供参考:

Figure 10.5: Generator network graph

鉴别网络

鉴别器将是一个经典的二进制分类卷积神经网络,它可以将生成器图像分类为假图像,并将实际的 SVHN 数据集图像分类为真实图像。

鉴别器网络可以编码如下:

def discriminator(img_dim,alpha=0.2):
    model = Sequential()
    model.add(
            Conv2D(64, kernel_size=5,strides=2,
            padding='same',
            input_shape=img_dim)
            )
    model.add(LeakyReLU(alpha))
    model.add(Conv2D(128,kernel_size=5,strides=2,padding='same'))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha))
    model.add(Conv2D(256,kernel_size=5,strides=2,padding='same'))
    model.add(BatchNormalization())
    model.add(LeakyReLU(alpha))
    model.add(Flatten())
    model.add(Dense(1))
    model.add(Activation('sigmoid'))
    return model

前一个代码块中定义的鉴别器网络将伪生成器图像和真实的 SVHN 图像作为输入,并在最终输出层之前通过3组 2D 卷积。该网络中的卷积之后不是池化,而是批量标准化和LeakyReLU激活。

鉴别器的网络架构如下图所示(图 10.6 ):

Figure 10.6: Discriminator network graph

鉴别器的输出激活函数是一个 sigmoid。这有助于从真实的 SVHN 图像中对伪生成图像进行二进制分类。

训练氮化镓

为生成性对抗网络建立训练流程并不简单,因为它需要大量的技术考虑。我们定义了如下三个培训网络:

  • 发电机网络g带参数
  • 带参数的鉴别器网络d
  • 由带有权重g_d表示的组合发生器鉴别器网络

生成器g创建假图像,d鉴别器会评估这些图像,并试图将其标记为假图像。

g_d网络中,g生成器创建假图像,然后试图欺骗d鉴别器相信它们是真实的。鉴别器网络是用二进制交叉熵损失编译的,并且损失是相对于鉴别器参数优化的,而g_d网络是相对于g发生器的参数编译的,以便欺骗鉴别器。因此,g_d网络损失是与鉴别器将所有假图像标记为真实图像相关的二进制交叉熵损失。在每个小批量中,发生器和鉴别器权重根据与g_dd网络相关的损耗优化进行更新:

def train(dest_train,outdir,
        gen_input_dim,gen_lr,gen_beta1,
        dis_input_dim,dis_lr,dis_beta1,
        epochs,batch_size,alpha=0.2,smooth_coef=0.1):

    #X_train,X_test = read_data(dest_train),read_data(dest_test)
    train_data = loadmat(dest_train + 'train_32x32.mat')
    X_train, y_train = train_data['X'], train_data['y']
    X_train = np.rollaxis(X_train, 3) 
    print(X_train.shape)
    #Image pixels are normalized between -1 to +1 so that one can use the tanh activation function
    #_train = (X_train.astype(np.float32) - 127.5)/127.5
    X_train = (X_train/255)*2-1
    g = generator(gen_input_dim,alpha)
    plot_model(g,show_shapes=True, to_file='generator_model.png')
    d = discriminator(dis_input_dim,alpha)
    d_optim = Adam(lr=dis_lr,beta_1=dis_beta1)
    d.compile(loss='binary_crossentropy',optimizer=d_optim)
    plot_model(d,show_shapes=True, to_file='discriminator_model.png')
    g_d = generator_discriminator(g, d)
    g_optim = Adam(lr=gen_lr,beta_1=gen_beta1)
    g_d.compile(loss='binary_crossentropy', optimizer=g_optim)
    plot_model(g_d,show_shapes=True, to_file=
    'generator_discriminator_model.png')
    for epoch in range(epochs):
        print("Epoch is", epoch)
        print("Number of batches", int(X_train.shape[0]/batch_size))
        for index in range(int(X_train.shape[0]/batch_size)):
            noise = 
            np.random.normal(loc=0, scale=1, size=(batch_size,gen_input_dim))
            image_batch = X_train[index*batch_size:(index+1)*batch_size,:]
            generated_images = g.predict(noise, verbose=0)
            if index % 20 == 0:
                combine_images(generated_images,outdir,epoch,index)
                # Images converted back to be within 0 to 255 

            print(image_batch.shape,generated_images.shape)
            X = np.concatenate((image_batch, generated_images))
            d1 = d.train_on_batch(image_batch,[1 - smooth_coef]*batch_size)
            d2 = d.train_on_batch(generated_images,[0]*batch_size)

            y = [1] * batch_size + [0] * batch_size
            # Train the Discriminator on both real and fake images 
            make_trainable(d,True)
            #_loss = d.train_on_batch(X, y)
            d_loss = d1 + d2

            print("batch %d d_loss : %f" % (index, d_loss))
            noise = 
            np.random.normal(loc=0, scale=1, size=(batch_size,gen_input_dim))
            make_trainable(d,False)
            #d.trainable = False
            # Train the generator on fake images from Noise 
            g_loss = g_d.train_on_batch(noise, [1] * batch_size)
            print("batch %d g_loss : %f" % (index, g_loss))
            if index % 10 == 9:
                g.save_weights('generator', True)
                d.save_weights('discriminator', True)

Adam优化器用于两个网络的优化。需要注意的一点是,需要编译网络g_d来优化发电机参数的损耗 G 。因此,我们需要在网络g_d中禁用鉴别器 D 参数的训练。

我们可以使用以下功能来禁用或启用网络参数的学习:

def make_trainable(model, trainable):
    for layer in model.layers:
        layer.trainable = trainable

我们可以通过将可训练设置为False来禁用参数的学习,而如果我们想要启用这些参数的训练,我们需要将其设置为True

噪声分布

输入到 GAN 的噪声需要遵循特定的概率分布。一般均匀分布U[-1,1]或标准正态分布即均值为0且标准差为1的正态分布用于对噪声向量的每个维度进行采样。从经验上看,从标准正态分布中采样噪声似乎比从均匀分布中采样效果更好。在这个实现中,我们将使用标准正态分布对随机噪声进行采样。

数据预处理

如前所述,我们将使用维度32 x 32 x 3的 SVHN 数据集图像。

数据集图像很容易以矩阵数据形式获得。图像的原始像素在[-1,1]范围内归一化,以实现更快、更稳定的收敛。由于这种变换,发生器的最终激活被保持tanh以确保生成的图像具有在[-1,1]内的像素值。

read_data可用于处理输入数据。dir_flag用于确定我们是否有原始处理的数据矩阵文件或图像目录。例如,当我们使用 SVHN 数据集时,dir_flag应该设置为False,因为我们已经有了一个名为train_32x32.mat的预处理数据矩阵文件。

然而,最好保持read_data函数是通用的,因为这允许我们为其他数据集重用脚本。来自scipy.ioloadmat功能可用于读取train_32x32.mat

如果输入的是放在目录中的原始图像,那么我们可以读取目录中可用的图像文件,并通过opencv读取它们。使用opencv可以使用load_img功能读取原始图像。

最后,为了更好地收敛网络,像素强度被归一化到[-1,1]的范围内:

def load_img(path,dim=(32,32)):

    img = cv2.imread(path)
    img = cv2.resize(img,dim)
    img = img.reshape((dim[1],dim[0],3))
    return img

def read_data(dest,dir_flag=False):

 if dir_flag == True:
 files = os.listdir(dest)
 X = []
 for f in files:
 img = load_img(dest + f)
 X.append(img)
 return X
 else:
 train_data = loadmat(path)
 X,y = train_data['X'], train_data['y']
 X = np.rollaxis(X,3) 
 X = (X/255)*2-1
 return X

调用培训

可以通过运行captcha_gan.py脚本的train功能调用 GAN 的训练,参数如下:

python captcha_gan.py train --dest_train '/home/santanu/Downloads/train_32x32.mat' --outdir '/home/santanu/ML_DS_Catalog-/captcha/SVHN/' --dir_flag False --batch_size 100 --gen_input_dim 100 --gen_beta1 0.5 --gen_lr 0.0001 --dis_input_dim '(32,32,3)' --dis_lr 0.001 --dis_beta1 0.5 --alpha 0.2 --epochs 100 --smooth_coef 0.1

前面的脚本使用fire Python 包来调用用户指定的函数,在我们的例子中是trainfire的好处在于,用户可以将函数的所有输入作为参数提供,正如我们从前面的命令中看到的那样。

众所周知,GANs 很难训练,因此需要调整这些参数,以便模型正常运行。以下是一些重要的参数:

| 参数 | | comment | | batch_size | 100 | 小批量随机梯度下降的批量。 | | gen_input_dim | 100 | 输入随机噪声向量维数。 | | gen_lr | 0.0001 | 发电机学习率。 | | gen_beta1 | 0.5 | beta_1是生成器的 Adam 优化器的参数。 | | dis_input_dim | (32,32,3) | 真假门牌号图像的形状鉴别器。 | | dis_lr | 0.001 | 鉴别器网络的学习速率。 | | dis_beta1 | 0.5 | beta_1是用于鉴别器的 Adam 优化器的参数。 | | alpha | 0.2 | 这是LeakyReLU激活的泄漏因子。当activation函数的输入为负时,这有助于提供梯度(此处为0.2)。它有助于解决垂死的ReLU问题。如果输入小于或等于0,ReLU 函数的输出相对于其输入的梯度为0。来自后面层的反向传播误差乘以这个0,没有误差传递到前面的层,尽管与这个ReLU.相关的神经元据说已经死亡,许多这样的死亡ReLUs会影响训练。LeakyReLU通过即使对于负的输入值也提供小的梯度来克服这个问题,从而确保训练不会由于缺乏梯度而停止。 | | epochs | 100 | 这是要运行的纪元数。 | | smooth_coef | 0.1 | 这个平滑系数的设计是为了减少真实样本对鉴别器的损失。例如,0.1smooth_coef会将归因于真实图像的损失减少到原始损失的 90%。这有助于 GANs 更好地收敛。 |

Training the GAN with these parameters takes around 3.12 hours, using a GeForce GTX 1070 GPU. Readers are advised to use a GPU for faster training.

培训期间验证码的质量

现在让我们研究一下在训练过程中不同时期生成的验证码的质量。以下为历元5(见图 10.7a )、历元51(见图 10.7b )和历元100(见图 10.7c 后的 CAPTCHAs 图像。我们可以看到验证码图像的质量随着训练的进行而提高。以下屏幕截图显示了在第 5 个时期生成的样本验证码的结果:

Figure 10.7a: Sample CAPTCHAs generated at epoch 5

以下屏幕截图显示了在第 51 个时期生成的样本验证码的结果:

Figure 10.7b: Sample CAPTCHAs generated at epoch 51

以下屏幕截图显示了在纪元 100 生成的样本验证码的结果:

Figure 10.7c: Sample CAPTCHAs generated at epoch 100

使用经过训练的生成器创建验证码以供使用

经过训练的 GAN 网络可以在运行时加载,以生成像验证码一样的街景房屋号码供使用。generate_captcha功能可以用来生成验证码使用,如下图所示:

def generate_captcha(gen_input_dim,alpha,
             num_images,model_dir,outdir):

    g = generator(gen_input_dim,alpha)
    g.load_weights(model_dir + 'generator')
    noise = 
    np.random.normal(loc=0, scale=1, size=(num_images,gen_input_dim))
    generated_images = g.predict(noise, verbose=1)
    for i in range(num_images):
        img = generated_images[i,:]
        img = np.uint8(((img+1)/2)*255)
        img = Image.fromarray(img)
        img.save(outdir + 'captcha_' + str(i) + '.png') 

您可能想知道如何为这些生成的验证码创建标签,因为它们需要验证用户是人类还是机器人。这个想法很简单:发送未标记的验证码和一些标记的验证码,这样用户就不知道哪个验证码会被评估。一旦你有足够的标签生成验证码,把大多数标签作为实际标签,并使用它来评估。

通过调用以下命令,可以从captcha_gan.py脚本中调用generate_captcha功能:

python captcha_gan.py generate-captcha --gen_input_dim 100 --num_images 200 --model_dir '/home/santanu/ML_DS_Catalog-/captcha/' --outdir '/home/santanu/ML_DS_Catalog-/captcha/captcha_for_use/' --alpha 0.2

下面的截图(图 10.8 )描述了通过调用generate_captcha函数生成的一些验证码。我们可以看到这些图像足够体面,可以用作验证码:

Figure 10.8: Generated CAPTCHAs using the generator of the trained GAN network

摘要

至此,我们结束了这一章。与本章相关的所有代码都可以在 GitHub 链接中找到:https://GitHub . com/PacktPublishing/Intelligent-Projects-use-Python/tree/master/chapter 10。你现在会对深度学习如何影响验证码有一个公平的想法。在光谱的一端,我们可以看到带有深度学习人工智能应用程序的机器人可以多么容易地解决验证码。然而,在另一端,我们看到深度学习可以如何用于利用给定的数据集并从随机噪声中创建新的验证码。您可以利用深度学习,扩展本章中关于生成性对抗网络的技术知识,构建智能验证码生成系统。现在,我们来到这本书的结尾。我希望这九个基于人工智能的实际应用的旅程是丰富的。祝你一切顺利!