小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

【中文分詞系列】 6. 基于全卷積網(wǎng)絡(luò)的中文分詞

 kieojk 2017-04-14

挖掘大數(shù)據(jù),從海量信息中汲取你所不知的秘密


之前已經(jīng)寫過用LSTM來做分詞的方案了,今天再來一篇用CNN的,準確來說是FCN,全卷積網(wǎng)絡(luò)。其實這個模型的主要目的并非研究中文分詞,而是練習tensorflow。本文就是練習一下如何用tensorflow處理不定長輸入任務(wù),以中文分詞為例,并在最后加入了硬解碼,將深度學習與詞典分詞結(jié)合了起來。


1
CNN

關(guān)于FCN,放到語言任務(wù)中看,(一維)卷積其實就是ngram模型,從這個角度來看其實CNN遠比RNN來得自然,RNN好像就是為序列任務(wù)精心設(shè)計的,而CNN則是傳統(tǒng)ngram模型的一個延伸。


另外不管CNN和RNN都有權(quán)值共享,看上去只是為了降低運算量的一個折中選擇,但事實上里邊大有道理。CNN中的權(quán)值共享是平移不變性的必然結(jié)果,而不是僅僅是降低運算量的一個選擇,試想一下,將一幅圖像平移一點點,或者在一個句子前插入一個無意義的空格(導(dǎo)致后面所有字都向后平移了一位),這樣應(yīng)該給出一個相似甚至相同的結(jié)果,而這要求卷積必然是權(quán)值共享的,即權(quán)值不能跟位置有關(guān)系。


RNN類模型,尤其是LSTM,一直語言任務(wù)的霸主,但最近引入門機制的卷積GCNN據(jù)說在語言模型上已經(jīng)超過了LSTM(一點點),這說明哪怕在語言任務(wù)中CNN還是很有潛力的。


LSTM的優(yōu)勢就是能夠捕捉長距離的信息,但事實上語言任務(wù)中真正長距離的任務(wù)不多,哪怕是語言模型,事實上后一個字的概率只取決于前面幾個字罷了,不用取決于前面的全文,而CNN只要層數(shù)多一點,卷積核大一點,其實也能達到這個效果了。


但CNN還有一個特別的優(yōu)勢:CNN比RNN快多了。用顯卡加速的話,顯卡最擅長的就是作卷積了,因為顯卡本身就是用來處理圖像的,GPU對CNN的加速要比對RNN的加速明顯多了...


以上內(nèi)容,就使得我更偏愛CNN,就像facebook那個團隊一樣(那個GCNN就是他們搞出來的)。全卷積網(wǎng)絡(luò)則是從頭到尾都使用卷積,可以應(yīng)對不定長輸入,而輸入不定長、但是輸入輸出長度相等的任務(wù)就更適合了。



2
語料

本文的任務(wù)是用FCN做一個中文分詞系統(tǒng),思路還是sbme字標注法,不清楚的讀者可以看回前幾篇文章,有監(jiān)督訓練,因此需要選語料。比較好的語料有兩個,一是2014年人民日報語料,二是backoff2005比賽中的語料,后者還帶有評測系統(tǒng)。我在兩個語料中都實踐過了。


如果用2014人民日報語料,那么預(yù)處理代碼為:


import glob

import re

from tqdm import tqdm

from collections import Counter, defaultdict

import json

import numpy as np

import os

 

txt_names = glob.glob('./2014/*/*.txt')

 

pure_texts = []

pure_tags = []

stops = u',。???;、:,\.!\?;:\n'

for name in tqdm(iter(txt_names)):

    txt = open(name).read().decode('utf-8', 'ignore')

    txt = re.sub('/[a-z\d]*|\[|\]', '', txt)

    txt = [i.strip(' ') for i in re.split('['+stops+']', txt) if i.strip(' ')]

    for t in txt:

        pure_texts.append('')

        pure_tags.append('')

        for w in re.split(' +', t):

            pure_texts[-1] += w

            if len(w) == 1:

                pure_tags[-1] += 's'

            else:

                pure_tags[-1] += 'b' + 'm'*(len(w)-2) + 'e'


如果用backoff2005語料,那么預(yù)處理代碼為


import re

from tqdm import tqdm

from collections import Counter, defaultdict

import json

import numpy as np

import os

 

pure_texts = []

pure_tags = []

stops = u',。???;、:,\.!\?;:\n'

for txt in tqdm(open('msr_training.txt')):

    txt = [i.strip(' ').decode('gbk', 'ignore') for i in re.split('['+stops+']', txt) if i.strip(' ')]

    for t in txt:

        pure_texts.append('')

        pure_tags.append('')

        for w in re.split(' +', t):

            pure_texts[-1] += w

            if len(w) == 1:

                pure_tags[-1] += 's'

            else:

                pure_tags[-1] += 'b' + 'm'*(len(w)-2) + 'e'


然后將語料按照字符串長度排序,這是因為tensorflow雖然支持變長輸入,但是在訓練的時候,每個batch內(nèi)的長度要想等,因此需要做一個簡單的聚類(按長度聚類)。接著得到一個映射表,這都是很常規(guī)的:

ls = [len(i) for i in pure_texts]

ls = np.argsort(ls)[::-1]

pure_texts = [pure_texts[i] for i in ls]

pure_tags = [pure_tags[i] for i in ls]

 

min_count = 2

word_count = Counter(''.join(pure_texts))

word_count = Counter({i:j for i,j in word_count.iteritems() if j >= min_count})

word2id = defaultdict(int)

id_here = 0

for i in word_count.most_common():

    id_here += 1

    word2id[i[0]] = id_here

 

json.dump(word2id, open('word2id.json', 'w'))

vocabulary_size = len(word2id) + 1

tag2vec = {'s':[1, 0, 0, 0], 'b':[0, 1, 0, 0], 'm':[0, 0, 1, 0], 'e':[0, 0, 0, 1]}


做一個生成器,用來生成每個batch的訓練樣本。要注意的是,這里的batch_size只是一個上限,因為要求每個batch內(nèi)的句子長度都要相同,這樣子并非每個batch的size都能達到1024。

batch_size = 1024

 

def data():

    l = len(pure_texts[0])

    x = []

    y = []

    for i in range(len(pure_texts)):

        if len(pure_texts[i]) != l or len(x) == batch_size:

            yield x,y

            x = []

            y = []

            l = len(pure_texts[i])

        x.append([word2id[j] for j in pure_texts[i]])

        y.append([tag2vec[j] for j in pure_tags[i]])



3
模型

到了搭建模型的時候了,其實很簡單,就是用了三層卷積疊起來,不指定輸入長度,就設(shè)為None,設(shè)置padding = 'SAME'使得輸入輸出同樣長度(基于這個目的,也不用池化),中間用relu激活,最后用softmax激活,用交叉熵作為損失函數(shù),就完了。用tensorlfow的話,得自己寫好每個過程,但其實也沒多復(fù)雜。

import tensorflow as tf

 

embedding_size = 128

keep_prob = tf.placeholder(tf.float32)

 

embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))

x = tf.placeholder(tf.int32, shape=[None, None])

embedded = tf.nn.embedding_lookup(embeddings, x)

embedded_dropout = tf.nn.dropout(embedded, keep_prob)

W_conv1 = tf.Variable(tf.random_uniform([3, embedding_size, embedding_size], -1.0, 1.0))

b_conv1 = tf.Variable(tf.random_uniform([embedding_size], -1.0, 1.0))

y_conv1 = tf.nn.relu(tf.nn.conv1d(embedded_dropout, W_conv1, stride=1, padding='SAME') + b_conv1)

W_conv2 = tf.Variable(tf.random_uniform([3, embedding_size, embedding_size/4], -1.0, 1.0))

b_conv2 = tf.Variable(tf.random_uniform([embedding_size/4], -1.0, 1.0))

y_conv2 = tf.nn.relu(tf.nn.conv1d(y_conv1, W_conv2, stride=1, padding='SAME') + b_conv2)

W_conv3 = tf.Variable(tf.random_uniform([3, embedding_size/4, 4], -1.0, 1.0))

b_conv3 = tf.Variable(tf.random_uniform([4], -1.0, 1.0))

y = tf.nn.softmax(tf.nn.conv1d(y_conv2, W_conv3, stride=1, padding='SAME') + b_conv3)

 

y_ = tf.placeholder(tf.float32, shape=[None, None, 4])

cross_entropy = - tf.reduce_sum(y_ * tf.log(y + 1e-20))

train_step = tf.train.AdamOptimizer().minimize(cross_entropy)

correct_prediction = tf.equal(tf.argmax(y, 2), tf.argmax(y_, 2))

accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))


以上就是模型的全部了,然后訓練。再次給大家推薦一下,用tqdm來輔助顯示進度(實時顯示進度、速度、精度),簡直是絕配啊。

init = tf.global_variables_initializer()

sess = tf.Session()

sess.run(init)

nb_epoch = 300

 

for i in range(nb_epoch):

    d = tqdm(data(), desc=u'Epcho %s, Accuracy: 0.0'%(i+1))

    k = 0

    accs = []

    for xxx,yyy in d:

        k += 1

        if k%100 == 0:

            acc = sess.run(accuracy, feed_dict={x: xxx, y_: yyy, keep_prob:1})

            accs.append(acc)

            d.set_description('Epcho %s, Accuracy: %s'%(i+1, acc))

        sess.run(train_step, feed_dict={x: xxx, y_: yyy, keep_prob:0.5})

    print u'Epcho %s Mean Accuracy: %s'%(i+1, np.mean(accs))

 

saver = tf.train.Saver()

saver.save(sess, './ckpt/lm.ckpt')

訓練過程輸出(這是用macbook的cpu訓練的,用gtx1060加速只需要3s一個epcho)


Epcho 1, Accuracy: 0.717359: 347it [01:06, 5.21it/s]

Epcho 1 Mean Accuracy: 0.56555

Epcho 2, Accuracy: 0.759943: 347it [01:08, 8.62it/s]

Epcho 2 Mean Accuracy: 0.74762

Epcho 3, Accuracy: 0.598692: 347it [01:08, 5.08it/s]

Epcho 3 Mean Accuracy: 0.693505

Epcho 4, Accuracy: 0.634529: 347it [01:07, 5.14it/s]

Epcho 4 Mean Accuracy: 0.613064

Epcho 5, Accuracy: 0.659949: 347it [01:07, 5.16it/s]

Epcho 5 Mean Accuracy: 0.643388

Epcho 6, Accuracy: 0.709635: 347it [01:07, 5.14it/s]

Epcho 6 Mean Accuracy: 0.679544

Epcho 7, Accuracy: 0.742839: 271it [00:42, 2.45it/s]



4
硬解碼

訓練完之后,剩下的就是預(yù)測、標注、分詞了,這都是很基本的。最后可以在backoff2005的評測集上達到93%的準確率(backoff2005提供的score腳本算出的準確率),不算最優(yōu),但夠了,主要還是下面的調(diào)整。


但眾所周知,基于字標注法的分詞,需要標簽語料訓練,訓練完之后,就適應(yīng)那一批語料了,比較難拓展到新領(lǐng)域;又或者說,如果發(fā)現(xiàn)有分錯的地方,則沒法很快調(diào)整過來。而基于詞表的方法則容易調(diào)整,只需要增減詞典或者調(diào)整詞頻即可。這樣可以考慮怎么將深度學習與詞典結(jié)合起來,這里簡單地在最后的解碼階段加入硬解碼(人工干預(yù)解碼)。


模型預(yù)測可以得到各個標簽的概率,接下來是用viterbi算法得到最優(yōu)路徑,但是在viterbi之前,可以利用詞表對各個標簽的概率進行調(diào)整。這里的做法是:添加一個add_dict.txt文件,每一行是一個詞,包括詞語和倍數(shù),這個倍數(shù)就是要將相應(yīng)的標簽概率擴大的倍數(shù),比如詞表中指定詞語“科學空間,10”,而對“科學空間挺好”進行分詞時,先用模型得到這六個字的標簽概率,然后查找發(fā)現(xiàn)“科學空間”這個詞在這個句子里邊,所以將第一個字為s的概率乘以10,將第二、三個字為m的概率乘以10,將第4個字為e的概率乘以10(不用歸一化,因為只看相對值就行了),同樣地,如果某些地方切漏了(該切的沒有切),也可以加入到詞表中,然后設(shè)置小于1的倍數(shù)就行了。


效果:

加入詞典前:掃描 二維碼 , 關(guān)注 微 信號 。

(加入詞典:微信號,10)加入詞典后:掃描 二維碼 , 關(guān)注 微信號 。


當然,這只是一個經(jīng)驗方法。后面部分代碼如下,由于這里只是演示效果,用了正則表達式遍歷查找,如果追求效率,應(yīng)當用AC自動機等多模式匹配工具:

trans_proba = {'ss':1, 'sb':1, 'bm':1, 'be':1, 'mm':1, 'me':1, 'es':1, 'eb':1}

trans_proba = {i:np.log(j) for i,j in trans_proba.iteritems()}

 

add_dict = {}

if os.path.exists('add_dict.txt'):

    with open('add_dict.txt') as f:

        for l in f:

            a,b = l.split(',')

            add_dict[a.decode('utf-8')] = np.log(float(b))

 

 

def viterbi(nodes):

    paths = nodes[0]

    for l in range(1,len(nodes)):

        paths_ = paths.copy()

        paths = {}

        for i in nodes[l].keys():

            nows = {}

            for j in paths_.keys():

                if j[-1]+i in trans_proba.keys():

                    nows[j+i]= paths_[j]+nodes[l][i]+trans_proba[j[-1]+i]

            k = np.argmax(nows.values())

            paths[nows.keys()[k]] = nows.values()[k]

    return paths.keys()[np.argmax(paths.values())]

 

 

def simple_cut(s):

    if s:

        nodes = [dict(zip('sbme', k)) for k in sess.run(y, feed_dict={x:[[word2id[i] for i in s]], keep_prob:1})[0]]

        for w,f in add_dict.iteritems():

            for i in re.finditer(w, s):

                if len(w) == 1:

                    nodes[i.start()]['s'] += f

                else:

                    nodes[i.start()]['b'] += f

                    nodes[i.end()-1]['e'] += f

                    for j in range(i.start()+1, i.end()-1):

                        nodes[j]['m'] *= f

        tags = viterbi(nodes)

        words = [s[0]]

        for i in range(1, len(s)):

            if tags[i] in ['s', 'b']:

                words.append(s[i])

            else:

                words[-1] += s[i]

        return words

    else:

        return []

 

def cut_words(s):

    i = 0

    r = []

    for j in re.finditer('['+stops+' ]'+'|[a-zA-Z\d]+', s):

        r.extend(simple_cut(s[i:j.start()]))

        r.append(s[j.start():j.end()])

        i = j.end()

    if i != len(s):

        r.extend(simple_cut(s[i:]))

    return r



    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多