{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":21755,"databundleVersionId":1475600,"sourceType":"competition"}],"dockerImageVersionId":30587,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Project Description\n* This is the Mini-project for course Introduction to Deep Learning Week 5 GANS\n* The dataset used in this project is from Kaggle Monet Painting Dataset [https://www.kaggle.com/competitions/gan-getting-started](http://)\n* The dataset contains painting images and photos in both jpg format and TFRecords format.\n* The goal of this project is to build a GAN which consists of at least two neural networks(generator and discriminator) to generate images in the style of Monet.\n* Citation @misc{gan-getting-started,\n    author = {Amy Jang, Ana Sofia Uzsoy, Phil Culliton},\n    title = {I’m Something of a Painter Myself},\n    publisher = {Kaggle},\n    year = {2020},\n    url = {https://kaggle.com/competitions/gan-getting-started}\n}\n* In order to achive the goal of the project, I will build a CycleGAN model for translating painting to photos and photos to painting. The CycleGAN is a technique that involves the automatic training of image-to-image translation model without paired examples. Which fits our project as the dataset provided are not paired examples.\n\n\n*   The CycleGAN is an extension of the GAN architecture that involves the simultaneous training of two generator models and two discriminator models.\n\nProcess:\n\n**GAN1**   \n*   Painting --> Generator1--> generated_photo1\n*   generated_photo-->discriminator--> real/fake\n\n**GAN2**\n*   photo-->Generator2--> generated_painting1\n*   generated_painting--> discriminator--> real/fake\n\n**Forward cycle**  :\n\n\n*   generated_photo1-->Generator2-->generated_painting_FW\n*   loss calculates the difference betwwen painting and generated_painting_FW\n*   update Generator2\n\n\n**Forward cycle consistency loss**:\n\n1.   Input paiting to GAN1\n2.   Output photo(generated) from GAN1\n\n1.   Input photo(generated) from GAN1 to GAN2\n2.   Output painting(generated) from GAN2\n\n1.   Compare painting(real) to painting from GAN2(generated)\n\n\n**Backward cycle** :\n\n*   generated_painting1-->Generator1-->generated_photo_BW\n*   loss calculates the difference betwwen photo and generated_photo_BW\n*   update Generator1\n\n**Backward cycle consistency loss**:\n\n1.   Input photo to GAN2\n2.   Output painting(generated) from GAN2\n\n1.   Input painting(generated) from GAN2 to GAN1\n2.   Output photo(generated) from GAN1\n\n1.   Compare painting(real) to painting from GAN1(generated)\n","metadata":{}},{"cell_type":"code","source":"!pip install git+https://www.github.com/keras-team/keras-contrib.git","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom matplotlib.image import imread\nimport os\n\nimport tensorflow_datasets as tfds\n\nfrom keras_contrib.layers.normalization.instancenormalization import InstanceNormalization\n\nfrom tensorflow.keras.preprocessing.image import img_to_array\nimport tensorflow_addons as tfa\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.initializers import RandomNormal\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.layers import Conv2D, LeakyReLU,Input,Activation,Concatenate,\\\nConv2DTranspose\nfrom tensorflow.keras.utils import plot_model,load_img\nimport PIL\n\n%matplotlib inline\n\nimport warnings\nwarnings.filterwarnings(\"ignore\")\nnp.random.seed(401)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# load the dataset path and take a look at all the folders inside\nIMG_DIR = '/kaggle/input/gan-getting-started'\n# get a list of folder names of the working directory\nprint(os.listdir(IMG_DIR))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"Monet_jpg = IMG_DIR+'/monet_jpg/'\nMonet_tfrec = IMG_DIR+'/monet_tfrec/'\nPhoto_jpg = IMG_DIR+'/photo_jpg/'\nPhoto_tfrec = IMG_DIR+'/photo_tfrec/'","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(f'There are {len(os.listdir(Monet_jpg))} images in Monet_jpg folder.')\nprint(f'There are {len(os.listdir(Monet_tfrec))} TFRecord files in Monet_tfrec folder.')\nprint(f'There are {len(os.listdir(Photo_jpg))} images in Photo_jpg folder.')\nprint(f'There are {len(os.listdir(Photo_tfrec))} TFRecord files in Photo_tfrec folder.')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# sample painting\ni = np.random.randint(0,300)\nsample = Monet_jpg+os.listdir(Monet_jpg)[i]\nsample_pt = imread(sample)\nplt.imshow(sample_pt)\nsample_pt.shape","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# sample photo\ni = np.random.randint(0,7038)\nsample = Photo_jpg+os.listdir(Photo_jpg)[i]\nsample_ph = imread(sample)\nplt.imshow(sample_ph)\nsample_ph.shape","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# check dimenson of all the Monet_jpg images\nDim1 = []\nDim2 = []\nCS = []\nfor ele in os.listdir(Monet_jpg):\n    img = imread(Monet_jpg+ele)\n    D1,D2,C = img.shape\n    Dim1.append(D1)\n    Dim2.append(D2)\n    CS.append(C)\n\nD_Monet = pd.DataFrame({'D1': Dim1,'D2':Dim2,'C':CS})\nD_Monet.describe()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# check dimenson of all the Photo_jpg images\nDim1 = []\nDim2 = []\nCS = []\nfor ele in os.listdir(Photo_jpg):\n    img = imread(Photo_jpg+ele)\n    D1,D2,C = img.shape\n    Dim1.append(D1)\n    Dim2.append(D2)\n    CS.append(C)\n\nD_Photo = pd.DataFrame({'D1': Dim1,'D2':Dim2,'C':CS})\nD_Photo.describe()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"img_size = (256,256)\nimg_channel = 3\nimg_shape = (256,256,3)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# function to load img and convert to array,scale\ndef load_images(path,size=(256,256)):\n    data_list = list()\n    _list=os.listdir(path)\n    # print(_list)\n    for file in _list:\n        # print(file)\n        # print(path+file)\n        img = load_img(path+file)\n        img = img_to_array(img)\n        img = img/127.5-1\n        data_list.append(img)\n    return np.asarray(data_list)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# load Monet painting, conver to array and save to local file\nMonet_arr = load_images(Monet_jpg)\nprint(Monet_arr.shape)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# load photo, conver to array and save to local file\nPhoto_arr = load_images(Photo_jpg)\nprint(Photo_arr.shape)\n\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Discriminator**\n\nDefine discriminator using PatchGAN which is designed based on the size of the receptive field that is the relationship between one output activation of the model to an area on the input image. Let Ck denote a Convolution-BatchNrom-ReLU layer with k filters, all convolustions are (4,4) spatical filters with stride 2 application. The 70x70 discriminator architecture is C64-C128-C256-C512. BatchNorm is not applied to the first C64 layer.","metadata":{}},{"cell_type":"code","source":"def CBR_block(d_in,k,instance_norm=True):\n    # initializa weight\n    init = RandomNormal(stddev=0.02)\n    d = Conv2D(k,(4,4),strides=(2,2),padding='same',kernel_initializer=init)(d_in)\n    if instance_norm:\n        d = InstanceNormalization(axis=-1)(d)\n    d = LeakyReLU(alpha=0.2)(d)\n    return d","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def discriminator(img_shape): #(256,256,3):\n    # initializa weight\n    init = RandomNormal(stddev=0.02)\n    # image input\n    in_img = Input(shape=img_shape)\n\n    #C64\n    d = CBR_block(in_img,64,instance_norm=False)\n    #C128\n    d = CBR_block(d,128,instance_norm=True)\n    #C256\n    d = CBR_block(d,256,instance_norm=True)\n    #C512\n    d = CBR_block(d,512,instance_norm=True)\n    # layer before output\n    d = Conv2D(512, (4,4), padding='same', kernel_initializer=init)(d)\n    d = InstanceNormalization(axis=-1)(d)\n    d = LeakyReLU(alpha=0.2)(d)\n    # output layer\n    patch_out =Conv2D(1,(4,4),padding='same',kernel_initializer=init)(d)\n    # define model\n    model = Model(in_img,patch_out)\n    # compile model\n    # mean squared error is used between the target values of class 1 (real) and class 0(fake)\n    # The loss for the discriminator is weighted by 50% for each model update\n    # to slow down the rate at which Discriminator learns relative to generator\n    model.compile(loss='mse',optimizer=Adam(learning_rate=0.0002,beta_1=0.5),loss_weights=[0.5])\n    return model","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"d_Monet = discriminator(img_shape)\nd_Photo = discriminator(img_shape)\nd_Monet.summary()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_model(d_Monet,to_file='discriminator_model_plot.png',show_shapes=True,show_layer_names=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Generator**\n\nDefine CycleGAN Generator which takes an imput image and generates a translated image as ouput.The model uses a combination of downsampling convolutional blocks, upsampling convolutional blocks and resnet block in between.\n\nThe ResNet blcok are two 3x3 CNN layers where the input to the block is concatenated to the output of the block, channel-wise. For a 256x256 input image, 9-resnet blcok is needed.\n","metadata":{}},{"cell_type":"code","source":"def resnet_block(n_filters, input_layer):\n    # initialize weight\n    init = RandomNormal(stddev=0.02)\n    # convolutional layer\n    g = Conv2D(n_filters,(3,3),padding='same',kernel_initializer=init)(input_layer)\n    g = InstanceNormalization(axis=-1)(g)\n    g = Activation('relu')(g)\n    # convolutional layer\n    g = Conv2D(n_filters,(3,3),padding='same',kernel_initializer=init)(g)\n    g = InstanceNormalization(axis=-1)(g)\n    # concatenate channel-wise with input layer\n    g = Concatenate()([g,input_layer])\n    return g","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def downsampling(in_put,k,size=(3,3),strides=(2,2)):\n    # initialize weight\n    init = RandomNormal(stddev=0.02)\n    g = Conv2D(k, size, strides=strides, padding='same', kernel_initializer=init)(in_put)\n    g = InstanceNormalization(axis=-1)(g)\n    g = Activation('relu')(g)\n    return g","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def upsampling(in_put,k,size=(3,3),strides=(2,2)):\n    # initialize weight\n    init = RandomNormal(stddev=0.02)\n    g = Conv2DTranspose(k, size, strides=strides, padding='same', kernel_initializer=init)(in_put)\n    g = InstanceNormalization(axis=-1)(g)\n    g = Activation('relu')(g)\n    return g","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def generator(img_shape=(256,256,3),n_resnet=9):\n    # initialize weight\n    init = RandomNormal(stddev=0.02)\n    # image in\n    in_img = Input(shape=img_shape)\n    # C7S1-64\n    g = downsampling(in_img,64,size=(7,7),strides=(1,1))\n    # D128\n    g = downsampling(g,128)\n    # D256\n    g = downsampling(g,256)\n    # R256\n    for i in range(n_resnet):\n        g = resnet_block(256,g)\n    # U128\n    g = upsampling(g,128)\n    # U64\n    g = upsampling(g,64)\n    # C7S1-3\n    g = Conv2DTranspose(3,(7,7),padding='same', kernel_initializer=init)(g)\n    g = InstanceNormalization(axis=-1)(g)\n    out_img = Activation('tanh')(g)\n\n    # define model\n    model = Model(in_img,out_img)\n    return model","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"g_MtoPH = generator(img_shape)\ng_PHtoM = generator(img_shape)\ng_MtoPH.summary()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_model(g_MtoPH,to_file='generator_model_plot.png',show_shapes=True,show_layer_names=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Composite model loss**\n\nThe gnerator models are updated via composite models, the model weights update depending on\n\n\n1.   Adversarial loss (L2 or mean squared error)\n2.   Identity loss (L1 or mean absolute error)\n\n1.   Forward cycle loss (L1 or mean absolute error)\n2.   Backward cycle loss (L1 or mean absolute error)\n\n**Composite model**\n\nThe composite model takes two inputes: one real painting from Monet and one photo. The model outputs four outputs: one for the discriminator, one for the generator for the idenity mapping, one from the forward cycle, and one from the backward cycle.","metadata":{}},{"cell_type":"code","source":"def composite_model(g_1,d,g_2,img_shape):\n    # make models need to be updated traninable\n    g_1.trainable = True\n    g_2.trainable = False\n    d.trainable = False\n    # discriminate process\n    input_gen = Input(shape=img_shape)\n    gen1_out =  g_1(input_gen)\n    d_output = d(gen1_out)\n    # identity process\n    input_id = Input(shape = img_shape)\n    output_id = g_1(input_id)\n    # froward cycle\n    output_f = g_2(gen1_out)\n    # backward cycle\n    gen2_out = g_2(input_id)\n    output_b = g_1(gen2_out)\n    # model graph\n    model = Model([input_gen,input_id],[d_output,output_id,output_f,output_b])\n    # optimization\n    opt = Adam(learning_rate=0.0002,beta_1=0.5)\n    # compile\n    # weighted loss parameter lambda\n    # forward and backward cycle loss is 10 while adversarial loss is set to 1.\n    model.compile(loss=['mse','mae','mae','mae'],loss_weights = [1,5,10,10],\n                  optimizer=opt)\n    return model","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"Monet_to_Photo = composite_model(g_MtoPH,d_Photo,g_PHtoM,img_shape)\nPhoto_to_Monet = composite_model(g_PHtoM,d_Monet,g_MtoPH,img_shape)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"plot_model(Monet_to_Photo,to_file='composite_model_plot.png',show_shapes=True,show_layer_names=True)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Training model","metadata":{}},{"cell_type":"code","source":"# select a batch of random samples\ndef generate_real_samples(dataset,n_samples, patch_shape):\n    # random choose\n    ix = np.random.randint(0,dataset.shape[0],n_samples)\n    # get selected img\n    X = dataset[ix]\n    # generate labels (1)\n    y = np.ones((n_samples,patch_shape,patch_shape,1))\n    return X,y","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# test for error\npatch_shape = 16\na,b=generate_real_samples(Monet_arr ,2, patch_shape)\na.shape,b.shape","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# generate a batch of fake images\ndef generate_fake_samples(g_model,dataset,patch_shape):\n    # generate img\n    X = g_model.predict(dataset)\n    # genreate labels (0)\n    y =  np.zeros((len(X),patch_shape,patch_shape,1))\n    return X,y","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# test for error\na_f,b_f = generate_fake_samples(g_MtoPH,a,patch_shape)\na_f.shape,b_f.shape","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# show the real img\nplt.imshow(a[0])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# show generated fake img\nplt.imshow(a_f[0])","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The genreated img is not looking good but that is ok. we have not train and update the generator yet.","metadata":{}},{"cell_type":"code","source":"# save generator models to local file\ndef save_models(step,g_MtoPH,g_PHtoM):\n    name1 = 'g_MtoPH_%06d.h5'%(step+1)\n    g_MtoPH.save(name1)\n\n    name2 ='g_PHtoM_%06d.h5'%(step+1)\n    g_PHtoM.save(name2)\n    print(f\"Done saving {step+1}\")","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# make a image pool for fake images, the maximum number in the pool is 100\n# when reach the maximum number, a new fake image will randomly replace an old one\ndef fake_pool(pool,images,max_size=100):\n    selected = list()\n    for image in images:\n        if len(pool)<max_size:\n            # can still push fake image in\n            pool.append(image)\n            selected.append(image)\n        elif np.random.random()<0.5:\n            # use image, but do not add it to the pool\n            selected.append(image)\n        else:\n            #replace an old one with a new one\n            ix = np.random.randint(0,len(pool))\n            selected.append(pool[ix])\n            pool[ix] = image\n    return np.asarray(selected)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def train_model(d_M,d_PH,g_MtoPH,g_PHtoM,c_MtoPH,c_PHtoM,trainM,trainPH):\n    # training properties\n    epochs = 2\n    batch_n = 1\n    #\n    n_patch = d_M.output_shape[1]\n    poolM,poolPH = list(),list()\n    bat_per_epo = int(len(trainM)/batch_n)\n    # number of training iterations\n    n_steps = bat_per_epo*epochs\n    # enumerate epochs\n    for i in range(n_steps):\n        # a batch of real samples\n        X_realM,y_realM = generate_real_samples(trainM,batch_n,n_patch)\n        X_realPH,y_realPH = generate_real_samples(trainPH,batch_n,n_patch)\n        # generate a batch of fake samples\n        X_fakeM,y_fakeM = generate_fake_samples(g_PHtoM,X_realM,n_patch)\n        X_fakePH,y_fakePH = generate_fake_samples(g_MtoPH,X_realPH,n_patch)\n        # update fake pool\n        X_fakeM = fake_pool(poolM,X_fakeM,max_size=100)\n        X_fakePH = fake_pool(poolPH,X_fakePH,max_size=100)\n        # update generator g_PHtoM\n        g_lossPHtoM,_,_,_,_=Photo_to_Monet.train_on_batch([X_realPH,X_realM],[y_realM,X_realM,X_realPH,X_realM])\n        # update discriminator for d_M\n        dM_loss1 = d_M.train_on_batch(X_realM,y_realM)\n        dM_loss2 = d_M.train_on_batch(X_fakeM,y_fakeM)\n        # update generator g_MtoPH\n        g_lossMtoPH,_,_,_,_ = Monet_to_Photo.train_on_batch([X_realM,X_realPH],[y_realPH,X_realPH,X_realM,X_realPH])\n        # update discriminator for d_PH\n        dPH_loss1 = d_PH.train_on_batch(X_realPH,y_realPH)\n        dPH_loss2 = d_PH.train_on_batch(X_fakePH,y_fakePH)\n        # save model once in a while\n        if (i+1) % 300 == 0:\n            save_models(i,g_MtoPH,g_PHtoM)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"Monet_arr.shape,Photo_arr.shape","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_model(d_Monet,d_Photo,g_MtoPH,g_PHtoM,Monet_to_Photo,Photo_to_Monet,Monet_arr,Photo_arr)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"g_PHtoM.save('model_dest.h5')\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import tensorflow as tf\ntf.keras.backend.clear_session()","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"from tensorflow.keras.models import load_model","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"cust = {'InstanceNormalization': InstanceNormalization}\ng_PhtoM = load_model('model_dest.h5',cust)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# test for error\n# get a real photo\nsam_img = generate_real_samples(Photo_arr,1,16)\n# genereate Monet style of the photo\nsam_gened=generate_fake_samples(g_PHtoM,sam_img[0],16)\n# show imgs side by side\nplt.subplot(121)\nplt.title('real photo')\nplt.imshow(sam_img[0][0])\nplt.axis('off')\n\nplt.subplot(122)\nplt.title('generated Monet style')\n# plt.imshow((sam_gened[0][0]+1)/2.0)\nplt.imshow(sam_gened[0][0]*0.5+0.5)\nplt.axis('off')\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# use the photo as input dataset and predict/generate Monet style img then save to the output directory \ndef pred_save(img_in,G_model,path,i):\n    prediction = G_model.predict(img_in)\n    prediction = prediction[0]\n    prediction = (prediction*127.5+127.5).astype(np.uint8)  # rescale \n#     print(prediction)\n#     print(prediction.shape)\n    im = PIL.Image.fromarray(prediction)\n    im.save(f'{path}{str(i)}.jpg')\n    ","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# create a new folder to store generated img\n\nos.makedirs('/kaggle/working/images/') \npath = '/kaggle/working/images/'\n# generate imgs for all the photos\nfor i in range(Photo_arr.shape[0]):\n    img_in = Photo_arr[i][np.newaxis,:]\n    pred_save(img_in,g_PHtoM,path,i)\n\nn = len(os.listdir(path))\nprint(f'{n} images generted!')","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# take a look at randomly selected img from generated folder\ni = np.random.randint(0,n)\nsample = path+os.listdir(path)[i]\nsample_ph = imread(sample)\nprint(sample_ph.shape)\nplt.imshow(sample_ph)","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import shutil\nshutil.make_archive('/kaggle/working/images', 'zip', '/kaggle/working/images')\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]}]}