Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification模型实现
1. Attention
H是一个矩阵,它是由LSTM层产生的多个向量 [ h 1 , h 2 , … , h T ] [h_1,h_2,\dots,h_T] [h1,h2,…,hT]组成的。其中T是句子的长度。句子的表示 r r r是输出向量的权重之和。
M = t a n ( H ) M = tan(H) M=tan(H) α = s o f t m a x ( W T M ) \alpha =softmax(W^TM) α=softmax(WTM) r = H α T r=H\alpha^T r=HαT
output_h = tf.add(output_forward, output_backward) # total_num, time_steps,gru_size
output_h的维度为[total_num, time_steps,gru_size],其中total_num指的是batch_size,time_steps指的是length of sentence。
attention_w = tf.get_variable(‘attention_omega’, [gru_size, 1]) #attention_w本质上是每个GRU单元不同的权重,也就是对每个单词赋予不同的权重。
- t1 = tf.tanh(output_h) #[total_num, time_steps,gru_size]
- t2 = t1.reshape(t1, [total_numtime_steps, gru_size]) #total_numtime_steps, gru_size
- t3 = tf.matmul(t2, attention_w)#total_num*time_steps,1
- t4 = tf.reshape(t3, [total_num, time_steps]) #total_num * time_steps
- t5 = tf.nn.softmax(t4) # softmax是针对每个词来说的, total_num * time_steps
- t6 = tf.reshape(t5, [total_num, 1, time_steps])
- t7 = tf.matmul(t6, output_h) # total_num * 1 * gru_size,其中三维张量的乘法,第一维不变(第一维比较相同),后两维进行二维矩阵的乘法。
- r = tf.reshape(t7, [total_num, gru_size])
- result = tf.tanh( r )
另外的实现代码如下所示:
def attention(inputs): # Trainable parameters # B*T*H B*T*1 hidden_size = inputs.shape[2].value u_omega = tf.get_variable("u_omega", [hidden_size], initializer=tf.keras.initializers.glorot_normal()) with tf.name_scope('v'): v = tf.tanh(inputs) # For each of the timestamps its vector of size A from `v` is reduced with `u` vector vu = tf.tensordot(v, u_omega, axes=1, name='vu') # (B,T) shape alphas = tf.nn.softmax(vu, name='alphas') # (B,T) shape # Output of (Bi-)RNN is reduced with attention vector; the result has (B,D) shape output = tf.reduce_sum(inputs * tf.expand_dims(alphas, -1), 1) # Final output with tanh output = tf.tanh(output) return output, alphas 2. BLSTM
class AttLSTM: def __init__(self, sequence_length, num_classes, vocab_size, embedding_size, hidden_size, l2_reg_lambda=0.0): # Placeholders for input, output and dropout self.input_text = tf.placeholder(tf.int32, shape=[None, sequence_length], name='input_text') self.input_y = tf.placeholder(tf.float32, shape=[None, num_classes], name='input_y') self.emb_dropout_keep_prob = tf.placeholder(tf.float32, name='emb_dropout_keep_prob') self.rnn_dropout_keep_prob = tf.placeholder(tf.float32, name='rnn_dropout_keep_prob') self.dropout_keep_prob = tf.placeholder(tf.float32, name='dropout_keep_prob') initializer = tf.keras.initializers.glorot_normal # Word Embedding Layer with tf.device('/cpu:0'), tf.variable_scope("word-embeddings"): self.W_text = tf.Variable(tf.random_uniform([vocab_size, embedding_size], -0.25, 0.25), name="W_text") self.embedded_chars = tf.nn.embedding_lookup(self.W_text, self.input_text) # Dropout for Word Embedding with tf.variable_scope('dropout-embeddings'): self.embedded_chars = tf.nn.dropout(self.embedded_chars, self.emb_dropout_keep_prob) # Bidirectional LSTM with tf.variable_scope("bi-lstm"): _fw_cell = tf.nn.rnn_cell.LSTMCell(hidden_size, initializer=initializer()) fw_cell = tf.nn.rnn_cell.DropoutWrapper(_fw_cell, self.rnn_dropout_keep_prob) _bw_cell = tf.nn.rnn_cell.LSTMCell(hidden_size, initializer=initializer()) bw_cell = tf.nn.rnn_cell.DropoutWrapper(_bw_cell, self.rnn_dropout_keep_prob) self.rnn_outputs, _ = tf.nn.bidirectional_dynamic_rnn(cell_fw=fw_cell, cell_bw=bw_cell, inputs=self.embedded_chars, sequence_length=self._length(self.input_text), dtype=tf.float32) self.rnn_outputs = tf.add(self.rnn_outputs[0], self.rnn_outputs[1]) # Attention with tf.variable_scope('attention'): self.attn, self.alphas = attention(self.rnn_outputs) # Dropout with tf.variable_scope('dropout'): self.h_drop = tf.nn.dropout(self.attn, self.dropout_keep_prob) # Fully connected layer with tf.variable_scope('output'): self.logits = tf.layers.dense(self.h_drop, num_classes, kernel_initializer=initializer()) self.predictions = tf.argmax(self.logits, 1, name="predictions") # Calculate mean cross-entropy loss with tf.variable_scope("loss"): losses = tf.nn.softmax_cross_entropy_with_logits_v2(logits=self.logits, labels=self.input_y) self.l2 = tf.add_n([tf.nn.l2_loss(v) for v in tf.trainable_variables()]) self.loss = tf.reduce_mean(losses) + l2_reg_lambda * self.l2 # Accuracy with tf.variable_scope("accuracy"): correct_predictions = tf.equal(self.predictions, tf.argmax(self.input_y, 1)) self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, tf.float32), name="accuracy") # Length of the sequence data @staticmethod def _length(seq): relevant = tf.sign(tf.abs(seq)) length = tf.reduce_sum(relevant, reduction_indices=1) length = tf.cast(length, tf.int32) return length 3. 训练模型
def train(): with tf.device('/cpu:0'): x_text, y = data_helpers.load_data_and_labels(FLAGS.train_path) # Build vocabulary # Example: x_text[3] = "A misty <e1>ridge</e1> uprises from the <e2>surge</e2>." # ['a misty ridge uprises from the surge <UNK> <UNK> ... <UNK>'] # => # [27 39 40 41 42 1 43 0 0 ... 0] # dimension = FLAGS.max_sentence_length vocab_processor = tf.contrib.learn.preprocessing.VocabularyProcessor(FLAGS.max_sentence_length) x = np.array(list(vocab_processor.fit_transform(x_text))) print("Text Vocabulary Size: {:d}".format(len(vocab_processor.vocabulary_))) print("x = {0}".format(x.shape)) print("y = {0}".format(y.shape)) print("") # Randomly shuffle data to split into train and test(dev) np.random.seed(10) shuffle_indices = np.random.permutation(np.arange(len(y))) x_shuffled = x[shuffle_indices] y_shuffled = y[shuffle_indices] # Split train/test set # TODO: This is very crude, should use cross-validation dev_sample_index = -1 * int(FLAGS.dev_sample_percentage * float(len(y))) x_train, x_dev = x_shuffled[:dev_sample_index], x_shuffled[dev_sample_index:] y_train, y_dev = y_shuffled[:dev_sample_index], y_shuffled[dev_sample_index:] print("Train/Dev split: {:d}/{:d}\n".format(len(y_train), len(y_dev))) with tf.Graph().as_default(): session_conf = tf.ConfigProto( allow_soft_placement=FLAGS.allow_soft_placement, log_device_placement=FLAGS.log_device_placement) session_conf.gpu_options.allow_growth = FLAGS.gpu_allow_growth sess = tf.Session(config=session_conf) with sess.as_default(): model = AttLSTM( sequence_length=x_train.shape[1], num_classes=y_train.shape[1], vocab_size=len(vocab_processor.vocabulary_), embedding_size=FLAGS.embedding_dim, hidden_size=FLAGS.hidden_size, l2_reg_lambda=FLAGS.l2_reg_lambda) # Define Training procedure global_step = tf.Variable(0, name="global_step", trainable=False) optimizer = tf.train.AdadeltaOptimizer(FLAGS.learning_rate, FLAGS.decay_rate, 1e-6) gvs = optimizer.compute_gradients(model.loss) capped_gvs = [(tf.clip_by_value(grad, -1.0, 1.0), var) for grad, var in gvs] train_op = optimizer.apply_gradients(capped_gvs, global_step=global_step) # Output directory for models and summaries timestamp = str(int(time.time())) out_dir = os.path.abspath(os.path.join(os.path.curdir, "runs", timestamp)) print("Writing to {}\n".format(out_dir)) # Summaries for loss and accuracy loss_summary = tf.summary.scalar("loss", model.loss) acc_summary = tf.summary.scalar("accuracy", model.accuracy) # Train Summaries train_summary_op = tf.summary.merge([loss_summary, acc_summary]) train_summary_dir = os.path.join(out_dir, "summaries", "train") train_summary_writer = tf.summary.FileWriter(train_summary_dir, sess.graph) # Dev summaries dev_summary_op = tf.summary.merge([loss_summary, acc_summary]) dev_summary_dir = os.path.join(out_dir, "summaries", "dev") dev_summary_writer = tf.summary.FileWriter(dev_summary_dir, sess.graph) # Checkpoint directory. Tensorflow assumes this directory already exists so we need to create it checkpoint_dir = os.path.abspath(os.path.join(out_dir, "checkpoints")) checkpoint_prefix = os.path.join(checkpoint_dir, "model") if not os.path.exists(checkpoint_dir): os.makedirs(checkpoint_dir) saver = tf.train.Saver(tf.global_variables(), max_to_keep=FLAGS.num_checkpoints) # Write vocabulary vocab_processor.save(os.path.join(out_dir, "vocab")) # Initialize all variables sess.run(tf.global_variables_initializer()) # Pre-trained word2vec if FLAGS.embedding_path: pretrain_W = utils.load_glove(FLAGS.embedding_path, FLAGS.embedding_dim, vocab_processor) sess.run(model.W_text.assign(pretrain_W)) print("Success to load pre-trained word2vec model!\n") # Generate batches batches = data_helpers.batch_iter(list(zip(x_train, y_train)), FLAGS.batch_size, FLAGS.num_epochs) # Training loop. For each batch... best_f1 = 0.0 # For save checkpoint(model) for batch in batches: x_batch, y_batch = zip(*batch) # Train feed_dict = { model.input_text: x_batch, model.input_y: y_batch, model.emb_dropout_keep_prob: FLAGS.emb_dropout_keep_prob, model.rnn_dropout_keep_prob: FLAGS.rnn_dropout_keep_prob, model.dropout_keep_prob: FLAGS.dropout_keep_prob } _, step, summaries, loss, accuracy = sess.run( [train_op, global_step, train_summary_op, model.loss, model.accuracy], feed_dict) train_summary_writer.add_summary(summaries, step) # Training log display if step % FLAGS.display_every == 0: time_str = datetime.datetime.now().isoformat() print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy)) # Evaluation if step % FLAGS.evaluate_every == 0: print("\nEvaluation:") feed_dict = { model.input_text: x_dev, model.input_y: y_dev, model.emb_dropout_keep_prob: 1.0, model.rnn_dropout_keep_prob: 1.0, model.dropout_keep_prob: 1.0 } summaries, loss, accuracy, predictions = sess.run( [dev_summary_op, model.loss, model.accuracy, model.predictions], feed_dict) dev_summary_writer.add_summary(summaries, step) time_str = datetime.datetime.now().isoformat() f1 = f1_score(np.argmax(y_dev, axis=1), predictions, labels=np.array(range(1, 19)), average="macro") print("{}: step {}, loss {:g}, acc {:g}".format(time_str, step, loss, accuracy)) print("[UNOFFICIAL] (2*9+1)-Way Macro-Average F1 Score (excluding Other): {:g}\n".format(f1)) # Model checkpoint if best_f1 < f1: best_f1 = f1 path = saver.save(sess, checkpoint_prefix + "-{:.3g}".format(best_f1), global_step=step) print("Saved model checkpoint to {}\n".format(path)) 4.验证模型
def test(): with tf.device('/cpu:0'): x_text, y = data_helpers.load_data_and_labels(FLAGS.test_path) # Map data into vocabulary text_path = os.path.join(FLAGS.checkpoint_dir, "..", "vocab") text_vocab_processor = tf.contrib.learn.preprocessing.VocabularyProcessor.restore(text_path) x = np.array(list(text_vocab_processor.transform(x_text))) checkpoint_file = tf.train.latest_checkpoint(FLAGS.checkpoint_dir) graph = tf.Graph() with graph.as_default(): session_conf = tf.ConfigProto( allow_soft_placement=FLAGS.allow_soft_placement, log_device_placement=FLAGS.log_device_placement) session_conf.gpu_options.allow_growth = FLAGS.gpu_allow_growth sess = tf.Session(config=session_conf) with sess.as_default(): # Load the saved meta graph and restore variables saver = tf.train.import_meta_graph("{}.meta".format(checkpoint_file)) saver.restore(sess, checkpoint_file) # Get the placeholders from the graph by name input_text = graph.get_operation_by_name("input_text").outputs[0] # input_y = graph.get_operation_by_name("input_y").outputs[0] emb_dropout_keep_prob = graph.get_operation_by_name("emb_dropout_keep_prob").outputs[0] rnn_dropout_keep_prob = graph.get_operation_by_name("rnn_dropout_keep_prob").outputs[0] dropout_keep_prob = graph.get_operation_by_name("dropout_keep_prob").outputs[0] # Tensors we want to evaluate predictions = graph.get_operation_by_name("output/predictions").outputs[0] # Generate batches for one epoch batches = data_helpers.batch_iter(list(x), FLAGS.batch_size, 1, shuffle=False) # Collect the predictions here preds = [] for x_batch in batches: pred = sess.run(predictions, {input_text: x_batch, emb_dropout_keep_prob: 1.0, rnn_dropout_keep_prob: 1.0, dropout_keep_prob: 1.0}) preds.append(pred) preds = np.concatenate(preds) truths = np.argmax(y, axis=1) prediction_path = os.path.join(FLAGS.checkpoint_dir, "..", "predictions.txt") truth_path = os.path.join(FLAGS.checkpoint_dir, "..", "ground_truths.txt") prediction_file = open(prediction_path, 'w') truth_file = open(truth_path, 'w') for i in range(len(preds)): prediction_file.write("{}\t{}\n".format(i, utils.label2class[preds[i]])) truth_file.write("{}\t{}\n".format(i, utils.label2class[truths[i]])) prediction_file.close() truth_file.close() perl_path = os.path.join(os.path.curdir, "SemEval2010_task8_all_data", "SemEval2010_task8_scorer-v1.2", "semeval2010_task8_scorer-v1.2.pl") process = subprocess.Popen(["perl", perl_path, prediction_path, truth_path], stdout=subprocess.PIPE) for line in str(process.communicate()[0].decode("utf-8")).split("\\n"): print(line)