if classify(news) == AML then extract(news) else []
由於是學期末看到的比賽,
比賽已經開始快一個月了。
估計學期結束才能開始。
算一算 7/6 才可以開始做,
距離測試賽估計只有兩個禮拜,
之後離正式賽也只有一個禮拜可以調整模型。
有了先前的基礎,
基本上只花了一個晚上就把資料都爬回來了。
爬蟲是相對容易,但是需要重複性勞動的工作,
以下介紹我是怎麼爬新聞的。
新聞網站大部份是動態網頁,
Server => template(content from database)
只要是同個網站的新聞,
大多會遵照一定的排版。
第一步就先來看看有哪些網站的排版要抓
import re
import pandas as pd
from pprint import pprint
csv = pd.read_csv('tbrain_train_final_0610.csv')
webs = set(re.findall(r'(https?://)?([^/]+)', l)[0][1]
for l in csv['hyperlink'])
pprint(webs)
可以得到這 39 個 domain:
{'ccc.technews.tw', 'domestic.judicial.gov.tw',
'ec.ltn.com.tw', 'ent.ltn.com.tw', 'estate.ltn.com.tw',
'finance.technews.tw', 'hk.on.cc', 'house.ettoday.net',
'm.ctee.com.tw', 'm.ltn.com.tw', 'money.udn.com',
'mops.twse.com.tw', 'news.cnyes.com', 'news.ebc.net.tw',
'news.ltn.com.tw', 'news.mingpao.com', 'news.tvbs.com.tw',
'ol.mingpao.com', 'sina.com.hk', 'technews.tw',
'tw.news.yahoo.com', 'www.bnext.com.tw',
'www.businesstoday.com.tw', 'www.chinatimes.com',
'www.cna.com.tw', 'www.coolloud.org.tw', 'www.cw.com.tw',
'www.ettoday.net', 'www.fsc.gov.tw', 'www.hbrtaiwan.com',
'www.hk01.com', 'www.managertoday.com.tw', 'udn.com',
'www.mirrormedia.mg', 'www.nextmag.com.tw', 'www.storm.mg',
'www.nownews.com', 'www.setn.com', 'www.wealth.com.tw'}
有了 domain 之後就是重複性的工作了。
農
舉個例子:http://finance.technews.tw/2019/09/06/palo-alto-networks-intends-to-acquire-zingbox/
按下 f12 後可以看到,
此網頁的 article tag 可以涵蓋所有內文,
之後我再把他 p tag 的內容抓出來就好。
把 39 個 domain 抓出來大概長這樣:
fetch_table = {
'www.mirrormedia.mg': ['article', {}],
'www.coolloud.org.tw': ['div', {'class':'field-items'}],
'm.ctee.com.tw': ['div', {'class': 'entry-main'}],
# ...
'www.storm.mg': ['article', {}],
}
def find_article_args_by(url):
for domain in fetch_table:
if domain in url:
tag, attr = fetch_table[domain]
return { 'name': tag, 'attrs': attr }
print("cannot find domain pattern in", url)
import re
import requests as rq
from functools import reduce
from operator import add
from bs4 import BeautifulSoup
url = 'https://www.storm.mg/article/1625523'
artics = BeautifulSoup(
rq.get(url, timeout = 10).text, "html.parser"
).findAll(**find_article_args_by(url))
dataBy = lambda f: reduce(add, [f(a) for a in artics])
ext1 = lambda a: a.findChildren("p")
ext2 = lambda a: a.find_all(r'^h[1-6]$')
print(' '.join([s for s in [p.get_text().strip()
for p in dataBy(ext1) + dataBy(ext2)]]))
抓到的文章為:
國安局特勤走私菸案今(23)日偵查終結,其中,最先被羈押禁見的總統府侍衛室少校吳宗憲雖只有訂1條790元的大衛杜夫(Davidoff),但由於他是本案主要的訂購及聯繫人,故北檢仍依違反《貪污治罪條例》及違反《稅捐稽徵法》等罪起訴吳宗憲。 據國安局調查報告,本案買菸者共76人,含國安局25人、侍衛室49人、憲兵警衛大隊2人,今日起訴吳宗憲、張恒嘉、前華航副總邱彰信等13人;另外,據北檢統計,本案總計下訂9441條菸,總金額為680萬2330元。(延伸閱讀:國安局私菸案今偵結 北檢起訴吳宗憲邱彰信等13人) 在涉案的國安特勤中,購買最多的為少校徐兆峰,一共購買1037條,但其中有792條是受妻子高中同學祁明昕所託下訂,其餘則為自用。徐、祁2人今日皆依違反《稅捐稽徵法》予以緩起訴,分別需繳交處分金60萬元及50萬元。
對於一些 404 的網頁,
我們可以想辦法把他找回來,
比方說 wayback machine 就是一個不錯的選擇。
使用範例就大概是這樣:
waybackpack -d wayback \
https://udn.com/news/story/7321/3845624
-d
,創一個你指定名字的資料夾,然後存進去。
waybackpack -d wayback \
https://udn.com/news/story/7321/3845624
waybackpack -d wayback \
https://udn.com/news/story/7321/3833161
$ tree -ifF wayback | grep -v '/$'
wayback
wayback/20190524225425/udn.com/news/story/7321/3833161
wayback/20190608120835/udn.com/news/story/7321/3845624
wayback/20190609133509/udn.com/news/story/7321/3845624
wayback/20190827225620/udn.com/news/story/7321/3845624
20 directories, 4 files
之後就是開個檔,
然後一樣餵給剛剛寫的 crawler 即可。
requests.get(url) => open(path)
.text => .read()
wayback machine 都沒有 => 搜尋引擎吧!
模型大概可以分成兩個部份。
以下就來介紹一下一開始是怎麼實作的。
同一類的東西 => 相似度會比較高,
出現過的詞想成向量。
比如這裡有三句話,我們把他當成三篇文章,
為一個 corpus,並且已經做好斷詞。
1. 太平洋/有/颱風/生成/,/請/民眾/關注/天氣/,/嚴防/大雨/。
2. 天氣/預報/:/氣流/影響/,/天氣/仍舊/不穩/,/留意/瞬間/大雨/。
3. 台灣/座落/於/西/太平洋/。
斷詞在實作上我們是使用 jieba 的 search_mode
。
import jieba
text = '台灣座落於西太平洋。'
print(jieba.lcut_for_search(text)) # 搜尋引擎模式
#['台灣', '座落', '於', '太平', '太平洋', '西太平洋', '。']
print(jieba.lcut(text, cut_all = True)) # 全模式
#['台', '灣', '座落', '於', '西太平洋', '太平', '太平洋', '', '']
print(jieba.lcut(text, cut_all = False)) # 精確模式
#['台灣', '座落', '於', '西太平洋', '。']
把停用詞 (stopword) 等一些常用的詞去掉,
例如 請
,於
, 仍舊
, 瞬間
一類的詞,
所有詞可以表示成一個 vector。
[太平洋, 颱風, 生成, 民眾, 關注, 天氣, 嚴防, 大雨,
預報, 氣流, 影響, 不穩, 留意, 台灣, 座落]
去掉停用詞的新文章為:
1. 太平洋/颱風/生成/民眾/關注/天氣/嚴防/大雨
2. 天氣/預報/氣流/影響/天氣/不穩/留意/大雨
3. 台灣/座落/太平洋
不難想到,一個詞如果在一篇文章中出現多次,
那這個詞和這篇文章的關聯度就會越高,
這個就是 TF (term frequency) 的概念,
一般可以計算為:
該詞出現在該文章的次數 / 該篇文章的詞數
[太平洋, 颱風, 生成, 民眾, 關注, 天氣, 嚴防, 大雨, 預報, 氣流, ...]
[ 1/8, 1/8, 1/8, 1/8, 1/8, 1/8, 1/8, 1/8, 0, 0, ...]
[ 0, 0, 0, 0, 0, 2/8, 0, 1/8, 1/8, 1/8, ...]
[ 1/3, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
用 TF 的直覺大概就是
如果兩篇文章擁有相同的詞越多,相似度可能就越高。
我們用 cosine similarity 來計算文章相似度:
\[\cos (t,e)= {t e \over \|t\| \| e\|} \\ = \frac{ \sum_{i=1}^{n}{t_i e_i} }{ \sqrt{\sum_{i=1}^{n}{(t_i)^2} } \sqrt{\sum_{i=1}^{n}{(e_i)^2} } }\]
from sklearn.metrics.pairwise import cosine_similarity
vecA = [1/8, 1/8, 1/8, 1/8, 1/8, 1/8, 1/8, 1/8, 0, ...]
vecB = [ 0, 0, 0, 0, 0, 2/8, 0, 1/8, 1/8, ...]
vecC = [1/3, 0, 0, 0, 0, 0, 0, 0, 0, ...]
print(cosine_similarity(
[vecA, vecB, vecC], [vecA, vecB, vecC]))
# a b c
[[1. 0.3354102 0.20412415] # a
[0.3354102 1. 0. ] # b
[0.20412415 0. 1. ]] # c
inverse document frequency,逆向文件頻率
一個詞只出現在某幾篇新聞中(比如 "洗錢"),
一個詞幾乎每篇都有(比如 "記者"),
那前者的重要性和獨特性應該會比後者高。
一般可計算為:
log(所有的文章數目 / (出現該詞的文章數 + 1))
log(3 / (2)) = 0.4 #因為 corpus 小,而且詞都有出現,所以就不做 +1
log(3 / (1)) = 1.1
[太平洋, 颱風, 生成, 民眾, 關注, 天氣, 嚴防, 大雨, 預報, 氣流, ...]
[ 0.4, 1.1, 1.1, 1.1, 1.1, 0.4, 1.1, 0.4, 1.1, 1.1, ...]
IDF 可以表達出一個詞的特徵值,
我們把他與 TF 相乘,
便可得到更有意義的特徵值。
[太平洋, 颱風, 生成, 民眾, 關注, 天氣, 嚴防, 大雨, 預報, 氣流, ...]
[ 0.05, 0.21,0.21,0.21,0.21, 0.05,0.21, 0.05, 0.0, 0.0, ...]
[ 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.05,0.21,0.21, ...]
[ 0.13, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ...]
我們一樣計算 cosine similarity 可得:
# a b c
[[1. 0.03368042 0.01800272] # a
[0.03368042 1. 0. ] # b
[0.01800272 0. 1. ]] # c
IR model 應用上述概念所做的搜尋引擎。
把關聯度高的文章排到前面。
而我們之前所作的 model 使用了:
IR model 當 classifier?
[ x 0 / 300 score = 0.0 ] Qry29: 【2019理財大事5】跌破...
[ x 11 / 300 score = 5.5 ] Qry30: 公開資訊觀測站...
[ v 223 / 300 score = 195.9 ] Qry31: 涉貪圖利 東檢聲押前台...
[ x 0 / 300 score = 0.0 ] Qry32: 昂山素姬明出席國際法 ...
[ x 0 / 300 score = 0.0 ] Qry33: 繼思想改造集中營之後 ...
[ x 0 / 300 score = 0.0 ] Qry34: 山頂纜車機件故障暫停 ...
[ v 251 / 300 score = 215.4 ] Qry35: 直銷妹誆「1年帶你住帝...
[ v 262 / 300 score = 224.4 ] Qry36: 潤寅詐貸案延燒 上市公...
[ v 206 / 300 score = 179.1 ] Qry37: 花蓮縣3議員涉收賄 貪 ...
[ x 0 / 300 score = 0.0 ] Qry38: 週三晚起東北季風增強 ...
[ x 0 / 300 score = 0.0 ] Qry39: 「灰天鵝」拉警報 | An...
[ x 1 / 300 score = 0.6 ] Qry40: 柯媽爆料:柯文哲絕對 ...
[ x 0 / 300 score = 0.0 ] Qry41: 媒體:特朗普涉嫌威脅 ...
[ x 0 / 300 score = 0.0 ] Qry42: 國銀海外投資豐收 8月O...
[ x 155 / 300 score = 63.6 ] Qry148: 12/3 今彩539頭獎 ...
[ x 155 / 300 score = 63.8 ] Qry797: 12/7 雙贏彩、今彩539 ...
人名提取是本次比賽的重點。
NER 可以識別出特殊的名詞,
例如人物、組織和地點等。
去年九月,
中研院的 ckip 開源了斷詞系統 ckiptagger。
"""
word embedding + BiLSTM => 斷詞系統
word embedding + BiLSTM + 斷詞 => 詞性標注
word embedding + BiLSTM + 斷詞 + 詞性標注 => 精確、多類別 的 NER
"""
[name for name in NER(news)
if name.type == PEOPLE and not match ["*嫌", "*婦"]]
# ex: 張嫌 陳婦
一個簡單的範例片段:
from ckiptagger import WS, POS, NER
ckipDir = 'ckip' # ckip pre-training path
doc = '重判12年又加保3億,法官怕中電前董周麗真逃亡。'
ws, pos, ner = [f(ckipDir) for f in [WS, POS, NER]]
word_s = ws([doc], sentence_segmentation=True,
segment_delimiter_set=set('??!!。,,;:、'))
names = set([e[3] \
for e in ner(word_s, pos(word_s))[0] \
if e[2] == 'PERSON'])
print(names) # {'周麗真'}
另一個範例片段:
from pprint import pprint
from ckiptagger import WS, POS, NER
docs = ['東太平洋漁場時價分析師兼操盤手暨洋流講師海龍王彼得。',
'浪漫Duke帶你找到屬於你的浪漫。','卑鄙源之助已經遲到一個小時了欸。',
'惡魔貓男,你今晚的惡夢','我台北暴徒,天不怕地不怕,aka 黑魔王']
ws, pos, ner = WS('ckip'), POS('ckip'), NER('ckip')
word_s = ws(docs, sentence_segmentation=True,
segment_delimiter_set=set('??!!。,,;:、'))
pprint([set([(e[3], e[2]) for e in doc]) \
for doc in ner(word_s, pos(word_s))])
[{('海龍王', 'PERSON'),
('東太平洋', 'LOC'),
('彼得', 'PERSON')},
set(),
{('一個小時', 'TIME')},
{('今晚', 'TIME')},
{('台北', 'GPE')}]
至此,一個不太精確的標記系統已經完成了,
此比賽模型也已經有了一個雛型。
接下來就講講如何把他接上 API,
提供服務給外界使用。
主辦單位提供了 Azure 雲端給我們使用,
主要有用的東西除了一個 Ubuntu 可以使用外,
還有 K80 的 GPU 及一個 IP。
不過原則上還是自己配的環境好用些。
API call 分作兩個部份 health check 和 inference,
health check 主要在確認 service availability,
而 inference 主要是負責答案的判定。
health check:
@app.route('/healthcheck', methods=['POST'])
def healthcheck(): # API for health check
data = request.get_json(force=True)
print(data)
t = datetime.datetime.now()
ts = str(int(t.utcnow().timestamp()))
server_uuid = generate_server_uuid(CAPTAIN_EMAIL+ts)
server_timestamp = t.strftime("%Y-%m-%d %H:%M:%S")
return jsonify({
'esun_uuid': data['esun_uuid'],
'server_uuid': server_uuid,
'captain_email': CAPTAIN_EMAIL,
'server_timestamp': server_timestamp
})
inference:
answer_cache = {}
@app.route('/inference', methods=['POST'])
def inference():
data = request.get_json(force=True)
esun_timestamp = data['esun_timestamp']
now = datetime.datetime.now()
server_timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
ts = str(int(now.utcnow().timestamp()))
server_uuid = generate_server_uuid(CAPTAIN_EMAIL+ts)
template = lambda ans: jsonify({
'esun_timestamp': data['esun_timestamp'],
'server_uuid': server_uuid,
'answer': ans,
'server_timestamp': server_timestamp,
'esun_uuid': data['esun_uuid']
})
if data['esun_uuid'] in cache_answer:
if cache_answer[data['esun_uuid']] != None:
return template(cache_answer[data['esun_uuid']])
else:
while cache_answer[data['esun_uuid']] == None:
sleep(4)
return template(cache_answer[data['esun_uuid']])
else:
cache_answer[data['esun_uuid']] = None
try:
log(data['news'])
answer = predict(data['news'])
log(answer)
except:
log('model error')
raise ValueError('Model error.')
cache_answer[data['esun_uuid']] = answer
return answer_template(answer)
inference 做了 cache,
原因是一個 inference 時間上限為五秒,
逾時就會重新發 request 過來,次數上限為三次。
為了避免逾時而重複 inference,
所以我們做了 cache。
不過 inference 通常滿快的,
一兩秒內就可以算完了。
Azure 對外不開放 80 和 443 以外的 port,
所以原則上把服務開在其中一個 port 即可。
那如果手上有比較好的顯卡,覺得 K80 跑得太慢,但該電腦又沒有固定 IP 的話怎麼辦呢?
反向代理
我們把 flask 開在 8080 port 上,
forward 到伺服器的 80 port 上,
http protocol 瀏覽伺服器的 IP 位置即可。
# /etc/ssh/sshd_config
AllowTcpForwarding yes
systemctl restart sshd.service
這邊使用 autossh 讓他自動重連比較穩定。
autossh -M 20000 -i ~/.ssh/id_rsa -NfR \
:8080:localhost:8080 user@azure
# foward local 8080 to remote 8080
遠端的 80 port 需要 root 權限,
ssh 會關掉 root 遠端登入。
所以這邊可以透過 python-port-forward:
sudo python2.7 port-forward.py 80:localhost:8080
前置作業都完成後,
只要把 web hook 掛給官方提供的 slack bot 即可。
之後比賽他就會去戳你給的 IP address 了。
到這邊,
已經可以開始拿做好的東西打一場比賽了。
接下來讓我們繼續把 model 調得更好!
Logistic Regression, SVM and XGBoost
從 sklearn 裡拿分類器,
用 bm25 + w2v feature 分類。
詳細教學可以參考 這篇文章。
我們嘗試了三種分類器:
LogisticRegression,SVC 和 XGBoost。
clf = LogisticRegression(
C=1.0,solver='lbfgs', multi_class='multinomial')
clf.fit(xtrain_tfv, ytrain)
predictions = clf.predict_proba(xvalid_tfv)
clf = SVC(C=1.0, probability=True)
clf.fit(xtrain_svd_scl, ytrain)
predictions = clf.predict_proba(xvalid_svd_scl)
clf = xgb.XGBClassifier(
max_depth=7, n_estimators=200, colsample_bytree=0.8,
subsample=0.8, nthread=10, learning_rate=0.1)
clf.fit(xtrain_tfv.tocsc(), ytrain)
predictions = clf.predict_proba(xvalid_tfv.tocsc())
這裡 classifier 的準確率來到了 88% 到 90% ,
大樂透類的新聞也被準確歸類了。
經由測試,XGBoost 的效果是最好的,
於是我們就把 classifier 換成 XGBoost。
人名前後 5 token 的 BM25
=> XGBoost
=> rule based filter
BERT 比較適合小文本的任務,
這次的比賽就是一個非常好的發揮空間。
利用 word embedding
unsupervised (特徵提取) + supervised (下游任務)
我們在測試賽前嘗試使用 BERT 建立新 classifier,準確度有大幅的提昇。
BERT from transformers
(BERT, XLNet…)
pre-training from hugface 的網站
因為此次是中文的比賽,所以我們使用了最基本款
bert-base-chinese
即可。
BERT 四大下游任務
BERT 512 的 token size 限制,
我們取了文章最後的 510 個 token 丟進 model 。
分類準確度從剛才的 90% => 99% 。
時間來到了測試賽。
測試賽開始:
僅測試伺服器的穩定度,
並沒有提供題目正確答案和分數。
BERT 也有提供 NER 的任務訓練,
而 ckip 的 NER 是用在廣泛用途的,
那何不用 BERT 自己也 train 一個呢?
用 bert-base-chinese
,做 tokenization。
接著根據幫匹配的人名標上標記。
"法官怕周麗真逃亡", ["周麗真"]
['法', '官', '怕', '周', '麗', '真','逃', '亡']
['O', 'O', 'O', 'B-PER', 'I-PER', 'I-PER', 'O', 'O']
只要寫個小小的 script 轉換完資料,
用 BertForTokenClassification 開始 NER 囉!
NER 似乎就有簡單的分類能力,
可以避開一些非 AML 相關的人名。
到這裡,基本的模型已經構建完畢,
這就是我們進行正式賽的 Model。
正式賽分作兩週,共八天。
正式賽第一周開始:
我們在這週的排名第一天在第四,
之後又掉到了五和六。
第一周結束的假日,
300 AML news + IR model => new classifier data
正式賽第二周開始:
爬回了四而隔天又掉回了五,
加入新資料似乎有一點提昇。
不過 model 似乎還要再加強,
=> 決定嘗試其他 Model。
XLNet, RoBERTa, Albert 沒有很大的提昇。
RoBERTa 在 classifier 表現上有好些。
Cinese-roberta-wwm-ext
前幾天的 query 當作 validation set,
RoBERTa 的準確度從 96% 上到 97%,
RoBERTa 的 classifier 似乎有變好,
於是我們將 classifier 換成 RoBERTa。
下圖是我們的最終架構圖:
第七天的 query 送成前一天的,
故第七天沒有列入計算。
最後我們跑到了第三名。
至此,整個賽程結束。
不過礙於時間關係,我們沒來得及做這些嘗試。
我們在嘗試前面的 Model 時,
有嘗試用演化計算來調整參數。
不過後來 Model 都轉移到 NN 上,
我們在傳統機器學習方法上就沒再做更多嘗試了。
其實演化計算的應用很廣,
或許可以應用在現在 Model 的參數微調上。
model 用法錯誤?更棒的 model?
pre-training + news => domain specific ability
NER 的部份,也可以將一千五百篇的人名做標記,
如此 NER 的效果可能會提昇一些。
跟圖片一樣,
NLP 的分類也可以使用 augmentation,
這似乎也是一個研究的方向:
一個中文數據增強的實現。
開箱結束,謝謝收看。