39
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Python3+Tensorflowで画像生成ネットワークの作成-GAN

Last updated at Posted at 2019-06-11

(20/11/09 追記)

Tensorflowv2以降だと動作しなかったので、v2で動作するように修正しました。
https://github.com/sey323/tf-gan

概要

KerasでGANを構築してあるケースは多々見かけるが,TensorflowのみでGANを構築しているケースがあまりないのでここで解説しながら作成を行う.

GANの構成はだいたいこんな感じ.Generatorでノイズから画像を生成し,Discriminatorで正解画像と生成された画像を識別する.この2つのネットワークがお互いのネットワークの性能を超える様に敵対して学習するため,敵対的学習と呼ばれている.
スクリーンショット 2019-06-11 22.17.23.png

GANの詳しい仕組みは以下のものがとてもわかりやすいので参考に
今さら聞けないGAN(1) 基本構造の理解 - Qiita

ソースコードの全体像は以下のリンクから
https://github.com/sey323/tf-gan/tree/master

実行環境

OS : Ubuntu 16.04
CPU : Intel(R) Core(TM) i9-7980XE CPU @ 2.60GHz
GPU : GEFORCE RTX 2080Ti

python :3.6
tensorflow :1.15.1

GANのクラスの作成

GANの計算グラフの定義と学習のプロセスを記述するGANクラスを作成する.

models/gan.py
def __init__(self ,
                input_size,
                channel= 3,
                layers = [ 64 , 128 , 256],
                filter_size = [ 5 , 5 ],
                drop_prob = 0.5,
                zdim = 100,
                batch_num = 64,
                learn_rate = 2e-4,
                max_epoch = 100,
                gpu_config = tf.GPUOptions(per_process_gpu_memory_fraction=0.1) ,
                save_folder = 'results/GAN' ):

        self.input_size = input_size
        self.channel = channel
        self.layers = layers
        self.filter_size = filter_size
        self.drop_prob = drop_prob
        self.zdim = zdim
        self.batch_num = batch_num
        self.learn_rate = learn_rate
        self.max_epoch = max_epoch

インスタンス変数 役割
input_size ネットワークに入力する画像のサイズ
channel ネットワークに入力する画像カラーチャネル(濃淡画像:1,カラー画像:3)
layers ネットワークの出力層の次元数
filter_size ネットワークの層のフィルタサイズ
drop_prob ネットワークのドロップアウトの確率
zdim 入力するノイズの次元数
batch_num 学習するバッチ数
learn_rate 学習率
max_epoch 最大学習回数
gpu_config GPUの設定
save_folder 学習モデルを保存するディレクトリ

GANはクラス内で自身のネットワークの構築や学習のプロセスを行う.

  • Generator()
    GANの画像を生成するGeneratorのネットワークが定義されているクラス.
  • Discriminator()
    GANのGeneratorで生成された画像と正解画像の識別を行う,Discriminatorのネットワークが定義されているクラス.
  • build_modle()
    GANのネットワークの構築や損失関数,最適化手法を定義し,学習する計算グラフを作成するメソッド.
  • train()
    build_modle()で作成された計算グラフに,実際に学習画像を入力し学習を行うメソッド.

ネットワーク

GANでは画像を生成するGeneratorと,GANの生成された画像が本物か偽物か識別するDiscriminatorの2つネットワークを用いている.それぞれのネットワークは別々に学習が行われるため,それぞれ別のメソッドで作成する.

Discriminatorの構築

Discriminatorは,生成された画像と正解画像の入力から,どちらの画像が生成された画像か識別する2値分類を行うCNNである.これは従来のニューラルネットワークと同様に,畳み込み層の積み重ねによりネットワークを構築し,全結合層においてクラス分類を行う.

models/gan.py
    def Discriminator(self, input, channel=3, reuse=False, name=""):
        """
        Discriminator

        Args
            input (tensor):
                本物か偽物か識別したい画像のTensor配列
            channel (int):
                入力するカラーチャネル
            reuse (Boolean):
                同じネットワークが呼び出された時に再定義し直すかどうか
        """
        logging.info("[NETWORK]\tDeep Convolutional Discriminator")
        with tf.compat.v1.variable_scope("Discriminator" + name, reuse=reuse) as scope:
            if reuse:
                scope.reuse_variables()

            for i, output_shape in enumerate(self.layers, 1):
                if i == 1:  # 1層目の時だけ
                    before_output = input
                # conv
                with tf.compat.v1.variable_scope("conv_layer{0}".format(i)) as scope:
                    conv = layer.conv2d(
                        input=before_output,
                        stride=2,
                        filter_size=[self.filter_size[0], self.filter_size[1]],
                        output_dim=output_shape,
                        batch_norm=True,
                        name="Conv_{}".format(i),
                    )
                    conv = layer.leakyReLU(conv)
                before_output = conv

            with tf.compat.v1.variable_scope("Discriminator_Flatten", reuse=reuse) as scope:
                # FC層
                flatten_1 = layer.flatten(before_output, "Flatten")
                output = layer.fc(flatten_1, 1, "Out", batch_norm=False)
        return output

層の深さは,self.layerに記述されているネットワークの出力層の次元数だけ繰り返す.最後の全結合層の出力は,入力された画像が生成された画像か正解画像かの2値分類なので1とする.

ここで重要なのはtf.variable_scopeでネットワークに名前をつけていること.これを用いることで後述のパラメータの更新の際に,更新するネットワーク(GeneratorかDiscriminator)を指定可能となり,別々の指標でネットワークの学習を進めることが可能となる.

Generatorの構築

GANのGeneratorは,1次元のランダムノイズzの入力を,学習画像のドメインに近い画像へ変換を行うネットワークである.Generatorでは,畳み込み層と逆の処理を行う逆畳み込み層(Deconvolution)を用いて,ノイズから画像を生成する.

models/gan.py
def Generator(self, input, channel=3, reuse=False):
        """
        Generator

        Args
            input (tensor):
                生成の基となるランダムノイズ
            channel (int):
                出力するカラーチャネル
            reuse (Boolean):
                同じネットワークが呼び出された時に再定義し直すかどうか
        """
        logging.info("[NETWORK]\tDeep Convolutional Generator")
        with tf.compat.v1.variable_scope("Generator", reuse=reuse) as scope:
            if reuse:
                scope.reuse_variables()

            # 逆FC
            with tf.compat.v1.variable_scope("Fc{0}".format("")) as scope:
                dim_h, dim_w = imutil.calcImageSize(
                    self.input_size[0],
                    self.input_size[1],
                    stride=2,
                    num=len(self.layers),
                )
                # 1層目
                defc_1 = layer.defc(
                    input,
                    output_shape=[dim_h, dim_w,],
                    output_dim=self.layers[-1],
                    name="defc",
                )
            before_output = defc_1

            # Deconv層
            for i, input_shape in enumerate(reversed(self.layers)):
                # 初期情報
                layer_no = len(self.layers) - i
                output_dim = self.layers[layer_no - 1]
                output_h, output_w = imutil.calcImageSize(
                    self.input_size[0], self.input_size[1], stride=2, num=layer_no
                )
                logging.debug(
                    "[OUTPUT]\t(batch_size, output_height:{0}, output_width:{1}, output_dim:{2})".format(
                        output_h, output_w, output_dim
                    )
                )

                # deconv
                with tf.compat.v1.variable_scope(
                    "deconv_layer{0}".format(layer_no)
                ) as scope:
                    deconv = layer.deconv2d(
                        before_output,
                        stride=2,
                        filter_size=[self.filter_size[0], self.filter_size[1]],
                        output_shape=[output_h, output_w],
                        output_dim=output_dim,
                        batch_norm=True,
                        name="Deconv_{}".format(layer_no),
                    )
                    before_output = layer.ReLU(deconv)

            # 最後の層で画像に復元
            with tf.compat.v1.variable_scope("image_reconstract") as scope:
                deconv_out = layer.deconv2d(
                    before_output,
                    stride=2,
                    filter_size=[self.filter_size[0], self.filter_size[1]],
                    output_shape=[self.input_size[0], self.input_size[1]],
                    output_dim=channel,
                    batch_norm=False,
                    name="Deconv_Output",
                )
            output = layer.tanh(deconv_out)
        return output

GeneratorもDiscriminatorと同様に,self.layerに記述されているネットワークの出力層の次元数だけ繰り返す.最後の畳み込み層の出力は,学習画像の次元に合わせてchannelの値とする.

損失関数と最適化関数の定義

学習に用いるネットワークと最適化関数や損失関数,実験に用いるセッションを構築する.

models/gan.py
 def build_model(self):
        '''
        ネットワークの全体を作成する
        '''

        '''変数の定義'''
        self.z      = tf.placeholder( tf.float32, [None, self.zdim],name="z")
        self.y_real = tf.placeholder( tf.float32, [None, self.input_size[0], self.input_size[1], 3],name="image")

        '''Generatorのネットワークの構築'''
        print('[BUILDING]\tGenerator')
        self.y_fake   = self.Generator(self.z,self.channel)
        self.y_sample = self.Generator(self.z,self.channel,reuse=True)

        '''Discrimnatorのネットワークの構築'''
        print('[BUILDING]\tDiscriminator')
        self.d_real  = self.Discriminator(self.y_real)
        self.d_fake  = self.Discriminator(self.y_fake,reuse=True)

        '''損失関数の定義'''
        print('[BUILDING]\tLoss Function')
        self.g_loss      = loss_function.cross_entropy( x=self.d_fake,labels=tf.ones_like (self.d_fake), batch_num = self.batch_num , name = "g_loss_fake")

        self.d_loss_real = loss_function.cross_entropy( x=self.d_real,labels=tf.ones_like (self.d_real), batch_num = self.batch_num , name = "d_loss_real")
        self.d_loss_fake = loss_function.cross_entropy( x=self.d_fake,labels=tf.zeros_like(self.d_fake), batch_num = self.batch_num , name = "d_loss_fake")
        self.d_loss      = self.d_loss_real + self.d_loss_fake

        '''最適化関数の定義'''
        print('[BUILDING]\tOptimizer')
        self.g_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.g_loss, var_list=[x for x in tf.trainable_variables() if "Generator"     in x.name])
        self.d_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.d_loss, var_list=[x for x in tf.trainable_variables() if "Discriminator" in x.name])

        '''Tensorboadに保存する設定'''
        print('[BUILDING]\tSAVE Node')
        tf.summary.scalar( "d_loss_real" , self.d_loss_real)
        tf.summary.scalar( "d_loss_fake" , self.d_loss_fake)
        tf.summary.scalar( "d_loss" , self.d_loss)
        tf.summary.scalar( "g_loss" , self.g_loss)

        '''Sessionの定義'''
        self.sess = tf.Session(config=self.gpu_config)

        ### saver
        self.saver = tf.train.Saver()
        self.summary = tf.summary.merge_all()
        if self.save_folder: self.writer = tf.summary.FileWriter(self.save_folder, self.sess.graph)

損失関数

Discriminatorの損失関数は交差誤差を用いて偽物の画像を偽物と識別した際の損失と,本物の画像を本物と識別した際の損失の合計値を用いる.
Generatorの損失関数は,交差誤差を用いてDiscriminatorが偽物の画像を正解の画像と誤って識別した際の損失を用いる.

最適化関数

models/gan.py
        '''最適化関数の定義'''
        print('[BUILDING]\tOptimizer')
        self.g_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.g_loss, var_list=[x for x in tf.trainable_variables() if "Generator"     in x.name])
        self.d_optimizer = tf.train.AdamOptimizer(self.learn_rate,beta1=0.5).minimize(self.d_loss, var_list=[x for x in tf.trainable_variables() if "Discriminator" in x.name])

最適化関数の部分では,Tensorflowのname_scopeの機能を用いて行う.tf.trainable_variables()には定義したネットワークの計算グラフとその名称が保存されている.train.AdamOptimizer.minimize()メゾットでは,更新の対象となる層を指定することが可能であるため,tf.trainable_variables()を用いて計算グラフの名称の一覧を取得し,その名前にDiscriminatorまたはGeneraotrが含まれる計算グラフのみをパラメータ更新の対象として指定する.

学習

学習ではutil/batchgen.pybatchクラスを用いて,ミニバッチ学習を行う.batchクラスについて簡単に説明すると,画像の教師ラベルと画像データがbatchクラスに格納されており,それらをbatch.getBatch({バッチ枚数})を用いることで取得できる.そして取得した画像を,ネットワークとして定義したDiscriminatorの計算グラフに代入し,学習を進める.

batch.getEpoch()で現在のバッチのループ回数を取得し,初期化の時に指定したmax_epoch回になるまで学習を進める.学習回数が10回ごとにsummaryの出力,100回ごとに途中結果の出力を行う.

models/gan.py

    def train(self, batch_o):
        self.build_model()
        initOP = tf.global_variables_initializer()
        self.sess.run(initOP)

        step = -1
        epoch = 0
        time_history=[]
        start = time.time()
        while batch_o.getEpoch()<self.max_epoch:
            step += 1

            # ランダムにバッチと画像を取得
            batch_images,batch_labels = batch_o.getBatch(self.batch_num)
            batch_z = np.random.uniform(-1.,+1.,[self.batch_num,self.zdim]).astype(np.float32)

             # Update Discrimnator
            _,d_loss,y_fake,y_real,summary = self.sess.run([self.d_optimizer,self.d_loss,self.y_fake,self.y_real,self.summary],feed_dict={self.z:batch_z, self.y_real:batch_images})
            # Update Generator
            _,g_loss = self.sess.run([ self.g_optimizer , self.g_loss ] , feed_dict={ self.z:batch_z })


            if step>0 and step%10==0:
                self.writer.add_summary(summary , step )

            if epoch != batch_o.getEpoch():
                # 実行時間と損失関数の出力
                train_time = time.time()-start
                print("epoch: %6, loss(D)=%.4e, loss(G)=%.4e; time/step = %.2f sec"%(batch_o.getEpoch(),d_loss,g_loss,train_time))
                self.dumper.add(batch_o.getEpoch(),step,d_loss,g_loss,train_time)

                # ノイズの作成.
                l0 = np.array([ x%10 for x in range( self.batch_num )] , dtype = np.int32)
                z1 = np.random.uniform(-1,+1,[ self.batch_num , self.zdim ])
                z2 = np.random.uniform(-1,+1,[ self.zdim ])
                z2 = np.expand_dims( z2 , axis = 0 )
                z2 = np.repeat( z2 , repeats = self.batch_num , axis = 0 )

                # 画像を作成して保存
                g_image1 = self.sess.run(self.y_sample,feed_dict={self.z:z1})
                g_image2 = self.sess.run(self.y_sample,feed_dict={self.z:z2})
                cv2.imwrite(os.path.join(self.save_folder,"images","img_%d_real.png"%step),imutil.tileImage( y_real ) * 255.+128.)
                self.create(z1 ,os.path.join(self.save_folder,"images","img_%d_fake1.png"%step))
                self.create(z2 ,os.path.join(self.save_folder,"images","img_%d_fake2.png"%step))
                self.saver.save(self.sess,os.path.join(self.save_folder,"model.ckpt"),step)

                epoch = batch_o.getEpoch()

                # 時間の計測の再開
                start = time.time()


        self.dumper.save()

実行ファイル

最後に作成したGANクラスを実行するプログラムを作成する.画像読み込みなどに使うモジュールの詳細は割愛する.tensorflowのFLAGS機能を使ってコンソール入力を受け取る.layers = [64 , 128 , 256]としてあるが,今回は64-128-256の3層のGANを構築した.

tf-gan/train.py
import os,sys
sys.path.append('./util')
import imload
from batchgen import *

# Ganモデルの読み込み
sys.path.append('./models')
from gan import *


def main(FLAGS):

   '''
   メイン関数
   '''
    # パラメータの取得
    img_path = os.getenv("DATASET_FOLDER", "dataset")
    save_path = os.getenv("SAVE_FOLDER", "results")


    '''設定ファイルからパラメータの読み込み'''
    print('[LOADING]\tmodel parameters loding')

    # ファイルのパラメータ
    folder = FLAGS.folder
    resize = [ FLAGS.resize , FLAGS.resize ]
    file_num = FLAGS.file_num
    gray = FLAGS.gray
    channel = 1 if gray else 3

    # GANのクラスに関するパラメータ
    layers = [64 , 128 , 256]
    max_epoch = FLAGS.max_epoch
    batch_num = FLAGS.batch_size
    save_folder = FLAGS.save_folder
    save_path = save_path + '/' + save_folder


    '''画像の読み込み'''
    train_image ,train_label = imload.make( folder , gray = gray , train_num = file_num , img_size = resize[0] )


    '''バッチの作成'''
    batch = batchgen( train_image , train_label)


    '''モデルの作成'''
    print("[LOADING]\tGAN")
    gan = GAN(           input_size=resize,
                         channel = channel,
                         layers = layers,
                         batch_num = batch_num ,
                         max_epoch = max_epoch ,
                         save_folder = save_path)

    # 学習の開始
    gan.train( batch )


if __name__=="__main__":
    flags = tf.app.flags
    FLAGS = flags.FLAGS

    # 実行するGANモデルの指定.
    flags.DEFINE_string('type', 'gan', 'Choice GAN type.')

    # 読み込む画像周り
    flags.DEFINE_string('folder', '', 'Directory to put the training data.')
    flags.DEFINE_integer('resize', 64, 'Size of Image.')
    flags.DEFINE_integer('file_num', 0, 'Loading Images Num.')
    flags.DEFINE_boolean('gray', False, 'Convert Gray Scale?')

    # GANの学習パラメータ
    flags.DEFINE_float('learning_rate', 0.001, 'Initial learning rate.')
    flags.DEFINE_integer('max_epoch', 100, 'Number of steps to run trainer.')
    flags.DEFINE_integer('batch_size', 25, 'Batch size.  ''Must divide evenly into the dataset sizes.')

    # 保存フォルダの決定
    flags.DEFINE_string('save_folder', '', 'Data save folder')

    main(FLAGS)

実験

データセットの準備

データセットは海外のセレブの画像のデータセットを集めた,FaceScrubを用いて行う.
画像の収集はこちらのリンクの手順で集める.
https://qiita.com/sey323/items/fc3cac3e9632c91ddd3f

集めた画像のうち,手っ取り早くA-Z順に上から10人適当に取ってきて,プログラムのディレクトリに移動する.教師データとなる正解画像は以下の様なものになる.

./tf-gan
face --- Aaron_Eckhart
      |- Adam_Brody 
      |- Adam_Mckay  
    ・
    ・
      |- Alec_Baldwin

img_0_real.png

学習の実行

学習の実行は以下の通り.

$ python train.py --folder=face --batch_size=25
[LOADING]	model parameters loding
[LOADING]	Label0	Name:Alan_Alda	Pictures exit. Unit On 111
[LOADING]	Label1	Name:Alan_Rickman	Pictures exit. Unit On 119
[LOADING]	Label2	Name:Adam_McKay	Pictures exit. Unit On 45
[LOADING]	Label3	Name:Al_Pacino	Pictures exit. Unit On 102
[LOADING]	Label4	Name:Adrien_Brody	Pictures exit. Unit On 113
[LOADING]	Label5	Name:Adam_Sandler	Pictures exit. Unit On 95
[LOADING]	Label6	Name:Alec_Baldwin	Pictures exit. Unit On 125
[LOADING]	Label7	Name:Aaron_Eckhart	Pictures exit. Unit On 117
[LOADING]	Label8	Name:Alan_Arkin	Pictures exit. Unit On 94
[LOADING]	Label9	Name:Adam_Brody	Pictures exit. Unit On 107
[LOADING]	GAN
[BUILDING]	Generator
[NETWORK]
Deep Convolutional Generator
[NETWORK]
Deep Convolutional Generator
[BUILDING]	Discriminator
[NETWORK]
Deep Convolutional Discriminator
[NETWORK]
Deep Convolutional Discriminator
[BUILDING]	Loss Function
[BUILDING]	Optimizer
[BUILDING]	SAVE Node
2019-06-10 22:22:31.394602: I tensorflow/core/platform/cpu_feature_guard.cc:140] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 AVX512F FMA
2019-06-10 22:22:31.584971: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:898] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2019-06-10 22:22:31.586126: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1356] Found device 0 with properties:
name: GeForce GTX 1080 Ti major: 6 minor: 1 memoryClockRate(GHz): 1.582
pciBusID: 0000:17:00.0
totalMemory: 10.92GiB freeMemory: 10.76GiB
2019-06-10 22:22:31.741673: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:898] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2019-06-10 22:22:31.742130: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1356] Found device 1 with properties:
name: GeForce GTX 1080 Ti major: 6 minor: 1 memoryClockRate(GHz): 1.582
pciBusID: 0000:65:00.0
totalMemory: 10.91GiB freeMemory: 10.76GiB
2019-06-10 22:22:31.742985: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1435] Adding visible gpu devices: 0, 1
2019-06-10 22:22:32.114452: I tensorflow/core/common_runtime/gpu/gpu_device.cc:923] Device interconnect StreamExecutor with strength 1 edge matrix:
2019-06-10 22:22:32.114491: I tensorflow/core/common_runtime/gpu/gpu_device.cc:929] 0 1
2019-06-10 22:22:32.114497: I tensorflow/core/common_runtime/gpu/gpu_device.cc:942] 0: N Y
2019-06-10 22:22:32.114501: I tensorflow/core/common_runtime/gpu/gpu_device.cc:942] 1: Y N
2019-06-10 22:22:32.114772: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1053] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 11178 MB memory) -> physical GPU (device: 0, name: GeForce GTX 1080 Ti, pci bus id: 0000:17:00.0, compute capability: 6.1)
2019-06-10 22:22:32.217003: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1053] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:1 with 11176 MB memory) -> physical GPU (device: 1, name: GeForce GTX 1080 Ti, pci bus id: 0000:65:00.0, compute capability: 6.1)
0: loss(D)=1.4175e+00, loss(G)=2.0379e+00; time/step = 2.53 sec
100: loss(D)=2.7534e-01, loss(G)=7.1763e+00; time/step = 17.97 sec
200: loss(D)=1.4626e-01, loss(G)=8.5171e+00; time/step = 18.54 sec
   ・
   ・

結果

生成された画像

学習回数:100回
img_100_fake1.png

学習回数:1000回
img_1000_fake1.png

学習回数:5000回
img_5100_fake1.png

学習回数:7500回
img_7500_fake1.png
学習回数:10000回
img_10100_fake1.png

学習回数が向上するにつれて,ノイズが少ない品質の高い画像が生成されていることが確認できうる.

損失関数の推移

tensorflowだと指定したパラメータをtensorboadで視覚化することができるので,tensorboadで学習結果を見て見る.

$ tensorboad --logdir={実行結果のパス}

g_loss
スクリーンショット 2019-06-10 23.00.15.png

d_loss
スクリーンショット 2019-06-10 23.00.00.png

g_lossは徐々にDiscriminatorを誤認識させる確率が向上しており,d_lossも徐々に識別性能が向上しており,うまく敵対的に学習して両方のネットワークの性能を高めていることが確認できる.

終わりに

今回は単純なDCGANを構築して画像を生成した.単純なGANでもかなり品質の高い画像が生成できている.次はcGANを作成し,生成する人物を指定した画像の生成をしたい.

このプログラム,読み込む画像のファイル構成を今回の実験と同じ様な形式で保存してもらえれば顔以外のデータセットでも学習可能なので,適当に試してみてください.

39
47
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
47

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?