GANs: 学习生成一维正态分布
GANs: 學習生成一維正態分布
本文同步于我的知乎專欄:https://zhuanlan.zhihu.com/p/352126210
本篇文章在 devnag 2018年實現的 GANs 基礎之上,修改、適配 PyTorch 1.71 之后,在原代碼上添加了注釋以及對于部分代碼段的個人理解。
首先,這篇文章不是從理論層面對 GANs 的原理進行解讀(如果想了解GANs背后的數學原理,請參見 GANs背后的數學原理);本文從宏觀上,由GANs的源碼出發,對GANs的程序結構,GANs的實現邏輯以及實現思路進行解讀。
Task goal:
本文中,針對數據對象是 一維數據樣本(one dimension samples);
GANs 的實現 目標(goal) 是由 均勻分布(uniform distribution) 生成 高斯正態分布(normal distribution);即,對服從**均勻分布(uniform distribution)**的數據樣本進行采樣,對采樣得到的數據經 GANs 處理后,生成 高斯正態分布(normal distribution)。
下面將從以下4個部分展開來講:
1.1 visualize data distribution
1.2 data preprocess
1.3 integration
2.1 Generator Network Architecture
2.2 Discriminator Network Architecture
3.1 initialize variables
3.2 alternate training G and D
1. Data Process
1.1 Visualize data distribution(觀察輸入及目標數據分布)
Input Data:
獲取一維 均分分布(uniform distribution) 樣本:
def get_generator_input_sampler():"""初始生成器(generator)數據分布:采用均勻分布(Uniform), 而非隨機噪聲, 以保證 Generator必須以非線性方法生成目標數據分布:return: 生成size為([m, n]), 范圍為(0, 1)樣本"""# 均勻分布# torch.rand(m, n)# lambda表達式:匿名函數, lambda 參數: 返回值return lambda m, n: torch.rand(m, n)繪制 一維均分分布(uniform distribution) 樣本采樣點:
def plot_input_distribution(generator_input_data):"""繪制輸入樣本分布:param generator_input_data: 生成器的輸入數據分布"""plt.plot(generator_input_data)plt.xlabel('Value')plt.ylabel('Count')plt.ylim(0, int(sum(generator_input_data) / 10))plt.title('Uniform Distribution of Inputs')plt.grid(True)plt.show()Target Data:
獲取一維 正態分布(normal distribution) 樣本:
def get_target_distribution_sampler(mu, sigma):"""采樣目標數據分布的樣本:param mu: 均值:param sigma: 方差:return: size=(1, n) 大小的樣本"""# Gaussian:正態分布、高斯分布# np.random.normal(mu, sigmoid, size=None)# size默認為None, 若size為None, 則返回單個樣本; 否則(m, n), 則返回: m * n 個樣本# 返回 lambda 表達式:匿名函數, lambda 參數: 返回值return lambda n: torch.Tensor(np.random.normal(mu, sigma, size=(1, n)))繪制一維 正態分布(normal distribution) 樣本采樣點:
def plot_generated_distribution(data_type, generated_fake_data):"""繪制生成的目標樣本分布:param data_type: 輸入到 鑒別器 D 的數據類別:param generated_fake_data: 生成器 G 生成的 fake data"""values = extract(generated_fake_data)print("Values: %s" % (str(values)))plt.hist(values, bins=50)plt.xlabel('Value')plt.ylabel('Count')plt.title(data_type + " Histogram of Generated Distribution")plt.grid(True)plt.show()1.2 data preprocess(數據預處理)
首先,GANs 由 Generator 和 Discriminator 構成;
Generator 接受 一維正態分布(normal distribution)樣本 作為 輸入(input);
那么 Discriminator 需要接收什么 輸入信息(Input information) 呢 ?
先來想想 Discriminator 需要的 輸入信息(Input information) 需要具備什么呢 ?
ans: 這個 輸入信息(Input information) 需要清晰明確的反映 目標數據分布(Target Distrubution) 的特點。
那么,什么最能反映數據分布的特點呢?
靈機一動!想到了,數據的**數字特征(numerical_characteristics)**最能反映數據分布的特點!
因此,我們講目標數據分布(Target Distrubution) 以及 Generator 生成的數據分布(Generated Distrubution) 的 數字特征(numerical characteristics) 作為 判斷標準 輸入到 Discriminator。
對于 Generator 生成的數據分布(Generated Distrubution) ,Discriminator 打低分(0);
對于 目標數據分布(Target Distrubution),Discriminator 打高分(1)!
此處,我們獲取數據的4個數字特征,分別是,
1. mean:均值 2. std:標準差 3. skewness: 偏度 4. kurtosis: 峰度
并將其作為 判別標準 輸入到 Discriminator 中。
def get_numerical_characteristics(data):"""返回數據(data)的 4 個數字特征(numerical characteristics):1. mean:均值2. std:標準差3. skewness: 偏度4. kurtosis: 峰度:param data: 數據:return: 一維數據: torch.Size([4])"""mean = torch.mean(data)diffs = data - meanvar = torch.mean(torch.pow(diffs, 2.0))std = torch.pow(var, 0.5)z_scores = diffs / std# 偏度:數據分布偏斜方向、程度的度量, 是數據分布非對稱程度的數字特征# 定義: 偏度是樣本的三階標準化矩skewness = torch.mean(torch.pow(z_scores, 3.0))# excess kurtosis, should be 0 for Gaussian# 峰度(kurtosis): 表征概率密度分布曲線在平均值處峰值高低的特征數# 若峰度(kurtosis) > 3, 峰的形狀會比較尖, 會比正態分布峰陡峭kurtoses = torch.mean(torch.pow(z_scores, 4.0)) - 3.0# reshape(1, ):將常量轉化為torch.Size([1])型張量(Tensor)final = torch.cat((mean.reshape(1, ), std.reshape(1, ), skewness.reshape(1, ), kurtoses.reshape(1, )))return final同樣的,可以將 原始數據(original data) 及其 L2 norm: ||x-mean||作為判別標準一起輸入到 **Discriminator,**使 鑒別器 D 了解更多數據分布的信息。
def decorate_with_diffs(data, exponent, remove_raw_data=False):"""L2 norm: ||x-mean||decorate_with_diffs 作用: 將原始數據(original data)以及 L2 norm 一起返回, 使 鑒別器 D 了解更多目標數據分布的信息:param data: Tensor: 張量:param exponent: 冪次:param remove_raw_data: 是否移除原始數據:return: torch.cat([data, diffs], dim=1), dim=0, 同型張量(Tensor)按行合并; dim=1, 同型張量(Tensor)按列合并;"""# dim=0, 行; dim=1, 列; keepdim: 做 mean后, 保持原數據的維度空間, 即, 原原數據為2維, mean 后仍為2維mean = torch.mean(data.data, dim=1, keepdim=True)# 利用廣播(broadcast)機制進行張量(Tensor)乘法mean_broadcast = torch.mul(torch.ones(data.size()), mean.tolist()[0][0])# data - data.mean[0]diffs = torch.pow(data - mean_broadcast, exponent)if remove_raw_data:return torch.cat([diffs], dim=1)else:# diffs: 返回樣本數據與樣本平均值的偏離程度(可以是n次方(exponent))# 并將樣本的偏離程度信息與原始樣本一同輸入到神經網絡中return torch.cat([data, diffs], dim=1)1.3 integration(整合)
def init(flag, remove_raw_data=False):"""1. name: 輸入到 鑒別器 D 的數據類別2. d_input_func: 對 Discriminator 的網絡輸入大小 input_size 進行調整3. preprocess: 對輸入到 Discriminator 數據進行預處理(pre_process), e.g.3.1. get_numerical_characteristics(data): 返回數據的4個數字特征:1. mean:均值; 2. std:標準差; 3. skewness: 偏度; 4. kurtosis: 峰度3.2. decorate_with_diffs(data, exponent, remove_raw_data): 將 data 處理成 diffs; 然后選擇是否與 diffs 連接(cat) 后返回:param flag: flag=0, 采用 decorate_with_diffs, 返回維度為 torch.Size([1, 1]);flag=1, 采用 get_numerical_characteristics(data), 返回維度為 torch.Size([1]);:param remove_raw_data: 用來標識 decorate_with_diffs 是否返回 [data, diffs]; 若 remove_raw_data=True,則 decorate_with_diffs 只返回 diffs; 否則, decorate_with_diffs 返回 [data, diffs]"""if flag == 0:# 返回數據及其數據方差"偏離程度"(data_type, preprocess, d_input_func) = ("Data and variances",lambda data: decorate_with_diffs(data, 2.0, remove_raw_data),lambda x: x if remove_raw_data else x * 2)elif flag == 1:# 返回數據及其數據均值"偏離程度"(data_type, preprocess, d_input_func) = ("Data and diffs",lambda data: decorate_with_diffs(data, 1.0, remove_raw_data),lambda x: x if remove_raw_data else x * 2)elif flag == 2:# 直接使用原始數據(data_type, preprocess, d_input_func) = ("Raw data", lambda data: data, lambda x: x)elif flag == 3:# 僅使用數據的 4 個數字特征(data_type, preprocess, d_input_func) = ("Only 4 numerical characteristics",lambda data: get_numerical_characteristics(data), lambda x: 4)else:data_type, preprocess, d_input_func = None, None, Noneprint("Flag input error!\n")return data_type, preprocess, d_input_func2. GANs Network Architecture
2.1 Generator(生成器) Network Architecture
class Generator(nn.Module):"""所有神經網絡(neural network)都需要繼承父類(nn.Module), 并實現方法 1. "__init__"; 2. "forward"1. __init__:當一個類實例化時, 會自動調用方法 __init__(); 類實例化時的參數會自動傳遞給 __init__2. forward(): 繼承nn.Module的類實例化后, 類的實例化變量會直接自動調用forward,此時, 傳入的參數會直接傳給forward, forward的返回值也會直接返回給對應變量, 即loss = model(input_data) <==> loss = model.forward(input_data)實質:不過是 model 又封裝了一層, 可以少寫一步.forward(input_data)"""# 類方法的第一個參數必須是self, 可參見 https://docs.python.org/zh-cn/3/tutorial/classes.htmldef __init__(self, input_size, hidden_size, output_size, f):# 同Java繼承機制(子類繼承父類的所有屬性和方法, 用父類的初始化方法對繼承自父類的屬性進行初始化)# 首先找到父類, 然后把self轉化為父類對象, 最后"被轉換"的父類對象調用自己的init函數super(Generator, self).__init__()# nn.Linear(in_features, out_features, bias):對輸入數據應用線性變換# in_feature, 輸入樣本size; out_features, 輸出樣本size; bias: 是否添加添加偏置單元# 注:map:映射self.map1 = nn.Linear(input_size, hidden_size)self.map2 = nn.Linear(hidden_size, hidden_size)self.map3 = nn.Linear(hidden_size, output_size)self.f = fdef forward(self, x):x = self.map1(x)x = self.f(x)x = self.map2(x)x = self.f(x)x = self.map3(x)return x2.2 Discriminator(鑒別器) Network Architecture
class Discriminator(nn.Module):def __init__(self, input_size, hidden_size, output_size, f):super(Discriminator, self).__init__()self.map1 = nn.Linear(input_size, hidden_size)self.map2 = nn.Linear(hidden_size, hidden_size)self.map3 = nn.Linear(hidden_size, output_size)self.f = fdef forward(self, x):x = self.map1(x)x = self.f(x)x = self.map2(x)x = self.f(x)x = self.map3(x)x = self.f(x)return x3. Training GANs
3.1 initialize variables(初始化參數變量)
# flag: 標識符 flag = 3 data_type, preprocess, d_input_func = init(flag=flag)print("Using data [%s]" % data_type)# 若GPU可用, 則使用GPU; 否則, 使用CPU device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")# 打印間隔 print_interval = 100# 繪圖間隔 plot_interval = 500dfe, dre, ge = 0, 0, 0d_real_data, d_fake_data, g_fake_data = None, None, None# 數據參數:均值, 偏差 data_mean = 4 data_stddev = 1.25# 鑒別器:獲取目標數據分布 d_sampler = get_target_distribution_sampler(data_mean, data_stddev) # 生成器:獲取均值分布 g_sampler = get_generator_input_sampler()# 鑒別器、生成器激活函數 generator_activation_function = torch.tanh discriminator_activation_function = torch.sigmoid# 生成器參數 g_input_size = 1 g_hidden_size = 5 # 輸出維度為 1 g_output_size = 1 g_steps = 20 G = Generator(input_size=g_input_size,hidden_size=g_hidden_size,output_size=g_output_size,f=generator_activation_function)# 鑒別器參數 d_input_size = 500 d_hidden_size = 10 # 輸出維度為 1 d_output_size = 1 d_steps = 20# 當使用init()的第1類數據類別時, d_input_func()會根據情況, 調整 鑒別器D 的輸入大小為 鑒別器D 的初始輸入大小或 2 倍 # 當使用init()的第2類數據類別時, d_input_func()會根據情況, 調整 鑒別器D 的輸入大小為 鑒別器D 的初始輸入大小或 2 倍 # 當使用init()的第3類數據類別時, d_input_func()會調整 鑒別器D 的輸入大小為 鑒別器D 的初始輸入大小 # 當使用init()的第4類數據類別時, d_input_func()會調整 鑒別器D 的輸入大小為 4, 即, 輸入數據的4個數字特征 D = Discriminator(input_size=d_input_func(d_input_size),hidden_size=d_hidden_size,output_size=d_output_size,f=discriminator_activation_function)# 二元交叉熵(BCELoss function): 計算 輸出(output) 與 目標(target) 之間的 距離error(loss) # loss(output, target) loss = nn.BCELoss()# d, g 學習率 d_learning_rate = 1e-3 g_learning_rate = 1e-3 # SGD動量 sgd_momentum = 0.9# 梯度下降:解決病態曲率同時加快搜索速度的方法(自適應方法) # Adam、RMSProp、Momentum: 三種主流自適應算法; Adam前景明朗, Momentum更加主流 # Adam更易于收斂到尖銳的極小值, Momentum更可能收斂到平坦的極小值, 通常認為平坦的極小值 好于 尖銳的極小值 d_optimizer = optim.SGD(D.parameters(), lr=d_learning_rate, momentum=sgd_momentum) g_optimizer = optim.SGD(G.parameters(), lr=g_learning_rate, momentum=sgd_momentum)# 將mini-batch大小置為discriminator的輸入大小 # 目的: 每次都能讓鑒別器D看到完整的目標數據分布 # 本文設置 d_input_size = mini-batch_size, 而非 d_input_size = 1; 因為, 如果 D 沒有看到real數據的完整分布的話, # 那么, 則會產生 Ian Goodfellow 2016年在GANs論文中提到的 "collapse to a single mode" 問題; # 即, 生成器G 最終生成的樣本分布均值(mean)是正確的, 但樣本分布方差(std)則會非常小 mini_batch_size = d_input_size# 繪制初始樣本分布 generator_input_data = g_sampler(mini_batch_size, g_input_size) plot_input_distribution(generator_input_data)d_fake_list = []3.2 alternate training G and D(交替訓練G,D)
成功訓練 GANs 需要迭代 5000 個左右 epoch,在每個epoch中分別先后訓練 鑒別器(Discriminator) 以及 生成器(Generator) 各 20 steps。
在訓練好具有一定能力的 鑒別器(Discriminator) 后,在 鑒別器(Discriminator) 有一定鑒別能力的基礎上, 保持 鑒別器(Discriminator) 不動,訓練 生成器(Generator);
訓練 生成器(Generator) 具有一定的鑒別能力后,保持 生成器(Generator) 不動,訓練 **鑒別器(Discriminator),**以提高 鑒別器(Discriminator) 的鑒別能力;交替執行,循環往復!
# 迭代周期num_epochs = 5001# 不斷在 D, G 之間進行交替訓練, 通過博弈提高 D, G 的能力# 每個 epoch 分別交替訓練 D, G 20 stepsfor epoch in range(num_epochs):# 訓練鑒別器(Discriminator)for d_index in range(d_steps):# 1. 分別在在real、fake數據上訓練 D# 注:此處要清楚: D.grad是loss對parameter的導數; 而SGD則是每次更新一小步(step);# 即, parameter = parameter - dloss/dparameter# 故, parameter利用PyTorch計算圖反向傳播填充在parameter.grad中的梯度(grad)進行更新后, 梯度(grad)便失去意義;# 并且, 因為反向傳播的梯度(grad)會累加, 故在進行下一次SGD更新時, 需要清楚上次填充的梯度(grad)# https://www.yht7.com/news/97242D.zero_grad()# 1.1: 在 real data 上訓練 Dd_real_data = d_sampler(d_input_size)# 將變量遷移到 GPU 上# 只使用 d_real_data.to(device), 變量仍在CPU上d_real_data = d_real_data.to(device)d_real_decision = D(preprocess(d_real_data))# 由于 d_real_decision 可能為 torch.Size([1]) 或 torch.Size([1, 1])# 故, 先對其進行求和(sum), 然后reshape成torch.Size([1])的張量# nn.BCELoss(output, target): 以 1 標識 real, 以 0 標識 faked_real_error = loss(torch.sum(d_real_decision).reshape(1, ), torch.ones([1]).to(device))# 計算/填充梯度, 但不改變 D 的權重d_real_error.backward()# 1.2: 在 fake data 上訓練 D# 1.2.1: 使用 Generator 生成數據# 按Generator的輸入大小,采樣mini-batch個輸入d_gen_input = g_sampler(mini_batch_size, g_input_size)# 將變量遷移到 GPU 上d_gen_input = d_gen_input.to(device)# https://blog.csdn.net/qq_34218078/article/details/109591000 講的很清楚!# detach(): 截斷反向傳播的梯度流; 將原計算圖中的某個node變成不需要梯度的node, 反向傳播不會從這個node向前傳播# 此處 G 僅作為隨即噪音生成器(以及迭代后有一定模仿能力的生成器), 教 D 鑒別fake data; 故, 應detach來避免在這些標簽上訓練 G(即, 保持 G 不動)# 雖然d_optimizer.step()僅優化了鑒別器 D 的參數; 但是, 由于此處仍然通過生成器 G 生成了假的數據;# 并且, 生成器 G 在生成假數據的過程中仍然調用了前向傳播(forward); 因此, 在d_fake_error進行反向傳播(backward)的過程中,# 仍然會對生成器G的網絡參數求導, 并填充到網絡參數的.grad屬性中; 這樣做雖然沒有對G的網絡參數進行更新,# 但在訓練Discriminator進行迭代的過程中, 確實會對生成器G網絡參數的 .grad 屬性中, 形成梯度積累;# 雖然, 在迭代訓練生成器 G 之前, 采用了 G.zero_grad() 進行梯度清零, 訓練 鑒別器 D 時, 生成器 G 形成的梯度積累不會影響# 到生成器 G 的網絡參數更新; 但梯度的計算開銷, 內存開銷以及時間成本是完全沒有必要的; 故, 在訓練 D 時, 采用 G.detach()# 截斷對G的反向傳播梯度流;d_fake_data = G(d_gen_input).detach()# x.t(): 對 x 進行轉置d_fake_decision = D(preprocess(d_fake_data.t()))# torch.zeros(1)與torch.zeros([1]) 的 size 均為 torch.Size([1])d_fake_error = loss(torch.sum(d_fake_decision).reshape(1, ), torch.zeros([1]).to(device))d_fake_error.backward()# 僅優化 D 的參數, 基于backward()在網絡結構中填充的梯度(grad)更新權重d_optimizer.step()# 提取 鑒別器D 對于real data的損失值和fake data的損失值;# dre 應隨著迭代趨近于 dfedre, dfe = extract(d_real_error)[0], extract(d_fake_error)[0]# 訓練生成器(Generator)for g_index in range(g_steps):# 在訓練 G 的過程中, 保持 D 不動(此時, D已具有一定的鑒別能力)# 在 D 有一定鑒別能力的基礎上, 訓練 GG.zero_grad()# 按Generator的輸入大小,采樣mini-batch個輸入gen_input = g_sampler(mini_batch_size, g_input_size)gen_input = gen_input.to(device)# G 生成假的數據分布g_fake_data = G(gen_input)dg_fake_decision = D(preprocess(g_fake_data.t()))# loss_fn: G 通過迭代使生成的假的數據分布讓 D 打更高分; 即, 使 G 生成的數據更靠近 D 的判別標準g_error = loss(torch.sum(dg_fake_decision).reshape(1, ), torch.ones([1]).to(device))g_error.backward()# 僅優化 生成器G 的參數g_optimizer.step()# 提取 生成器G 的損失ge = extract(g_error)[0]if epoch % print_interval == 0:print("Epoch %s: D (%s real_err, %s fake_err) G (%s err); Real Dist (%s), Fake Dist (%s) " %(epoch, dre, dfe, ge, get_mean_and_std(extract(d_real_data)), get_mean_and_std(extract(d_fake_data))))d_fake_list.append([extract(d_fake_data), epoch])if epoch % plot_interval == 0:# 繪制生成器G生成的數據分布圖plot_generated_distribution(data_type=data_type, generated_fake_data=g_fake_data)4. Results evaluation
以下列舉了每訓練500個epochs時, 生成器 G 生成的數據分布結果。
epoch = 0:
epochs = 500:
epochs = 1000:
…
epochs = 4500:
由于,我們初始時設置的目標正態分布的均值以及方差如下:
# 數據參數:均值, 偏差 data_mean = 4 data_stddev = 1.25可以看到,經過 5000 個 epochs 的迭代后,生成器生成的數據的正態分布峰值位于 4 附近。
epochs = 5000:
可視化迭代訓練過程中,生成器生成的數據分布的樣本均值以及方差,可以看到,經過一定的epochs之后,生成數據的樣本 均值(mean) 以及 方差(variance) 均 穩定收斂 到我們初始化時,為目標數據分布設置的初始值。GANs 訓練成功!
感謝大家的支持,如果喜歡的話,不要忘記點贊!
后續,還會分享 GANs 生成 2 維數據樣本的解讀,歡迎關注!
GANs 完整代碼 github 地址:
https://github.com/RaySunWHUT/Generative-adversarial-networks
歡迎大家star、fork!😁
References:
總結
以上是生活随笔為你收集整理的GANs: 学习生成一维正态分布的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 机器人改变生活利弊英语作文_机器人对生活
- 下一篇: HCFT和HCFTstar在OTB数据集