代码详解:Sequence2Sequence模型让NER so easy

当下有一种非常流行的自然语言处理任务,叫做命名实体识别(Named Entity Recognition NER)。简而言之,NER是从一系列单词(句子)中提取名称实体的任务。

例如,给出句子:

“Jim bought 300 shares of Acme Corp. in 2006.”

“吉姆”是一个人,“Acme”是一个组织,“2006”是时间。

为此,本文将使用公开的Kaggle数据集,跳过所有数据处理代码,重点关注实际问题和解决方案。

在此数据集中有许多实体类型,如Person(PER),Organization(ORG)等。对于每种实体类型,有两种类型的标签:“B-SOMETAG”和“I-SOMETAG”。 B表示实体名称的开头,I表示该实体的延续。因此,如果有像“Word Health Organization”这样的实体,相应的标签将是[B-ORG,I-ORG,I-ORG]。

以下是数据集中的例子:

import pandas as pd

ner_df = pd.read_csv('ner_dataset.csv')

ner_df.head(30)

所以我们得到一些序列(句子),想要预测每个单词的“类”。这不像分类或回归这样的机器学习任务一样琐碎。得到一个序列,输出应该是一个大小相同的序列。

有很多方法可以解决这个问题。在这里,需要做以下事情:

构建一个非常简单的模型,将此任务视为句子中每个单词的分类,并将其用作基准。

使用Keras构建Seq2Seq模型。

谈谈衡量和比较结果的正确方法。

在Seq2Seq模型中使用预先训练好的Glove嵌入。

随意跳转到任何部分。

词袋(Bag of Words)和多级分类

正如之前提到的,输出应该是一系列的分类。但是,本文想探索一些简单的方法——一个简单的多级分类模型。将句子中的每个单词视为一个单独的实例,并且对于每个实例(单词)都能预测其分类,即O,B-ORG,I-ORG,B-PER等中的一个。

这当然不是模拟这个问题的最佳方法,但这样做有两个原因:创建一个尽可能保持简单的基准;当我们使用序列模型时,sequence2sequence会运行得更好。

很多时候,当我们试图模拟现实生活中的问题时,并不总是清楚正在处理什么类型的问题。有时需要尝试将这些问题建模为简单的分类任务,而实际上,序列模型可能会更好。

正如先前所说,将这种方法视为基准,并尽可能简化事物。因此对于每个单词(实例)来说,这个方法会将这些单词矢量(Bag of words)简化,同一句子中的其他单词也是这样处理的。

def sentence_to_instances(words, tags, bow, count_vectorizer):

X = []

y = []

for w, t in zip(words, tags):

v = count_vectorizer.transform([w])[0]

v = scipy.sparse.hstack([v, bow])

X.append(v)

y.append(t)

return scipy.sparse.vstack(X), y

可以看一下以下句子:

“The World Health Organization says 227 people have died from bird flu”

我们为每个单词准备12个实例。

the O

world B-org

health I-org

organization I-org

says O

227 O

people O

have O

died O

from O

bird O

flu O

现在我们的任务是在每个句子中用一个单词来预测它的类别。

数据集中有47958个句子,将其分为“训练”集和“测试”集:

train_size = int(len(sentences_words) * 0.8)

train_sentences_words = sentences_words[:train_size]

train_sentences_tags = sentences_tags[:train_size]

test_sentences_words = sentences_words[train_size:]

test_sentences_tags = sentences_tags[train_size:]

# ============== Output

==============================

Train: 38366

Test: 9592

使用上述方法将所有句子转换为多个单词实例。在训练数据集中,有839,214个单词实例。

train_X, train_y = sentences_to_instances(train_sentences_words,

train_sentences_tags,

count_vectorizer)

print 'Train X shape:', train_X.shape

print 'Train Y shape:', train_y.shape

# ============== Output

==============================

Train X shape: (839214, 50892)

Train Y shape: (839214,)

在X中,有50892个维度:当前单词有一个热矢量,同一个句子中所有其他单词有一个词袋矢量。

使用Gradient Boosting Classifier作为预测器:

clf = GradientBoostingClassifier().fit(train_X, train_y)

predicted = clf.predict(test_X)

print classification_report(test_y, predicted)

然后得到以下信息:

precision recall f1-score support

B-art 0.57 0.05 0.09 82

B-eve 0.68 0.28 0.40 46

B-geo 0.91 0.40 0.56 7553

B-gpe 0.96 0.84 0.90 3242

B-nat 0.52 0.27 0.36 48

B-org 0.93 0.31 0.46 4082

B-per 0.80 0.52 0.63 3321

B-tim 0.91 0.66 0.76 4107

I-art 0.09 0.02 0.04 43

I-eve 0.33 0.02 0.04 44

I-geo 0.82 0.55 0.66 1408

I-gpe 0.86 0.62 0.72 40

I-nat 0.20 0.08 0.12 12

I-org 0.88 0.24 0.38 3470

I-per 0.93 0.25 0.40 3332

I-tim 0.67 0.15 0.25 1308

O 0.91 1.00 0.95 177215

avg/ 0.91 0.91 0.89 209353

total

这样的效果好吗?很难知道。我们可能会考虑几种方法来改进模型,但这不是这篇文章的目标,正如先前所说的——我们想让它成为一个非常简单的基准。

这不是衡量模型的正确方法。我们得到了每个单词的精确度/召回率,但它并没有告诉我们任何关于真实的实体信息。以相同的句子为例,下面是一个简单的例子:

“The World Health Organization says 227 people have died from bird flu”

我们有3个ORG分类,如果只正确预测其中两个,将得到66%的准确率,但我们没有正确提取“World Health Organization”实体,所以实体的准确性将是0!

本文将在后面讨论更好的方法来测量命名实体识别模型,但首先需要构建 Sequence to Sequence模型。

Sequence to Sequence模型

前一个方法的主要缺点是丢失了依赖性信息。给出句子中的单词,知道左侧(或右侧)的单词是实体可能是有用的。为每个单词构建实例时,不仅很难实现,而且也没有在预测时间内获得这些信息。这是将整个序列用作实例的原因。

我们可以使用许多不同的模型来实现这一点。像隐马尔可夫模型(HMM)或条件随机场(CRF)这样的算法可能效果很好,但在这里将用Keras实现递归神经网络。

要使用Keras,需要将句子转换为数字序列,每个数字代表一个单词。我们需要使所有序列的长度相同。对此,可以使用Keras util方法来完成它。

首先,使用标记器来将单词转换为数字。将它只安装在训练集是非常重要。

words_tokenizer = Tokenizer(num_words=VOCAB_SIZE,

filters=[],

oov_token='__UNKNOWN__')

words_tokenizer.fit_on_texts(map(lambda s: ' '.join(s),

train_sentences_words))

word_index = words_tokenizer.word_index

word_index['__PADDING__'] = 0

index_word = {i:w for w, i in word_index.iteritems()}

# ============== Output

==============================

print 'Unique tokens:', len(word_index)

接下来,使用标记器创建序列并对其进行填充以获得相同长度的序列:

train_sequences = words_tokenizer.texts_to_sequences(map(lambda s: ' '.join(s), train_sentences_words))

test_sequences = words_tokenizer.texts_to_sequences(map(lambda s: ' '.join(s), test_sentences_words))

train_sequences_padded = pad_sequences(train_sequences,

maxlen=MAX_LEN)

test_sequences_padded = pad_sequences(test_sequences,

maxlen=MAX_LEN)

print train_sequences_padded.shape, test_sequences_padded.shape

# ============== Output

==============================

(38366, 75) (9592, 75)

可以看到,在训练集中有38,366个序列,在测试集中有9,592个序列,每个序列中有75个标记。

print train_tags_padded.shape, test_tags_padded.shape

# ============== Output

==============================

(38366, 75, 1) (9592, 75, 1)

在训练集中有38,366个序列,在测试集中有9,592个序列,每个序列中有17个标签。

现在准备建立模型。使用理解长短期记忆模型(LSTM)层,事实证明对这些任务非常有效:

input = Input(shape=(75,), dtype='int32')

emb = Embedding(V_SIZE, 300, max_len=75)(input)

x = Bidirectional(LSTM(64, return_sequences=True))(emb)

preds = Dense(len(tag_index), activation='softmax')(x)

model = Model(sequence_input, preds)

model.compile(loss='sparse_categorical_crossentropy',

optimizer='adam',

metrics=['sparse_categorical_accuracy'])

让我们看看这里有什么:

第一层是Input它接受形状向量(75),并且匹配X变量(在训练和测试中的每个序列中都有75个标记)。

接下来是嵌入层。这一层会抓取每个标记/文字,并把它变成一个容量大小为300的密集向量。将它看作一个巨大的查找表(或字典),其中标记(单词id)作为键,实际向量作为值。该查找表是可训练的,即在模型训练期间的每个阶段,更新这些向量以匹配输入。

在嵌入层之后,输入从长度为75的向量变为大小为75,300的矩阵。 这75个标记,每个现在都有一个大小为300的向量。

一旦有了这个,就可以使用双向LSTM层,每个标记都会在句子中查看两种方式并返回一个状态,以帮助我们稍后对该单词进行分类。默认情况下,LSTM层将返回单个向量(最后一个),但在示例中,我们需要每个标记的向量,因此我们使用return_sequences = True。

操作如下:

该层的输出是一个大小为75,128的矩阵——其中包括75个标记,一个方向为64个,另一个也为64个。

最后,有一个时间分布密集层(当我们使用return_sequences = True时,它变为时间分布)。

它采用LSTM层输出的75,128个矩阵并返回所需的75,18个矩——包括75个标记,每个标记有17个标签概率和一个__PADDING__。

使用model.summary()方法很容易看到发生了什么:

___________________________

Layer (type) Output Shape Param # ===========================

input_1 (InputLayer) (None, 75) 0

___________________________

embedding_1 (Embedding) (None, 75, 300) 8646600 ___________________________

bidirectional_1 (Bidirection (None, 75, 128) 186880 ___________________________

dense_2 (Dense) (None, 75, 18) 627 ==========================

Total params: 8,838,235

Trainable params: 8,838,235

Non-trainable params: 0

___________________________

可以使用输入和输出形状查看所有图层。此外,我们可以看到模型中的参数数量。你可能已经注意到嵌入层的参数最多。这是因为我们有很多单词,需要为每个单词学习300个数字。之后,我们会使用预先训练的嵌入来改进模型。

开始训练我们的模型:

model.fit(train_sequences_padded, train_tags_padded,

batch_size=32,

epochs=10,

validation_data=(test_sequences_padded, test_tags_padded))

# ============== Output

==============================

Train on 38366 samples, validate on 9592 samples

Epoch 1/10

38366/38366 [==============================] - 274s

7ms/step - loss: 0.1307 - sparse_categorical_accuracy: 0.9701 - val_loss:

0.0465 - val_sparse_categorical_accuracy: 0.9869

Epoch 2/10

38366/38366 [==============================] - 276s

7ms/step - loss: 0.0365 - sparse_categorical_accuracy: 0.9892 - val_loss:

0.0438 - val_sparse_categorical_accuracy: 0.9879

Epoch 3/10

38366/38366 [==============================] - 264s

7ms/step - loss: 0.0280 - sparse_categorical_accuracy: 0.9914 - val_loss:

0.0470 - val_sparse_categorical_accuracy: 0.9880

Epoch 4/10

38366/38366 [==============================] - 261s

7ms/step - loss: 0.0229 - sparse_categorical_accuracy: 0.9928 - val_loss:

0.0480 - val_sparse_categorical_accuracy: 0.9878

Epoch 5/10

38366/38366 [==============================] - 263s

7ms/step - loss: 0.0189 - sparse_categorical_accuracy: 0.9939 - val_loss:

0.0531 - val_sparse_categorical_accuracy: 0.9878

Epoch 6/10

38366/38366 [==============================] - 294s

8ms/step - loss: 0.0156 - sparse_categorical_accuracy: 0.9949 - val_loss:

0.0625 - val_sparse_categorical_accuracy: 0.9874

Epoch 7/10

38366/38366 [==============================] - 318s

8ms/step - loss: 0.0129 - sparse_categorical_accuracy: 0.9958 - val_loss:

0.0668 - val_sparse_categorical_accuracy: 0.9872

Epoch 8/10

38366/38366 [==============================] - 275s

7ms/step - loss: 0.0107 - sparse_categorical_accuracy: 0.9965 - val_loss:

0.0685 - val_sparse_categorical_accuracy: 0.9869

Epoch 9/10

38366/38366 [==============================] - 270s

7ms/step - loss: 0.0089 - sparse_categorical_accuracy: 0.9971 - val_loss:

0.0757 - val_sparse_categorical_accuracy: 0.9870

Epoch 10/10

38366/38366 [==============================] - 266s

7ms/step - loss: 0.0076 - sparse_categorical_accuracy: 0.9975 - val_loss:

0.0801 - val_sparse_categorical_accuracy: 0.9867

测试集的准确率达到98.6%。这种准确性并没有告诉我们多少实际价值,因为大多数标签都是“0”(其他)。我们希望像以前一样看到每个类的精确度/召回率,但这也不是评估模型的最佳方法。这种方法可以看到有多少不同类型的实体能够被正确预测。

Sequence to Sequence模型的评估

使用序列时,标签/实体也可能是序列。如果将“World Health Organisation”作为真正的实体,从单词角度来讲,预测“World Organisation”或“World Health”可能会给出66%的准确度,但两者都是错误的预测。这里我们包装每个句子中的所有实体,并将它们与预测的实体进行比较。

我们可以使用非常好的seqeval库。对于每个句子,seqeval库可以查找所有不同的标签并构造实体。通过对真实标签和预测标签进行处理,可以比较真实实体值而不仅仅是单词的价值。在这种情况下,没有“B-”或“I-”标签,我们比较实体的实际类型而不是单词分类。

使用预测值,这是一个概率矩阵,我们想要为每个句子构建一个具有原始长度的标签序列(而不是我们所做的那样),因此可以将它们与真实值进行比较。为LSTM模型和词袋模型执行以下操作:

lstm_predicted = model.predict(test_sequences_padded)

lstm_predicted_tags = []

bow_predicted_tags = []

for s, s_pred in zip(test_sentences_words, lstm_predicted):

tags = np.argmax(s_pred, axis=1)

tags = map(index_tag_wo_padding.get,tags)[-len(s):]

lstm_predicted_tags.append(tags)

bow_vector, _ = sentences_to_instances([s],

[['x']*len(s)],

count_vectorizer)

bow_predicted = clf.predict(bow_vector)[0]

bow_predicted_tags.append(bow_predicted)

现在我们准备使用seqeval库评估模型:

from seqeval.metrics import classification_report, f1_score

print 'LSTM'

print '='*15

print classification_report(test_sentences_tags,

lstm_predicted_tags)

print

print 'BOW'

print '='*15

print classification_report(test_sentences_tags,bow_predicted_tags)

可以得到:

LSTM

===============

precision recall f1-score support

art 0.11 0.10 0.10 82

gpe 0.94 0.96 0.95 3242

eve 0.21 0.33 0.26 46

per 0.66 0.58 0.62 3321

tim 0.84 0.83 0.84 4107

nat 0.00 0.00 0.00 48

org 0.58 0.55 0.57 4082

geo 0.83 0.83 0.83 7553

avg / 0.77 0.75 0.76 22481

total

BOW

===============

precision recall f1-score support

art 0.00 0.00 0.00 82

gpe 0.01 0.00 0.00 3242

eve 0.00 0.00 0.00 46

per 0.00 0.00 0.00 3321

tim 0.00 0.00 0.00 4107

nat 0.00 0.00 0.00 48

org 0.01 0.00 0.00 4082

geo 0.03 0.00 0.00 7553

avg / 0.01 0.00 0.00 22481

total

这呈现出了很大的不同。可以看到词袋模型几乎无法正确预测任何事情,而LSTM模型却表现得很好。

当然,我们可以在词袋模型上多次操作以获得更好的结果,但是结果显而易见,在这种情况下,Sequence to Sequence模型更合适。

如前所述,大多数模型参数都是嵌入层。训练这一层非常困难,因为单词量很大但训练数据有限。用预先训练的嵌入层非常常见。大多数当前的嵌入模型使用所谓的“分布假设”,其表明同一上下文中的单词具有相似的含义。通过构建一个预测给定上下文(或其他方式)的单词模型,可以生成具有良好的单词含义表示的单词向量。虽然这与我们的任务没有直接关系,但使用这些嵌入可能有助于模型更好地为其目标锁定单词。

还有其他方法可以构建单词嵌入,从简单的共生矩阵到更复杂的语言模型。在此之前,我尝试使用了图像构建单词嵌入。

在这里,会使用流行的Glove嵌入。Word2Vec或任何其他模型的效果也很好。

将Glove模型下载下来,加载单词向量并创建嵌入矩阵。我们将使用此矩阵作为嵌入层的不可训练权重:

embeddings = {}

with open(os.path.join(GLOVE_DIR, 'glove.6B.300d.txt')) as f:

for line in f:

values = line.split()

word = values[0]

coefs = np.asarray(values[1:], dtype='float32')

embeddings[word] = coefs

num_words = min(VOCAB_SIZE, len(word_index) + 1)

embedding_matrix = np.zeros((num_words, 300))

for word, i in word_index.items():

if i >= VOCAB_SIZE:

continue

embedding_vector = embeddings.get(word)

if embedding_vector is not None:

embedding_matrix[i] = embedding_vector

下面是我们的模型:

input = Input(shape=(75,), dtype='int32')

emb = Embedding(VOCAB_SIZE, 300,

embeddings_initializer=Constant(embedding_matrix),

input_length=MAX_LEN,

trainable=False)(input)

x = Bidirectional(LSTM(64, return_sequences=True))(emb)

preds = Dense(len(tag_index), activation='softmax')(x)

model = Model(sequence_input, preds)

model.compile(loss='sparse_categorical_crossentropy',

optimizer='adam',

metrics=['sparse_categorical_accuracy'])

model.summary()

# ============== Output

==============================

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

input_2 (InputLayer) (None, 75) 0

_________________________________________________________________

embedding_2 (Embedding) (None, 75, 300) 8646600

_________________________________________________________________

bidirectional_2 (Bidirection (None, 75, 128) 186880

_________________________________________________________________

dropout_2 (Dropout) (None, 75, 128) 0

_________________________________________________________________

dense_4 (Dense) (None, 75, 18) 627

=================================================================

Total params: 8,838,235

Trainable params: 191,635

Non-trainable params: 8,646,600

_________________________________________________________________

所有的内容都和以前一样。唯一的区别是,现在的嵌入层具有恒定的非训练权重。可以看到总参数的数量没有改变,而可训练参数的数量要低得多。

让我们来拟合模型:

Train on 38366 samples, validate on 9592 samples

Epoch 1/10

38366/38366 [==============================] - 143s

4ms/step - loss: 0.1401 - sparse_categorical_accuracy: 0.9676 - val_loss:

0.0514 - val_sparse_categorical_accuracy: 0.9853

Epoch 2/10

38366/38366 [==============================] - 143s

4ms/step - loss: 0.0488 - sparse_categorical_accuracy: 0.9859 - val_loss:

0.0429 - val_sparse_categorical_accuracy: 0.9875

Epoch 3/10

38366/38366 [==============================] - 138s

4ms/step - loss: 0.0417 - sparse_categorical_accuracy: 0.9876 - val_loss:

0.0401 - val_sparse_categorical_accuracy: 0.9881

Epoch 4/10

38366/38366 [==============================] - 132s

3ms/step - loss: 0.0381 - sparse_categorical_accuracy: 0.9885 - val_loss:

0.0391 - val_sparse_categorical_accuracy: 0.9887

Epoch 5/10

38366/38366 [==============================] - 146s

4ms/step - loss: 0.0355 - sparse_categorical_accuracy: 0.9891 - val_loss:

0.0367 - val_sparse_categorical_accuracy: 0.9891

Epoch 6/10

38366/38366 [==============================] - 143s

4ms/step - loss: 0.0333 - sparse_categorical_accuracy: 0.9896 - val_loss:

0.0373 - val_sparse_categorical_accuracy: 0.9891

Epoch 7/10

38366/38366 [==============================] - 145s

4ms/step - loss: 0.0318 - sparse_categorical_accuracy: 0.9900 - val_loss:

0.0355 - val_sparse_categorical_accuracy: 0.9894

Epoch 8/10

38366/38366 [==============================] - 142s

4ms/step - loss: 0.0303 - sparse_categorical_accuracy: 0.9904 - val_loss:

0.0352 - val_sparse_categorical_accuracy: 0.9895

Epoch 9/10

38366/38366 [==============================] - 138s

4ms/step - loss: 0.0289 - sparse_categorical_accuracy: 0.9907 - val_loss:

0.0362 - val_sparse_categorical_accuracy: 0.9894

Epoch 10/10

38366/38366 [==============================] - 137s

4ms/step - loss: 0.0278 - sparse_categorical_accuracy: 0.9910 - val_loss:

0.0358 - val_sparse_categorical_accuracy: 0.9895

准确性没有太大变化,但正如之前所见,准确性并不是合适的指标。让我们换一种方式对其进行评估,并与之前的模型进行比较:

lstm_predicted_tags = []

for s, s_pred in zip(test_sentences_words, lstm_predicted):

tags = np.argmax(s_pred, axis=1)

tags = map(index_tag_wo_padding.get,tags)[-len(s):]

lstm_predicted_tags.append(tags)

print 'LSTM + Pretrained Embbeddings'

print '='*15

print classification_report(test_sentences_tags, lstm_predicted_tags)

# ============== Output

==============================

LSTM + Pretrained Embbeddings

===============

precision recall f1-score support

art 0.45 0.06 0.11 82

gpe 0.97 0.95 0.96 3242

eve 0.56 0.33 0.41 46

per 0.72 0.71 0.72 3321

tim 0.87 0.84 0.85 4107

nat 0.00 0.00 0.00 48

org 0.62 0.56 0.59 4082

geo 0.83 0.88 0.86 7553

avg / 0.80 0.80 0.80 22481

total

效果特别好, F1得分从76增加到80!

结论

Sequence to Sequence模型非常强大,适用于许多任务,如命名实体识别(NER),词性(POS)标记,解析等。有很多技术和方式来训练它们,但最重要的是知道何时使用何种方式以及如何正确地模拟问题。

留言 点赞 发个朋友圈

我们一起分享AI学习与发展的干货

编译组:草田

相关链接:

https://towardsdatascience.com/solving-nlp-task-using-sequence2sequence-model-from-zero-to-hero-c193c1bd03d1

如需转载,请后台留言,遵守转载规范

打开APP阅读更多精彩内容