本文亮點:
將用於天然語言處理的CNN架構,從keras0.3.3搬運到了keras2.x,強行練習了Sequential+Model的混合使用,具體來講,是Model裏嵌套了Sequential。
本文背景:
暑假在作一個推薦系統的小項目,老師讓咱們蒐集推薦系統領域Top5的算法和模型,要求結合深度學習。
我和小夥伴選擇了其中的兩篇文獻深刻研究,我負責跑通文獻Convolutional Matrix Factorization for Document Context-Aware Recommendation裏的模型,這是Recsys16下載量Top2的文章,僅次於youtube那篇。論文連接:http://dm.postech.ac.kr/~cartopy/ConvMF/ ,文章最下有數據集和源代碼。
源代碼的配置環境比較舊,基於python2.7和keras0.3.3,高級的地方是使用了GPU加速訓練。其中用到的處理item document的CNN架構徹底用keras0.3.3寫的(這裏的CNN用於NLP,與用於圖像識別的CNN架構上有必定區別),其他的.py文件都是用py2寫的node
1、codes搬運:從python2到python3
如今py3用的比較多,也是趨勢,因此我首先肯定把py2的代碼改寫成py3能用的
python2和python3在code時有必定區別,在改寫成py3時,有些簡單的根據報錯信息,百度google下很容易debug出來,好比:
print的內容在py3裏要加括號
xrange-->range
zip()[]-->list(zip())[]
以上這些涉及到的代碼就至關多了
真正麻煩耗時的是,二進制文件的讀取,涉及編碼類型。文獻用的movielens數據集不是csv文件,下載下來查看了下有的編碼是"utf-8",有的是"ANSI"。
我在下面這些地方研究了好久,簡要記錄下:
cpickle-->pikle,pickle.load,pickle.dump,"rb","wb","r"...
codecs.open(,"utf-8-sig")python
2、CNN架構搬運:從keras0.3.3到keras2.x
兩個版本的區別:keras0.3.3和keras2.x相差太多(keras0.x和keras1.x就相差不少了),主要是後者移除了Graph,新增了函數式模型--Model。
兩個模型的區別:簡單來講,Sequential是單輸入單輸出,Model(Graph)是多輸入多輸出。Graph圖模型,建模時依然是向其中add_input, add_node, add_output,而2.x版的Model是一種函數式模型,建模時先定義Input,傳遞給Layer1,作了一次矩陣計算,再將當前層的輸出,傳遞給Layer2,又作了一次矩陣計算···將最後的輸出定義爲整個模型的輸出,最開始的輸入定義爲整個模型的輸入,至關於給一個輸入,獲得一個輸出,中間通過的黑箱就是咱們的模型,這種行爲就是函數,因此叫函數式模型。在2.x裏,更強調Sequential是Model的特殊狀況(這是文檔裏的原話,那不就意味着,必要時,Sequential也能當函數式模型用)。
改寫難度:就該文獻而言,改寫難度在於源代碼混合使用了Sequential+Graph,具體來講,是Graph裏嵌套了Sequential。從0.x到2.x能夠簡單認爲Graph對應着Model,這意味我可能要同時使用Sequential+Model,這是思路1。此時最但願的就是有現成的例子能夠參考啊,而博客、文檔中能找到的例子都是單獨使用Sequential和Graph(Model)的,難道改寫時,須要換成全用Sequential或全用Model,這裏分別產生了思路2和思路3。
我很快決定採用思路1的主要緣由有兩點:
1)直覺:源代碼混合使用了Sequential+Graph,從0.x到2.x能夠簡單認爲Graph對應着Model,這暗示Sequential+Model可行(debug到實在不行我再所有改用Sequential);
2)就像前面文檔裏提到的,「在2.x裏,更強調Sequential是Model的特殊狀況」,那不就意味着,必要時,Sequential也能當函數式模型用(在寫這篇博客時,我發現這裏和思路3本質相同);
其實對於keras小白來講,快速選擇思路1,主要仍是靠直覺,後面也是理解了CNN架構、源代碼邏輯,仔細查看兩個版本的文檔時,愈來愈確認思路1可行。因此,這也是下面要介紹的。
改寫的正確姿式:看懂CNN的架構(原理圖、流程圖)-->看懂keras0.3.3寫的架構-->對應着改寫到keras2.xgit
一、CNN的架構
github
輸入層:
輸入對象爲文檔,可當作長度爲l的詞序列,每篇文檔長度不等,統一設置max_len = 300(後面在keras序列數據預處理時用pad_sequence填充)
keras0.x:add_input算法
model = Graph()
'''Input'''
model.add_input(name='input', input_shape=(max_len,), dtype=int)架構
keras2.x:Inputapp
'''Input'''
doc_input = Input(shape=(max_len,), dtype='int32', name='doc_input')
print("Builded input...")dom
嵌入層:
將one-hot編碼的詞向量(每一個詞向量維度=詞典單詞總數,這裏vocab_size = 8000)嵌入成p維,這裏emb_dim = 200,此時詞序列的shape爲p*l
python2.7
Reshape層:
進入Conv2D以前,reshape成channels last的形式
卷積層:3種不一樣window size的filter,每種filter提取出的特徵圖深度均爲100,nb_filters=100,
ide
池化層:3種不一樣window size的filter
\(W_c^j\)提取\(c_i^j\)(第\(i\)個單詞的\(j\)號上下文特徵),每篇文檔詞序列長度爲\(l\),共有\(l-ws+1\)個單詞會被提取上下文特徵,每一個單詞由\(n_c\)種不一樣的共享權重\(W_c^1,...,W_c^j,...,W_c^{n_c}\)提取\(n_c\)種特徵,因此一篇文檔,經卷積層提取出的上下文特徵的shape是\(n_c*(l-ws+1)\),\(n_c\)至關於用於圖像識別的CNN中特徵圖深度這一律念,這裏特徵圖深度\(n_c=nbfilters=100\)
\[c^1=[c_1^1,c_2^1,...c_i^1,...c_{l-ws+1}^1]\]
\[c^2=[c_1^2,c_2^2,...c_i^2,...c_{l-ws+1}^2]\]
\[...\]
\[c^j=[c_1^j,c_2^j,...c_i^j,...c_{l-ws+1}^j]\]
\[...\]
\[c^{n_c}=[c_1^{n_c},c_2^{n_c},...c_i^{n_c},...c_{l-ws+1}^{n_c}]\]
這裏文檔長度\(l\)和卷積核\(ws\)不一樣,提取出的特徵矩陣shape不一樣,但只要通過maxpooling層,每一個\(c^j\)取最大,最終獲得\(n_c=100\)維的特徵向量
\[d_f=[max(c^1),max(c^2),...,max(c^j),...,max(c^{n_c}))]\]
flatten層:
全鏈接層(200)&projection層(50):
二、源代碼邏輯
這裏的CNN架構本質上是單輸入單輸出,輸入是每個item的document,輸出是文檔隱向量。中間用了3個不一樣window size的filter對reshape的輸出作卷積&池化,flatten以後(斜體部分用循環生成了3個Sequential,注意這裏不是共享層,共享層要求結構相同,這裏3個不一樣的filter作卷積&池化,layer的結構明顯不一樣),有3個outputs(都是100維的向量),再做爲inputs輸入到後面的layers(依次:全鏈接層200維,projection層50維)時,實際將3個inputs拼接起來了。
我一開始沒注意到這裏的拼接,文獻和源代碼也沒有明顯指出3個inputs的拼接,只是若是不拼接,每一個input單獨輸入後面的共享層,從100維到200維再到50維,很奇怪!!通常都是高維的特徵進入全鏈接層進行特徵壓縮,應該降維纔對,這裏竟然升維,而若是3個100維的inputs拼接起來變成300維,再降到200維,最後降到50維,這樣才合理,這是個人推斷。後來我在keras0.3.3寫的源代碼找到了一個拼接的證據,這步裏用到了add_node:
model.add_node(Dense(vanila_dimension, activation='tanh'), name='fully_connect', inputs=['unit_' + str(i) for i in filter_lengths])
這裏的3個inputs實際是concat了在一塊兒,變成3*100維,下面通過fc變成200維,再通過pj變成50維
keras0.3.3文檔中關於add_node,默認merge_mode=‘cancat’
文檔是這樣定義的:https://keras.io/layers/containers/
add_node(layer, name, input=None, inputs=[], merge_mode='concat', concat_axis=-1, dot_axes=-1, create_output=False)
解釋:Add a node in the graph. It can be connected to multiple inputs, which will first be merged into one tensor according to the mode specified.
另外,我也找到Graph對於多輸入作merge的例子:https://github.com/cartopy/keras-0.3.3/blob/master/docs/templates/models.md
思考卷積&池化,選擇3種window size的filters意義何在:不一樣ws的filter對每一個單詞提取的上下文範圍不一樣,對應了不一樣的空間尺度空間,最後再組合起來。有點相似圖像處理裏,將二維圖像分解到不一樣空間尺度上。這裏將每一個單詞的上下文特徵分解到3個尺度上,最後再拼接回來。固然也能夠分解到更多空間尺度上。
========================================================================================================
分割線內容寫於這篇blog初步發表的一天後:選擇3個不一樣window size的filter處理嵌入後的文本:卷積&池化(&flatten)以後再拼接,這是用CNN作NLP的基本操做,改寫codes的時候,我只學過CNN作圖像識別,後來學了點CNN作文本分類,恍然大悟,原來就是須要拼接的
========================================================================================================
三、兩個版本的代碼對好比下:
首先是keras0.3.3:
# coding: utf-8
'''
Created on Dec 8, 2015@author: donghyun
'''import numpy as np
np.random.seed(1337)from keras.callbacks import EarlyStopping
from keras.layers.containers import Sequential
from keras.layers.convolutional import Convolution2D, MaxPooling2D
from keras.layers.core import Reshape, Flatten, Dropout, Dense
from keras.layers.embeddings import Embedding
from keras.models import Graph
from keras.preprocessing import sequenceoutput_dimesion = 50
vocab_size = 8000
dropout_rate = 0.2
emb_dim = 200
max_len = 300
nb_filters = 100
init_W = Nonemax_features = vocab_size
vanila_dimension = 200
projection_dimension = output_dimesionfilter_lengths = [3, 4, 5]
model = Graph()
print("Building Embedding Layer...")
'''Embedding Layer'''
model.add_input(name='input', input_shape=(max_len,), dtype=int)if init_W is None:
model.add_node(Embedding(max_features, emb_dim, input_length=max_len), name='sentence_embeddings', input='input')
else:
self.model.add_node(Embedding(max_features, emb_dim, input_length=max_len, weights=[init_W / 20]), name='sentence_embeddings', input='input')print("Building Convolution Layer & Max Pooling Layer...")
'''Convolution Layer & Max Pooling Layer'''
for i in filter_lengths:
model_internal = Sequential()
model_internal.add(Reshape(dims=(1, self.max_len, emb_dim), input_shape=(self.max_len, emb_dim)))
model_internal.add(Convolution2D(nb_filters, i, emb_dim, activation="relu"))
model_internal.add(MaxPooling2D(pool_size=(self.max_len - i + 1, 1)))
model_internal.add(Flatten())
model.add_node(model_internal, name='unit_' + str(i), input='sentence_embeddings')
# 以上for循環體內的代碼要縮進print("Building Fully Connect Layer & Dropout Layer...")
'''Fully Connect Layer'''
model.add_node(Dense(vanila_dimension, activation='tanh'), name='fully_connect', inputs=['unit_' + str(i) for i in filter_lengths])
# 這裏的3個inputs實際是concat了在一塊兒,變成3*100維,下面通過fc變成200維,再通過pj變成50維
# keras0.3.3文檔中關於add_node,默認merge_mode=‘cancat’,https://keras.io/layers/containers/'''Dropout Layer'''
'''Projection Layer & Output Layer'''
model.add_node(Dropout(dropout_rate), name='dropout', input='fully_connect')
print("Building Projection Layer & Output Layer...")
model.add_node(Dense(projection_dimension, activation='tanh'), name='projection', input='dropout')
# Output Layer
model.add_output(name='output', input='projection')
model.compile('rmsprop', {'output': 'mse'})
keras2.x:
import numpy as np
np.random.seed(1337)from keras.callbacks import EarlyStopping
from keras.layers.convolutional import Conv2D, MaxPooling2D
from keras.layers.core import Reshape, Flatten, Dropout
from keras.layers import Input, Embedding, Dense
from keras.models import Model, Sequential
from keras.preprocessing import sequenceoutput_dimesion = 50
vocab_size = 8000
dropout_rate = 0.2
emb_dim = 200
max_len = 300
nb_filters = 100
init_W = Nonemax_features = vocab_size
vanila_dimension = 200
projection_dimension = output_dimesionfilter_lengths = [3, 4, 5]
'''Input'''
doc_input = Input(shape=(max_len,), dtype='int32', name='doc_input')
print("Builded input...")'''Embedding Layer'''
if init_W is None:
sentence_embeddings = Embedding(output_dim=emb_dim, input_dim=max_features, input_length=max_len, name='sentence_embeddings')(doc_input)
else:
sentence_embeddings = Embedding(output_dim=emb_dim, input_dim=max_features, input_length=max_len, weights=[init_W / 20], name='sentence_embeddings')(doc_input)print("Builded Embedding Layer...")
'''Reshape Layer'''
flatten_ = []
reshape = Reshape(target_shape=(max_len, emb_dim, 1), name='reshape')(sentence_embeddings) # chanels last
print("Builded Reshape Layer...")
for i in filter_lengths:
model_internal = Sequential()
model_internal.add(Conv2D(nb_filters, (i, emb_dim), activation="relu", name='conv2d_' + str(i), input_shape=(self.max_len, emb_dim, 1)))
# chanels last,默認了strides=(1,1), padding='valid'
model_internal.add(MaxPooling2D(pool_size=(self.max_len - i + 1, 1), name='maxpool2d_' + str(i)))
model_internal.add(Flatten())
flatten = model_internal(reshape)
flatten_.append(flatten)
# 以上for循環體內的代碼要縮進
'''Fully Connect Layer & Dropout Layer'''
fully_connect = Dense(vanila_dimension, activation='tanh', name='fully_connect')(concatenate(flatten_, axis=-1))
dropout = Dropout(dropout_rate, name='dropout')(fully_connect)
print("Builded Fully Connect Layer & Dropout Layer...")
'''Projection Layer & Output Layer'''
pj = Dense(projection_dimension, activation='tanh', name='output') # output layer
projection = pj(dropout)
print("Builded Projection Layer & Output Layer...")
# Compile Model: set inputs & outputs ...
model = Model(inputs=doc_input, outputs=projection)
model.compile(optimizer='rmsprop', loss='mse')
參考資料: 一、keras0.3.3文檔關於model的部分:https://github.com/cartopy/keras-0.3.3/blob/master/docs/templates/models.md (keras0.3.3的文檔很難找,一開始找到的老是2.x的文檔,後來baidu+google+bing找到了github上的這篇,目前還不會用github,聽說很好找東西,用git??,值得研究) 介紹了keras0.x中用的兩種模型, 二、keras2.x文檔很好找,重點關注Models和各類Layers