天天看點

SimGAN-Captcha代碼閱讀與複現

項目介紹

項目位址:戳這裡大概的講一下這個項目的起因是大神要參加HackMIT,需要他們在15000張驗證碼中識别出10000張或者每個字元的識别準确率要到90%。然後他不想标注資料(就是這麼任性~)。于是決定先自己生成一批驗證碼(synthesizer合成器),然後把這些驗證碼用一個refiner(GAN)去對這批合成的驗證碼做一些調整讓它們看起來和真實的訓練樣本的樣式差不多。這樣他就相當于有了一批标注好的驗證碼,用這部分的标注驗證碼去訓練一個分類器,然後對需要hack的15000張圖檔做分類。借鑒的paper是Apple在2016年發的,戳這裡。但是呢,他發現他的這批資料訓練出來的模型對真實樣本的準确率隻有55%,于是他讓一個同學标注了4000張要hack的圖檔(這個同學原本打算标注10000張),最後開開心心的一張圖檔都沒标注的有了參加這個比賽的資格。 下面如果不想關注paper細節可以跳過這部分,直接到項目代碼這一塊就可以。

Overview

下圖是paper中的總體結構。paper中是要合成和訓練集相似的眼睛圖檔。

SimGAN-Captcha代碼閱讀與複現

Overview.jpg 模拟器先合成一些圖檔(Synthetic),然後用一個Refiner對這個圖檔進行refine(改善,調整),再用一個判别器(discriminator)去判别refine之後的圖檔和真實的但沒有标注的圖檔。目标是讓判别器沒有辦法區分真實圖檔和refine出來的圖檔。那麼我們就可以用模拟器生成一批有标注的資料,然後用refiner去進行修正,得到的圖檔就和原始的訓練資料集很相近了。

Objective

這裡簡要的概述一下模型需要用到的損失函數。Simulated+Unsupervised learning要用一些沒有标注的的真實圖檔Y來學習一個Refiner,這個Refiner進一步用來refine我們的合成圖檔X。關鍵點是需要讓合成的圖檔x'看起來和真實的圖檔差不多,并且還要保留标注的資訊。比如你要讓你的合成圖檔的紋理和真實圖檔的紋理是一樣的,同時你不能丢失合成圖檔的内容資訊(realism)(驗證碼上面的數字字母)。是以有兩個loss需要Refiner去優化:

SimGAN-Captcha代碼閱讀與複現
SimGAN-Captcha代碼閱讀與複現

上圖中的l_real指的是refine之後的合成圖檔(x_i')和真實圖檔Y之間的loss。l_reg是原始合成圖檔x_i和被refine之後的合成圖檔的x_i'之間的loss。lambda是一個高參。 Refiner的目标就是盡可能的糊弄判别器D,讓判别器沒有辦法區分一個圖檔是real還是合成的。判别器D的目标正好相反,是盡可能的能夠區分出來。那麼判别器的loss是這樣的:

SimGAN-Captcha代碼閱讀與複現

這個是一個二分類的交叉熵,D(.)是輸入圖檔是合成圖檔的機率,1-D(.)就是輸入圖檔是真實圖檔的機率。換句話說,如果輸入的圖檔是合成圖檔,那麼loss就是前半部分,如果輸入是真實圖檔,loss就是後半部分。在實作的細節裡面,當輸入是合成圖檔x_i那麼label就是1,反之為0。并且每個mini-batch當中,我們會随機采樣一部分的真實圖檔和一部分的合成圖檔。模型方面用了ConvNet,最後一層輸出是sample是合成圖檔的機率。最後用SGD來更新參數。(這裡的判别器就是用了一個卷積網絡,然後加了一個binary_categorical_crossentropy,再用SGD降低loss)。 那麼和判别器目标相反,refiner應該是迫使判别器沒有辦法區分refine之後的合成圖檔。是以它的l_real是醬紫的:

接下來是l_reg, 為了保留原始圖檔的内容資訊,我們需要一個loss來迫使模型不要把圖檔修改的和原始圖檔差異很大,這裡引入了self-regularization loss。這個loss就是讓refine之後的圖檔像素點和原始的圖檔的像素點之間的差不要太大。 綜合起來refiner的loss如下:

在訓練過程中,我們分别減小refiner和discriminator的loss。在更新refiner的時候就把discriminator的參數固定住不更新,在更新discriminator的參數的時候就固定refiner的參數。 這裡有兩個tricks。

  1. local adversarial lossrefiner在學習為真實圖檔模組化的時候不應該引入artifacts, 當我們訓練一個強判别器的時候,refiner會傾向于強調一些圖檔特征來fool目前的判别器,進而導緻生成了一些artifacts。那麼怎麼解決呢?我可以可以觀察到如果我們從refine的合成圖檔上挖出一塊(patch),這一塊的統計資訊(statistics)應該是和真實圖檔的統計資訊應該是相似的。是以,我們可以不用定義一個全局的判别器(對整張圖檔判斷合成Or真實),我們可以對圖檔上的每一塊都判别一下。這樣的話,不僅僅是限定了接收域(receptive field),也為訓練判别器提供了更多的樣本。判别器是一個全卷積網絡,它的輸出是w*h個patches是合成圖檔的機率。是以在更新refiner的時候,我們可以把這些w*h個patches的交叉熵loss相加。

比如上面這張圖,輸出就是2*3的矩陣,每個值表示的是這塊patch是合成圖檔的機率值。算loss的時候把這6塊圖檔的交叉熵都加起來。 2.用refined的曆史圖檔來更新判别器對抗訓練的一個問題是判别器隻關注最近的refined圖檔,這會引起兩個問題-對抗訓練的分散和refiner網絡又引進了判别器早就忘掉的artifacts。是以通過用refined的曆史圖檔作為一個buffer而不單單是目前的mini-batch來更新分類器。具體方法是,在每一輪分類器的訓練中,我們先從目前的batch中采樣b/2張圖檔,然後從大小為B的buffer中采樣b/2張圖檔,合在一起來更新判别器的參數。然後這一輪之後,用新生成的b/2張圖檔來替換掉B中的b/2張圖檔。

參數細節

實作細節:Refiner:輸入圖檔55*35=> 64個3*3的filter => 4個resnet block => 1個1*1的fitler => 輸出作為合成的圖檔(黑白的,是以1個通道)1個resnet block是醬紫的:

Discriminator:96個3*3filter, stride=2 => 64個3*3filter, stride = 2 => max_pool: 3*3, stride=1 => 32個3*3filter,stride=1 => 32個1*1的filter, stride=1 => 2個1*1的filter, stride=1 => softmax 我們的網絡都是全卷積網絡的,Refiner和Disriminator的最後層是很相似的(refiner的輸出是和原圖一樣大小的, discriminator要把原圖縮一下變成比如W/4 * H/4來表示這麼多個patch的機率值)。 首先隻用self-regularization loss來訓練Refiner網絡1000步, 然後訓練Discriminator 200步。接着每次更新一次判别器,我們都更新Refiner兩次。 算法具體細節如下:

項目代碼Overview

challenges:需要預測的資料樣本檔案夾imgs: 從challenges解壓之後的圖檔檔案夾SimGAN-Captcha.ipynb: 整個項目的流程notebookarial-extra.otf: 模拟器生成驗證碼的字型類型avg.png: 比賽主辦方根據每個人的資訊做了一些加密生成的一些線條,訓練的時候需要去掉這些線條。image_history_buffer.py:

預處理

這部分原本作者是寫了需要從某個位址把圖檔對應的base64加密的圖檔下載下傳下來,但是因為這個是去年的比賽,url已經不管用了。是以作者把對應的檔案直接放到了challenges裡面。我們直接從第二步解壓開始就可以了。因為python2和python3不太一樣,作者應該用的是Python2, 我這裡給出python3版本的代碼。

解壓

每個challenges檔案下下的檔案都是一個json檔案,包含了1000個base64加密的jpg圖檔檔案,是以對每一個檔案,我們把base64的str解壓成一個jpeg,然後把他們放到orig檔案夾下。

import requests
import threading
URL = "https://captcha.delorean.codes/u/rickyhan/challenge"
DIR = "challenges/"
NUM_CHALLENGES = 
lock = threading.Lock()

import json, base64, os
IMG_DIR = "./orig"
fnames = ["{}/challenge-{}".format(DIR, i) for i in range(NUM_CHALLENGES)]
if not os.path.exists(IMG_DIR):
    os.mkdir(IMG_DIR)
def save_imgs(fname):
    with open(fname,'r') as f:
        l = json.loads(f.read(), encoding="latin-1")
    for image in l['images']:
        byte_image = bytes(map(ord,image['jpg_base64']))
        b = base64.decodebytes(byte_image)
        name = image['name']
        with open(IMG_DIR+"/{}.jpg".format(name), 'wb') as f:
            f.write(b)

for fname in fnames:
    save_imgs(fname)
assert len(os.listdir(IMG_DIR)) ==  * NUM_CHALLENGES
                

解壓之後的圖檔長這個樣子:

from PIL import Image
imgpath = IMG_DIR + "/"+ os.listdir(IMG_DIR)[]
imgpath2 = IMG_DIR + "/"+ os.listdir(IMG_DIR)[]
im = Image.open(example_image_path)
im2 = Image.open(example_image_path2)
IMG_FNAMES = [IMG_DIR + '/' + p for p in os.listdir(IMG_DIR)]
                
im
           
SimGAN-Captcha代碼閱讀與複現
img2
           
SimGAN-Captcha代碼閱讀與複現

轉換成黑白圖檔

二值圖會節省很大的計算,是以我們這裡設定了一個門檻值,然後把圖檔一張張轉換成相應的二值圖。(這裡采用的轉換方式見下面的注釋。)

def gray(img_path):
    # convert to grayscale, then binarize
    #L = R * 299/1000 + G * 587/1000 + B * 114/1000
    img = Image.open(img_path).convert("L") # convert to gray scale, one 8-bit byte per pixel
    img = img.point(lambda x:  if x >  or x ==  else x) # value found through T&E
    img = img.point(lambda x:  if x <  else , "1") # convert to binary image
    img.save(img_path)

for img_path in IMG_FNAMES:
    gray(img_path)
                
im = Image.open(example_image_path)
im
           
SimGAN-Captcha代碼閱讀與複現

抽取mask

可以看到這些圖檔上面都有相同的水準的線,前面講過,因為是比賽,是以這些captcha上的線都是根據參賽者的名字生成的。在現實生活中,我們可以用openCV的一些 形态轉換函數(morphological transformation)來把這些噪音給過濾掉。這裡作者用的是把所有圖檔相加取平均得到了mask。他也推薦大家可以用bit mask(&=)來過濾掉。

mask = np.ones((height, width))
for im in ims:
    mask &= im
           

這裡是把所有圖檔相加取平均:

import numpy as np
WIDTH, HEIGHT = im.size
MASK_DIR = "avg.png"
def generateMask():
    N=*NUM_CHALLENGES
    arr=np.zeros((HEIGHT, WIDTH),np.float)
    for fname in IMG_FNAMES:
        imarr=np.array(Image.open(fname),dtype=np.float)
        arr=arr+imarr/N
    arr=np.array(np.round(arr),dtype=np.uint8)
    out=Image.fromarray(arr,mode="L")  # save as gray scale
    out.save(MASK_DIR)

generateMask()
im = Image.open(MASK_DIR) # ok this can be done with binary mask: &=
im
                
SimGAN-Captcha代碼閱讀與複現

再修正一下

im = Image.open(MASK_DIR)
im = im.point(lambda x: if x >  else x)
im = im.point(lambda x: if x< else , "1") # 1-bit bilevel, stored with the leftmost pixel in the most significant bit. 0 means black, 1 means white.
im.save(MASK_DIR)
im
                
SimGAN-Captcha代碼閱讀與複現

真實圖檔的生成器

我們在訓練的時候也需要把真實的圖檔丢進去,是以這裡直接用keras的

flow_from_directory

來自動生成圖檔并且把圖檔做一些預處理。

from keras import models
from keras import layers
from keras import optimizers
from keras import applications
from keras.preprocessing import image
import tensorflow as tf
                
# Real data generator

datagen = image.ImageDataGenerator(
    preprocessing_function=applications.xception.preprocess_input
    #  調用imagenet_utils的preoprocess input函數
    #  tf: will scale pixels between -1 and 1,sample-wise.
)

flow_from_directory_params = {'target_size': (HEIGHT, WIDTH),
                              'color_mode': 'grayscale',
                              'class_mode': None,
                              'batch_size': BATCH_SIZE}

real_generator = datagen.flow_from_directory(
        directory=".",
        **flow_from_directory_params
)
                

(Dumb)生成器(模拟器Simulator)

接着我們需要定義個生成器來幫我們生成(驗證碼,标注label)對,這些生成的驗證碼應該盡可能的和真實圖檔的那些比較像。

# Synthetic captcha generator
from PIL import ImageFont, ImageDraw
from random import choice, random
from string import ascii_lowercase, digits
alphanumeric = ascii_lowercase + digits


def fuzzy_loc(locs):
    acc = []
    for i,loc in enumerate(locs[:]):
        if locs[i+] - loc < :
            continue
        else:
            acc.append(loc)
    return acc

def seg(img):
    arr = np.array(img, dtype=np.float)
    arr = arr.transpose()
    # arr = np.mean(arr, axis=2)
    arr = np.sum(arr, axis=)
    locs = np.where(arr < arr.min() + )[].tolist()
    locs = fuzzy_loc(locs)
    return locs

def is_well_formed(img_path):
    original_img = Image.open(img_path)
    img = original_img.convert('1')
    return len(seg(img)) == 

noiseimg = np.array(Image.open("avg.png").convert("1"))
# noiseimg = np.bitwise_not(noiseimg)
fnt = ImageFont.truetype('./arial-extra.otf', )
def gen_one():
    og = Image.new("1", (,))
    text = ''.join([choice(alphanumeric) for _ in range()])
    draw = ImageDraw.Draw(og)
    for i, t in enumerate(text):
        txt=Image.new('L', (,))
        d = ImageDraw.Draw(txt)
        d.text( (, ), t,  font=fnt, fill=)
        if random() > :
            w=txt.rotate(*(random()),  expand=)
            og.paste( w, (i* + int(*random()), int(+*(random()))),  w)
        else:
            w=txt.rotate(*(random()),  expand=)
            og.paste( w, (i* + int(*random()), int(*random())),  w)
    segments = seg(og)
    if len(segments) != :
        return gen_one()
    ogarr = np.array(og)
    ogarr = np.bitwise_or(noiseimg, ogarr)
    ogarr = np.expand_dims(ogarr, axis=).astype(float)
    ogarr = np.random.random(size=(,,)) * ogarr
    ogarr = (ogarr > ).astype(float) # add noise
    return ogarr, text


def synth_generator():
    arrs = []
    while True:
        for _ in range(BATCH_SIZE):
            img, text = gen_one()
            arrs.append(img)
        yield np.array(arrs)
        arrs = []
                

上面這段代碼主要是随機産生了不同的字元數字,然後進行旋轉,之後把字元貼在一起,把原來的那個噪音圖檔avg.png加上去,把一些重合的字元的驗證碼給去掉。這裡如果發現有問題,強烈建議先更新一下PILLOW,debug了好久....sigh~

def get_image_batch(generator):
    """keras generators may generate an incomplete batch for the last batch"""
    #img_batch = generator.next()
    img_batch = next(generator)
    if len(img_batch) != BATCH_SIZE:
        img_batch = generator.next()

    assert len(img_batch) == BATCH_SIZE

    return img_batch
                

看一下真實的圖檔長什麼樣子

import matplotlib.pyplot as plt
%matplotlib inline
imarr = get_image_batch(real_generator)
imarr = imarr[, :, :, ]
plt.imshow(imarr)
                
SimGAN-Captcha代碼閱讀與複現

我們生成的圖檔長什麼樣子

imarr = get_image_batch(synth_generator())[0, :, :, 0]
print imarr.shape
plt.imshow(imarr)
           
SimGAN-Captcha代碼閱讀與複現

注意上面的圖檔之是以顯示的有顔色是因為用了plt.imshow, 實際上是灰白的二值圖。

這部分生成的代碼,我個人覺得讀者可以直接在github上下載下傳一個驗證碼生成器就好,然後把圖檔根據之前的步驟搞成二值圖就行,而且可以盡可能的選擇跟自己需要預測的驗證碼比較相近的字型。

模型定義

整個網絡一共有三個部分

  1. Refiner

    Refiner,Rθ,是一個RestNet, 它在像素次元上去修改我們生成的圖檔,而不是整體的修改圖檔内容,這樣才可以保留整體圖檔的結構和标注。(要不然就尴尬了,萬一把字母a都變成别的字母标注就不準确了)

  2. Discriminator

    判别器,Dφ,是一個簡單的ConvNet, 包含了5個卷積層和2個max-pooling層,是一個二分類器,區分一個驗證碼是我們合成的還是真實的樣本集。

  3. 把他們合在一起

    把refined的圖檔合到判别器裡面

Refiner

主要是4個resnet_block疊加在一起,最後再用一個1*1的filter來構造一個feature_map作為生成的圖檔。可以看到全部的border_mode都是same,也就是說當中任何一步的輸出都和原始的圖檔長寬保持一緻(fully convolution)。

一個resnet_block是醬紫的:

SimGAN-Captcha代碼閱讀與複現

我們先把輸入圖檔用64個3*3的filter去conv一下,得到的結果(input_features)再把它丢到4個resnet_block中去。

def refiner_network(input_image_tensor):
    """
    :param input_image_tensor: Input tensor that corresponds to a synthetic image.
    :return: Output tensor that corresponds to a refined synthetic image.
    """
    def resnet_block(input_features, nb_features=, nb_kernel_rows=, nb_kernel_cols=):
        """
        A ResNet block with two `nb_kernel_rows` x `nb_kernel_cols` convolutional layers,
        each with `nb_features` feature maps.
        See Figure 6 in https://arxiv.org/pdf/1612.07828v1.pdf.
        :param input_features: Input tensor to ResNet block.
        :return: Output tensor from ResNet block.
        """
        y = layers.Convolution2D(nb_features, nb_kernel_rows, nb_kernel_cols, border_mode='same')(input_features)
        y = layers.Activation('relu')(y)
        y = layers.Convolution2D(nb_features, nb_kernel_rows, nb_kernel_cols, border_mode='same')(y)

        y = layers.merge([input_features, y], mode='sum')
        return layers.Activation('relu')(y)

    # an input image of size w × h is convolved with 3 × 3 filters that output 64 feature maps
    x = layers.Convolution2D(, , , border_mode='same', activation='relu')(input_image_tensor)

    # the output is passed through 4 ResNet blocks
    for _ in range():
        x = resnet_block(x)

    # the output of the last ResNet block is passed to a 1 × 1 convolutional layer producing 1 feature map
    # corresponding to the refined synthetic image
    return layers.Convolution2D(, , , border_mode='same', activation='tanh')(x)
                

Discriminator

這裡注意一下subsample就是strides, 由于subsample=(2,2)是以會把圖檔長寬減半,因為有兩個,是以最後的圖檔會變成原來的1/16左右。比如一開始圖檔大小是10050, 經過一次變換之後是5025,再經過一次變換之後是25*13。

SimGAN-Captcha代碼閱讀與複現

最後生成了兩個feature_map,一個是用來判斷是不是real還有一個用來判斷是不是refined的。

def discriminator_network(input_image_tensor):
    """
    :param input_image_tensor: Input tensor corresponding to an image, either real or refined.
    :return: Output tensor that corresponds to the probability of whether an image is real or refined.
    """
    x = layers.Convolution2D(, , , border_mode='same', subsample=(, ), activation='relu')(input_image_tensor)
    x = layers.Convolution2D(, , , border_mode='same', subsample=(, ), activation='relu')(x)
    x = layers.MaxPooling2D(pool_size=(, ), border_mode='same', strides=(, ))(x)
    x = layers.Convolution2D(, , , border_mode='same', subsample=(, ), activation='relu')(x)
    x = layers.Convolution2D(, , , border_mode='same', subsample=(, ), activation='relu')(x)
    x = layers.Convolution2D(, , , border_mode='same', subsample=(, ), activation='relu')(x)

    # here one feature map corresponds to `is_real` and the other to `is_refined`,
    # and the custom loss function is then `tf.nn.sparse_softmax_cross_entropy_with_logits`
    return layers.Reshape((, ))(x)    # (batch_size, # of local patches, 2)

                

把它們合起來

refiner 加到discriminator中去。這裡有兩個loss:

  1. self_regularization_loss

    論文中是這麼寫的: The self-regularization term minimizes the image difference

    between the synthetic and the refined images. 就是用來控制refine的圖檔不至于跟原來的圖檔差别太大,由于paper中沒有具體寫公式,但是大緻就是讓生成的像素值和原始圖檔的像素值之間的距離不要太大。這裡項目的原作者是用了:

def self_regularization_loss(y_true, y_pred):
    delta =   # FIXME: need to figure out an appropriate value for this
    return tf.multiply(delta, tf.reduce_sum(tf.abs(y_pred - y_true)))
                

y_true: 丢到refiner裡面的input_image_tensor

y_pred: refiner的output

這裡的delta是用來控制這個loss的權重,論文裡面是lambda。

整個loss就是把refiner的輸入圖檔和輸出圖檔的每個像素點值相減取絕對值,最後把整張圖檔的內插補點都相加起來再乘以delta。

  1. local_adversarial_loss

    為了讓refiner能夠學習到真實圖檔的特征而不是一些artifacts來欺騙判别器,我們認為我們從refined的圖檔中sample出來的patch, 應該是和真實圖檔的patch的statistics是相似的。是以我們在所有的local patches上定義判别器而不是學習一個全局的判别器。

def local_adversarial_loss(y_true, y_pred):
    # y_true and y_pred have shape (batch_size, # of local patches, 2), but really we just want to average over
    # the local patches and batch size so we can reshape to (batch_size * # of local patches, 2)
    y_true = tf.reshape(y_true, (, ))
    y_pred = tf.reshape(y_pred, (, ))
    loss = tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=y_pred)

    return tf.reduce_mean(loss)
                

合起來如下:

# Refiner
synthetic_image_tensor = layers.Input(shape=(HEIGHT, WIDTH, )) #合成的圖檔
refined_image_tensor = refiner_network(synthetic_image_tensor)
refiner_model = models.Model(input=synthetic_image_tensor, output=refined_image_tensor, name='refiner') 

# Discriminator
refined_or_real_image_tensor = layers.Input(shape=(HEIGHT, WIDTH, )) #真實的圖檔
discriminator_output = discriminator_network(refined_or_real_image_tensor)
discriminator_model = models.Model(input=refined_or_real_image_tensor, output=discriminator_output,
                                   name='discriminator')

# Combined
refiner_model_output = refiner_model(synthetic_image_tensor)
combined_output = discriminator_model(refiner_model_output)
combined_model = models.Model(input=synthetic_image_tensor, output=[refiner_model_output, combined_output],
                              name='combined')

def self_regularization_loss(y_true, y_pred):
    delta =   # FIXME: need to figure out an appropriate value for this
    return tf.multiply(delta, tf.reduce_sum(tf.abs(y_pred - y_true)))

# define custom local adversarial loss (softmax for each image section) for the discriminator
# the adversarial loss function is the sum of the cross-entropy losses over the local patches
def local_adversarial_loss(y_true, y_pred):
    # y_true and y_pred have shape (batch_size, # of local patches, 2), but really we just want to average over
    # the local patches and batch size so we can reshape to (batch_size * # of local patches, 2)
    y_true = tf.reshape(y_true, (, ))
    y_pred = tf.reshape(y_pred, (, ))
    loss = tf.nn.softmax_cross_entropy_with_logits(labels=y_true, logits=y_pred)

    return tf.reduce_mean(loss)


# compile models
BATCH_SIZE = 
sgd = optimizers.RMSprop()

refiner_model.compile(optimizer=sgd, loss=self_regularization_loss)
discriminator_model.compile(optimizer=sgd, loss=local_adversarial_loss)
discriminator_model.trainable = False
combined_model.compile(optimizer=sgd, loss=[self_regularization_loss, local_adversarial_loss])

                

預訓練

預訓練對于GAN來說并不是一定需要的,但是預訓練可以讓GAN收斂的更快一些。這裡我們兩個模型都先預訓練。

對于真實樣本label标注為[1,0], 對于合成的圖檔label為[0,1]。

# the target labels for the cross-entropy loss layer are 0 for every yj (real) and 1 for every xi (refined)
# discriminator_model.output_shape = num of local patches
y_real = np.array([[[, ]] * discriminator_model.output_shape[]] * BATCH_SIZE)
y_refined = np.array([[[, ]] * discriminator_model.output_shape[]] * BATCH_SIZE)
assert y_real.shape == (BATCH_SIZE, discriminator_model.output_shape[], )
                

對于refiner, 我們根據self_regularization_loss來預訓練,也就是說對于refiner的輸入和輸出都是同一張圖(類似于auto-encoder)。

LOG_INTERVAL = 
MODEL_DIR = "./model/"
print('pre-training the refiner network...')
gen_loss = np.zeros(shape=len(refiner_model.metrics_names))

for i in range():
    synthetic_image_batch = get_image_batch(synth_generator())
    gen_loss = np.add(refiner_model.train_on_batch(synthetic_image_batch, synthetic_image_batch), gen_loss)

    # log every `log_interval` steps
    if not i % LOG_INTERVAL:
        print('Refiner model self regularization loss: {}.'.format(gen_loss / LOG_INTERVAL))
        gen_loss = np.zeros(shape=len(refiner_model.metrics_names))

refiner_model.save(os.path.join(MODEL_DIR, 'refiner_model_pre_trained.h5'))··
                

對于判别器,我們用一個batch的真實圖檔來訓練,再用另一個batch的合成圖檔來交替訓練。

from tqdm import tqdm
print('pre-training the discriminator network...')
disc_loss = np.zeros(shape=len(discriminator_model.metrics_names))

for _ in tqdm(range()):
    real_image_batch = get_image_batch(real_generator)
    disc_loss = np.add(discriminator_model.train_on_batch(real_image_batch, y_real), disc_loss)

    synthetic_image_batch = get_image_batch(synth_generator())
    refined_image_batch = refiner_model.predict_on_batch(synthetic_image_batch)
    disc_loss = np.add(discriminator_model.train_on_batch(refined_image_batch, y_refined), disc_loss)

discriminator_model.save(os.path.join(MODEL_DIR, 'discriminator_model_pre_trained.h5'))

# hard-coded for now
print('Discriminator model loss: {}.'.format(disc_loss / ( * )))
                

訓練

這裡有兩個點1)用refined的曆史圖檔來更新判别器,2)訓練的整體流程

1)用refined的曆史圖檔來更新判别器

對抗訓練的一個問題是判别器隻關注最近的refined圖檔,這會引起兩個問題-對抗訓練的分散和refiner網絡又引進了判别器早就忘掉的artifacts。是以通過用refined的曆史圖檔作為一個buffer而不單單是目前的mini-batch來更新分類器。具體方法是,在每一輪分類器的訓練中,我們先從目前的batch中采樣b/2張圖檔,然後從大小為B的buffer中采樣b/2張圖檔,合在一起來更新判别器的參數。然後這一輪之後,用新生成的b/2張圖檔來替換掉B中的b/2張圖檔。

由于論文中沒有寫B的大小為多少,這裡作者用了100*batch_size作為buffer的大小。

2)訓練流程

xi是合成的的圖檔

yj是真實的圖檔

T是步數(steps)

K_d是每個step,判别器更新的次數

K_g是每個step,生成網絡的更新次數(refiner的更新次數)

這裡要注意在判别器更新的每一輪,其中的合成的圖檔的minibatch已經用1)當中的采樣方式來替代了。

from image_history_buffer import ImageHistoryBuffer


k_d =   # number of discriminator updates per step
k_g =   # number of generative network updates per step
nb_steps = 

# TODO: what is an appropriate size for the image history buffer?
image_history_buffer = ImageHistoryBuffer((, HEIGHT, WIDTH, ), BATCH_SIZE * , BATCH_SIZE)

combined_loss = np.zeros(shape=len(combined_model.metrics_names))
disc_loss_real = np.zeros(shape=len(discriminator_model.metrics_names))
disc_loss_refined = np.zeros(shape=len(discriminator_model.metrics_names))

# see Algorithm 1 in https://arxiv.org/pdf/1612.07828v1.pdf
for i in range(nb_steps):
    print('Step: {} of {}.'.format(i, nb_steps))

    # train the refiner
    for _ in range(k_g * ):
        # sample a mini-batch of synthetic images
        synthetic_image_batch = get_image_batch(synth_generator())

        # update θ by taking an SGD step on mini-batch loss LR(θ)
        combined_loss = np.add(combined_model.train_on_batch(synthetic_image_batch,
                                                             [synthetic_image_batch, y_real]), combined_loss) #注意combine模型的local adversarial loss是要用y_real來對抗學習,進而迫使refiner去修改圖檔來做到跟真實圖檔很像

    for _ in range(k_d):
        # sample a mini-batch of synthetic and real images
        synthetic_image_batch = get_image_batch(synth_generator())
        real_image_batch = get_image_batch(real_generator)

        # refine the synthetic images w/ the current refiner
        refined_image_batch = refiner_model.predict_on_batch(synthetic_image_batch)

        # use a history of refined images
        half_batch_from_image_history = image_history_buffer.get_from_image_history_buffer()
        image_history_buffer.add_to_image_history_buffer(refined_image_batch)

        if len(half_batch_from_image_history):
            refined_image_batch[:batch_size // ] = half_batch_from_image_history

        # update φ by taking an SGD step on mini-batch loss LD(φ)
        disc_loss_real = np.add(discriminator_model.train_on_batch(real_image_batch, y_real), disc_loss_real)
        disc_loss_refined = np.add(discriminator_model.train_on_batch(refined_image_batch, y_refined),
                                   disc_loss_refined)

    if not i % LOG_INTERVAL:
        # log loss summary
        print('Refiner model loss: {}.'.format(combined_loss / (LOG_INTERVAL * k_g * )))
        print('Discriminator model loss real: {}.'.format(disc_loss_real / (LOG_INTERVAL * k_d * )))
        print('Discriminator model loss refined: {}.'.format(disc_loss_refined / (LOG_INTERVAL * k_d * )))

        combined_loss = np.zeros(shape=len(combined_model.metrics_names))
        disc_loss_real = np.zeros(shape=len(discriminator_model.metrics_names))
        disc_loss_refined = np.zeros(shape=len(discriminator_model.metrics_names))

        # save model checkpoints
        model_checkpoint_base_name = os.path.join(MODEL_DIR, '{}_model_step_{}.h5')
        refiner_model.save(model_checkpoint_base_name.format('refiner', i))
        discriminator_model.save(model_checkpoint_base_name.format('discriminator', i))
                

SimGAN的結果

我們從合成圖檔的生成器中拿一個batch的圖檔,用訓練好的refiner去Predict一下,然後顯示其中的一張圖(我運作生成的圖檔當中是一些點點的和作者的不太一樣,但是跟真實圖檔更像,待補充):

synthetic_image_batch = get_image_batch(synth_generator())
arr = refiner_model.predict_on_batch(synthetic_image_batch)
plt.imshow(arr[200, :, :, 0])
plt.show()
           
plt.imshow(get_image_batch(real_generator)[2,:,:,0])
plt.show()
                

這裡作者認為生成的圖檔中字母的邊都模糊和有噪音的,不那麼的平滑了。(我覺得和原始圖檔比起來,在refine之前的圖檔看起來和真實圖檔也很像啊,唯一不同的應該是當中那些若有若無的點啊,讀者可以在生成圖檔的時候把噪音給去掉,再來refine圖檔,看能不能生成字母邊是比較噪音的(noisy),我這邊refine之後的圖檔就是當中有一點一點的,圖檔待補充)

開始運用到實際的驗證碼識别

那麼有了可以很好的生成和要預測的圖檔很像的refiner之後,我們就可以構造我們的驗證碼分類模型了,這裡作者用了多輸出的模型,就是給定一張圖檔,有固定的輸出(這裡是4,因為要預測4個字母)。

我們先用之前的合成圖檔的生成器(gen_one)來構造一個生成器,接着用refiner_model來預測一下作為這個generator的輸出圖檔。由于分類模型的輸出要用categorical_crossentropy,是以我們需要把輸出的字母變成one-hot形式。

n_class = len(alphanumeric)
def mnist_generator(batch_size=):
    X = np.zeros((batch_size, HEIGHT, WIDTH, ), dtype=np.uint8)
    y = [np.zeros((batch_size, n_class), dtype=np.uint8) for _ in range()] # 4 chars
    while True:
        for i in range(batch_size):
            im, random_str = gen_one()
            X[i] = im
            for j, ch in enumerate(random_str):
                y[j][i, :] = 
                y[j][i, alphanumeric.find(ch)] =    # one_hot形式,讓目前字母的index為1
        yield refiner_model.predict(np.array(X)), y

mg = mnist_generator().next()
                

模組化

from keras.layers import *

input_tensor = Input((HEIGHT, WIDTH, ))
x = input_tensor
x = Conv2D(, kernel_size=(, ),
                 activation='relu')(x)
# 4個conv-max_polling
for _ in range():
    x = Conv2D(, (, ), activation='relu')(x)
    x = MaxPooling2D(pool_size=(, ))(x)
x = Dropout()(x)
x = Flatten()(x)
x = Dense(, activation='relu')(x)
x = Dropout()(x)
x = [Dense(n_class, activation='softmax', name='c%d'%(i+))(x) for i in range()] # 4個輸出

model = models.Model(inputs=input_tensor, outputs=x)
model.compile(loss='categorical_crossentropy',
              optimizer='rmsprop',
              metrics=['accuracy'])

from keras.callbacks import History
history = History()  # history call back現在已經是每個模型在訓練的時候都會自帶的了,fit函數會傳回,主要用于記錄事件,比如loss之類的
model.fit_generator(mnist_generator(), steps_per_epoch=, epochs=, callbacks=[history])
                

測試模型

先看一下在合成圖檔上的預測:

def decode(y):
    y = np.argmax(np.array(y), axis=)[:,]
    return ''.join([alphanumeric[x] for x in y])

X, y = next(mnist_generator())
y_pred = model.predict(X)
plt.title('real: %s\npred:%s'%(decode(y), decode(y_pred)))
plt.imshow(X[, :, :, ], cmap='gray')
plt.axis('off')
                
SimGAN-Captcha代碼閱讀與複現

看一下對于要預測的圖檔的預測:

X = next(real_generator)
X = refiner_model.predict(X) 
 # 不确定作者為什麼要用refiner來predict,應該是可以省去這一步的
# 事實證明是不可以的,後面會分析
y_pred = model.predict(X)
plt.title('pred:%s'%(decode(y_pred)))
plt.imshow(X[,:,:,], cmap='gray')
plt.axis('off')
                
SimGAN-Captcha代碼閱讀與複現

後續補充

  1. 将預測模型這裡的圖檔替換掉,改成實際操作時候生成的圖檔

    在訓練過程中可以發現判别器的loss下降的非常快,并且到後面很難讓refine的和real的loss都變高。有的時候運氣好的話也許可以。我在訓練的時候出現了兩種情況:

    第一種情況:

    合成前:

    SimGAN-Captcha代碼閱讀與複現
    合成後:
    SimGAN-Captcha代碼閱讀與複現
    可以看到合成之後的圖檔中也是有一點一點的。拿這種圖檔去做訓練,後面對真實圖檔做預測的時候就可以直接丢進分類器訓練了。

第二種情況(作者notebook中展示的):

也就是前面寫到的情況。

類似于下面這樣,看起來refiner之後沒什麼變化的感覺:

SimGAN-Captcha代碼閱讀與複現

這個看起來并沒有感覺和真實圖檔很像啊!!!

可是神奇的是,作者在預測真實的圖檔的時候,他居然用refiner去predict真實的圖檔!

真實的圖檔之前是長這個樣子的:

SimGAN-Captcha代碼閱讀與複現

refiner之後居然長成了這樣:

SimGAN-Captcha代碼閱讀與複現

無語了呢!它居然把那些噪聲點給去掉了一大半........他這波反向的操作讓我很措手不及。于是他用refine之後的真實圖檔丢到分類器去做預測.....效果居然還不錯.....

反正我已經淩亂了呢..............................

不過如何讓模型能夠學到我們人腦做識别的過程是件非常重要的事情呢...這裡如果你想用合成的圖檔直接當作訓練集去訓練然後預測真實圖檔,準确率應該會非常低(我試了一下),也就是說模型在學習的過程中還是沒有學習到字元的輪廓概念,但是我們又沒辦法控制教會它去學習怎麼"識别"物體,應該學習哪些特征,最近釋出的論文(戳這裡)大家可以去看看(我還沒有看...)。

未完待續

  1. 評估準确率
  2. 修改驗證碼生成器,改成其他任意的生成器
  3. 将模型用到更複雜的背景的驗證碼上,評估準确率

繼續閱讀