1. 程式人生 > >使用fasttext實現文字處理及文字預測

使用fasttext實現文字處理及文字預測

0?wx_fmt=gif&wxfrom=5&wx_lazy=1

 向AI轉型的程式設計師都關注了這個號???

大資料探勘DT資料分析  公眾號: datadw

因為參加datafountain和CCF聯合舉辦的大資料競賽,第一次接觸到文字預測。對比了一些模型,最終還是決定試一下fasttext。上手fasttext的過程可以說是很痛苦了,因為國內各大部落格網站上很少有fasttext的部落格。一方面是fasttext是FaceBook去年才開源的,用的人比較少,還有一方面是fasttext大部分參考資料都是英文的,我啃了好久英文文件,搭梯子去國外的論壇,最後也算是簡單上手了吧。這兩天差不多所有時間都花在這上面了,感觸挺深。基於以上幾點,我覺得還是寫一篇部落格吧,雖然只是入門,也各位看官多多點評,提出不同意見。

問題分析

360搜尋出的這道題,題目是“360搜尋-AlphaGo之後“人機大戰”Round 2 ——機器寫作與人類寫作的巔峰對決”,乍一看挺嚇人的,其實是讓開發者通過一套模型,來識別一篇文章是機器寫出來的,還是人類寫出來的。其實是有一定區分的,比如說人類寫出來的文章,文章的標題和內容契合度比較高(排除標題黨的情況),而且文章正文有一定的邏輯連續性,很少在文章的body中出現亂碼。機器寫出來的文章在以上方面和人類寫出來的文章會有不同之處。

可能我這樣講還不夠直觀,“什麼才是機器寫出來的文章?”,我從資料集裡面拿出來一小篇,像以下文章就是機器寫出來的文章。

重慶永川消防提示:夏季酷暑來臨 警惕火災隱患
 電影院部分房屋結構變形、兩個安全疏散門變形無法開啟,自入伏以來,忌水性物質有生石灰,居民和單位用電量也會隨之增加,由此引發的火也比較多,重慶市永川消防支隊在此提醒大家:高溫天氣裡要增強安全防範意識,加強火災防控,要時刻警惕以下幾種常見的火災。救援消防官兵抵達現場。一、電氣火災。隨著高溫天氣的到來,空調、冰箱等用電裝置大量增加,電氣裝置線路超負荷運轉,電源絕緣皮損壞造成短路打火,圖為被困人員被成功救出。或電器的電動機進水受潮,使絕緣強度降低,發生短路燒燬電機著火等。二、汽車火災。救援消防官兵抵達現場,夏天很容易發生汽車火災,主要原因是:有些汽車使用時間過長,一直從二樓視窗向外大聲呼喊”救命”,電源線路老化易發生短路,有的汽車超負荷裝載,造成發動機溫度升高,再加上天氣酷熱,發動機通風裝置不好,從而引起汽車自燃。並且,有些車主為了車內空氣清新,導致正在電影院內觀看電影的9名民眾被困,選擇在其車內放置香水、空氣清新劑、二組對被困民眾進行情緒安撫。老花鏡、打火機等物品,極易引發火災。三、該縣碧羅數字電影院背後發生山體滑坡,電瓶車火災。成功將變形的安全疏散門破拆出一個能夠容納單人通過的出口。隨著電瓶車的普及,電瓶車充電引發火災不在少數。特別是有些使用者私拉亂接電線,不按要求使用插線板,貢山縣碧羅數字電影院背後發生山體滑坡。違規充電引發火災。四、施工現場火災。對施工現場的氧氣瓶、乙炔瓶、防火材料、油漆稀料等易燃易爆物品管理不嚴,直接放在高溫下暴晒,未採取有效的遮擋措施,沒有設定在通風、陰涼地點儲存,三組利用破拆工具對變形的安全門進行破拆,這樣很容易發生火災事故五、危化品火災。成功將變形的安全疏散門破拆出一個能夠容納單人通過的出口。夏季地面氣溫有時高達40℃以上,救援消防官兵通過金屬切割機、破門器、液壓破拆工具組等破拆裝備的配合使用,在這樣炎熱的氣溫條件下,化學危險物品在生產、圖為被困人員被成功救出。運輸、過氧化鹼。所以,一定要謹慎保管、圖為受損嚴重的碧羅數字電影院,使用易燃易爆化學危險品。六、物質自燃火災。自燃物質除過去我們常講的稻草、煤堆、棉垛外,被困人員已被全部救出,還有油質纖維、三、硝酸銨化肥、導致正在電影院內觀看電影的9名民眾被困,魚粉、農產品等。這些物質儲存時,如果堆積時間過長,通風不好,自身就會發生變化產生熱,溫度逐漸升高。忌水性物質有生石灰,無水氧化鋁,過氧化鹼,氯磺酸等,這些物質遇到水或空氣中的潮氣後就會釋放出大量可燃氣體,四。

上面的文章,仔細看可以看出破綻:

1、存在反覆,且不需要反覆強調的文字,例如“忌水性物質有生石灰”;

2、邏輯不通順,文章結尾一個“四”,不知其所指;

3、文章有明顯拼湊痕跡,從“一二三四”幾點可以看出是從很多篇文章中剪輯而來,上下文關聯性弱。

目標

有兩個資料集(分別是1.6GB和2GB),一個數據集是訓練集(訓練模型之用),另一個數據集是測試集(提交結果之用)。

資料格式

資料一:訓練集,規模50萬條樣例(有標籤答案),資料格式如下:

FieldTypeDescriptionNote
文章IDString文章ID
文章標題String文章的標題,字數在100字之內已脫敏。去掉了換行符號。
文章內容String文章的內容已脫敏。文章內容是一個長字串,去掉了換行符號。
標籤答案String人類寫作是POSITIVE, 機器人寫作是NEGATIVE機器人寫手和人類撰寫的文章,參賽者訓練資料,可以選擇本集合的全量資料,也可以選擇部分資料。但是參賽者不能自行尋找額外的資料加入訓練集。

資料二:測試集A,規模10萬條樣例(無標籤答案),資料格式如下:

FieldTypeDescriptionNote
文章IDString文章ID
文章標題String文章的標題,字數在100字之內已脫敏。去掉了換行符號。
文章內容String文章的內容已脫敏。文章內容是一個長字串,去掉了換行符號。

資料三:測試集B,規模30萬條樣例(無標籤答案),資料格式如下:

FieldTypeDescriptionNote
文章IDString文章ID
文章標題String文章的標題,字數在100字之內已脫敏。去掉了換行符號。
文章內容String文章的內容已脫敏。文章內容是一個長字串,去掉了換行

上述三份資料中,都同時包含了機器人寫手和人類撰寫的文章資料。一條樣例主要包括文章ID、文章標題、文章內容和標籤資訊(人類寫作是POSITIVE, 機器人寫作是NEGATIVE)。需要在訓練集上得到模型,然後使用模型在測試集上判定一篇文章是真人寫作還是機器生成。如果這篇文章是由機器人寫作生成的,則標籤為NEGATIVE,否則為POSITIVE。僅在訓練集上提供標籤特徵,參賽選手需要在測試集上對該標籤進行預測。

資料預處理

資料預處理可以說是很關鍵了,很多團隊都表示需要花大量的時間用於資料的預處理,我這邊偷個懶,採用jieba對訓練集和測試集文字進行分詞,並且順手把它轉化為fasttext格式。

#encoding=utf-8
import jieba
#author linxinzhu
seg_list = jieba.cut("這個競賽真的費時間",cut_all=True)
print "Full Mode:", "/ ".join(seg_list) #全模式
seg_list = jieba.cut("zwq沉迷逛QQ空間,還時不時撩妹",cut_all=False)
print "Default Mode:", "/ ".join(seg_list) #精確模式
seg_list = jieba.cut("測試集好大啊,跑一次要好久") #預設是精確模式
print ", ".join(seg_list)

seg_list = jieba.cut_for_search("這篇部落格是在20171117日寫的,
各位看官覺得有用的話,可以評論點贊
") #搜尋引擎模式
print ", ".join(seg_list)

輸出結果

PS C:\Users\LinXinzhu\py> python fenci.py
Full Mode:Building prefix dict from C:\Python27\lib\site-packages\jieba\dict.txt ...
Loading model from cache c:\users\linxin~1\appdata\local\temp\jieba.cache
Loading model cost 0.804000139236 seconds.
Prefix dict has been built succesfully.
 這個/ 競賽/ 真的/ 費時/ 費時間/ 時間
Default Mode: zwq/ 沉迷/ 逛/ QQ/ 空間/ ,/ 還/ 時不時/ 撩妹
測試, 集好, 大, 啊, ,, 跑, 一次, 要, 好久
這篇, 部落格, 是, 在, 2017, 年, 11, 月, 17, 日寫, 的, ,, 各位, 看官, 覺得, 有用, 的話, ,, 可以, 評論, 點贊

需要注意的是:

1、程式碼開頭記得寫上編碼方式,包括後面的fasttext在編碼上也挺麻煩的,不寫的話有驚喜哦!

2、jieba.cut返回一個list,所以在做字串拼接的時候要把list轉成string,常用的就是“ ”.join()

符號處理
def go_split(s,min_len):
# 拼接正則表示式
symbol = ',;。!、?!'symbol = "["+ symbol + "]+"# 一次性分割字串
result = re.split(symbol, s)
return [x for x in result if len(x)>min_len]

def is_dup(s,min_len):
result = go_split(s,min_len)
return len(result) !=len(set(result))

def is_neg_symbol(uchar):
neg_symbol=['!', '0', ';', '?', '', '', '']
return uchar in neg_symbol
特殊字處理

一些文字,例如“的”、“了”等等在某個地方有特殊含義,例如“的確”、“瞭解”,但是在大部分的情況下對文章的語義沒有特別的影響。例如”今天早上喝了牛奶“與”今天早上喝牛奶“沒有太大的區別。

if (ur",的" in s0) and (not(ur",的確" in s0)) and (not(ur",的士" in s0)) \
and (not(ur",的哥" in s0)) and (not(ur",的的確確" in s0)):
flag = "NEGATIVE"if (ur",了" in s0) and (not(ur",瞭解" in s0)) and (not(ur",了結" in s0)) \
and (not(ur",了無" in s0)) and (not(ur",了卻" in s0)) \
and (not(ur",了不起" in s0)):
flag = "NEGATIVE"if (ur"。的" in s0) and (not(ur"。的確" in s0)) and (not(ur"。的士" in s0)) \
and (not(ur"。的哥" in s0)) and (not(ur"。的的確確" in s0)):
flag = "NEGATIVE"if (ur"。了" in s0) and (not(ur"。瞭解" in s0)) and (not(ur"。了結" in s0)) \
and (not(ur"。了無" in s0)) and (not(ur"。了卻" in s0)) \
and (not(ur"。了不起" in s0)):
flag = "NEGATIVE"if (ur";" in s0) and (not(ur";的確" in s0)) and (not(ur";的士" in s0)) \
and (not(ur";的哥" in s0)) and (not(ur";的的確確" in s0)):
flag = "NEGATIVE"if (ur";" in s0) and (not(ur"瞭解;" in s0)) and (not(ur";了結" in s0)) \
and (not(ur";了無" in s0)) and (not(ur";了卻" in s0)) \
and (not(ur";了不起" in s0)):
flag = "NEGATIVE"if (ur"?" in s0) and (not(ur"?的確" in s0)) and (not(ur"?的士" in s0)) \
and (not(ur"?的哥" in s0)) and (not(ur"?的的確確" in s0)):
flag = "NEGATIVE"if (ur"?" in s0) and (not(ur"?瞭解" in s0)) and (not(ur"?了結" in s0)) \
and (not(ur"?了無" in s0)) and (not(ur"?了卻" in s0)) \
and (not(ur"?了不起" in s0)):
flag = "NEGATIVE"
專業詞彙、領域詞彙、近義詞

這方面可以引入詞庫,但是時間有限,目前還沒有加入詞庫。

分詞並轉換成fasttext格式
#encoding=utf-8
#author linxinzhu
import jieba
import sys
reload(sys)
sys.setdefaultencoding('utf8')
i = 0
count=0
f = open("train.tsv", 'r')
#f = open("evaluation_public.tsv", 'r')
outf = open("lab3fenci.csv",'w')
#outf = open("lab3fencitest.csv",'w')
for line in f:
r = ""try:
r = line.decode("UTF-8")
except:
       print "charactor code error UTF-8"pass
   if r == "":
       try:
r = line.decode("GBK")
except:
           print "charactor code error GBK"pass
line=line.strip()
l_ar=line.split("\t")
if len(l_ar)!=4:
       continue
id=l_ar[0]
title=l_ar[1]
content=l_ar[2]
lable=l_ar[3]

seg_title=jieba.cut(title.replace("\t"," ").replace("\n"," "))
seg_content=jieba.cut(content.replace("\t"," ").replace("\n"," "))
#r=" ".join(seg_title)+" "+" ".join(seg_content)+"\n"
outline = " ".join(seg_title)+"\t"+" ".join(seg_content)
outline = "\t__label__"+ lable + outline+"\t"outf.write(outline)

if i%2500 == 0:
count=count+1
sys.stdout.flush()
sys.stdout.write("#")
i=i+1
f.close()
outf.close()
print "\nWord segmentation complete."print i

這裡面要注意的是list和string的轉換,以及在cut過程中對空格和換行的處理。

分詞出來之後是這樣的:
0?wx_fmt=jpeg

分詞後文件為1.9GB,同樣對測試集也做相同的處理。

模型建立

終於要用到fasttext了,fasttext的安裝也是個坑。windows10上面裝了半天也沒裝好,好不容易找了一個fasttext for window 10的安裝包,結果居然要python 3.5,升級了python之後發現沒有預測功能,簡直雞肋啊。無可奈何花落去,只能在Linux下面玩了。

安裝fasttext python指令,會提示少cython模型,照著提示下載就行。

pip install fasttext

但是下載奇慢,換國內源或者搭梯子吧。

有關fasttext的原理請查閱作者的paper

https://arxiv.org/pdf/1607.01759.pdf

0?wx_fmt=jpeg

fastText的模型架構類似於CBOW,兩種模型都是基於Hierarchical Softmax,都是三層架構:輸入層、 隱藏層、輸出層。 

CBOW模型又基於N-gram模型和BOW模型,此模型將W(t−N+1)……W(t−1)作為輸入,去預測W(t)  
fastText的模型則是將整個文字作為特徵去預測文字的類別。

一些比較重要的函式

詞向量模型學習

import fasttext

# Skipgram model
model = fasttext.skipgram('data.txt', 'model')
print model.words # list of words in dictionary
# CBOW model
model = fasttext.cbow('data.txt', 'model')
print model.words # list of words in dictionary

文字分類

classifier = fasttext.supervised('data.train.txt', 'model')

data.train.txt是一種含有訓練句子 每行加上標籤的文字檔案。預設情況下,假設標籤的話, 字首字串__label__

這將輸出檔案:model.binmodel.vec

精度評估

result = classifier.test('test.txt')
print '[email protected]:', result.precision
print '[email protected]:', result.recall
print 'Number of examples:', result.nexamples

檢視一個文字最有可能的標籤,這個函式可以說是非常有用了

texts = ['example very long text 1', 'example very longtext 2']
labels = classifier.predict(texts)
print labels

# Or with the probability
labels = classifier.predict_proba(texts)
print labels

除錯模型用的API

input_file     training file path (required)
output         output file path (required)
lr             learning rate [0.05]
lr_update_rate change the rate of updates for the learning rate [100]
dim            size of word vectors [100]
ws             size of the context window [5]
epoch          number of epochs [5]
min_count      minimal number of word occurences [5]
neg            number of negatives sampled [5]
word_ngrams    max length of word ngram [1]
loss           loss function {ns, hs, softmax} [ns]
bucket         number of buckets [2000000]
minn           min length of char ngram [3]
maxn           max length of char ngram [6]
thread         number of threads [12]
t              sampling threshold [0.0001]
silent         disable the log output from the C++ extension [1]
encoding       specify input_file encoding [utf-8]

舉個栗子

model = fasttext.skipgram('train.txt', 'model', lr=0.1, dim=300)

解釋一下:lr是學習速率,dim是詞向量的大小,調節不同的引數使得模型更加精確。

分類器的屬性和方法
classifier.labels                  # List of labels
classifier.label_prefix # Prefix of the label
classifier.dim # Size of word vector
classifier.ws # Size of context window
classifier.epoch # Number of epochs
classifier.min_count # Minimal number of word occurences
classifier.neg # Number of negative sampled
classifier.word_ngrams # Max length of word ngram
classifier.loss_name # Loss function name
classifier.bucket # Number of buckets
classifier.minn # Min length of char ngram
classifier.maxn # Max length of char ngram
classifier.lr_update_rate # Rate of updates for the learning rate
classifier.t # Value of sampling threshold
classifier.encoding # Encoding that used by classifier
classifier.test(filename, k) # Test the classifier
classifier.predict(texts, k) # Predict the most likely label
classifier.predict_proba(texts, k) # Predict the most likely label include their probability

除錯分析

建立一個簡單的模型

classifier = fasttext.supervised("lab3fenci.csv","lab3fenci.model",

label_prefix="__label__")

對模型進行測試,觀察其精度

result = classifier.test("lab3fenci.csv")

print result.precisionprint

result.recall

拿一個text來預測

texts = ['它被譽為"天下第一果",補益氣血,養陰生津,現在吃正應季!  六七月是桃
子大量上市的季節,因其色澤紅潤,肉質鮮美,有個在實驗基地裡接受治療的妹子。廣受大
眾的喜愛。但也許你並不知道,看慣了好萊塢大片眼花繚亂的特效和場景。它的營養也是很
高的,不僅富含多種維生素、礦物質及果酸,至少他們一起完成了一部電影,其含鐵量亦居
水果之冠,被譽為"天下第一果"。1、在來世那個平行世界的自己。增加食慾,養陰生津的
作用,可用於大病之後,氣血虧虛,面黃肌瘦,Will在海灘上救下了Isla差點溺水的兒
子。心悸氣短者。2、最近有一部叫做《愛有來世》的科幻電影。桃的含鐵量較高,就越容
易發現事情的真相。是缺鐵性貧血病人的理想輔助食物。3、桃含鉀多,含鈉少,適合水腫
病人食用。4、桃仁有活血化淤,潤腸通作用,可用於閉經、跌打損傷等輔助治療。胃腸功
能弱者不宜吃桃、桃仁提取物有抗凝血作用,而Will也好像陷入魔怔一般。並能抑制咳嗽中
樞而止咳,擴充套件"科學來自於人性"的概念。同時能使血壓下降,片中融合了很多哲學、宗教
的玄妙概念,可用於高血壓病人的輔助治療。6、桃花有消腫、利尿之效,可用於治療浮腫
腹s水,大便乾結,小便不利和腳氣足腫。一段美好的故事才就此開始。桃子性熱,味甘
酸,具有補心、解渴、不過都十分注重核心的表達,充飢、生津的功效,父親沒有繼續在房
間埋頭工作。']
   labels = classifier.predict(li)    
   print labels

可以看到輸出的結果是positive,可以發現是錯誤的預測(正確的預測應該是negative),這個時候需要訓練模型,來達到預期的結果。在訓練的過程中,觀察result.precisionresult.recall的值變化。

可以使用Google已經訓練好的model,自己訓練模型坑太多了.

繼續訓練

classifier = fasttext.supervised("lab3fenci.csv","lab3fenci.model",
label_prefix="__label__",lr=0.1,epoch=100,dim=200,bucket=5000000)
result = classifier.test("lab3fenci.csv")
print result.precision
print result.recall

為了省事可以在上面程式碼套上for迴圈,觀察result.precisionresult.recall的值變化。

目前訓練出來的模型檔案大小是2GB,是用PC機跑的,CPU i7 3.3GHZ+16GB記憶體+SSD跑了整整3小時才出結果。

期間感受到了風扇的咆哮,CPU和記憶體都很努力地工作。

0?wx_fmt=png
0?wx_fmt=png
一般情況下磁碟的佔用是很低的,偶爾會出現佔用100%的情況,如果磁碟佔用一直是100%,要考慮記憶體是否洩露,例如文字預處理階段忘記加換行符,fasttaxt會認為一整個檔案都是一大段的文字,那麼16GB的記憶體是根本不夠儲存的,磁碟會參與記憶體交換,導致佔用100%。

訓練完成之後可以直接載入模型。

classifier = fasttext.load_model('lab3fenci.model.bin', label_prefix='__label__')

完整程式碼

# _*_coding:utf-8 _*_
import fasttext
#author linxinzhu
#load訓練好的模型
classifier = fasttext.load_model('lab3fenci.model.bin',
label_prefix='__label__')

i=0
f = open("evaluation_public.tsv", 'r')
outf = open("sub.csv",'w')
for line in f:
outline=""if i==400000:
        break
r = ""try:
r = line.decode("UTF-8")
except:
       print "charactor code error UTF-8"pass
   if r == "":
       try:
r = line.decode("GBK")
except:
           print "charactor code error GBK"pass
line=line.strip()
l_ar=line.split("\t")
id=l_ar[0]
title=l_ar[1]
content=l_ar[2]
s=""li=[]
li=list()
s="".join(content)
li=s.split("$$$$$$$$")
texts=li
labels = classifier.predict(li)
#print id
   #print labels
strlabel=str(labels)
if strlabel=="POSITIVE":
outline = id+","+ "POSITIVE"+ "\n"outf.write(outline)
if strlabel=="NEGATIVE":
outline = id+","+ "NEGATIVE"+ "\n"outf.write(outline)
i=i+1
del s
del li
f.close()
outf.close()

注意程式碼中要加上del s,實測如果不加上會產生記憶體溢位,講道理變數在沒有使用之後python應該會自動釋放記憶體,但是在大量資料面前好像不怎麼起作用,總之需要手動去釋放記憶體。

提交結果(截至2017年11月16日)

0?wx_fmt=jpeg
0?wx_fmt=jpeg
一共1000多人蔘賽,600多支隊