選自Medium
作者:yonatan hadar
機器之心編譯
轉自:機器之心
參與:Nurhachu Null、路
本文介紹了三種用於表徵句子的無監督深度學習方法:自編碼器、語言模型和 Skip-Thought 向量模型,並與基線模型 Average Word2Vec 進行了對比。
近年來,由於用連續向量表示詞語(而不是用稀疏的 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_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_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_size=batch_size,
epochs=5,
validation_split=0.12)
#Feature extraction
headlines = tokenizer.texts_to_sequences(data['headline'].values)
headlines = pad_sequences(headlines,maxlen=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_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_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,maxlen=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_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_size=batch_size,
epochs=10,
validation_split=0.12)
#Feature extraction
headlines = tokenizer.texts_to_sequences(data['headline'].values)
headlines = pad_sequences(headlines,maxlen=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
投稿郵箱:liuyali@c2.org.cn
長按下方二維碼 免費訂閱!
如何加入學會
註冊學會會員:
個人會員:
關注學會微信:中國指揮與控制學會(c2_china),回復「個人會員」獲取入會申請表,按要求填寫申請表即可,如有問題,可在公眾號內進行留言。通過學會審核後方可在線進行支付寶繳納會費。
單位會員:
關注學會微信:中國指揮與控制學會(c2_china),回復「單位會員」獲取入會申請表,按要求填寫申請表即可,如有問題,可在公眾號內進行留言。通過學會審核後方可繳納會費。
長按下方學會二維碼,關注學會微信