owned this note
owned this note
Published
Linked with GitHub
---
title: Taiwan Candidates Face Analysis & Generation
tags: Templates, Talk
description: View the slide with "Slide Mode".
---
# 器宇不凡,必成大器?使用 GAN 生成一張臺灣無敵選舉臉
<!-- Put the link to this slide here so people can follow -->
本專案透過參選人資訊之分析,嘗試尋找政治人物面相與選舉表現之關聯性,最終透過生成式對抗網路(GAN)產生符合台灣候選人面部特徵之臉部
[EDA code](https://colab.research.google.com/drive/1NaTjdHHjKGJBEf0yFcC7DrBKktu-zJYJ?usp=sharing)
---
## Why doing this
在個人成長回憶中,選舉期間總能聽到諸如「我看這人長得獐頭鼠目,一定選不上」、「這個人長得真好看,一定要給他/她一個機會」、「XX黨的人都長得OOO,我一定不投的啦!」...等等論述,聽起來像是單純的性情中言,但說的人多了又令人覺得這似乎體現了一部份選民的投票喜好,而這樣的喜好是否會影響最終結果?**候選人的長相真的會決定其選舉表現嗎?**
出於對上述疑問的好奇,我在本專案中收集 2000年 ~ 2020年臺灣各大選舉之參選人照片及其選舉資料,嘗試找出候選人臉部資訊及選舉資訊之間的可預測性,並進一歩透過生成式對抗網路(GAN)重現出相同特徵的臉,看看**是否真的存在一張讓人想用選票好好教訓一下的臉?**
## How to do
這裡總結了當初在想如何開始時的一些想法,太冗長可以直接跳到「執行過程」章節,不影響閱讀~
專案分成「資料收集」、「臉部輸出特徵」、「資料分析」、「模型推論」及「GAN實作」等部份執行,說明如下:
### 一、資料收集 (Data Collection)
*使用工具:Selenium、requests*
在網路上查詢相關資料集後,我將資料收集分為兩個部分:
#### 1. 選舉資訊
在「政府資料開放平台」(https://data.gov.tw/dataset/13119)可下載各大選舉之資訊,包含選區、候選人資訊及得票情形
#### 2. 參選人照片
在「選舉黃頁」(https://elections.olc.tw/)中有大量參選人大頭照,在詢問過網站管理人江明宗先生取得同意後預計在此撈取照片
### 二、臉部輸出特徵 (Face features Embedding)
*使用工具:FaceNet、BayesianGaussianMixtureModel*
本專案之假設為「不同的臉部特徵與選舉表現存在可預測性」,我在這裡先作假設「長相類似的臉應有類似的選舉表現」,因在此階段將相近的臉找出並放在同一類別供下一階段分析驗證使用,作法分為兩個部分:
#### 1. FaceNet
FaceNet 是 2015 年發表之臉部辨識 (Face Recognition) 模型,其特色為:可將臉部圖片輸出為一個高維度向量 (一般為128維),由於模型使用 Tiplet Loss 的設計,相似特徵的輸出向量會在高維空間中的距離接近,換言之,**長得越接近的臉輸出結果的距離就越接近**
#### 2. 資料聚類 (Clustering)
基於 FaceNet 對不同臉部特徵之間建立的距離性質,我們就可以進一歩實現對於專案的假設:「長相類似的臉應有類似的選舉表現」,將長相類似的臉進行類別標記
ML 中對於 Clustering 有許多不同的作法,在純粹的非監督式學習中不容易說出哪一種作法比較好;我將依照過去的使用經驗採用半監督式學習的作法:**先使用貝氏高斯混合模型對 30% 的資料作 Clustering,拿來訓練貝氏高斯混合分類器後對剩餘 70% 的資料作 Classification**
### 三、資料分析 (EDA)
在上一階段取得臉部的資訊及類別後,對於「長相類似的臉應有類似的選舉表現」這個論述已取得一半的資訊,接下來需要做的就是如何定義「選舉表現」?
臺灣有各種不同級別選舉,各級別之間的選民人口及當選門檻皆不同,如果單純以當選與否評定參選人的表現,便會出現一個問題:**在里長選舉中取得 80% 得票率的當選者跟在總統大選中取得 20% 得票率的落選者,誰的表現更好?**
因此我認為需建立一個整合「得票率」、「得票數」及「當選率」的總合指標,作為該參選人整體的選舉表現,該指標公式如下:
> **選舉表現指標:log(10 * (當選率 +1) * (得票率 * 得票量) +1 )**
取得選舉表現的資訊後,皆下來終於進入資料分析的階段了
這個部份我會針對以下兩個目標進行分析:
#### 1. 臉部類別-選舉表現分析
作為本專案最初的假設,這邊會期待結果是**幾個很明顯不同的分佈**
#### 2. 臉部類別-其它資訊分析
資料集中除得票情形外,還有「性別」、「年齡」、「政黨」...等的資訊,在這邊也會一併分析,期待**找出類別之間相異的資訊**
分析的過程及結論在「執行過程」章節中會再作詳述
### 四、模型推論 (Modeling)
在此階段將使用迴歸模型對「不同的臉部特徵與選舉表現存在可預測性」論述進行驗證,使用臉部特徵資訊作為觀測值(X)對選舉表現(y)進行預測,這邊會**期待最終結果之 R-square 值可以有至少 0.5 以上的水準**
推論的過程及結論在「執行過程」章節中會再作詳述
### 五、GAN 實作
GAN 在這次專案中的任務是「生成一張讓人想投給他的臉」,我把它認定為是一種 CGAN (Conditional GAN) 的任務,至於這樣的 Condition 是一種 **判斷不同類別的分類問題** 亦或是一種 **逼近到某個數值的迴歸問題**,可能要等前面對於「不同的臉部特徵與選舉表現存在可預測性」得到結論才可以下定論 (也有可能就沒有定論...) (悲觀)
不過! 在那之前還是先針對預期的規劃作點說明,在這次的專案我規劃 GAN 的架構大致有以下內容:
#### 1. WGAN (Wasserstein GAN)
這邊不多贅述 GAN 的基本架構,WGAN 對傳統 GAN 帶來的改變有兩點:
##### (1) 將 Wasserstein Distance 取代原本的 Loss Function ( BinaryCrossEntropy ),讓 GAN 在正負樣本分佈拉扯的過程中避免計算 Log loss (the log D trick) 而造成 Generator 無法順利更新權重而學壞掉的問題
##### (然而後續有論文指出其實 WGAN 所做的 Wasserstein Distance並不是真正的 Wasserstein,認為 WGAN 的貢獻主要來自 Gradient 上的限制,嗯好我再打下去這個括號就太長了)
##### (2) 定義 Lipschitz 限制來避免正樣本及負樣本梯度跑到無窮大而造成梯度太陡峭的問題,最初是使用 Gradient-Clip ,後續有其它限制 Gradient 的方式,例如本專案要用到的 Gradient-Penalty
用 WGAN 的理由很簡單,它避免了傳統 GAN 中只要 Discriminator 太強(把正負樣本分佈分得太開) Generator 就會學壞掉的問題,除了減少訓練麻煩外也讓 Discriminator 可以變更強然後更好地引導 Generator
#### 2. ACGAN (Auxiliary Conditional GAN)
ACGAN 是 Conditional GAN 的一種延伸,它與最開始的 CGAN 的不同點是使用額外 Classifier 進行 Condition 的判別,並將 Loss 傳回到 GAN 網絡中。 讓原本的 GAN 除了「生成逼真的假數據」這個任務外需要同時進行「生成明顯不同類別的數據」
(原始的 CGAN 是將 Condition concat 在輸入向量中,Generator 和 Discriminator 其實還是在做同樣一件事情)
ACGAN 有兩種型態,**一種是將 Classifier 包在 Discriminator 內**,讓 Discriminator 除了輸出「像不像真的」的資訊外再輸出「像不像某個類別」的資訊,並將 Classification Loss 加到 Discriminator;**另一種是將 Classifier 放在跟 Discriminator 平行的位置**,變成 Generator 要一次騙過 Discriminator 跟 Classifier,並將 Classification Loss 加到 Generator,等於是兩個模型在監督 Generator,一個負責看「像不像真的」另一個負責看「像不像某個類別」
我預計使用 ACGAN 第二種架構的理由有:
##### (1) 額外的 Classifier 可以跟 GAN 分開訓練,代表一開始就可以用訓練好的 Classifier 來引導 Generator
##### (2) 由於 WGAN 會使用 Gradient Penalty,如果把 Classifier 包在 Discriminator 內可能會使分類的學習效果變差,不如一開始就丟到外面
##### (事後想想第一種架構也可以先把 Classifier 訓練好然後把權重丟到 Discriminator 裡,但在 Discriminator 判別真假的時候又會動到這些權重,覺得分開來整件事會比較單純)
但這樣做也是有一點風險
##### 到時候 Generator 要面對的就是 Discriminator 跟訓練好的 Classifier ,對於 Loss 可能就需要一些控制避免 Generator 出現 mode-collapse (都只生成差不多的臉)
實際上許多論文有 ACGAN 及 WGAN 合併使用的案例,這邊會參考[這篇論文](http://www.ijicic.org/ijicic-180303.pdf?fbclid=IwAR1Y33d1vexN_A_KBBXzrDnQAe9MGKENMosvDJ7FykGZdH0DI8SdHK3APhY)的架構如下:
![](https://i.imgur.com/mKDuujt.png)
#### 3. 嘗試新架構:CorrectNet (看情況使用)
當初在查資料的時候,在知乎上看到了 2022 年初發的[這篇文章](https://zhuanlan.zhihu.com/p/488560474),作者在他的 GAN 中加入了新的 Model 叫 CorrectNet (校正器),這樣的作法用更少的 epoch 達到更好的生成效果! (還是跟 WGAN-gp比!)
下面會引用文中的 code ,文章中 CorrectNet 的邏輯是:
##### (1) 先用類似 WGAN-gp 中 gradient-penalty 的做法,在正樣本(真資料)分佈與負樣本(假資料)分佈中各隨機 sample 幾個點,找到在正樣本 sample 的點與假樣本 sample 的點中間隨機再抓取一些點,先稱作「中間點」
```
rate = (0.1 * torch.rand([len(dataT), 1])).to(device)
mid = (1 - rate) * dataT + rate * (dataF-dataF.mean(-1).view(-1,1)).detach()
```
##### (2) 校正器的工作就是輸入這些「中間點」後輸出一個最接近正樣本的點,意即「只要校正器吃到正負樣本之間的任一個點,就會回傳真實數據」,因此校正器的訓練長這樣(loss function 是 MSE):
```
# 训练一次校正网络
out1 = cNet(mid)
lossC = lossCfun(dataT, out1)
optimC.zero_grad()
lossC.backward()
optimC.step()
```
##### (3) 接下來在訓練 generator 的時候,校正器會輸入 generator 生成的假資料並回傳真實資料,再將 generate 的假資料 (dataF) 和校正器的校正資料 (dataC) 計算距離 (Loss) 後加到 generator 身上,意即「告訴 generator 真實樣本的樣子,讓 generator 參考以更容易騙過 discriminator」
```
#将假数据通过校正网络得到校正数据和校正误差
dataC=cNet(dataF.detach())
lossC=lossCfun(dataC,dataF)
#总误为
lossG=(max(0.1,0.99**(ep//10)))* -pGF.mean()+0.2*lossC
lossG.backward()
optimG.step()
```
其實看完的時候有個疑問:步驟一中在正負樣本的 sample 是完全隨機的,那為何還要用校正器?直接給 generator 看正樣本不好嗎?
另外也有人提到 GAN 的價值就是生成不存在的東西,直接給他看真實數據讓他背答案不就沒意義了嗎?也是蠻有道理 (或許這有回答到我的疑問?也或許作者把 lossC 乘 0.2 是要因應這問題?)
不過這樣的想法確實是蠻有趣的,希望在後續實作的過程中可以嘗試,只要我的 san 值...沒有因為 train GAN 掉太多...就試試看!
---
## 執行過程
### 一、資料收集 (Data Collection)
「選舉資訊」上「政府公開資料庫」下載即可
「參選人照片」我一開始是用 Selenium 開 firefox 瀏覽器去一張一張下載,在等待期間突然發現選舉黃頁是有 API 提供資料撈取的!
在問過網站負責人取得同意後,直接用 requests 快速撈到所有照片
### 二、臉部資訊收集 (Face features Embedding)
#### 1. FaceNet Embedding
透過 FaceNet 預訓練模型將各個照片轉換成向量,我這邊是選用 VGGFace2 的 pretrained model,另外原文指出在輸出特徵(Embedding)為 128 維度的狀況下模型表現相對較好
#### 2. Data Clustering
在開始分群前,我先透過 PCA 以 95% 的訊息保存,將 FaceNet 輸出的臉部資訊向量維度從128 維降到 28 維
接著使用 K-Means Elbow 法,判斷分群數應設為 6 組
![](https://i.imgur.com/HISsIbs.png)
接著使用貝氏高斯混合模型及分類器進行半監督式學習將各圖片分群,結果如下圖:
![](https://i.imgur.com/boRtFQK.jpg)
不太容易判斷成效如何... 不過第一、三、四、六組中每張臉感覺真的有蠻高的相似度
接下來看一下各組總人數統計:
![](https://i.imgur.com/XEDs66V.png)
由圖判斷第五組 (face_group=4) 有明顯較多的人數
### 三、資料分析 (EDA)
在專案規劃中有說到要使用自定義的選舉表現指標
> **選舉表現指標:log(10 * (當選率 +1) * (得票率 * 得票量) +1 )**
這邊將每一位參選人的選舉表現指標算出後,繪製出「當選率-選舉表現指標」kde圖以方便觀察:
![](https://i.imgur.com/LscONRb.png)
X軸為「選舉表現指標」分佈,Y軸為「當選率」分佈;由圖判斷:
多數參選人分佈於左下,屬於當選率及選舉表現均較低的區域;以一些較有名的參選人為例:陳水扁及柯文哲在選舉中表現極佳,其中又以陳水扁所參選之選舉級別及次數皆高於柯文哲,分佈於右上區域;宋楚瑜雖當選率低,然而多次參選亦累積一定的表現分數,分佈於右下區域
(本專案僅統計 2000-2020 年之數據)
#### 1. 臉部類別-選舉表現分析
判斷目前手上資料與選舉表現相關的資訊應為「表現指標」及「當選率」,以下針對這兩項資訊進行分析
首先觀察「各臉部類別的當選率-選舉表現」kde 分佈如下:
![](https://i.imgur.com/WAdi9V5.png)
除了第三組以外,其餘各組的差異看起來不太直觀... 因此我們再將「表現指標」及「當選率」拆開來看看
「各臉部類別的選舉表現」kde 分佈:
![](https://i.imgur.com/yqNLqgh.png)
「各臉部類別的當選率」kde 分佈:
![](https://i.imgur.com/FycUAb2.png)
單看 kde 分佈判斷各組皆主要分佈於低分區域,但若往高分區域觀察則各組之間又有些許不同
觀察高分群的分佈後得到以下關於選舉表現的排序: 第四組 ~= 第五組 > 第一組 ~= 第二組 > 第六組 > 第三組
另外觀察高當選率的分佈後得到關於當選率的排序: 第四組 ~= 第五組 > 第一組 ~= 第二組 > 第六組 > 第三組
「選舉表現」及「當選率」所得到的結論相似,**不同臉部類別的分佈情形雖大致雷同,但在「表現較好」的分佈上確實存在著差異**
最後將各類別的「選舉表現」及「當選率」作成其它圖表:
![](https://i.imgur.com/PX2o9Yq.png)
![](https://i.imgur.com/xzWA0pS.png)
各統計表結論皆與前述大致相同,值得注意的是第一、二組存在表現極佳的離群值
總上所述,就目前為止暫時概略地下個小結論:
> 整體選舉表現排序:第四組 ~= 第五組 > 第一組 ~= 第二組 > 第六組 > 第三組
#### 2. 臉部類別-其它資訊分析
為方便更好地刻劃每一組的輪廓,這裡將分析一些選舉相關的資訊
##### 2.1 政黨比例
![](https://i.imgur.com/9nykmL5.png)
表現比較好的組別民進黨的比例似乎較高,而較差的組別無黨的佔比則較高
##### 2.2 年齡分佈
![](https://i.imgur.com/wD0FKwK.png)
由於許多參選者多年參選,年齡的部份我是取該參選人平均參選年齡作統計
表現比較好的組別年齡有較年輕的分佈,此處由於第三組有大量年齡缺失值,因此分佈可能與實際較有出入
##### 2.3 學歷比例
![](https://i.imgur.com/fGKAVXM.png)
學歷這邊有一個有趣的點:同一個參選人可能會在不同時間報上不同的學歷,所以我直接把同一個人報過的所有學歷都拿來統計
由圖表判斷:表現較好的組別傾向於報上學歷 (灰色區塊為未報學歷),且「碩士」、「大學」的比例較高,「高中」比例較低
##### 2.4 性別比例
![](https://i.imgur.com/t6DDd4N.png)
性別的部份很明顯地:第四組的女性比例較其它組別高上許多
##### 2.4 參與全國性選舉比例
![](https://i.imgur.com/CDQEDHT.png)
在分析選區的時候遇到一個問題:「政府資料開放平台」的資料僅告知編號 0 為全國性選舉,剩下的其它編號都沒有說明,我在網路上也找不到相關資訊...
因此這邊以「參與全國性選舉佔比」作為統計資料,結果發現第二組的參選人參與全國性選舉的比例明顯較其它組高
##### 2.5 小結
透過以上的分析,我們可以大致描繪各組別的輪廓如下:
> 第一組:人數第二多的組別,各個特質較為中庸,多數人從地方選舉開始穩札穩打,存在少數精英有十分優秀的表現
> 第二組:由於無黨籍比例>(國民黨比例+民進黨比例),失去傳統大黨的援助資金上可能短缺,但仍勇敢挑戰全國性大型選舉的組別
> 第三組:謎一般的組別(一堆資料缺失),絕大部份為無黨籍,雖多數人在選舉上表現不佳,仍不懈地持續參加各類選舉,是一群衝到第一線關心臺灣的平民百姓
> 第四組:女性為多數的組別,雖人數較少,但選舉表現、當選率皆較高甚至是最高,同時擁有較高的學歷、較年輕的參選人,是質量頗高的菁英族群
> 第五組:人數最多的組別,在選舉表現、當選率等指標皆表現優秀,絕大多數為男性
> 第六組:年齡分佈較高的組別,同時全國性選舉的參與率也最低,是一群長期深耕地方的長輩們,默默並持續地為熟悉的土地貢獻心力
### 四、模型推論 (Modeling)
這裡開始會對我一開始最關心的問題:「不同的臉部特徵與選舉表現是否存在可預測性?」進行驗證
我打算先以單純的臉部資訊 (FaceNet 輸出向量經 PCA 降維後的 28 維向量、臉部分群類別) 作為 X,把選舉表現指標作為 y 進行預測。如效果實在不理想,再嘗試將其餘資訊納入 X
#### 1. 臉部資訊-選舉表現指標
![](https://i.imgur.com/zQ89i6b.png)
分別測試幾種迴歸模型的 MSE 及 R-square 表現:
(1) Dummy_prediction: 單純以平均值測試 R-square=0 時的表現
(2) LinearRegressor: 作為 base-line
(3) XgbRegressor
```
xgb = XGBRegressor(booster='gbtree', n_estimators=550, learning_rate=0.01,
gamma=100, subsample=0.2, max_depth=3)
```
| | Dummy_pred | LinearRegr | XgbRegr |
| --- | ---------- | ---------- | ------- |
| MSE | 18.64 | 16.77 | 16.71 |
| R2 | 0.0 | 0.100 | 0.103 |
tune 到最後似乎沒有太明顯的起色...
R-square 跟最初預期目標 (0.5 以上) 還是差距太大,接下來嘗試結合其它資訊再跑跑看
#### 2. 綜合資訊-選舉表現指標
![](https://i.imgur.com/gFdukV9.png)
分別測試幾種迴歸模型的 MSE 及 R-square 表現:
(1) Dummy_prediction: 單純以平均值測試 R-square=0 時的表現
(2) LinearRegressor: 作為 base-line
(3) XgbRegressor
```
xgb = XGBRegressor(n_estimators=500, learning_rate=0.01, gamma=0, subsample=0.75,
colsample_bytree=1, max_depth=7, seed=42)
```
| | Dummy_pred | LinearRegr | XgbRegr |
| --- | ---------- | ---------- | ------- |
| MSE | 18.30 | 1861 | 12.17 |
| R2 | 0.0 | -101 | 0.335 |
XgbRegressor 有了較明顯的起色,但離 R-square=0.5 還有一段距離...
再嘗試僅保留「臉部類別」跟其它選舉資訊跑跑看
#### 3. 臉部類別-選舉表現指標
![](https://i.imgur.com/rwWFkjr.png)
分別測試幾種迴歸模型的 MSE 及 R-square 表現:
(1) Dummy_prediction: 單純以平均值測試 R-square=0 時的表現
(2) LinearRegressor: 作為 base-line
(3) XgbRegressor
```
xgb = XGBRegressor(n_estimators=500, learning_rate=0.01, gamma=0, subsample=0.75,
colsample_bytree=1, max_depth=7, seed=42)
```
| | Dummy_pred | LinearRegr | XgbRegr |
| --- | ---------- | ---------- | ------- |
| MSE | 18.30 | 312 | 11.7 |
| R2 | 0.0 | -18 | 0.360 |
XgbRegressor R-square 進一歩提升,仍未達到 0.5
Xgb 的 feature importance 如下:
![](https://i.imgur.com/JIj6sVw.png)
排名較前面的有:「年齡」、「是否為副手」、「是否為現任」... 等,「臉部類別」 (face_group) 中第三、五、六 (2, 4, 5) 排序在前10名 ( X 總共 171 維)
再看看 Shapley-Value 排序:
![](https://i.imgur.com/7PQkBro.png)
臉部類別:三、五、四、六組對模型輸出具有一定的貢獻度
接下來試試看單純使用選舉資訊跑預測,對照看看臉部資訊對於預測是否有正面的影響
#### 4. 拿掉臉部資訊-選舉表現指標
![](https://i.imgur.com/2nTHKk5.png)
分別測試幾種迴歸模型的 MSE 及 R-square 表現:
(1) Dummy_prediction: 單純以平均值測試 R-square=0 時的表現
(2) LinearRegressor: 作為 base-line
(3) XgbRegressor
```
xgb = XGBRegressor(n_estimators=800, learning_rate=0.01, gamma=10, subsample=0.75,
colsample_bytree=1, max_depth=7, seed=42)
```
| | Dummy_pred | LinearRegr | XgbRegr |
| --- | ---------- | ---------- | ------- |
| MSE | 18.30 | 726 | 13.1 |
| R2 | 0.0 | -39 | 0.282 |
拿掉了臉部資訊 (28維向量 & 臉部類別) 模型表現又往下掉,看起來臉部相關的資訊在可預測性是有幫助的
#### 5. 小結
綜合模型推論的成果,整理結論如下:
> 單純以臉部特徵預測選舉表現,幾乎沒有學習成果
> 配合「臉部特徵」、「臉部類別」及「選舉資訊」預測選舉表現,有稍微的學習成果
> 僅使用「臉部類別」及「選舉資訊」預測選舉表現,學習成果更佳,然而 R-square 仍未達到目標的數值
> 「臉部類別」各個類別對選舉表現預測有一定的貢獻度
### 實作 GAN 之前...
依照模型推論的結論,最開始的問題「不同的臉部特徵與選舉表現是否存在可預測性?」得到了一個否定的答案...
但是 !
還記得在 EDA 探討出六個臉部類別各自的輪廓嗎?
或許因為「不同的臉部特徵與選舉表現不存在明顯可預測性」而令我們無法讓 GAN 生成「選舉表現最好的臉」,但是因為 **「不同類別的臉各自有顯著不同的特性」,我們仍然可以讓 GAN 生成「某個類別,具備某種選舉特性的臉」**
而「臉部類別」在 F-Score 和 Shapley-Value 中也有一定的貢獻度,**或許我們可以先讓 GAN 生成各類別的臉再交給人們進行投票 (label),持續收集的資料就能繼續拿來訓練模型進而提升模型表現!** ( 樂觀 )
總之我想表達的是:**以「臉部類別」作為 condition 讓 GAN 進行生成是有意義的**,後續我將以此作為目標訓練 GAN 來生成人臉
### 五、GAN 實作
終於到了打最終 boss 的時候,有沒有看到畫面下方出現了長長的血條呢?
![](https://i.imgur.com/LBXezcF.gif)
之前規劃這次專案預計使用 ACWGAN 的架構,參考架構圖長這樣:
![](https://i.imgur.com/Gx5wDjc.png)
主要會用到的角色有: Classifier、Generator 和 Discriminator,一些比較重要的作法有: 「用 Wasserstein Distance 取代 BCE Loss」、「Gradient Penalty」和「Generator loss 權重調整」;前兩項是 WGAN (應該說 WGAN-gp) 需要的工具,第三項則是為了避免因為 Loss 太大讓 Generator 直接擺爛 (mode-collapse)
接下來按照 「訓練 Classifier」和「訓練 ACWGAN」分項說明
#### 1. 訓練 Classifier
根據前面資料分析和模型推論的結論,我們的 ACWGAN 需要能夠判斷的是「一張臉屬於哪一個臉部類別 (face_group)」,因此 Classifier 的目標十分明確,就是要「學會分類六種不同的臉部類別」
由於我們的臉部類別是經由 FaceNet 輸出的向量聚類來的,自然會想到直接把 FaceNet 拿來用,於是我開始建 Classifier:
```
from facenet_pytorch import InceptionResnetV1
class Classifier(nn.Module):
def __init__(self, num_classes=6):
super(Classifier, self).__init__()
self.facenet = InceptionResnetV1(pretrained='vggface2', classify=True, num_classes=num_classes)
def forward(self, x):
c_out = self.facenet(x)
return c_out
classifier = Classifier().to(device)
c_loss = nn.CrossEntropyLoss()
lr = 1e-4
cls_optimizer = optim.Adam(classifier.parameters(), lr)
```
就是...長得蠻膚淺的這樣...
原本還想說前面用 FaceNet 開 eval() 接上自己架的 classifier,結果還是原本的 pretrained classifier 的表現最好...
之後的訓練也都很單純跑 20e 就差不多收斂了,由於我是分段跑所以下圖是 10e 的 loss_history
![](https://i.imgur.com/wjrfKvA.png)
那現在有了 Classifier ,接下來就進到 ACWGAN 的訓練吧
#### 2. 訓練 ACWGAN
Generator 生成影像的邏輯大致可以理解為:將輸入的資訊「降解析」成幾個帶有資訊的 feature-map,然後再將這些 feature-map 「超解析」合併成圖片大小輸出
Discriminator 判斷真偽也有類似的邏輯:將輸入的圖片「降解析」成幾個帶有資訊的 feature-map,再用幾層 Linear Layer 將這些資訊計算後輸出分數
因此我在一開始先定義好一些用得到的 block
```
class up_block(nn.Sequential):
def __init__(self, in_planes, out_planes, kernel_size, stride, padding=1):
padding = (kernel_size-1)//2
norm_layer = nn.InstanceNorm2d
in_planes = in_planes//4
super(up_block, self).__init__(
nn.PixelShuffle(2),
nn.Conv2d(in_planes, out_planes, kernel_size, stride, (padding, padding),
bias=True, padding_mode='reflect'),
norm_layer(out_planes),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(out_planes, out_planes, kernel_size, stride, (padding, padding),
bias=True, padding_mode='reflect'),
norm_layer(out_planes),
nn.LeakyReLU(0.2, inplace=True),
)
class down_block(nn.Sequential):
def __init__(self, in_planes, out_planes, kernel_size, stride, padding=1):
padding = kernel_size//2
norm_layer = nn.InstanceNorm2d
super(down_block, self).__init__(
nn.Conv2d(in_planes, out_planes, kernel_size, stride, (padding, padding),
bias=True, padding_mode='reflect'),
norm_layer(out_planes),
nn.LeakyReLU(0.2, inplace=True),
)
```
up_block 是用來實現前述「超解析」的描述,down_block 則是用來實現「降解析」;另外值得提的是 PixelShuffle() 的作用類似於 Conv_Transpose(),大概講述他的工作邏輯是: 假設輸入的圖像資料是 (n, d ** 2, h, w), pixelshuffle 會讓圖像的 h 和 w 去資料的深度 (d) 中取得資訊來擴增,最終輸出 (n, 1, d * h, d * w) 擴增後的影像;這樣的做法被認為解決了 transpose convolution 在生成影像時容易出現黑點的問題。更詳細的說明可以參考[這篇](https://blog.csdn.net/g11d111/article/details/82855946)
另外, reflection padding、InstanceNorm 跟 LeakyReLU 都是做影像生成很常用到的工具
##### 2.1 Generator
接下來 Generator 的部分,我們需要 GAN 判斷的 Condition 會和 latent vector 合併後再進行生成,意即叫 Generator 「看著 Condition 的條件生成圖像」
至於 Condition 合併的方式不只一種,我是先用 Embedding layer 吃下 condition,再將他的輸出跟 latent vector concat 在一起:
```
class Generator(nn.Module):
def __init__(self, class_num=6, emb_dim=10):
super(Generator, self).__init__()
input_dim = 100 + emb_dim*class_num # latent vector dim + embedding output
n_nodes = 128*128*3
self.label_embedding = nn.Embedding(class_num, emb_dim).cpu()
self.hidden_layer = nn.Sequential(
nn.Linear(input_dim, n_nodes),
nn.LeakyReLU(0.2)
)
self.down_sample = nn.Sequential(
# input: 3*128*128
down_block(3, 64, 3, 2), # 64*64*64
down_block(64, 128, 3, 2), # 128*32*32
down_block(128, 256, 3, 2), ##256*16*16
down_block(256, 512, 3, 2), #512*8*8
# down_block(512, 512, 3, 2) #512*4*4
)
self.up_stack = nn.Sequential(
# up_block(512, 512, 3, 1), #8*8
up_block(512, 256, 3, 1), #16*16
up_block(256, 128, 3, 1), #32*32
up_block(128, 64, 3, 1), #64*64
up_block(64, 32, 3, 1), ##128*128
nn.Conv2d(32, 3, 3, 1, 1), # 3*n*n
nn.Tanh()
)
def forward(self, x, c):
c = self.label_embedding(c)
c = c.view(-1, 6*10) # emb_dim*class_num
x = torch.cat([x, c], 1)
x = self.hidden_layer(x)
x = x.view([-1, 3, 128, 128])
x = self.down_sample(x)
output = self.up_stack(x)
return output
```
中間的運算就跟前面講到的內容差不多,記得最後輸出的 activation 設成 Tanh() 才能看到比較柔順的生成圖片
##### 2.2 Discriminator
接下來 discriminator 的任務是分辨圖片的真偽,由於不需要 Condition 的判斷,因此架構就如同前面提到的內容:
```
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
output_dim = 512*8*8
self.down_sample = nn.Sequential(
# input: 3*128*128
down_block(3, 64, 3, 2), # 64*64*64
down_block(64, 128, 3, 2), # 128*32*32
down_block(128, 256, 3, 2), ##256*16*16
down_block(256, 512, 3, 2), #512*8*8
# down_block(512, 512, 3, 2) #512*4*4
)
self.output_layer = nn.Sequential(
nn.Linear(output_dim, 1),
nn.LeakyReLU(0.2, inplace=True),
)
def forward(self, x):
x = self.down_sample(x)
x = x.view(x.shape[0], -1)
output = self.output_layer(x)
return output
```
接著是前面提到的 Wasserstein Distance 及 gradient-penalty 的內容如下:
```
def w_distance(fake, real=None):
if real is None:
return -torch.mean(fake)
else:
return torch.mean(fake)-torch.mean(real)
def gradient_penalty(D, real, fake):
alpha = torch.Tensor(np.random.random((real.size(0), 1, 1, 1))).to(device)
x_hat = (alpha*real + ((1 - alpha) * fake)).requires_grad_(True)
d_x_hat = D(x_hat)
output_grads = torch.Tensor(real.shape[0], 1).fill_(1.0).to(device)
output_grads.requires_grad = False
gradients = torch.autograd.grad(
outputs=d_x_hat,
inputs=x_hat,
grad_outputs=output_grads,
create_graph=True,
retain_graph=True,
only_inputs=True,
)
gradients = gradients[0].view(gradients[0].size(0), -1)
gradient_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean()
return gradient_penalty
```
工具都有了,接下來終於來到訓練的部份了
我們再看一次參考的架構
![](https://i.imgur.com/ElIqPMH.png)
右方 GAN 的訓練有幾個動作:
(1) 叫 generator 看著 condition 生出幾張圖
(2) 讓 discriminator 看著生成的「假圖」和資料集的「真圖」並評分
(3) 將「真圖評分」及「假圖評分」丟到 w_distance() 取得 discriminator 的 Wasserstein Distance
(4) 再讓 discriminator 過一次 gradient_penalty(),取得 discriminator 的 penalty loss
(5) 將 discriminator 的 Wasserstein Distance 及 penalty loss 加起來後拿去更新 discriminator 的 optimizer
(6) 叫 generator 再看著 condition 生出幾張圖
(7) 把生成的「假圖」丟給 discriminator 得到 「假圖評分」
(8) 將「假圖評分」丟到 w_distance() 取得 generator 的 Wasserstein Distance
(9) 再把生成的「假圖」連同 condition 丟給 classifier 取得 CrossEntropyLoss
(10) 將 generator 的 Wasserstein Distance 及 classifier 的 CrossEntropyLoss **依某個比例**加起來後拿去更新 generator 的 optimizer
上述的動作裡:
(1) ~ (5) 與 discriminator 訓練有關
(6) ~ (10) 與 generator 訓練有關
再貼上來感覺很擠...我會將程式碼附上,總之就是該 detach() 的要加、重覆訓練的話要小心 backward() retain_graph 的坑、記得沒事就存一下 model 不然會發生遺憾的事情... 之類的,反正痛過就知道了 (苦笑)
#### 3. 訓練成果
目前正在努力訓練及調參
但訓練要花蠻久的時間... Colab 又很常斷掉...
有比較好的結果後會再更新這邊的內容
(2022/9/25)
---
## 結論
從個人成長經歷的角度觀察,臺灣社會長期對於選舉候選人的面相有著各種評論,而「這些面相相關的評論是否會影響人們的投票行為,進而影響各個候選人的選舉表現?」則是本專案所關切的問題
為觀察上述問題,我先提出假設:「長相相似的人應有相似的表現」,接下來分成以下幾個步驟來驗證假設:
**一、 找出相似的臉
二、 定義選舉表現
三、 觀察不同種臉的特質
四、 驗證「是否可透過臉預測一個人的選舉表現」**
以下分別說明
### 一、 找出相似的臉
我透過 FaceNet 及 Clustering 的技術,將 2000 - 2020 年各大選舉的參選人**依照他們臉的相似度**分成以下 6 組
![](https://i.imgur.com/d6sAAZ0.jpg)
### 二、 定義選舉表現
由於不同參選人所面對的選舉層級不同,因此我整合相關資訊定義出「選舉表現指標」
> **選舉表現指標:log(10 * (當選率 +1) * (得票率 * 得票量) +1 )**
以此指標觀察現今所有參選人,結果如下圖:
![](https://i.imgur.com/a1JyCMC.png)
(X軸為選舉表現指標,Y軸為當選率)
### 三、觀察不同種臉的特質
後續透過選舉資料分析,發現這幾組「面容相似」的人們在選舉上有以下幾個顯著不同的特質:
**1. 選舉表現**
運用「當選率」以及自定義的「選舉表現指標」評估,這 6 組人員在選舉表現上有高下之分,其排序為:
> **第四組 ~= 第五組 > 第一組 ~= 第二組 > 第六組 > 第三組**
**2. 其它特質**
透過相關資料分析,各組之輪廓描述如下:
> ##### 第一組:人數第二多的組別,各個特質較為中庸,多數人從地方選舉開始穩札穩打,存在少數精英有十分優秀的表現
>
> ##### 第二組:由於無黨籍比例>(國民黨比例+民進黨比例),失去傳統大黨的援助資金上可能短缺,但仍勇敢挑戰全國性大型選舉的組別
>
> ##### 第三組:謎一般的組別(一堆資料缺失),絕大部份為無黨籍,雖多數人在選舉上表現不佳,仍不懈地持續參加各類選舉,是一群衝到第一線關心臺灣的平民百姓
>
> ##### 第四組:女性為多數的組別,雖人數較少,但選舉表現、當選率皆較高甚至是最高,同時擁有較高的學歷、較年輕的參選人,是質量頗高的菁英族群
>
> ##### 第五組:人數最多的組別,在選舉表現、當選率等指標皆表現優秀,絕大多數為男性
>
> ##### 第六組:年齡分佈較高的組別,同時全國性選舉的參與率也最低,是一群長期深耕地方的長輩們,默默並持續地為熟悉的土地貢獻心力
### 四、 驗證「是否可透過臉預測一個人的選舉表現」
接下來我透過模型驗證 「是否可透過臉部來預測一個人的選舉表現」,結論如下:
> ##### 1. 「臉部資訊」對於選舉表現之預測確實有所影響
>
> ##### 2. 目前「選舉表現」之可預測性並不明顯,尚無法達到可靠的預測
>
> ##### 3. 後續可嘗試透過持續 label 的方法提升目前模型預測的可靠度
### 總結
臺灣選舉候選人的臉確實有某種資訊可將其納入六種類別的其中一類,而每一個類別有著顯著不同的輪廓、特質甚至是選舉表現
然而,目前來說這樣的論述僅止於統計上而言,尚無法僅透過臉部及相關資訊準確地預測一個人的選舉表現
不過,這樣否定的結論或許可以透過持續收集資料改善,因此我在專案中提出可能的解法為:透過 AI 技術生成「符合這六種類別特徵的假臉」讓人們進行投票,並將此投票結果拿來持續訓練模型,或許便有機會提高模型的可靠度,進而達到「看臉就知道你選不選得上」
該解法目前正在執行中,未來有任何成果會再更新內容