spaCy v1.0深度学习与自定义流水线集成指南

本文详细介绍了spaCy v1.0如何通过自定义流水线集成Keras驱动的LSTM情感分析模型,包括代码示例、架构设计和实际应用方法,涵盖模型训练、特征提取和运行时优化等技术细节。

spaCy v1.0: 使用自定义流水线和Keras进行深度学习

2016年10月19日 | 7分钟阅读

很高兴宣布spaCy 1.0版本的发布,这是全球最快的NLP库。1.0版本最出色的功能是一个新系统,用于将自定义模型集成到spaCy中。本文介绍这些变化,并展示如何使用新的自定义流水线功能将Keras驱动的LSTM情感分析模型添加到spaCy流水线中。

如何通过Keras使用LSTM模型为spaCy添加情感分析

有许多优秀的开源库用于研究、训练和评估神经网络。然而,这些库的关注点通常在获得评估分数和模型文件时结束。spaCy一直设计用于协调多个文本注释模型,并帮助在应用程序中一起使用它们。spaCy 1.0现在使得使用自定义模型计算这些注释变得更加容易。

什么是Keras?

Keras提供了一个高级声明式接口来定义神经网络。模型默认使用某中心的TensorFlow进行训练,也支持Theano。

在本教程中,将使用Keras,因为它是最流行的Python深度学习库。假设您编写了一个自定义情感分析模型,预测文档是正面还是负面。现在您想找出哪些实体通常与正面或负面文档相关联。以下是运行时的一个快速示例。

运行时使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def count_entity_sentiment(nlp, texts):
    '''计算文本中每个实体的净文档情感。'''
    entity_sentiments = collections.Counter(float)
    for doc in nlp.pipe(texts, batch_size=1000, n_threads=4):
        for ent in doc.ents:
            entity_sentiments[ent.text] += doc.sentiment
    return entity_sentiments

def load_nlp(lstm_path, lang_id='en'):
    def create_pipeline(nlp):
        return [nlp.tagger, nlp.entity, SentimentAnalyser.load(lstm_path, nlp)]
    return spacy.load(lang_id, create_pipeline=create_pipeline)

只需将一个create_pipeline回调函数传递给spacy.load()。该函数应将一个spacy.language.Language对象作为其唯一参数,并返回一个可调用序列。每个可调用对象应接受一个Doc对象,就地修改它,并返回None。

当然,对单个文档进行操作效率低下,尤其是对于深度学习模型。通常需要注释许多文本,并希望并行处理它们。因此,应确保模型组件也支持.pipe()方法。.pipe()方法应是一个行为良好的生成器函数,处理任意大的序列。它应消耗一小部分文档缓冲区,并行处理它们,并逐个生成。

自定义注释器类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class SentimentAnalyser(object):
    @classmethod
    def load(cls, path, nlp):
        with (path / 'config.json').open() as file_:
            model = model_from_json(file_.read())
        with (path / 'model').open('rb') as file_:
            lstm_weights = pickle.load(file_)
        embeddings = get_embeddings(nlp.vocab)
        model.set_weights([embeddings] + lstm_weights)
        return cls(model)

    def __init__(self, model):
        self._model = model

    def __call__(self, doc):
        X = get_features([doc], self.max_length)
        y = self._model.predict(X)
        self.set_sentiment(doc, y)

    def pipe(self, docs, batch_size=1000, n_threads=2):
        for minibatch in cytoolz.partition_all(batch_size, docs):
            Xs = get_features(minibatch)
            ys = self._model.predict(X)
            for i, doc in enumerate(minibatch):
                doc.sentiment = ys[i]

    def set_sentiment(self, doc, y):
        doc.sentiment = float(y[0])
        # 情感有一个原生槽用于单个浮点数。
        # 对于任意数据存储,可以使用:
        # doc.user_data['my_data'] = y

def get_features(docs, max_length):
    Xs = numpy.zeros((len(docs), max_length), dtype='int32')
    for i, doc in enumerate(minibatch):
        for j, token in enumerate(doc[:max_length]):
            Xs[i, j] = token.rank if token.has_vector else 0
    return Xs

默认情况下,spaCy 1.0下载并使用300维GloVe常见爬取向量。也很容易用自己训练的向量替换这些向量,或完全禁用词向量。如果已将词向量安装到spaCy的Vocab对象中,以下是如何在Keras模型中使用它们:

使用Keras训练

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def train(train_texts, train_labels, dev_texts, dev_labels,
        lstm_shape, lstm_settings, lstm_optimizer, batch_size=100, nb_epoch=5):
    nlp = spacy.load('en', parser=False, tagger=False, entity=False)
    embeddings = get_embeddings(nlp.vocab)
    model = compile_lstm(embeddings, lstm_shape, lstm_settings)
    train_X = get_features(nlp.pipe(train_texts))
    dev_X = get_features(nlp.pipe(dev_texts))
    model.fit(train_X, train_labels, validation_data=(dev_X, dev_labels),
                nb_epoch=nb_epoch, batch_size=batch_size)
    return model

def compile_lstm(embeddings, shape, settings):
    model = Sequential()
    model.add(
        Embedding(
            embeddings.shape[1],
            embeddings.shape[0],
            input_length=shape['max_length'],
            trainable=False,
            weights=[embeddings]
        )
    )
    model.add(Bidirectional(LSTM(shape['nr_hidden'])))
    model.add(Dropout(settings['dropout']))
    model.add(Dense(shape['nr_class'], activation='sigmoid'))
    model.compile(optimizer=Adam(lr=settings['lr']), loss='binary_crossentropy',
                    metrics=['accuracy'])
    return model

def get_embeddings(vocab):
    max_rank = max(lex.rank for lex in vocab if lex.has_vector)
    vectors = numpy.ndarray((max_rank+1, vocab.vectors_length), dtype='float32')
    for lex in vocab:
        if lex.has_vector:
            vectors[lex.rank] = lex.vector
    return vectors

def get_features(docs, max_length):
    Xs = numpy.zeros(len(list(docs)), max_length, dtype='int32')
    for i, doc in enumerate(docs):
        for j, token in enumerate(doc[:max_length]):
            Xs[i, j] = token.rank if token.has_vector else 0
    return Xs

对于大多数应用程序,建议使用预训练的词嵌入而不进行"微调"。这意味着将在不同模型中使用相同的嵌入,并避免在训练数据上学习调整它们。嵌入表很大,预训练向量提供的值已经很好。因此,微调嵌入表是浪费"参数预算"。通常最好通过其他方式使网络更大,例如添加另一个LSTM层、使用注意力机制、使用字符特征等。

属性钩子(实验性)

之前,我们看到了如何将数据存储在新的通用user_data字典中。这很好地泛化,但并不十分令人满意。理想情况下,希望让自定义数据驱动更"原生"的行为。例如,考虑spaCy的Doc、Token和Span对象提供的.similarity()方法:

多态相似性示例

1
2
3
span.similarity(doc)
token.similarity(span)
doc1.similarity(doc2)

实现说明 钩子位于Doc对象上,因为Span和Token对象是延迟创建的,并且不拥有任何数据。它们只是代理到其父Doc。这在这里很方便——只需担心在一个地方安装钩子。

默认情况下,这只是对每个文档的向量进行平均,并计算它们的余弦。显然,spaCy应使安装自己的相似性模型变得容易。这引入了一个棘手的设计挑战。当前的解决方案是向Doc对象添加三个更多字典:

名称 描述
user_hooks 自定义doc.vector、doc.has_vector、doc.vector_norm或doc.sents的行为
user_token_hooks 自定义token.similarity、token.vector、token.has_vector、token.vector_norm或token.conjuncts的行为
user_span_hooks 自定义span.similarity、span.vector、span.has_vector、span.vector_norm或span.root的行为

总结一下,以下是一个钩入自定义.similarity()方法的示例:

添加自定义相似性钩子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class SimilarityModel(object):
    def __init__(self, model):
        self._model = model

    def __call__(self, doc):
        doc.user_hooks['similarity'] = self.similarity
        doc.user_span_hooks['similarity'] = self.similarity
        doc.user_token_hooks['similarity'] = self.similarity

    def similarity(self, obj1, obj2):
        y = self._model([obj1.vector, obj2.vector])
        return float(y[0])

下一步是什么?

属性钩子可能会略有发展,并且肯定需要一些调整才能完全一致。也期待为标记器、解析器和实体识别器提供改进的模型。在过去十二个月中,研究表明双向LSTM模型是这些任务的简单有效方法。 resulting模型在内存中也应显著 smaller。

资源

可以在Semantic Scholar上找到更多描述基于双向LSTM模型的论文。 教程代码:GitHub上的完整代码。 spaCy v1.0发布说明 spaCy文档 Keras文档

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计