從風景相片到莫內畫作:探索CycleGAN的模型架構
最近,ChatGPT推出了一項令人興奮的功能:將普通相片轉換為吉卜力動畫風格的圖片!這項技術瞬間引爆熱潮,網友們爭相將自拍、風景照甚至寵物照片轉成宮崎駿筆下的夢幻畫風。然而,這股熱潮也讓OpenAI的伺服器不堪重負,最終不得不暫停服務。這場風波讓人不禁思考有沒有可能,我們自己在自…
從風景相片到莫內畫作:探索CycleGAN的模型架構

最近,ChatGPT推出了一項令人興奮的功能:將普通相片轉換為吉卜力動畫風格的圖片!這項技術瞬間引爆熱潮,網友們爭相將自拍、風景照甚至寵物照片轉成宮崎駿筆下的夢幻畫風。然而,這股熱潮也讓OpenAI的伺服器不堪重負,最終不得不暫停服務。這場風波讓人不禁思考有沒有可能,我們自己在自己的電腦上,做出類似效果的生成模型,於似乎直接聯想到生成式對抗網絡(GAN),一種強大的神經網路架構,特別適合相片風格轉換的任務。
在眾多GAN模型中,CycleGAN以其無監督學習的獨特優勢脫穎而出。它能將風景相片轉為莫內(Claude Monet)的印象派畫作,或反過來將畫作還原為相片風格,無需成對的訓練數據。本文將帶你深入探索CycleGAN的核心組件 — — 生成器、鑑別器和損失函數,揭開這項技術如何將平凡相片化為藝術傑作的秘密。
什麼是CycleGAN?
CycleGAN是一種生成對抗網絡(GAN)的變體,專為無監督圖像風格轉換設計。它由兩個生成器和兩個鑑別器組成,透過對抗學習實現兩個不同風格域之間的映射。例如:
- 風景相片 → 莫內風格:將真實相片轉為印象派畫作。
- 莫內風格 → 風景相片:將畫作還原為相片風格。
CycleGAN的獨特之處在於「循環一致性」(Cycle Consistency),確保圖像在兩個域之間的轉換是可逆的,從而保留原始內容。這使得它無需成對的訓練數據(這一點大大降低處理數據集的成本),適用於風景相片與莫內畫作的風格轉換。
CycleGAN的起源:CycleGAN由Jun-Yan Zhu、Taesung Park、Phillip Isola和Alexei A. Efros於2017年提出,論文《Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks》。
這篇論文開創了無監督圖像轉換的新領域,廣泛應用於藝術生成、照片增強等場景。原始論文可在arXiv獲取,推薦對技術細節感興趣的讀者可以去深入閱讀。
以下依據論文所提出的網路架構,並作一些簡化,一步步建立相片風格轉換模型:
生成器:打造莫內風格的繪畫師
生成器是CycleGAN的核心,負責將輸入圖像(例如風景相片)轉為目標風格(莫內畫作)。其架構基於ResNet(殘差網絡),包含以下五個主要網路層塊:
1. 初始卷積層
- 結構:7x7卷積核,3通道(RGB)輸入,64通道輸出,步幅1,填充3,後接實例正規化(InstanceNorm)和ReLU激活。
- 功能:提取低級特徵(如邊緣、紋理),保持圖像尺寸(例如256x256)。大卷積核捕捉廣泛上下文,實例正規化穩定訓練,適合風格轉換。張量變化:輸入256x256x3 → 輸出256x256x64(尺寸不變)。
2. 下採樣階段(卷積層)
- 結構:兩層3x3卷積,步幅2,通道數從64增加到128,再到256,每層後接實例正規化和ReLU。
- 功能:壓縮圖像尺寸(256x256 → 128x128 → 64x64),提取高級特徵(如形狀、結構)。步幅2快速降低計算量,通道數倍增增強特徵表達。張量變化:256x256x64 → 128x128x128 → 64x64x256。
3. 殘差塊(Residual Blocks)
- 結構:6個殘差塊,每塊包含兩個3x3卷積(通道數256,步幅1),後接實例正規化,透過殘差連接將輸入與輸出相加。
- 功能:學習風格轉換(例如莫內的筆觸和色彩),保留圖像結構。殘差連接緩解深層網絡的梯度消失,確保結構信息不丟失。張量變化:64x64x256(尺寸不變)。
4. 上採樣階段(逆卷積、轉置卷積)
- 結構:兩層3x3轉置卷積,步幅2,通道數從256減少到128,再到64,每層後接實例正規化和ReLU。
- 功能:恢復圖像尺寸(64x64 → 128x128 → 256x256),生成目標風格的細節。與下採樣對稱,確保尺寸精確恢復。張量變化:64x64x256 → 128x128x128 → 256x256x64。
5. 輸出卷積層
- 結構:7x7卷積核,64通道輸入,3通道(RGB)輸出,後接Tanh激活。
- 功能:生成最終的莫內風格圖像,並且透過激發函數將素質像素值映射到[-1, 1]。與初始卷積對稱,平滑輸出,Tanh匹配數據預處理 。張量變化:256x256x64 → 256x256x3。
鑑別器:真假圖像的鑑定師
鑑別器採用PatchGAN架構,判斷圖像是真實的(來自數據集)還是生成的(來自生成器)。其輸出是一個特徵圖,每個元素對應圖像的一個「補丁」區域(約70x70像素),表示局部真實性。鑑別器包含以下五層:
1. 第一卷積層
- 結構:4x4卷積核,3通道輸入,64通道輸出,步幅2,填充1,後接LeakyReLU(斜率0.2)。
- 功能:提取低級特徵,尺寸減半(256x256 → 128x128)。4x4卷積核平衡效率與特徵捕捉,LeakyReLU增強梯度流動。
2. 第二卷積層
- 結構:4x4卷積核,64通道輸入,128通道輸出,步幅2,填充1,後接批次正規化和LeakyReLU。
- 功能:提取中級特徵,尺寸減半(128x128 → 64x64)。加入批次正規化穩定單樣本處理,通道數倍增增強表達能力。
3. 第三卷積層
- 結構:4x4卷積核,128通道輸入,256通道輸出,步幅2,填充1,後接批次正規化和LeakyReLU。
- 功能:提取高級特徵,尺寸減半(64x64 → 32x32)。聚焦風格特徵,增強對細微差異的敏感性。
4. 第四卷積層
- 結構:4x4卷積核,256通道輸入,512通道輸出,步幅1,填充1,後接批次正規化和LeakyReLU。聚焦高級特徵,並保持尺寸(32x32)。步幅1保留局部信息,512通道可以提升鑑別能力。
5. 輸出卷積層
- 結構:4x4卷積核,512通道輸入,1通道輸出,步幅1,填充1,不使用激發函數。
- 功能:生成~30x30x1特徵圖,每個元素判斷一個補丁的真實性。PatchGAN局部判斷,計算效率高,適合高分辨率圖像。
損失函數:模型訓練的指南針
損失函數引導生成器和鑑別器的訓練,確保生成圖像逼真、結構保留且風格一致。CycleGAN的損失函數包括三個核心部分(相關數學含是可以參考原始論文),並透過權重調整平衡生成效果:
1. 對抗損失(GAN Loss)
- 生成器試圖欺騙鑑別器,使生成的圖像被判為真;鑑別器區分真實和生成圖像。使用均方誤差(MSE)損失,生成器目標為全1(真),鑑別器目標為真圖像全1、假圖像全0。促進生成逼真圖像,捕捉莫內風格的筆觸和色彩。
2. 循環一致性損失(Cycle Consistency Loss)
- 確保圖像在兩個域之間的轉換可逆,例如相片 → 莫內 → 相片應接近原始相片。使用L1損失計算循環圖像與原始圖像的像素級差異,權重lambda_cycle=10.0。保留圖像內容和結構,解決無監督學習的挑戰。
- 權重調整影響:
- 增加 lambda_cycle(例如從10.0到20.0):強調結構保留,生成圖像的內容(例如風景的形狀、布局)更接近原始相片,但可能限制莫內風格的表現力,導致畫作風格不夠鮮明。
- 降低 lambda_cycle(例如從10.0到5.0):允許生成器更專注於風格轉換,生成的莫內畫作可能有更強烈的印象派筆觸和色彩,但可能犧牲部分結構細節,導致圖像內容失真。
3. 身份損失(Identity Loss)
確保生成器對相同域的圖像不進行不必要改變,例如莫內畫作輸入莫內生成器應保持不變。使用L1損失(L1 Norm)計算輸出與輸入的像素級差異,權重lambda_identity=5.0。增強生成器穩定性和一致性,提升生成質量。
- 權重調整影響:
- 增加 lambda_identity(例如從5.0到10.0):生成器對相同域圖像的改變更少,例如莫內畫作輸入莫內生成器時幾乎不變,增強穩定性,但可能限制生成器學習新風格的能力,導致轉換效果過於保守。
- 降低 lambda_identity(例如從5.0到2.0):生成器對相同域圖像的改變更自由,可能引入不必要的風格變化,導致生成圖像不穩定或出現偽影,但有利於探索更多樣的風格表現。
實作Cycle GAN
數據集:
下載方法參考Kaggle說明或是直接參考程式碼
環境設置:
安裝PyTorch、Torchvision等庫。
所有模型訓練皆在Colab環境下使用GPU(例如A100、T4)加速訓練。
本次的實作特別選擇PyTorch
過去,我大多使用TensorFlow來建構神經網絡,但TensorFlow在CUDA版本與GPU驅動程式的相容性上常遇到問題,尤其是在個人筆電或個人主機上設置訓練環境時,版本衝突常常導致GPU無法正常使用。為了解決這一問題,這次CycleGAN的實現改用PyTorch,其環境配置更為簡單且穩定,對CUDA和GPU驅動程式的相容性要求更低,讓開發者能更專注於模型訓練而非環境調試。
以下程式碼可以直接在Colab上直接運行
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, Dataset
from torchvision import transforms
from PIL import Image
import os
import numpy as np
import matplotlib.pyplot as plt
# 定義數據集類別
#
class ImageDataset(Dataset):
def __init__(self, photo_dir, target_dir, transform=None):
self.photo_dir = photo_dir
self.target_dir = target_dir
self.transform = transform
self.photo_files = sorted(os.listdir(photo_dir))
self.target_files = sorted(os.listdir(target_dir))
def __len__(self):
return max(len(self.photo_files), len(self.target_files))
# 取得資料集資料照片與圖片的方法
def __getitem__(self, index):
photo_idx = index % len(self.photo_files)
target_idx = index % len(self.target_files)
photo_path = os.path.join(self.photo_dir,self.photo_files[photo_idx])
target_path = os.path.join(self.target_dir, self.target_files[target_idx])
photo = Image.open(photo_path).convert("RGB")
target = Image.open(target_path).convert("RGB")
if self.transform:
photo = self.transform(photo)
target = self.transform(target)
return photo, target
# 定義生成器(使用Restnet)
class Generator(nn.Module):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.main = nn.Sequential(
# 初步捲積層,提取低級特徵(邊緣、紋理),7x7捲積核保持空間訊息,用以適合圖像風格轉換
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, stride=1, padding=3),
nn.InstanceNorm2d(num_features=64), #對每一個通道做獨立正規化(減少批量依賴保留風格相關特徵)
nn.ReLU(inplace=True),
# 輸出 256x256
# 對圖片更多細節進行採樣
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, stride=2, padding=1),
nn.InstanceNorm2d(num_features=128),
nn.ReLU(inplace=True),
# 輸出128x128
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, stride=2, padding=1),
nn.InstanceNorm2d(num_features=256),
nn.ReLU(inplace=True),
# 輸出64x64
# 建立殘差區塊 (可重複區塊 range(n))
# 透過殘差塊緩解梯度消失問題,通道數保持256是其轉注於特徵轉換,而非空間變化
# 殘差塊連接可以確保輸入圖像的空間訊息不丟失,保留結構細節
*[self.residul_block(256) for _ in range(9)],
# 使用反捲積,重建圖片
nn.ConvTranspose2d(in_channels=256, out_channels=128, kernel_size=3, stride=2, padding=1, output_padding=1),
nn.InstanceNorm2d(128),
nn.ReLU(inplace=True),
# 輸出128x128
nn.ConvTranspose2d(in_channels=128, out_channels=64, kernel_size=3, stride=2, padding=1, output_padding=1),
nn.InstanceNorm2d(64),
nn.ReLU(inplace=True),
# 輸出256x256
nn.Conv2d(in_channels=64,out_channels=3, kernel_size=7, stride=1, padding=3),
nn.Tanh(),
# 輸出3x256x256
)
def residul_block(self, filters):
return nn.Sequential(
nn.Conv2d(filters, filters, kernel_size=3, stride=1, padding=1),
nn.InstanceNorm2d(filters),
nn.ReLU(inplace=True),
nn.Conv2d(filters, filters, kernel_size=3, stride=1, padding=1),
nn.InstanceNorm2d(filters),
)
def forward(self, x):
return self.main(x)
# 定義鑑別器(Patch GAN)
class Discriminator(nn.Module):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.main = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=4, stride=2, padding=1),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(in_channels=64, out_channels=128, kernel_size=4, stride=2, padding=1),
nn.InstanceNorm2d(num_features=128),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(in_channels=128, out_channels=256, kernel_size=4, stride=2, padding=1),
nn.InstanceNorm2d(num_features=256),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(in_channels=256, out_channels=512, kernel_size=4, stride=1, padding=1),
nn.InstanceNorm2d(num_features=512),
nn.LeakyReLU(0.2, inplace=True),
# 生成1D特徵圖,這是Patch GAN的核心,使鑑別器判斷局部特徵的真實性,而非全域
# 不使用激發函數(直接輸出分數),便於後續loss計算
nn.Conv2d(in_channels=512, out_channels=1, kernel_size=4, stride=1, padding=1),
)
def forward(self, x):
return self.main(x)
#Download Monet Dataset
#Need to upload kaggle.json (API-KEY) first!
#===================================================
!mkdir ~/.kaggle
!mkdir /content/.kaggle
!cp /content/kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json
#此處的user name和API KEY要去kaggle註冊,並下載kaggle.json,然後上傳到colab上
api_token = {"username":"Your User Name","key":"Your API KEY"}
import json
with open('/content/.kaggle/kaggle.json', 'w') as file:
json.dump(api_token, file)
!chmod 600 ~/.kaggle/kaggle.json
!kaggle competitions download -c gan-getting-started
!unzip /content/gan-getting-started.zip
photo_dir = "/content/photo_jpg"
target_dir = "/content/monet_jpg"
#
# 數據預處理
#A
batch_size = 8
transform = transforms.Compose([
transforms.Resize((256,256)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
])
# Dataset Create for Model
dataset = ImageDataset(photo_dir, target_dir, transform=transform)
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True)
# 初始化模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
Gen_photo_to_target = Generator().to(device)
Gen_target_to_photo = Generator().to(device)
Disc_origin_photo = Discriminator().to(device)
Disc_target_photo = Discriminator().to(device)
# 定義loss function (計算特徵距離損失,使用MSE)
# 對抗損失,包含生成器對抗損失與鑑別器對抗損失
Gan_loss = nn.MSELoss()
# 循環損失,用來確保兩個圖域轉換是可逆的
# 原始圖片轉成目標圖像再轉回原始圖片時要接近原始圖像
# 目標圖像轉成原始圖片再轉成目標圖像時要接近目標圖像
Cycle_loss = nn.MSELoss()
# 身分輸出損失,用來確保生成器對相同圖域的圖像進行不必要的改變
# 將目標圖像輸入<原始相片轉目標圖片生成器>應生成接近目標圖像的圖
# 將原始相片輸入<目標圖像轉原始相片生成器>應生成接近原始相片的圖
identity_loss = nn.L1Loss()
# 定義優化器
#
Gen_parameters = list(Gen_photo_to_target.parameters()) + list(Gen_target_to_photo.parameters())
optimizer_Gen = optim.Adam(Gen_parameters, lr=0.0002, betas=(0.5, 0.999))
optimizer_Disc_photo = optim.Adam(Disc_origin_photo.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_Disc_target = optim.Adam(Disc_target_photo.parameters(), lr=0.0002, betas=(0.5, 0.999))
# load model weights from google driver,
# if needed, set load_pretrained as True
load_pretrained = True
if load_pretrained:
import google.colab.drive
google.colab.drive.mount('/content/drive')
!cp /content/drive/MyDrive/Gen_photo_to_target_monet.pth /content
!cp /content/drive/MyDrive/Gen_target_to_photo_monet.pth /content
!cp /content/drive/MyDrive/Disc_origin_photo_monet.pth /content
!cp /content/drive/MyDrive/Disc_target_photo_monet.pth /content
Gen_photo_to_target.load_state_dict(torch.load("Gen_photo_to_target_monet.pth"))
Gen_target_to_photo.load_state_dict(torch.load("Gen_target_to_photo_monet.pth"))
Disc_origin_photo.load_state_dict(torch.load("Disc_origin_photo_monet.pth"))
Disc_target_photo.load_state_dict(torch.load("Disc_target_photo_monet.pth"))
# 訓練循環網路
num_epochs = 3
# loss weight
lambda_cycle = 10.0
lambda_identity = 5.0
for epoch in range(num_epochs):
for i, (origin_photo, target_photo) in enumerate(dataloader):
origin_photo = origin_photo.to(device)
target_photo = target_photo.to(device)
# 訓練生成器
#
optimizer_Gen.zero_grad() # 梯度歸零
# 生成圖像
fake_target = Gen_photo_to_target(origin_photo)
fake_origin = Gen_target_to_photo(target_photo)
# 鑑別器預測
pred_fake_target = Disc_target_photo(fake_target)
pred_fake_origin = Disc_origin_photo(fake_origin)
# 計算GAN對抗損失
label_target = torch.ones_like(pred_fake_target)
Gan_loss_origin_to_target = Gan_loss(pred_fake_target, label_target)
label_origin = torch.ones_like(pred_fake_origin)
Gan_loss_target_to_origin = Gan_loss(pred_fake_origin, label_origin)
# 計算循環損失
cycle_origin_photo = Gen_target_to_photo(fake_target)
cycle_target_photo = Gen_photo_to_target(fake_origin)
loss_cycle_origin = Cycle_loss(cycle_origin_photo, origin_photo)
loss_cycle_target = Cycle_loss(cycle_target_photo, target_photo)
# 計算身分損失
identity_target_photo = Gen_photo_to_target(target_photo)
identity_origin_photo = Gen_target_to_photo(origin_photo)
loss_identity_target = identity_loss(identity_target_photo, target_photo)
loss_identity_origin = identity_loss(identity_origin_photo, origin_photo)
# 總生成器損失
loss_G = (Gan_loss_origin_to_target + Gan_loss_target_to_origin +
lambda_cycle * (loss_cycle_origin + loss_cycle_target) +
lambda_identity * (loss_identity_origin + loss_identity_target))
loss_G.backward() # 總生成器梯度反向傳播
optimizer_Gen.step() # 生成器權重更新
# 訓練原始圖像鑑別器
optimizer_Disc_photo.zero_grad()
pred_origin_photo = Disc_origin_photo(origin_photo) #訓練原始圖片鑑別器
pred_fake_origin_photo = Disc_origin_photo(fake_origin.detach()) #訓練目標圖片鑑別器
loss_D_origin_photo = Gan_loss(pred_origin_photo, torch.ones_like(pred_origin_photo))
loss_D_fake_origin_photo = Gan_loss(pred_fake_origin_photo, torch.zeros_like(pred_fake_origin_photo))
loss_D_origin = (loss_D_origin_photo + loss_D_fake_origin_photo) * 0.5
loss_D_origin.backward()
optimizer_Disc_photo.step()
# 訓練目標圖像鑑別器
optimizer_Disc_target.zero_grad()
pred_target_photo = Disc_target_photo(target_photo)
pred_fake_target_photo = Disc_target_photo(fake_target.detach())
loss_D_target_photo = Gan_loss(pred_target_photo, torch.ones_like(pred_target_photo))
loss_D_fake_target_photo = Gan_loss(pred_fake_target_photo, torch.zeros_like(pred_fake_target_photo))
loss_D_target = (loss_D_target_photo + loss_D_fake_target_photo) * 0.5
loss_D_target.backward()
optimizer_Disc_target.step()
if i % 1 == 0:
print("\r",f"epoch[{epoch + 1}/{num_epochs}] Batch[{i}/{len(dataloader)}]",
f"Loss_G: {loss_G.item():.4f} Loss_D_origin: {loss_D_origin.item():.4f}",
f"Loss_D_target: {loss_D_target.item():.4f}", end="", flush=True)
if i % 100 == 0:
origin_img = origin_photo.cpu().detach().permute(0,2,3,1).numpy()[0]
origin_img = (origin_img + 1.0)/2.0
target_img = Gen_photo_to_target(origin_photo)
target_img = target_img.cpu().detach().permute(0,2,3,1).numpy()[0]
target_img = (target_img + 1.0)/2.0
fig, axes = plt.subplots(1,2,figsize=(8,8))
axes[0].imshow(origin_img)
axes[0].axis("off")
axes[1].imshow(target_img)
axes[1].axis("off")
plt.show()

累積數個epochs後的模型轉換圖像成果
整個模型訓練頗花費時間,因此可以再訓練任意的epochs後儲存參數,之後若覺得訓練的疊代次數不夠,可以再讀入權重參數,這樣就無需從頭開始訓練。
結語
CycleGAN透過生成器、鑑別器和精心設計的損失函數,將普通的風景相片轉換為充滿印象派魅力的莫內畫作。其生成器的對稱架構、鑑別器的PatchGAN設計,以及對抗、循環一致性和身份損失的結合,實現了無監督風格轉換的奇蹟。
除了莫內風格的轉換,CycleGAN的應用潛力遠不止於此。你可以探索其他風格的資料集,例如將人物相片轉為素描風格,或將街景照片轉為梵谷的星空畫風。只要準備好對應的資料集(例如素描圖像集合),就能訓練出專屬的風格轉換模型。更進一步,你甚至可以自製獨特的資料集,例如收集某位藝術家的作品或設計自己的視覺風格,打造個人化的CycleGAN模型,讓你的創意無限延伸!
想親手打造一幅莫內風格的畫作,或開創屬於你的風格轉換?快去準備你的風景相片或自製資料集,啟動CycleGAN,讓AI為你揮灑藝術的筆觸吧!如果你有任何問題或想分享你的生成結果,歡迎在下方留言!
後記
我在kaggle上找到一個資料集,是有關吉卜力(Ghibli)影像的資料集,內容包含一般照片以及吉卜力影像,可以直接安裝kaggle然後下載資料集來進行訓練:
# Ghibli Dataset
#
import kagglehub
# Download latest version
path = kagglehub.dataset_download("shubham1921/real-to-ghibli-image-dataset-5k-paired-images")
print("Path to dataset files:", path)
photo_dir = os.path.join(path, "dataset/trainA/")
target_dir = os.path.join(path, "dataset/trainB_ghibli/")
以下影像是由經過訓練的模型將相片轉換成吉卜力風格的成果:







메타데이터
- post_id
- 288e89a2e39e
- slug
- 從風景相片到莫內畫作-探索cyclegan的模型架構-288e89a2e39e
- url
- https://medium.com/@wkol1126/%E5%BE%9E%E9%A2%A8%E6%99%AF%E7%9B%B8%E7%89%87%E5%88%B0%E8%8E%AB%E5%85%A7%E7%95%AB%E4%BD%9C-%E6%8E%A2%E7%B4%A2cyclegan%E7%9A%84%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%A7%8B-288e89a2e39e
- canonical_url
- https://medium.com/@wkol1126/%E5%BE%9E%E9%A2%A8%E6%99%AF%E7%9B%B8%E7%89%87%E5%88%B0%E8%8E%AB%E5%85%A7%E7%95%AB%E4%BD%9C-%E6%8E%A2%E7%B4%A2cyclegan%E7%9A%84%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%A7%8B-288e89a2e39e
- author_url
- https://medium.com/@wkol1126
- status
- ok
- fetched_at
- 2026-07-19 19:33:58