是否可以导出和使用没有词汇的 spaCy NER 模型并即时注入 tokens/vectors?

Is it possible to export and use a spaCy NER model without an vocab and inject tokens/vectors on the fly?

简介

我的问题离标题有点远,但本质上很好地总结了我目前遇到的问题。

我需要将 spacy 的 NER 模型集成为复杂的分布式 NLP 管道的一部分,我正在做的是:

  1. 基于 en_core_web_lg 模型训练一个新的 NER 模型以识别我在 NER 任务中的自定义实体

  2. 保存模型跳过词汇以节省磁盘space和内存使用

  3. 最终将模型加载到 运行 一些推理,使用某人 pre-computed 之前在我的管道中的标记和向量,而不是使用模型词汇表再次计算它(标准方式)。

我保存没有词汇表的模型的原因是因为在我的分布式管道中,首先要做的事情之一是对文本进行标记化/矢量化,以便其余任务具有此输入。

→ 在继续之前,我想澄清的是,以标准方式(保存词汇),我可以训练我的自定义 NER,保存/加载和 运行 一个没有重大问题的推理,具有非常好的准确性。

之后,从标记列表和自定义词汇表(在我的例子中是一个空词汇表)中读取 spaCy documentation, I found that it is possible to save my model without the vocabulary, and you can even build the Doc。此外,我还能够使用之前有人在我的管道中为我计算的文档向量来设置文档向量。

但是,当我将模型保存在 ner/cfg 文件中时,我看到有一个对训练 NER 模型的向量的引用 (en_core_web_lg.vectors):

{
   "disable":[
     "tagger",
     "parser"
   ],
   "beam_width":1,
   "beam_density":0.0,
   "beam_update_prob":1.0,
   "cnn_maxout_pieces":3,
   "nr_feature_tokens":6,
   "deprecation_fixes":{
      "vectors_name":"en_core_web_lg.vectors"
   },
   "nr_class":86,
   "hidden_depth":1,
   "token_vector_width":96,
   "hidden_width":64,
   "maxout_pieces":2,
   "pretrained_vectors":"en_core_web_lg.vectors",
   "bilstm_depth":0,
   "self_attn_depth":0,
   "conv_depth":4,
   "conv_window":1,
   "embed_size":2000
 }

当我尝试在内存中没有这些向量的情况下加载模型(换句话说,没有加载该词汇)时,此引用是错误的原因。

如果我删除 cfg 文件中的那些引用,模型会正确加载,我可以 运行 使用我的向量进行推断,但获得的预测与我使用第一个模型获得的预测非常不同(使用原始词汇)并包含一些错误。

问题

这让我想到了我原来的问题:是否可以用空词汇保存 NER 模型,然后 运行 使用 SpaCy Doc 以某种方式从我的管道中先前计算的标记和向量构建的推理?

非常感谢!

顺便说一句,我正在使用 spaCy 2.3.7,我在下面放了一些代码片段来说明:

1。训练

    nlp = spacy.load("en_core_web_lg", disable=['tagger', 'parser'])

    ner = nlp.get_pipe('ner')
    ner.add_label("FOO_ENTITY")
    ner.add_label("BAR_ENTITY")
    ner.add_label("COOL_ENTITY")

    # Start the training
    optimizer = nlp.begin_training()

    # Loop for EPOCHS iterations
    losses_hist = []
    for itn in range(30):
        # Shuffle the training data
        random.shuffle(Xy_train)
        losses = {}

        # Batch the examples and iterate over them
        for batch in spacy.util.minibatch(Xy_train, size=32):
            texts = [text for text, entities, _ in batch]
            golds = [{'entities': entities} for text, entities, _ in batch]

            # Update the model
            nlp.update(docs=texts, golds=golds, losses=losses)

        print(losses)
        losses_hist.append(losses)

2.a 运行 推理(标准):

    # I already have the text split in tokens
    doc = Doc(nlp.vocab, words=tokens)  # Create doc from tokens
    ner = nlp.get_pipe("ner")
    doc = ner(doc)  # Call NER step for doc

    for ent in doc.ents:
        print(f"value: {ent.text}, start: {ent.start_char}, end: {ent.end_char}, entity: {ent.label_}")

2.b 运行 用外部向量推断

    # I already have the text split in tokens and their vectors
    vectors = Vectors(data=embeddings, keys=tokens)
    nlp.vocab.vectors = vectors
    doc = Doc(nlp.vocab, words=tokens)
    
    ner = nlp.get_pipe("ner")
    doc = ner(doc)  # Call NER step for doc

    for ent in doc.ents:
        print(f"value: {ent.text}, start: {ent.start_char}, end: {ent.end_char}, entity: {ent.label_}")

3。保存/加载模型:

# Save model
nlp.to_disk(str(dir_)

# Load model
nlp = spacy.load(str(dir_), exclude=['vocab']) 

在 spacy v2(不是 v3!)中,有一些隐藏的背景步骤,这些步骤以特定名称全局注册向量,以用作统计模型中的特征。 (这背后的想法是,同一进程中的多个模型可能会共享 RAM 中的相同向量。)

要获得适用于特定文本的矢量子集,您需要以正确的名称注册新矢量:

  • 在创建向量时使用与模型元数据中相同的 Vectors(name=)(类似于 en_core_web_lg.vectors
  • 运行 spacy._ml.link_vectors_to_models(vocab)

我敢肯定,如果您对具有相同名称的不同矢量集重复执行此操作,这将开始打印警告并根据数据形状在内部重命名矢量。我认为您可以忽略警告,它适用于单个文本,但它可能会破坏在同一脚本中加载的任何其他模型,这些模型使用相同的向量 name/shape.

如果您在实践中经常这样做,您可能想要编写 link_vectors_to_models 的自定义版本,它可以更有效地迭代词汇表中的单词以获取非常小的向量表,或者只修改单词在你知道你需要的词汇中。这实际上取决于您 运行 宁 link_vectors_to_models.

时的词汇大小

经过多次测试,我发现以下在内存中为我的模型的词汇重新加载向量的方法对我来说是正确的。另一方面, 答案起到了启发作用,但不适用于 spaCy 2.1.4。

有趣的是:

  • 在更新之前,我必须对词汇进行深度复制
  • 向量的名称必须与训练 NER 模型的向量的名称完全相同(en_core_web_lg.vectors 在我的例子中)
  • 需要更新内存中的向量如下:thinc.extra.load_nlp.VECTORS
    import copy
    import spacy
    import thinc
    from spacy.vectors import Vectors
    from spacy.vocab import Vocab
    from thinc.v2v import Model
    
    vocab = copy.deepcopy(nlp.vocab)
    vectors = Vectors(data=embeddings, keys=tokens, name='en_core_web_lg.vectors')
    ops = Model.ops
    thinc.extra.load_nlp.VECTORS[(ops.device, 'en_core_web_lg.vectors')] = vectors.data  # Must re-load vectors
    vocab.vectors = vectors
    
    doc = Doc(vocab, words=tokens)  # Create doc from tokens an custom vocab
    ner = nlp.get_pipe("ner")
    doc = ner(doc)  # Call NER step for doc

希望对其他用户有用!