前言
經過數據預處理,現在開始正式編寫代碼。。。
數據預處理部分,請參考:Tensorflow使用LSTM實現中文文本分類(一)
整體結構
代碼流程圖:
- 對詞表進行 embeding
- 構建 lstm 層
- 構建 fc 層
- 構建 train_op
- 訓練流程
其中需要封裝的幾個方法:
- 數據集的封裝
api: next_batch(batch_size) 獲得樣本batch - 詞表封裝
api:sentence2id(text_sentence) 將 句子 轉化爲 id - 類別的封裝
api:categoryid2id(text_category) 將 類別 轉化爲 id
代碼演示
# -*- coding:utf-8 -*-
import tensorflow as tf
import os
import sys
import numpy as np
import math
# 打印出 log
tf.logging.set_verbosity(tf.logging.INFO)
# lstm 需要的參數
def get_default_params():
return tf.contrib.training.HParams(
num_embedding_size = 16, # 每個詞語的向量的長度
# 指定 lstm 的 步長, 一個sentence中會有多少個詞語
# 因爲執行的過程中是用的minibatch,每個batch之間還是需要對齊的
# 在測試時,可以是一個變長的
num_timesteps = 50, # 在一個sentence中 有 50 個詞語
num_lstm_nodes = [32, 32], # 每一層的size是多少
num_lstm_layers = 2, # 和上句的len 是一致的
# 有 兩層 神經單元,每一層都是 32 個 神經單元
num_fc_nodes = 32, # 全連接的節點數
batch_size = 100,
clip_lstm_grads = 1.0,
# 控制lstm的梯度,因爲lstm很容易梯度爆炸或者消失
# 這種方式就相當於給lstm設置一個上限,如果超過了這個上限,就設置爲這個值
learning_rate = 0.001,
num_word_threshold = 10, # 詞頻太少的詞,對於模型訓練是沒有幫助的,因此設置一個門限
)
hps = get_default_params() # 生成 參數 對象
# 設置文件路徑
train_file = './news_data/cnews.train.seg.txt'
val_file = './news_data/cnews.val.seg.txt'
test_file = './news_data/cnews.test.seg.txt'
vocab_file = './news_data/cnews.vocab.txt' # 統計的詞頻
category_file = './news_data/cnews.category.txt' # 標籤
output_folder = './news_data/run_text_rnn'
if not os.path.exists(output_folder):
os.mkdir(output_folder)
class Vocab:
'''
詞表的封裝
'''
def __init__(self, filename, num_word_threahold):
# 每一個詞,給她一個id,另外還要統計詞頻。ps:前面帶下劃線的爲私有成員
self._word_to_id = {}
self._unk = -1 # 先給 unk 賦值一個 負值,然後根據實際情況在賦值
self._num_word_theshold = num_word_threahold # 低於 這個值 就忽略掉該詞
self._read_dict(filename) # 讀詞表方法
def _read_dict(self, filename):
'''
讀這個詞表
:param filename: 路徑
:return: none
'''
with open(filename, 'r') as f:
lines = f.readlines()
for line in lines:
word, frequency = line.strip('\n').split('\t')
word = word # 獲得 單詞
frequency = int(frequency) # 獲得 頻率
if frequency < self._num_word_theshold:
continue # 門限過濾一下
idx = len(self._word_to_id) #這裏使用了一個id遞增的小技巧
if word == '<UNK>': # 如果是空格,就把上一個id號給它
# 如果是 unk的話, 就特殊處理一下
self._unk = idx
self._word_to_id[word] = idx
# 如果 word 存在,就把 idx 當做值,將其綁定到一起
# 如果 word 在詞表中不存在,就把nuk的值賦予它
def word_to_id(self, word):
'''
爲單詞分配id值
:param word: 單詞
:return:
'''
# 字典.get() 如果有值,返回值;無值,返回默認值(就是第二個參數)
return self._word_to_id.get(word, self._unk)
def sentence_to_id(self, sentence):
'''
將句子 轉換成 id 向量
:param sentence: 要輸入的句子(分詞後的句子)
:return:
'''
# 單條句子的id vector
word_ids = [self.word_to_id(cur_word) for cur_word in sentence.split(' ')]
# cur_word 有可能不存在,需要使用函數進行過濾一下
return word_ids
# 定義幾個 訪問私有成員屬性的方法
# Python內置的 @ property裝飾器就是負責把一個方法變成屬性調用的
@ property
def unk(self):
return self._unk
def size(self):
return len(self._word_to_id)
class CategoryDict:
'''
和 詞表的 方法 幾乎一樣
'''
def __init__(self, filename):
self._category_to_id = {}
with open(filename, 'r') as f:
lines = f.readlines()
for line in lines:
category = line.strip('\r\n')
idx = len(self._category_to_id)
self._category_to_id[category] = idx
def size(self):
return len(self._category_to_id)
def category_to_id(self, category):
if not category in self._category_to_id:
raise Exception('%s is not in our category list' % category)
return self._category_to_id[category]
# 獲得 詞表 對象
vocab = Vocab(vocab_file, hps.num_word_threshold)
# 詞表長度
vocab_size = vocab.size()
# 獲得 類別表 對象
category_vocab = CategoryDict(category_file)
# 類別 總數
num_classes = category_vocab.size()
# 封裝數據集
class TextDataSet:
'''
數據集 封裝
功能: 1、將數據集向量化。2、返回batch
'''
def __init__(self, filename, vocab, category_vocab, num_timesteps):
'''
封裝數據集
:param filename: 可以是訓練數據集、測試數據集、驗證數據集等
:param vocab: 詞表 對象
:param category_vocab: 類別 對象
:param num_timesteps: 步長 (sentence的總長度)
'''
# 將 各個對象 賦值
self._vocab = vocab
self._category_vocab = category_vocab
self._num_timesteps = num_timesteps
# matrix
self._inputs = []
# vector
self._outputs = []
# batch 起始點
self._indicator = 0
# 將文本數據 解析 成 matrix
self._parse_file(filename) # 進行解析
def _parse_file(self, filename):
tf.logging.info('Loading data from %s', filename)
with open(filename, 'r') as f:
lines = f.readlines()
for line in lines:
label, content = line.strip('\n').split('\t')
# 得到 一個 label 的 id
id_label = self._category_vocab.category_to_id(label)
# 得到 一個 vector
id_words = self._vocab.sentence_to_id(content)
# 需要在每一個minibatch上進行對齊,對 word 進行 對齊 操作
# 如果 超出了界限,就 截斷, 如果 不足,就 填充
id_words = id_words[0: self._num_timesteps] # 超過了 就 截斷
# 低於 num_timesteps 就填充,也就是說,上一句和下面兩句 可以完全並列寫,神奇!!
# 這裏的編碼方式感覺很巧妙!!!
padding_num = self._num_timesteps - len(id_words)
id_words = id_words + [self._vocab.unk for i in range(padding_num)]
self._inputs.append(id_words)
self._outputs.append(id_label)
# 轉變爲 numpy 類型
self._inputs = np.asarray(self._inputs, dtype=np.int32)
self._outputs = np.asarray(self._outputs, dtype=np.int32)
# 對數據進行隨機化
self._random_shuffle()
self._num_sample = len(self._inputs)
def _random_shuffle(self):
p = np.random.permutation(len(self._inputs))
self._inputs = self._inputs[p]
self._outputs = self._outputs[p]
def next_batch(self, batch_size):
end_indicator = self._indicator + batch_size
if end_indicator > len(self._inputs):
self._random_shuffle()
self._indicator = 0
end_indicator = batch_size
if end_indicator > len(self._inputs):
raise Exception('batch_size: %d is too large' % batch_size)
batch_inputs = self._inputs[self._indicator: end_indicator]
batch_outputs = self._outputs[self._indicator: end_indicator]
self._indicator = end_indicator
return batch_inputs, batch_outputs
def num_samples(self):
return self._num_sample
# 得到 三個 文本對象,當中都包含了 input 和 label
train_dataset = TextDataSet(train_file, vocab, category_vocab, hps.num_timesteps)
val_dataset = TextDataSet(val_file, vocab, category_vocab, hps.num_timesteps)
test_dataset = TextDataSet(test_file, vocab, category_vocab, hps.num_timesteps)
# 開始計算圖模型 (重點)
def create_model(hps, vocab_size, num_classes):
'''
構建lstm
:param hps: 參數對象
:param vocab_size: 詞表 長度
:param num_classes: 分類數目
:return:
'''
num_timesteps = hps.num_timesteps # 一個句子中 有 num_timesteps 個詞語
batch_size = hps.batch_size
# 設置兩個 placeholder, 內容id 和 標籤id
inputs = tf.placeholder(tf.int32, (batch_size, num_timesteps))
outputs = tf.placeholder(tf.int32, (batch_size, ))
# dropout keep_prob 表示要keep多少值,丟掉的是1-keep_prob
keep_prob = tf.placeholder(tf.float32, name='keep_prob')
global_step = tf.Variable(
tf.zeros([], tf.int64),
name='global_step',
trainable = False) # 可以保存 當前訓練到了 哪一步,而且不訓練
# 隨機的在均勻分佈下初始化, 構建 embeding 層
embeding_initializer = tf.random_uniform_initializer(-1.0, 1.0)
# 和 name_scope 作用是一樣的,他可以定義指定 initializer
# tf.name_scope() 和 tf.variable_scope() 的區別 參考:
# https://www.cnblogs.com/adong7639/p/8136273.html
with tf.variable_scope('embedding', initializer=embeding_initializer):
# tf.varialble_scope() 一般 和 tf.get_variable() 進行配合
# 構建一個 embedding 矩陣,shape 是 [詞表的長度, 每個詞的embeding長度 ]
embeddings = tf.get_variable('embedding', [vocab_size, hps.num_embedding_size], tf.float32)
# 每一個詞,都要去embedding中查找自己的向量
# [1, 10, 7] 是一個句子,根據 embedding 進行轉化
# 如: [1, 10, 7] -> [embedding[1], embedding[10], embedding[7]]
embeding_inputs = tf.nn.embedding_lookup(embeddings, inputs)
# 上句的輸入: Tensor("embedding/embedding_lookup:0", shape=(100, 50, 16), dtype=float32)
# 輸出是一個三維矩陣,分別是:100 是 batch_size 大小,50 是 句子中的單詞數量,16 爲 embedding 向量長度
# lstm 層
# 輸入層 大小 加上 輸出層的大小,然後開方
scale = 1.0 / math.sqrt(hps.num_embedding_size + hps.num_lstm_nodes[-1]) / 3.0
lstm_init = tf.random_uniform_initializer(-scale, scale)
with tf.variable_scope('lstm_nn', initializer = lstm_init):
cells = [] # 保存兩個lstm層
# 循環這兩層 lstm
for i in range(hps.num_lstm_layers):
# BasicLSTMCell類是最基本的LSTM循環神經網絡單元。
# 輸入參數和BasicRNNCell差不多, 設置一層 的 lstm 神經元
cell = tf.contrib.rnn.BasicLSTMCell(
hps.num_lstm_nodes[i], # 每層的 節點個數
state_is_tuple = True # 中間狀態是否是一個元組
)
cell = tf.contrib.rnn.DropoutWrapper( # 進行 dropout
cell,
output_keep_prob = keep_prob # dropout 的 比例
)
cells.append(cell)
cell = tf.contrib.rnn.MultiRNNCell(cells)
# 該方法的作用是:將兩層的lstm 連到一起,比如:上層的輸出是下層的輸入
# 此時的cell,已經是一個多層的lstm,但是可以當做單層的來操作,比較簡單
# 保存中間的一個隱含狀態,隱含狀態在初始化的時候初始化爲0,也就是零矩陣
initial_state = cell.zero_state(batch_size, tf.float32)
# rnn_outputs: [batch_size, num_timesteps, lstm_outputs[-1](最後一層的輸出)]
# _ 代表的是隱含狀態
rnn_outputs, _ = tf.nn.dynamic_rnn(
cell, embeding_inputs, initial_state = initial_state
) # 現在的rnn_outputs 代表了每一步的輸出
# 獲得最後一步的輸出,也就是說,最後一個step的最後一層的輸出
last = rnn_outputs[:, -1, :]
# print(last) Tensor("lstm_nn/strided_slice:0", shape=(100, 32), dtype=float32)
# 將最後一層的輸出 鏈接到一個全連接層上
# 參考鏈接:https://www.w3cschool.cn/tensorflow_python/tensorflow_python-fy6t2o0o.html
fc_init = tf.uniform_unit_scaling_initializer(factor=1.0)
with tf.variable_scope('fc', initializer = fc_init): # initializer 此範圍內變量的默認初始值
fc1 = tf.layers.dense(last,
hps.num_fc_nodes,
activation = tf.nn.relu,
name = 'fc1')
# 進行 dropout
fc1_dropout = tf.nn.dropout(fc1, keep_prob)
# 進行更換 參考:https://blog.csdn.net/UESTC_V/article/details/79121642
logits = tf.layers.dense(fc1_dropout, num_classes, name='fc2')
# 沒有東西需要初始化,所以可以直接只用name_scope()
with tf.name_scope('metrics'):
softmax_loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
logits = logits,
labels = outputs
)
# 該方法 做了三件事:1,labels 做 onehot,logits 計算softmax概率,3. 做交叉熵
loss = tf.reduce_mean(softmax_loss)
#
y_pred = tf.argmax(
tf.nn.softmax(logits),
1,
#output_type = tf.int64
)
# 這裏做了 巨大 修改,如果問題,優先檢查這裏!!!!!!
#print(type(outputs), type(y_pred))
correct_pred = tf.equal(outputs, tf.cast(y_pred, tf.int32)) # 這裏也做了修改
accuracy = tf.reduce_mean(tf.cast(correct_pred, tf.float32))
with tf.name_scope('train_op'):
tvars = tf.trainable_variables() # 獲取所有可以訓練的變量
for var in tvars:
tf.logging.info('variable name: %s' % (var.name)) # 打印出所有可訓練變量
# 對 梯度進行 截斷.
# grads是截斷之後的梯度
grads, _ = tf.clip_by_global_norm(
tf.gradients(loss, tvars), # 在可訓練的變量的梯度
hps.clip_lstm_grads
) # 可以 獲得 截斷後的梯度
optimizer = tf.train.AdamOptimizer(hps.learning_rate) # 將每個梯度應用到每個變量上去
train_op = optimizer.apply_gradients(
zip(grads, tvars), # 將 梯度和參數 綁定起來
global_step = global_step # 這個參數 等會兒,再好好研究一下
)
return ((inputs, outputs, keep_prob),
(loss, accuracy),
(train_op, global_step))
placeholders, metrics, others = create_model(
hps, vocab_size, num_classes
)
inputs, outputs, keep_prob = placeholders
loss, accuracy = metrics
train_op, global_step = others
init_op = tf.global_variables_initializer()
train_keep_prob_value = 0.8
test_keep_prob_value = 1.0
num_train_steps = 100000
# 驗證集、測試集 輸出函數
def eval_holdout(sess, dataset_for_test, batch_size):
# 計算出 該數據集 有多少batch
num_batches = dataset_for_test.num_samples() // batch_size # // 整除 向下取整
accuracy_vals = []
loss_vals = []
for i in range(num_batches):
batch_inputs, batch_labels = dataset_for_test.next_batch(batch_size)
accuracy_val, loss_val = sess.run([accuracy, loss],
feed_dict={
inputs: batch_inputs,
outputs: batch_labels,
keep_prob: train_keep_prob_value
}
)
accuracy_vals.append(accuracy_val)
loss_vals.append(loss_val)
return np.mean(accuracy_vals), np.mean(loss_vals)
with tf.Session() as sess:
sess.run(init_op)
for i in range(num_train_steps):
batch_inputs, batch_labels = train_dataset.next_batch(hps.batch_size)
outputs_val = sess.run(
[loss, accuracy, train_op, global_step],
feed_dict={
inputs: batch_inputs,
outputs: batch_labels,
keep_prob:train_keep_prob_value
}
)
loss_val, accuracy_val, _, global_step_val = outputs_val
if global_step_val % 200 == 0:
tf.logging.info(
'Step: %5d, loss: %3.3f, accuracy: %3.3f'%(global_step_val, loss_val, accuracy_val))
if global_step_val % 1000 == 0:
validdata_accuracy, validdata_loss = eval_holdout(sess, val_dataset, hps.batch_size)
testdata_accuracy, testdata_loss = eval_holdout(sess, test_dataset, hps.batch_size)
tf.logging.info(
' valid_data Step: %5d, loss: %3.3f, accuracy: %3.5f' % (global_step_val, validdata_loss, validdata_accuracy))
tf.logging.info(
' test_data Step: %5d, loss: %3.3f, accuracy: %3.5f' % (global_step_val, testdata_loss, testdata_accuracy))
’‘’
結果:
INFO:tensorflow:Step: 10000, loss: 0.067, accuracy: 0.990
INFO:tensorflow: valid_data Step: 10000, loss: 0.614, accuracy: 0.90000
INFO:tensorflow: test_data Step: 10000, loss: 0.723, accuracy: 0.90000
‘’‘
訓練一萬次的訓練結果,在訓練集上準確率達到99%,測試集上90%, 驗證集上90%。
感覺還闊以 ?