Add first draft of normalizer trainer
diff --git a/tf-ner-poc/src/main/python/normalizer/normalizer.py b/tf-ner-poc/src/main/python/normalizer/normalizer.py
new file mode 100644
index 0000000..18e9338
--- /dev/null
+++ b/tf-ner-poc/src/main/python/normalizer/normalizer.py
@@ -0,0 +1,287 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+import re
+
+import tensorflow as tf
+import numpy as np
+
+from math import floor
+
+def load_data(file):
+    with open(file, encoding="utf-8") as f:
+        target = []
+        source = []
+        for line in f:
+            parts = re.split(r'\t+', line)
+            target.append(parts[0].strip());
+            source.append(parts[1].strip())
+    return source, target
+
+def encode_name(char_dict, names):
+
+    max_length = 0
+    for name in names:
+        length = len(name)
+        if length > max_length:
+            max_length = length
+
+    # TODO: To be able to use padding for variable length sequences
+    #       pad with the eos marker
+
+    encoded_names = np.zeros((len(names), max_length))
+
+    for bi in range(len(names)):
+        for ci in range(len(names[bi])):
+            encoded_names.itemset((bi, ci), char_dict[names[bi][ci]])
+
+    return encoded_names
+
+def mini_batch(target_char_dict, target, source_char_dict, source, batch_size, batch_index):
+
+    begin = batch_index
+    end = min(batch_index + batch_size, len(source))
+
+    target_batch = target[begin : end]
+
+    target_length = []
+    for i in range(begin, end):
+        target_length.append(len(target[i]) + 1) # TODO: The correction should be done in the graph ...
+
+    source_batch = source[batch_index : batch_index + batch_size]
+    source_length = []
+    for i in range(begin, end):
+        source_length.append(len(source[i]))
+
+    return encode_name(target_char_dict, target_batch), np.asarray(target_length), \
+           encode_name(source_char_dict, source_batch), np.asarray(source_length)
+
+def create_graph(mode, batch_size, encoder_nchars, max_target_length, decoder_nchars):
+
+    # Hyper  parameters
+    encoder_char_dim = 100
+    num_units = 256
+
+    # Encoder
+    encoder_char_ids_ph = tf.placeholder(tf.int32, shape=[batch_size, None], name="encoder_char_ids")
+    encoder_lengths_ph = tf.placeholder(tf.int32, shape=[batch_size], name="encoder_lengths")
+
+    encoder_embedding_weights = tf.get_variable(name="char_embeddings", dtype=tf.float32,
+                        shape=[encoder_nchars, encoder_char_dim])
+
+    encoder_emb_inp = tf.nn.embedding_lookup(encoder_embedding_weights, encoder_char_ids_ph)
+
+    encoder_emb_inp = tf.transpose(encoder_emb_inp, perm=[1, 0, 2])
+
+    encoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
+    initial_state = encoder_cell.zero_state(batch_size, dtype=tf.float32)
+
+    encoder_outputs, encoder_state = tf.nn.dynamic_rnn(
+        encoder_cell, encoder_emb_inp, initial_state=initial_state,
+        sequence_length=encoder_lengths_ph,
+        time_major=True, swap_memory=True)
+
+    # Decoder
+    decoder_char_ids_ph = tf.placeholder(tf.int32, shape=[batch_size, None], name="decoder_char_ids")
+    decoder_lengths = tf.placeholder(tf.int32, shape=[batch_size], name="decoder_lengths")
+
+    # decoder output (decoder_input shifted to the left by one)
+
+    decoder_char_dim = 100
+    decoder_embedding_weights = tf.get_variable(name="decoder_char_embeddings", dtype=tf.float32,
+                                             shape=[decoder_nchars, decoder_char_dim])
+
+    projection_layer = tf.layers.Dense(units=decoder_nchars, use_bias=True) # To predict one output char at a time ...
+
+    attention_states = tf.transpose(encoder_outputs, [1, 0, 2])
+
+    attention_mechanism = tf.contrib.seq2seq.LuongAttention(
+        num_units, attention_states,
+        memory_sequence_length=encoder_lengths_ph)
+
+    decoder_cell = tf.nn.rnn_cell.BasicLSTMCell(num_units)
+
+    decoder_cell = tf.contrib.seq2seq.AttentionWrapper(decoder_cell, attention_mechanism,
+        attention_layer_size=num_units)
+
+    # decoder_initial_state = encoder_state
+    decoder_initial_state = decoder_cell.zero_state(dtype=tf.float32, batch_size=batch_size)
+
+    if "TRAIN" == mode:
+
+        decoder_input = tf.pad(decoder_char_ids_ph, tf.constant([[0,0], [1,0]]),
+                               'CONSTANT', constant_values=(decoder_nchars-2))
+
+        decoder_emb_inp = tf.nn.embedding_lookup(decoder_embedding_weights, decoder_input)
+        decoder_emb_inp = tf.transpose(decoder_emb_inp, perm=[1, 0, 2])
+
+        helper = tf.contrib.seq2seq.TrainingHelper(
+            decoder_emb_inp, [max_target_length for _ in range(batch_size)], time_major=True)
+
+
+        decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, helper,
+                                                  decoder_initial_state, output_layer=projection_layer)
+
+        outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoder, output_time_major=True, swap_memory=True )
+
+        # TODO: Use attention to improve training performance ...
+
+        logits = outputs.rnn_output
+        train_prediction = outputs.sample_id
+
+        decoder_output = tf.pad(tf.transpose(decoder_char_ids_ph, perm=[1, 0]), tf.constant([[0,1], [0,0]]),
+                                'CONSTANT', constant_values=(decoder_nchars-1))
+
+        crossent = tf.nn.sparse_softmax_cross_entropy_with_logits(
+            labels=decoder_output, logits=logits, name="crossent")
+
+        loss = tf.reduce_sum(crossent * tf.to_float(decoder_lengths)) / (batch_size * max_target_length)
+
+        # Optimizer
+        # TODO: Tutorial suggest to swap to SGD for alter iterations
+        # optimizer = tf.train.AdamOptimizer()
+        optimizer = tf.train.RMSPropOptimizer(learning_rate=0.001)
+        gradients, v = zip(*optimizer.compute_gradients(loss))
+        gradients, _ = tf.clip_by_global_norm(gradients, 10.0)
+        optimize = optimizer.apply_gradients(zip(gradients, v))
+
+        # decoder is here ...
+        #helperE = tf.contrib.seq2seq.GreedyEmbeddingHelper(
+        #    decoder_embedding_weights,
+        #    tf.fill([batch_size], decoder_nchars-2), decoder_nchars-1)
+        #decoderE = tf.contrib.seq2seq.BasicDecoder(
+        #    decoder_cell, helperE, encoder_state,
+        #    output_layer=projection_layer)
+        #outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoderE, maximum_iterations=15, output_time_major=True)
+
+
+        return encoder_char_ids_ph, encoder_lengths_ph, decoder_char_ids_ph, decoder_lengths, optimize, train_prediction, outputs
+
+    if "EVAL" == mode:
+        helperE = tf.contrib.seq2seq.GreedyEmbeddingHelper(
+            decoder_embedding_weights,
+            tf.fill([batch_size], decoder_nchars-2), decoder_nchars-1)
+        decoderE = tf.contrib.seq2seq.BasicDecoder(
+            decoder_cell, helperE, decoder_initial_state,
+            output_layer=projection_layer)
+        outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(decoderE, maximum_iterations=15)
+
+        translations = outputs.sample_id
+
+        # the outputs don't decode anything ...
+        return encoder_char_ids_ph, encoder_lengths_ph, outputs
+
+def encode_chars(names):
+    char_set = set()
+    for name in names:
+        char_set = char_set.union(name)
+    return {k: v for v, k in enumerate(char_set)}
+
+def main():
+
+    checkpoints_path = "/tmp/model/checkpoints"
+
+    source_train, target_train = load_data("date_train.txt")
+    source_dev, target_dev = load_data("date_dev.txt")
+    source_test, target_test = load_data("date_test.txt")
+
+    source_char_dict = encode_chars(source_train + source_dev + source_test)
+    target_char_dict = encode_chars(target_train + target_dev + target_test)
+
+    # TODO: Find better chars for begin and end markers
+    target_char_dict['S'] = len(target_char_dict)
+    target_char_dict['E'] = len(target_char_dict)
+
+    target_dict_rev = {v: k for k, v in target_char_dict.items()}
+
+    batch_size = 20
+
+    # TODO: Don't hard code this ...
+    target_max_len = 9
+
+    train_graph = tf.Graph()
+    eval_graph = tf.Graph()
+
+    with train_graph.as_default():
+        t_encoder_char_ids_ph, t_encoder_lengths_ph, t_decoder_char_ids_ph, t_decoder_lengths, t_adam_optimize, t_train_prediction, t_dec_out = \
+            create_graph("TRAIN", batch_size, len(source_char_dict), target_max_len, len(target_char_dict))
+        train_saver = tf.train.Saver()
+        train_sess = tf.Session()
+        train_sess.run(tf.global_variables_initializer())
+
+    with eval_graph.as_default():
+        e_encoder_char_ids_ph, e_encoder_lengths_ph, e_dec_out = \
+            create_graph("EVAL", batch_size, len(source_char_dict), target_max_len, len(target_char_dict))
+        eval_saver = tf.train.Saver()
+
+    eval_sess = tf.Session(graph=eval_graph)
+
+
+    for epoch in range(200):
+        print("Epoch " + str(epoch))
+
+        for batch_index in range(floor(len(source_train) / batch_size)):
+            if batch_index > 0 and batch_index % 100 == 0:
+                print("batch_index " + str(batch_index))
+
+            target_batch, target_length, source_batch, source_length = \
+                mini_batch(target_char_dict, target_train, source_char_dict, source_train, batch_size, batch_index)
+
+            feed_dict = {t_encoder_lengths_ph: source_length, t_encoder_char_ids_ph: source_batch,
+                         t_decoder_lengths: target_length, t_decoder_char_ids_ph: target_batch}
+
+            t1, dec1 = train_sess.run([t_adam_optimize, t_dec_out], feed_dict)
+            dec2 = train_sess.run([t_dec_out], feed_dict)
+            tv=1
+
+        # Save train model, and restore it into the eval session
+        checkpoint_path = train_saver.save(train_sess, checkpoints_path, global_step=epoch)
+        eval_saver.restore(eval_sess, checkpoint_path)
+
+        count_correct = 0
+        for batch_index in range(floor(len(source_dev) / batch_size)):
+            target_batch, target_length, source_batch, source_length = \
+                mini_batch(target_char_dict, target_dev, source_char_dict, source_dev, batch_size, batch_index)
+
+            begin = batch_index
+            end = min(batch_index + batch_size, len(source_dev))
+            target_strings = target_dev[begin:end]
+
+
+            feed_dict = {e_encoder_lengths_ph: source_length, e_encoder_char_ids_ph: source_batch}
+            result = eval_sess.run(e_dec_out, feed_dict)
+
+
+            decoded_dates = []
+
+            for coded_date in result.sample_id:
+                date = ""
+                for char_id in coded_date:
+                    if not char_id == len(target_char_dict) - 1:
+                        date = date + (target_dict_rev[char_id])
+                decoded_dates.append(date)
+
+            for i in range(len(target_strings)):
+                if target_strings[i] == decoded_dates[i]:
+                    count_correct = count_correct + 1
+
+        print("Dev: " + str(count_correct / len(target_dev)))
+
+if __name__ == "__main__":
+    main()