# coding=utf-8
# Copyright 2018 The Google AI Language Team Authors and The HuggingFace Inc. team.
# Copyright (c) 2018, NVIDIA CORPORATION.  All rights reserved.
#
# Licensed 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.
"""BERT finetuning runner."""

from __future__ import absolute_import, division, print_function

import argparse
import csv
import logging
import os
import json
import time
import random
import sys
import math
import collections


import numpy as np
import torch
from torch.utils.data import (DataLoader, RandomSampler, SequentialSampler,
                              TensorDataset)
from torch.utils.data.distributed import DistributedSampler
from tqdm import tqdm, trange

from torch.nn import CrossEntropyLoss, MSELoss
from scipy.stats import pearsonr, spearmanr
from sklearn.metrics import matthews_corrcoef, f1_score

from pytorch_pretrained_bert.file_utils import PYTORCH_PRETRAINED_BERT_CACHE, WEIGHTS_NAME, CONFIG_NAME
from pytorch_pretrained_bert.modeling import BertForSequenceClassification,\
    BertConfig, CVTBertForSequenceClassification
from pytorch_pretrained_bert.tokenization import BertTokenizer
from pytorch_pretrained_bert.optimization import BertAdam, warmup_linear

logger = logging.getLogger(__name__)


class InputExample(object):
    """A single training/test example for simple sequence classification."""

    def __init__(self, guid, text_a, text_b=None, label=None, ner_labels=None):
        """Constructs a InputExample.

        Args:
            guid: Unique id for the example.
            text_a: string. The untokenized text of the first sequence. For single
            sequence tasks, only this sequence must be specified.
            text_b: (Optional) string. The untokenized text of the second sequence.
            Only must be specified for sequence pair tasks.
            label: (Optional) string. The label of the example. This should be
            specified for train and dev examples, but not for test examples.
        """
        self.guid = guid
        self.text_a = text_a
        self.text_b = text_b
        self.label = label
        self.ner_labels = ner_labels


class InputFeatures(object):
    """A single set of features of data."""

    def __init__(self, input_ids, input_mask, segment_ids, label_id, ner_ids, sub_token_mask):
        self.input_ids = input_ids
        self.input_mask = input_mask
        self.segment_ids = segment_ids
        self.label_id = label_id
        self.ner_ids = ner_ids
        self.sub_token_mask = sub_token_mask


class DataProcessor(object):
    """Base class for data converters for sequence classification data sets."""

    def get_train_examples(self, data_dir):
        """Gets a collection of `InputExample`s for the train set."""
        raise NotImplementedError()

    def get_dev_examples(self, data_dir, output_dir=None):
        """Gets a collection of `InputExample`s for the dev set."""
        raise NotImplementedError()

    def get_labels(self):
        """Gets the list of labels for this data set."""
        raise NotImplementedError()

    def get_ner_labels(self):
        """Gets the list of labels for this data set."""
        raise NotImplementedError()

    @classmethod
    def _read_tsv(cls, input_file, quotechar=None):
        """Reads a tab separated value file."""
        with open(input_file, "r", encoding="utf-8") as f:
            reader = csv.reader(f, delimiter="\t", quotechar=quotechar)
            lines = []
            for line in reader:
                if sys.version_info[0] == 2:
                    line = list(unicode(cell, 'utf-8') for cell in line)
                lines.append(line)
            return lines


class CVTQnliProcessor(DataProcessor):
    """Processor for the STS-B data set (GLUE version)."""

    def get_train_examples(self, data_dir):
        """See base class."""
        return self._create_examples(
            self._read_json(os.path.join(data_dir, "train_spacy.jsonl")), "train")

    def get_dev_examples(self, data_dir, output_dir=None):
        """See base class."""
        return self._create_examples(
            self._read_json(os.path.join(data_dir, "dev_spacy.jsonl")), "dev")

    def get_labels(self):
        """See base class."""
        return [0, 1]

    def get_ner_labels(self):
        return ["O", "I-PERSON","I-ORG","I-LOC","I-TIME","I-PRODUCT","I-EVENT","I-DOCUMENT","I-NUM","I-LANGUAGE", "X"]

    def parse_label(self, label):
        """PERSON,ORG,LOC,TIME,PRODUCT,EVENT,DOCUMENT,NUM,LANGUAGE."""
        if label == '':
            return 'O'
        if label == 'PERCENT' or label == 'QUANTITY' or label == 'CARDINAL' or label == 'MONEY' or label == 'ORDINAL':
            return 'I-NUM'
        if label == 'DATE' or label == 'TIME':
            return 'I-TIME'
        if label == 'WORK_OF_ART' or label == 'LAW':
            return 'I-DOCUMENT'
        if label == 'GPE' or label == 'LOC' or label == 'FAC':
            return 'I-LOC'
        if label == 'NORP' or label == 'ORG':
            return 'I-ORG'
        return "I-"+label

    def _create_examples(self, lines, set_type):
        """Creates examples for the training and dev sets."""
        examples = []
        for (i, line) in enumerate(lines):
            if i == 0:
                continue
            # if i > 200:
            #     break
            guid = "%s-%s" % (set_type, line['id'])
            text_a = '<space>'.join(line['Atokens'])
            text_b = '<space>'.join(line['Btokens'])
            label = line['label']
            ner_labels = '<space>'.join([self.parse_label(label) for label in line['Aner']] +
                                  [self.parse_label(label) for label in line['Bner']])
            examples.append(
                InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label, ner_labels=ner_labels))
        return examples

    @classmethod
    def _read_json(cls, input_file):
        data = [json.loads(line) for line in open(input_file)]
        data = [d for d in data if (len(d['Atokens']) < 128 and len(d['Btokens']) < 128)]
        return data


def convert_examples_to_features(examples, label_list, ner_label_list, max_seq_length,
                                 tokenizer, output_mode, mask_cls=True):
    """Loads a data file into a list of `InputBatch`s."""
    label_map = {label : i for i, label in enumerate(label_list)}
    ner_label_map = {label: i for (i, label) in enumerate(ner_label_list, 1)}  # 0 index for '0' padding

    features = []
    for (ex_index, example) in enumerate(examples):
        if ex_index % 10000 == 0:
            logger.info("Writing example %d of %d" % (ex_index, len(examples)))

        # tokens_a = tokenizer.tokenize(example.text_a)
        sentence_a = example.text_a.split("<space>")
        ner_label_list = example.ner_labels.split("<space>")
        ner_a = ner_label_list[:len(sentence_a)]
        ner_b = ner_label_list[len(sentence_a):]

        tokens = []
        tokens.append("[CLS]")
        ner_labels = []
        ner_labels.append("O")
        sub_token_mask = []
        sub_token_mask.append(0)
        for i, word in enumerate(sentence_a):
            token = tokenizer.tokenize(word)
            tokens.extend(token)
            label = ner_a[i]
            for m in range(len(token)):
                if m == 0:
                    ner_labels.append(label)
                    sub_token_mask.append(1)
                else:
                    ner_labels.append("X")
                    sub_token_mask.append(0)
        sub_token_mask.append(0)
        tokens.append("[SEP]")
        ner_labels.append("O")
        segment_ids = [0] * len(tokens)

        # tokens_b = None
        if example.text_b:
            # tokens_b = tokenizer.tokenize(example.text_b)
            sentence_b = example.text_b.split("<space>")
            assert len(sentence_b) == len(ner_b)
            for i, word in enumerate(sentence_b):
                token = tokenizer.tokenize(word)
                tokens.extend(token)
                label = ner_b[i]
                for m in range(len(token)):
                    if m == 0:
                        ner_labels.append(label)
                        sub_token_mask.append(1)
                    else:
                        ner_labels.append("X")
                        sub_token_mask.append(0)
            # Modifies `tokens_a` and `tokens_b` in place so that the total
            # length is less than the specified length.
            # Account for [CLS], [SEP], [SEP] with "- 3"
            if len(tokens) > max_seq_length - 1:
                tokens = tokens[0:max_seq_length-1]
                ner_labels = ner_labels[0:max_seq_length-1]
                sub_token_mask = sub_token_mask[0:max_seq_length-1]
            tokens += ["[SEP]"]
            sub_token_mask.append(0)
            ner_labels.append("O")

        segment_ids += [1] * (len(tokens) - len(segment_ids))

        input_ids = tokenizer.convert_tokens_to_ids(tokens)
        ner_label_ids = [ner_label_map[label] for label in ner_labels]
        # The mask has 1 for real tokens and 0 for padding tokens. Only real
        # tokens are attended to.
        if mask_cls:
            input_mask = [0]
        else:
            input_mask = [1]
        input_mask += [1] * (len(input_ids)-1)

        # Zero-pad up to the sequence length.
        while len(input_ids) != max_seq_length:
            input_ids.append(0)
            input_mask.append(0)
            segment_ids.append(0)
            ner_label_ids.append(0)
            sub_token_mask.append(0)

        assert len(input_ids) == max_seq_length
        assert len(input_mask) == max_seq_length
        assert len(segment_ids) == max_seq_length
        assert len(ner_label_ids) == max_seq_length
        assert len(sub_token_mask) == max_seq_length

        if output_mode == "classification":
            label_id = label_map[example.label]
        elif output_mode == "regression":
            label_id = float(example.label)
        else:
            raise KeyError(output_mode)

        if ex_index < 5:
            logger.info("*** Example ***")
            logger.info("guid: %s" % (example.guid))
            logger.info("tokens: %s" % " ".join(
                    [str(x) for x in tokens]))
            logger.info("input_ids: %s" % " ".join([str(x) for x in input_ids]))
            logger.info("input_mask: %s" % " ".join([str(x) for x in input_mask]))
            logger.info(
                    "segment_ids: %s" % " ".join([str(x) for x in segment_ids]))
            print("ner_label_ids: %s" %
                  " ".join([str(x) for x in ner_label_ids]))
            print("sub_token_mask: %s" %
                  " ".join([str(x) for x in sub_token_mask]))
            logger.info("label: %s (id = %d)" % (example.label, label_id))

        features.append(
                InputFeatures(input_ids=input_ids,
                              input_mask=input_mask,
                              segment_ids=segment_ids,
                              label_id=label_id,
                              ner_ids=ner_label_ids,
                              sub_token_mask=sub_token_mask
                              ))
    return features


def simple_accuracy(preds, labels):
    return (preds == labels).mean()


def acc_and_f1(preds, labels):
    acc = simple_accuracy(preds, labels)
    f1 = f1_score(y_true=labels, y_pred=preds)
    return {
        "acc": acc,
        "f1": f1,
        "acc_and_f1": (acc + f1) / 2,
    }


def pearson_and_spearman(preds, labels):
    pearson_corr = pearsonr(preds, labels)[0]
    spearman_corr = spearmanr(preds, labels)[0]
    return {
        "pearson": pearson_corr,
        "spearmanr": spearman_corr,
        "corr": (pearson_corr + spearman_corr) / 2,
    }


def compute_metrics(task_name, preds, labels, output_dir=None):
    assert len(preds) == len(labels)
    if task_name == "cola":
        return {"mcc": matthews_corrcoef(labels, preds)}
    elif task_name == "sst-2":
        return {"acc": simple_accuracy(preds, labels)}
    elif task_name == "mrpc":
        return acc_and_f1(preds, labels)
    elif task_name == "sts-b":
        return pearson_and_spearman(preds, labels)
    elif task_name == "qqp":
        return acc_and_f1(preds, labels)
    elif task_name == "mnli":
        return {"acc": simple_accuracy(preds, labels)}
    elif task_name == "mnli-mm":
        return {"acc": simple_accuracy(preds, labels)}
    elif task_name == "qnli":
        return acc_and_f1(preds, labels)
    elif task_name == "ner":
        return simple_accuracy(preds, labels)
    elif task_name == "rte":
        return {"acc": simple_accuracy(preds, labels)}
    elif task_name == "wnli":
        return {"acc": simple_accuracy(preds, labels)}
    else:
        raise KeyError(task_name)


def main():
    parser = argparse.ArgumentParser()

    ## Required parameters
    parser.add_argument("--data_dir",
                        default=None,
                        type=str,
                        required=True,
                        help="The input data dir. Should contain the .tsv files (or other data files) for the task.")
    parser.add_argument("--bert_model", default=None, type=str, required=True,
                        help="Bert pre-trained model selected in the list: bert-base-uncased, "
                        "bert-large-uncased, bert-base-cased, bert-large-cased, bert-base-multilingual-uncased, "
                        "bert-base-multilingual-cased, bert-base-chinese.")
    parser.add_argument("--task_name",
                        default=None,
                        type=str,
                        required=True,
                        help="The name of the task to train.")
    parser.add_argument("--output_dir",
                        default=None,
                        type=str,
                        required=True,
                        help="The output directory where the model predictions and checkpoints will be written.")

    ## Other parameters
    parser.add_argument("--cache_dir",
                        default="",
                        type=str,
                        help="Where do you want to store the pre-trained models downloaded from s3")
    parser.add_argument("--max_seq_length",
                        default=128,
                        type=int,
                        help="The maximum total input sequence length after WordPiece tokenization. \n"
                             "Sequences longer than this will be truncated, and sequences shorter \n"
                             "than this will be padded.")
    parser.add_argument("--do_train",
                        action='store_true',
                        help="Whether to run training.")
    parser.add_argument("--do_eval",
                        action='store_true',
                        help="Whether to run eval on the dev set.")
    parser.add_argument("--do_lower_case",
                        action='store_true',
                        help="Set this flag if you are using an uncased model.")
    parser.add_argument("--train_batch_size",
                        default=32,
                        type=int,
                        help="Total batch size for training.")
    parser.add_argument("--eval_batch_size",
                        default=1,
                        type=int,
                        help="Total batch size for eval.")
    parser.add_argument("--learning_rate",
                        default=5e-5,
                        type=float,
                        help="The initial learning rate for Adam.")
    parser.add_argument("--layer_index_to_prob",
                        default=12,
                        type=int,
                        help="which layer to use to get the output")
    parser.add_argument("--mask_cls",
                        action='store_true',
                        help="Whether or not to use the [CLS] vector")
    parser.add_argument("--freeze_bert",
                        action='store_true',
                        help="Whether not to freeze the bert weights when fine-tune")
    parser.add_argument("--num_train_epochs",
                        default=3.0,
                        type=float,
                        help="Total number of training epochs to perform.")
    parser.add_argument("--patience",
                        default=3.0,
                        type=float,
                        help="Break after these epochs when metric does not improve.")
    parser.add_argument("--threshold",
                        default=0.005,
                        type=float,
                        help="threshold for restricted improvements.")
    parser.add_argument("--warmup_proportion",
                        default=0.1,
                        type=float,
                        help="Proportion of training to perform linear learning rate warmup for. "
                             "E.g., 0.1 = 10%% of training.")
    parser.add_argument("--no_cuda",
                        action='store_true',
                        help="Whether not to use CUDA when available")
    parser.add_argument("--local_rank",
                        type=int,
                        default=-1,
                        help="local_rank for distributed training on gpus")
    parser.add_argument('--seed',
                        type=int,
                        default=42,
                        help="random seed for initialization")
    parser.add_argument('--gradient_accumulation_steps',
                        type=int,
                        default=1,
                        help="Number of updates steps to accumulate before performing a backward/update pass.")
    parser.add_argument('--fp16',
                        action='store_true',
                        help="Whether to use 16-bit float precision instead of 32-bit")
    parser.add_argument('--loss_scale',
                        type=float, default=0,
                        help="Loss scaling to improve fp16 numeric stability. Only used when fp16 set to True.\n"
                             "0 (default value): dynamic loss scaling.\n"
                             "Positive power of 2: static loss scaling value.\n")
    parser.add_argument('--server_ip', type=str, default='', help="Can be used for distant debugging.")
    parser.add_argument('--server_port', type=str, default='', help="Can be used for distant debugging.")
    args = parser.parse_args()

    processors = {
        "cvt-qnli": CVTQnliProcessor
    }

    output_modes = {
        "cvt-qnli": "classification"
    }

    if args.local_rank == -1 or args.no_cuda:
        device = torch.device("cuda" if torch.cuda.is_available() and not args.no_cuda else "cpu")
        n_gpu = torch.cuda.device_count()
    else:
        torch.cuda.set_device(args.local_rank)
        device = torch.device("cuda", args.local_rank)
        n_gpu = 1
        # Initializes the distributed backend which will take care of sychronizing nodes/GPUs
        torch.distributed.init_process_group(backend='nccl')

    logging.basicConfig(format='%(asctime)s - %(levelname)s - %(name)s -   %(message)s',
                        datefmt='%m/%d/%Y %H:%M:%S',
                        level=logging.INFO if args.local_rank in [-1, 0] else logging.WARN)

    logger.info("device: {} n_gpu: {}, distributed training: {}, 16-bits training: {}".format(
        device, n_gpu, bool(args.local_rank != -1), args.fp16))

    if args.gradient_accumulation_steps < 1:
        raise ValueError("Invalid gradient_accumulation_steps parameter: {}, should be >= 1".format(
            args.gradient_accumulation_steps))

    args.train_batch_size = args.train_batch_size // args.gradient_accumulation_steps

    random.seed(args.seed)
    np.random.seed(args.seed)
    torch.manual_seed(args.seed)
    if n_gpu > 0:
        torch.cuda.manual_seed_all(args.seed)

    if not args.do_train and not args.do_eval:
        raise ValueError("At least one of `do_train` or `do_eval` must be True.")

    if os.path.exists(args.output_dir) and os.listdir(args.output_dir) and args.do_train:
        raise ValueError("Output directory ({}) already exists and is not empty.".format(args.output_dir))
    if not os.path.exists(args.output_dir):
        os.makedirs(args.output_dir)

    task_name = args.task_name.lower()

    if task_name not in processors:
        raise ValueError("Task not found: %s" % (task_name))

    processor = processors[task_name]()
    output_mode = output_modes[task_name]

    label_list = processor.get_labels()
    ner_label_list = processor.get_ner_labels()
    num_labels = len(label_list)
    num_ner_labels = len(ner_label_list)

    tokenizer = BertTokenizer.from_pretrained(args.bert_model, do_lower_case=args.do_lower_case)

    train_examples = None
    num_train_optimization_steps = None
    if args.do_train:
        train_examples = processor.get_train_examples(args.data_dir)
        num_train_optimization_steps = int(
            math.ceil(len(train_examples) / args.train_batch_size / args.gradient_accumulation_steps) * args.num_train_epochs)
        if args.local_rank != -1:
            num_train_optimization_steps = num_train_optimization_steps // torch.distributed.get_world_size()
    logger.info("num_train_optimization_steps:{}".format(num_train_optimization_steps))

    # Prepare model
    cache_dir = args.cache_dir if args.cache_dir else os.path.join(str(PYTORCH_PRETRAINED_BERT_CACHE),
                                                                   'distributed_{}'.format(args.local_rank))
    model = CVTBertForSequenceClassification.from_pretrained(args.bert_model,
                                                             cache_dir=cache_dir,
                                                             num_labels=num_labels,
                                                             num_ner_labels=num_ner_labels)

    model.to(device)
    if args.local_rank != -1:
        try:
            from apex.parallel import DistributedDataParallel as DDP
        except ImportError:
            raise ImportError(
                "Please install apex from https://www.github.com/nvidia/apex to use distributed and fp16 training.")

        model = DDP(model)
    elif n_gpu > 1:
        model = torch.nn.DataParallel(model)

    if args.freeze_bert:
        param_optimizer = model.named_parameters()
        freeze = ['bert']
        for n, param in param_optimizer:
            if any(name in n for name in freeze):
                param.requires_grad = False

    # Prepare optimizer
    if args.do_train:
        list_param_optimizer = list(model.named_parameters())
        no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
        optimizer_grouped_parameters = [
            {'params': [p for n, p in list_param_optimizer if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
            {'params': [p for n, p in list_param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
        ]

        optimizer = BertAdam(optimizer_grouped_parameters,
                             lr=args.learning_rate,
                             warmup=args.warmup_proportion,
                             t_total=num_train_optimization_steps)
        ner_optimizer = BertAdam(optimizer_grouped_parameters,
                                 lr=args.learning_rate,
                                 warmup=args.warmup_proportion,
                                 t_total=num_train_optimization_steps)
    best_epoch = 0
    best_metric = 0
    global_step = 0
    freeze = []
    if args.do_train:
        # Train data loader
        train_features = convert_examples_to_features(
            train_examples, label_list, ner_label_list, args.max_seq_length, tokenizer, output_mode, args.mask_cls)
        all_train_input_ids = torch.tensor([f.input_ids for f in train_features], dtype=torch.long)
        all_train_input_mask = torch.tensor([f.input_mask for f in train_features], dtype=torch.long)
        all_train_segment_ids = torch.tensor([f.segment_ids for f in train_features], dtype=torch.long)
        all_train_subtoken_mask = torch.tensor([f.sub_token_mask for f in train_features], dtype=torch.long)

        all_train_label_ids = torch.tensor([f.label_id for f in train_features], dtype=torch.long)
        all_train_ner_label_ids = torch.tensor([f.ner_ids for f in train_features], dtype=torch.long)

        train_data = TensorDataset(all_train_input_ids, all_train_input_mask, all_train_subtoken_mask,
                                   all_train_segment_ids, all_train_label_ids, all_train_ner_label_ids)
        if args.local_rank == -1:
            train_sampler = RandomSampler(train_data)
        else:
            train_sampler = DistributedSampler(train_data)
        train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=args.train_batch_size)

        # Eval data loader
        eval_examples = processor.get_dev_examples(args.data_dir, args.output_dir)
        eval_features = convert_examples_to_features(
            eval_examples, label_list, ner_label_list, args.max_seq_length, tokenizer, output_mode, args.mask_cls)
        all_eval_input_ids = torch.tensor([f.input_ids for f in eval_features], dtype=torch.long)
        all_eval_input_mask = torch.tensor([f.input_mask for f in eval_features], dtype=torch.long)
        all_eval_segment_ids = torch.tensor([f.segment_ids for f in eval_features], dtype=torch.long)
        all_eval_subtoken_mask = torch.tensor([f.sub_token_mask for f in eval_features], dtype=torch.long)

        all_eval_label_ids = torch.tensor([f.label_id for f in eval_features], dtype=torch.long)
        all_eval_ner_label_ids = torch.tensor([f.ner_ids for f in eval_features], dtype=torch.long)

        eval_data = TensorDataset(all_eval_input_ids, all_eval_input_mask, all_eval_subtoken_mask,
                                  all_eval_segment_ids, all_eval_label_ids, all_eval_ner_label_ids)
        # Run prediction for full data
        eval_sampler = SequentialSampler(eval_data)
        eval_dataloader = DataLoader(eval_data, sampler=eval_sampler, batch_size=args.eval_batch_size)

        logger.info("***** Running training *****")
        logger.info("  Num examples = %d", len(train_examples))
        logger.info("  Batch size = %d", args.train_batch_size)
        logger.info("  Num steps = %d", num_train_optimization_steps)
        for epoch in trange(int(args.num_train_epochs), desc="Epoch"):
            tr_loss = 0
            nb_tr_examples, nb_tr_steps = 0, 0
            model.train()

            for step, batch in enumerate(tqdm(train_dataloader, desc="Iteration")):
                batch = tuple(t.to(device) for t in batch)
                input_ids, input_mask, subtoken_mask, segment_ids, label_ids, ner_label_ids = batch

                # define a new function to compute loss values for both output_modes
                logits, ner_logits = model(input_ids, segment_ids, input_mask, labels=None,
                                           layer_index=args.layer_index_to_prob)

                cls_loss_fct = CrossEntropyLoss()
                ner_loss_fct = CrossEntropyLoss()
                # cls label loss
                loss = cls_loss_fct(logits.view(-1, num_labels), label_ids.view(-1))
                # ner loss
                active_loss = subtoken_mask.view(-1) == 1
                active_logits = ner_logits.view(-1, num_ner_labels)[active_loss]
                active_labels = ner_label_ids.view(-1)[active_loss]
                ner_loss = ner_loss_fct(active_logits, active_labels)

                if n_gpu > 1:
                    loss = loss.mean()  # mean() to average on multi-gpu.
                if args.gradient_accumulation_steps > 1:
                    loss = loss / args.gradient_accumulation_steps

                loss.backward(retain_graph=True)
                tr_loss += loss.item()
                nb_tr_examples += input_ids.size(0)
                nb_tr_steps += 1
                if (step + 1) % args.gradient_accumulation_steps == 0:
                    for n, param in model.named_parameters():
                        param.requires_grad = True
                    optimizer.step()
                    optimizer.zero_grad()
                    # ner optimizer
                    # freeze top
                    for c in range(9, 12):
                        layer_name = "bert.encoder.layer.{}.".format(str(int(c)))
                        if layer_name not in freeze:
                            freeze.append(layer_name)
                    for n, param in model.named_parameters():
                        if any(name in n for name in freeze):
                            param.requires_grad = False
                    ner_loss.backward()
                    ner_optimizer.step()
                    ner_optimizer.zero_grad()

                    global_step += 1
            # dev
            logger.info("***** Running evaluation *****")
            logger.info("  Num examples = %d", len(eval_examples))
            logger.info("  Batch size = %d", args.eval_batch_size)
            model.eval()
            preds = []
            ner_preds = None
            ner_out_label_ids = None
            for input_ids, input_mask, subtoken_mask, segment_ids, label_ids, ner_label_ids in tqdm(eval_dataloader, desc="Evaluating"):
                input_ids = input_ids.to(device)
                input_mask = input_mask.to(device)
                segment_ids = segment_ids.to(device)

                with torch.no_grad():
                    logits, ner_logits = model(input_ids, segment_ids, input_mask, labels=None, layer_index=args.layer_index_to_prob)
                    active_loss = subtoken_mask.view(-1) == 1
                    active_logits = ner_logits.view(-1, num_ner_labels)[active_loss]
                    active_labels = ner_label_ids.view(-1)[active_loss]
                    # active logits(num_ex, num_labels), active labels(num_ex)
                if len(preds) == 0:
                    preds.append(logits.detach().cpu().numpy())
                    ner_preds = active_logits.detach().cpu().numpy()
                    ner_out_label_ids = active_labels.detach().cpu().numpy()
                else:
                    preds[0] = np.append(preds[0], logits.detach().cpu().numpy(), axis=0)
                    ner_preds = np.append(ner_preds, active_logits.detach().cpu().numpy(), axis=0)
                    ner_out_label_ids = np.append(ner_out_label_ids, active_labels.detach().cpu().numpy(), axis=0)

            # cls
            preds = preds[0]
            preds = np.argmax(preds, axis=1)
            result = compute_metrics("qnli", preds, all_eval_label_ids.numpy())
            # ner

            ner_preds = np.argmax(ner_preds, axis=-1)
            ner_result = compute_metrics("ner", ner_preds, ner_out_label_ids)

            metric = result["acc"]
            logger.info("CLS # Epoch: {}, Acc: {} F1: {} \n".format(epoch, result["acc"], result["f1"]))
            logger.info("NER # Epoch: {}, Acc: {} \n".format(epoch, ner_result))

            if metric >= best_metric + args.threshold: # threshold for metric improvement
                best_metric = metric
                best_epoch = epoch
                # Save a trained model, configuration and tokenizer
                model_to_save = model.module if hasattr(model, 'module') else model  # Only save the model it-self

                # If we save using the predefined names, we can load using `from_pretrained`
                output_model_file = os.path.join(args.output_dir, WEIGHTS_NAME)
                output_config_file = os.path.join(args.output_dir, CONFIG_NAME)

                torch.save(model_to_save.state_dict(), output_model_file)
                model_to_save.config.to_json_file(output_config_file)
                tokenizer.save_vocabulary(args.output_dir)
            else:
                if metric > best_metric:
                    best_metric = metric
                    best_epoch = epoch
                    # Save a trained model, configuration and tokenizer
                    model_to_save = model.module if hasattr(model, 'module') else model  # Only save the model it-self

                    # If we save using the predefined names, we can load using `from_pretrained`
                    output_model_file = os.path.join(args.output_dir, WEIGHTS_NAME)
                    output_config_file = os.path.join(args.output_dir, CONFIG_NAME)

                    torch.save(model_to_save.state_dict(), output_model_file)
                    model_to_save.config.to_json_file(output_config_file)
                    tokenizer.save_vocabulary(args.output_dir)
                if epoch >= best_epoch + args.patience:
                    break
        logger.info("Best Epoch: {}, Metric: {}".format(best_epoch, best_metric))
        output_eval_file = os.path.join(args.output_dir, "eval_results.txt")
        with open(output_eval_file, "w") as writer:
            writer.write("best epoch: %s \nacc: %s\n" % (best_epoch, best_metric))


if __name__ == "__main__":
    main()
