手把手教你用ELMo模型提取文本特征(附代码实现细节)

说明:本文是A Step-by-Step NLP Guide to Learn ELMo for Extracting Features from Text(Prateek Joshi — March 11, 2019)的译文以及代码实现细节说明。文中黑色文字为译文,紫色文字为本人代码实现的细节说明以及环境配置心得。

介绍

我致力于解决不同的自然语言处理 (NLP) 问题(成为数据科学家的好处!)。 每个 NLP 问题都以自己独特的方式面临着挑战。 这恰好反映了人类语言是多么复杂和美妙。

但一直困扰 NLP 实践者的一件事是(机器)无法理解句子的真正含义。 是的,我说的是上下文。 当被要求执行一项基本任务时,传统的 NLP 技术和框架非常有用。 当我们试图添加上下文时,事情很快就恶化了。

在过去 18 个月左右的时间里,NLP 格局发生了重大变化。 像 Google 的 BERT 和 Zalando 的 Flair 这样的 NLP 框架能够解析句子并掌握它们的写作上下文。

语言模型嵌入 (ELMo)

这方面最大的突破之一要归功于 ELMo,这是一种由 AllenNLP 开发的最先进的 NLP 框架。 当您完成这篇文章时,您也会像我一样成为 ELMo 的忠实粉丝。

在本文中,我们将探索 ELMo(来自语言模型的嵌入)并使用它在真实的数据集上利用 Python 构建一个令人兴奋的 NLP 模型。

注意:本文假设您熟悉不同类型的词嵌入和 LSTM 架构。 您可以参考以下文章以了解有关主题的更多信息:  

目录

1. 什么是ELMo?

2. 了解 ELMo 的工作原理

3. ELMo 与其他词嵌入有何不同?

4. 实现:用于 Python 文本分类的 ELMo

    4.1 理解问题陈述

    4.2 关于数据集

    4.3 导入库

    4.4 读取和检查数据

    4.5 文本清理和预处理

    4.6 TensorFlow Hub 简介

    4.7 准备ELMo向量

    4.8 模型构建和评估

5. 我们还能用 ELMo 做什么?

1. 什么是ELMo?

我们说的ELMo并不是《芝麻街》(Sesame Street)的角色! 这也是一个体现了上下文语境的重要性的典型例子。

ELMo 是一种在向量或嵌入中表示词汇的新方法。 这些词嵌入在下列几种NLP问题中能有助于生成最先进(SOAT)的结果:

全球的 NLP 科学家已经开始在学术或应用领域中将 ELMo 用于各种 NLP 任务。 建议您在https://arxiv.org/pdf/1802.05365.pdf查看ELMo 原始研究论文 。 我通常不要求人们阅读论文,因为它们往往又长又复杂,但这篇论文不同,它很好地解释了ELMo原理和设计过程。

2. 理解ELMo工作原理

在实践之前让我们需要先直观了解一下ELMo是如何运作的。为什么说这一步很重要?

试想如下场景:你已经成功地从GitHub上下载了ELMo的python代码并在自己的文本数据集上构建了模型,但只得到了一般的结果,所以你需要改进。如果你不理解ELMo的架构你将如何改进呢?如果没有研究过又怎么知道需要调整哪些参数呢?

这种思路适用于其他所有机器学习算法,你不需要了解它们的推导过程但必须对它们有足够的认识来玩转和改进你的模型。

现在,让我们回到ELMo的工作原理。

正如我之前提到的,ELMo的词向量是在双层双向语言模型(two-layer bidirectional language model , biLM)上计算的。这种模型由两层叠在一起,每层都有前向(forward pass)和后向(backward pass)两种迭代。

ELMo structure

  • 上图中的结构使用字符级卷积神经网络(convolutional neural network, CNN)来将文本中的词转换成原始词向量(raw word vector)
  • 将这些原始词向量输入双向语言模型中第一层
  • 前向迭代中包含了该词以及该词之前的一些词汇或语境的信息
  • 后向迭代中包含了该词之后的信息
  • 这两种迭代的信息组成了中间词向量(intermediate word vector)
  • 这些中间词向量被输入到模型的下一层
  • 最终表示(ELMo)就是原始词向量和两个中间词向量的加权和

 因为双向语言模型的输入度量是字符而不是词汇,该模型能捕捉词的内部结构信息。比如beautybeautiful,即使不了解这两个词的上下文,双向语言模型也能够识别出它们在一定程度上的相关性。

3. ELMo与其他词嵌入的区别是什么?

与word2vec或GLoVe等传统词嵌入不同,ELMo中每个词对应的向量实际上是一个包含该词的整个句子的函数。因此,同一个词在不同的上下文中会有不同的词向量。

你可能会问:这种区别会对我处理NLP问题有什么帮助吗?让我通过一个例子来解释清楚:

我们有以下两个句子:

  1. read the book yesterday.
  2. Can you read the letter now?

花些时间考虑下这两个句子的区别,第一个句子中的动词“read”是过去式,而第二个句子中的“read”却是现在式,这是一种一词多义现象。 

语言是多么精妙而复杂

传统的词嵌入会对两个句子中的词“read”生成同样的向量,所以这些架构无法区别多义词,它们无法识别词的上下文。

与之相反,ELMo的词向量能够很好地解决这种问题。ELMo模型将整个句子输入方程式中来计算词嵌入。因此,上例中两个句子的“read”会有不同的ELMo向量。

4. 实现:在python中应用ELMo模型进行文本分类

现在是你们最期待的部分——在python中实现ELMo!让我们逐步进行:

理解问题陈述

处理数据科学问题的第一步是明确问题陈述,这将是你接下来行动的基础。

对于这篇文章,我们已经有如下问题陈述:

情感分析一直是NLP领域中的一个关键问题。这次我们从Twitter上收集了消费者对于生产和销售手机、电脑等高科技产品的多个公司的推文,我们的任务是判断这些推文是否对这些公司或产品有负面评价 。

这显然是一个文本的二分类任务,要求我们从提取的推文预测情感。

数据集介绍

我们已经分割了数据集:

  • 训练集中有7920条推文
  • 测试集中有1953条推文

你可以从这里下载数据集。请注意,您必须注册或登录才能这样做。(注:此链接中无法下载到数据,从github上搜索train_2kmZucJ.csv以及test_oJQbWVk.csv,可以得到数据集)。

注意:推文中的大多数亵渎和粗俗的术语已被替换为“$&@*#”。 但是,请注意,数据集可能仍包含可能被视为亵渎、粗俗或冒犯的文本。

好的,让我们启动我们最喜欢的 Python IDE 并开始编码!

导入库

导入我们将要用到的库:

import pandas as pd
import numpy as np
import spacy
from tqdm import tqdm
import re
import time
import pickle
pd.set_option('display.max_colwidth', 200)

导入和检查数据

# read data
train = pd.read_csv("train_2kmZucJ.csv")
test = pd.read_csv("test_oJQbWVk.csv")

train.shape, test.shape

Output: ((7920, 3), (1953, 2))

训练集中有7920条推文,训练集中有1953条推文。接下来我们检查下训练集中的类别分布:

train['label'].value_counts(normalize = True)

Output:

0    0.744192
1    0.255808
Name: label, dtype: float64

这里1代表负面推文,0代表非负面的推文。

现在让我们看一下训练集的前五行:

train.head()

我们有三列数据,其中“tweet”列是独立变量,“label”列是目标变量。

文本清洗和预处理

理想状况下我们会有一个整洁且结构化的数据集,但目前NLP领域还很难做到。

我们需要花费一定时间来清洗数据,为模型构建做准备。从清洗后的文本中提取特征会变得简单,甚至特征中也会包含更多信息。你会发现你的数据质量越高,模型的表现也就会越好。

所以让我们先清理一下已有的数据集吧。

可以发现有些推文中有URL链接,它们对情感分析没有帮助,所以我们需要移除它们。

# remove URL's from train and test
train['clean_tweet'] = train['tweet'].apply(lambda x: re.sub(r'http\S+', '', x))
test['clean_tweet'] = test['tweet'].apply(lambda x: re.sub(r'http\S+', '', x))

我们使用正则表达式(Regular Expression)来移除URL。

:你可以从这里了解正则表达式。

我们现在来做一些常规的数据清理工作:

# remove punctuation marks
punctuation = '!"#$%&()*+-/:;<=>?@[\\]^_`{|}~'
 
train['clean_tweet'] = train['clean_tweet'].apply(lambda x: ''.join(ch for ch in x if ch not in set(punctuation)))
test['clean_tweet'] = test['clean_tweet'].apply(lambda x: ''.join(ch for ch in x if ch not in set(punctuation)))
 
# convert text to lowercase
train['clean_tweet'] = train['clean_tweet'].str.lower()
test['clean_tweet'] = test['clean_tweet'].str.lower()
 
# remove numbers
train['clean_tweet'] = train['clean_tweet'].str.replace("[0-9]", " ")
test['clean_tweet'] = test['clean_tweet'].str.replace("[0-9]", " ")
 
# remove whitespaces
train['clean_tweet'] = train['clean_tweet'].apply(lambda x:' '.join(x.split()))
test['clean_tweet'] = test['clean_tweet'].apply(lambda x: ' '.join(x.split()))

接下来我们将文本标准化,这一步会将词简化成它的基本形式,比如“produces”、“production”和“producing”会变成“product”。通常来讲,同一个词的多种形式并不重要,我们只需要它们的基本形式就可以了。

我们使用流行的spaCy库来进行标准化(注:本人电脑安装spacy的没有en模型,只有en_core_web_sm模型, 故需将以下spacy.load()语句中的en替换为en_core_web_sm):

# import spaCy's language model
nlp = spacy.load('en', disable=['parser', 'ner']) 

# function to lemmatize text
def lemmatization(texts):
    output = []
    for i in texts:
        s = [token.lemma_ for token in nlp(i)]
        output.append(' '.join(s))
    return output

在测试集和训练集中进行归类(Lemmatize):

train['clean_tweet'] = lemmatization(train['clean_tweet'])
test['clean_tweet'] = lemmatization(test['clean_tweet'])

现在让我们看一下原始推文和清洗后的推文的对比:

train.sample(10)

仔细查看上图中的两列推文的对比,清洗后的推文变得更加清晰易理解。

然而,在清洗文本这一步中其实还有很多可以做的,我鼓励大家进一步探索数据,去发现文本中可以提升的地方。

简要介绍TensorFlow Hub

等等,TensorFlow跟我们这篇教程有什么关系呢?

TensorFlow Hub是一个允许迁移学习(Transfer learning)的库,它支持用多种机器学习模型处理不同任务。ELMo是其中一例,这也是为什么我们的实现中需要通过TensorFlow Hub来使用ELMo。
 

TensorFlow Hub

 我们首先需要安装TensorFlow Hub,你必须安装或升级tensorflow到1.7版本以上来使用(注:没有说明具体是tensorflow的哪个版本以及是否是gpu版的,导致后面调试时出现一堆问题):

$ pip install "tensorflow>=1.7.0"
$ pip install tensorflow-hub

准备ELMo模型向量

我们现在需要引入事先训练过的ELMo模型。请注意这个模型的大小超过350 mb所以可能需要一些时间来下载。(注:以下代码无法下载模型,总是报超时错误。解决办法:在能够科学上网的前提下,在浏览器中直接输入网址,可以下载模型,模型名为elmo_2.tar.gz,大小为347.1MB  。将该文件复制到ubuntu系统的某个目录下(如/home/ai/tfhub_modules/elmo_2),将压缩包的文件解压到此目录,然后将下面的语句改为elmo = hub.Module("home/ai/tfhub_modules/elmo_2", trainable=True)即可

import tensorflow_hub as hub
import tensorflow as tf

elmo = hub.Module("https://tfhub.dev/google/elmo/2", trainable=True)

我会先展示如何给一个句子生成ELMo向量,你只需要将一列字符串输入elmo中:

# just a random sentence
x = ["Roasted ants are a popular snack in Columbia"]

# Extract ELMo features
embeddings = elmo(x, signature="default", as_dict=True)["elmo"]

embeddings.shape

Output: TensorShape([Dimension(1), Dimension(8), Dimension(1024)])

(在jupyter 中报错,为RuntimeError: variable_scope module_1/ was unused but the corresponding name_scope was already token. 上网查了一圈,似乎很难解决。后在pycharm中运行,没有出现错误。不知道为啥又区别?

这个输出是一个三维张量(1, 8, 1024):

  • 第一个维度表示训练样本的数量,在这个案例中是1;
  • 第二个维度表示输入列表中的最大长度,因为我们现在只输入了一个字符串,所以第二个维度就是该字符串的长度8;
  • 第三个维度等于ELMo向量的长度。

因此,输入语句中的每个词都有个长度为1024的ELMo向量。

让我们开始提取测试集和训练集中清洗过推文的ELMo向量。如果想得到整个的推文的ElMo向量,我们需要取推文中每个词的向量的平均值。

我们可以通过定义一个函数来实现:

def elmo_vectors(x):
  embeddings = elmo(x.tolist(), signature="default", as_dict=True)["elmo"]

  with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    sess.run(tf.tables_initializer())
    # return average of ELMo features
    return sess.run(tf.reduce_mean(embeddings,1))

如果使用上述代码来一次性处理所有推文,你可能会耗尽所有内存。我们可以通过将训练集和测试集分割成一系列容量为100条的样本来避免这个问题,然后将他们相继传递给elmo_vectors()函数。

我选择用列表储存这些样本:

list_train = [train[i:i+100] for i in range(0,train.shape[0],100)]
list_test = [test[i:i+100] for i in range(0,test.shape[0],100)]

现在让我们在这些样本上迭代并提取ELMo向量,这会花很长时间:

# Extract ELMo embeddings
elmo_train = [elmo_vectors(x['clean_tweet']) for x in list_train]
elmo_test = [elmo_vectors(x['clean_tweet']) for x in list_test]

注:运行elmo_vectors(x)函数时报错,似乎(记不清了)是INFO:tensorflow:Saver not created because there are no variables in the graph,也可能是tensorflow 1.14 Failed to get convolution algorithm. This is probably because cuDNN failed to initialize。推测是tensorflow似乎不匹配,本项目用的tensorflow 1.14 cpu版的。于是安装tensorflow-gpu 1.15.0,安装成功后,此错误没有了。但出现另一个报错信息,大意为本系统中使用的cudnn 7.4.2,而源模型使用的是cudnn 7.6.0。 从tensorflow官网(https://tensorflow.google.cn/install/source)查询tensorflow、cuda, cudnn的对应关系,得知tensorflow_gpu-1.15.0--7.4(cudnn)--10.0(duda)(符合本系统目前使用的,即tensorflow_gpu-1.15.0,cuda为10.0,cudnn为7.4.2),而tensorflow-2.1.0--7.6(cudnn)  --10.1(duda)
     (1)在终端输入nvidia-smi, 显示“NVIDIA-SMI 430.34    Driver Version: 430.34  CUDA Version: 10.1”,说明显卡驱动可以支持 cuda 10.1, 满足条件。 然后输入nvcc -V, 显示“Cuda compilation tools, release 10.0, V10.0.×××”字样,说明本系统目前安装的cuda是10.0。从这两个命令显示的信息看,需要安装cuda10.1,以及和匹配cuda10.1的cudnn7.6.×。
    (2)从nvidia官网下载
cuda_10.1.105_418.39_linux.runcudnn-10.1-linux-x64-v7.6.5.32.tgz(需要登录),安装成功。然后在~/.bashrc文件末尾添加:
        export PATH="/usr/local/cuda-10.1/bin:$PATH"
        export LD_LIBRARY_PATH="/usr/lcoal/cuda-10.1/lib64:$LD_LIBRARY_PATH"

   并在终端输入使其生效的命令:
        source ~/.bashrc
    (3)在pycharm中打开终端, 输入
       
 pip install tensorflow-gpu==2.1.0 -i https://pypi.douban.com/simple/
   安装tensorflow-gpu 2.1.0。
    (4)以上安装成功后,重新运行程序,原来的错误信息消失。但报
“GPU Could not load dynamic library ‘libcudart.so.10.1,找不到文件 LD_LIBRARY_PATH: /usr/local/cuda10.0/lib64”等4条错误信息,都是找不到文件LD_LIBRARY_PATH: /usr/local/cuda10.0/lib64。 进入/usr/lcoal/cuda-10.1/lib64里面查看,报错信息中提示的相关文件都在。怀疑是系统路径设置有问题, 要不然怎么会提示在/usr/local/cuda10.0/lib64查找呢? 百般尝试,开始怀疑bashrc中加入的两条路径有问题,上网找各个设置文章,看基本都是如此,于是使用export输出系统设置,可以看到LD_LIBRARY_PATH="/usr/local/cuda-10.1/lib64",也没发现问题。继续搜索文章,发现“详解tensorflow2.x版本无法调用gpu的一种解决方法(https://www.jb51.net/article/187228.htm)”一文中添加路径的文件不一样(在/etc/profile),于是打开本机的/etc/profile文件,发现最后两行霍然为:

export PATH=/usr/local/cuda-10.0/bin${PATH:+:${PATH}}
exportLD_LIBRARY_PATH=/usr/local/cuda-10.0/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

 果断将其中的cuda-10.0改为cuda-10.1。然后运行程序,Could not load dynamic library等报错信息没有,gpu能够正常使用。
    (5)程序继续运行后,报“
RuntimeError: Exporting/importing meta graphs is not supported when eager execution is enab when eager execution is enabled.”,上网搜索,发现是tensorflow 2.×与tensorflow1.×之间的差异造成的。解决方案:
      import tensorflow.compat.v1 as tf
      tf.disable_eager_execution()

   大功告成!)。

一旦我们得到了所有向量,我们可以将它们整合成一个数组:

elmo_train_new = np.concatenate(elmo_train, axis = 0)
elmo_test_new = np.concatenate(elmo_test, axis = 0)

我建议你将这些数组储存好,因为我们需要很长时间来得到它们的ELMo向量。我们可以将它们存为pickle文件:

# save elmo_train_new
pickle_out = open("elmo_train_03032019.pickle","wb")
pickle.dump(elmo_train_new, pickle_out)
pickle_out.close()

# save elmo_test_new
pickle_out = open("elmo_test_03032019.pickle","wb")
pickle.dump(elmo_test_new, pickle_out)
pickle_out.close()

然后用以下代码来将它们重新加载:

# load elmo_train_new
pickle_in = open("elmo_train_03032019.pickle", "rb")
elmo_train_new = pickle.load(pickle_in)

# load elmo_train_new
pickle_in = open("elmo_test_03032019.pickle", "rb")
elmo_test_new = pickle.load(pickle_in)

构建模型并评估

让我们用ELMo来构建NLP模型吧!

我们可以用训练集的ELMo向量来构建一个分类模型。然后,我们会用该模型在测试集上进行预测。但在做这些之前,我们需要将elmo_train_new分成训练集和验证集来检验我们的模型。

from sklearn.model_selection import train_test_split

xtrain, xvalid, ytrain, yvalid = train_test_split(elmo_train_new, 
                                                  train['label'],  
                                                  random_state=42, 
                                                  test_size=0.2)

由于我们的目标是设置基线分数,我们将用ELMo向量作为特征来构建一个简单的逻辑回归模型:

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score

lreg = LogisticRegression()
lreg.fit(xtrain, ytrain)

到了预测的时间了!首先,在验证集上:

preds_valid = lreg.predict(xvalid)

我们用F1矩阵来评估我们的模型,因为这是竞赛的官方评估指标:

f1_score(yvalid, preds_valid)

Output: 0.789976

验证集上的F1分数很不错,接下来我们在测试集上进行预测:

# make predictions on test set
preds_test = lreg.predict(elmo_test_new)

准备将要上传到竞赛页面的提交文件:

# prepare submission dataframe
sub = pd.DataFrame({'id':test['id'], 'label':preds_test})

# write predictions to a CSV file
sub.to_csv("sub_lreg.csv", index=False)

公开排行榜显示我们的预测结果得到了0.875672分,可以说这个结果非常的好,因为我们只进行了相对基础的预处理过程,而且用了一个很简单的模型。可以预见如果我们用了更先进的技术将会得到更好的分数,大家可以自行尝试并将结果告诉我!

5. 我们还能用ELMo做什么?

我们刚刚见证了在文本识别中ELMo是多么高效,如果能搭配一个更复杂的模型它一定会有更出色的表现。ELMo的应用并不局限于文本分类,只要你需要将文本数据向量化都可以用它。

以下是几种可以使用ELMo进行处理的NLP问题:

  • 机器翻译(Machine Translation)
  • 语言模型(Language Modeling)
  • 文本摘要(Text Summarization)
  • 命名实体识别(Named Entity Recognition)
  • 问答系统(Question-Answering Systems)

6. 结语

ELMo无疑是NLP的重大进步,并且将保持趋势。鉴于NLP研究的进展速度非常快,最近几个月还出现了其他新的最先进的词嵌入,如Google BERT和Falando's Flair。可以说令NLP从业者激动的时代到来了!

我强烈建议你在其他数据集上使用ELMo,并亲自体验性能提升的过程。如果你有任何问题或希望与我和社区分享你的经验,请在下面的评论板块中进行。如果你刚在NLP领域起步,你还应该查看以下NLP相关资源:

参考:

1. A Step-by-Step NLP Guide to Learn ELMo for Extracting Features from Text

2. NLP详细教程:手把手教你用ELMo模型提取文本特征(附代码&论文)

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值