簡述表征句子的3種無監(jiān)督深度學(xué)習(xí)方法
近年來,由于用連續(xù)向量表示詞語(而不是用稀疏的 one-hot 編碼向量(Word2Vec))技術(shù)的發(fā)展,自然語言處理領(lǐng)域的性能獲得了重大提升。
Word2Vec 示例
盡管 Word2Vec 性能不錯,并且創(chuàng)建了很不錯的語義,例如 King - Man + Woman = Queen,但是我們有時(shí)候并不在意單詞的表征,而是句子的表征。
本文將介紹幾個用于句子表征的無監(jiān)督深度學(xué)習(xí)方法,并分享相關(guān)代碼。我們將展示這些方法在特定文本分類任務(wù)中作為預(yù)處理步驟的效果。
分類任務(wù)
用來展示不同句子表征方法的數(shù)據(jù)基于從萬維網(wǎng)抓取的 10000 篇新聞類文章。分類任務(wù)是將每篇文章歸類為 10 個可能的主題之一(數(shù)據(jù)具備主題標(biāo)簽,所以這是一個有監(jiān)督的任務(wù))。為了便于演示,我會使用一個 logistic 回歸模型,每次使用不同的預(yù)處理表征方法處理文章標(biāo)題。
基線模型——Average Word2Vec
我們從一個簡單的基線模型開始。我們會通過對標(biāo)題單詞的 Word2Vec 表征求平均來表征文章標(biāo)題。正如之前提及的,Word2Vec 是一種將單詞表征為向量的機(jī)器學(xué)習(xí)方法。Word2Vec 模型是通過使用淺層神經(jīng)網(wǎng)絡(luò)來預(yù)測與目標(biāo)詞接近的單詞來訓(xùn)練的。你可以閱讀更多內(nèi)容來了解這個算法是如何運(yùn)行的:http://mccormickml.com/2016/04/19/word2vec-tutorial-the-skip-gram-model/。
我們可以使用 Gensim 訓(xùn)練我們自己的 Word2Vec 模型,但是在這個例子中我們會使用一個 Google 預(yù)訓(xùn)練 Word2Vec 模型,它基于 Google 的新聞數(shù)據(jù)而建立。在將每一個單詞表征為向量后,我們會將一個句子(文章標(biāo)題)表征為其單詞(向量)的均值,然后運(yùn)行 logistic 回歸對文章進(jìn)行分類。
- #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 模型達(dá)到了 68% 的準(zhǔn)確率。這很不錯了,那么讓我們來看一看能不能做得更好。
average Word2Vec 方法有兩個弱點(diǎn):它是詞袋模型(bag-of-words model),與單詞順序無關(guān),所有單詞都具備相同的權(quán)重。為了進(jìn)行句子表征,我們將在下面的方法中使用 RNN 架構(gòu)解決這些問題。
自編碼器
自編碼器是一種無監(jiān)督深度學(xué)習(xí)模型,它試圖將自己的輸入復(fù)制到輸出。自編碼器的技巧在于中間隱藏層的維度要低于輸入數(shù)據(jù)的維度。所以這種神經(jīng)網(wǎng)絡(luò)必須以一種聰明、緊湊的方式來表征輸入,以完成成功的重建。在很多情況下,使用自編碼器進(jìn)行特征提取被證明是非常有效的。
我們的自編碼器是一個簡單的序列到序列結(jié)構(gòu),由一個輸入層、一個嵌入層、一個 LSTM 層,以及一個 softmax 層組成。整個結(jié)構(gòu)的輸入和輸出都是標(biāo)題,我們將使用 LSTM 的輸出來表征標(biāo)題。在得到自編碼器的表征之后,我們將使用 logistics 回歸來預(yù)測類別。為了得到更多的數(shù)據(jù),我們會使用文章中所有句子來訓(xùn)練自編碼器,而不是僅僅使用文章標(biāo)題。
- #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)
我們實(shí)現(xiàn)了 60% 的準(zhǔn)確率,比基線模型要差一些。我們可能通過優(yōu)化超參數(shù)、增加訓(xùn)練 epoch 數(shù)量或者在更多的數(shù)據(jù)上訓(xùn)練模型,來改進(jìn)該分?jǐn)?shù)。
語言模型
我們的第二個方法是訓(xùn)練語言模型來表征句子。語言模型描述的是某種語言中一段文本存在的概率。例如,「我喜歡吃香蕉」(I like eating bananas)這個句子會比「我喜歡吃卷積」(I like eating convolutions)這個句子具備更高的存在概率。我們通過分割 n 個單詞組成的窗口以及預(yù)測文本中的下一個單詞來訓(xùn)練語言模型。你可以在這里了解到更多基于 RNN 的語言模型的內(nèi)容:http://karpathy.github.io/2015/05/21/rnn-effectiveness/。通過構(gòu)建語言模型,我們理解了「新聞英語」(journalistic English)是如何建立的,并且模型應(yīng)該聚焦于重要的單詞及其表征。
我們的架構(gòu)和自編碼器的架構(gòu)是類似的,但是我們只預(yù)測一個單詞,而不是一個單詞序列。輸入將包含由新聞文章中的 20 個單詞組成的窗口,標(biāo)簽是第 21 個單詞。在訓(xùn)練完語言模型之后,我們將從 LSTM 的輸出隱藏狀態(tài)中得到標(biāo)題表征,然后運(yùn)行 logistics 回歸模型來預(yù)測類別。
- #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% 的準(zhǔn)確率,要比基線模型好一些,那我們能否讓它變得更好呢?
Skip-Thought 向量模型
在 2015 年關(guān)于 skip-thought 的論文《Skip-Thought Vectors》中,作者從語言模型中獲得了同樣的直覺知識。然而,在 skip-thought 中,我們并沒有預(yù)測下一個單詞,而是預(yù)測之前和之后的句子。這給模型關(guān)于句子的更多語境,所以,我們可以構(gòu)建更好的句子表征。您可以閱讀這篇博客
(https://medium.com/@sanyamagarwal/my-thoughts-on-skip-thoughts-a3e773605efa),了解關(guān)于這個模型的更多信息。
skip-thought 論文中的例子(https://arxiv.org/abs/1506.06726)
我們將構(gòu)造一個類似于自編碼器的序列到序列結(jié)構(gòu),但是它與自編碼器有兩個主要的區(qū)別。***,我們有兩個 LSTM 輸出層:一個用于之前的句子,一個用于下一個句子;第二,我們會在輸出 LSTM 中使用教師強(qiáng)迫(teacher forcing)。這意味著我們不僅僅給輸出 LSTM 提供了之前的隱藏狀態(tài),還提供了實(shí)際的前一個單詞(可在上圖和輸出***一行中查看輸入)。
- #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)
這一次我們達(dá)到了 74% 的準(zhǔn)確率。這是目前得到的***準(zhǔn)確率。
總結(jié)
本文中,我們介紹了三個使用 RNN 創(chuàng)建句子向量表征的無監(jiān)督方法,并且在解決一個監(jiān)督任務(wù)的過程中展現(xiàn)了它們的效率。自編碼器的結(jié)果比我們的基線模型要差一些(這可能是因?yàn)樗玫臄?shù)據(jù)集相對較小的緣故)。skip-thought 向量模型語言模型都利用語境來預(yù)測句子表征,并得到了***結(jié)果。
能夠提升我們所展示的方法性能的可用方法有:調(diào)節(jié)超參數(shù)、訓(xùn)練更多 epoch 次數(shù)、使用預(yù)訓(xùn)練嵌入矩陣、改變神經(jīng)網(wǎng)絡(luò)架構(gòu)等等。理論上,這些高級的調(diào)節(jié)工作或許能夠在一定程度上改變結(jié)果。但是,我認(rèn)為每一個預(yù)處理方法的基本直覺知識都能使用上述分享示例實(shí)現(xiàn)。
原文鏈接:
https://blog.myyellowroad.com/unsupervised-sentence-representation-with-deep-learning-104b90079a93
【本文是51CTO專欄機(jī)構(gòu)“機(jī)器之心”的原創(chuàng)譯文,微信公眾號“機(jī)器之心( id: almosthuman2014)”】