【論文筆記】:Convolutional Neural Networks for Sentence Classification用CNN做句子分類


本文是對論文的解讀和復現。

論文地址:https://arxiv.org/abs/1408.5882

參考代碼:https://mp.weixin.qq.com/s?__biz=MzI3ODgwODA2MA==&mid=2247488163&idx=4&sn=5ad2454db800f12e09564b2de3ae524c&chksm=eb500630dc278f263976590419bd9354815bc0249e81cfb1b721926ff163fe0e469e014dc9de&mpshare=1&scene=23&srcid=0314rFaFU9ZiLpyyQU3rdVsB&sharer_sharetime=1584435544460&sharer_shareid=470498a3f8c258720b36d27077b3ab82#rd

一、論文

在預先訓練的詞向量上訓練卷積神經網絡(CNN)用於句子級分類任務的實驗。證明了一個簡單的CNN,它只需要很少的超參數調整和靜態向量,就可以在多個基准上獲得很好的結果。通過微調學習特定於任務的向量可以進一步提高性能。本文討論的CNN模型改進了7項任務中的4項,包括情感分析和問題分類。

1.introduce

近年來,深度學習模型在計算機視覺(Krizhevsky et al.,2012)和語音識別(Graves et al.,2013)方面取得了顯著的效果。在自然語言處理中,許多使用深度學習方法的工作涉及通過神經語言模型學習詞向量表示(Bengio等人,2003;Yih等人,2011;Mikolov等人,2013)和對學習到的詞向量進行構圖以進行分類(Collobert等人,2011)。

詞向量,就是將單詞經由隱藏層,從稀疏編碼(大小為V,V是詞匯表長度)投影到低維向量空間。詞向量本質上是對單詞的語義特征進行維度編碼的特征提取器。在這種稠密的表示中,語義相近的詞在低維向量空間中的歐幾里德距離或余弦距離上同樣相近。

卷積神經網絡(CNN)利用帶卷積濾波器的層,作用於局部特征(LeCun等人,1998)。CNN模型最初是為計算機視覺而發明的,后來被證明對NLP有效,並在語義分析(Yih等人,2014)、搜索查詢檢索(Shen等人,2014)、句子建模(Kalchbrenner等人,2014)和其他傳統NLP任務(Collobert等人,2011)方面取得了優異的結果。

在現有的工作中,我們在一個無監督的神經語言模型中得到的詞向量上加入一層卷積。(2013)詞向量來自於1000億字的公開的谷歌新聞上。最初保持詞向量不變,只學習模型的其他參數。盡管很少調整超參數,這個簡單的模型在多個基准上取得了很好的結果,這表明預先訓練的詞向量是通用的特征提取器,可以用於各種分類任務。通過進一步的微調可以學習特定於任務的詞向量。最后,我們描述了對體系結構的一個簡單修改,允許通過多個通道使用預先訓練的向量和任務特定的向量。

我們的工作表明,對於圖像分類,從預訓練的深度學習模型獲得的特征提取器在各種任務上表現良好,包括與訓練特征提取器的原始任務非常不同的任務。

 2.模型

圖上最左邊這個n×k的矩陣格子,每一行表示的是一個k維的詞,用xi來表示句子中第i個k維向量表示的詞,上圖就表示了一個長度為n的句子的卷積過程。

從左一到左二圖表示着卷積運算,包含一個濾波器w∈Rhk,該濾波器作用在大小為h的窗口上以產生新的特征。

例如圖上的紅線部分,窗口大小為2,每次選擇兩個詞進行特征提取;

黃線部分窗口大小為3,每次選擇三個詞進行特征提取,也就是說,“窗口”的含義是“每次作用幾個單詞”,反應在圖上就是“濾波器一次性遍歷幾行”;

我們可以看見,對於左二圖的離我們最近的這列格子,長得條狀就叫做 channel吧,它的窗口為2,每作用兩個單詞都會得到一個格子,因為這句話有9個單詞,從上往下滑動,就會得到7個格子;如果窗口為3,即離我們最遠的這列黃格子,從上往下滑動,智能得到5個格子。

上式就是用一個窗口大小為h的濾波器,作用在$x_{i}$到$x_{i+h-1}$上,計算得到特征$c_{i}$的過程,b是個偏置常量,w是卷積層的矩陣。$f$是個非線性函數例如$tanh$。不同大小的濾波器各自在句子上上滑動,最后得到多個特征向量$c=\left [ c_{1},c_{2},...,c_{n-h+1} \right ]$。

這個圖更好的展示了過程,彩色矩陣部分是代表着卷積核,它們分次對句子進行計算,這里有2個窗口為2的卷積核(黃色系),2個窗口為3的卷積核(綠色系),2個窗口為4的卷積核(紅色系),每個彩色矩陣輸出了一個channel,也就是每個kernel_size 有兩個輸出 channel。然后進行池化,我們就可以得到6個能夠很好的代表特征的格子,也就是數字。

之前一直很糾結的為什么論文它要選窗口為2,3,4的卷積核各兩個來做,原來是因為:首先即便是大小一樣的卷積核,它們所能檢測到的特征也是不一樣的,其次考慮到后邊需要做池化,如果只有1個某窗口大小的卷積核的話,輸出就只有1個格子了,會丟失很多信息;最后不同的卷積核的檢測范圍不一樣,就像圖片一樣,小的卷積核發現了眼睛,大的卷積核一看原來是人臉。

然后,從左二到左三這個過程,對特征映射應用一個最大超時池操作(Collobert等人,2011),並將最大值$\widehat{c}=max\left \{ c \right \}$作為該過濾器提取出來的特征。其思想是一個濾波器只要捕捉該句子最重要的特征就可以了。這種池機制自然處理可變的句子長度。

三、代碼思路

 反正先導庫,tensorflow2以后導keras前都要加一個tensorflow.:

import logging
%tensorflow_version 2.x
import tensorflow as tf
from tensorflow.keras.layers import Conv1D, MaxPool1D, Dense, Flatten, concatenate, Embedding,Input
from tensorflow.keras.models import Model
from tensorflow.keras.utils import plot_model

 

我們從tensorflow.keras.layers里導了Conv1D(用來卷積), MaxPool1D(用來池化), Dense(用來寫輸出層), Flatten(用來展平向量), concatenate(用來把池化結果拼接), Embedding(用來處理詞嵌入),Input(用來構建輸入層)

以及import了Model用來組合這些輸入層嵌入層卷積層池化層輸出層。還用plot_model來看網絡結構。

1.構建輸入,這里並不考慮一次性輸入幾個句子,就用None表示。

注意:輸入只需要知道句子的長度就可以了,也就是一句話中的分詞數量。當時我很迷惑,因為就算是onehot編碼的詞向量組成的句子,它也是一個矩陣形式呀,形式為(單詞數量,詞匯表長度),為什么詞匯表的長度單獨作為一個參數輸入?

當我看見嵌入矩陣的時候更瘋了,什么情況,嵌入矩陣依舊是(詞匯表長度,詞向量長度),這個跟我們定義的句子(句子長度,)這個矩陣,怎么乘?最后還得到了(句子長度,詞向量長度)?還成功了?

原來我們具體輸入的,不是通常意義上像onehot編碼那樣的東西,比如['i','love','you']這樣的一個已經分好詞的句子結果,然后我們基於語料庫訓練出來了一個字典D,於是乎我們輸入這個框架的x_input就應該是這樣的形式[4,5,6],4代表‘i’在字典里的位置,5代表‘love’在字典里的位置。現在就好理解多了,然后我們的嵌入矩陣,只要根據4,5,6,抽出第4行,第5行,第6行即可構成我們的目標詞向量。這就是官方embedding文檔中說的將正整數(索引值)轉換為固定尺寸的稠密向量

 

  #max_sequence_length:句子的長度
  #max_token_num:詞匯表的長度
  #embedding_dim:嵌入矩陣的維度
  #output_dim:嵌入矩陣處理后的詞向量維度
#1.構建embedding層
  x_input = Input(shape=(max_sequence_length,))#輸入一個長度為max_sequence_length的句子
  logging.info("x_input.shape: %s" % str(x_input.shape))  # (?, 60)
  if embedding_matrix is None:
    x_emb = Embedding(input_dim=max_token_num, output_dim=embedding_dim, input_length=max_sequence_length)(x_input)
  else:
    x_emb = Embedding(input_dim=max_token_num, output_dim=embedding_dim, input_length=max_sequence_length,weights=[embedding_matrix], trainable=True)(x_input)
  logging.info("x_emb.shape: %s" % str(x_emb.shape))  # (?, 60, 300)

 

2.卷積和池化

很多教程里在卷積部分會提一嘴,說embedding出來的張量是三維的,所以按照這個論文來說還要添加一維才能輸進卷積層里,這個當初也困擾了我很久。如果是只有一種類型的卷積核,那就愉快的卷積輸出就好了,壞就壞在有三種不同窗口的卷積核(2,3,4),你待會算出來的結果還要進行一個池化,不同窗口的卷積核你總不能放在一層進行一個最大池化吧,得多一層維度,這就是那些博客的思想。不過這里用了一個for循環,把不同kernel_size的卷積核分開了,也就不存在要多一個維度的事情了

#2.構建卷積層和池化層
  pool_output = []
  kernel_sizes = [2, 3, 4] 
  for kernel_size in kernel_sizes:
      c = Conv1D(filters=2, kernel_size=kernel_size, strides=1)(x_emb)#卷積
      p = MaxPool1D(pool_size=int(c.shape[1]))(c)#池化
      pool_output.append(p)
      logging.info("kernel_size: %s \t c.shape: %s \t p.shape: %s" % (kernel_size, str(c.shape), str(p.shape)))
  pool_output = concatenate([p for p in pool_output])
  logging.info("pool_output.shape: %s" % str(pool_output.shape))  # (?, 1, 6)

最后展平輸出即可。

 

 

  #3.展平+輸出
  x_flatten = Flatten()(pool_output)  # (?, 6)
  y = Dense(output_dim, activation='softmax')(x_flatten)  # (?, 2)
  logging.info("y.shape: %s \n" % str(y.shape))

  model = Model([x_input], outputs=[y])
  if model_img_path:
    plot_model(model, to_file=model_img_path, show_shapes=True, show_layer_names=False)
  model.summary()
  return model

 

 

 

 

 

 

 

 

 

全代碼如下:

import logging
%tensorflow_version 2.x
import tensorflow as tf
from tensorflow.keras.layers import Conv1D, MaxPool1D, Dense, Flatten, concatenate, Embedding,Input
from tensorflow.keras.models import Model
from tensorflow.keras.utils import plot_model

def textcnn(max_sequence_length, max_token_num, embedding_dim, output_dim, model_img_path=None, embedding_matrix=None):
  #max_sequence_length:句子的長度
  #max_token_num:詞匯表的長度
  #embedding_dim:嵌入矩陣的維度
  #output_dim:嵌入矩陣處理后的詞向量維度
#1.構建embedding層
  x_input = Input(shape=(max_sequence_length,))#輸入一個長度為max_sequence_length的句子
  logging.info("x_input.shape: %s" % str(x_input.shape))  # (?, 60)
  if embedding_matrix is None:
    x_emb = Embedding(input_dim=max_token_num, output_dim=embedding_dim, input_length=max_sequence_length)(x_input)
  else:
    x_emb = Embedding(input_dim=max_token_num, output_dim=embedding_dim, input_length=max_sequence_length,weights=[embedding_matrix], trainable=True)(x_input)
  logging.info("x_emb.shape: %s" % str(x_emb.shape))  # (?, 60, 300)
#2.構建卷積層和池化層
  pool_output = []
  kernel_sizes = [2, 3, 4] 
  for kernel_size in kernel_sizes:
      c = Conv1D(filters=2, kernel_size=kernel_size, strides=1)(x_emb)#卷積
      p = MaxPool1D(pool_size=int(c.shape[1]))(c)#池化
      pool_output.append(p)
      logging.info("kernel_size: %s \t c.shape: %s \t p.shape: %s" % (kernel_size, str(c.shape), str(p.shape)))
  pool_output = concatenate([p for p in pool_output])
  logging.info("pool_output.shape: %s" % str(pool_output.shape))  # (?, 1, 6)

  #3.展平+輸出
  x_flatten = Flatten()(pool_output)  # (?, 6)
  y = Dense(output_dim, activation='softmax')(x_flatten)  # (?, 2)
  logging.info("y.shape: %s \n" % str(y.shape))

  model = Model([x_input], outputs=[y])
  if model_img_path:
    plot_model(model, to_file=model_img_path, show_shapes=True, show_layer_names=False)
  model.summary()
  return model

textcnn(max_sequence_length=60, max_token_num=5000, embedding_dim=100,output_dim=100)

輸出結果為:

 

from  tensorflow.keras.models  import  Model


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM