1. 程式人生 > >NLP漢語自然語言處理原理與實踐 9 NLP中的深度學習

NLP漢語自然語言處理原理與實踐 9 NLP中的深度學習

9.2 Word2Vec簡介

     http://004123.ichengyun.net/thread-1598-1-1.html

     下載word2vec原始碼

     使用MSR分詞語料庫http://www.threedweb.cn/thread-1593-1-1.html

    ./word2vec -train msr.txt -output vectors.bin -cbow 0 -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -threads 12 -binary 1

    ./distance vectors.bin

    可使用更大規模的語料

    也可使用詞向量模型資料http://txt2vec.codeplex.com/下載二進位制包

    需要使用https://github.com/zhongkaifu/RNNSharp解析,這是一個基於CRF-LSTM的高精度命名實體識別工具。

大規模上下位關係的自動識別

   人工構建知識庫是一種知識密集且費時的工作。然而,類似HowNet和WordNet這些語義資源卻在實際使用中都極大地受限於它們覆蓋的領域,效果並不好。因此,許多研究人員多年來都不斷嘗試自動提取語義關係或構建分類法。


    由哈工大的劉挺為首的《大詞林》(http://www.bigcilin.com/browser/)專案組最終解決上述問題。經過大量的研究,他們觀察到,詞嵌入通過捕捉大量的句法/語義關係,保留了很多語言規律。一個著名的例子:v(king>-v(queen) 約等於 v(man) - v(woman),表明詞嵌入向量的差值確實代表兩個詞對之間的某種語義關係。

     是否這種情況也適用於上下位關係。他們設計了一個簡單的實驗,使用一些隨機抽取的漢語中的上下位詞對,計算嵌入向量的差值以測量它們之間的相似性。

    

    為了應對這一挑戰,我們提出了使用投影矩陣學習的上下文義關係。

    1、均勻線性對映:假設所有的詞可以基於統一的轉移矩陣被對映到其上位詞。y=Qx

    2、分段線性對映:先聚類再對映

    3、識別上下位關係:他們設計了三個模型


9.3 NLP與RNN

     一個神經網路如果能夠應用於NLP,則必須能夠解決序列標註的問題。而RNN通過使用反向傳播和記憶的機制,能夠處理任意長度的序列,在架構上比前饋神經網路更加符合生物神經網路的結構。因此,其正是為了解決這類問題應運而生的。

    RNN和LSTM從原理來看,它們都源於認知語言學中的“順序相似性”原理:文字元號與其上下文構成了一個“像”,這個“像”可以被認為是符號與符號的組合----詞彙,也可以被認為是詞彙與詞彙的句法關係----依存關係。演算法的訓練過程,是通過正向和反饋兩個過程從訓練語料中學習出識別這些“像”的能力,並記錄下識別“像”的模型資料,當輸入新的句子時,演算法可以利用儲存的模型資料識別出新輸入中類似的“像”。 

     序列標註任務使用的是“many to many”方式。

RNN由於能夠接受序列輸入,也能得到序列輸出,在NLP中取得了巨大成功,並得到廣泛應用。

     RNN是簡單的迴圈神經網路,稱為Simple-RNN:迴圈可以使得資訊從當前時間步傳遞到下一時間步。

     www.threedweb.cn/thread-1595-1-1.html

     雖然Simple-RNN實現了迴圈網路的最初雛形,但是在實際應用中,使用得並不多,主要原因有如下兩個:

  • 因為網路是根據輸入而展開的,輸入越長,展開的網路就越深,對於”深度“網路訓練的困難最常見的是”梯度爆炸“和梯度消失的問題,這在Yoshua Bengio的論文On the difficulty of training recurrent neural networks有詳細的說明,這裡不再說明。
  • Simple-RNN善於基於先前的詞來預測下一個詞,但在一些更加複雜的場景中,例如,”我出生在法國......我能講一口流利的法語。”,“法國”和“法語”則需要更長時間的預測,而當上下文之間的間隔不斷增大時,Simple-RNN會喪失學習到連線如此遠的資訊的能力。

LSTM:是RNN一個變種,專門用於解決Simple-RNN的上述兩個問題,Hocheriter & Schmidhuber早在1997年就提出了LSTM網路,後來Alex Graves對其進行了改良和推廣。在NLP問題上,LSTM都取得了巨大的成功,並得到了廣泛的使用。

      LSTM通過對迴圈層的刻意設計來避免長期依賴和梯度消失等問題。長期資訊的記憶在LSTM中是預設行為,而無須付出代價即可獲得此能力。

      內部結構http://colah.github.io/posts/2015-08-Understanding-LSTMs/    

9.4 深度學習框架與應用

      Keras框架介紹

      兩種常用的網路架構如下:

1)堆疊的LSTM網路架構

   有狀態迴圈模型(LSTM)將前一批樣本生成的內部狀態(記憶)儲存,在下一批重用。它能處理更長的序列,並降低計算複雜性



      2、Keras序列標註:中文分詞的例子

seqlib.py

# -*- coding: utf-8 -*-
import os
import sys
import numpy as np
from numpy import *
import nltk
import codecs
import pandas as pd
from nltk.probability import FreqDist
from gensim.models import word2vec
from cPickle import load, dump
from keras.preprocessing import sequence
from keras.optimizers import SGD, RMSprop, Adagrad
from keras.utils import np_utils
from keras.models import Sequential #, Graph
from keras.layers.core import Dense, Dropout, Activation #, TimeDistributedDense
from keras.layers.embeddings import Embedding 
from keras.layers.recurrent import LSTM, GRU, SimpleRNN
from keras.layers.core import Reshape, Flatten, Dropout
from keras.regularizers import l1, l2
from keras.layers.convolutional import Convolution2D, MaxPooling2D, MaxPooling1D
from sklearn.cross_validation import train_test_split

#讀單個文字
def load_file(input_file):
	input_data = codecs.open(input_file, 'r', 'utf-8')
	input_text = input_data.read()
	return input_text

#我們使用gensim的word2vec庫
def trainW2V(corpus, epochs=20, num_features = 100, sg=1, 
	min_word_count = 1, num_workers=4,
	context=4, sample=1e-5, negative=5):
	w2v = word2vec.Word2Vec(workers=num_workers, sample=sample, 
		size=num_features, min_count=min_word_count, window=context)
	np.random.shuffle(corpus)
	w2v.build_vocab(corpus)
	w2v.train(corpus, total_examples=w2v.corpus_count, epochs=epochs)
	'''
	for epoch in range(epochs):
		print('epoch' + str(epoch))
		np.random.shuffle(corpus)
		w2v.train(corpus, total_examples=w2v.corpus_count, epochs=epochs)
		w2v.alpha *= 0.9
		w2v.min_alpha = w2v.alpha
	'''
	print("word2vec DONE.")
	return w2v

# nltk 輸入文字,輸出詞頻表
def freq_func(input_txt):
	corpus = nltk.Text(input_txt)
	fdist = FreqDist(corpus)
	w = fdist.keys()
	v = fdist.values()
	#print(w[:10], v[:10])
	freqdf = pd.DataFrame({'word':w, 'freq':v})
	freqdf.sort_values('freq', ascending=False, inplace=True)
	freqdf['idx'] = np.arange(len(v))
	#print(freqdf)
	return freqdf

# 初始化權重
def initweightlist(w2v, idx2word, word2idx):
	init_weight_wv = []
	for i in range(len(idx2word)):
		init_weight_wv.append(w2v[idx2word[i]])
	# 定義'U'為未登入新字,‘P’為兩頭padding用途,並增加兩個相應的向量表示
	char_num = len(init_weight_wv)
	idx2word[char_num] = u'U'
	word2idx[u'U'] = char_num
	idx2word[char_num+1] = u'P'
	word2idx[u'P'] = char_num+1
	init_weight_wv.append(np.random.randn(100, ))
	init_weight_wv.append(np.zeros(100, ))
	return init_weight_wv, idx2word, word2idx

# 加入標註標籤: SBME
def character_tagging(input_file, output_file):
	input_data = codecs.open(input_file, 'r', 'utf-8')
	output_data = codecs.open(output_file, 'w', 'utf-8')
	for line in input_data.readlines():
		word_list = line.strip().split()
		for word in word_list:
			if len(word) == 1:
				output_data.write(word + "/S ")
			else:
				output_data.write(word[0] + "/B ")
				for w in word[1: len(word) -1]:
					output_data.write(w + "/M ")
				output_data.write(word[len(word)-1] + "/E ")
		output_data.write("\n")
	input_data.close()
	output_data.close()

def featContext(sentence, word2idx = '', context=7):
	predict_word_num = []
	# 文字中的字典如果在詞典中則轉為數字,如果不在則設定為'U'
	for w in sentence:
		if w in word2idx:
			predict_word_num.append(word2idx[w])
		else:
			predict_word_num.append(word2idx[u'U'])
	num = len(predict_word_num)   # 首尾 padding
	pad = int( (context-1) * 0.5 )
	for i in range(pad):
		predict_word_num.insert(0, word2idx[u'P'])
		predict_word_num.append(word2idx[u'P'])
	train_x = []
	for i in range(num):
		train_x.append(predict_word_num[i: i+context])
	return train_x

# 訓練語料
class Lstm_Net(object):
	def __init__(self):
		self.init_weight = []
		self.batch_size = 128
		self.word_dim = 100
		self.maxlen = 7
		self.hidden_units = 100
		self.nb_classes = 0

	def buildnet(self):
		self.maxfeatures = self.init_weight[0].shape[0]   #詞典大小
		self.model = Sequential()
		print 'stacking LSTM'   #使用了堆疊的LSTM架構
		self.model.add(Embedding(self.maxfeatures, self.word_dim, input_length=self.maxlen))
		self.model.add(LSTM(output_dim=self.hidden_units, return_sequences=True))
		self.model.add(LSTM(output_dim=self.hidden_units, return_sequences=False))
		self.model.add(Dropout(0.5))
		self.model.add(Dense(self.nb_classes))
		self.model.add(Activation('softmax'))
		self.model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy'])

	def train(self, modelname):
		result = self.model.fit(self.train_X, self.Y_train, 
			batch_size=self.batch_size, epochs=20, 
			validation_data=(self.test_X, self.Y_test), show_accuracy=True)
		self.model.save_weights(modelname)

	def splitset(self, train_word_num, train_label, train_size=0.9, random_state=1):
		self.train_X, self.test_X, train_y, test_y = train_test_split(train_word_num, train_label, train_size=0.9, random_state=1)
		self.Y_train = np_utils.to_categorical(train_y, self.nb_classes)
		self.Y_test = np_utils.to_categorical(test_y, self.nb_classes)

	# 根據輸入得到標註推斷
	def predict_num(self, input_num, input_txt, label_dict='', num_dict=''):
		input_num = np.array(input_num)
		predict_prob = self.model.predict_proba(input_num, verbose=False)
		predict_lable = self.model.predict_classes(input_num, verbose=False)
		for i, lable in enumerate(predict_lable[:-1]):
			if i == 0:   # 如果是首字,不可為E, M
				predict_prob[i, label_dict[u'E']] = 0
				predict_prob[i, label_dict[u'M']] = 0
			if lable == label_dict[u'B']:   # 前字為B,後字不可為B,S
				predict_prob[i+1, label_dict[u'B']] = 0
				predict_prob[i+1, label_dict[u'S']] = 0
			if lable == label_dict[u'E']:   # 前字為E,後字不可為M,E
				predict_prob[i+1, label_dict[u'M']] = 0
				predict_prob[i+1, label_dict[u'E']] = 0
			if lable == label_dict[u'M']:   # 前字為M,後字不可為B,S
				predict_prob[i+1, label_dict[u'B']] = 0
				predict_prob[i+1, label_dict[u'S']] = 0
			if lable == label_dict[u'S']:   # 前字為S,後字不可為M,E
				predict_prob[i+1, label_dict[u'M']] = 0
				predict_prob[i+1, label_dict[u'E']] = 0
			predict_lable[i+1] = predict_prob[i+1].argmax()
		predict_lable_new = [num_dict[x] for x in predict_lable]
		result = [w+'/' + l for w, l in zip(input_txt, predict_lable_new)]
		return ' '.join(result) + '\n'

	def getweights(self, wfname):
		return self.model.load_weights(wfname)
corpus2vector.py
# -*- coding: utf-8 -*-
from seqlib import *
reload(sys)
sys.setdefaultencoding('utf-8')

corpuspath = "msr.txt"
input_text = load_file(corpuspath)

# word2vec是一個二維陣列
txtwv = [line.split() for line in input_text.split('\n') if line != '']
#print(txtwv[0].encode('utf8'))
# 
w2v = trainW2V(txtwv)
w2v.save("wordvector.bin")
preprocess.py 執行結果
# -*- coding: utf-8 -*-
from seqlib import *

corpuspath = "msr.txt"
input_text = load_file(corpuspath)

#計算詞頻
txtnltk = [w for w in input_text.split()] # 為計算詞頻準備的文字格式
freqdf = freq_func(txtnltk) # 計算詞頻表

# 建立兩個對映詞典
#print(freqdf)
word2idx = dict( (c, i) for c, i in zip(freqdf.word, freqdf.idx) )
idx2word = dict( (i, c) for c, i in zip(freqdf.word, freqdf.idx) )
w2v = word2vec.Word2Vec.load("wordvector.bin")
#print(idx2word[0])
#print(w2v[idx2word[0]])

#初始化向量
init_weight_wv, idx2word, word2idx = initweightlist(w2v, idx2word, word2idx)
dump(word2idx, open('word2idx.pickle', 'wb'))
dump(idx2word, open('idx2word.pickle', 'wb'))
dump(init_weight_wv, open('init_weight_wv.pickle', 'wb'))

#讀取資料,將格式進行轉換為帶4種標籤 S B M E
output_file = 'msr.tagging.utf8'
character_tagging(corpuspath, output_file)

# 分離 word 和 label
with open(output_file) as f:
	lines = f.readlines()
	train_line = [[w[0] for w in line.decode('utf-8').split() ] for line in lines]
	train_label = [ w[2] for line in lines for w in line.decode('utf-8').split() ]

# 將所有訓練文字轉成數字list
train_word_num = []
for line in train_line:
	train_word_num.extend(featContext(line, word2idx))

# 持久化
dump(train_word_num, open('train_word_num.pickle', 'wb'))
dump(train_label, open('train_label.pickle', 'wb'))
segment_lstm.py
# -*- coding: utf-8 -*-
from seqlib import *

train_word_num = load(open('train_word_num.pickle', 'rb'))
train_label = load(open('train_label.pickle', 'rb'))
nb_classes = len(np.unique(train_label))
print(nb_classes)   # 4

# 初始字向量格式準備
init_weight_wv = load(open('init_weight_wv.pickle', 'rb'))
print shape(init_weight_wv)    # (68947, 100)

# 建立兩個詞典
label_dict = dict(zip(np.unique(train_label), range(4)))
num_dict = {n: l for l, n in label_dict.iteritems()}

# 將目標變數轉為數字
train_label = [ label_dict[y] for y in train_label ]
print shape(train_label)   # (3763026,)

train_word_num = np.array(train_word_num)
print shape(train_word_num)  # (3763026, 7)

# stacking LSTM
modelname = 'my_model_weights.h5'
net = Lstm_Net()
net.init_weight = [np.array(init_weight_wv)]
net.nb_classes = nb_classes
net.splitset(train_word_num, train_label)
print "Train..."
net.buildnet()
net.train(modelname)
執行分詞:
# -*- coding: utf-8 -*-
from seqlib import *
word2idx = load(open('word2idx.pickle', 'rb'))
train_word_num = load(open('train_word_num.pickle', 'rb'))
train_label = load(open('train_label.pickle', 'rb'))
nb_classes = len(np.unique(train_label))
init_weight_wv = load(open('init_weight_wv.pickle', 'rb'))

# 建立兩個詞典
label_dict = dict(zip(np.unique(train_label), range(4)))
num_dict = {n: l for l, n in label_dict.iteritems()}

temp_txt = u'羅馬尼亞的首都是布加勒斯特。'
temp_txt = list(temp_txt)
temp_num = featContext(temp_txt, word2idx = word2idx)

net = Lstm_Net()
net.init_weight = [np.array(init_weight_wv)]
net.nb_classes = nb_classes
net.buildnet()
net.getweights('my_model_weights.h5')
temp = net.predict_num(temp_num, temp_txt, label_dict=label_dict, num_dict=num_dict)
print(temp)
實現了98.71%的精度

9.4.3依存句法的演算法原理

   由於最新的依存句法分析用到了DP的思想