{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":1504266,"sourceType":"datasetVersion","datasetId":885385},{"sourceId":2520560,"sourceType":"datasetVersion","datasetId":1526975},{"sourceId":7457578,"sourceType":"datasetVersion","datasetId":1698586}],"dockerImageVersionId":30005,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CycleGAN Image-to-Image Translation\n![](https://junyanz.github.io/CycleGAN/images/failures.jpg)\n### CycleGAN and Notebook Structure\n\nCycleGAN được tạo thành từ hai loại mạng: **discriminators, and generators**. Trong ví dụ này, discriminators chịu trách nhiệm phân loại hình ảnh là thật hoặc giả (đối với cả loại hình ảnh $X$ và $Y$). Generators có nhiệm vụ tạo ra những hình ảnh giả mạo, thuyết phục cho cả hai loại hình ảnh.\n","metadata":{}},{"cell_type":"markdown","source":"## Load and Visualize the Data","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport torchvision\nfrom torch.utils.data import DataLoader\nfrom torchvision import transforms\nimport numpy as np\nfrom torchvision.datasets import ImageFolder\nfrom torch.utils.data import Dataset\nimport cv2","metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","execution":{"iopub.status.busy":"2024-05-30T16:24:14.802155Z","iopub.execute_input":"2024-05-30T16:24:14.802532Z","iopub.status.idle":"2024-05-30T16:24:14.808564Z","shell.execute_reply.started":"2024-05-30T16:24:14.802500Z","shell.execute_reply":"2024-05-30T16:24:14.807438Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"### Các tham số\n\n The function has the following parameters:\n* `image_type`: tên các thư mục lưu trữ hình ảnh X và Y\n* `image_dir`: tên của thư mục  train, chứa tất cả các hình ảnh train và test\n* `image_size`: tỉ lệ hình ảnh cần thay đổi\n* `batch_size`: số lượng hình ảnh trong một lô dữ liệu\n\n","metadata":{}},{"cell_type":"code","source":"data_dir='/kaggle/input/cat-and-dogs/dataset/training_set'\nclass GANdataGenerator(Dataset):\n    def __init__(self,data_dir ,  data_files , image_size ):\n        super(GANdataGenerator , self).__init__()\n        self.data_dir = data_dir\n        self.data = data_files\n        self.transform = transforms.Compose([\n            transforms.ToPILImage(),\n            transforms.Resize(image_size),\n            transforms.CenterCrop(image_size),\n            transforms.ToTensor()\n        ])\n        \n    def __len__(self):\n        return len(self.data)\n    \n    def __getitem__(self, index):\n        data_file = self.data[index]\n        data_file = os.path.join(self.data_dir , data_file)\n        img_bgr = cv2.imread(data_file)\n        img_rgb = cv2.cvtColor(img_bgr , cv2.COLOR_BGR2RGB)\n        img_tensor = self.transform(img_rgb)\n        \n        return img_tensor","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:14.810522Z","iopub.execute_input":"2024-05-30T16:24:14.810978Z","iopub.status.idle":"2024-05-30T16:24:14.823490Z","shell.execute_reply.started":"2024-05-30T16:24:14.810937Z","shell.execute_reply":"2024-05-30T16:24:14.822689Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"photo_jpg = os.listdir('/kaggle/input/portrait-paintings/Images')\nhuman_jpg = os.listdir('/kaggle/input/human-faces/Humans')\n\nX_type='human_jpg'\nY_type='photo_jpg'\nX_set = GANdataGenerator(data_dir='/kaggle/input/human-faces/Humans' , data_files=human_jpg , image_size=128 )\nY_set = GANdataGenerator(data_dir='/kaggle/input/portrait-paintings/Images' , data_files=photo_jpg , image_size=128 )\n\nX_dataloader = DataLoader(X_set , batch_size=16 , shuffle=True , num_workers=0)\nY_dataloader = DataLoader(Y_set , batch_size=16 , shuffle=True , num_workers=0)\n\nx_data = next(iter(X_dataloader))\ny_data = next(iter(Y_dataloader))","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:14.848974Z","iopub.execute_input":"2024-05-30T16:24:14.849230Z","iopub.status.idle":"2024-05-30T16:24:14.855428Z","shell.execute_reply.started":"2024-05-30T16:24:14.849205Z","shell.execute_reply":"2024-05-30T16:24:14.854651Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import matplotlib.pyplot as plt\ndef imshow(img):\n    npimg = img.numpy()\n    plt.imshow(np.transpose(npimg, (1, 2, 0)))\nfig = plt.figure(figsize=(12, 8))\nimshow(torchvision.utils.make_grid(x_data))","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:16.942173Z","iopub.execute_input":"2024-05-30T16:24:16.942466Z","iopub.status.idle":"2024-05-30T16:24:17.193300Z","shell.execute_reply.started":"2024-05-30T16:24:16.942437Z","shell.execute_reply":"2024-05-30T16:24:17.192588Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"fig = plt.figure(figsize=(12, 8))\nimshow(torchvision.utils.make_grid(y_data))","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.194319Z","iopub.execute_input":"2024-05-30T16:24:17.194570Z","iopub.status.idle":"2024-05-30T16:24:17.443277Z","shell.execute_reply.started":"2024-05-30T16:24:17.194543Z","shell.execute_reply":"2024-05-30T16:24:17.442526Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import matplotlib.pyplot as plt\ndef viz_sample(samples_x , samples_y):\n    #get random sample from the x and y\n    #plot the sample\n    plt.title(\"Real sample\")\n    plt.imshow(samples_x)\n    plt.show()\n    plt.title(\"Fake sample\")\n    plt.imshow(samples_y)\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.444609Z","iopub.execute_input":"2024-05-30T16:24:17.444898Z","iopub.status.idle":"2024-05-30T16:24:17.450315Z","shell.execute_reply.started":"2024-05-30T16:24:17.444853Z","shell.execute_reply":"2024-05-30T16:24:17.449322Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Pre-processing: \n\nChuẩn hóa dữ liệu về khoảng [-1, 1], là một kỹ thuật tiền xử lý dữ liệu được sử dụng để biến đổi các giá trị dữ liệu sao cho nằm trong khoảng từ -1 đến 1. Điều này rất hữu ích trong học máy và mô hình thống kê, giúp các đặc trưng khác nhau có cùng thang đo, cải thiện hiệu suất của các thuật toán phụ thuộc vào tính toán khoảng cách ","metadata":{}},{"cell_type":"code","source":"img = x_data[0]\n\nprint(\"Min : \",img.min())\nprint(\"Max : \",img.max())\n\ndef rescale(x , feature_range=(-1,1)):\n    min , max = feature_range\n    x = x*(max - min) +min\n    \n    return x","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.451761Z","iopub.execute_input":"2024-05-30T16:24:17.452143Z","iopub.status.idle":"2024-05-30T16:24:17.465817Z","shell.execute_reply.started":"2024-05-30T16:24:17.452103Z","shell.execute_reply":"2024-05-30T16:24:17.464881Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# scaled range\nscaled_img = rescale(img)\n\nprint('Scaled min: ', scaled_img.min())\nprint('Scaled max: ', scaled_img.max())","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.467206Z","iopub.execute_input":"2024-05-30T16:24:17.467508Z","iopub.status.idle":"2024-05-30T16:24:17.475060Z","shell.execute_reply.started":"2024-05-30T16:24:17.467478Z","shell.execute_reply":"2024-05-30T16:24:17.474148Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"---\n##  Model\n\n\n\nCác bộ phân biệt, $D_X$ và $D_Y$, trong CycleGAN này là các mạng thần kinh tích chập nhìn thấy một hình ảnh và cố gắng phân loại nó là thật hay giả. Trong trường hợp này, giá trị thực được biểu thị bằng đầu ra gần bằng 1 và giá trị giả gần bằng 0. Bộ phân biệt đối xử có kiến ​​trúc sau:\n\n![](https://ohinfa.files.wordpress.com/2017/04/discriminator.png?w=676)\n\nMạng này nhìn thấy hình ảnh 256x256x3 và chuyển nó qua 5 lớp chập để lấy mẫu hình ảnh xuống theo hệ số 2. Bốn lớp chập đầu tiên có chức năng kích hoạt BatchNorm và ReLu được áp dụng cho đầu ra của chúng và lớp cuối cùng hoạt động như một lớp phân loại xuất ra một giá trị.\n### Hàm 'conv'\n\nĐể xác định các yếu tố phân biệt đối xử, bạn phải sử dụng hàm `conv` được cung cấp để tạo một lớp chập + một lớp định mức lô tùy chọn.","metadata":{}},{"cell_type":"code","source":"import torch.nn as nn\nimport torch.nn.functional as F\n\n#define the conv block for discriminator\ndef conv_block(in_channels , out_channels , kernel_size=4 , strides=2 ,padding_type='reflect' , padding=1 , batch_norm=True):\n    layers = []\n    if(padding_type=='reflect'):\n        pad = nn.ReflectionPad2d(padding)\n        layers.append(pad)\n        \n    if(padding_type=='replicate'):\n        pad = nn.ReplicationPad2d(padding)\n        layers.append(pad)\n        \n        \n    conv_layer = nn.Conv2d(in_channels=in_channels , out_channels=out_channels , \n                          kernel_size=kernel_size , stride=strides , padding=0 ,  bias=False)\n    layers.append(conv_layer)\n    if(batch_norm):\n        bn = nn.InstanceNorm2d(out_channels)\n        layers.append(bn)\n        \n    return nn.Sequential(*layers)","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.479021Z","iopub.execute_input":"2024-05-30T16:24:17.479318Z","iopub.status.idle":"2024-05-30T16:24:17.489615Z","shell.execute_reply.started":"2024-05-30T16:24:17.479281Z","shell.execute_reply":"2024-05-30T16:24:17.488846Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"\n\nHàm `__init__` : với  5 lớp được chỉ định với kernel size of 4x4 mỗi lớp\n\n","metadata":{}},{"cell_type":"markdown","source":"Hàm `forward` xác định cách hình ảnh đầu vào di chuyển qua bộ phân biệt và điều quan trọng nhất là chuyển nó qua các lớp chập của bạn theo thứ tự, với chức năng kích hoạt **ReLu** được áp dụng cho tất cả trừ lớp cuối cùng.","metadata":{}},{"cell_type":"code","source":"class Discriminator(nn.Module):\n    \n    def __init__(self , conv_dim , padding_mode):\n        super(Discriminator , self).__init__()\n        \n        self.input_channels = 3\n        self.output_channels = 1\n        self.padding_mode = padding_mode\n        # 256 - 256 -3 --> 128 - 128 -64\n        self.conv_block1 = conv_block(self.input_channels , conv_dim  , padding_type=padding_mode)\n        # 128 - 128 - 64 --> 64 - 64 - 128\n        self.conv_block2 = conv_block(conv_dim , conv_dim*2 , padding_type=padding_mode)\n        # 64 - 64 - 128 --> 32 - 32 - 256\n        self.conv_block3 = conv_block(conv_dim*2 , conv_dim*4 , padding_type=padding_mode )\n        # 32 - 32 - 256 --> 16 - 16 - 512\n        self.conv_block4 = conv_block(conv_dim*4 , conv_dim*8 , padding_type=padding_mode)\n        # 16 - 16 - 512 --> 8 - 8 - 1024\n        self.conv_block5 = conv_block(conv_dim*8 , self.output_channels , kernel_size=1 ,\n                                      strides=1 ,padding_type=None ,  padding=0 , batch_norm=False)\n        self.leaky_relu = nn.LeakyReLU(negative_slope=0.2, inplace=True)\n        \n    def forward(self , x):\n        x = self.leaky_relu(self.conv_block1(x))\n        x = self.leaky_relu(self.conv_block2(x))\n        x = self.leaky_relu(self.conv_block3(x))\n        x = self.leaky_relu(self.conv_block4(x))\n        \n        out = self.conv_block5(x)\n        \n        return out\n        ","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.491113Z","iopub.execute_input":"2024-05-30T16:24:17.491439Z","iopub.status.idle":"2024-05-30T16:24:17.505842Z","shell.execute_reply.started":"2024-05-30T16:24:17.491410Z","shell.execute_reply":"2024-05-30T16:24:17.504926Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Generators\n\n`G_XtoY` and `G_YtoX` được mã hóa bằng hàm encoder\n\n![](https://miro.medium.com/max/2692/1*_KxtJIVtZjVaxxl-Yl1vJg.png)\n\n","metadata":{}},{"cell_type":"markdown","source":"---\n### Residual Block Class\n\nĐể xác định các trình tạo, bạn phải xác định lớp `ResidualBlock` sẽ giúp bạn kết nối các phần mã hóa và giải mã của các trình tạo. \n![](https://cdn-images-1.medium.com/max/1024/1*S3TlG0XpQZSIpoDIUCQ0RQ.jpeg)\n\nMột Residual Block thường bao gồm các thành phần sau:\n\n   - Layer Norms: Lớp chuẩn hóa như Batch Normalization giúp ổn định quá trình huấn luyện.\n   - Activation Function: Hàm kích hoạt thường dùng là ReLU (Rectified Linear Unit).\n   - Convolutional Layers: Các lớp tích chập (convolutional layers) để học các đặc trưng từ dữ liệu đầu vào.\n   - Skip Connection: Một đường nối tắt (skip connection) bỏ qua một hoặc nhiều lớp, kết nối trực tiếp đầu vào của khối với đầu ra của nó.\n\n\n\n\n\n![](https://cdn-images-1.medium.com/max/1600/1*5zSgo2L71FJos8XendgCvQ.jpeg)\n\n### Residual Functions\n\nResidual Functions, hay hàm dư, là phần của Residual Block chịu trách nhiệm học các đặc trưng từ dữ liệu đầu vào, và sự khác biệt chính giữa đầu vào và đầu ra của Residual Block.\n\nTrong một Residual Block, đầu vào 𝑥 được đưa qua một chuỗi các lớp mạng nơ-ron, thường bao gồm các lớp tích chập (convolutional layers), lớp chuẩn hóa (như Batch Normalization), và hàm kích hoạt (như ReLU). Kết quả của chuỗi này được gọi là Residual Function, kí hiệu là \nF(x).\n\nCông thức toán học\n - Đầu ra y của một Residual Block được tính bằng cách cộng đầu vào ban đầu x với kết quả của Residual Function \n\n     F(x): y=F(x)+x\n    + x là đầu vào của Residual Block.\n    + F(x) là Residual Function, thường là kết quả của một hoặc nhiều lớp tích chập, chuẩn hóa, và kích hoạt.\n    + y là đầu ra của Residual Block.\n","metadata":{}},{"cell_type":"code","source":"\nclass ResidualBlock(nn.Module):\n    \"\"\"\n    apply two conv layers with Batchnormalization and relu activation on first block\n    \"\"\"\n    def __init__(self , conv_dim , padding_mode):\n        super(ResidualBlock , self).__init__()\n        self.conv_dim = conv_dim\n        self.conv_block1 = conv_block(self.conv_dim , self.conv_dim , kernel_size=3 ,\n                                      strides=1 ,padding_type=padding_mode , padding=1 , batch_norm=True)\n        \n        self.conv_block2 = conv_block(self.conv_dim , self.conv_dim , kernel_size=3 ,\n                                      strides=1 ,padding_type=padding_mode, padding=1 , batch_norm=True)\n        \n        self.dropout = nn.Dropout(p=0.3)\n        self.relu = nn.ReLU(inplace=True)\n        \n    def forward(self , x):\n        res_1 = self.dropout(self.relu(self.conv_block1(x)))\n        res_2 = self.conv_block2(res_1)\n        out  = self.relu(res_2 + x)\n        \n        return out","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.507504Z","iopub.execute_input":"2024-05-30T16:24:17.507811Z","iopub.status.idle":"2024-05-30T16:24:17.519462Z","shell.execute_reply.started":"2024-05-30T16:24:17.507782Z","shell.execute_reply":"2024-05-30T16:24:17.518673Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Transpose Convolutional Helper Function\n\n![](data:image/png;base64,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)\n\nTranspose Convolutional Helper Function  là một hàm trợ giúp được sử dụng trong các mạng nơ-ron tích chập (CNN) để thực hiện các phép tích chập chuyển vị (transpose convolution) hoặc còn được gọi là deconvolution. Các hàm này giúp tăng kích thước của đầu vào, ngược lại với tích chập thông thường (convolution) mà giảm kích thước đầu vào.","metadata":{}},{"cell_type":"code","source":"def deconv_block(in_channels , out_channels , kernel_size=4 , strides=2 , padding=1 , batch_norm=True):\n    layers = []        \n    deconv_layer = nn.ConvTranspose2d(in_channels=in_channels , out_channels=out_channels ,\n                                      kernel_size=kernel_size , stride=strides , padding=padding , bias=False)\n    layers.append(deconv_layer)\n    if(batch_norm):\n        bn = nn.InstanceNorm2d(out_channels)\n        layers.append(bn)\n        \n    return nn.Sequential(*layers)","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.520901Z","iopub.execute_input":"2024-05-30T16:24:17.521171Z","iopub.status.idle":"2024-05-30T16:24:17.531353Z","shell.execute_reply.started":"2024-05-30T16:24:17.521144Z","shell.execute_reply":"2024-05-30T16:24:17.530630Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class CycleGenerator(nn.Module):\n    def __init__(self , conv_dim , n_resblocks , padding_mode):\n        super(CycleGenerator , self).__init__()\n        self.conv_dim = conv_dim\n        self.n_resblocks = n_resblocks\n        self.in_channels = 3\n        self.out_channels = 3\n        self.encoder = nn.Sequential(\n            conv_block(self.in_channels , conv_dim , padding_type=padding_mode) ,\n            nn.ReLU(True),\n            conv_block(conv_dim , conv_dim*2 , padding_type=padding_mode), \n            nn.ReLU(True),\n            conv_block(conv_dim*2 , conv_dim*4 , padding_type=padding_mode),\n            nn.ReLU(True)\n        )\n        layers = []\n        for i_res in range(self.n_resblocks):\n            layers.append(ResidualBlock(conv_dim*4 , padding_mode=padding_mode))\n        self.res_block = nn.Sequential(*layers)\n        \n        self.decoder =nn.Sequential(\n            deconv_block(conv_dim*4 , conv_dim*2 ),\n            nn.ReLU(True),\n            deconv_block(conv_dim*2, conv_dim ),\n            nn.ReLU(True),\n            deconv_block(conv_dim , self.out_channels , batch_norm=False),\n            nn.Tanh()\n        )\n    def forward(self, x):\n        #inference throght the encoder\n        x = self.encoder(x)\n        x = self.res_block(x)\n        x = self.decoder(x)\n        \n        return x","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.532316Z","iopub.execute_input":"2024-05-30T16:24:17.532549Z","iopub.status.idle":"2024-05-30T16:24:17.545621Z","shell.execute_reply.started":"2024-05-30T16:24:17.532525Z","shell.execute_reply":"2024-05-30T16:24:17.544925Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"---\n## Create the complete network\n\nSau nhiều bước thì các tham số đã cho sẽ phù hợp cho việc đào tạo.\n\nĐầu tiên,ta tạo hai bộ phân biệt, một để kiểm tra xem hình ảnh mẫu $X$ có phải là thật hay không và một để kiểm tra xem hình ảnh mẫu $Y$ có phải là thật hay không. Sau đó là máy phát điện. Khởi tạo hai trong số chúng, một để chuyển một bức tranh thành một bức ảnh thực tế và một để chuyển một bức ảnh thành một bức tranh.","metadata":{}},{"cell_type":"code","source":"def weights_init_normal(m):\n    classname = m.__class__.__name__\n    if(classname.find('Conv') != -1):\n        torch.nn.init.normal(m.weight.data , 0.0 , 0.02)\n    elif(classname.find('Batchnorm2d')!= -1):\n        torch.nn.init.normal(m.weight.data , 0.0 , 0.02)\n        torch.nn.init.constant(m.bias.data , 0.0)","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.546767Z","iopub.execute_input":"2024-05-30T16:24:17.547059Z","iopub.status.idle":"2024-05-30T16:24:17.557552Z","shell.execute_reply.started":"2024-05-30T16:24:17.547027Z","shell.execute_reply":"2024-05-30T16:24:17.556774Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"def create_model(g_conv_dim=64 , d_conv_dim=64 , n_resblocks = 9 , padding_mode='reflect'):\n    G_X2Y = CycleGenerator(g_conv_dim ,n_resblocks , padding_mode)\n    G_Y2X = CycleGenerator(g_conv_dim ,n_resblocks , padding_mode)\n    \n    D_X = Discriminator(d_conv_dim  ,padding_mode)\n    D_Y = Discriminator(d_conv_dim , padding_mode)\n    \n    G_X2Y.apply(weights_init_normal)\n    G_Y2X.apply(weights_init_normal)\n    D_X.apply(weights_init_normal)\n    D_Y.apply(weights_init_normal)\n    \n    device = 'cuda:0' if torch.cuda.is_available() else 'cpu'\n    #move the models to gpu if availabele\n    G_X2Y.to(device)\n    G_Y2X.to(device)\n    D_X.to(device)\n    D_Y.to(device)\n    \n    return G_X2Y , G_Y2X , D_X , D_Y","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.558701Z","iopub.execute_input":"2024-05-30T16:24:17.559045Z","iopub.status.idle":"2024-05-30T16:24:17.568900Z","shell.execute_reply.started":"2024-05-30T16:24:17.559013Z","shell.execute_reply":"2024-05-30T16:24:17.567883Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"G_X2Y , G_Y2X , D_X , D_Y = create_model(padding_mode='replicate')","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:17.570263Z","iopub.execute_input":"2024-05-30T16:24:17.570560Z","iopub.status.idle":"2024-05-30T16:24:18.107952Z","shell.execute_reply.started":"2024-05-30T16:24:17.570531Z","shell.execute_reply":"2024-05-30T16:24:18.107074Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"criterion_Idt = nn.L1Loss()\n#define loss functions which are helpful\ndef real_mse_loss(D_out):\n    # how close is the produced output from being \"real\"?\n    return torch.mean((D_out -1)**2)\n\ndef fake_mse_loss(D_out):\n    # how close is the produced output from being \"false\"?\n    return torch.mean(D_out**2)\n    \n\ndef cycle_consistency_loss(real_im, reconstructed_im, lambda_weight):\n    # calculate reconstruction loss \n    # return weighted loss\n    reconstruct_loss = torch.mean(torch.abs(real_im-reconstructed_im))\n    return reconstruct_loss*lambda_weight\n\ndef identity_loss(lambda_idt , idt_B ,real_B , idt_A , real_A , lambda_weight):\n    \n    if lambda_idt > 0:\n        # G_A should be identity if real_B is fed: ||G_A(B) - B||\n        loss_idt_A = criterion_Idt(idt_B, real_B) * lambda_weight * lambda_idt\n        # G_B should be identity if real_A is fed: ||G_B(A) - A||\n        loss_idt_B = criterion_Idt(idt_A, real_A) * lambda_weight * lambda_idt\n    else:\n        self.loss_idt_A = 0\n        self.loss_idt_B = 0\n        \n    return loss_idt_A + loss_idt_B","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:18.109258Z","iopub.execute_input":"2024-05-30T16:24:18.109556Z","iopub.status.idle":"2024-05-30T16:24:18.118787Z","shell.execute_reply.started":"2024-05-30T16:24:18.109526Z","shell.execute_reply":"2024-05-30T16:24:18.117853Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"\nimport torch.optim as optim\nfrom torch.optim.lr_scheduler import MultiStepLR\n\n# Tối ưu hóa tham số với thuật toán Adam\nlr=0.0002\nbeta1=0.5\nbeta2= 0.999\n\ng_params = list(G_X2Y.parameters()) + list(G_Y2X.parameters())  # Get generator parameters\n\n# Create optimizers for the generators and discriminators\ng_optimizer   = optim.Adam(g_params, lr, [beta1, beta2])\nd_x_optimizer = optim.Adam(D_X.parameters(), lr, [beta1, beta2])\nd_y_optimizer = optim.Adam(D_Y.parameters(), lr, [beta1, beta2])\n\nlambda1 = 3500\nlambda2 = 8500\n","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:18.120229Z","iopub.execute_input":"2024-05-30T16:24:18.120603Z","iopub.status.idle":"2024-05-30T16:24:18.131313Z","shell.execute_reply.started":"2024-05-30T16:24:18.120563Z","shell.execute_reply":"2024-05-30T16:24:18.130532Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"---\n\n## Training a CycleGAN\n\n\n\n**Training the Discriminators**\n1. Tính tổn thất phân biệt $D_X$ trên ảnh thật\n2. Tạo hình ảnh giả trông giống tên miền $X$ dựa trên hình ảnh thật trong miền $Y$\n3. Tính khoản lỗ giả cho $D_X$\n4. Tính tổng tổn thất và thực hiện lan truyền ngược và tối ưu hóa $D_X$\n5. Chỉ lặp lại các bước 1-4 với $D_Y$ và tên miền của bạn đã được chuyển đổi!\n\n**Training the Generators**\n1. Tạo hình ảnh giả giống tên miền $X$ dựa trên hình ảnh thật trong miền $Y$\n2. Tính toán tổn thất của máy phát điện dựa trên cách $D_X$ phản ứng với $X$ giả\n3. Tạo hình ảnh $\\hat{Y}$ *được tái tạo* dựa trên hình ảnh $X$ giả được tạo ở bước 1\n4. Tính toán độ mất nhất quán của chu kỳ bằng cách so sánh các kết quả tái tạo với hình ảnh $Y$ thực\n5. Lặp lại các bước 1-4 chỉ trao đổi tên miền\n6. Cộng tất cả các tổn thất của trình tạo và tái thiết rồi thực hiện lan truyền ngược + tối ưu hóa\n![](https://www.researchgate.net/profile/Hritwick_Banerjee/publication/329368817/figure/fig7/AS:717806182076420@1548149738922/A-Training-Architecture-of-CycleGAN-B-Training-Architecture-of-a-CyCADA.png)","metadata":{}},{"cell_type":"code","source":"\ndef training_loop(dataloader_X, dataloader_Y, test_dataloader_X , test_dataloader_Y, \n                  n_epochs=10000):\n    \n    print_every=50\n    checkpoint_every=1000\n    # keep track of losses over time\n    losses = []\n\n    test_iter_X = iter(dataloader_X)\n    test_iter_Y = iter(dataloader_Y)\n\n    # Get some fixed data from domains X and Y for sampling. These are images that are held\n    # constant throughout training, that allow us to inspect the model's performance.\n    fixed_X = test_iter_X.next()[0]\n    fixed_Y = test_iter_Y.next()[0]\n    fixed_X = rescale(fixed_X) # make sure to scale to a range -1 to 1\n    fixed_Y = rescale(fixed_Y)\n\n    # batches per epoch\n    iter_X = iter(dataloader_X)\n    iter_Y = iter(dataloader_Y)\n    batches_per_epoch = min(len(iter_X), len(iter_Y))\n\n    for epoch in range(1, n_epochs+1):\n        \n        \n        # Reset iterators for each epoch\n        if epoch % batches_per_epoch == 0:\n            iter_X = iter(dataloader_X)\n            iter_Y = iter(dataloader_Y)\n\n        images_X = iter_X.next()\n        images_X = rescale(images_X) # make sure to scale to a range -1 to 1\n\n        images_Y = iter_Y.next()\n        images_Y = rescale(images_Y)\n        \n        # move images to GPU if available (otherwise stay on CPU)\n        device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n        images_X = images_X.to(device)\n        images_Y = images_Y.to(device)\n\n\n       \n        ##############  TRAIN THE DISCRIMINATORS   ##################################\n\n\n      \n\n        # 1. Tính toán tổn thất phân biệt đối xử trên ảnh thật trong miền X\n        Dx_real_out = D_X(images_X)\n        Dx_real_loss = real_mse_loss(Dx_real_out)\n        \n        # 2. Tạo hình ảnh giả giống miền X dựa trên hình ảnh thật ở miền Y\n        Gx_fake = G_Y2X(images_Y)\n        \n        # 3. Tính tổn thất giả cho D_X trên X giả\n        Dx_fake_out= D_X(Gx_fake)\n        Dx_fake_loss = fake_mse_loss(Dx_fake_out)\n        \n        # 4.Tính tổn thất giả cho D_X trên X giả\n        d_x_loss = Dx_real_loss + Dx_fake_loss\n       \n        d_x_optimizer.zero_grad()\n        \n        d_x_loss.backward()\n        #optimze the descrimintaer x\n        d_x_optimizer.step()\n\n        \n     \n        Dy_real_out = D_Y(images_Y)\n        Dy_real_loss = real_mse_loss(Dy_real_out)\n        \n        # tạo ảnh giả trông giống miền y trên ảnh thật ở miền x\n        Gy_fake = G_X2Y(images_X)\n        \n      \n        Dy_fake_out = D_Y(Gy_fake)\n        Dy_fake_loss = fake_mse_loss(Dy_fake_out)\n        \n        d_y_loss = Dy_fake_loss + Dy_real_loss\n        #reset the optimizer\n        d_y_optimizer.zero_grad()\n        #backprop the loss\n        d_y_loss.backward()\n        #optimize the model\n        d_y_optimizer.step()\n        \n\n\n      \n        ################    TRAIN THE GENERATORS       #############\n      \n\n        ##     Tạo hình ảnh X giả và hình ảnh Y được tái tạo   ##\n\n       \n        Gx_fake = G_Y2X(images_Y)\n        Dx_fake_out = D_X(Gx_fake)\n        Gx_fake_loss = real_mse_loss(Dx_fake_out)\n        Gy_fake_re =  G_X2Y(Gx_fake)   \n        Gy_reconstruct_loss = cycle_consistency_loss(images_Y , Gy_fake_re , lambda_weight=10)\n\n        ##    Tạo ảnh Y giả và ảnh X được tái tạo  ##\n        Gy_fake = G_X2Y(images_X)\n        Dy_fake_out = D_Y(Gy_fake)\n        Gy_fake_loss = real_mse_loss(Dy_fake_out)\n        Gx_fake_re = G_Y2X(Gy_fake)\n        Gx_reconstuct_loss = cycle_consistency_loss(images_X , Gx_fake_re , lambda_weight=10)\n        \n        idt_B = G_X2Y(images_Y)\n        idt_A = G_Y2X(images_X)\n        id_loss = identity_loss(0.1 , idt_B , images_Y ,\n                                idt_A , images_X , lambda_weight=8)\n\n        g_total_loss = Gx_fake_loss + Gy_fake_loss + Gy_reconstruct_loss + Gx_reconstuct_loss + id_loss\n        \n        \n        #optimize the generator model\n        g_optimizer.zero_grad()\n        #back prop the loss\n        g_total_loss.backward()\n        #optimize the generator model\n        g_optimizer.step()\n\n        \n       \n        if epoch % print_every == 0:\n         \n            losses.append((d_x_loss.item(), d_y_loss.item(), g_total_loss.item()))\n            print('Epoch [{:5d}/{:5d}] | d_X_loss: {:6.4f} | d_Y_loss: {:6.4f} | g_total_loss: {:6.4f}'.format(\n                    epoch, n_epochs, d_x_loss.item(), d_y_loss.item(), g_total_loss.item()))\n\n            \n        sample_every=1000\n       \n        if epoch % sample_every == 0:\n            G_Y2X.eval() # set generators to eval mode for sample generation\n            G_X2Y.eval()\n            save_samples(epoch, fixed_Y, fixed_X, G_Y2X, G_X2Y, batch_size=16)\n            G_Y2X.train()\n            G_X2Y.train()\n\n    return losses\n","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:18.132613Z","iopub.execute_input":"2024-05-30T16:24:18.133180Z","iopub.status.idle":"2024-05-30T16:24:18.162343Z","shell.execute_reply.started":"2024-05-30T16:24:18.133140Z","shell.execute_reply":"2024-05-30T16:24:18.161515Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"\nimport os\nimport pdb\nimport pickle\nimport argparse\n\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\n# import torch\nimport torch\n\n\n# numpy & scipy imports\nimport numpy as np\nimport scipy\nimport scipy.misc\n\n\ndef checkpoint(iteration, G_XtoY, G_YtoX, D_X, D_Y, checkpoint_dir='checkpoints_cyclegan'):\n    \"\"\"Saves the parameters of both generators G_YtoX, G_XtoY and discriminators D_X, D_Y.\n        \"\"\"\n    G_XtoY_path = os.path.join(checkpoint_dir, 'G_XtoY.pkl')\n    G_YtoX_path = os.path.join(checkpoint_dir, 'G_YtoX.pkl')\n    D_X_path = os.path.join(checkpoint_dir, 'D_X.pkl')\n    D_Y_path = os.path.join(checkpoint_dir, 'D_Y.pkl')\n    torch.save(G_XtoY.state_dict(), G_XtoY_path)\n    torch.save(G_YtoX.state_dict(), G_YtoX_path)\n    torch.save(D_X.state_dict(), D_X_path)\n    torch.save(D_Y.state_dict(), D_Y_path)\n\n\ndef merge_images(sources, targets, batch_size=16):\n    \"\"\"Creates a grid consisting of pairs of columns, where the first column in\n        each pair contains images source images and the second column in each pair\n        contains images generated by the CycleGAN from the corresponding images in\n        the first column.\n        \"\"\"\n    _, _, h, w = sources.shape\n    row = int(np.sqrt(batch_size))\n    merged = np.zeros([3, row*h, row*w*2])\n    for idx, (s, t) in enumerate(zip(sources, targets)):\n        i = idx // row\n        j = idx % row\n        merged[:, i*h:(i+1)*h, (j*2)*h:(j*2+1)*h] = s\n        merged[:, i*h:(i+1)*h, (j*2+1)*h:(j*2+2)*h] = t\n    merged = merged.transpose(1, 2, 0)\n    return merged\n    \n\ndef to_data(x):\n    \"\"\"Converts variable to numpy.\"\"\"\n    if torch.cuda.is_available():\n        x = x.cpu()\n    x = x.squeeze(0)\n    x = x.data.numpy()\n    x = np.transpose(x,(1,2,0))\n    x = ((x +1)*255 / (2)).astype(np.uint8) # rescale to 0-255\n    return x\n\ndef save_samples(iteration, fixed_Y, fixed_X, G_YtoX, G_XtoY, batch_size=16, sample_dir='samples_cyclegan'):\n    \"\"\"Saves samples from both generators X->Y and Y->X.\n        \"\"\"\n    # move input data to correct device\n    device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n\n    fake_X = G_YtoX(fixed_Y.unsqueeze(0).to(device))\n    fake_Y = G_XtoY(fixed_X.unsqueeze(0).to(device))\n    \n    X, fake_X = to_data(fixed_X), to_data(fake_X)\n    Y, fake_Y = to_data(fixed_Y), to_data(fake_Y)\n    \n\n    viz_sample(X,fake_Y)","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:18.163585Z","iopub.execute_input":"2024-05-30T16:24:18.163985Z","iopub.status.idle":"2024-05-30T16:24:18.187665Z","shell.execute_reply.started":"2024-05-30T16:24:18.163955Z","shell.execute_reply":"2024-05-30T16:24:18.186715Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"n_epochs = 3000 \n\nlosses = training_loop(X_dataloader, Y_dataloader, X_dataloader, Y_dataloader, n_epochs=n_epochs)","metadata":{"execution":{"iopub.status.busy":"2024-05-30T16:24:18.189226Z","iopub.execute_input":"2024-05-30T16:24:18.189604Z","iopub.status.idle":"2024-05-30T17:26:26.111508Z","shell.execute_reply.started":"2024-05-30T16:24:18.189549Z","shell.execute_reply":"2024-05-30T17:26:26.110473Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Conversion Visualization","metadata":{}},{"cell_type":"code","source":"test_iter_X = iter(X_dataloader)\ntest_iter_Y = iter(Y_dataloader)\n\n# Get some fixed data from domains X and Y for sampling. These are images that are held\n# constant throughout training, that allow us to inspect the model's performance.\nfixed_X = test_iter_X.next()\nfixed_Y = test_iter_Y.next()\nfixed_X = rescale(fixed_X) # make sure to scale to a range -1 to 1\nfixed_Y = rescale(fixed_Y)\nfor fix_x , fix_y in zip(fixed_X , fixed_Y):\n    save_samples(0, fix_y, fix_x, G_Y2X, G_X2Y, batch_size=1)","metadata":{"execution":{"iopub.status.busy":"2024-05-30T17:26:26.113341Z","iopub.execute_input":"2024-05-30T17:26:26.113691Z","iopub.status.idle":"2024-05-30T17:26:32.702193Z","shell.execute_reply.started":"2024-05-30T17:26:26.113656Z","shell.execute_reply":"2024-05-30T17:26:32.701377Z"},"trusted":true},"execution_count":null,"outputs":[]}]}