玉山人工智慧公開挑戰賽 2020 夏季賽 - NLP 應用挑戰賽

第七名-Brainchild

--

前言

----------

大家好,我們是本次比賽第七名 Brainchild。

這次比賽起因於銀行的借貸業務,為了評估申請借貸者的信用,銀行有三個可用的資料,其一是申請者在申請時提交的資料,其二是行內的相關消費、信用、資產資料,其三是金管會建立的高 AML(洗錢)風險名單。

但在實務上申請者資料不一定會主動揭發他自己的風險,同時也不一定會有行內的紀錄,金管會的高風險名單也是一個相對落後、嚴謹的名單,換句話說,那些正在、疑似、有一點風險的申請者,審核時很難被發現。但這樣的漏洞,其實可以從新聞發現,只是要用人力去每天 follow 看完海量的新聞顯然不可能,所以涉及到這種重複性工作,就是 AI 的專長了。

本次比賽(網址)需要使用從官方提供的五千筆新聞連結與相對應的焦點人物名單,要我們先以爬蟲爬取將新聞內文,再利用內文及人物名單建模並識別該新聞內文是否含有 AML 相關焦點人物,並擷取出焦點人物名單。我們在這次比賽採用 BERT 模型,分別建立四個 model 完成各自的小任務,最後提取出新聞當中涉及到 AML 的人名。另外為了符合主辦的答題需求,我們也用 GCP 建立的一個 API server。

下文將會分成三個部分介紹 — 爬蟲、建模及 API 部署。

爬蟲

----------

這次比賽比較特別的地方,是官方為了模擬真實專案執行過程,僅提供網址與新聞的部分文章,參賽隊伍需自己爬取完整內文。主要的困難在於官方提供的新聞不是單一網站,而是分散在不同來源,如:自由、中時、聯合、TVBS 等四十幾個新聞網,若要一一去解析所有網站並不實際。

由於大部分的網頁都會把文字內容放在<p>標籤裡,故我們選擇先爬所有 <p>…</p> 裡面的文字,若產生例外,再逐筆確認是單純 404,或根據該網站的解析做例外處理。

以下為部分 Code 片段:

爬下來後再檢查是否都有包含官方所標示的人名,這並不是一個 100% 保證正確的確認方法,但至少能確保有 AML 的文章是沒問題的。

由於直接取 <p> 裡的內容資料非常髒,會包含很多廣告、推薦新聞、不需要的前後文等,所以最後一步就要拿來跟官方給的上下文進行比對,去除不需要的部分,確保資料品質。

爬蟲介紹到此結束~

模型

----------

以下將介紹從比賽開始到正式賽各階段所遇到問題的解決方式、模型的改進方向及特徵,過程將不會細列程式碼。其原因為當一個專案在進行時,最困難的不是如何把想法實際付諸程式碼,而是如何從無到有生出一個有效且可行的 IDEA。因此希望讀者著重在我們的思考脈絡,並同時思考其優劣及改善方式,如此將有助於未來任何分析專案上!

若不感興趣的讀者可直接跳至本節底部。此外我們將會省略說明模型背後原理及概念,因此開始之前如果不熟悉 BERT 及 NLP,建議先看以下神人的說明,定會獲益良多:

  1. ELMO, BERT, GPT — 李弘毅教授
  2. 進擊的BERT:NLP 界的巨人之力與遷移學習 — LeeMeng

模型架構及思路

我們最初的想法是把官方近五千筆爬蟲資料有焦點人名的新聞設為 1、無焦點人名的設為 0 ,嘗試訓練出一個判斷是否為 AML 新聞的二元分類模型(以下簡稱Model 1,此模型有缺陷,將在之後說明),若為 AML 新聞,則透過 CKIP(中研院研發的斷詞套件)辨識出人名。

第一階段模型

Q1:新聞中可能也會有無辜的人名,要如何將他排除?

第一階段的模型是將所有新聞人名都辨識出來,但其中一定也有無辜的人名。為了改善這盲點,我們讓新聞經過 CKIP 辨識出人名後取其在新聞中的前後句,並嘗試將這些句子丟回 Model 1 裡面判斷,希望能盡量排除無辜的人名。

第二階段模型

Q2:CKIP 會辨識出單字名、二字簡稱及其他不相關字詞怎麼辦?

相信有用 CKIP 的參賽者都有遇到這個情況,辨識的結果常會出現「台北、嘉檢、陳犯罪」等亂七八糟不是人名的字詞:

嘉義地檢署今年8月查獲台商吳承霖、陳玟叡夫妻從事兩岸地下匯兌,兩年來的資金往來達80億元,並查扣2.4億餘元現金,檢調再掌握男子黃文鴻是吳男夫婦的夥伴,黃男也是集團主嫌之一,負責台灣北部地下匯兌業務;上月2日,調查幹員趁黃男現身台北地院為另案出庭時,將他逮捕,以涉違反銀行法涉嫌重大,向嘉義地院聲押未獲准,未料黃男獲釋落跑,嘉檢上月16日發布通緝,通緝時效為30年。['吳承霖', '陳玟叡', '黃文鴻', '吳男', '黃男', '嘉檢']

由於 CKIP 不是專門用來判別人名的模型,我們的做法是將 CKIP 辨識出來的人名的字詞先經過人工篩選,保留單字名、人名簡稱及全名,並將不相關的詞剔除,再將其結果當作 Target 丟進 NER 模型內訓練(以上文為例就是將 "嘉檢" 剃除後當訓練資料),這樣能大幅有效增加模型辨識人名的正確率。

第三階段模型

Q3:重複使用 Model 1 預測準確度不高,如何解決?

此時我們在 AML 新聞部分的 F1 Score 僅可拿到總分的 75 ~ 80%(原資料切分出的測試集),主要原因為單純取前後句時常會取到不相關的句子,以這新聞片段為例:

由於「化工廠」距離瑪鋉港僅3分鐘車程,研判毒品原料應由漁船裝船後夾帶入境,緝毒單位正擴大清查走私漁船。吳落網後坦承犯行,希望能獲交保,不過檢察官張志明複訊後,認為吳男有逃亡、串證之虞,前天將他聲押禁見獲准。海巡署基隆查緝隊等緝毒單位在新北市萬里區民宅內查獲毒品。

照之前的做法,檢察官張明志的前後句為「希望能獲交保,不過檢察官張志明複訊後,認為吳男有逃亡、串證之虞」,當中包含交保、逃亡、串證這類犯罪字眼,很容易被誤判成 AML 的人物,也因此我們換了前後句的截取方式:

  • 以包含該人名的句子為中間句,並加上前後各一句(以。,?;切)
  • 若前句有句點則不取,若中間句遇到句點則取到該句為止(後句不取)
  • 若前後句包含其他單字名、人名簡稱、全名則捨棄
  • 若中間句包含其他單字名、人名簡稱、全名則置換成空字串
  • 若一則新聞出現同一名字多次,則以逗點合併全部前後句
前後句取法範例

這樣檢察官張明志的前後句為將會變成「不過檢察官張志明複訊後,」如此一來就不會包含明顯犯罪相關字眼。

此外,我們也更新了第二階段的 Model 1。最初的 Model 1 是把整篇新聞丟進去訓練,字數非常多並包含很多與 AML 無關的廢話,但人名前後句字數較少且通常包含該人名是否有洗錢風險的重要證據,不適合套用在 Model 1 上。故我們選擇先將所有有包含焦點人物的資料丟進 NER 篩選並截取出新聞中所有包含「人物全名」的前後句,並以此當訓練集來訓練判斷該人名是否為 AML 人物的模型(以下稱人名 AML 模型)

第四階段模型

經過這些調整,順利讓 AML 分數穩定站上 80%。

Q4:為何仍有些與 AML 完全不相關的新聞被預測成 AML?

以比賽的評分方式,在有焦點人名的新聞中預測錯一個名字會被扣 0.X 分,但在無焦點人名的新聞預測出人名將會被直接扣 1 分,所以再繼續精進 AML 人名分數之前必須讓無焦點人名的新聞都預測正確才行!

經反覆測試模型之後,我們發現不管怎麼調整,總有一些看似與洗錢無關的也被列入,例如前陣子沸沸揚揚的殺警案:

李承翰姊夫獲模範警察 蔡總統頒獎輕拍肩囑「多照顧李媽媽」〔記者姚岳宏/台北報導〕警政署今舉行警察節慶祝大會,總統蔡英文、行政院長蘇貞昌親自頒獎表揚今年獲選的28位全國模範警察,其中殉職鐵警李承翰的姊夫莊貽麟也在受頒行列,原本答應要陪同觀禮的岳父李增文日前突然過世,讓獲獎的莊貽麟徒留遺憾及不捨。鐵警李承翰遇害後,一審法院判決鄭姓凶手無罪,李父李增文為此抑鬱而亡,蔡總統很關心此事,頒獎時特別叮囑莊貽麟與妻子,還輕拍肩請他「多照顧李媽媽」,多幫忙注意家裡。莊貽麟在台中市警局第六分局擔任偵查佐,他工作認真,過去一年破獲多起包括德製衝鋒槍、奧地利克拉克手槍等長短槍及一、二級毒品案,勤餘時間,他也經常帶著太太陪著岳父母外出散心,一個月前莊得知自己獲獎,當時岳父還說要陪他去領獎,未料卻發生憾事。

那時就在思考:這篇應該在 Model 1 就被擋下來,為何還是會進到後面的模型呢?

仔細研究發現由於 Model 1 是用官方將近五千筆的爬蟲資料下去訓練,有焦點人名的設為 1、無焦點人名的設為 0,然而標記為 0 的資料內仍有包含「少量不含人名的 AML 新聞」、「少量犯罪新聞」、「大量與犯罪無關的新聞」,因此 Model 1 學習成了一個是否有犯罪的二元分類模型(以下將 Model 1 改稱犯罪模型)而不是是否有 AML 犯罪的二元分類模型。

那究竟要如何判斷該新聞是否與 AML 有關?

雖然做錯了,但我們決定保留此犯罪模型並修正訓練資料,把有犯罪的資料設成 1,無犯罪的資料設為 0 來訓練。並在此犯罪模型之後,NER 之前加一個用來判斷犯罪且是否為 AML 犯罪的二元分類模型(以下稱為 AML 犯罪模型),將近五千筆爬蟲資料有焦點人名的設為 1,無焦點人名無 AML 關鍵字如「詐欺、背信、偽造文書」等等的犯罪新聞設成 0 送進去訓練。此外由於資料過少,我們用關鍵字額外爬了一千多筆犯罪新聞增加資料量。

第五階段模型

經過這兩個模型後,也順利將先前一直預測錯誤的殺人、偷竊、性侵等犯罪新聞過濾掉,辨識非 AML 新聞的準確度也到達 99% 以上。

Q5:為何很多無辜的檢察官仍會被判斷為焦點人物?另外無罪定讞、不起訴算不算是焦點人物?

經過上文改善前後句的截取方式,雖然可以降低好人的誤判率,但仍有一些問題,如檢察官、警察前後句通常會有對這起案件處理方式的陳述:

嘉義地檢署主任檢察官蔡英俊表示,針對地下匯兌案件,係因嘉義地院就準現行犯之認定與檢察官有異,法院因而以逮捕程序不合法予以駁回羈押之聲請,本署針對此部分將提起抗告。

在此檢察官「蔡英俊」的前後句為「嘉義地檢署主任檢察官蔡英俊表示,針對地下匯兌案件,」因此很容易被模型誤判。我們的方式是以正則判斷,若檢察官、員警等後面有接人名,且在人名之後有調查、偵訊等動作,就直接在模型輸出排除。

此外,觀察官方資料集可以發現,只要某人無罪定讞或不起訴,大多不會被列進 AML 名單中,故我們也將前後句中有無罪定讞、無罪確定、不起訴等相關字眼的人名設為一個規則從輸出一併排除。

Q6:為什麼人名 AML 模型容易 Over fitting?

目前為止人名 AML 模型相比於犯罪模型及 AML 犯罪模型顯得非常不穩定,原因是訓練資料過少,且前後句字數通常不多,故常在第二個 epoch 就開始 Over fitting,即使加了 Dropout 層也無法挽救。因此我們又額外爬取並人工標記出一千多篇有 AML 人名的新聞下去訓練(眼睛快脫窗🤧),但由於不知道官方 AML 人物的標準為何,讓原本的模型學偏了,這也是我們正式賽前三天分數偏低的原因。

坦白說目前 Over fitting 依然存在,但由於時間關係,從第四天便放棄探討Over fitting的解決方式,並放棄那些額外爬取的資料,將人名 AML 模型改成的訓練資料改成原本有焦點人名的官方新聞 + 正式賽 1~3 的新聞,在標準和官方一致的情況下,AML 的 F1 總分約在的 88%~ 92% 之間。

以下便是我們最後模型的架構

  1. 犯罪模型

將有犯罪事實標為 1,與犯罪無關標為 0 的資料訓練。初步篩選包含犯罪之新聞。

2. AML 犯罪模型

將犯罪且與 AML 有關標為 1,犯罪且與 AML 無關標為 0 的資料訓練。用以篩選有 AML 相關犯罪之新聞。

3. NER 模型

用 CKIP 初步辨識並篩選出人名(包含三字、兩字簡稱及單名),以此訓練 NER 模型。辨識新聞中所有人名、簡稱及單名。

4. 人名 AML 模型

取官方原始 331 筆包含 AML 人名新聞 + 正式賽 1~3 天的新聞中所有人名前後句訓練,將 AML 人名的前後句標為 1,非 AML 人名的標為 0。以前後句判斷是否為 AML 人名。

最終模型

模型參數

1. 犯罪模型:
• Bert + BiLSTM(128) + Dense(1)
• maxlen = 512,batch_size = 8,epochs = 3,threshold = 0.4

2. AML 犯罪模型:
•Bert 微調 Encoder-12 + Dense(64) + Dense(1)
•maxlen = 512,batch_size = 8,epochs = 4,threshold = 0.3

3. NER 模型:
•Bert + BiLSTM(128) + CRF(3)
• maxlen = 512,batch_size = 8,epochs = 3

4. 人名 AML 模型:
•Bert 微調 Encoder-12 + Dense(64) + Dense(1)
•maxlen = 256,batch_size = 8,epochs = 3,threshold = 0.4

※以上模型 optimizer 均為 AdamWarmup,lr = 1e-3 ~ 1e-5
※NER 模型 loss 為 crf_loss,其餘為 binary_crossentropy

特徵

  1. 前處理:刪除< >、【】、()、〔〕內的字、記者… 報導、及長度「」中長度四以下的字,防止無關的記者或假名納入。

2. 犯罪模型、AML犯罪模型:若僅用開頭 512 字去預測有時會有盲點,例如:

部落客「貴婦奈奈」與老公黃博健開設杏立博全醫美診所,日前無預警倒閉,夫妻2人也神隱落跑,造成多家廠商、顧客求償無門。北市主任消保官何修蘭今表示,目前已接獲50起申訴案,當中更有消費者儲值了高達50萬新台幣的醫美課程尚未使用,目前信用卡消費部分,已告知銀行業者停止扣款,若消費者當初給付現金,只能等想辦法找到當事人,再進行消費爭議調解。

根據晚間最新消息,因黃博健有醫師資格,但無執業執照,出生醫師世家的他,開設杏立博全醫美診所時,找來也是嘉義婦產科名醫的父親黃立雄掛名擔任院長,據消保官表示,因黃博健已躲起來,要處理後續求償事宜,需找到該診所負責人黃立雄出面說明,比如受害者要請刷卡銀行止付,需由該診所醫師證明療程已使用到什麼程度、需退多少費用,這些資料都在診所裡,因此需找到負責人、才能找出相關證明,幫消費者爭取權益、要求銀行止付,但北市衛生局事發至今五天,卻仍找不到黃博健的爸爸、黃立雄。

經查黃立雄在台大醫院雲林分院的婦產部門診,原訂本周五有門診,這2周均已停診。台大醫院今晚對此說明,黃立雄已算退休,目前在雲林分院兼任醫師,只有門診時間才會出現,目前只確定整個12月都請假,詳細請假時間起迄要明天上班時間再問。

黃立雄醫師是婦科權威,是台大醫學系畢業、曾任台大雲林分院主任,也是引進台灣超音波的始祖,曾獲雜誌評選為百大良醫之一,如今恐受兒子黃博健投資失利連累,而一併失聯中。

根據消保官統計至少50人受害,消保官何修蘭表示,今日相關案件投訴量爆增到50件,還發現有消費者儲值將近50萬醫美課程尚未使用,消保官初步統計總金額超過300萬新台幣,另外有些未寫金額僅寫次數的課程,未列入統計內。

何修蘭說,當初消費者若用刷卡方式付款,消費者可通知發卡銀行以爭議款方式處理,屆時未給付的金額會退款給消費者,若當初消費者支付現金,只能先想辦法找出業者並做後續處理。

大安分局今下午針對貴婦奈奈蘇陳端和丈夫挨告詐欺等案件,報請台北地檢署指揮,北檢已指派民生專組檢察官調查,釐清蘇女等人是否涉及詐欺、背信等刑責。

這篇新聞僅在結尾處有詐欺、背信等較明顯的 AML 字眼,若只取開頭512字,則會取到第三段為止,僅有無預警倒閉、求償無門等較隱晦的關鍵字,故我們會將每則輸入的新聞合併頭尾各 256 字來預測。

3. NER 模型:用維基百科內百家姓輔助判斷並篩選正確人名

維基百科百家姓

此外若新聞長度大於 512 小於 1024 則以句點平分成兩句,若大於 1024 則以句點平分為三句來辨識名字(Bert 字數上限為 512)。

4. 人名 AML 模型:前後句取法

  • 以包含該人名的句子為中間句,並加上前後各一句(以。,?;切)
  • 若前句有句點則不取,若中間句遇到句點則取到該句為止(後句不取)
  • 若前後句包含其他單字名、人名簡稱、全名則捨棄
  • 若中間句包含其他單字名、人名簡稱、全名則置換成空字串
  • 若一則新聞出現同一名字多次,則以逗點合併全部前後句

模型缺陷

由於目前句子只取人名前後句的關係,只要犯罪事實所在的句子不在該人名的前後句,則無法準確判斷出此人是否為 AML 人物,這也是我們錯最多的部分。

另外少部分為由於模型架構、資料少的缺陷,仍有三個問題無法完美解決:

  1. 父子問題(ex:陳其邁的父親陳哲男貪汙)
  2. 檢察官問題(ex:檢察官陳英俊表示,嫌犯於本月遭逮捕)
  3. 無罪問題(ex:陳水扁一審無罪,經再審後改判有罪)

目前僅能透過簡單規則篩選,若其他參賽者有好辦法歡迎一起討論!

另外由於不確定官方 AML 標準為何,無法自行增加資料,未來可從 token 之間的關聯性,找到如指定代名詞所意指的對象。

模型介紹到此結束~

API 部署

----------

所謂的 API 就是 Application Programming Interface,指多個軟體之間的中介活動。而要提供這樣的服務給官方,除了需要撰寫程式以外,還需要一個 server 來架設服務,我們的 API server 是使用 GCP,由三個部分組成:

Model、Flask、Gunicorn

以下將分為五部分說明:

  1. 技術選型
  2. GCP 的 VM 建立
  3. Flask的補充說明
  4. Gunicorn
  5. 模型的環境

技術選型

Server 的選擇非常多,包含用個人電腦或 Cloud,我們選擇 Cloud 是因為比較方便,成員都可以上去訪問,而且也比較方便裝 Linux。而選擇 GCP 主要原因為中文資源多,且不必擔心官方提供的 Azure 因花費過多時間在測試上,在正式賽前就把 200 小時消耗殆盡的問題。

而API 僅用於 response 一個 JSON,故不需要 UI 前端,要選擇的只有後端部分,而基於主辦已經有提供一個 Flask API 範例檔案,自然是採用Flask。

最後用 Gunicorn 的原因則是需要解決一個簡單的問題:使用 Cloud VM 時,只要關掉 Terminal 一段時間,執行中的 Flask 就會自動關掉。所以我們用 Gunicorn 來掛 Flask,這樣就不會因為 Terminal 關掉而中斷服務。

GCP 的 VM 建立

這次競賽需要用到 GPU 進行運算,但一般 VM 預設是沒有的,因此必須特別進行申請,可以參考這裡

而我們操作 VM 的方式都是用 Putty 透過 SSH 連上去,上傳檔案的方式則隨大家喜歡,我們是用 WinsSCP。雖然每個 Cloud 都有它自己的 SDK 或訂製的 Terminal,不過因為此方法較為通用,不必每換一個平台就重新學一次,因此建議大家用這樣的方式操作。

Flask的補充說明

我們在 Windows 環境測試時一切順利,直到把程式傳上 server 啟動時卻發現報錯:

ValueError: Tensor Tensor(“dense_1/Sigmoid:0”, shape=(?, 1), dtype=float32) is not an element of this graph.

原因是 Google 發現由於 Flask 是多執行緒(multiple threads),而我們的模型未在同個執行緒(thread)上所以會出現此錯誤,網路上的解決方法是強制使用 global default graph,所以需要在 Create model 和 Predict 時需改寫,另外由於我們有多個模型,需提前分配 GPU 的 RAM,詳細如下:

Create model

# 預載模型
# 分配 GPU RAM
gpu_options = tf.GPUOptions(allow_growth=True)
# model 1
sess1 = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
graph1 = tf.get_default_graph()
set_session(sess1)
model = bert_LSTM_model()
model.load_weights(bert_LSTM_model_path)

Predict

# 第一階段預測,大於 aml_threshold 者為疑似 aml 文章
global sess1
global graph1
with graph1.as_default():
set_session(sess1)
prediction_1 = model.predict(data)

或許未來可以考慮使用 Docker 的解決環境問題,而更多 Flask 與 Keras 會遇到的問題可參考這裡

Gunicorn

安裝上很簡單,就是 pip install 就好了,比較值得一提的是執行時的資源配置。我們的模型其實用了 4 個 Tensorflow 的 session,而 Tensorflow 的 session 都是預佔資源的,所以會需要在一開始就分配 RAM(動態或固定)。經過實驗發現,雖然平常不需要管 CPU 資源分配,但一個 session 其實要預佔至少一個執行緒(thread),所以像我們 4 個 session 就開至少 4 個 thread,具體的設置如下

sudo gunicorn -w 1 — thread=4 -b 0.0.0.0:8080 — timeout=600 api:app — daemon

模型的環境

在 VM 的 OS 我們選用 Ubuntu 18.04,一個免費又穩定的版本,而部署 API 最主要的麻煩還是在於 Python 與 GPU 的環境安裝,以下會簡述我們的安裝過程:

Python 安裝步驟

  1. 更新 Python3 (Cloud 的 VM 不一定夠新)
sudo apt updatesudo apt install python3 python3-dev python3-venv

2. 安裝 pip

wget https://bootstrap.pypa.io/get-pip.pysudo python3 get-pip.py

3. 利用 requirements.txt 安裝需要的 pack(不用一個一個手動裝)

Upload requirements.txtpip install -r requirements.txt

※補充 requirements.txt:

可以透過 pip freeze > requirements.txt 紀錄環境所使用的套件,好處是方便環境控管,分享給其他組員也能保持開發環境的一致性。

4. 安裝其他所有你需要的 packages

pip install tensorflow-gpu==1.15

GPU 環境設置步驟

  1. 確認 GPU driver(Cloud VM 通常不會有問題,PC 請到 NVIDIA 確認)
  2. 找到對應的版本號(官網
  3. 安裝對應版本的 CUDA
  4. 安裝對應版本的 Cudnn(我都是點 cuDNN Library for Linux 載點。下載下來的副檔名很奇怪,但就直接當 .tar.gz 解壓縮)
  5. 驗證一下可用
from tensorflow.python.client import device_libdevice_libprint(device_lib.list_local_devices())

出現以下畫面代表成功!

測試成功

API 部署介紹到此結束~

結語

----------

這次比賽的勝負關鍵仍在如何提高 AML 新聞辨識出焦點人物的正確性,我們使用 BERT 做了四個深度學習模型來達成這個目標,沒有特別艱澀的技術,也沒有極具創意的巧思,僅運用書上的知識及網路上神人的教學,如同法國哲學家伯納德所說:「我們就像站在巨人肩膀上的侏儒,可以比前人看得更多、更遠。」

感謝玉山 Tbrain 舉辦如此具有挑戰性的競賽,為了模擬實際專案執行過程,這次參賽者不僅需要建模,還須具備爬蟲、部署 API 的及對文章洞悉的能力,相比之前的比賽難度提升了一個檔次,但也因此比之前豐富有趣許多。

另外也看到有些隊伍因 API 異常,比賽剛開始就知道與名次無望,卻不放棄把握剩下的天數,到比賽後期單天分數甚至能超越許多前段班隊伍,雖然很可惜,但把歷練當經驗、志在參與的精神著實令我敬佩,也感謝你們的堅持讓比賽變得更有意義。

最後也不免俗的感謝我的隊友:徐正憲、游璿達、劉家達黃郁傑跟我一起沒日沒夜的瘋,上班忙得要死還要偷閒改個模型、每周開會講幹話喇豬屎,雖仍與第一名有一段差距,不過已經很滿足了。

感謝各位耐心觀看,Brainchild 全體組員,下台一鞠躬!

--

--