簡述表征句子的3種無監督深度學習方法
近年來,由于用連續向量表示詞語(而不是用稀疏的 one-hot 編碼向量(Word2Vec))技術的發展,自然語言處理領域的性能獲得了重大提升。
Word2Vec 示例
盡管 Word2Vec 性能不錯,并且創建了很不錯的語義,例如 King - Man + Woman = Queen,但是我們有時候并不在意單詞的表征,而是句子的表征。
本文將介紹幾個用于句子表征的無監督深度學習方法,并分享相關代碼。我們將展示這些方法在特定文本分類任務中作為預處理步驟的效果。
分類任務
用來展示不同句子表征方法的數據基于從萬維網抓取的 10000 篇新聞類文章。分類任務是將每篇文章歸類為 10 個可能的主題之一(數據具備主題標簽,所以這是一個有監督的任務)。為了便于演示,我會使用一個 logistic 回歸模型,每次使用不同的預處理表征方法處理文章標題。
基線模型——Average Word2Vec
我們從一個簡單的基線模型開始。我們會通過對標題單詞的 Word2Vec 表征求平均來表征文章標題。正如之前提及的,Word2Vec 是一種將單詞表征為向量的機器學習方法。Word2Vec 模型是通過使用淺層神經網絡來預測與目標詞接近的單詞來訓練的。你可以閱讀更多內容來了解這個算法是如何運行的:http://mccormickml.com/2016/04/19/word2vec-tutorial-the-skip-gram-model/。
我們可以使用 Gensim 訓練我們自己的 Word2Vec 模型,但是在這個例子中我們會使用一個 Google 預訓練 Word2Vec 模型,它基于 Google 的新聞數據而建立。在將每一個單詞表征為向量后,我們會將一個句子(文章標題)表征為其單詞(向量)的均值,然后運行 logistic 回歸對文章進行分類。
- #load data and Word2vec model
- df = pd.read_csv("news_dataset.csv")
- data = df[['body','headline','category']]
- w2v = gensim.models.KeyedVectors.load_word2vec_format('/GoogleNews-vectors-negative300.bin', binary=True)
- #Build X and Y
- x = np.random.rand(len(data),300)
- for i in range(len(data)):
- k = 0
- non = 0
- values = np.zeros(300)
- for j in data['headline'].iloc[i].split(' '):
- if j in w2v:
- values+= w2v[j]
- k+=1
- if k > 0:
- x[i,:]=values/k
- else: non+=1
- y = LabelEncoder().fit_transform(data['category'].values)
- msk = np.random.rand(len(data)) < 0.8
- X_train,y_train,X_test,y_test = x[msk],y[msk],x[~msk],y[~msk]
- #Train the model
- lr = LogisticRegression().fit(X_train,y_train)
- lr.score(X_test,y_test)
我們的基線 average Word2Vec 模型達到了 68% 的準確率。這很不錯了,那么讓我們來看一看能不能做得更好。
average Word2Vec 方法有兩個弱點:它是詞袋模型(bag-of-words model),與單詞順序無關,所有單詞都具備相同的權重。為了進行句子表征,我們將在下面的方法中使用 RNN 架構解決這些問題。
自編碼器
自編碼器是一種無監督深度學習模型,它試圖將自己的輸入復制到輸出。自編碼器的技巧在于中間隱藏層的維度要低于輸入數據的維度。所以這種神經網絡必須以一種聰明、緊湊的方式來表征輸入,以完成成功的重建。在很多情況下,使用自編碼器進行特征提取被證明是非常有效的。
我們的自編碼器是一個簡單的序列到序列結構,由一個輸入層、一個嵌入層、一個 LSTM 層,以及一個 softmax 層組成。整個結構的輸入和輸出都是標題,我們將使用 LSTM 的輸出來表征標題。在得到自編碼器的表征之后,我們將使用 logistics 回歸來預測類別。為了得到更多的數據,我們會使用文章中所有句子來訓練自編碼器,而不是僅僅使用文章標題。
- #parse all sentences
- sentenses = []
- for i in data['body'].values:
- for j in nltk.sent_tokenize(i):
- sentenses.append(j)
- #preprocess for keras
- num_words=2000
- maxlen=20
- tokenizer = Tokenizer(num_wordsnum_words = num_words, split=' ')
- tokenizer.fit_on_texts(sentenses)
- seqs = tokenizer.texts_to_sequences(sentenses)
- pad_seqs = []
- for i in seqs:
- if len(i)>4:
- pad_seqs.append(i)
- pad_seqs = pad_sequences(pad_seqs,maxlen)
- #The model
- embed_dim = 150
- latent_dim = 128
- batch_size = 64
- #### Encoder Model ####
- encoder_inputs = Input(shape=(maxlen,), name='Encoder-Input')
- emb_layer = Embedding(num_words, embed_dim,input_length = maxlen, name='Body-Word-Embedding', mask_zero=False)
- # Word embeding for encoder (ex: Issue Body)
- x = emb_layer(encoder_inputs)
- state_h = GRU(latent_dim, name='Encoder-Last-GRU')(x)
- encoder_model = Model(inputs=encoder_inputs, outputs=state_h, name='Encoder-Model')
- seq2seq_encoder_out = encoder_model(encoder_inputs)
- #### Decoder Model ####
- decoded = RepeatVector(maxlen)(seq2seq_encoder_out)
- decoder_gru = GRU(latent_dim, return_sequences=True, name='Decoder-GRU-before')
- decoder_grudecoder_gru_output = decoder_gru(decoded)
- decoder_dense = Dense(num_words, activation='softmax', name='Final-Output-Dense-before')
- decoder_outputs = decoder_dense(decoder_gru_output)
- #### Seq2Seq Model ####
- #seq2seq_decoder_out = decoder_model([decoder_inputs, seq2seq_encoder_out])
- seq2seq_Model = Model(encoder_inputs,decoder_outputs )
- seq2seq_Model.compile(optimizer=optimizers.Nadam(lr=0.001), loss='sparse_categorical_crossentropy')
- history = seq2seq_Model.fit(pad_seqs, np.expand_dims(pad_seqs, -1),
- batch_sizebatch_size=batch_size,
- epochs=5,
- validation_split=0.12)
- #Feature extraction
- headlines = tokenizer.texts_to_sequences(data['headline'].values)
- headlines = pad_sequences(headlines,maxlenmaxlen=maxlen)x = encoder_model.predict(headlines)
- #classifier
- X_train,y_train,X_test,y_test = x[msk],y[msk],x[~msk],y[~msk]
- lr = LogisticRegression().fit(X_train,y_train)
- lr.score(X_test,y_test)
我們實現了 60% 的準確率,比基線模型要差一些。我們可能通過優化超參數、增加訓練 epoch 數量或者在更多的數據上訓練模型,來改進該分數。
語言模型
我們的第二個方法是訓練語言模型來表征句子。語言模型描述的是某種語言中一段文本存在的概率。例如,「我喜歡吃香蕉」(I like eating bananas)這個句子會比「我喜歡吃卷積」(I like eating convolutions)這個句子具備更高的存在概率。我們通過分割 n 個單詞組成的窗口以及預測文本中的下一個單詞來訓練語言模型。你可以在這里了解到更多基于 RNN 的語言模型的內容:http://karpathy.github.io/2015/05/21/rnn-effectiveness/。通過構建語言模型,我們理解了「新聞英語」(journalistic English)是如何建立的,并且模型應該聚焦于重要的單詞及其表征。
我們的架構和自編碼器的架構是類似的,但是我們只預測一個單詞,而不是一個單詞序列。輸入將包含由新聞文章中的 20 個單詞組成的窗口,標簽是第 21 個單詞。在訓練完語言模型之后,我們將從 LSTM 的輸出隱藏狀態中得到標題表征,然后運行 logistics 回歸模型來預測類別。
- #Building X and Y
- num_words=2000
- maxlen=20
- tokenizer = Tokenizer(num_wordsnum_words = num_words, split=' ')
- tokenizer.fit_on_texts(df['body'].values)
- seqs = tokenizer.texts_to_sequences(df['body'].values)
- seq = []
- for i in seqs:
- seq+=i
- X = []
- Y = []
- for i in tqdm(range(len(seq)-maxlen-1)):
- X.append(seq[i:i+maxlen])
- Y.append(seq[i+maxlen+1])
- X = pd.DataFrame(X)
- Y = pd.DataFrame(Y)
- Y[0]=Y[0].astype('category')
- Y =pd.get_dummies(Y)
- #Buidling the network
- embed_dim = 150
- lstm_out = 128
- batch_size= 128
- model = Sequential()
- model.add(Embedding(num_words, embed_dim,input_length = maxlen))
- model.add(Bidirectional(LSTM(lstm_out)))
- model.add(Dense(Y.shape[1],activation='softmax'))
- adam = Adam(lr=0.001, beta_1=0.7, beta_2=0.99, epsilon=None, decay=0.0, amsgrad=False)
- model.compile(loss = 'categorical_crossentropy', optimizer=adam)
- model.summary()
- print('fit')
- model.fit(X, Y, batch_sizebatch_size =batch_size,validation_split=0.1, epochs = 5, verbose = 1)
- #Feature extraction
- headlines = tokenizer.texts_to_sequences(data['headline'].values)
- headlines = pad_sequences(headlines,maxlenmaxlen=maxlen)
- inp = model.input
- outputs = [model.layers[1].output]
- functor = K.function([inp]+ [K.learning_phase()], outputs )
- x = functor([headlines, 1.])[0]
- #classifier
- X_train,y_train,X_test,y_test = x[msk],y[msk],x[~msk],y[~msk]
- lr = LogisticRegression().fit(X_train,y_train)
- lr.score(X_test,y_test)
這一次我們得到了 72% 的準確率,要比基線模型好一些,那我們能否讓它變得更好呢?
Skip-Thought 向量模型
在 2015 年關于 skip-thought 的論文《Skip-Thought Vectors》中,作者從語言模型中獲得了同樣的直覺知識。然而,在 skip-thought 中,我們并沒有預測下一個單詞,而是預測之前和之后的句子。這給模型關于句子的更多語境,所以,我們可以構建更好的句子表征。您可以閱讀這篇博客
(https://medium.com/@sanyamagarwal/my-thoughts-on-skip-thoughts-a3e773605efa),了解關于這個模型的更多信息。
skip-thought 論文中的例子(https://arxiv.org/abs/1506.06726)
我們將構造一個類似于自編碼器的序列到序列結構,但是它與自編碼器有兩個主要的區別。***,我們有兩個 LSTM 輸出層:一個用于之前的句子,一個用于下一個句子;第二,我們會在輸出 LSTM 中使用教師強迫(teacher forcing)。這意味著我們不僅僅給輸出 LSTM 提供了之前的隱藏狀態,還提供了實際的前一個單詞(可在上圖和輸出***一行中查看輸入)。
- #Build x and y
- num_words=2000
- maxlen=20
- tokenizer = Tokenizer(num_wordsnum_words = num_words, split=' ')
- tokenizer.fit_on_texts(sentenses)
- seqs = tokenizer.texts_to_sequences(sentenses)
- pad_seqs = pad_sequences(seqs,maxlen)
- x_skip = []
- y_before = []
- y_after = []
- for i in tqdm(range(1,len(seqs)-1)):
- if len(seqs[i])>4:
- x_skip.append(pad_seqs[i].tolist())
- y_before.append(pad_seqs[i-1].tolist())
- y_after.append(pad_seqs[i+1].tolist())
- x_before = np.matrix([[0]+i[:-1] for i in y_before])
- x_after =np.matrix([[0]+i[:-1] for i in y_after])
- x_skip = np.matrix(x_skip)
- y_before = np.matrix(y_before)
- y_after = np.matrix(y_after)
- #Building the model
- embed_dim = 150
- latent_dim = 128
- batch_size = 64
- #### Encoder Model ####
- encoder_inputs = Input(shape=(maxlen,), name='Encoder-Input')
- emb_layer = Embedding(num_words, embed_dim,input_length = maxlen, name='Body-Word-Embedding', mask_zero=False)
- x = emb_layer(encoder_inputs)
- _, state_h = GRU(latent_dim, return_state=True, name='Encoder-Last-GRU')(x)
- encoder_model = Model(inputs=encoder_inputs, outputs=state_h, name='Encoder-Model')
- seq2seq_encoder_out = encoder_model(encoder_inputs)
- #### Decoder Model ####
- decoder_inputs_before = Input(shape=(None,), name='Decoder-Input-before') # for teacher forcing
- dec_emb_before = emb_layer(decoder_inputs_before)
- decoder_gru_before = GRU(latent_dim, return_state=True, return_sequences=True, name='Decoder-GRU-before')
- decoder_gru_output_before, _ = decoder_gru_before(dec_emb_before, initial_state=seq2seq_encoder_out)
- decoder_dense_before = Dense(num_words, activation='softmax', name='Final-Output-Dense-before')
- decoder_outputs_before = decoder_dense_before(decoder_gru_output_before)
- decoder_inputs_after = Input(shape=(None,), name='Decoder-Input-after') # for teacher forcing
- dec_emb_after = emb_layer(decoder_inputs_after)
- decoder_gru_after = GRU(latent_dim, return_state=True, return_sequences=True, name='Decoder-GRU-after')
- decoder_gru_output_after, _ = decoder_gru_after(dec_emb_after, initial_state=seq2seq_encoder_out)
- decoder_dense_after = Dense(num_words, activation='softmax', name='Final-Output-Dense-after')
- decoder_outputs_after = decoder_dense_after(decoder_gru_output_after)
- #### Seq2Seq Model ####
- seq2seq_Model = Model([encoder_inputs, decoder_inputs_before,decoder_inputs_after], [decoder_outputs_before,decoder_outputs_after])
- seq2seq_Model.compile(optimizer=optimizers.Nadam(lr=0.001), loss='sparse_categorical_crossentropy')
- seq2seq_Model.summary()
- history = seq2seq_Model.fit([x_skip,x_before, x_after], [np.expand_dims(y_before, -1),np.expand_dims(y_after, -1)],
- batch_sizebatch_size=batch_size,
- epochs=10,
- validation_split=0.12)
- #Feature extraction
- headlines = tokenizer.texts_to_sequences(data['headline'].values)
- headlines = pad_sequences(headlines,maxlenmaxlen=maxlen)x = encoder_model.predict(headlines)
- #classifier
- X_train,y_train,X_test,y_test = x[msk],y[msk],x[~msk],y[~msk]
- lr = LogisticRegression().fit(X_train,y_train)
- lr.score(X_test,y_test)
這一次我們達到了 74% 的準確率。這是目前得到的***準確率。
總結
本文中,我們介紹了三個使用 RNN 創建句子向量表征的無監督方法,并且在解決一個監督任務的過程中展現了它們的效率。自編碼器的結果比我們的基線模型要差一些(這可能是因為所用的數據集相對較小的緣故)。skip-thought 向量模型語言模型都利用語境來預測句子表征,并得到了***結果。
能夠提升我們所展示的方法性能的可用方法有:調節超參數、訓練更多 epoch 次數、使用預訓練嵌入矩陣、改變神經網絡架構等等。理論上,這些高級的調節工作或許能夠在一定程度上改變結果。但是,我認為每一個預處理方法的基本直覺知識都能使用上述分享示例實現。
原文鏈接:
https://blog.myyellowroad.com/unsupervised-sentence-representation-with-deep-learning-104b90079a93
【本文是51CTO專欄機構“機器之心”的原創譯文,微信公眾號“機器之心( id: almosthuman2014)”】