class Actions(Labels):
    Shift = Action("SHIFT")
    Node = Action("NODE")
    RemoteNode = Action("REMOTE-NODE")
    Implicit = Action("IMPLICIT")
    Label = Action("LABEL")
    Reduce = Action("REDUCE")
    LeftEdge = Action("LEFT-EDGE")
    RightEdge = Action("RIGHT-EDGE")
    LeftRemote = Action("LEFT-REMOTE")
    RightRemote = Action("RIGHT-REMOTE")
    Swap = Action("SWAP")
    Finish = Action("FINISH")

    def __init__(self, actions=None, size=None):
        super().__init__(size=size)
        self._all = None
        self._ids = None
        if actions is not None:
            self.all = actions

    def init(self):
        # edge and node action will be created as they are returned by the oracle
        args = Config().args
        self.all = [Actions.Reduce, Actions.Shift, Actions.Finish] + \
            (list(map(Actions.Swap, range(1, args.max_swap))) if args.swap == COMPOUND else
             [Actions.Swap] if args.swap else [])

    @property
    def all(self):
        if self._all is None:
            self.init()
        return self._all

    @all.setter
    def all(self, actions):
        self._all = [Action(**a) if isinstance(a, dict) else a for a in actions]
        self._ids = {(action.type_id, action.tag): i for i, action in enumerate(self._all)}
        for action in self._all:
            self.generate_id(action)

    @property
    def ids(self):
        if self._all is None:
            self.init()
        return self._ids

    def generate_id(self, action, create=True):
        if action.id is None:
            key = (action.type_id, action.tag)
            action.id = self.ids.get(key)
            if create and action.id is None:  # New action, add to list
                # noinspection PyTypeChecker
                action.id = len(self.all)
                self.all.append(action(tag=action.tag, id_=action.id))
                self.ids[key] = action.id class Classifier:
    """
    Interface for classifier used by the parser.
    """

    def __init__(self, config, labels, input_params=None):
        """
        :param config: Config to get hyperparameters from
        :param labels: dict of axis (string) -> Labels object, can be updated later to add new axes and labels
        :param input_params: dict of feature type name -> FeatureInformation
        """
        self.config = config
        self.labels = labels
        self.input_params = input_params
        self.model_type = self.config.args.classifier
        self.learning_rate = self.config.args.learning_rate
        self.learning_rate_decay = self.config.args.learning_rate_decay
        self.model = self.labels_t = None
        self.is_frozen = False
        self.updates = self.epoch = self.best_score = 0
        self._num_labels = self.num_labels

    @property
    def num_labels(self):
        return OrderedDict((a, len(l.all)) for a, l in self.labels.items()) if self.labels \
            else OrderedDict((a, len(l) if l else s) for a, (l, s) in self.labels_t.items()) if self.labels_t else {}
    
    @property
    def input_dim(self):
        raise NotImplementedError()

    def score(self, features, axis):
        if not self.is_frozen:
            self._update_num_labels()

    def init_features(self, features, axes, train=False, passage=None, lang=None):
        pass

    def update(self, features, axis, pred, true, importance=None):
        """
        Update classifier weights according to predicted and true labels
        :param features: extracted feature values
        :param axis: axis of the label we are predicting
        :param pred: label predicted by the classifier (non-negative integer bounded by num_labels[axis])
        :param true: true labels (non-negative integers bounded by num_labels[axis])
        :param importance: how much to scale the update for the weight update for each true label
        """
        assert not self.is_frozen, "Cannot update a frozen model"
        self._update_num_labels()

    def _update_num_labels(self):
        """
        self.num_labels is a property, and so updated automatically when a label is added to self.labels,
        but we may need to resize the weight matrices whenever that happens
        """
        if self._num_labels != self.num_labels:
            self._num_labels = self.num_labels
            self.resize()

    def resize(self):
        raise NotImplementedError()

    def finalize(self, *args, finished_epoch=False, **kwargs):
        assert not self.is_frozen, "Cannot freeze a frozen model"
        self._update_num_labels()
        if finished_epoch:
            self.epoch += 1

    def finished_step(self, train=False):
        """
        Called by the parser when a single step is finished
        """
        pass

    def finished_item(self, train=False, renew=True):
        """
        Called by the parser when a whole item is finished
        """
        pass

    def transition(self, action, axis):
        pass

    def save(self, filename, skip_labels=(), **kwargs):
        """
        Save all parameters to file
        """
        d = OrderedDict((
            ("type", self.model_type),
            ("axes", OrderedDict(
                (a, OrderedDict((("index", i), ("labels", l.save(skip=a in skip_labels)))))  # (all, size)
                for i, (a, l) in enumerate(self.labels.items())
            )),
            ("is_frozen", self.is_frozen),
            ("learning_rate", self.learning_rate),
            ("learning_rate_decay", self.learning_rate_decay),
            ("updates", self.updates),
            ("epoch", self.epoch),
            ("best_score", self.best_score),
        ) + tuple(kwargs.items()))
        self.save_model(filename, d)
        save_json(filename + ".json", d)

    def save_model(self, filename, d):
        """
        Save all parameters to file
        """
        pass

    def load(self, filename):
        """
        Load all parameters from file
        """
        d = self.load_file(filename, clear=True)
        model_type = d.get("type")
        assert model_type is None or model_type == self.model_type, "Model type does not match: %s" % model_type
        self.labels_t = OrderedDict((a, l["labels"]) for a, l in sorted(d["axes"].items(), key=lambda x: x[1]["index"]))
        self.is_frozen = d["is_frozen"]
        self.config.args.learning_rate = self.learning_rate = d["learning_rate"]
        self.config.args.learning_rate_decay = self.learning_rate_decay = d["learning_rate_decay"]
        self.updates = d.get("updates", 0)
        self.epoch = d.get("epoch", 0)
        self.best_score = d.get("best_score", 0)
        self.load_model(filename, d)

    def load_model(self, filename, d):
        pass

    @classmethod
    def get_property(cls, filename, prop):
        return cls.load_file(filename).get(prop, None)

    LOADED = {}  # Cache for loaded JSON files

    @classmethod
    def load_file(cls, filename, clear=False):
        d = cls.LOADED.get(filename)
        if d is None:
            d = load_json(filename + ".json")
            cls.LOADED[filename] = d
        if clear:
            cls.LOADED.clear()
        return d

    def all_params(self):
        return OrderedDict((k, v.all) for k, v in self.labels.items())

    def __str__(self):
        return "Labels: %s, features: %s" % tuple(map(dict_value, (self.num_labels, self.input_dim)))

    def print_params(self, max_rows=10):
        pass class Config(object, metaclass=Singleton):
    def __init__(self, *args):
        self.arg_parser = ap = ArgParser(description="Transition-based parser for UCCA.",
                                         formatter_class=ArgumentDefaultsHelpFormatter)

        add_boolean_option(ap, "use-bert", default=False, description="whether to use bert embeddings")
        ap.add_argument("--bert-model", choices=["bert-base-uncased", "bert-large-uncased", "bert-base-cased",
                                                 "bert-large-cased", "bert-base-multilingual-cased"],
                        default="bert-base-multilingual-cased")
        ap.add_argument("--bert-layers", type=int, nargs='+', default=[-1, -2, -3, -4])
        ap.add_argument("--bert-layers-pooling", choices=["weighted", "sum", "concat"], default="weighted")
        ap.add_argument("--bert-token-align-by", choices=["first", "sum", "mean"], default="sum")
        ap.add_argument("--bert-multilingual", choices=[0], type=int)
        add_boolean_option(ap, "bert-use-default-word-embeddings", default=False,
                           description="whether to use default word embeddings")
        ap.add_argument("--bert-dropout", type=float, default=0, choices=np.linspace(0, 0.9, num=10))
        add_boolean_option(ap, "bert-gpu", default=True, description="whether to use CUDA and GPU for BERT embeddings")

        ap.add_argument("passages", nargs="*", help="passage files/directories to test on/parse")
        ap.add_argument("--version", action="version", version="")
        ap.add_argument("-C", "--config", is_config_file=True, help="configuration file to get arguments from")
        ap.add_argument("-m", "--models", nargs="+", help="model file basename(s) to load/save, ensemble if >1 "
                                                          "(default: <format>_<model_type>")
        ap.add_argument("-c", "--classifier", choices=CLASSIFIERS, default=BIRNN, help="model type")
        ap.add_argument("-B", "--beam", type=int, choices=(1,), default=1, help="beam size for beam search")
        add_boolean_option(ap, "evaluate", "evaluation of parsed passages", short="e")
        add_verbose_arg(ap, help="detailed parse output")
        constructions.add_argument(ap)
        add_boolean_option(ap, "sentences", "split to sentences")
        add_boolean_option(ap, "paragraphs", "split to paragraphs")
        ap.add_argument("--timeout", type=float, help="max number of seconds to wait for a single passage")

        group = ap.add_argument_group(title="Training parameters")
        group.add_argument("-t", "--train", nargs="+", default=(), help="passage files/directories to train on")
        group.add_argument("-d", "--dev", nargs="+", default=(), help="passage files/directories to tune on")
        group.add_argument("-I", "--iterations", nargs="+", type=Iterations,
                           default=(Iterations(50), Iterations("100 --optimizer=" + EXTRA_TRAINER)),
                           help="number of training iterations along with optional hyperparameters per part")
        group.add_argument("--folds", type=int, choices=(3, 5, 10), help="#folds for cross validation")
        group.add_argument("--seed", type=int, default=1, help="random number generator seed")
        add_boolean_option(group, "early-update", "early update procedure (finish example on first error)")
        group.add_argument("--save-every", type=int, help="every this many passages, evaluate on dev and save model")
        add_boolean_option(group, "eval-test", "evaluate on test whenever evaluating on dev, but keep results hidden")
        add_boolean_option(group, "ignore-case", "pre-convert all input files to lower-case in training and test")

        group = ap.add_argument_group(title="Output files")
        group.add_argument("-o", "--outdir", default=".", help="output directory for parsed files")
        group.add_argument("-p", "--prefix", default="", help="output filename prefix")
        add_boolean_option(group, "write", "writing parsed output to files", default=True, short_no="W")
        group.add_argument("-j", "--join", help="if output format is textual, write all to one file with this basename")
        group.add_argument("-l", "--log", help="output log file (default: model filename + .log)")
        group.add_argument("--devscores", help="output CSV file for dev scores (default: model filename + .dev.csv)")
        group.add_argument("--testscores", help="output CSV file for test scores (default: model filename + .test.csv)")
        group.add_argument("--action-stats", help="output CSV file for action statistics")
        add_boolean_option(group, "normalize", "apply normalizations to output in case format is UCCA", default=False)
        ap.add_argument("-f", "--formats", nargs="+", choices=FILE_FORMATS, default=(),
                        help="input formats for creating all parameters before training starts "
                             "(otherwise created dynamically based on filename suffix), "
                             "and output formats for written files (each will be written; default: UCCA XML)")
        ap.add_argument("-u", "--unlabeled", nargs="*", choices=FORMATS, help="to ignore labels in")
        ap.add_argument("--lang", default="en", help="two-letter language code to use as the default language")
        add_boolean_option(ap, "multilingual", "separate model parameters per language (passage.attrib['lang'])")

        group = ap.add_argument_group(title="Sanity checks")
        add_boolean_option(group, "check-loops", "check for parser state loop")
        add_boolean_option(group, "verify", "check for oracle reproducing original passage")
        add_boolean_option(group, "validate-oracle", "require oracle output to respect constraints", default=True)
        add_param_arguments(ap)

        group = ap.add_argument_group(title="DyNet parameters")
        group.add_argument("--dynet-mem", help="memory for dynet")
        group.add_argument("--dynet-weight-decay", type=float, default=1e-5, help="weight decay for parameters")
        add_boolean_option(group, "dynet-apply-weight-decay-on-load", "workaround for clab/dynet#1206", default=False)
        add_boolean_option(group, "dynet-gpu", "GPU for training")
        group.add_argument("--dynet-gpus", type=int, default=1, help="how many GPUs you want to use")
        add_boolean_option(group, "dynet-autobatch", "auto-batching of training examples")
        add_boolean_option(group, "dynet-check-validity", "check validity of expressions immediately")
        DYNET_ARG_NAMES.update(get_group_arg_names(group))

        ap.add_argument("-H", "--hyperparams", type=HyperparamsInitializer.action, nargs="*",
                        help="shared hyperparameters or hyperparameters for specific formats, "
                             'e.g., "shared --lstm-layer-dim=100 --lstm-layers=1" "ucca --word-dim=300"',
                        default=[HyperparamsInitializer.action("shared --lstm-layers 2")])
        ap.add_argument("--copy-shared", nargs="*", choices=FORMATS, help="formats whose parameters shall be "
                                                                          "copied from loaded shared parameters")
        self.args = FallbackNamespace(ap.parse_args(args if args else None))

        if self.args.config:
            print("Loading configuration from '%s'." % self.args.config)

        if self.args.passages and self.args.write:
            os.makedirs(self.args.outdir, exist_ok=True)

        if self.args.models:
            if not self.args.log:
                self.args.log = self.args.models[0] + ".log"
            if self.args.dev and not self.args.devscores:
                self.args.devscores = self.args.models[0] + ".dev.csv"
            if self.args.passages and not self.args.testscores:
                self.args.testscores = self.args.models[0] + ".test.csv"
        elif not self.args.log:
            self.args.log = "parse.log"
        self.sub_configs = []  # Copies to be stored in Models so that they do not interfere with each other
        self._logger = self.format = self.hyperparams = self.iteration_hyperparams = None
        self._vocab = {}
        self.original_values = {}
        self.random = np.random
        self.update()

    def create_original_values(self, args=None):
        return {attr: getattr(self.args, attr) if args is None else args[attr]
                for attr in RESTORED_ARGS if args is None or attr in args}

    def set_format(self, f=None, update=False, recursive=True):
        if f in (None, "text") and not self.format:  # In update or parsing UCCA (with no extra["format"]) or plain text
            f = "ucca"  # Default output format is UCCA
        if update or self.format != f:
            if f not in (None, "text"):
                self.format = f
            self.update_by_hyperparams()
        if recursive:
            for config in self.descendants():
                config.set_format(f=f, update=update, recursive=False)

    def descendants(self):
        ret = []
        configs = [self]
        while configs:
            c = configs.pop(0)
            ret += c.sub_configs
            configs += c.sub_configs
        return ret

    def is_unlabeled(self, f=None):
        # If just -u or --unlabeled is given then its value is [], and we want to treat that as "all formats"
        # If not given at all it is None, and we want to treat that as "no format"
        return self.args.unlabeled == [] or (f or self.format) in (self.args.unlabeled or ())

    def max_actions_unlabeled(self):
        return 6 + (  # Shift Node Reduce LeftEdge RightEdge Finish
            3 if self.args.remote else 0) + (  # RemoteNode LeftRemote RightRemote
            1 if self.args.swap == REGULAR else (self.args.max_swap if self.args.swap == COMPOUND else 0)) + (  # Swap
            1 if self.args.implicit else 0) + (  # Implicit
            2 if self.args.node_labels and not self.args.use_gold_node_labels else 0)  # Label x 2

    def set_dynet_arguments(self):
        self.random.seed(self.args.seed)
        kwargs = dict(random_seed=self.args.seed)
        if self.args.dynet_mem:
            kwargs.update(mem=self.args.dynet_mem)
        if self.args.dynet_weight_decay:
            kwargs.update(weight_decay=self.args.dynet_weight_decay)
        if self.args.dynet_gpus and self.args.dynet_gpus != 1:
            kwargs.update(requested_gpus=self.args.dynet_gpus)
        if self.args.dynet_autobatch:
            kwargs.update(autobatch=True)
        dynet_config.set(**kwargs)
        if self.args.dynet_gpu:
            dynet_config.set_gpu()

    def update(self, params=None):
        if params:
            for name, value in params.items():
                setattr(self.args, name, value)
        self.original_values.update(self.create_original_values(params))
        self.hyperparams = self.create_hyperparams()
        for f, num in EDGE_LABELS_NUM.items():
            self.hyperparams.specific[f].max_edge_labels = num
        amr_hyperparams = self.hyperparams.specific["amr"]
        for k, v in dict(node_label_dim=20, max_node_labels=1000, node_category_dim=5, max_node_categories=25).items():
            if k not in amr_hyperparams and not getattr(amr_hyperparams, k, None):
                setattr(amr_hyperparams, k, v)
        self.set_format(update=True)
        self.set_dynet_arguments()

    def create_hyperparams(self):
        return Hyperparams(parent=self.args, **{h.name: h.args for h in self.args.hyperparams or ()})

    def update_hyperparams(self, **kwargs):
        self.update(dict(hyperparams=[HyperparamsInitializer(k, **v) for k, v in kwargs.items()]))

    def update_iteration(self, iteration, print_message=True, recursive=True):
        if iteration.hyperparams:
            if print_message:
                print("Updating: %s" % iteration.hyperparams)
            self.iteration_hyperparams = iteration.hyperparams.args
            self.update_by_hyperparams()
            if recursive:
                for config in self.descendants():
                    config.update_iteration(iteration, print_message=False, recursive=False)

    def update_by_hyperparams(self):
        format_values = dict(self.original_values)
        for hyperparams in (self.iteration_hyperparams, self.hyperparams.specific[self.format]):
            if hyperparams:
                format_values.update({k: v for k, v in hyperparams.items() if not k.startswith("_")})
        for attr, value in sorted(format_values.items()):
            self.print("Setting %s=%s" % (attr, value))
            setattr(self.args, attr, value)
        if self.format != "amr":
            self.args.node_labels = False
            self.args.node_label_dim = self.args.max_node_labels = \
                self.args.node_category_dim = self.args.max_node_categories = 0
        if self.is_unlabeled():
            self.args.max_edge_labels = self.args.edge_label_dim = 0
            self.args.max_action_labels = self.max_actions_unlabeled()
        else:
            self.args.max_action_labels = max(self.args.max_action_labels, 6 * self.args.max_edge_labels)

    @property
    def line_end(self):
        return "\n" if self.args.verbose > 2 else " "  # show all in one line unless verbose

    @property
    def passage_word(self):
        return "sentence" if self.args.sentences else "paragraph" if self.args.paragraphs else "passage"

    @property
    def passages_word(self):
        return " %ss" % self.passage_word

    def log(self, message):
        try:
            if self._logger is None:
                FileHandler(self.args.log,
                            format_string="{record.time:%Y-%m-%d %H:%M:%S} {record.message}").push_application()
                if self.args.verbose > 1:
                    StderrHandler(bubble=True).push_application()
                self._logger = Logger("tupa")
            self._logger.warn(message)
        except OSError:
            pass

    def vocab(self, filename=None, lang=None):
        if filename is None:
            args = self.hyperparams.specific[lang] if lang else self.args
            filename = args.vocab
        if not filename:
            return None
        vocab = self._vocab.get(filename)
        if vocab:
            return vocab
        vocab = load_enum(filename)
        self._vocab[filename] = vocab
        return vocab

    def print(self, message, level=3):
        if self.args.verbose >= level:
            try:
                print(message() if hasattr(message, "__call__") else message, flush=True)
            except UnicodeEncodeError:
                pass

    def save(self, filename):
        out_file = filename + ".yml"
        print("Saving configuration to '%s'." % out_file)
        with open(out_file, "w") as f:
            name = None
            values = []
            for arg in shlex.split(str(self), "--") + ["--"]:
                if arg.startswith("--"):
                    if name and name not in ("train", "dev"):
                        if len(values) > 1:
                            values[0] = "[" + values[0]
                            values[-1] += "]"
                        elif name.startswith("no-"):
                            name = name[3:]
                            values = ["false"]
                        print("%s: %s" % (name, ", ".join(values) or "true"), file=f)
                    name = arg[2:]
                    values = []
                else:
                    values.append(arg)

    def copy(self):
        cls = self.__class__
        ret = cls.__new__(cls)
        ret.arg_parser = self.arg_parser
        ret.args = deepcopy(self.args)
        ret.original_values = deepcopy(self.original_values)
        ret.hyperparams = deepcopy(self.hyperparams)
        ret.iteration_hyperparams = deepcopy(self.iteration_hyperparams)
        ret.format = self.format
        ret.random = self.random
        ret._logger = self._logger
        ret._vocab = dict(self._vocab)
        ret.sub_configs = []
        self.sub_configs.append(ret)
        return ret

    def args_str(self, args):
        return ["--" + ("no-" if v is False else "") + k.replace("_", "-") +
                ("" if v is False or v is True else
                 (" " + str(" ".join(map(str, v)) if hasattr(v, "__iter__") and not isinstance(v, str) else v)))
                for (k, v) in sorted(args.items()) if
                v not in (None, (), "", self.arg_parser.get_default(k))
                and not k.startswith("_")
                and (args.node_labels or ("node_label" not in k and "node_categor" not in k))
                and (args.swap or "swap_" not in k)
                and (args.swap == COMPOUND or k != "max_swap")
                and (not args.require_connected or k != "orphan_label")
                and (args.classifier == SPARSE or k not in SPARSE_ARG_NAMES)
                and (args.classifier in NN_CLASSIFIERS or k not in NN_ARG_NAMES | DYNET_ARG_NAMES)
                and k != "passages"]

    def __str__(self):
        self.args.hyperparams = [HyperparamsInitializer(name, **args.vars()) for name, args in self.hyperparams.items()]
        return " ".join(list(self.args.passages) + self.args_str(self.args)) SEPARATOR = "." SPARSE = "sparse" MLP = "mlp" BIRNN = "bilstm" HIGHWAY_RNN = "highway" HIERARCHICAL_RNN = "hbirnn" NOOP = "noop" class FeatureParameters(Labels):
    def __init__(self, suffix, dim, size, dropout=0, updated=True, num=1, init=None, data=None, indexed=False,
                 copy_from=None, filename=None, min_count=1, enabled=True, node_dropout=0, vocab=None,
                 lang_specific=False):
        """
        :param suffix: one-character title for feature
        :param dim: vector dimension or, filename to load vectors from, or Word2Vec object
        :param size: maximum number of distinct values
        :param dropout: value of dropout parameter to use during training
        :param updated: whether the feature is learned (otherwise kept constant)
        :param num: how many such features exist per step
        :param init: array of values to use as initial value of embedding matrix
        :param data: DefaultOrderedDict of raw value to running numerical representation, or embedding matrix
        :param indexed: whether the feature is to be used as index into initialized values (otherwise used directly)
        :param copy_from: suffix of other parameter to copy values from instead of extracting them directly
        :param filename: name of file to load data from
        :param min_count: minimum number of occurrences for a feature value before it is actually added
        :param enabled: whether to actually use this parameter in feature extraction
        :param node_dropout: probability to drop whole node in feature extraction
        :param vocab: name of file to load mapping of integer ID to word form (to avoid loading spaCy)
        :param lang_specific: whether the feature params should be separate per language
        """
        super().__init__(size)
        self.suffix = suffix
        self.dim = dim
        self.size = size
        self.dropout = dropout
        self.updated = updated
        self.num = num
        self.init = init
        self.data = data
        self.indexed = indexed
        self.copy_from = copy_from
        self.filename = filename
        self.min_count = min_count
        self.enabled = enabled
        self.node_dropout = node_dropout
        self.vocab = vocab
        self.lang_specific = lang_specific

    def __repr__(self):
        return type(self).__name__ + "(" + ", ".join(
            map(str, (self.suffix, self.dim, self.size, self.dropout, self.updated, self.num, self.init, self.data,
                      self.indexed, self.copy_from, self.filename, self.min_count, self.enabled, self.node_dropout,
                      self.vocab, self.lang_specific))) + ")"

    def __eq__(self, other):
        return self.suffix == other.suffix and self.dim == other.dim and self.size == other.size and \
               self.dropout == other.dropout and self.updated == other.updated and self.num == other.num and \
               self.indexed == other.indexed and self.min_count == other.min_count and \
               self.numeric == other.numeric and self.node_dropout == other.node_dropout and \
               self.lang_specific == other.lang_specific

    def __hash__(self):
        return hash(self.suffix)

    def init_data(self):
        if self.data is None and not self.numeric:
            keys = ()
            if self.dim and self.external:
                vectors = self.word_vectors()
                keys = vectors.keys()
                self.init = np.array(list(vectors.values()))
            self.data = DropoutDict(size=self.size, keys=keys, dropout=self.dropout, min_count=self.min_count)

    def word_vectors(self):
        lang = Config().args.lang
        vectors, self.dim = get_word_vectors(self.dim, self.size, self.filename, Config().vocab(self.vocab) or lang)
        if self.size is not None:
            assert len(vectors) <= self.size, "Wrong number of loaded vectors: %d > %d" % (len(vectors), self.size)
        assert vectors, "Cannot load word vectors. Install using `python -m spacy download %s` or choose a file " \
                        "using the --word-vectors option." % lang
        self.size = len(vectors)
        return vectors

    @property
    def all(self):
        return self.data.all

    @all.setter
    def all(self, labels):
        self.data.all = labels

    @property
    def numeric(self):
        return False

    @property
    def prop(self):
        return self.copy_from or self.suffix

    @property
    def external(self):
        return self.copy_from is not None

    @staticmethod
    def copy(params, copy_dict=dict, copy_init=True, order=None):
        return OrderedDict((key, param.copy_with_data(copy_dict, copy_init)) for key, param in
                           (params.items() if order is None else sorted(params.items(), key=lambda x:
                            -1 if x[0] == NumericFeatureParameters.SUFFIX else order.index(x[0][0]))))

    def copy_with_data(self, copy_dict, copy_init):
        data = None if self.data is None else copy_dict(self.data)
        if hasattr(data, "size"):  # It may be an UnknownDict but we still want it to know its size
            data.size = self.size
        return FeatureParameters(suffix=self.suffix, dim=self.dim, size=self.size, dropout=self.dropout,
                                 updated=self.updated, num=self.num, init=self.init if copy_init else None, data=data,
                                 indexed=self.indexed, copy_from=self.copy_from, filename=self.filename,
                                 min_count=self.min_count, enabled=self.enabled,
                                 node_dropout=getattr(self, "node_dropout", 0),
                                 vocab=getattr(self, "vocab", None),
                                 lang_specific=getattr(self, "lang_specific", False))

    def unfinalize(self):
        self.data = DropoutDict(self.data, size=self.size, dropout=self.dropout, min_count=self.min_count) class UnknownDict(AutoIncrementDict):
    """
    DefaultOrderedDict that has a single default value for missing keys
    """
    def __init__(self, d=None):
        """
        :param d: base dict to initialize by
        """
        super().__init__(size=None, d=d) class AutoIncrementDict(DefaultOrderedDict):
    """
    DefaultOrderedDict that returns an auto-incrementing index for new keys
    """
    def __init__(self, size=None, keys=(), d=None, unknown=UNKNOWN_VALUE):
        """
        :param size: maximum size to allow, after which `unknown' will be returned
        :param keys: initial sequence of keys
        :param d: dictionary to initialize from
        :param unknown: value to return for missing keys
        """
        super().__init__(None, sorted(d.items(), key=itemgetter(1)) if d else {}, size=size)
        self.finalized = (size is None)
        self.unknown = self.setdefault(None, unknown)
        for key in keys:
            self.__missing__(key)

    def __missing__(self, key):
        if not self.finalized and len(self) < self.size:
            ret = self[key] = len(self)
            return ret
        return self.unknown

    def first_items(self, n=3):
        return ", ".join(map(str, sorted(self, key=self.get)[:n])) + (", ..." if len(self) > n else "")

    def __str__(self):
        return "{%s}" % self.first_items()

    def __repr__(self):
        return "%s(size=%s, keys=[%s], unknown=%s)" % (type(self).__name__, self.size, self.first_items(), self.unknown) def remove_backup(*filenames):
    for filename in filenames:
        for backup in glob(filename + "*~"):
            try:
                os.remove(backup)
            except OSError:
                pass def save_json(filename, d):
    """
    Save dictionary to JSON file
    :param filename: file to write to
    :param d: dictionary to save
    """
    remove_existing(filename)
    print("Saving to '%s'." % filename)
    with open(filename, "w") as h:
        json.dump(d, h, default=jsonify) def load_json(filename):
    """
    Load dictionary from JSON file
    :param filename: file to read from
    """
    print("Loading from '%s'." % filename)
    with open(filename, "r") as h:
        d = json.load(h)
    return d
from collections import OrderedDict
from enum import Enum
from ucca import textutil
from ucca.layer0 import Terminal
from .action import Actions
from .classifiers.classifier import Classifier
from .config import Config, SEPARATOR, SPARSE, MLP, BIRNN, HIGHWAY_RNN, HIERARCHICAL_RNN, NOOP
from .features.feature_params import FeatureParameters
from .model_util import UnknownDict, AutoIncrementDict, remove_backup, save_json, load_json
            from .features.sparse_features import SparseFeatureExtractor
            from .classifiers.linear.sparse_perceptron import SparsePerceptron
            from .features.empty_features import EmptyFeatureExtractor
            from .classifiers.noop import NoOp
            from .features.dense_features import DenseFeatureExtractor
            from .classifiers.nn.neural_network import NeuralNetwork
        lang = state.passage.attrib.get("lang")
        self.classifier.init_features(self.feature_extractor.init_features(state), axes, train, passage, lang)

    def finalize(self, finished_epoch):
        """
        Copy model, finalizing features (new values will not be added during subsequent use) and classifier (update it)
        :param finished_epoch: whether this is the end of an epoch (or just intermediate checkpoint), for bookkeeping
        :return: a copy of this model with a new feature extractor and classifier (actually classifier may be the same)
        """
        self.config.print("Finalizing model", level=1)
        self.init_model()
        return Model(None, config=self.config.copy(), model=self, is_finalized=True,
                     feature_extractor=self.feature_extractor.finalize(),
                     classifier=self.classifier.finalize(finished_epoch=finished_epoch))

    def save(self, save_init=False):
        """
        Save feature and classifier parameters to files
        """
        if self.filename is not None:
            self.init_model()
            try:
                self.feature_extractor.save(self.filename, save_init=save_init)
                node_labels = self.feature_extractor.params.get(NODE_LABEL_KEY)
                skip_labels = (NODE_LABEL_KEY,) if node_labels and node_labels.size else ()
                bert_configs = {
                        "use_bert": self.config.args.use_bert,
                        "bert_model": self.config.args.bert_model,
                        "bert_layers": self.config.args.bert_layers,
                        "bert_layers_pooling": self.config.args.bert_layers_pooling,
                        "bert_token_align_by": self.config.args.bert_token_align_by,
                        "bert_multilingual": self.config.args.bert_multilingual,
                        "bert_use_default_word_embeddings": self.config.args.bert_use_default_word_embeddings,
                        "bert_dropout": self.config.args.bert_dropout}\
                    if self.config.args.use_bert else {"use_bert": self.config.args.use_bert}
                self.classifier.save(self.filename, skip_labels=skip_labels,
                                     multilingual=self.config.args.multilingual,
                                     omit_features=self.config.args.omit_features,
                                     **bert_configs)
                textutil.models["vocab"] = self.config.args.vocab
                save_json(self.filename + ".nlp.json", textutil.models)
                remove_backup(self.filename)
            except Exception as e:
                raise IOError("Failed saving model to '%s'" % self.filename) from e

    def load(self, is_finalized=True):
        """
        Load the feature and classifier parameters from files
        :param is_finalized: whether loaded model should be finalized, or allow feature values to be added subsequently
        """
        if self.filename is not None:
            try:
                self.config.args.classifier = Classifier.get_property(self.filename, "type")
                self.config.args.multilingual = Classifier.get_property(self.filename, "multilingual")
                self.config.args.omit_features = Classifier.get_property(self.filename, "omit_features")
                self.config.args.use_bert = Classifier.get_property(self.filename, "use_bert")
                if self.config.args.use_bert:
                    self.config.args.bert_model = Classifier.get_property(self.filename, "bert_model")
                    self.config.args.bert_layers = Classifier.get_property(self.filename, "bert_layers")
                    self.config.args.bert_layers_pooling = Classifier.get_property(self.filename, "bert_layers_pooling")
                    self.config.args.bert_token_align_by = Classifier.get_property(self.filename, "bert_token_align_by")
                    self.config.args.bert_multilingual = Classifier.get_property(self.filename, "bert_multilingual")
                    self.config.args.bert_use_default_word_embeddings =\
                        Classifier.get_property(self.filename, "bert_use_default_word_embeddings")
                    self.config.args.bert_dropout = Classifier.get_property(self.filename, "bert_dropout")
                self.init_model(init_params=False)
                self.feature_extractor.load(self.filename, order=[p.name for p in self.param_defs()])
                if not is_finalized:
                    self.feature_extractor.unfinalize()
                self._update_input_params()  # Must be before classifier.load() because it uses them to init the model
                self.classifier.load(self.filename)
                self.is_finalized = is_finalized
                self.load_labels()
                for param_def in self.param_defs(self.config):
                    param_def.load_to_config(self.feature_extractor.params)
                try:
                    textutil.models.update(load_json(self.filename + ".nlp.json"))
                    vocab = textutil.models.get("vocab")
                    if vocab:
                        self.config.args.vocab = vocab
                except FileNotFoundError:
                    pass
                self.config.print("\n".join("%s: %s" % i for i in self.feature_params.items()), level=1)
            except FileNotFoundError:
                self.feature_extractor = self.classifier = None
                raise
            except Exception as e:
                raise IOError("Failed loading model from '%s'" % self.filename) from e

    def restore(self, model, feature_extractor=None, classifier=None, is_finalized=None):
        """
        Set all attributes to a reference to existing model, except labels, which will be copied.
        :param model: Model to restore
        :param feature_extractor: optional FeatureExtractor to restore instead of model's
        :param classifier: optional Classifier to restore instead of model's
        :param is_finalized: whether the restored model is finalized
        """
        if is_finalized is None:
            is_finalized = model.is_finalized
        self.config.print("Restoring %sfinalized model" % ("" if is_finalized else "non-"), level=1)
        self.filename = model.filename
        self.feature_extractor = feature_extractor or model.feature_extractor
        self.classifier = classifier or model.classifier
        self.is_finalized = is_finalized
        self._update_input_params()
        self.classifier.labels_t = OrderedDict((a, l.save()) for a, l in self.classifier.labels.items())
        self.load_labels()

    def load_labels(self):
        """
        Copy classifier's labels to create new Actions/Labels objects
        Restoring from a model that was just loaded from file, or called by restore()
        """
        for axis, all_size in self.classifier.labels_t.items():  # all_size is a pair of (label list, size limit)
            if axis == NODE_LABEL_KEY:  # These are node labels rather than action labels
                node_labels = self.feature_extractor.params.get(NODE_LABEL_KEY)
                if node_labels and node_labels.size:  # Also used for features, so share the dict
                    del all_size
                    labels = node_labels.data
                else:  # Not used as a feature, just get labels

                    labels = UnknownDict() if self.is_finalized else AutoIncrementDict()