tf.keras文本分類小例子
前幾天學(xué)了tf.keras,趁熱打鐵我就整了一個自己比較熟悉的文本分類任務(wù)來試試手,效果可能不是很重要,重要的是能把流程走通,所以一切從簡。
當(dāng)然,我這里不想只是簡單的弄個流程,碼面條那么簡單,我還是希望能整一次完整的工程化的代碼來練練手,2天時間,純手打歡迎各位前輩拍磚,也希望對各位有所幫助吧。
懶人目錄:
文件結(jié)構(gòu)和模塊劃分思路 預(yù)訓(xùn)練 分類模型 MAIN 預(yù)處理部分 word2vector 建模操作 小結(jié) 后續(xù)改進計劃 思路小結(jié)
文件結(jié)構(gòu)和模塊劃分思路
先聊聊整套方案的算法視角思路,文本分類任務(wù)的常規(guī)基線是TextCNN,這里為了簡單我只用了一個簡單的卷積層,而沒有用TextCNN里面那種復(fù)雜形式(有關(guān)這個模型的具體解釋詳見:NLP.TM[24] | TextCNN的個人理解)。
先來看核心代碼的文件夾結(jié)構(gòu):

cls,文本分類模型,以及具體的任務(wù)實驗。 ptm,預(yù)訓(xùn)練模型。 util,工具。
這里面的base_pipline.py是一套完整的流程化代碼,接近180行,覆蓋加載數(shù)據(jù)、預(yù)處理、訓(xùn)練、測試等全流程,當(dāng)然我不滿足于此,我把里面的關(guān)鍵步驟模塊化,形成一個個分別的模塊來分別實現(xiàn)。
預(yù)訓(xùn)練
預(yù)訓(xùn)練我單獨拉出來,沒有和整體模型放一起,主要是因為預(yù)訓(xùn)練模型需要單獨訓(xùn)練,也完整地維護了起來,這里我只寫了個最簡單的word2vector,模型部分使用的也只是調(diào)了gensim的包,來看看完整的類代碼。
import?numpy?as?np
from?nlu_model.util.pkl_impl?import?save_pkl,?load_pkl
from?gensim.models.word2vec?import?Word2Vec
class?Word2vector(object):
????def?__init__(self):
????????self.word2idx_dic?=?{}
????????self.embedding_weights?=?[]
????def?train(self,?
??????????????train_data,??????????????????????????#?訓(xùn)練數(shù)據(jù)
??????????????N_DIM?=?300,?????????????????????????#?word2vec的數(shù)量
??????????????MIN_COUNT?=?5,???????????????????????#?保證出現(xiàn)的詞數(shù)足夠做才進入詞典
??????????????w2v_EPOCH?=?15,??????????????????????#?w2v的訓(xùn)練迭代次數(shù)
??????????????MAXLEN?=?50??????????????????????????#?句子最大長度
??????????????):
????????self.N_DIM?=?N_DIM
????????self.MIN_COUNT?=?MIN_COUNT
????????self.w2v_EPOCH?=?w2v_EPOCH
????????self.MAXLEN?=?MAXLEN
????????#?Initialize?model?and?build?vocab
????????imdb_w2v?=?Word2Vec(size=N_DIM,?min_count=MIN_COUNT)
????????imdb_w2v.build_vocab(train_data)
????????#?Train?the?model?over?train_reviews?(this?may?take?several?minutes)
????????imdb_w2v.train(train_data,?total_examples=len(train_data),?epochs=w2v_EPOCH)
????????#?word2vec后處理
????????n_symbols?=?len(imdb_w2v.wv.vocab.keys())?+?2
????????embedding_weights?=?[[0?for?i?in?range(N_DIM)]?for?i?in?range(n_symbols)]
????????np.zeros((n_symbols,?300))
????????idx?=?1
????????word2idx_dic?=?{}
????????w2v_model_metric?=?[]
????????for?w?in?imdb_w2v.wv.vocab.keys():
????????????embedding_weights[idx]?=?imdb_w2v[w]
????????????word2idx_dic[w]?=?idx
????????????idx?=?idx?+?1
????????#?留給未登錄詞的位置
????????avg_weights?=?[0?for?i?in?range(N_DIM)]
????????for?wd?in?word2idx_dic:
????????????avg_weights?=?[(avg_weights[idx]+embedding_weights[word2idx_dic[wd]][idx])?for?idx?in?range(N_DIM)]
????????avg_weights?=?[avg_weights[idx]?/?len(word2idx_dic)?for?idx?in?range(N_DIM)]
????????embedding_weights[idx]?=?avg_weights
????????word2idx_dic["" ]?=?idx
????????#?留給pad的位置
????????word2idx_dic["" ]?=?0
????????self.word2idx_dic?=?word2idx_dic
????????self.embedding_weights?=?embedding_weights
????def?save(self,
?????????????word2idx_dic_path,???????????????????#?詞到ID詞典路徑
?????????????embedding_path,??????????????????????#?embedding詞向量路徑
?????????????model_conf_path?????????????????????#?模型配置加載)
?????????????):
??????
????????#?保存w2id詞典
????????save_pkl(word2idx_dic_path,?self.word2idx_dic)
????????#?保存詞向量矩陣
????????save_pkl(embedding_path,?self.embedding_weights)
????????#?保存配置
????????save_pkl(model_conf_path,?[self.N_DIM,?self.MIN_COUNT,?self.w2v_EPOCH,?self.MAXLEN])
????def?__load_default__(self):
????????self.load("./data/ptm/shopping_reviews/w2v_word2idx2020100601.pkl",
??????????????????"./data/ptm/shopping_reviews/w2v_model_metric_2020100601.pkl",?
??????????????????"./data/ptm/shopping_reviews/w2v_model_conf_2020100601.pkl")
????def?load(self,?word2idx_dic_path,?embedding_path,?model_conf_path):
????????self.N_DIM,?self.MIN_COUNT,?self.w2v_EPOCH,?self.MAXLEN?=?load_pkl(model_conf_path)
????????self.embedding_weights?=?load_pkl(embedding_path)
????????self.word2idx_dic?=?load_pkl(word2idx_dic_path)
????def?word2idx(self,?word):
????????if?len(self.word2idx_dic)?==?0:
????????????self.__load_default__()
????????if?word?in?self.word2idx_dic:
????????????return?self.word2idx_dic[word]
????????else:
????????????return?len(self.word2idx_dic)?-?1
????def?sentence2idx(self,?sentence):
????????sentence_idx?=?[]
????????for?idx?in?range(len(sentence)):
????????????sentence_idx.append(self.word2idx(sentence[idx]))
????????return?sentence_idx
????def?batch2idx(self,?source_data):
????????result_data?=?[]
????????for?idx?in?range(len(source_data)):
????????????result_data.append(self.sentence2idx(source_data[idx]))
????????return?result_data
????def?get_np_weights(self):
????????return?np.array(self.embedding_weights)
這里占比最大的是模型訓(xùn)練過程中的數(shù)據(jù)處理,剩下都是圍繞著這個訓(xùn)練完的word2vector做的一些操作,我來畫幾個重點吧:
詞匯的id化。tensorflow在訓(xùn)練過程中embedding_lookup本身是數(shù)字計算,所以所有的詞匯都要轉(zhuǎn)化為id,而這個id的生成其實來源于word2vector的訓(xùn)練,因此我把映射詞典也放在這個類里面維護了,那就包括了各種粒度的映射了。 模型和映射詞典以及一些必要的參數(shù)保存,我用的是pkl來進行保存,這個保存和加載都比較簡單,來看具體的 pkl_impl我是怎么寫的:
import?pickle
def?save_pkl(path,?data):
?output?=?open(path,?'wb')
?pickle.dump(data,?output)
?output.close()
def?load_pkl(path):
?pkl_file?=?open(path,?'rb')
?data?=?pickle.load(pkl_file)
?pkl_file.close()
?return?data
在詞典部分,我手動加了兩個特殊詞條:未登錄詞,對應(yīng)的詞向量是所有詞向量的均值,pad補全,對應(yīng)詞向量全都是0。
當(dāng)然,后續(xù)還可能有更多預(yù)訓(xùn)練的模型,自己可以再調(diào)整,這也是模塊化的好處,后續(xù)要更新模型,就和換零件一樣。
分類模型
分類模型這塊是分了兩層,一個cls模型類,一個具體的模型也把他整成一個類了(這個后續(xù)會整一個抽象類讓他繼承吧)。
首先看具體模型的類,textcnn_small,畢竟這個不是正兒八經(jīng)的那個textcnn。
from?tensorflow?import?keras
class?TextCNNSmall():
????"""docstring?for?TextCNNSmall"""
????def?__init__(self,?model_conf,?train_conf={"batch_size":64,"epochs":3,?"verbose":1}):
????????self.model_conf?=?model_conf
????????self.train_conf?=?train_conf
????????self.__build_structure__()
????def?__build_structure__(self):
????????inputs?=?keras.layers.Input(shape=(self.model_conf["MAX_LEN"],))
????????embedding_layer?=?keras.layers.Embedding(output_dim?=?self.model_conf["w2c_len"],
????????????????????????????????????input_dim?=?len(self.model_conf["emb_model"].embedding_weights),?
????????????????????????????????????weights=[self.model_conf["emb_model"].get_np_weights()],?
????????????????????????????????????input_length=self.model_conf["MAX_LEN"],?
????????????????????????????????????trainable=True
????????????????????????????????????)
????????x?=?embedding_layer(inputs)
????????l_conv1?=?keras.layers.Conv1D(filters=self.model_conf["w2c_len"],?kernel_size=3,?activation='relu')(x)??
????????l_pool1?=?keras.layers.MaxPool1D(pool_size=3)(l_conv1)
????????l_pool11?=?keras.layers.Flatten()(l_pool1)
????????out?=?keras.layers.Dropout(0.5)(l_pool11)
????????output?=?keras.layers.Dense(32,?activation='relu')(out)
?????????
????????pred?=?keras.layers.Dense(units=1,?activation='sigmoid')(output)
?????????
????????self.model?=?keras.models.Model(inputs=inputs,?outputs=pred)
????????self.model.summary()
????????self.model.compile(loss="binary_crossentropy",?optimizer="adam",?metrics=['accuracy'])
????def?fit(self,?x_train,?y_train,?x_test,?y_test):
????????history?=?self.model.fit(x_train,?y_train,?batch_size=self.train_conf.get("batch_size",?64),
????????????????????????????????epochs=self.train_conf.get("epochs",?3),
????????????????????????????????validation_data=(x_test,?y_test),
????????????????????????????????verbose=self.train_conf.get("verbose",?1))
????????return?history
????def?evaluate(self,?x_test,?y_test):
????????return?self.model.evaluate(x_test,?y_test)
????def?predict(self,?sentences):
????????return?self.model.predict(sentences)
????def?save(self,?path):
????????if?self.model:
????????????self.model.save(path)
????def?load(self,?path):
????????self.model?=?keras.load_model(path)
就科研而言模型還是核心,但實際上我還做了很多模型法之外的事情:
簡單的模型構(gòu)建,這塊沒什么難的了。 fit、evaluate、predict,這是經(jīng)典的3個模型關(guān)鍵步驟,訓(xùn)練、評估、預(yù)測,對于工程而言,最關(guān)鍵的應(yīng)該就是預(yù)測了。 模型的加載和保存,這塊我也涉及到了。 這里面的參數(shù)我都配置化了,從外面輸入進來,當(dāng)然這種字典的形式只是個權(quán)宜之計,后續(xù)我會匯總好形成各種接口來往外直接暴露。
模型類之上我還整了一個模型類,用來對各種同類型的模型進行維護。
import?jieba
from?tensorflow?import?keras
from?nlu_model.cls.model.textcnn_small?import?TextCNNSmall
class?ClsModel(object):
????def?__init__(self,?model_choice,?model_conf={},?train_conf={}):
????????self.model_choice?=?model_choice
????????self.model_conf?=?model_conf
????????self.train_conf?=?train_conf
????????self.__model_select__()
????def?__model_select__(self):
????????if?self.model_choice?==?"textcnn_small":
????????????self.model?=?TextCNNSmall(self.model_conf,?self.train_conf)
????????if?self.model_choice?==?"load":
????????????self.load(self.model_conf["path"])
????def?preprocess(self,?sentences):
????????sentences?=?[list(jieba.cut(i))?for?i?in?sentences]
????????sentence_id?=?self.model_conf["emb_model"].batch2idx(sentences)
????????return?keras.preprocessing.sequence.pad_sequences(sentence_id,
??????????????????????????????????????????????????????????value=0,
??????????????????????????????????????????????????????????padding='post',
??????????????????????????????????????????????????????????maxlen=50)
????def?fit(self,?x_train,?y_train,?x_test,?y_test):
????????return?self.model.fit(x_train,?y_train,?x_test,?y_test)
????def?evaluate(self,?x_test,?y_test):
????????return?self.model.evaluate(x_test,?y_test)
????def?pred(self,?sentence):
????????return?self.model.predict(sentence)
????def?predict(self,?sentences):
????????sentence_id?=?self.preprocess(sentences)
????????return?self.pred([sentence_id])[0][0]
????def?save(self,?path):
????????self.model.save(path)
????def?load(self,?path):
????????self.model?=?keras.models.load_model(path)
同樣畫畫重點。
這里的模型首先由 __model_select__來進行統(tǒng)一維護和選擇,目前支持兩種模式,textcnn_small即一個具體的模型,另外的load是加載模式,維護具體的一個模型。__用來區(qū)分是private類型成員還是public類型成員,即外界是否能讀到,一般不需要外界讀的盡量整成__,這里我還需要優(yōu)化。預(yù)處理的這塊工作具有一定的重用性,但是僅在文本分類中使用,因此我也維護在這里了,后續(xù)可以嘗試看看能不能放在util里面。 訓(xùn)練、評估、預(yù)測3連,但是這里我整了兩個預(yù)測,一個是直接針對預(yù)處理好的id化序列進行預(yù)測,另一個是針對原句來進行預(yù)測,其實可以看到后者調(diào)用了前者的那個函數(shù)。
MAIN
有了這些模塊,我們就需要把他們給串起來了,這里我用的是網(wǎng)上找的一套電商評論好壞評的分類數(shù)據(jù)(https://blog.csdn.net/churximi/article/details/61210129)
預(yù)處理部分
首先是預(yù)處理部分:
#?data?preprocess
def?loadfile():
????#?加載并預(yù)處理模型
????neg?=?pd.read_excel('./data/cls/shopping_reviews/neg.xls',?header=None,?index=None)
????pos?=?pd.read_excel('./data/cls/shopping_reviews/pos.xls',?header=None,?index=None)
????def?cw(x):?
????????punctuation?=?r"[\s+\.\!\/_,$%^*(+\"\']+|[+——!,。?、~@#¥%……&*():]+"
????????x?=?re.sub(punctuation,?"",?x)
????????return?list(jieba.cut(x))
????pos['words']?=?pos[0].apply(cw)
????neg['words']?=?neg[0].apply(cw)
????y?=?np.concatenate((np.ones(len(pos)),?np.zeros(len(neg))))
????x_train,?x_test,?y_train,?y_test?=?train_test_split(
????????np.concatenate((pos['words'],?neg['words'])),?y,?test_size=0.2)
????
????return?x_train,?x_test,?y_train,?y_test
x_train,?x_test,?y_train,?y_test?=?loadfile()
with?open("./data/cls/shopping_reviews/train.txt",?"w")?as?f:
????for?idx?in?range(len(x_train)):
????????f.write("%s\t%s\n"?%?(y_train[idx],?"?".join(x_train[idx])))
with?open("./data/cls/shopping_reviews/test.txt",?"w")?as?f:
????for?idx?in?range(len(x_test)):
????????f.write("%s\t%s\n"?%?(y_test[idx],?"?".join(x_test[idx])))
源文件在excel里面,我去了標(biāo)點后切詞,切完之后做了數(shù)據(jù)劃分,然后分別保存起來了。
word2vector
word2vector?=?Word2vector()
word2vector.train(x_train)
word2vector.save("./data/ptm/shopping_reviews/w2v_word2idx2020100601.pkl",
?????????????????"./data/ptm/shopping_reviews/w2v_model_metric_2020100601.pkl",?
?????????????????"./data/ptm/shopping_reviews/w2v_model_conf_2020100601.pkl")
然后是Word2vector的訓(xùn)練,可以看到這塊代碼非常簡潔方便,后續(xù)自己再用的時候就很舒服,所以我本身非常喜歡去包裝這些東西。
建模操作
建模這塊,主要有這幾個細(xì)節(jié)步驟:
訓(xùn)練測試數(shù)據(jù)的結(jié)構(gòu)轉(zhuǎn)化,要使其符合模型需要的類型。 模型建立和初始化。 訓(xùn)練、評估。 單獨測試case。
然后來看看代碼:
#?訓(xùn)練測試數(shù)據(jù)的結(jié)構(gòu)轉(zhuǎn)化,要使其符合模型需要的類型。
x_train?=?word2vector.batch2idx(x_train)
x_test?=?word2vector.batch2idx(x_test)
x_train?=?keras.preprocessing.sequence.pad_sequences(x_train,
????????????????????????????????????????????????????value=0,
????????????????????????????????????????????????????padding='post',
????????????????????????????????????????????????????maxlen=50)
x_test?=?keras.preprocessing.sequence.pad_sequences(x_test,
????????????????????????????????????????????????????value=0,
????????????????????????????????????????????????????padding='post',
????????????????????????????????????????????????????maxlen=50)
#?模型建立和初始化
model_conf?=?{"MAX_LEN":?50,
??????????????"w2c_len":?300,?
??????????????"emb_model":?word2vector}
train_conf?=?{"batch_size":?64,
??????????????"epochs":?5,?
??????????????"verbose":?1}
cls_model?=?ClsModel("textcnn_small",?model_conf,?train_conf)
#?訓(xùn)練、評估
cls_model.fit(x_train,?y_train,?x_test,?y_test)
print(cls_model.evaluate(x_test,?y_test))
cls_model.save("./data/cls/shopping_reviews/model_20201007")
#?單獨測試case,這里包括用新訓(xùn)練好的模型和保存加載后的模型
sentence?=?"這臺手機真性能還挺好的"
print(cls_model.predict([sentence]))
sentence?=?"這臺手機真性能還挺好的"
word2vector?=?Word2vector()
cls_model?=?ClsModel("load",?model_conf={"path":"./data/cls/shopping_reviews/model_20201007",?"emb_model":?word2vector},?train_conf={})
print(cls_model.predict([sentence]))
小結(jié)
后續(xù)改進計劃
這只是初步建立的一個架構(gòu),細(xì)節(jié)還不太完整,需要完善。
注釋太少。(為了在今晚發(fā)問,所以省了些大家不會介意吧) 在一個超大的項目下,其實還缺少打包、部署之類的框架類腳本和模塊,例如用flask等。 同樣是大項目下,有多個模型和詞典時,需要一個model_manager之類的來維護,有興趣的可以去看看jieba的源碼。 沒有日志、沒有耗時計算工具。 針對單字的操作可以做。
時候未到,尚未開源,敬請期待。
思路小結(jié)
會做實驗寫算法是一方面,會把自己的東西規(guī)范化、結(jié)構(gòu)化整上線又是另一回事,真正的算法工程師,要足夠全面,了解工程,最終才能夠完成這個項目,有了這個框架,自己嘗試更多的模型其實會更加快哈哈哈。
