一、一堆廢話
研一剛開學,選了導師以后開始定方向,本來是想去做mhy那個動作生成的崗位,然后去給導師說做姿態識別,老師給我表示支持以后叫我開始看雙流法啥的,后來想了想,發現應該是學GAN才對
給導師說了以后,導師還是給了支持並且告訴我大致方向(嗚嗚嗚王老師太好了,又負責又包容),所以導師給了推薦就是李宏毅老師的GAN教學視頻,用了快不到兩個星期刷了前面(后面的各類gan我還沒看,只想趕緊做點東西出來)然后就看見有個hw是動漫頭像的生成
二、真正的CV工程師!(ctrl+c&ctrl+v)
我自己總結了一下gan的大致內容准備開始憑着自己的理解寫代碼,最后倒是寫完了,但是生成的全是黑的圖片,問題應該是我最后生成的像素的值由於歸一化或者sigmoid函數之類的東西把我最后輸出來的像素值全部只有0到1的大小
接着去網上扒了別人的代碼,是csdn一個叫張先生您好的博主,里面說的很明白了,我這里把我自己總結出來的流程大概寫一下
從知乎一個叫一個完整的Pytorch深度學習項目代碼,項目結構是怎樣的?這個問題里有回答總結了流程
- 模型定義
- 數據處理和加載
- 訓練模型(Train and Validate)
- 測試模型
- 訓練過程可視化(可選)
但是在GAN里好像沒有測試模型這一步(目前我不知道,知道以后會回來補充的),畢竟也不像分類問題那樣可以測試,所以我總結的是
- 各項參數定義
- 模型定義
- 訓練模型(數據處理和加載也包含在內)
- 輸出生成器的生成結果
整個項目文件已經上傳到我的gitee了,沒有包含數據集(反正也是寫給自己看的),要數據集的話搜一下也能找到
其中運行"main.py"就可以跑起來整個代碼

1 import train 2 import generate 3 4 def main(): 5 # 訓練模型 6 #train.train() 7 # 生成圖片 8 generate.generate() 9 10 11 if __name__ == '__main__': 12 main()
main方法中明確了主要就是訓練和輸出整個訓練結束的效果
所以我會依照上面的步驟貼上每個代碼
三、具體步驟
1、各項參數定義

1 class Config(object): 2 """ 3 定義一個配置類 4 """ 5 # 0.參數調整 6 data_path = '/extra_data' 7 virs = "result" 8 num_workers = 4 # 多線程 9 img_size = 64 # 剪切圖片的像素大小 10 batch_size = 256 # 批處理數量 11 max_epoch = 400 # 最大輪次 12 lr1 = 2e-4 # 生成器學習率 13 lr2 = 2e-4 # 判別器學習率 14 beta1 = 0.5 # 正則化系數,Adam優化器參數 15 gpu = True # 是否使用GPU運算(建議使用) 16 nz = 100 # 噪聲維度 17 ngf = 64 # 生成器的卷積核個數 18 ndf = 64 # 判別器的卷積核個數 19 20 # 1.模型保存路徑 21 save_path = 'save_img/' # opt.netg_path生成圖片的保存路徑 22 # 判別模型的更新頻率要高於生成模型 23 d_every = 1 # 每一個batch 訓練一次判別器 24 g_every = 5 # 每1個batch訓練一次生成模型 25 save_every = 5 # 每save_every次保存一次模型 26 netd_path = None 27 netg_path = None 28 29 # 測試數據 30 gen_img = "result1.png" 31 # 選擇保存的照片 32 # 一次生成保存64張圖片 33 gen_num = 64 34 gen_search_num = 512 35 gen_mean = 0 # 生成模型的噪聲均值 36 gen_std = 1
2、模型定義
這里用的是最基礎的GAN,所以就是vector→生成器→圖片→判別器→打分這樣的結構,沒有其他的網絡了
所以定義的就是兩個模型Generation和Discrimination
Generation

1 import torch.nn as nn 2 3 class Generation(nn.Module): 4 def __init__(self, opt): 5 super(Generation, self).__init__() 6 self.ngf = opt.ngf 7 self.Gene = nn.Sequential( 8 nn.ConvTranspose2d(in_channels=opt.nz, out_channels=self.ngf * 8, kernel_size=4, stride=1, padding=0, 9 bias=False), 10 nn.BatchNorm2d(self.ngf * 8), 11 nn.ReLU(inplace=True), 12 13 # 輸入一個4*4*ngf*8 14 nn.ConvTranspose2d(in_channels=self.ngf * 8, out_channels=self.ngf * 4, kernel_size=4, stride=2, padding=1, 15 bias=False), 16 nn.BatchNorm2d(self.ngf * 4), 17 nn.ReLU(inplace=True), 18 19 # 輸入一個8*8*ngf*4 20 nn.ConvTranspose2d(in_channels=self.ngf * 4, out_channels=self.ngf * 2, kernel_size=4, stride=2, padding=1, 21 bias=False), 22 nn.BatchNorm2d(self.ngf * 2), 23 nn.ReLU(inplace=True), 24 25 # 輸入一個16*16*ngf*2 26 nn.ConvTranspose2d(in_channels=self.ngf * 2, out_channels=self.ngf, kernel_size=4, stride=2, padding=1, 27 bias=False), 28 nn.BatchNorm2d(self.ngf), 29 nn.ReLU(inplace=True), 30 31 # 輸入一張32*32*ngf 32 nn.ConvTranspose2d(in_channels=self.ngf, out_channels=3, kernel_size=5, stride=3, padding=1, bias=False), 33 34 # Tanh收斂速度快於sigmoid,遠慢於relu,輸出范圍為[-1,1],輸出均值為0 35 nn.Tanh(), 36 37 ) # 輸出一張96*96*3 38 39 def forward(self, x): 40 return self.Gene(x)
用convtranspose2d反卷積做上采樣
批標准化層的作用,使得每一層的輸出都盡力較為分散的落在數軸的兩端,盡量使得數據處於梯度的敏感區域,加速梯度下降的過程如下
這里的relu函數里有一個inplace參數,這個參數作用為
產生的計算結果不會有影響。利用in-place計算可以節省內(顯)存,同時還可以省去反復申請和釋放內存的時間。但是會對原變量覆蓋,所以只要不帶來錯誤就用。
Discrimination

1 import torch.nn as nn 2 3 class Discrimination(nn.Module): 4 def __init__(self, opt): 5 super(Discrimination, self).__init__() 6 self.ndf = opt.ndf 7 self.Discrim = nn.Sequential( 8 nn.Conv2d(in_channels=3, out_channels=self.ndf, kernel_size=5, stride=3, padding=1, bias=False), 9 nn.LeakyReLU(negative_slope=0.2, inplace=True), 10 11 nn.Conv2d(in_channels=self.ndf, out_channels=self.ndf * 2, kernel_size=4, stride=2, padding=1, bias=False), 12 nn.BatchNorm2d(self.ndf * 2), 13 nn.LeakyReLU(0.2, True), 14 15 nn.Conv2d(in_channels=self.ndf * 2, out_channels=self.ndf * 4, kernel_size=4, stride=2, padding=1, 16 bias=False), 17 nn.BatchNorm2d(self.ndf * 4), 18 nn.LeakyReLU(0.2, True), 19 20 nn.Conv2d(in_channels=self.ndf * 4, out_channels=self.ndf * 8, kernel_size=4, stride=2, padding=1, 21 bias=False), 22 nn.BatchNorm2d(self.ndf * 8), 23 nn.LeakyReLU(0.2, True), 24 25 26 nn.Conv2d(in_channels=self.ndf * 8, out_channels=1, kernel_size=4, stride=1, padding=0, bias=True), 27 28 nn.Sigmoid() 29 ) 30 31 def forward(self, x): 32 # 展平后返回 33 return self.Discrim(x).view(-1)
這里用了leakyrelu(也就是PRelu,parameter relu)的激活函數,GAN的創始人Ian Goodfellow說過(知乎看到的,但是我沒找到出處): Leaky relu helps to make sure the gradient can flow through the entire architecture. That's an important consideration for any machine learning model, but even more important for GAN's.
leakyrelu防止了relu由於輸出為負時,反向傳播梯度為零導致神經元死亡
relu和leakyrelu函數圖如下
3、訓練模型(數據處理和加載也包含在內)
因為圖像全部都在一個文件夾里,所以這里沒有重寫dataset來做數據集而用的是imagefolder,以后做音樂到動作的生成的話,還需要自己重寫一些dataset

1 from tqdm import tqdm 2 import torch 3 import torchvision as tv 4 from torch.utils.data import DataLoader 5 import torch.nn as nn 6 from Config import Config 7 from Model.Generation import Generation 8 from Model.Discrimination import Discrimination 9 10 opt = Config() 11 12 def train(**kwargs): 13 # 配置屬性 14 # 如果函數無字典輸入則使用opt中設定好的默認超參數 15 for k_, v_ in kwargs.items(): 16 setattr(opt, k_, v_) 17 18 # device(設備),分配設備 19 if opt.gpu: 20 device = torch.device("cuda") 21 else: 22 device = torch.device('cpu') 23 24 # 數據預處理1 25 # transforms 模塊提供一般圖像轉換操作類的功能,最后轉成floatTensor 26 # tv.transforms.Compose用於組合多個tv.transforms操作,定義好transforms組合操作后,直接傳入圖片即可進行處理 27 # tv.transforms.Resize,對PIL Image對象作resize運算, 數值保存類型為float64 28 # tv.transforms.CenterCrop, 中心裁剪 29 # tv.transforms.ToTensor,將opencv讀到的圖片轉為torch image類型(通道,像素,像素),且把像素范圍轉為[0,1] 30 # tv.transforms.Normalize,執行image = (image - mean)/std 數據歸一化操作,一參數是mean,二參數std 31 # 因為是三通道,所以mean = (0.5, 0.5, 0.5),從而轉成[-1, 1]范圍 32 transforms = tv.transforms.Compose([ 33 # 3*96*96 34 tv.transforms.Resize(opt.img_size), # 縮放到 img_size* img_size 35 # 中心裁剪成96*96的圖片。因為本實驗數據已滿足96*96尺寸,可省略 36 # tv.transforms.CenterCrop(opt.img_size), 37 38 # ToTensor 和 Normalize 搭配使用 39 tv.transforms.ToTensor(), 40 tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) 41 ]) 42 43 # 加載數據並使用定義好的transforms對圖片進行預處理,這里用的是直接定義法 44 # dataset是一個包裝類,將數據包裝成Dataset類,方便之后傳入DataLoader中 45 # 寫法2: 46 # 定義類Dataset(Datasets)包裝類,重寫__getitem__(進行transforms系列操作)、__len__方法(獲取樣本個數) 47 # ### 兩種寫法有什么區別 48 dataset = tv.datasets.ImageFolder(root=opt.data_path, transform=transforms) 49 50 # 數據預處理2 51 # 查看drop_last操作, 52 dataloader = DataLoader( 53 dataset, # 數據加載 54 batch_size=opt.batch_size, # 批處理大小設置 55 shuffle=True, # 是否進行洗牌操作 56 # num_workers=opt.num_workers, # 是否進行多線程加載數據設置 57 drop_last=True # 為True時,如果數據集大小不能被批處理大小整除,則設置為刪除最后一個不完整的批處理。 58 ) 59 60 # 初始化網絡 61 netg, netd = Generation(opt), Discrimination(opt) 62 # 判斷網絡是否有權重數值 63 # ### storage存儲 64 map_location = lambda storage, loc: storage 65 66 # torch.load模型加載,即有模型加載模型在該模型基礎上進行訓練,沒有模型則從頭開始 67 # f:類文件對象,如果有模型對象路徑,則加載返回 68 # map_location:一個函數或字典規定如何remap存儲位置 69 # net.load_state_dict將加載出來的模型數據加載到構建好的net網絡中去 70 if opt.netg_path: 71 netg.load_state_dict(torch.load(f=opt.netg_path, map_location=map_location)) 72 if opt.netd_path: 73 netd.load_state_dict(torch.load(f=opt.netd_path, map_location=map_location)) 74 75 # 搬移模型到之前指定設備,本文采用的是cpu,分配設備 76 netd.to(device) 77 netg.to(device) 78 79 # 定義優化策略 80 # torch.optim包內有多種優化算法, 81 # Adam優化算法,是帶動量的慣性梯度下降算法 82 optimize_g = torch.optim.Adam(netg.parameters(), lr=opt.lr1, betas=(opt.beta1, 0.999)) 83 optimize_d = torch.optim.Adam(netd.parameters(), lr=opt.lr2, betas=(opt.beta1, 0.999)) 84 85 # 計算目標值和預測值之間的交叉熵損失函數 86 # BCEloss:-w(ylog x +(1 - y)log(1 - x)) 87 # y為真實標簽,x為判別器打分(sigmiod,1為真0為假),加上負號,等效於求對應標簽下的最大得分 88 # to(device),用於指定CPU/GPU 89 criterions = nn.BCELoss().to(device) 90 91 # 定義標簽,並且開始注入生成器的輸入noise 92 true_labels = torch.ones(opt.batch_size).to(device) 93 fake_labels = torch.zeros(opt.batch_size).to(device) 94 95 # 生成滿足N(1,1)標准正態分布,opt.nz維(100維),opt.batch_size個數的隨機噪聲 96 noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device) 97 98 # 用於保存模型時作生成圖像示例 99 fix_noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device) 100 101 # 訓練網絡 102 # 設置迭代 103 for epoch in range(opt.max_epoch): 104 # tqdm(iterator()),函數內嵌迭代器,用作循環的進度條顯示 105 for ii_, (img, _) in tqdm((enumerate(dataloader))): 106 # 將處理好的圖片賦值 107 real_img = img.to(device) 108 109 # 開始訓練生成器和判別器 110 # 注意要使得生成的訓練次數小於一些 111 # 每一輪更新一次判別器 112 if ii_ % opt.d_every == 0: 113 # 優化器梯度清零 114 optimize_d.zero_grad() 115 116 # 訓練判別器 117 # 把判別器的目標函數分成兩段分別進行反向求導,再統一優化 118 # 真圖 119 # 把所有的真樣本傳進netd進行訓練, 120 output = netd(real_img) 121 # 用之前定義好的交叉熵損失函數計算損失 122 error_d_real = criterions(output, true_labels) 123 # 誤差反向計算 124 error_d_real.backward() 125 126 # 隨機生成的假圖 127 # .detach() 返回相同數據的 tensor ,且 requires_grad=False 128 # .detach()做截斷操作,生成器不記錄判別器采用噪聲的梯度 129 noises = noises.detach() 130 # 通過生成模型將隨機噪聲生成為圖片矩陣數據 131 fake_image = netg(noises).detach() 132 # 將生成的圖片交給判別模型進行判別 133 output = netd(fake_image) 134 # 再次計算損失函數的計算損失 135 error_d_fake = criterions(output, fake_labels) 136 # 誤差反向計算 137 # 求導和優化(權重更新)是兩個獨立的過程,只不過優化時一定需要對應的已求取的梯度值。 138 # 所以求得梯度值很關鍵,而且,經常會累積多種loss對某網絡參數造成的梯度,一並更新網絡。 139 error_d_fake.backward() 140 141 # ‘’‘ 142 # 關於為什么要分兩步計算loss: 143 # 我們已經知道,BCEloss相當於計算對應標簽下的得分,那么我們 144 # 把真樣本傳入時,因為標簽恆為1,BCE此時只有第一項,即真樣本得分項 145 # 要補齊成前文提到的判別器目標函數,需要再添置假樣本得分項,故兩次分開計算梯度,各自最大化各自的得分(假樣本得分是log(1-D(x))) 146 # 再統一進行梯度下降即可 147 # ’‘’ 148 # 計算一次Adam算法,完成判別模型的參數迭代 149 # 多個不同loss的backward()來累積同一個網絡的grad,計算一次Adam即可 150 optimize_d.step() 151 152 # 訓練判別器 153 if ii_ % opt.g_every == 0: 154 optimize_g.zero_grad() 155 # 用於netd作判別訓練和用於netg作生成訓練兩組噪聲需不同 156 noises.data.copy_(torch.randn(opt.batch_size, opt.nz, 1, 1)) 157 fake_image = netg(noises) 158 output = netd(fake_image) 159 # 此時判別器已經固定住了,BCE的一項為定值,再求最小化相當於求二項即G得分的最大化 160 error_g = criterions(output, true_labels) 161 error_g.backward() 162 163 # 計算一次Adam算法,完成判別模型的參數迭代 164 optimize_g.step() 165 166 # 保存模型 167 if (epoch + 1) % opt.save_every == 0: 168 fix_fake_image = netg(fix_noises) 169 tv.utils.save_image(fix_fake_image.data[:64], "%s/%s.png" % (opt.save_path, epoch), normalize=True) 170 171 torch.save(netd.state_dict(), 'sava_pra/' + 'netd_{0}.pth'.format(epoch)) 172 torch.save(netg.state_dict(), 'sava_pra/' + 'netg_{0}.pth'.format(epoch))
1 setattr(opt, k_, v_)
這個函數的作用是可以更改opt類里的屬性k_的值為v_
晚上讀代碼的時候和組內的大佬一起討論了一下這個transforms.compose里的resize是怎么做到的,我們本來以為是padding黑邊,然后后來經過試驗發現不是黑邊,所以去讀了一下文檔看看是怎么說的,文檔里說resize是線性插值的幾種方法,在不設定的時候默認插值方法是InterpolationMode.BILINEAR,這也想想也對,如果插值黑邊的話,對於原圖的特征提取其實也是一種噪聲,用插值的方法做出來的圖不見得最好,但起碼不會是一個壞的選擇
接着是這里有一個lambda匿名函數,傳入的參數是storage和loc,返回storage(目前不知道這個函數能干嘛,但是注釋掉的話下面的那個torch.load里的maplocation會報錯)
4、輸出生成器的生成結果

1 import torch 2 import torchvision as tv 3 from Config import Config 4 from Model.Generation import Generation 5 from Model.Discrimination import Discrimination 6 7 opt = Config() 8 9 @torch.no_grad() 10 def generate(**kwargs): 11 # 用訓練好的模型來生成圖片 12 13 for k_, v_ in kwargs.items(): 14 setattr(opt, k_, v_) 15 16 device = torch.device("cuda") if opt.gpu else torch.device("cpu") 17 18 # 加載訓練好的權重數據 19 netg, netd = Generation(opt).eval(), Discrimination(opt).eval() 20 # 兩個參數返回第一個 21 map_location = lambda storage, loc: storage 22 23 # opt.netd_path等參數有待修改 24 netd.load_state_dict(torch.load('sava_pra/netd_399.pth', map_location=map_location), False) 25 netg.load_state_dict(torch.load('sava_pra/netg_399.pth', map_location=map_location), False) 26 netd.to(device) 27 netg.to(device) 28 29 # 生成訓練好的圖片 30 # 初始化512組噪聲,選其中好的拿來保存輸出。 31 noise = torch.randn(opt.gen_search_num, opt.nz, 1, 1).normal_(opt.gen_mean, opt.gen_std).to(device) 32 33 fake_image = netg(noise) 34 score = netd(fake_image).detach() 35 36 # 挑選出合適的圖片 37 # 取出得分最高的圖片 38 indexs = score.topk(opt.gen_num)[1] 39 40 result = [] 41 42 for ii in indexs: 43 result.append(fake_image.data[ii]) 44 45 # 以opt.gen_img為文件名保存生成圖片 46 tv.utils.save_image(torch.stack(result), opt.gen_img, normalize=True, range=(-1, 1))
# @torch.no_grad():數據不需要計算梯度,也不會進行反向傳播
這里取了64張照片出來,具體的做法是discriminator會給512張照片打分,然后用topk函數取64個分最高的出來,記錄他們的下標到index里,遍歷index然后用下標吧每張圖像append到result中
這里說一下topk函數
1 import torch 2 3 list = [2,3,5,7,2,98,9,2,4,78,8] 4 t = torch.tensor(list).detach() 5 6 print(t.topk(3)) 7 print('-----------------') 8 print(t.topk(3)[0]) 9 print('-----------------') 10 print(t.topk(3)[1]) 11 print('-----------------')
打印結果為
topk函數可以返回前k大的張量以及他們的下標
但是由於result是列表(相當於所有圖在一行),所以下面用了一個stack把列表升維成了一張8*8的圖組成的大圖
最后我挑了一張圖做了頭像(粉毛妹子好耶!)