作者丨蘇劍林
單位丨廣州火焰資訊科技有限公司
研究方向丨NLP,神經網路
個人主頁丨kexue.fm
話在開頭
在上一篇文章細水長flow之NICE:流模型的基本概念與實現中,我們介紹了 flow 模型中的一個開山之作:NICE 模型。從 NICE 模型中,我們能知道 flow 模型的基本概念和基本思想,最後筆者還給出了 Keras 中的 NICE 實現。
本文我們來關心 NICE 的升級版:RealNVP 和 Glow。
Glow 模型的取樣演示:
精巧的flow
不得不說,flow 模型是一個在設計上非常精巧的模型。總的來看,flow 就是想辦法得到一個 encoder 將輸入 x 編碼為隱變數 z,並且使得 z 服從標準正態分佈。得益於 flow 模型的精巧設計,這個 encoder 是可逆的,從而我們可以立馬從 encoder 寫出相應的 decoder(生成器)出來,因此,只要 encoder 訓練完成,我們就能同時得到 decoder,完成生成模型的構建。
為了完成這個構思,不僅僅要使得模型可逆,還要使得對應的雅可比行列式容易計算,為此,NICE 提出了加性耦合層,透過多個加性耦合層的堆疊,使得模型既具有強大的擬合能力,又具有單位雅可比行列式。就這樣,一種不同於 VAE 和 GAN 的生成模型——flow 模型就這樣出來了,它透過巧妙的構造,讓我們能直接去擬合機率分佈本身。
待探索的空間
NICE 提供了 flow 模型這樣一種新的思路,並完成了簡單的實驗,但它同時也留下了更多的未知的空間。flow 模型構思巧妙,相比之下,NICE 的實驗則顯得過於粗糙:只是簡單地堆疊了全連線層,並沒有給出諸如摺積層的用法,論文雖然做了多個實驗,但事實上真正成功的實驗只有 MNIST,說服力不夠。
因此,flow 模型還需要進一步挖掘,才能在生成模型領域更加出眾。這些拓展,由它的“繼承者”RealNVP 和 Glow 模型完成了,可以說,它們的工作使得 flow 模型大放異彩,成為生成模型領域的佼佼者。
RealNVP
這部分我們來介紹 RealNVP 模型,它是 NICE 的改進,來自論文 Density estimation using Real NVP [1]。它一般化了耦合層,併成功地在耦合模型中引入了摺積層,使得可以更好地處理影象問題。更進一步地,它還提出了多尺度層的設計,這能夠降低計算量,透過還提供了強大的正則效果,使得生成質量得到提升。至此,flow 模型的一般框架開始形成。
後面的 Glow 模型基本上沿用了 RealNVP 的框架,只是對部分內容進行了修改(比如引入了可逆 1×1 摺積來代替排序層)。不過值得一提的是,Glow 簡化了 RealNVP 的結構,表明 RealNVP 中某些比較複雜的設計是沒有必要的。因此本文在介紹 RealNVP 和 Glow 時,並沒有嚴格區分它們,而只是突出它們的主要貢獻。
仿射耦合層
其實 NICE 和 RealNVP 的第一作者都是 Laurent Dinh,他是 Bengio 的博士生,他對 flow 模型的追求和完善十分讓我欽佩。在第一篇 NICE 中,他提出了加性耦合層,事實上也提到了乘性耦合層,只不過沒有用上;而在 RealNVP 中,加性和乘性耦合層結合在一起,成為一個一般的“仿射耦合層”。
這裡的 s,t 都是 x1 的向量函式,形式上第二個式子對應於 x2 的一個仿射變換,因此稱為“仿射耦合層”。
仿射耦合的雅可比矩陣依然是一個三角陣,但對角線不全為 1,用分塊矩陣表示為:
很明顯,它的行列式就是 s 各個元素之積。為了保證可逆性,一般我們約束 s 各個元素均大於零,所以一般情況下,我們都是直接用神經網路建模輸出 log s,然後取指數形式。
註:從仿射層大概就可以知道 RealNVP 的名稱來源了,它的全稱為“real-valued non-volume preserving”,強行翻譯為“實值非體積保持”。相對於加性耦合層的行列式為 1,RealNVP 的雅可比行列式不再恆等於 1,而我們知道行列式的幾何意義就是體積(請參考《新理解矩陣5:體積=行列式》[2]),所以行列式等於 1 就意味著體積沒有變化,而仿射耦合層的行列式不等於 1 就意味著體積有所變化,所謂“非體積保持”。
隨機打亂維度
在 NICE 中,作者透過交錯的方式來混合資訊流(這也理論等價於直接反轉原來的向量),如下圖(對應地,這裡已經換為本文的仿射耦合層圖示):
▲ NICE透過交叉耦合,充分混合資訊
而 RealNVP 發現,透過隨機的方式將向量打亂,可以使資訊混合得更加充分,最終的 loss 可以更低,如圖:
▲ RealNVP透過隨機打亂每一步輸出的整個向量,使得資訊混合得更充分均勻
這裡的隨機打亂,就是指將每一步 flow 輸出的兩個向量 h1,h2 拼接成一個向量 h,然後將這個向量重新隨機排序。
引入摺積層
RealNVP 中給出了在 flow 模型中合理使用摺積神經網路的方案,這使得我們可以更好地處理影象問題,並且減少引數量,還可以更充分發揮模型的並行效能。
註意,不是任意情況下套用摺積都是合理的,用摺積的前提是輸入(在空間維度)具有區域性相關性。影象本身是具有區域性相關性的,因為相鄰之間的畫素是有一定關聯的,因此一般的影象模型都可以使用摺積。
但是我們註意 flow 中的兩個操作:
1. 將輸入分割為兩部分 x1,x2,然後輸入到耦合層中,而模型 s,t 事實上只對 x1 進行處理;
2. 特徵輸入耦合層之前,要隨機打亂原來特徵的各個維度(相當於亂序的特徵)。這兩個操作都會破壞區域性相關性,比如分割操作有可能割裂原來相鄰的畫素,隨機打亂也可能將原來相鄰的兩個畫素分割得很遠。
所以,如果還要堅持使用摺積,就要想辦法保留這種空間的區域性相關性。我們知道,一幅影象有三個軸:高度(height)、寬度(width)、通道(channel),前兩個屬於空間軸,顯然具有區域性相關性,因此能“搞”的就只有“通道”軸。
為此,RealNVP 約定分割和打亂操作,都只對“通道”軸執行。也就是說,沿著通道將輸入分割為 x1,x2 後,x1 還是具有區域性相關性的,還有沿著通道按著同一方式打亂整體後,空間部分的相關性依然得到保留,因此在模型 s,t 中就可以使用摺積了。
▲ 沿著通道軸進行分割,不損失空間上的區域性相關性
▲ 沿著空間軸交錯(棋盤)分割,也是一種保持空間區域性相關性的方案
註:在 RealNVP 中,將輸入分割為兩部分的操作稱為 mask,因為這等價於用 0/1 來區別標註原始輸入。除了前面說的透過通道軸對半分的 mask 外,RealNVP 事實上還引入了一種空間軸上的交錯 mask,如上圖的右邊,這種 mask 稱為棋盤式 mask(格式像國際象棋的棋盤)。
這種特殊的分割也保留了空間區域性相關性,原論文中是兩種 mask 方式交替使用的,但這種棋盤式 mask 相對複雜,也沒有什麼特別明顯的提升,所以在 Glow 中已經被拋棄。
不過想想就會發現有問題。一般的影象通道軸就只有三維,像 MNIST 這種灰度圖還只有一維,怎麼分割成兩半?又怎麼隨機打亂?為瞭解決這個問題,RealNVP 引入了稱為 squeeze 的操作,來讓通道軸具有更高的維度。
其思想很簡單:直接 reshape,但 reshape 時區域性地進行。具體來說,假設原來影象為 h×w×c 大小,前兩個軸是空間維度,然後沿著空間維度分為一個個 2×2×c 的塊(這個 2 可以自定義),然後將每個塊直接 reshape 為 1×1×4c,也就是說最後變成了 h/2×w/2×4c。
▲ squeeze操作圖示,其中2×2的小區域可以換為自定義大小的區域
有了 squeeze 這個操作,我們就可以增加通道軸的維數,但依然保留區域性相關性,從而我們前面說的所有事情都可以進行下去了,所以 squeeze 成為 flow 模型在影象應用中的必備操作。
多尺度結構
除了成功地引入摺積層外,RealNVP 的另一重要進展是加入了多尺度結構。跟摺積層一樣,這也是一個既減少了模型複雜度、又提升了結果的策略。
▲ RealNVP中的多尺度結構圖示
多尺度結構其實並不複雜,如圖所示。原始輸入經過第一步 flow 運算(“flow 運算”指的是多個仿射耦合層的複合)後,輸出跟輸入的大小一樣,這時候將輸入對半分開兩半 z1,z2(自然也是沿著通道軸),其中 z1 直接輸出,而只將 z2 送入到下一步 flow 運算,後面的依此類推。比如圖中的特例,最終的輸出由 z1,z3,z5 組成,總大小跟輸入一樣。
多尺度結構有點“分形”的味道,原論文說它啟發於 VGG。每一步的多尺度操作直接將資料尺寸減少到原來的一半,顯然是非常可觀的。但有一個很重要的細節,在 RealNVP 和 Glow 的論文中都沒有提到,我是看了原始碼才明白的,那就是最終的輸出 [z1,z3,z5] 的先驗分佈應該怎麼取?按照 flow 模型的通用假設,直接設為一個標準正態分佈?
事實上,作為不同位置的多尺度輸出,z1,z3,z5 的地位是不對等的,而如果直接設一個總體的標準正態分佈,那就是強行將它們對等起來,這是不合理的。最好的方案,應該是寫出條件機率公式:
由於 z3,z5 是由 z2 完全決定的,z5 也是由 z4 完全決定的,因此條件部分可以改為:
RealNVP 和 Glow 假設右端三個機率分佈都是正態分佈,其中 p(z1|z2) 的均值方差由 z2 算出來(可以直接透過摺積運算,這有點像 VAE),p(z3|z4) 的均值方差由 z4 算出來,p(z5) 的均值方差直接學習出來。
顯然這樣的假設會比簡單認為它們都是標準正態分佈要有效得多。我們還可以換一種表述方法:上述的先驗假設相當於做瞭如下的變數代換:
然後認為 [ẑ1,ẑ3,ẑ5] 服從標準正態分佈。同 NICE 的尺度變換層一樣,這三個變換都會導致一個非 1 的雅可比行列式,也就是要往 loss 中加入形如的這一項。
乍看之下多尺度結構就是為了降低運算量,但並不是那麼簡單。由於 flow 模型的可逆性,輸入輸出維度一樣,事實上這會存在非常嚴重的維度浪費問題,這往往要求我們需要用足夠複雜的網路去緩解這個維度浪費。
多尺度結構相當於拋棄了 p(z) 是標準正態分佈的直接假設,而採用了一個組合式的條件分佈,這樣儘管輸入輸出的總維度依然一樣,但是不同層次的輸出地位已經不對等了,模型可以透過控制每個條件分佈的方差來抑制維度浪費問題(極端情況下,方差為 0,那麼高斯分佈坍縮為狄拉克分佈,維度就降低 1),條件分佈相比於獨立分佈具有更大的靈活性。而如果單純從 loss 的角度看,多尺度結構為模型提供了一個強有力的正則項(相當於多層影象分類模型中的多條直連邊)。
Glow
整體來看,Glow 模型在 RealNVP 的基礎上引入了 1×1 可逆摺積來代替前面說的打亂通道軸的操作,並且對 RealNVP 的原始模型做了簡化和規範,使得它更易於理解和使用。
■ 論文 | https://www.paperweekly.site/papers/2101
■ 部落格 | https://blog.openai.com/glow/
■ 原始碼 | https://github.com/openai/glow
可逆1×1摺積
這部分介紹 Glow 的主要改進工作:可逆 1×1 摺積。
置換矩陣
可逆 1×1 摺積源於我們對置換操作的一般化。我們知道,在 flow 模型中,一步很重要的操作就是將各個維度重新排列,NICE 是簡單反轉,而 RealNVP 則是隨機打亂。不管是哪一種,都對應著向量的置換操作。
事實上,對向量的置換操作,可以用矩陣乘法來描述,比如原來向量是 [1,2,3,4],分別交換第一、二和第三、四兩個數,得到 [2,1,4,3],這個操作可以用矩陣乘法來描述:
其中右端第一項是“由單位矩陣不斷交換兩行或兩列最終得到的矩陣”稱為置換矩陣。
一般化置換
既然這樣,那很自然的想法就是:為什麼不將置換矩陣換成一般的可訓練的引數矩陣呢?所謂 1×1 可逆摺積,就是這個想法的結果。
註意,我們一開始提出 flow 模型的思路時就已經明確指出,flow 模型中的變換要滿足兩個條件:一是可逆,二是雅可比行列式容易計算。如果直接寫出變換:
那麼它就只是一個普通的沒有 bias 的全連線層,並不能保證滿足這兩個條件。為此,我們要做一些準備工作。首先,我們讓 h 和 x 的維度一樣,也就是說 W 是一個方陣,這是最基本的設定;其次,由於這隻是一個線性變換,因此它的雅可比矩陣就是,所以它的行列式就是 det W,因此我們需要把 −log |det W| 這一項加入到 loss 中;最後,初始化時為了保證 W 的可逆性,一般使用“隨機正交矩陣”初始化。
利用LU分解
以上做法只是一個很基本的解決方案,我們知道,算矩陣的行列式運算量特別大,還容易上限溢位。而 Glow 給出了一個非常巧妙的解決方案:LU 分解的逆運用。具體來說,是因為任意矩陣都可以分解為:
其中 P 是一個置換矩陣,也就是前面說的 shuffle 的等價矩陣;L 是一個下三角陣,對角線元素全為 1;U 是一個上三角陣。這種形式的分解稱為 LU 分解。如果知道這種矩陣的表達形式,顯然求雅可比行列式是很容易的,它等於:
也就是 U 的對角線元素的絕對值對數之和。既然任意矩陣都可以分解成 (7) 式,我們何不直接設W的形式為 (7) 式?這樣一來矩陣乘法計算量並沒有明顯提升,但求行列式的計算量大大降低,而且計算起來也更為容易。
這就是 Glow 中給出的技巧:先隨機生成一個正交矩陣,然後做 LU 分解,得到 P,L,U,固定 P,也固定 U 的對角線的正負號,然後約束 L 為對角線全 1 的下三角陣,U 為上三角陣,最佳化訓練 L,U 的其餘引數。
結果分析
上面的描述只是基於全連線的。如果用到影象中,那麼就要在每個通道向量上施行同樣的運算,這等價於 1×1 的摺積,這就是所謂的可逆 1×1 摺積的來源。事實上我覺得這個名字起得不大好,它本質上就是共享權重的、可逆的全連線層,單說 1×1 摺積,就把它侷限在影象中了,不夠一般化。
▲ 三種不同的打亂方案最終的loss曲線比較(來自OpenAI部落格)
Glow 的論文做了對比實驗,表明相比於直接反轉,shuffle 能達到更低的 loss,而相比 shuffle,可逆 1×1 摺積能達到更低的 loss。我自己的實驗也表明瞭這一點。
不過要指出的是:可逆 1×1 摺積雖然能降低 loss,但是有一些要註意的問題。第一,loss 的降低不代表生成質量的提高,比如 A 模型用了 shuffle,訓練 200 個 epoch 訓練到 loss=-50000,B 模型用了可逆摺積,訓練 150 個 epoch 就訓練到 loss=-55000,那麼通常來說在當前情況下 B 模型的效果還不如 A(假設兩者都還沒有達到最優)。事實上可逆 1×1 摺積只能保證大家都訓練到最優的情況下,B 模型會更優。第二,在我自己的簡單實驗中貌似發現,用可逆 1×1 摺積達到飽和所需要的 epoch 數,要遠多於簡單用 shuffle 的 epoch 數。
Actnorm
RealNVP 中用到了 BN 層,而 Glow 中提出了名為 Actnorm 的層來取代 BN。不過,所謂 Actnorm 層事實上只不過是 NICE 中的尺度變換層的一般化,也就是 (5) 式提到的縮放平移變換:
其中 μ,σ 都是訓練引數。Glow 在論文中提出的創新點是用初始的 batch 的均值和方差去初始化 μ,σ 這兩個引數,但事實上所提供的原始碼並沒有做到這一點,純粹是零初始化。
所以,這一點是需要批評的,純粹將舊概念換了個新名字罷了。當然,批評的是 OpenAI 在 Glow 中亂造新概念,而不是這個層的效果。縮放平移的加入,確實有助於更好地訓練模型。而且,由於 Actnorm 的存在,仿射耦合層的尺度變換已經顯得不那麼重要了。
我們看到,相比於加性耦合層,仿射耦合層多了一個尺度變換層,從而計算量翻了一倍。但事實上相比加性耦合,仿射耦合效果的提升並不高(尤其是加入了 Actnorm 後),所以要訓練大型的模型,為了節省資源,一般都只用加性耦合,比如 Glow 訓練 256×256 的高畫質人臉生成模型,就只用到了加性耦合。
原始碼分析
事實上 Glow 已經沒有什麼可以特別解讀的了。但是 Glow 整體的模型比較規範,我們可以逐步分解一下 Glow 的模型結構,為我們自己搭建類似的模型提供參考。這部分內容源自我對 Glow 原始碼的閱讀,主要以示意圖的方式給出。
模型總圖
整體來看,Glow 模型並不複雜,就是在輸入加入一定量的噪聲,然後輸入到一個 encoder 中,最終用“輸出的平均平方和”作為損失函式(可以將模型中產生的對數雅可比行列式視為正則項),註意,loss 不是“平方平均誤差(MSE)”,而僅僅是輸出的平方和,也就是不用減去輸入。
▲ Glow模型總圖
encoder
下麵對總圖中的 encoder 進行分解,大概流程為:
▲ encoder流程圖
encoder 由 L 個模組組成,這些模組在原始碼中被命名為 revnet,每個模組的作用是對輸入進行運算,然後將輸出對半分為兩份,一部分傳入下一個模組,一部分直接輸出,這就是前面說的多尺度結構。Glow 原始碼中預設 L=3,但對於 256×256 的人臉生成則用到 L=6。
revnet
現在來進一步拆解 encoder,其中 revnet 部分為:
▲ revnet結構圖
其實它就是前面所說的單步 flow 運算,在輸入之前進行尺度變換,然後打亂軸,並且進行分割,接著輸入到耦合層中。如此訓練 K 次,這裡的 K 稱為“深度”,Glow 中預設是 32。其中 actnorm 和仿射耦合層會帶來非 1 的雅可比行列式,也就是會改動 loss,在圖上也已註明。
split2d
Glow 中的定義的 split2d 不是簡單的分割,而是混合了對分割後的變換運算,也就是前面所提到的多尺度輸出的先驗分佈選擇。
▲ glow中的split2d並不是簡單的分割
對比 (5) 和 (9),我們可以發現條件先驗分佈與 Actnorm 的區別僅僅是縮放平移量的來源,Actnorm 的縮放平移引數是直接最佳化而來,而先驗分佈這裡的縮放平移量是由另一部分透過某個模型計算而來,事實上我們可以認為這種一種條件式 Actnorm(Cond Actnorm)。
f
最後是 Glow 中的耦合層的模型(放射耦合層的 s,t),原始碼中直接命名為 f,它用了三層 relu 摺積:
▲ glow中耦合層的變換模型
其中最後一層使用零初始化,這樣就使得初始狀態下輸入輸出一樣,即初始狀態為一個恆等變換,這有利於訓練深層網路。
復現
可以看到 RealNVP 其實已經做好了大部分工作,而 Glow 在 RealNVP 的基礎上進行去蕪存菁,並加入了自己的一些小修改(1×1 可逆摺積)和規範。但不管怎麼樣,這是一個值得研究的模型。
Keras版本
官方開源的 Glow 是 TensorFlow 版的。這麼有意思的模型,怎麼能少得了 Keras 版呢,先奉上筆者實現的 Keras 版:
https://github.com/bojone/flow/blob/master/glow.py
已經 pull request 到 Keras 官方的 examples,希望過幾天能在 Keras 的 github 上看到它。
由於某些函式的限制,目前只支援 TensorFlow 後端,我的測試環境包括:Keras 2.1.5 + tensorflow 1.2 和 Keras 2.2.0 + tensorflow 1.8,均在 Python 2.7 下測試。
效果測試
剛開始讀到 Glow 時,我感到很興奮,彷彿像發現了新大陸一樣。經過一番學習後,我發現……Glow 確實是一塊新大陸,然而卻非我等平民能輕鬆登上的。
讓我們來看 Glow 的 github 上的兩個 issue:
How many epochs will be take when training celeba? [3]
The samples we show in the paper are after about 4000 training epochs…
Anyone reproduced the celeba-HQ results in the paper? [4]
Yes we trained with 40 GPU’s for about a week, but samples did start to look good after a couple of days…
我們看到 256×256 的高畫質人臉影象生成,需要訓練 4000 個 epoch,用 40 個 GPU 訓練了一週,簡單理解就是用 1 個 GPU 訓練一年…(卒)
好吧,我還是放棄這可望而不可及的任務吧,我們還是簡簡單單玩個 64×64,不,還是 32×32 的人臉生成,做個 demo 出來就是了。
▲ 用glow模型生成的32×32人臉,150個epoch
▲ 用glow模型生成的cifar10,700個epoch
感覺還可以吧,我用的是 L=3,K=6,每個 epoch 要 70s 左右(GTX1070)。跑了 150 個 epoch,這裡的 epoch 跟通常概念的 epoch 不一樣,我這裡的一個 epoch 就是隨機抽取的 3.2 萬個樣本,如果每次跑完完整的 epoch,那麼用時更久。同樣的模型,順手也跑了一下 cifar10,跑了 700 個 epoch,不過效果不大好。就是遠看似乎還可以,近看啥都不是的那種。
當然,其實 cifar10 雖然不大(32×32),但事實上生成 cifar10 可比生成人臉難多了(不管是哪種生成模型),我們就跳過吧。話說 64×64 的人臉,我也作死地嘗試了一下,這時候用了 L=3,K=10,跑了 200 個 epoch(這時候每個 epoch 要 6 分鐘了)。結果..……
▲ 用glow模型生成的64×64人臉,230個epoch
人臉是人臉了,不過看上去更像妖魔臉。看來網路深度和 epoch 數都還不夠,我也跑不下去了。
艱難結束
好了,對 RealNVP 和 Glow 的介紹終於可以結束了。本著對 Glow 的興趣,利用前後兩篇文章把三個 flow 模型都捋了一遍,希望對讀者有幫助。
總體來看,諸如 Glow 的 flow 模型整體確實很優美,但運算量還是偏大了,訓練時間過長,不像一般的 GAN 那麼友好。個人認為 flow 模型要在當前以 GAN 為主的生成模型領域中站穩腳步,還有比較長的路子要走,可謂任重而道遠呀。
參考文獻
[1]. Dinh, L., Sohl-Dickstein, J., and Bengio, S. (2016). Density estimation using Real NVP. arXiv preprint arXiv:1605.08803.
[2]. https://kexue.fm/archives/2208
[3]. https://github.com/openai/glow/issues/14#issuecomment-406650950
[4]. https://github.com/openai/glow/issues/37#issuecomment-410019221
點選以下標題檢視作者其他文章:
#投 稿 通 道#
讓你的論文被更多人看到
如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢? 答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學習心得或技術乾貨。我們的目的只有一個,讓知識真正流動起來。
? 來稿標準:
• 稿件確系個人原創作品,來稿需註明作者個人資訊(姓名+學校/工作單位+學歷/職位+研究方向)
• 如果文章並非首發,請在投稿時提醒並附上所有已釋出連結
• PaperWeekly 預設每篇文章都是首發,均會新增“原創”標誌
? 投稿郵箱:
• 投稿郵箱:hr@paperweekly.site
• 所有文章配圖,請單獨在附件中傳送
• 請留下即時聯絡方式(微信或手機),以便我們在編輯釋出時和作者溝通
?
現在,在「知乎」也能找到我們了
進入知乎首頁搜尋「PaperWeekly」
點選「關註」訂閱我們的專欄吧
關於PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智慧前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號後臺點選「交流群」,小助手將把你帶入 PaperWeekly 的交流群裡。
▽ 點選 | 閱讀原文 | 檢視作者部落格