天天看點

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

本文來自AI新媒體量子位(QbitAI)

大家好,我是思聰 · 格裡爾斯,我将向您展示如何從世界上某些競争最激烈的比賽中拿到金牌。我将面臨一個月的比賽挑戰,在這些比賽中缺乏正确的求生技巧,你甚至拿不到銅牌。這次,我來到了亞馬遜熱帶雨林。

當我和我的隊友們進入這片雨林的時候,這場長達三個月的比賽已經進行了兩個月,想要彎道超車,後來居上,那可不是件容易的事。我們最後在比賽結束的時候,獲得了Public Leaderboard第一, Private Leaderboard第六的成績,斬獲一塊金牌。這個過程中,我們設計并使用了一套簡潔有效的流程,還探索出了一些略顯奇怪的技巧。

使用這套流程,我們從Public Leaderboard一百多名起步,一路殺進金牌區,一直到比賽結束前,占據Public Leaderboard榜首數天,都沒有遇到明顯的阻力。

在這篇文章裡,我不僅會介紹這個流程本身,還會把我們産生這套流程的思路也分享出來,讓大家看完之後,下次面對一個新問題,也知道該如何下手。

在文章的結尾,我還會講一講我們比賽最後一夜的瘋狂與刺激,結果公布時的懵逼,冷靜之後的分析,以及最後屈服于偉大的随機性的故事。

目錄

初探雨林:概述(Overview)與資料(Data)

痕迹與工具:讨論區(Discussion)和Kernel區

探險開始:解決方案的規劃和選擇

學習,奮鬥,結果與偉大的随機性

隊伍成員介紹

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

探險的第一步是要弄清楚問題的定義和資料的形式,這部分看起來會比較繁瑣,但是如果想要走得遠,避免落入陷阱,這一步還是比較值得花功夫的,是以請大家耐心地看一下。如果是已經參加過這個比賽的讀者,可以直接跳過這個部分。

我們先看一下這個比賽的标題:

Planet: Understanding the Amazon from Space

Use satellite data to track the human footprint in the Amazon rainforest

翻譯一下就是:

Planet(舉辦比賽的組織名):從太空中了解亞馬遜

使用衛星資料來跟蹤人類在亞馬遜雨林中的足迹

看來這是一個關于亞馬遜雨林的衛星圖像比賽,為了進一步了解問題,我們需要閱讀的是比賽的Overview和Data兩個部分。

Overview的Description(描述)部分告訴了我們主辦方的意圖,原來是為了從衛星圖檔監控亞馬遜雨林的各種變化,以便當地政府群組織可以更好保護亞馬遜雨林。看我發現了什麼,這個Overview的尾部附帶有一個官方提供的ipython notebook代碼的連結。

Overview位址:

https://www.kaggle.com/c/planet-understanding-the-amazon-from-space

ipython notebook代碼位址:

https://www.kaggle.com/robinkraft/getting-started-with-the-data-now-with-docs

這個ipython notebook有不少資訊量,包含對資料的讀取,探索,相關性分析,可以大緻讓我們對資料有一個基本的感覺,并且可以下載下傳下來進一步分析,可以省上不少功夫。

如果官方沒有提供這樣一個notebook, Kernel區一般也會有人發出自己的一些分析,實在沒有最好也自己做一下這個步驟,因為這個可以為後面的一些決策提供資訊。

然後我們可以先跳過Overview的其他部分,去看一下Data部分。Data部分一般提供資料的下載下傳和說明,先把資料點着下載下傳,然後仔細閱讀說明。

Data位址:

https://www.kaggle.com/c/planet-understanding-the-amazon-from-space/data

其中訓練集大概有四萬張圖像,測試集大概有六萬張圖像。資料說明包括了資料的構成和标簽的來源。我們可以先看一下這張圖:

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

這次比賽中的每個圖像樣本都是256*256像素,并且每個像素寬約對應地面的寬度大約是3.7m。每個樣本都有jpg和tif兩種格式,tif好像是比正常的RGB通道多了一個紅外線通道,嗯,可能會有用。

資料的标簽有17個類,其中4個天氣類,7個常見普通類,以及6個少見普通類。

天氣類包括:Clear,Partly Cloudy,Cloudy,Haze。其中隻要有 Cloudy的就不會有其他類别(因為被雲覆寫住了什麼都看不到)。

常見普通類包括:Primary Rain Forest,Water (Rivers & Lakes),Habitation,Agriculture,Road,Cultivation,Bare Ground。

少見普通類包括:Slash and Burn,Selective Logging,Blooming,Conventional Mining,”Artisinal” Mining,Blow Down。

普通類描述的是叢林中出現的各種景觀,包括河流、道路、耕種用地、采礦基地等等。

下面是一些樣本的示例圖,圖中用紅色字型打上了類别資訊:

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

官方還附帶了這些類别的說明和相關新聞報道,其中類别的說明最好讀一下,有助于對任務的了解。

我們在最開始對每個類的含義和特性進行了分析,然而最後探索出來的方案并沒有對不同類别進行針對性的處理。雖說如此,下次遇到一個新問題我們仍然會嘗試進行分析。

理論上每幅圖都擁有一個天氣類外加若幹個普通類,是以這是一個Multi-Label (多标簽)的問題。其中少見普通類比較少,大概四萬個樣本中有的類甚至不到一百個。

在最後的Submission中,我們要送出一個包含大概六萬個樣本的标簽的csv檔案,其中大約四萬個用于Public Leaderboard的分數計算,兩萬個用于Private Leaderboard的分數計算。

官方還提到資料是衆包平台上标注的,是以會包含一些錯誤的标簽,因為其中一些圖像他們組織裡的專家都分不清楚,更不要說衆包标注的勞工了,是以我們要意識到這是一個富含噪聲的資料集。

最後的比賽結果也證明了這一點,因為前63名的分數都在93.0%到93.3%之間,甚至都突破不了94%。這裡的分數是指什麼呢?請看下一小節。

弄清了問題的形式,接下來我們可以傳回閱讀Overview的剩下部分。Evaluation告訴我們這次的評價名額是各個樣本F2-score的均值,F2-score的定義如下:

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

其中p是精度(precision),表示我們預測出來的類出現在标簽中的比例;r是召回率(recall),表示标簽中出現的類被我們預測出來的比例。F2-score相對偏好召回率,是以在比較不确定的時候,預測多一點可能會比預測準一點來得好。

這次比賽的獎金第一名有3萬美刀,第二名2萬美刀,第三名1萬美刀。雖然沒有類似Zillow那個一百多萬美刀那麼驚人,但也是一筆不少的外快了。

比賽開始于4月20日,7月13号則是參加截止日期以及合隊截止日期。一般來說,即便你是和幾個小夥伴一起參賽,也不要急着太早合隊,因為每個隊伍每天隻有固定的送出次數可用,不合隊的話所有人加起來可以獲得數倍的送出機會,這對于初期的方案探索是非常有益的。

另外,7月13日也同時是預訓練模型聲明截止的時間,因為圖像類比賽經常會使用ImageNet上預訓練過的模型。

為了公平起見,所有人都隻能使用讨論區一個置頂帖中聲明過的預訓練模型,如果選手所使用的預訓練模型沒在裡面,那就要在截止時間前自覺去文章裡添加聲明,否則視為作弊。

比賽最後于UTC時間7月20号晚上11點59分結束,對于身在國内的我們來說,這意味着最後一天要通宵陪歐洲人民沖刺到早上八點。

一個老練的探險隊員要善于利用前人留下的資訊。我們隊裡常說,一個能善于使用讨論區、工程能力不差并且有時間精力的人,應該有很大可能性拿到一個銀牌。

讨論區裡包含着官方的一些申明通知,還有其他隊伍的一些經驗分享,Kernel區包含了一些公開釋出的代碼。這些都是所有參賽隊伍共享的資訊,對于一個新手和後進場的隊伍,從這裡面擷取足夠資訊可以取得比較好的開端。

此外,常被忽略的一個點是,其他一些已經結束的類似比賽中,也包含了大量對這個比賽有用的資訊。

比如,這個比賽是衛星圖像的多标簽分類比賽,那麼其他衛星圖像比賽or圖像or多标簽分類比賽的資訊都會對這個比賽有用,這些比賽的讨論區經常包含了大量優秀的解決方案,這對我們後面設計方案會有幫助。

最後要小心的是,讨論區裡面的發言也不一定對,Kernel區的代碼可能也有些bug,比如這次比賽有一些隊伍因為使用了一個有bug的submission生成代碼,最後都掉了八九百名,場面十分血腥。

我們從參賽的時候從讨論區擷取的一些有用資訊如下:

1. tif圖像資料在RGB通道之外包含紅外通道,按理來說多使用上這個資訊應該會提高效果,然而恰恰相反,讨論區的人說用了之後反而變差了,這可能是因為有些圖檔的紅外通道跟RGB通道是錯開的。

是以到比賽結束我們也隻是稍微嘗試了一下去利用tif的紅外通道,并沒有在上面浪費太多時間。

2. 其他隊伍有可能使用了哪些預訓練模型,每個模型的大體性能如何,這給我們提供了很有用的參考,比如我們在嘗試了一些比較小規模的模型(如ResNet18、ResNet34)之後,以為這些模型已經夠了,再大再複雜的模型可能會過拟合。

但是從讨論區我們看到,大模型還是有明顯優勢的,這就促使我們敢于花大量時間去跑那些笨重的ResNet152、DenseNet161等預訓練模型。

3. 從其他類似比賽的讨論區我們看到,高分隊伍一般不會使用特别複雜的Ensemble方法,甚至會僅僅使用簡單的bagging和stacking(下面會講),是以我們就把更多的精力花在單模型的調優。

事實證明,即便到了比賽後期,還不斷有一些更好的單模型新鮮出爐,使我們Ensemble後的效果猛地一竄,竄到了Public Learderboard前三乃至第一。

以上準備可能會花上你一到兩天的時間,但磨刀不誤砍柴工,我們也差不多可以開始我們的征程了。

上面提到,這次比賽問題是Multi-Label(多标簽)分類問題,評價名額是F2 Score,但F2 Score并不是可以直接優化的值,是以我們采取的方法是:

1. 每個輸出接Sigmoid層,分别預測每個類的機率,使用Binary Cross Entropy Loss優化。這其實是多标簽分類問題的常見套路,本質是獨立地對每個類做二分類學習。雖說不同類之間可能存在互相依賴,但我們假設這些依賴可以通過共享底層參數來間接實作。

2. 在訓練上述二分類任務時,由于正負樣本數目不均衡,我們并不能直接拿p = 0.5作為二分類的門檻值進行預測,而需要為每個類搜尋一個合适的門檻值,使得整體的F2-Score最大。具體來說,我們采取了讨論區放出的一個方案,貪婪地對每個類的門檻值進行暴力搜尋,邏輯如下:

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

這肯定不是最優的方案,但卻已經足夠好。雖然後期我們優化了讨論區的代碼使用GPU加速計算,并嘗試了諸如随機初始值、随機優化順序然後多次随機取最好,步長大小調優,進化計算搜尋等方法,但都因為送出次數限制沒來得及測試。

不過要注意的是,雖然我們以BCE Loss為訓練目标,但實際上BCE Loss變低,F2-Score卻未必變高,可以想象一下,如果模型把一些本來就能被分對的樣本的預測機率變得更搞,BCE Loss是會降低,但F2-Score還是一樣。

為什麼要強調這點呢?因為有隊友在探索Ensemble方法的時候,看着BCE Loss不好就放棄了;再往後另一個隊友重新實作了一樣的Ensemble方法,看的是F2-Score,卻發現效果拔群!

是以說,如果隻看Loss,不看最終評價名額,很容易做出誤判,錯過有用的方案,這對于其他問題來說也是成立的。

另外讨論區也有人提到一種直接對F2 Score進行優化的方法,我們因為時間有限還沒來得及進行嘗試。

一開始将官方的訓練資料(Train Data)劃分訓練集(Train Set)和驗證集(Validation Set),或者均勻劃分成K個部分,用于做K折交叉驗證(K-Fold Cross Validation)。關鍵的是,随機劃分結果要隊伍内和方案間共享。

不然的話,這個模型訓練用的K折劃分和那個模型訓練用的K折劃分不同,還怎麼嚴格比較它們之間的優劣呢?而且這也是為後面資料分析和模型的Ensemble(內建)做準備。

這一次我們将資料平均劃分了五折(編号0-4),使用一折作為驗證集,使用其他四個折作為訓練集,可以有五種組合。然後探索初期方案期間,隻使用其中一種組合,例如将第0折作為驗證集,1-4折作為訓練集。

在模型确定後,如果想用上全部資料作為訓練,我們可以使用五種組合,每種組合用四折訓練一個模型(對應下圖中4個灰色大塊),在剩下的一個折作為驗證集預測(對應下圖中5個藍色小塊),周遊五種組合後我們可以獲得每一個折的驗證集預測結果(還是對應下圖中5個藍色小塊)。

因為這些驗證結果都是從沒有在它們上面訓練過的模型預測出來的,我們把這個五個驗證折拼在一起的結果稱為out-of-fold,包含整個訓練集的驗證結果(對應下圖中的藍色長塊)。

在out-of-fold上面進行門檻值調優得到的F2-Score可以較好的代表模型的能力,可以真實地反映模型的泛化性能,多個模型的out-of-fold拼接在一起也可以作為第二階段的內建學習的輸入。

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

在上面的5種組合上做了5次訓練,測試的時候我們就有了5個模型,每個模型預測一遍測試集就得到了5個機率矩陣,每個機率矩陣的形狀都是(測試集樣本數x17)。

我們可以将5個機率矩陣直接求平均後做二分類預測,也可以分别做完二分類預測,再做投票,來獲得最終的多類預測結果。這個結果實際上用到了所有5個折的訓練資料,會更加準确,也更加穩定。

當然如果隻是想用上所有資料的話,更簡單的辦法就是直接把整個訓練集用這個模型跑一遍,再把訓練好的模型模型對測試集作預測。

不過我們沒有采用這第二種方式,一來,所有訓練樣本都被這模型“看光了”,沒有額外的驗證集,難以評估其泛化性能;二來,我們認為第一種方法中,5個模型的預測結果做了個簡單的Ensemble,會更穩定一點。

折數劃得越多,訓練驗證所需要的計算力和時間也就越多,最好根據問題和自身計算力做一個權衡。

通過調查我們可以發現,Kaggle圖像比賽現在基本被深度學習方法所統治。雖然在一些細節上傳統方法還有發揮空間,但還是以CNN(卷積神經網絡)為主體。

雖然之前我主用TensorFlow,不過PyTorch提供的Model Zoo使用起來很友善,代碼比較輕量級,隊内會用的人數也比較多,是以這次比賽我們最終采用了PyTorch作為主體架構——除了隊内某個異端,他用TensorFlow為自己寫了一個高效的DataLoader。

PyTorch的Model Zoo提供了AlexNet,VGG, Inceptionv3, SqueezeNet, ResNet, DenseNet等架構的預訓練模型參數。我們還嫌這些模型不夠用,就嘗試了從TensorFlow上遷移過來的的Inception v4和Inception Res v2。

可惜的是,大概由于這兩個模型走的不是“正規管道”,是“偷渡”過來的,大概哪裡出了偏差,總之訓練結果一塌塗地,果斷放棄。在這裡我們呼籲大家支援正版。

PyTorch文檔提供了不同模型在ImageNet上Top-1、Top-5的錯誤率,可以大概看出這些模型的能力,雖然這不一定和它們在比賽中的表現性能正相關。

在我們這次比賽中,ResNet表現最好,DenseNet緊随其後,這不是偶然的。它們有一個引人注目的共同點,就是從底層到高層有Skip-Connection,其中ResNet采用的是兩路疊加,Densenet是多路拼接。

為什麼重要呢?我們認為,第一是因為Skip-Connection可以自适應調節模型複雜度,避免過拟合,第二是因為17個類所利用的圖像特征層次不同,比如Cloudy更偏向底層紋理特征,Water和Road更偏向高層語義,而Skip-Connection有助于讓底層特征到很高層仍然保留,而不會淹沒在幾十層網絡的變換中。

稍弱一點是VGG和Inception v3,最弱的是SqueezeNet和AlexNet。從TensorFlow上遷移過來Inception v4和Inception Res v2基本上不收斂,再次呼籲大家支援正版。

我們一開始在ResNet-18這個輕量級的模型分别嘗試預訓練參數(Pretrained)和随機初始化參數(From Scratch)進行訓練,結果發現,随機初始化的模型的收斂速度比預訓練的模型要慢上十倍左右,最終收斂結果也差上一截。

有隊友還試着自己設計一些網絡架構,但結果也遠遠比不上預訓練模型。

是以做完這波實驗後,我們也大概确定這次的比賽,跑Model Zoo将是主要的手段。聽起來并不像自己設計網絡結構那麼激動人心,但我們也可以在上面做些一些魔改,魔改之後也取得了意料之外的提升,具體見下一節。

使用預訓練模型的時候要注意,PyTorch文檔中說明了這些模型都是在224x224的圖像上進行預訓練,而且要求圖檔要經過歸一化并減掉某個均值、除以某個方差,然後才輸入模型。如果想要模型能最大程度的利用預訓練的資訊,一定要對我們輸入圖檔也做同樣的操作。

不過雖然模型的輸入要求是224x224,但一部分模型(比如ResNet,DenseNet)的卷積層結束時會接一個Global Average Pooling,将每個通道的Feature Map求平均,這樣不管輸入的圖檔尺寸多大,經過Global Average Pooling之後Feature Map的尺寸都會變成1x1,是以理論上是可以直接使用的。

然而這裡有一個坑點是,PyTorch預訓練模型卷積層最後其實使用是一個7x7固定大小的Average Pooling,并不是真正的Global AveragePooling。

要想輸入其他尺寸大小的圖,我們應該把該層替換成AdaptiveAvgPool,并将輸出設定大小為1,這樣就能保證無論上一層Feature Map尺寸是多少,出來的尺寸都會是1x1。

當然,有可能一下子縮到1x1太小了,損失了太多資訊,是以我們也可以把AdaptiveAvgPool輸出大小設定為2或3,使得輸出尺寸變成2x2和3x3,這樣的好處是保留下更多的資訊。為了與之比對,還需要改動後面整個全連接配接層的尺寸。我們後來在Densenet和ResNet上嘗試這個改動,取得了不錯的效果。

以上是針對ResNet和DenseNet來說的,而像VGG這種模型,最後Feature Map是直接Flatten(拉直)然後接全連接配接,我們如果要利用到後面的預訓練全連接配接層資訊,我們就隻能将輸入圖檔縮放成224x224了。

除了更改Pooling輸出尺寸,另一個嘗試成功的魔改是關于全連接配接層的。ImageNet模型最後一層是1000類,而我們需要的是17類輸出,以往常見的做法是把最後一層全連接配接層換掉,換成一個output size為17的新全連接配接層,然後重新初始化它的參數。

然而,我們的兩個隊友卻因為偷懶發現了效果更好的做法,就是直接在預訓練模型的1000維輸出後面,直接就接上一個1000x17的全連接配接層。我們猜測,它效果好的原因是額外地保留下了全連接配接層的預訓練資訊。

另外有些隊友擔心,這個比賽的大多數圖檔都可能被預訓練模型識别為草地之類的ImageNet類别,是以可能基本上都隻激活1000維中的少數幾個,會很稀疏,這樣其實應該是對訓練不利的。

針對這種疑慮,我們将很多比賽圖檔輸入預訓練模型後,發現它們在1000類上預測的機率值并不稀疏,是以應該沒太大問題。不過,這種新做法也可能隻對這次比賽任務有效,在其他任務上還是建議先試着把最後一層全連接配接換掉或是整個随機初始化,因為一般來說最後一層的可遷移性更差一點。

至于具體要怎麼在全連接配接層中加Batchnorm、Dropout就看個人選擇了,我們在發現這個任務上沒有太顯著影響,後面大部分模型都沒有加。

我們在探索Data部分的時候可以得知,四個天氣類會且隻會出現一個,這很容易讓我們想到将這四個類單獨拿去來接一個Softmax層而不是Sigmoid層,使四個類機率和為1,預測的時候隻預測最大機率的天氣類。

但這樣做實際效果并不好,因為我們上面提到過這次比賽的評價名額是F2-Score,更希望有比較高的召回率而不是準确率,如果最高兩個天氣類非常接近,那把它們一起預測為正,雖然有一個肯定會猜錯,但卻可能可以取得更高的F2-Score,總體上反而是劃算的。

關于模型的訓練,我們使用的是Adam作為優化器,因為它對學習率有一定程度的自适應微調,收斂速度快,而且對一些小類的更新也比較友好。

我們嘗試了1e-2, 1e-3, 1e-4, 1e-5, 幾個範圍後,大緻确定了1e-4是一個比較好的初始學習率,後面我們對不同的模型調整初始學習率都是對這個值乘以2、4倍或除以2、4倍,主要是随着Batch Size等比例變化。

我們的Batch Size大概是在32到128之間,取決于GPU是否能裝得下多大。有時候我們也會将一些調低Batch Size到32做一下實驗。

圖像比賽的一個重頭戲就是資料增強,我們為什麼要做資料增強呢?

我們的訓練模型是為了拟合原樣本的分布,但如果訓練集的樣本數和多樣性不能很好地代表實際分布,那就容易發生過拟合訓練集的現象。資料增強使用人類先驗,盡量在原樣本分布中增加新的樣本點,是緩解過拟合的一個重要方法。

需要小心的是,資料增強的樣本點最好不要将原分布的變化範圍擴大,比如訓練集以及測試集的光照分布十分均勻,就不要做光照變化的資料增強,因為這樣隻會增加拟合新訓練集的難度,對測試集的泛化性能提升卻比較小。

另外,新增加的樣本點最好和原樣本點有較大不同,不能随便換掉幾個像素就說是一個新的樣本,這種變化對大部分模型來說基本是可以忽略的。

一些常見的圖像資料增強方式有:

亮度,飽和度,對比度的随機變化

随機裁剪(Random Crop)

随機縮放(Random Resize)

水準/垂直翻轉(Horizontal/Vertiacal Filp)

旋轉(Rotation)

加模糊(Blurring)

加高斯噪聲(Gaussian Noise)

對于這個衛星圖像識别的任務來說,最好的資料增強方法是什麼呢?顯然是旋轉和翻轉。具體來說,我們對這個資料集一張圖檔先進行水準翻轉得到兩種表示,再配合0度,90度,180度,270度的旋轉,可以獲得一張圖的八種表示。

以人類的先驗來看,新的圖檔與原來的圖檔是屬于同一個分布的,标簽也不應該發生任何變化,而對于一個卷積神經網絡來說,它又是8張不同的圖檔。比如下圖就是某張圖檔的八個方向,光看這些我們都沒辦法判斷哪張圖是原圖,但顯然它們擁有相同的标簽。

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

其他的資料增強方法就沒那麼好用了,我們挑幾個分析:

亮度,飽和度,對比度随機變化:在這個比賽的資料集中,官方已經對圖檔進行了比較好的預處理,亮度、飽和度、對比度的波動都比較小,是以在這些屬性上進行資料增強沒有什麼好處。

随機縮放:還記得我們在Overview和Data部分看到的資訊嗎?這些圖檔中的一個像素寬大概對應3.7米,也不應該有太大的波動,是以随機縮放不會有立竿見影的增強效果。

随機裁剪:我們觀察到有些圖檔因為邊上出現了一小片雲朵,被标注了partly cloudy,如果随機裁剪有可能把這塊雲朵裁掉,但是label卻仍然有partly cloudy,這顯然是在引入錯誤的标注樣本,有百害而無一利。同樣的例子也出現在别的類别上,說明随機裁剪的方法并不适合這個任務。

一旦做了這些操作,新的圖檔會擴大原樣本的分布,是以這些資料增強也就沒有翻轉、旋轉那麼優先。在最後的方案中,我們隻用了旋轉和翻轉。并不是說其他資料增強完全沒效果,隻是相比旋轉和翻轉,它們帶來的好處沒那麼直接。

按照一般的做法,資料增強的流程是一個Epoch一個Epoch地訓練整個訓練集,每次對輸入的樣本進行随機的資料增強,這也是本次比賽大多數隊伍的做法。

但是我們卻采取了不同的做法,顯著縮小了訓練一個模型需要的時間,提高了我們在初期的方案疊代速度。首先,我們注意以下兩個點:

1. 采用的旋轉和90度倍數的翻轉,很容易可以周遊完所有八個情況,是以樣本量剛好就是擴充八倍;反之,像光照,飽和度,對比度這些狀态連續的資料增強,很難提前預計樣本量擴充多少倍才合理,是以必須在訓練過程中不斷地随機增強。

2. 模型如果第二次、第三次見到某個已經學得很好樣本,有可能會過拟合到該樣本,使驗證集Loss反增。

是以我們預先生成了八種方向的樣本,把訓練集擴充了八倍,再随機打亂,再這些樣本都隻訓練一遍就停止,相當于隻跑了一個Epoch(當然這裡的一個Epoch的時間等于原來八個Epoch)。

這樣做之後就保證每個樣本的8種方向都隻被模型看過一遍,不給模型過拟合的機會,而且這樣在時間上也節省了許多。如果是按正常的随機增強做法,可能你要等到很久之後才能把8個方向都随機到,而在此之前又會讓模型多次見到同一樣本的同一方向,既浪費了時間,又增加了過拟合的風險。

在擴充的增強訓練集上使用Adam優化器進行訓練,我們觀察到模型在過完整個增強訓練集就收斂到一個接近最優的水準,然後繼續訓練下去驗證集就會開始收斂或反增,這也支援了我們“隻掃一遍”的想法大緻是正确的。不過,有隊友還是不滿足于隻掃一遍,于是就有了下面的改進。

在Loss收斂的時候降低學習率繼續訓練,是深度學習一種常見的Trick。像我們上面那樣隻将訓練集過一遍,會導緻一些樣本隻在前期模型還很不穩定的時候被見過,并沒有很好地被學習。是以我們也想到用降低學習率的方式,将訓練集再過一遍。

一開始我們嘗試了常見的做法,即降低10倍學習率,但發現還是會很快過拟合,是以就放棄了。直到後來我們隊裡有人試着将學習率降低50、100倍,可以讓模型在過第二遍訓練集的時候,既有第二次機會見到以前沒學好的樣本,又不會因為在已經學得很好的樣本上過度訓練而導緻過拟合,将效果又提升了一截。

後面一直到比賽結束,我們都使用了這套做法,即用初始學習率将訓練集過一遍,再降低50倍學習率訓練第二遍,總的訓練時間相當于原來的2 x 8 = 16個Epoch,相比之下,讨論區裡面我們看到其他隊伍采取傳統的資料增強方法,需要跑上二三十個Epoch。是以這套方法極大地節省了我們模型疊代和方案驗證的時間。

是以有的時候不是方法不行,隻是你還不夠用力。

上面我們提到訓練時怎麼使用資料增強,但是測試時資料增強(TTA)也可以對預測效果進行很大的提升。

具體做法也比較簡單,我們可以将一個樣本的八個方向都進行預測,獲得八個預測機率,接着可以将八個機率直接平均,也可以使用預測的類标簽投票來獲得最後結果。通過幾輪測試,我們采取的是平均的方案,因為效果更好。

我們測試集的F2門檻值是在out-of-fold的驗證預測結果上搜尋選取的。對于驗證集我們也對每個樣本預測八個方向的結果,然後把它們拼接成一個32萬樣本的驗證集。

我們觀察到,在這個集合上搜尋得到的門檻值,比把八個方向預測結果平均得到4萬樣本的驗證集上搜尋得到的門檻值有更好的泛化性能。

整體上我們觀察到的現象就是,搜尋門檻值時使用的樣本數越大,這個門檻值的泛化性能很可能也就越好,對于小樣本來說,這個門檻值很容易過拟合。想象隻有一個樣本的時候,我們很容易可以找個一組門檻值讓F2 Score為1.0。

有另一種調整門檻值的方式是使得讓在out-of-fold驗證集上預測出來各個類的個數和它們的标簽中個數一樣。我們沒有嘗試這種做法,因為我們預測出來的各個類占比和标簽中的占比本來就十分接近。

到這裡,我大緻已經介紹完我們訓練一個單模型流程,在開始介紹Ensemble(模型內建)前,我還是要介紹和強調一下結果的存儲、記錄和分析的重要性。

結果的存儲、記錄和分析是新手很容易忽略的一個環節,一開始如果沒注意好,到後面模型多起來的時候就容易手忙腳亂。

以下是我們這次比賽記錄的數值:

模型超參:預訓練模型類型,模型改動,輸入圖檔大小,資料增強類型,Batch Size,學習率,疊代次數等;

評價結果:K折交叉驗證各個折的Loss,各個折的均值、方差,整個out-of-fold的Loss和F2-score,做完TTA的F2-score,Public Leaderboard的F2-score等。

我們希望,單模型本地out-of-fold的驗證集上的F2-Score,能夠較好地反映Public Leaderboarrd的F2-Score,這樣我們無需耗費寶貴的送出機會就能對新方案的效果進行大緻評估。

事實上這兩個F2-Score确實足夠相關,如下面的散點圖所示,雖然存在一些抖動,但整體上還是呈現一種正相關的關系。不過這裡out-of-fold是由五折各種的驗證集拼接在一起,八方向預測結果平均搜尋門檻值得到的F2-Score,後期我們發現八方向預測結果拼接的搜尋得到的驗證F2-Score其實更加穩定,在Public Leaderboard的表現也更好。

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

最後,我們發現驗證和測試結果以及submission的格式的定義和檔案名的管理也要注意,這一點我們隊伍内一開始沒有統一标準,比賽後期的合并結果和賽後的統計分析也花了一番功夫。

我們這次比賽,使用了三種Ensemble, 關于Ensemble的基本套路可以參考《分分鐘帶你殺入Kaggle Top 1%》中模型內建(Ensemble)部分:

文章位址:

https://zhuanlan.zhihu.com/p/27424282

一開始模型比較少的時候,我們直接把不同模型的結果進行平均(Average Bagging)

到後面模型比較多的時候,我們開始使用Bagging Ensemble Selection。

最後我們使用了Stacking,我們這次用來做的Stacking算法除了Logistic Regression、Ridge Regression,我們還試着自己設計了一種我們自己稱之為Attention Stacking的算法。

在做Ensemble階段,對于每個樣本我們有一個(模型數 x 17)大小的機率矩陣,我們的目标是獲得一個長度17的機率向量。

對于三種Ensemble,我們對它們的模組化分别為:

Average Bagging:所有模型有相同的權重,将機率矩陣沿模型數次元進行平均。

Bagging Ensemble Selection:每個模型有不同的權重,在Selection的過程中,有的模型可能被選到多次,有的模型也可能一次也沒被選到,按照被選中次數為權重,機率矩陣沿模型次元進行權重平均。

Stacking:每個模型的每個類都有自己的權重,比如某個模型擅長對氣象類進行區分,卻對正常類性能很差,那顯然這個模型在氣象類和正常類的權重應該不一樣。我們需要對每個類别單獨學習一組函數或一組權重。

Logistic Regression、Ridge Regression是對輸入模型進行非線性的組合,為了探索其他可能性,我們也試着設計對輸入模型進行線性組合的模型。

我們稱之為Attention Stacking的模型相對比較簡單,對于每個類,我們初始化一組模型數長度的向量,對這個向量進行Softmax,我們就獲得一組求和為1權重,這樣我們對這個類别所有模型的預測機率按這組權重進行權重平均,就可以得到這個類别的預測結果。

因為這種權重求和的形式和流行的Attention機制有點像,我們就叫它Attention Stacking,雖然它可能有其他更正式的叫法,但我們還沒時間仔細查文獻,是以暫且這麼稱呼。

Stacking階段我們按照單模型階段的五折劃分進行了交叉驗證,整個流程和單模型階段有點像。不過Stacking階段,按驗證集的F2-Score進行early stopping,在驗證集上求門檻值的階段,我們有不同的兩套方案:

方案一:out-of-fold的做法,這個方案還是在out-of-fold上搜尋門檻值,要注意的一點是,每個折的模型的測試輸入要使用第一階段對應的折的預測結果,確定産生搜尋門檻值用的驗證集和測試集的輸入機率矩陣由相同的第一階段模型産生。

方案二:非out-of-fold的做法,下圖可以看做是一個将Attention Stacking每個類的權重拼在一起得到的矩陣,沿模型次元每列求和為1。因為Attention Stacking的做的其實是對每個類不同模型預測結果的一種線性組合,我們可以把五折求出來的五個權重矩陣直接平均獲得一個新的權重矩陣。

然後用這個新的權重矩陣對所有訓練資料和測試資料進行權重平均,在權重平均的訓練資料上搜尋門檻值,應用在測試資料的權重平均結果上得到類預測。這種方案也保持了搜尋門檻值所用的集合與測試集預測結果産生的方式一緻。

經過我們的測試中,方案二比方案一表現得更好。

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

到此,我們的方案也基本講解完畢。最後,我想給大家講講我們比賽中的一些經曆和對比賽結果的分析。

在上次參加Quora Question Pairs的過程中,我們在獲得一些文本類比賽實戰經驗的同時,也對Kaggle比賽的流程和基本方法有了一定的了解,并将經驗總結寫成了《分分鐘帶你殺入Kaggle Top 1%》。為了學習一些新的東西以及驗證我們對Kaggle比賽套路的了解,我們選擇了正在進行的 Planet: Understanding the Amazon from Space,這是一個圖像多标簽的分類任務,和Quora Question Pairs的文本二分類任務有很大不同。

在參加這個比賽前,我們隊裡并沒有人有太多參加圖像比賽的經驗,關注到這個比賽的時候,三個月的比賽也隻剩下一個月。

一開始的十天,因為大家還有各種的項目工作沒有完結,隻有兩三個隊友零星地探索,遊蕩在Public LB一百多名。在剩下最後二十天的時候,我們陸續完成了手頭的工作,騰出了時間和計算資源,全力地參加這個比賽。

我們花了一兩天搜集了這個比賽和其它類似比賽的資訊,從中總結出了一些基本的套路。在探索和确定出基本方案後,我們隊内各自獨立地去實作和探索,每個人都有自己一套代碼。

保持代碼和結果的獨立,主要是為了合隊的時候能夠有更多樣性的結果,這往往能給Ensemble結果帶來較大的提升。我們發現,即使在這樣一套不算複雜的解決方案中,大家對各種細節的了解也有很多不同,這些不同讓我們每次合隊時都有不小的提升。

其實一開始我們隻是想試試能不能拿塊金牌,但我們很快發現,情況似乎有點失控。我們的方案似乎顯得格外有效,不到一周,我們就進入了金牌區,接着有的隊友隻是對三四個模型Bagging了一下,就直接進入Public LB的前三,最誇張的時候,Public LB前五名中有三名是我們的隊的。

在最後一周前合隊完畢,我們竄到了第一名,從0.93396升到0.93421,這個Public LB分數到一周後比賽結束時,也隻有五支隊伍能夠超過。

情況似乎非常順利,我和隊友們都感覺自己優勢很大,早已經不滿足于金牌,還想要留在前三,甚至幻想最終奪冠。懷着這樣的心态,我們來到了最後一夜,準備通宵戰鬥到早上八點結束。

在前進的過程中,作為一隻新晉隊伍我們也關注着其他老牌隊伍,其中有些在最後一天來了個大爆發,給了我們很大壓力,下面就會說到。在此之前,先介紹其中幾個隊伍:

Kyle和我們隊伍裡的一名隊員的ID重名,是上一次衛星圖像比賽的冠軍,在我們加入比賽的時候,他已經在第一名的位置盤踞許久。不過感覺他可能計算力資源不是很豐富,最後一周有點乏力,最後Private LB剛好留在金牌區内。

http://deepsense.io是上一次衛星圖像比賽的第四名,好像是一個做圖像的公司。

ZFTurbo是圖像類比賽活躍的GrandMaster(Kaggle頭銜),上一次衛星圖像比賽的亞軍,後面還與當時排名第三的Stanislav Semenov進行了組隊,這支隊伍十分強大。

他們的隊名也很會玩,一開始懶得起名,直接叫做Team Name,他們在最後一天猛地提升到達了Public LB頂端之後,就改名為Russian Bears,一個帶着強烈戰鬥民族色彩的隊名,這讓我們嚴肅地考慮要不要改名為Chinese Panda / Chinse Dragon / Make China Great Again之類的,嗯,不過最後并沒有改。

他們最後是Private LB第三名,留在了獎金池内。ZFTurbo賽後釋出的一個拼圖Trick也十分有趣,方法是找到一副圖像切片周圍鄰接的切片,然後利用周圍切片作為上下文,一起對中央圖像進行預測。這個trick貌似是ZFTurbo在以往就慣用的套路了,看來他很熱衷于拼圖。

team-amazon-forest這支隊伍在評論區從頭到尾都十分活躍,尤其是Heng CherKeng,在讨論區給大家提供了很多探索結果和技術細節。

我們早期也從中獲得不少啟發,非常感謝他的分享,賽後讨論區也出現了對他的感謝帖。不過可能因為他分享了太多,後期被後面很多新晉隊伍超過了,最後掉出金牌區。

Urucu隊裡有Kaggle積分全站排名第一的Gilberto Titericz Junior,他們在比賽結束前十幾分鐘沖到了Public LB第三的位置,但卻在Private LB中掉出了金牌區,十分可惜。

Clear Sky隊伍裡可能有一到兩個華人,實力也十分強勁,也是我們關注的對象。

bestfitting是一位名字開挂的選手,best fitting(最好的拟合),最後從Public LB的第九直接上升到了Private LB的第一,确實是best fitting。他的賽後方案總結也包含了很多值得學習的地方。

我們因為參賽比較晚,經驗相對不足,一直到最後一天都還有很多Ensemble方案沒有來得及驗證。再加上機房在最後三天因為暴雨短路停了一天,我們到比賽結束前幾個小時才基本跑完了想要跑的大部分單模型。

在等待單模型新鮮出爐的同時,隊裡幾乎所有人都在通宵地驗證分析各種Ensemble方案。

在最後一天大家都隻剩下5次Submission的機會,使用都十分謹慎,不像之前那麼随意。一整個白天我們都線上下實作和驗證Ensemble方案,壓着不送出。

我們還寫了一個腳本,時刻監控着Public LB的變化和前十幾名的Submission剩餘次數,看到排名靠前的很多隊伍也非常沉得住氣,前面12個小時基本都沒有送出,可以說變數非常地大。不過由于我們的分數與第二名的差距足足等于第二名到第九名的差距,是以我們也不怎麼着急。

然而,Russian Bears僅僅第一次送出就打破了我們的平靜,他們一舉從0.93320升到了0.93348,看上去跟我們的分數0.93348是一樣的,但是在後面沒顯示出來的小數位上赢了,占據第一,給了我們很大壓力。我們心想,第一次送出就這麼誇張,後面那還得了?不過他們後面剩下的四次嘗試再也沒有提升,讓人暫時松了一口氣。

很快我們也嘗試送出了兩次,分别是不同的Ensemble方案,然而都沒能打破記錄,當時非常的緊張。經過讨論,我們決定暫時先不冒險,而是想辦法復原到前一天的代碼,在那份代碼上我們取得了目前的最佳分數0.93348。

但是由于之前太過大意,管理這份實作的沒有記錄下來究竟是哪一次git commit上跑出了最佳效果,因為覺得後面肯定會跑出更好的結果,卻沒想到現在要靠這份Ensemble代碼來救場。

中間花了幾個小時,根據git log上面的送出時間、單模型檔案的修改時間、微信聊天記錄之間的比對,該隊員終于戲劇性地恢複了之前的代碼。

之前這份ensemble方案僅僅使用了57個單模型,加入新的單模型之後,不出意外地提升了,達到0.93449,重回Public LB第一。我們最後是用了64個模型進行Ensemble,一個程式員看起來十分舒服的數。

後面我們又在這份救場代碼上嘗試了兩種改進,但是都沒有再提升了。最後一份Submission檔案生成完後,距離比賽結束還剩一個小時,我們非常惡趣味地等着看Russian Bears隊伍的最後兩次送出,然而他們送出了一次之後就不動了。

一直等到最後半個小時我們實在等不下去,把最後一份Submission交了,結果才過了一分鐘他們也交了最後一個的Submission,似乎也是在惡趣味地等着我們。

Urucu也在最後十幾分鐘的時候送出了一個0.93444,到達第三,成功加入Public LB 0.93440+ 俱樂部。

早上8點一過,我們重新整理出Private LB的排名是第六,當時就懵逼了。雖然我們早就知道會存在抖動,選擇的Submission也是在驗證集和Public LB上表現都比較好的,但抖動還是比我們預計的要大得多。

最後幾天的送出基本在0.93430到0.93450之間,我們預估抖動可能會比0.0002大一點,因為Private LB隻有兩萬樣本,但抖動在我們的Submission中的是0.001左右,大概我們預估的5倍左右。

事實上,從BreakfastPirate的一個分析貼看,這次比賽Top 10%的隊伍的排名抖動程度(即Public LB和Private LB的差異)在整個Kaggle的曆史上也可以排上前十,非常誇張。

我們試着對這個結果進行了分析,下面是賽後對我們Submission進行分析畫的散點圖。

Kaggle獲獎者自述比賽求生指南:我們如何“穿越”亞馬遜熱帶雨林1. 初探雨林:概述(Overview)與資料(Data)2. 痕迹與工具:讨論區(Discussion)和Kernel區3. 探險開始:解決方案的規劃和選擇4. 學習,奮鬥,結果與偉大的随機性5. 隊伍成員介紹結語

說明如下:

橫軸是Public LB Score, 縱軸是Private LB Score。

橘色的點代表單模型送出,藍色、紅色、黃綠色的點代表多模型Ensemble的送出,紅色的點是我們最後選中的兩個Submission,Kaggle會根據每個參賽隊伍選中的兩個Submission中Private LB分數最高的,來計算最終排名。黃綠色的點是比賽中因為送出次數限制沒有送出、賽後才送出的Submission。

藍色斜線是對線性拟合曲線。

銅色橫線以上是銅牌區,銀色橫線以上是銀牌區,金色橫線以上是金牌區,綠色橫線以上是獎金池。

可以看到,我們最後一周送出的Ensemble模型都在金牌區以内,甚至有3個單模型也進入其中,分别是ResNet50、ResNet101和ResNet152。我們最後一段時間有很多好的單模型沒有送出,它們中應該也有可以進入金牌區的。

我們賽中的送出有6個進入獎金池,其中最高一個的F2-Score為0.93322,比Private LB第一名bestfitting最後的Submission 0.93318還高一點,當然我們相信其他隊伍也應該和我們一樣,有一些更好的Submission但是沒有被選中。賽後送出的4個Submission中也有2個進入獎金池。

上圖可以看出Public LB到Private LB的抖動大概在0.001左右。

從Private LB第一的bestfitting的賽後方案總結看出,他對比賽的Public LB到Private LB可能的抖動(Shake up)使用模拟進行了估計,得出這個F2-Score的抖動大概在0.001-0.0025,而Public LB前面的隊伍的差别隻有0.0005-0.001,是以最後的排名出現較大抖動也十分正常。從最後的結果看來他的估計也是挺準的。

造成這種抖動的原因應該是來着資料集中一些難以明确分類的樣本,也就是Data部分提到的即使是官方組織内部的專家也難以區分的樣本,比如河流和道路有時候完全分不清楚。這類樣本的标注基本是随機的,讓同一個人重新标注都可能标得不同。

冠軍選手bestfitting的這種模拟抖動分析十分值得我們學習,因為這一方面可以避免自己過分關注微小的提升,另一方面,如果已經知道随機抖動程度甚至都超過了前幾名之間的細微差距,那我們最終選兩個Submission時就不應該去理會Public LB最好的那個,而是先選一個穩妥方案的Submission,再從其他不錯的Submission中随機選一個,把勝負交給偉大的随機性來決定誰才是天選之人。

我們隊伍總共6個人,都是中山大學潘嵘老師CIS實驗室的研究所學生(這也是我們隊名叫SYSU CISLab的原因),劉思聰、黃正傑、鄭華濱、張晉斌是研二的學碩,吳曉晖和蔣禮斌是研一的專碩,每個人的貢獻如下:

劉思聰:主要負責模型設計、查找有用資訊、隊内任務配置設定協調。設計了單模型訓練的基本流程,包括資料增強的類型和使用方式,發現Loss和F2-Score的相關性在Ensemble階段與單模型階段的不同,Ensemble階段的Attention Stacking的設計實作,單模型的調優,多次随機搜尋F2-Score門檻值的方案設計。

黃正傑:主要負責K折交叉驗證設計,實驗記錄的分析和管理,Bagging Ensemble Selection的實作,Attention Stacking方案一的實作,單模型的調優,嘗試使用進化計算搜尋F2-Score的門檻值。

鄭華濱:提出第二輪訓練猛降50倍學習率的做法并驗證其有效性。實作了F2-Score門檻值搜尋函數的GPU版本,大大加速了Ensemble階段根據F2-Score做early stopping的政策。設計實作了Attention Stacking方案二的設計和實作。對比了測試集F2-Score門檻值的平均方案與拼接方案的效果差異。

張晉斌:查找資訊,探索其他可能的資料增強方法,嘗試Ridge Regression的Stacking。

吳曉晖:單模型調優,編寫Leaderboard監控程式,賽後資料的分析和探索,多次随機搜尋F2-Score門檻值的方案實作與探索。

蔣禮斌:修改模型結構,嘗試修改Resnet,DenseNet卷積最後Pooling層,提升單模型在Amazon任務上的表現,隊裡最擅長單模型調優的人,最好的一批單模型基本都是他調出的。

由于篇幅和時間限制,文中一些内容沒有詳細展開,對細節有疑惑或者發現有錯誤的地方,都歡迎大家在評論區指出。另外,我們建立了一個微信交流群,希望和有興趣的朋友一起交流Kaggle參賽經驗,可以戳“閱讀原文”找到加入。

歡迎檢視點選左下角“閱讀原文”處,可以檢視帶各種連結的文章原文

也可以解鎖作者的更多文章~

—— 完 ——

本文作者:劉思聰

原文釋出時間:2017-08-11 

繼續閱讀