1. 程式人生 > >theano學習指南--詞向量的迴圈神經網路(翻譯)

theano學習指南--詞向量的迴圈神經網路(翻譯)

最近在學習Git,所以正好趁這個機會,把學習到的知識實踐一下~ 看完DeepLearning的原理,有了大體的瞭解,但是對於theano的程式碼,還是自己擼一遍印象更深 所以照著deeplearning.net上的程式碼,重新寫了一遍,註釋部分是原文翻譯和自己的理解。 感興趣的小夥伴可以一起完成這個工作哦~ 有問題歡迎聯絡我 Email: [email protected] QQ: 3062984605

概述

本教程中,你將會學到:

  • 詞向量
  • 迴圈神經網路架構
  • 文字視窗

從而實現Semantic Parsing / Slot-Filling(自然語言的理解)。

程式碼—引用—聯絡方式

程式碼

論文

如果使用本教程,請引用下列文獻:

  • Grégoire Mesnil, Xiaodong He, Li Deng and Yoshua Bengio. Investigation of Recurrent-Neural-Network Architectures and Learning Methods for Spoken Language Understanding. Interspeech, 2013.
  • Gokhan Tur, Dilek Hakkani-Tur and Larry Heck. What is left to be understood in ATIS?
  • Christian Raymond and Giuseppe Riccardi. Generative and discriminative algorithms for spoken language understanding. Interspeech, 2007.
  • Christian Raymond and Giuseppe Riccardi. Generative and discriminative algorithms for spoken language understanding. Interspeech, 2007.
  • Bergstra, James, Breuleux, Olivier, Bastien, Frédéric, Lamblin, Pascal, Pascanu, Razvan, Desjardins, Guillaume, Turian, Joseph, Warde-Farley, David, and Bengio, Yoshua. Theano: a CPU and GPU math expression compiler. In Proceedings of the Python for Scientific Computing Conference (SciPy), June 2010.

謝謝!

聯絡方式

有問題請聯絡 Grégoire Mesnil (first-add-a-dot-last-add-at-gmail-add-a-dot-com)。我們很樂意收到您的反饋。

任務

Slot-Filling (Spoken Language Understanding)是對給定的句子中每個單詞標定標籤。這是一個分類問題。

資料集

資料集是DARPA的一個小型資料集:ATIS (Airline Travel Information System)。這裡的語句例子使用Inside Outside Beginning (IOB)表示 。

input(words) show flights from Boston to New York today
Output(labels) O O O B-dept O B-arr I-arr B-date

ATIS 包含單詞4978個,句子893個,測試集合包含單詞56590個,句子9198個(平均句子長度為15)。類的數量(不同的slots)為128,其中包括O標籤(NULL)。
在論文 Microsoft Research people,對於只出現一次的單詞,標記為,運用同樣的方法標記未出現的單詞。在論文Ronan Collobert and colleagues中,用數字表示字串,例如1984表示DIGITDIGITDIGITDIGIT。
我們將訓練集合分為訓練集和驗證集,分別包含80%和20%的訓練語句。 Significant performance improvement difference has to be greater than 0.6% in F1 measure at the 95% level due to the small size of the dataset。為了驗證效果,實驗中定義了三個矩陣:

迴圈神經網路模型

原始輸入編碼

一個token對應一個單詞。ATIS中詞彙表對應的每個token都有相應的索引。每個語句是索引的陣列(int32)。其次,每個集合(訓練集、驗證集、測試集)是索引陣列的列表。定義python字典將索引對映到單詞。

>>> sentence
array([383, 189,  13, 193, 208, 307, 195, 502, 260, 539,
        7,  60,  72, 8, 350, 384], dtype=int32)
>>> map(lambda x: index2word[x], sentence)
['please', 'find', 'a', 'flight', 'from', 'miami', 'florida',
        'to', 'las', 'vegas', '<UNK>', 'arriving', 'before', 'DIGIT', "o'clock", 'pm']

對於標籤,採用同樣的方法:

>>> labels
array([126, 126, 126, 126, 126,  48,  50, 126,  78, 123,  81, 126,  15,
        14,  89,  89], dtype=int32)
>>> map(lambda x: index2label[x], labels)
['O', 'O', 'O', 'O', 'O', 'B-fromloc.city_name', 'B-fromloc.state_name',
        'O', 'B-toloc.city_name', 'I-toloc.city_name', 'B-toloc.state_name',
        'O', 'B-arrive_time.time_relative', 'B-arrive_time.time',
        'I-arrive_time.time', 'I-arrive_time.time']

文字窗

給定語句:索引的陣列,視窗大小:1,3,5,…。現在需要將語句中每個詞根據文字窗選定該詞周圍的詞。具體實現如下:

def contextwin(l, win):
    '''
    win :: int corresponding to the size of the window
    given a list of indexes composing a sentence

    l :: array containing the word indexes

    it will return a list of list of indexes corresponding
    to context windows surrounding each word in the sentence
    '''
    assert (win % 2) == 1
    assert win >= 1
    l = list(l)

    lpadded = win // 2 * [-1] + l + win // 2 * [-1]
    out = [lpadded[i:(i + win)] for i in range(len(l))]

    assert len(out) == len(l)
    return out

PADDING索引中的-1插在語句的開始/結束位置。
例子如下:

>>> x
array([0, 1, 2, 3, 4], dtype=int32)
>>> contextwin(x, 3)
[[-1, 0, 1],
 [ 0, 1, 2],
 [ 1, 2, 3],
 [ 2, 3, 4],
 [ 3, 4,-1]]
>>> contextwin(x, 7)
[[-1, -1, -1, 0, 1, 2, 3],
 [-1, -1,  0, 1, 2, 3, 4],
 [-1,  0,  1, 2, 3, 4,-1],
 [ 0,  1,  2, 3, 4,-1,-1],
 [ 1,  2,  3, 4,-1,-1,-1]]

總的來說,輸入為一個索引的陣列,輸出為索引的矩陣。每行是指定單詞的文字窗。

詞向量

將語句轉換成文字窗:索引的矩陣,下一步需要將索引轉換為詞向量。使用Theano。程式碼如下:

import theano, numpy
from theano import tensor as T

# nv :: size of our vocabulary
# de :: dimension of the embedding space
# cs :: context window size
nv, de, cs = 1000, 50, 5

embeddings = theano.shared(0.2 * numpy.random.uniform(-1.0, 1.0, \
    (nv+1, de)).astype(theano.config.floatX)) # add one for PADDING at the end

idxs = T.imatrix() # as many columns as words in the context window and as many lines as words in the sentence
x    = self.emb[idxs].reshape((idxs.shape[0], de*cs))

符號變數x表示矩陣的維度(語句中單詞數量,文字窗的長度)。
下面開始編譯theano函式:

>>> sample
array([0, 1, 2, 3, 4], dtype=int32)
>>> csample = contextwin(sample, 7)
[[-1, -1, -1, 0, 1, 2, 3],
 [-1, -1,  0, 1, 2, 3, 4],
 [-1,  0,  1, 2, 3, 4,-1],
 [ 0,  1,  2, 3, 4,-1,-1],
 [ 1,  2,  3, 4,-1,-1,-1]]
>>> f = theano.function(inputs=[idxs], outputs=x)
>>> f(csample)
array([[-0.08088442,  0.08458307,  0.05064092, ...,  0.06876887,
        -0.06648078, -0.15192257],
       [-0.08088442,  0.08458307,  0.05064092, ...,  0.11192625,
         0.08745284,  0.04381778],
       [-0.08088442,  0.08458307,  0.05064092, ..., -0.00937143,
         0.10804889,  0.1247109 ],
       [ 0.11038255, -0.10563177, -0.18760249, ..., -0.00937143,
         0.10804889,  0.1247109 ],
       [ 0.18738101,  0.14727569, -0.069544  , ..., -0.00937143,
         0.10804889,  0.1247109 ]], dtype=float32)
>>> f(csample).shape
(5, 350)

我們現在得到了文字窗詞向量的一個序列(長度為5,表示語句長度),該詞向量非常適用迴圈神經網路。

Elman迴圈神經網路

Elman迴圈神經網路(E-RNN)的輸入為當前輸入(t時刻)和之前隱層狀態(t-1時刻)。然後重複該步驟。
在之前章節中,我們構造輸入為時序結構。在上述矩陣中,第0行表示t=0時刻,第1行表示t=1時刻,如此等等。
E-RNN中需要學習的引數如下:

  • 詞向量(真實值矩陣)
  • 初始隱藏狀態(真實值向量)
  • 作用於線性過程的t時刻輸入和t-1時刻隱層狀態的兩個矩陣
  • (優化)偏置。建議:不使用
  • 頂層的softmax分類器

整個網路的超引數如下:

  • 詞向量的維度
  • 詞彙表的數量
  • 隱層單元的數量
  • 類的數量
  • 用於初始化模型的隨機種子

程式碼如下:

class RNNSLU(object):
    ''' elman neural net model '''
    def __init__(self, nh, nc, ne, de, cs):
        '''
        nh :: dimension of the hidden layer
        nc :: number of classes
        ne :: number of word embeddings in the vocabulary
        de :: dimension of the word embeddings
        cs :: word window context size
        '''
        # parameters of the model
        self.emb = theano.shared(name='embeddings',
                                 value=0.2 * numpy.random.uniform(-1.0, 1.0,
                                 (ne+1, de))
                                 # add one for padding at the end
                                 .astype(theano.config.floatX))
        self.wx = theano.shared(name='wx',
                                value=0.2 * numpy.random.uniform(-1.0, 1.0,
                                (de * cs, nh))
                                .astype(theano.config.floatX))
        self.wh = theano.shared(name='wh',
                                value=0.2 * numpy.random.uniform(-1.0, 1.0,
                                (nh, nh))
                                .astype(theano.config.floatX))
        self.w = theano.shared(name='w',
                               value=0.2 * numpy.random.uniform(-1.0, 1.0,
                               (nh, nc))
                               .astype(theano.config.floatX))
        self.bh = theano.shared(name='bh',
                                value=numpy.zeros(nh,
                                dtype=theano.config.floatX))
        self.b = theano.shared(name='b',
                               value=numpy.zeros(nc,
                               dtype=theano.config.floatX))
        self.h0 = theano.shared(name='h0',
                                value=numpy.zeros(nh,
                                dtype=theano.config.floatX))

        # bundle
        self.params = [self.emb, self.wx, self.wh, self.w,
                       self.bh, self.b, self.h0]

以下程式碼構造詞矩陣的輸入:

 idxs = T.imatrix()
        x = self.emb[idxs].reshape((idxs.shape[0], de*cs))
        y_sentence = T.ivector('y_sentence')  # labels

呼叫scan函式實現遞迴,效果很神奇:

def recurrence(x_t, h_tm1):
            h_t = T.nnet.sigmoid(T.dot(x_t, self.wx)
                                 + T.dot(h_tm1, self.wh) + self.bh)
            s_t = T.nnet.softmax(T.dot(h_t, self.w) + self.b)
            return [h_t, s_t]

        [h, s], _ = theano.scan(fn=recurrence,
                                sequences=x,
                                outputs_info=[self.h0, None],
                                n_steps=x.shape[0])

        p_y_given_x_sentence = s[:, 0, :]
        y_pred = T.argmax(p_y_given_x_sentence, axis=1)

Theano會自動的計算所有梯度用於最大最小化似然概率:

lr = T.scalar('lr')

sentence_nll = -T.mean(T.log(p_y_given_x_sentence)
                               [T.arange(x.shape[0]), y_sentence])
sentence_gradients = T.grad(sentence_nll, self.params)
sentence_updates = OrderedDict((p, p - lr*g)
                                       for p, g in
                                       zip(self.params, sentence_gradients))

然後編譯函式:

self.classify = theano.function(inputs=[idxs], outputs=y_pred)
self.sentence_train = theano.function(inputs=[idxs, y_sentence, lr],
                                              outputs=sentence_nll,
                                              updates=sentence_updates)

在每次更新之後,需要將詞向量正則化:

        self.normalize = theano.function(inputs=[],
                                         updates={self.emb:
                                                  self.emb /
                                                  T.sqrt((self.emb**2)
                                                  .sum(axis=1))
                                                  .dimshuffle(0, 'x')})

這就是所有的工作!

評估

根據之前定義的函式,你可以比較預測標籤和真實標籤,並計算相關矩陣。在這個github倉庫,封裝了conlleval文字。計算關於Inside Outside Beginning (IOB)的矩陣是十分必要的。如果詞起始、詞中間、詞末端預測都是正確的,那麼就認為該預測是正確的。需要注意的是,文字字尾是txt,而計算過程中需要將其轉換為pl。

訓練

更新

對於隨機梯度下降法(SGD)的更新,我們將整句作為一個mini-batch,並對每句執行一次更新。對於純SGD(不同於mini-batch),每個單詞執行一次更新。
每次迴圈/更新之後,需要正則化詞向量,保證它們有統一的單位。

停止引用

在驗證集上提前結束是一種常規技術:訓練集執行一定的代數,每代在驗證集上計算F1得分,並保留最好的模型。

超引數選擇

儘管已經有關於超引數選擇的研究/程式碼,這裡我們使用KISS隨機搜尋。
以下引數是一些建議值:

  • 學習率:uniform([0.05,0.01])
  • 視窗大小:集合{3,…,19}的隨機數
  • 隱層單元數量:{100,200}之間的隨機數
  • 詞向量維度:{50,100}之間的隨機數

執行程式

使用download.sh命令下載資料檔案後,可以呼叫以下命令執行程式:

python code/rnnslu.py

('NEW BEST: epoch', 25, 'valid F1', 96.84, 'best test F1', 93.79)
[learning] epoch 26 >> 100.00% completed in 28.76 (sec) <<
[learning] epoch 27 >> 100.00% completed in 28.76 (sec) <<
...
('BEST RESULT: epoch', 57, 'valid F1', 97.23, 'best test F1', 94.2, 'with the model', 'rnnslu')

時間

使用github倉庫中的程式碼測試ATIS資料集,每代少於40秒。實驗平臺為:n i7 CPU 950 @ 3.07GHz using less than 200 Mo of RAM。

[learning] epoch 0 >> 100.00% completed in 34.48 (sec) <<

進行若干代之後,F1得分下降為94.48% 。

NEW BEST: epoch 28 valid F1 96.61 best test F1 94.19
NEW BEST: epoch 29 valid F1 96.63 best test F1 94.42
[learning] epoch 30 >> 100.00% completed in 35.04 (sec) <<
[learning] epoch 31 >> 100.00% completed in 34.80 (sec) <<
[...]
NEW BEST: epoch 40 valid F1 97.25 best test F1 94.34
[learning] epoch 41 >> 100.00% completed in 35.18 (sec) <<
NEW BEST: epoch 42 valid F1 97.33 best test F1 94.48
[learning] epoch 43 >> 100.00% completed in 35.39 (sec) <<
[learning] epoch 44 >> 100.00% completed in 35.31 (sec) <<
[...]

詞向量近鄰

我們可以對學習到的詞向量進行K近鄰檢查。L2距離和cos距離返回結果相同,所以我們畫出詞向量的cos距離。

atlanta back ap80 but aircraft business a august actually cheap
phoenix live ap57 if plane coach people september provide weekday
denver lives ap up service first do january prices weekdays
tacoma both connections a airplane fourth but june stop am
columbus how tomorrow now seating thrift numbers december number early
seattle me before amount stand tenth abbreviation november flight sfo
minneapolis out earliest more that second if april there milwaukee
pittsburgh other connect abbreviation on fifth up july serving jfk
ontario plane thrift restrictions turboprop third serve jfk thank shortest
montreal service coach mean mean twelfth database october ticket bwi
philadelphia fare today interested amount sixth passengers may are lastest

可以看出,較少的詞彙表(大約500單詞)可以較少計算量。根據人為識別,發現有些分類效果好,有些則較差。