例
将计算结果显示在 <output> 元素中:
<form oninput="x.value=parseInt(a.value)+parseInt(b.value)">0
<input type="range" id="a" value="50">100
+<input type="number" id="b" value="50">
=<output name="x" for="a b"></output>
</form>
浏览器支持
Firefox、Opera、Chrome 和 Safari 浏览器都支持 <output> 标签。
注意:Internet Explorer 浏览器不支持 <output> 标签。
标签定义及使用说明
<output> 标签作为计算结果输出显示(比如执行脚本的输出)。
在HTML 4.01 与 HTML5中的差异
<output> 标签是 HTML 5 中的新标签。
属性
New:HTML5 新属性。
属性 | 值 | 描述 |
---|---|---|
forNew | element_id | 描述计算中使用的元素与计算结果之间的关系。 |
formNew | form_id | 定义输入字段所属的一个或多个表单。 |
nameNew | name | 定义对象的唯一名称(表单提交时使用)。 |
全局属性
<output> 标签支持全局属性,查看完整属性表 HTML全局属性。
事件属性
<output> 标签支持所有 HTML事件属性。
如您还有不明白的可以在下面与我留言或是与我探讨QQ群308855039,我们一起飞!
读:本文的目标是介绍一些Python库,帮助你从类似于PDF和Word DOCX 这样的二进制文件中提取数据。我们也将了解和学习如何从网络信息源(web feeds)(如RSS)中获取数据,以及利用一个库帮助解析HTML文本并从文档中提取原始文本。
我们还将学习如何从不同来源提取原始文本,对其进行规范化,并基于它创建一个用户定义的语料库。
在本文中,你将学习7个不同的实例。我们将学习从PDF文件、Word文档和Web中获取数据。PDF和Word文档是二进制文件,通过Web,你将获得HTML格式的数据,因此,我们也会对数据执行规范化和原始文本转换任务。
作者:克里希纳·巴夫萨、纳雷什·库马尔、普拉塔普·丹蒂
如需转载请联系大数据(ID:hzdashuju)
作为一名NLP专家,你将要处理大量的文本内容。当你在处理文本时,你必须知道一些字符串操作。我们将从几个简短的范例入手,帮助你理解str类及其在Python中的相关操作。
1. 准备工作
这里,你仅仅需要Python解释器和一个文本编辑器。我们将使用join(连接)、split(分割)、addition(加法)和multiplication(乘法)运算符以及索引。
2. 如何实现
(1)创建一个新的Python文件,命名为StringOps1.py。
(2)定义以下两个对象:
namesList=['Tuffy','Ali','Nysha','Tim' ] sentence='My dog sleeps on sofa'
第一个对象nameList是一个包含若干名字的字符串列表,第二个对象sentence是一个包含一句话的字符串对象。
(3)首先,我们看看join函数的特点以及它的功能:
names=';'.join(namesList) print(type(names), ':', names)
join()函数可以被任意一个string对象调用,它的输入参数是一个str对象的列表。通过将调用字符串的内容作为连接分隔符,它将所有str对象连接成一个str对象,并返回连接后的对象。运行这两行代码后,你得到的输出如下:
<class 'str'> : Tuffy;Ali;Nysha;Tim
(4)接下来,我们来看split方法的功能:
wordList=sentence.split(' ') print((type(wordList)), ':', wordList)
当split函数调用一个字符串时,它会将其内容分割为多个str对象,创建一个包含这些字符串对象的列表,并返回该列表。该函数接受单个str对象作为参数,表示分隔符。运行代码,得到如下输出:
<class 'list'> : ['My', 'dog', 'sleeps', 'on', 'sofa']
(5)算术运算符+和*也可以用于字符串。添加以下代码并输出:
additionExample='ganehsa' + 'ganesha' + 'ganesha' multiplicationExample='ganesha' * 2 print('Text Additions :', additionExample) print('Text Multiplication :', multiplicationExample)
我们首先看一下输出结果,随后讨论其工作原理:
Text Additions: ganehsaganeshaganesha Text Multiplication: ganeshaganesha
+运算符被称为连接符,它将字符串连接为单个str对象,产生一个新的字符串。如前所述,我们也可以使用*运算符对字符串做乘法。此外,需要注意的是这些操作不会添加任何额外的内容,例如在字符串之间插入空格。
(6)接下来,我们来了解一下字符串中的字符索引。添加下列几行代码:
str='Python NLTK' print(str[1]) print(str[-3])
首先,我们声明一个新的 string 对象。然后可以直接访问字符串中的第二个字符(y)。这里还有个小技巧:Python允许你在访问任何列表对象时使用负索引,比如说-1意味着最后一个成员,-2是倒数第二个成员,依此类推。例如,在前面代码的str对象中,索引7和-4是相同的字符N:
Output: <class 'str'> : Tuffy;Ali;Nysha;Tim <class 'list'> : ['My', 'dog', 'sleeps', 'on', 'sofa'] Text Additions : ganehsaganeshaganesha Text Multiplication : ganeshaganesha y L
3. 工作原理
我们使用split()函数将一个字符串变成了一个字符串列表,并使用join()函数将一个字符串列表变成了一个字符串。接下来我们了解了有关字符串的一些算术运算符的用法。
需要注意的是,我们不能在字符串中使用“-”(负号)和“/”(除法)运算符。最后,我们了解了如何在任一字符串中访问单个字符,特别值得一提的是,我们可以在访问字符串时使用负索引。
本段实例非常简单和直观,主要是介绍Python允许的一些常见和不常见的字符串操作。接下来,我们将在以上操作基础上继续学习一些字符串操作。
接下来,我们将了解子字符串、字符串替换以及如何访问一个字符串的所有字符。
1. 如何实现
(1)创建一个新的Python文件,命名为StringOps2.py并定义以下string对象:
str='NLTK Dolly Python'
(2)访问str对象中以第四个字符作为结束的子串。
print('Substring ends at:',str[:4])
我们知道索引从零开始,因此将返回由第0个到第3个字符组成的子串。运行代码,输出如下:
Substring ends at: NLTK
(3)访问str对象中从某个点开始直到末尾的子串:
print('Substring starts from:',str[11:] )
以上代码指示解释器返回str对象中从索引11到结束的一个子串。运行代码,得到以下输出:
Substring starts from: Python
(4)从str对象中访问包含Dolly的子串。添加以下行:
print('Substring :',str[5:10])
以上代码返回从索引5到10的字符,不包括第10个字符。输出是:
Substring : Dolly
(5)我们在前一段中已经了解了负索引在字符串操作中的应用。现在我们试试看它在获取子串中的作用:
print('Substring fancy:', str[-12:-7]) Run and check the output, it will be – Substring fancy: Dolly
这里得到的输出与上一步完全相同!为了理解这个结果,我们做一些计算:-1表示最后一个字符,-2是倒数第二个字符,依此类推。你将会发现[5:10]和[-12:-7]在这个例子中得出的子串是相同的。
(6)了解in操作符在if语句中的用法:
if 'NLTK' in str: print('found NLTK')
运行以上代码,程序的输出如下所示:
found NLTK
如上所示,in操作符会检查左边的字符串是否属于右边字符串的子串。
(7)了解str对象中replace函数的使用:
replaced=str.replace('Dolly', 'Dorothy') print('Replaced String:', replaced)
replace函数只需要两个参数。第一个是需要被替换的子字符串,第二个是用来替换前面子字符串的新子字符串。replace函数返回一个新的string对象,并且它不会修改调用它的字符串,运行代码,有如下输出:
Replaced String: NLTK Dorothy Python
(8)最后,迭代上面得到的replaced对象并访问它的每一个字符:
print('Accessing each character:') for s in replaced: print(s)
以上操作每次在新的一行输出replaced对象的每个字符,最终输出如下:
Output: Substring ends at: NLTK Substring starts from: Python Substring : Dolly Substring fancy: Dolly found NLTK Replaced String: NLTK Dorothy Python Accessing each character: N L T K D o r o t h y P y t h o n
2. 工作原理
字符串对象只是一个字符列表。正如第一步所示,我们可以像访问一个列表那样用for语句来访问字符串中的每个字符。任何列表的方括号内的字符“:”表示我们想要的一个子列表。
方括号内,如果字符“:”之后是一个数字n,表示我们希望获得一个从列表索引0开始到索引n-1结束的子列表。同样地,一个数字m后跟着字符“:”,则表示我们想要一个从列表索引m开始到列表末尾的子列表。
这个实例是从Python中访问PDF文件。首先,你需要安装PyPDF2库。
1. 准备工作
假设你已经安装了pip。然后,在Python2或Python3版本上用pip安装PyPDF2库,你只需要在命令行中运行以下命令:
pip install pypdf2
如果你成功安装了PyPDF2库,就完成了准备工作。与此同时,你需要通过以下链接下载一些我们将在本段用到的测试文档:
https://www.dropbox.com/sh/bk18dizhsu1p534/AABEuJw4TArUbzJf4Aa8gp5Wa?dl=0
2. 如何实现
(1)创建一个新的Python文件,命名为pdf.py并添加以下代码:
from PyPDF2 import PdfFileReader
这行代码会导入PyPDF2库中的PdfFileReader类。
(2)在上面创建的文件中添加如下Python函数,它的功能是读取一个PDF文件并返回其全文:
def getTextPDF(pdfFileName, password='')
该函数需要两个参数,一个是你要读取的PDF文件路径,一个是这个PDF文件的密码(如果有的话)。可见,password 参数是可选的。
(3)现在我们来定义这个函数。在该函数下添加如下代码:
pdf_file=open(pdfFileName, 'rb') read_pdf=PdfFileReader(pdf_file)
第一行代码会以读取和反向查找模式打开文件。第一行本质是一个Python文件打开命令/函数,仅能打开非文本的二进制文件。第二行将打开的文件传递给PdfFileReader类,用于处理PDF文档。
(4)如果文件设置了密码保护,接下来是解密被密码保护的PDF文件:
if password !='': read_pdf.decrypt(password)
如果在函数调用时设置了密码,那么我们在解密这个文件时也同样需要密码。
(5)从PDF文件中读取文本:
text=[] for i in range(0,read_pdf.getNumPages()-1): text.append(read_pdf.getPage(i).extractText())
创建一个字符串列表,并将每一页的文本都添加到这个列表中。
(6)返回最终的输出结果:
return '\n'.join(text)
将列表中所有的字符串都连接起来,并且在每个字符串之间都加一个换行符,返回连接后的单一字符串。
(7)在pdf.py目录下创建另一个名为TestPDFs.py 的文件,添加以下导入语句:
import pdf
(8)现在我们打印输出两个文档中的文本,其中一个是受密码保护的,一个是未加密的:
pdfFile='sample-one-line.pdf' pdfFileEncrypted='sample-one-line.protected.pdf' print('PDF 1: \n',pdf.getTextPDF(pdfFile)) print('PDF 2: \n',pdf.getTextPDF(pdfFileEncrypted,'tuffy'))
输出:本实例的前六步只是创建了一个Python函数,并不向控制台输出内容,第七和第八步会输出以下内容:
This is a sample PDF document I am using to demonstrate in the tutorial. This is a sample PDF document password protected.
3. 工作原理
PyPDF2是用于提取PDF文件内容的一个纯Python库。该库有很多功能,可用于裁剪页面、叠加图像数字签名、创建新的PDF文件等。但是,对NLP工程师需要实现的文本分析任务来说,该库只用来读取内容。
在第二步中,以反向查找模式打开文件很重要,因为当加载文件内容时,PyPDF2模块试图从尾部开始读取文件内容。此外,如果PDF文件是受密码保护的,而你没有在访问文件前解密文件,Python解释器将抛出一个PdfReadError错误。
这里,我们将学习如何加载和读取Word/DOCX文档。用于读取Word/DOCX文件的相关库会更加全面,在这些库中我们还可以处理段落边界、文本样式以及对所谓的run对象的操作。我们将会了解以上提到的所有内容,因为这些内容在文本分析任务中是至关重要的。
Tip: 如果你没有安装Microsoft Word软件,你可以使用Liber Office和Open Office软件的开源版本来创建和编辑“.docx”文件。
1. 准备工作
假设你已经在你的机器上安装了pip,我们将使用pip来安装python-docx库。不要将它与另一个名为docx的库混淆,这是两个完全不同的库。我们将从python docx库中导入docx对象。在命令行中执行下面的命令将安装这个库:
pip install python-docx
成功安装了该库后,继续下一步,我们将在这个实例中使用一个测试文档,如果你已经通过本文第一段提供的链接下载了所有文档,你应该已具备相关文档。如果没有,请从以下链接下载sample-one-line.docx文档。
https://www.dropbox.com/sh/bk18dizhsu1p534/AABEuJw4TArUbzJf4Aa8gp5Wa?dl=0
现在,准备工作就全部完成了。
2. 如何实现
(1)创建一个新的Python文件,命名为word.py并添加以下导入代码:
import docx
这里只需导入python-docx模块的docx对象。
(2)定义getTextWord函数:
def getTextWord(wordFileName):
该函数需要一个字符串参数wordFileName,包含你要读取的Word文件的绝对路径。
(3)初始化doc 对象:
doc=docx.Document(wordFileName)
此时doc对象加载了你要读取的Word文件。
(4)接下来我们要从已经加载文档的doc对象中读取文本,添加以下代码来实现:
fullText=[] for para in doc.paragraphs: fullText.append(para.text)
首先初始化一个字符串列表fullText,然后采用for循环逐段从文档中读取文本,并把每段都放到fullText列表中去。
(5)然后,我们将所有的片段/段落连接为一个字符串对象,并将其作为函数的输出结果返回:
return '\n'.join(fullText)
通过以上操作,我们将fullText数组的所有元素用“\ n”分隔符连接起来,并返回连接后的对象。最后保存该Python文件并退出。
(6)创建另一个Python文件,命名为TestDocX.py,并添加以下导入声明:
import docx import word
这里只需导入docx库以及我们在前五步中实现的word.py文件。
(7)现在我们将要读取一个DOCX文件并使用我们在word.py中实现的API打印输出它的全部内容。添加以下两行代码:
docName='sample-one-line.docx' print('Document in full :\n',word.getTextWord(docName))
首先在第一行代码中初始化文档的路径,然后使用API打印输出文档的全部内容。当你运行这部分代码时,得到以下输出:
Document in full :
这是一个带有一些粗体文本、一些斜体文本和一些下划线文本的PDF示例文档。我们还嵌入了一个标题,如下所示:
This is my TITLE. This is my third paragraph.
(8)正如前面提到的,Word / DOCX文档是一个更加丰富的信息来源,除了提供文本内容外,还能提供很多信息。现在我们来看有关段落的信息。添加以下四行代码:
doc=docx.Document(docName) print('Number of paragraphs :',len(doc.paragraphs)) print('Paragraph 2:',doc.paragraphs[1].text) print('Paragraph 2 style:',doc.paragraphs[1].style)
以上代码的第二行打印出了给定文档中段落的数量。第三行打印出了文档中第二段的内容。而第四行将会打印出第二段的样式,比如在这个例子中的样式就是Title类型。当你运行以上代码后,输出将如下所示:
Number of paragraphs : 3 Paragraph 2: This is my TITLE. Paragraph 2 style: _ParagraphStyle('Title') id: 4374023248
(9)接下来,我们将了解什么是run对象。添加以下代码:
print('Paragraph 1:',doc.paragraphs[0].text) print('Number of runs in paragraph 1:',len(doc.paragraphs[0].runs)) for idx, run in enumerate(doc.paragraphs[0].runs): print('Run %s : %s' %(idx,run.text))
首先,我们获得文档第一段的全部内容。然后,我们获得第一段中run对象的数目。最后,我们把每个run对象打印输出。
(10)为了明确每个run对象的格式,添加以下代码:
print('is Run 0 underlined:',doc.paragraphs[0].runs[5].underline) print('is Run 2 bold:',doc.paragraphs[0].runs[1].bold) print('is Run 7 italic:',doc.paragraphs[0].runs[3].italic)
这段代码的各行分别在检查相应run对象的下划线样式、粗体样式以及斜体样式。最终输出如下:
Output: Document in full : This is a sample PDF document with some text in BOLD, some in ITALIC and some underlined. We are also embedding a Title down below. This is my TITLE. This is my third paragraph. Number of paragraphs : 3 Paragraph 2: This is my TITLE. Paragraph 2 style: _ParagraphStyle('Title') id: 4374023248 Paragraph 1: This is a sample PDF document with some text in BOLD, some in ITALIC and some underlined. We're also embedding a Title down below. Number of runs in paragraph 1: 8 Run 0 : This is a sample PDF document with Run 1 : some text in BOLD Run 2 : , Run 3 : some in ITALIC Run 4 : and Run 5 : some underlined. Run 6 : We are also embedding a Title down below Run 7 : . is Run 0 underlined: True is Run 2 bold: True is Run 7 italic: True
3. 工作原理
首先,我们在word.py文件中写了一个函数,它将读取给定的DOCX文件并返回一个包含文件全部内容的字符串对象。前面的输出内容大都是不需要解释的,我特别阐述了关于Paragraph和Run的输出内容。DOCX文件的结构可以用python-docx库的三个数据类型来表示,其中最高一级是Document对象。
每个文档都包含多个段落。文档中出现新的一行或一个回车,就表示开始一个新的段落。每个段落用多个Run对象表示段落内格式的变化,这里的格式包含有字体、尺寸、颜色和其他样式元素(如粗体、斜体、下划线等等)。这些元素每次发生变化时,都会创建一个新的Run对象。
现在我们要创建自己的语料库,而不是使用从互联网上得到的语料库。
1. 准备工作
在准备方面,我们将使用本文第一个实例中提到的Dropbox文件夹中的几个文件。如果你已经从那个文件夹中下载了全部的文件,那么你已经完成了准备工作。否则,请从
https://www.dropbox.com/sh/bk18dizhsu1p534/AABEuJw4TArUbzJf4Aa8gp5Wa?dl=0
下载如下文件:
如果你没有按照本文的顺序来完成实例,那么你需要先回头看看本文的前两个实例。我们将用到本文前两个实例中完成的两个模块 word.py和pdf.py。本段实例更多是关于本文前两个实例所做工作的应用以及语料库概念的应用。下面我们来看实际的代码。
2. 如何实现
(1)创建一个新的Python文件,命名为createCorpus.py并添加以下代码:
import os import word, pdf from nltk.corpus.reader.plaintext import PlaintextCorpusReader
我们导入os库用于与文件有关的操作,word库和pdf库是本文前两段完成的库,最后导入的PlaintextCorpusReader是为了完成语料库建立这一最终目标。
(2)编写一个简单的函数,用来打开并读取一个纯文本文件,并将其全部内容作为string对象返回。添加以下代码:
def getText(txtFileName): file=open(txtFileName, 'r') return file.read()
第一行代码定义了函数及其输入参数。第二行代码以只读方式打开文件(open函数的第二个参数r表示以只读方式打开)。第三行代码读取打开文件的内容并将其作为string对象返回。
(3)在磁盘或文件系统中创建一个新文件夹corpus。添加以下三行代码:
newCorpusDir='mycorpus/' if not os.path.isdir(newCorpusDir): os.mkdir(newCorpusDir)
第一行定义的string对象包含了新文件夹名,第二行检查该文件夹在磁盘或文件系统中是否存在,第三行则通过执行os.mkdir()函数在磁盘上创建一个给定名字的文件夹。以上代码执行后将在你的Python文件所在的工作目录下创建一个名为mycorpus的新文件夹。
(4)然后,逐个读取前面提到的三个文件。首先从纯文本文件开始,添加以下代码:
txt1=getText('sample_feed.txt')
调用之前完成的getText函数,它将读取Sample_feed.txt文件并将输出结果存入名为txt1的字符串对象中。
(5)现在,添加以下代码来读取PDF文件:
txt2=pdf.getTextPDF('sample-pdf.pdf')
这里使用了PDF.py模块的getTextPDF()函数,它将读取sample-pdf.pdf文件并将文件内容存入名为txt2的字符串对象中。
(6)最后,通过以下代码读取DOCX文件:
txt3=word.getTextWord('sample-one-line.docx')
这里使用了word.py模块的getTexWord()函数,它将读取sample-one-line.docx文件并将文件内容存入名为txt3的字符串对象中。
(7)接下来,将上面读到的三个字符串对象写到磁盘文件中。添加以下代码:
files=[txt1,txt2,txt3] for idx, f in enumerate(files): with open(newCorpusDir+str(idx)+'.txt', 'w') as fout: fout.write(f)
(8)在mycorpus目录下,也就是我们之前存放文件的目录下新建一个PlainTextCorpus对象:
newCorpus=PlaintextCorpusReader(newCorpusDir, '.*')
以上一行代码看似简单,但是它在内部做了很多的文本处理,如识别段落、句子、单词等等。该函数的两个参数分别是语料库目录的路径以及要处理的文件名模式(这里我们已经设置corpus reader可以处理该目录下所有的文件)。通过以上步骤,我们创建了一个用户自定义的语料库。
(9)接下来,我们来看PlainTextCorpusReader是否加载正常。添加以下代码来进行测试:
print(newCorpus.words()) print(newCorpus.sents(newCorpus.fileids()[1])) print(newCorpus.paras(newCorpus.fileids()[0]))
第一行代码将打印输出语料库包含的所有单词数组(部分)。第二行代码将打印输出文件1.txt中的句子。第三行代码将打印输出文件0.txt中的段落:
Output: ['Five', 'months', '.', 'That', "'", 's', 'how', ...] [['A', 'generic', 'NLP'], ['(', 'Natural', 'Language', 'Processing', ')', 'toolset'], ...] [[['Five', 'months', '.']], [['That', "'", 's', 'how', 'long', 'it', "'", 's', 'been', 'since', 'Mass', 'Effect', ':', 'Andromeda', 'launched', ',', 'and', 'that', "'", 's', 'how', 'long', 'it', 'took', 'BioWare', 'Montreal', 'to', 'admit', 'that', 'nothing', 'more', 'can', 'be', 'done', 'with', 'the', 'ailing', 'game', "'", 's', 'story', 'mode', '.'], ['Technically', ',', 'it', 'wasn', "'", 't', 'even', 'a', 'full', 'five', 'months', ',', 'as', 'Andromeda', 'launched', 'on', 'March', '21', '.']], ...]
3. 工作原理
该实例最后一步的输出很简单直接,展示了各个对象不同的特征。输出内容的第一行是新语料库的单词列表,它与句子、段落、文件等更高级的结构没有关系。
第二行是1.txt文件中所有句子组成的列表,其中每个句子都是由该句子中单词组成的列表。
第三行是0.txt文件中所有段落组成的列表,其中每个段落对象又是由该段落中的句子组成的列表。从中可以发现,这些段落和句子保留了很多原有的结构。
丰富网站摘要(Rich Site Summary,RSS)信息源(feed)是一种计算机可读格式,用于传送互联网上定期更新的内容。大多数提供通知信息的网站以这种格式提供更新,例如新闻文章、在线出版物等。订阅者可以通过规范化格式定期访问其更新信息。
1. 准备工作
本段实例的目标是读取一个RSS信息源并访问其中的一条内容。为此,我们将使用全球之声(Mashable)提供的RSS信息源。全球之声是一个数字媒体网站。简而言之,它是一个科技和社交媒体的博客列表。该网站的RSS信息源网址(URL)是:
http://feeds.mashable.com/Mashable
另外,我们需要用feedparser库来读取RSS信息源。打开终端并运行以下命令即可在你的计算机上安装这个库:
pip install feedparser
安装好feedparser库后,我们就可以开始实现第一个读取RSS信息源的Python程序。
2. 如何实现
(1)创建一个新的Python文件,命名为rssReader.py,并添加以下代码:
import feedparser
(2)将全球之声信息源(Mashable feed)载入内存中,添加以下代码:
myFeed=feedparser.parse("http://feeds.mashable.com/Mashable")
myFeed对象包含全球之声信息源的第一页,通过feedparser自动下载和解析该信息源并填充到合适的位置。myFeed对象的条目列表将包含每个帖子(post)。
(3)检查当前信息源的标题并计算帖子数目:
print('Feed Title :', myFeed['feed']['title']) print('Number of posts :', len(myFeed.entries))
在第一行代码中,我们通过myFeed对象获取到了信息源的标题。在第二行代码中,我们计算了myFeed对象中entries对象的长度。如前所述,entries对象是一个包含解析后信息源中所有帖子的列表。运行代码,输出如下所示:
Feed Title: Mashable Number of posts : 30
标题是Mashable,当前,Mashable每次最多存放30个帖子到信息源。
(4)从entries列表中获取第一个post,并打印输出其标题:
post=myFeed.entries[0] print('Post Title :',post.title)
在第一行代码中,我们获取了entries列表中的第一个元素并将其加载到post对象中。在第二行代码中,我们打印输出了post对象的标题。运行代码,输出应该与以下内容相似:
Post Title: The moon literally blocked the sun on Twitter
这里提到输出内容应该与其相似而不是完全一样,是因为信息源在不断自我更新。
(5)访问post的原始HTML内容,并将其打印输出:
content=post.content[0].value print('Raw content :\n',content)
首先,我们访问post的内容对象并获取其具体值,打印输出如下:
Output: Feed Title: Mashable Number of posts : 30 Post Title: The moon literally blocked the sun on Twitter Raw content : <img alt="" src="http://www.hmttv.cn/uploadfile/2024/1012/20241012090441364.jpg" /><div style="float: right; width: 50px;"><a href="http://twitter.com/share?via=Mashable&text=The+moon+literally +blocked+the+sun+on+Twitter&url=http%3A%2F%2Fmashable.com%2F2017%2F 08%2F21%2Fmoon-blocks-sun-eclipse-2017- twitter%2F%3Futm_campaign%3DMash-Prod-RSS-Feedburner-All- Partial%26utm_cid%3DMash-Prod-RSS-Feedburner-All-Partial" style="margin: 10px;"> <p>The national space agency threw shade the best way it knows how: by blocking the sun. Yep, you read that right. </p> <div><div><blockquote> <p>HA HA HA I've blocked the Sun! Make way for the Moon<a href="https://twitter.com/hashtag/SolarEclipse2017?src=hash">#Solar Eclipse2017</a> <a href="https://t.co/nZCoqBlSTe">pic.twitter.com/nZCoqBlSTe</a></p> <p>— NASA Moon (@NASAMoon) <a href="https://twitter.com/NASAMoon/status/899681358737539073">Augus t 21, 2017</a></p> </blockquote></div></div>
3. 工作原理
互联网上大多数的RSS信息源都以时间顺序排列,将最新的帖子放到最上面。因此,在该实例中我们每次访问的都是信息源提供的最新内容。信息源本身是不断更新的。所以,每次运行程序时,输出的格式保持不变,但是输出的内容却可能发生改变,这取决于信息源更新的速度。
另外,我们在控制台直接输出原始的HTML文本而不是其文本内容。接下来,我们将解析HTML并从页面获取我们需要的信息。最后,本实例可以附加以下内容:读取你想要的任何信息源,将信息源中所有帖子的信息存储到磁盘,并利用它创建一个纯文本的语料库。当然,你可以从上一个和下一个实例中获得启发。
大多数情况下,你需要处理的网上数据都以HTML页面的形式存在。因此,我们认为有必要向你介绍Python的HTML解析方法。有很多Python模块可以用来解析HTML,在接下来的实例中,我们将使用BeautifulSoup4库来解析HTML。
1. 准备工作
BeautifulSoup4包适用于Python2和Python3。在使用这个包之前,我们需要提前下载并将它安装在解释器上。和之前一样,我们将使用pip来安装这个包。在命令行运行以下命令:
pip install beautifulsoup4
另外,你还需要本文Dropbox文件夹中的sample-html.html文件。如果你还没有下载该文件,请从以下链接下载:
https://www.dropbox.com/sh/bk18dizhsu1p534/AABEuJw4TArUbzJf4Aa8gp5Wa?dl=0
2. 如何实现
(1)完成所有准备工作后,从导入以下声明开始:
from bs4 import BeautifulSoup
从bs4模块中导入BeautifulSoup类,它将用于解析HTML。
(2)将一个HTML文件加载到BeautifulSoup对象中:
html_doc=open('sample-html.html', 'r').read() soup=BeautifulSoup(html_doc, 'html.parser')
在第一行代码中,我们将sample-html.html文件的内容加载到str对象html_doc中。然后,创建了一个BeautifulSoup对象,需要解析的HTML文件作为第一个参数,html.parser作为第二个参数。通过以上操作,BeautifulSoup对象使用html解析器来解析文档。它将文档内容加载到soup对象中进行解析以备使用。
(3)soup对象最主要、最简单且最有用的功能就是去除所有的HTML标签并获取文本内容。添加以下代码:
print('\n\nFull text HTML Stripped:') print(soup.get_text())
在soup对象上调用的get_text()方法将返回HTML标签去除后的文件内容。运行以上代码,将得到以下输出:
Full text HTML Stripped: Sample Web Page Main heading This is a very simple HTML document Improve your image by including an image. Add a link to your favorite Web site. This is a new sentence without a paragraph break, in bold italics. This is purely the contents of our sample HTML document without any of the HTML tags.
(4)有时不仅需要去除HTML标签,可能还需要获取特定标签的内容。访问其中的一个标签:
print('Accessing the <title> tag :', end=' ') print(soup.title)
soup.title将返回文件中的第一个标题(title)标签。以上代码的输出如下所示:
Accessing the <title> tag : <title>Sample Web Page</title>
(5)现在,我们需要某个HTML标签的文本内容。通过以下代码获取<h1>标签的内容:
print('Accessing the text of <H1> tag :', end=' ') print(soup.h1.string)
soup.h1.string命令将返回以<h1>标签开头的文本。以上代码的输出如下所示:
Accessing the text of <H1> tag : Main heading
(6)访问标签的属性。这里,我们将访问img标签的alt属性。添加以下代码行:
print('Accessing property of <img> tag :', end=' ') print(soup.img['alt'])
通过仔细观察,你会发现访问标签属性的语法和访问标签文本的语法是不同的。运行以上代码,得到以下输出:
Accessing property of <img> tag : A Great HTML Resource
(7)最后,一个HTML文件中同一类型的标签可能多次出现。使用“.”语法仅能获取文件中第一次出现的标签。为了获取所有的标签,我们将使用find_all()函数,如下所示:
print('\nAccessing all occurences of the <p> tag :') for p in soup.find_all('p'): print(p.string)
在BeautifulSoup对象上调用find_all()函数,参数是标签名,它将搜索整个HTML树并返回符合条件的标签列表。我们使用for循环来遍历该列表,并将BeautifulSoup对象中所有<p>标签的内容/文本打印并输出:
Output: Full text HTML Stripped: Sample Web Page Main heading This is a very simple HTML document Improve your image by including an image. Add a link to your favorite Web site. This is a new sentence without a paragraph break, in bold italics. Accessing the <title> tag : <title>Sample Web Page</title> Accessing the text of <H1> tag : Main heading Accessing property of <img> tag : A Great HTML Resource Accessing all occurences of the <p> tag : This is a very simple HTML document Improve your image by including an image. None
3. 工作原理
BeautifulSoup4是一个很方便的库,可以用于解析任何HTML和XML内容。它支持Python内置的HTML解析器,但是你也可以使用其他第三方的解析器,例如,lxml解析器和纯Python的html5lib解析器。
这里,我们使用Python内置的HTML解析器。如果你了解了HTML并会编写简单的HTML代码的话,输出结果是非常容易理解的。
关于作者:克里希纳·巴夫萨(KrishnaBhavsar)花了大约10年时间在各行业领域如酒店业、银行业、医疗行业等进行自然语言处理、社交媒体分析和文本挖掘方面的研究。他致力于用不同的NLP语料库如StanfordCoreNLP、IBM的 SystemText和BigInsights、GATE和NLTK来解决与文本分析有关的行业问题。纳雷什·库马尔(NareshKumar)曾为财富500强企业设计、实施和运行超大型因特网应用程序,在这方面他拥有超过十年的专业经验。他是一位全栈架构师,在电子商务、网络托管、医疗、大数据及分析、数据流、广告和数据库等领域拥有丰富的实践经验。
本文摘编自《自然语言处理Python进阶》,经出版方授权发布。
延伸阅读《自然语言处理Python进阶》
推荐语:本书包含的实例可以让你学会使用NLTK(处理NLP任务的主要Python平台)完成自然语言处理的各种任务,涵盖了自然语言理解、自然语言处理和句法分析等。
自Ahmed BESBES
作者:Ahmed Besbes
机器之心编译
参与:李诗萌、路
本文介绍了用于文本分类任务的 7 个模型,包括传统的词袋模型、循环神经网络,也有常用于计算机视觉任务的卷积神经网络,以及 RNN + CNN。
本文是我之前写过的一篇基于推特数据进行情感分析的文章(https://ahmedbesbes.com/sentiment-analysis-on-twitter-using-word2vec-and-keras.html)的延伸内容。那时我建立了一个简单的模型:基于 keras 训练的两层前馈神经网络。用组成推文的词嵌入的加权平均值作为文档向量来表示输入推文。
我用的嵌入是用 gensim 基于语料库从头训练出来的 word2vec 模型。该是一个二分类任务,准确率能达到 79%。
本文目标在于探索其他在相同数据集上训练出来的 NLP 模型,然后在给定的测试集上对这些模型的性能进行评估。
我们将通过不同的模型(从依赖于词袋表征的简单模型到部署了卷积/循环网络的复杂模型)了解能否得到高于 79% 的准确率!
首先,将从简单的模型开始,逐步增加模型的复杂度。这项工作是为了说明简单的模型也能很有效。
我会进行这些尝试:
文末附有这些 NLP 技术的样板代码。这些代码可以帮助你开启自己的 NLP 项目并获得最优结果(这些模型中有一些非常强大)。
我们还可以提供一个综合基准,我们可以利用该基准分辨哪个模型最适合预测推文中的情绪。
在相关的 GitHub 库中还有不同的模型、这些模型的预测结果以及测试集。你可以自己尝试并得到可信的结果。
import os import re import warnings warnings.simplefilter("ignore", UserWarning) from matplotlib import pyplot as plt %matplotlib inline import pandas as pd pd.options.mode.chained_assignment=None import numpy as np from string import punctuation from nltk.tokenize import word_tokenize from sklearn.model_selection import train_test_split from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, auc, roc_auc_score from sklearn.externals import joblib import scipy from scipy.sparse import hstack
0. 数据预处理
你可以从该链接(http://thinknook.com/twitter-sentiment-analysis-training-corpus-dataset-2012-09-22/)下载数据集。
加载数据并提取所需变量(情感及情感文本)。
该数据集包含 1,578,614 个分好类的推文,每一行都用 1(积极情绪)和 0(消极情绪)进行了标记。
作者建议用 1/10 的数据进行测试,其余数据用于训练。
data=pd.read_csv('./data/tweets.csv', encoding='latin1', usecols=['Sentiment', 'SentimentText'])
data.columns=['sentiment', 'text']
data=data.sample(frac=1, random_state=42)
print(data.shape)
(1578614, 2)
for row in data.head(10).iterrows():
print(row[1]['sentiment'], row[1]['text'])
1 http://www.popsugar.com/2999655 keep voting for robert pattinson in the popsugar100 as well!!
1 @GamrothTaylor I am starting to worry about you, only I have Navy Seal type sleep hours.
0 sunburned...no sunbaked! ow. it hurts to sit.
1 Celebrating my 50th birthday by doing exactly the same as I do every other day - working on our websites. It's just another day.
1 Leah and Aiden Gosselin are the cutest kids on the face of the Earth
1 @MissHell23 Oh. I didn't even notice.
0 WTF is wrong with me?!!! I'm completely miserable. I need to snap out of this
0 Was having the best time in the gym until I got to the car and had messages waiting for me... back to the down stage!
1 @JENTSYY oh what happened??
0 @catawu Ghod forbid he should feel responsible for anything!
推文数据中存在很多噪声,我们删除了推文中的网址、主题标签和用户提及来清理数据。
def tokenize(tweet): tweet=re.sub(r'http\S+', '', tweet) tweet=re.sub(r"#(\w+)", '', tweet) tweet=re.sub(r"@(\w+)", '', tweet) tweet=re.sub(r'[^\w\s]', '', tweet) tweet=tweet.strip().lower() tokens=word_tokenize(tweet) return tokens
将清理好的数据保存在硬盘上。
data['tokens']=data.text.progress_map(tokenize) data['cleaned_text']=data['tokens'].map(lambda tokens: ' '.join(tokens)) data[['sentiment', 'cleaned_text']].to_csv('./data/cleaned_text.csv') data=pd.read_csv('./data/cleaned_text.csv') print(data.shape) (1575026, 2) data.head()
既然数据集已经清理干净了,就可以准备分割训练集和测试集来建立模型了。
本文数据都是用这种方式分割的。
x_train, x_test, y_train, y_test=train_test_split(data['cleaned_text'], data['sentiment'], test_size=0.1, random_state=42, stratify=data['sentiment']) print(x_train.shape, x_test.shape, y_train.shape, y_test.shape) (1417523,) (157503,) (1417523,) (157503,)
将测试集标签存储在硬盘上以便后续使用。
pd.DataFrame(y_test).to_csv('./predictions/y_true.csv', index=False, encoding='utf-8')
接下来就可以应用机器学习方法了。
1. 基于词级 ngram 的词袋模型
那么,什么是 n-gram 呢?
如图所示,ngram 是将可在源文本中找到的长度为 n 的相邻词的所有组合。
我们的模型将以 unigrams(n=1)和 bigrams(n=2)为特征。
用矩阵表示数据集,矩阵的每一行表示一条推文,每一列表示从推文(已经经过分词和清理)中提取的特征(一元模型或二元模型)。每个单元格是 tf-idf 分数(也可以用更简单的值,但 tf-idf 比较通用且效果较好)。我们将该矩阵称为文档-词项矩阵。
略经思考可知,拥有 150 万推文的语料库的一元模型和二元模型去重后的数量还是很大的。事实上,出于计算力的考虑,我们可将这个数设置为固定值。你可以通过交叉验证来确定这个值。
在向量化之后,语料库如下图所示:
I like pizza a lot
假设使用上述特征让模型对这句话进行预测。
由于我们使用的是一元模型和二元模型后,因此模型提取出了下列特征:
i, like, pizza, a, lot, i like, like pizza, pizza a, a lot
因此,句子变成了大小为 N(分词总数)的向量,这个向量中包含 0 和这些 ngram 的 tf-idf 分数。所以接下来其实是要处理这个大而稀疏的向量。
一般而言,线性模型可以很好地处理大而稀疏的数据。此外,与其他模型相比,线性模型的训练速度也更快。
从过去的经验可知,logistic 回归可以在稀疏的 tf-idf 矩阵上良好地运作。
vectorizer_word=TfidfVectorizer(max_features=40000, min_df=5, max_df=0.5, analyzer='word', stop_words='english', ngram_range=(1, 2)) vectorizer_word.fit(x_train, leave=False) tfidf_matrix_word_train=vectorizer_word.transform(x_train) tfidf_matrix_word_test=vectorizer_word.transform(x_test)
在为训练集和测试集生成了 tf-idf 矩阵后,就可以建立第一个模型并对其进行测试。
tf-idf 矩阵是 logistic 回归的特征。
lr_word=LogisticRegression(solver='sag', verbose=2) lr_word.fit(tfidf_matrix_word_train, y_train)
一旦训练好模型后,就可以将其应用于测试数据以获得预测值。然后将这些值和模型一并存储在硬盘上。
joblib.dump(lr_word, './models/lr_word_ngram.pkl') y_pred_word=lr_word.predict(tfidf_matrix_word_test) pd.DataFrame(y_pred_word, columns=['y_pred']).to_csv('./predictions/lr_word_ngram.csv', index=False)
得到准确率:
y_pred_word=pd.read_csv('./predictions/lr_word_ngram.csv') print(accuracy_score(y_test, y_pred_word)) 0.782042246814
第一个模型得到了 78.2% 的准确率!真不赖。接下来了解一下第二个模型。
2. 基于字符级 ngram 的词袋模型
我们从未说过 ngram 仅为词服务,也可将其应用于字符上。
如你所见,我们将对字符级 ngram 使用与图中一样的代码,现在直接来看 4-grams 建模。
基本上这意味着,像「I like this movie」这样的句子会有下列特征:
I, l, i, k, e, ..., I li, lik, like, ..., this, ... , is m, s mo, movi, ...
字符级 ngram 很有效,在语言建模任务中,甚至可以比分词表现得更好。像垃圾邮件过滤或自然语言识别这样的任务就高度依赖字符级 ngram。
与之前学习单词组合的模型不同,该模型学习的是字母组合,这样就可以处理单词的形态构成。
基于字符的表征的一个优势是可以更好地解决单词拼写错误的问题。
我们来运行同样的流程:
vectorizer_char=TfidfVectorizer(max_features=40000, min_df=5, max_df=0.5, analyzer='char', ngram_range=(1, 4)) vectorizer_char.fit(tqdm_notebook(x_train, leave=False)); tfidf_matrix_char_train=vectorizer_char.transform(x_train) tfidf_matrix_char_test=vectorizer_char.transform(x_test) lr_char=LogisticRegression(solver='sag', verbose=2) lr_char.fit(tfidf_matrix_char_train, y_train) y_pred_char=lr_char.predict(tfidf_matrix_char_test) joblib.dump(lr_char, './models/lr_char_ngram.pkl') pd.DataFrame(y_pred_char, columns=['y_pred']).to_csv('./predictions/lr_char_ngram.csv', index=False) y_pred_char=pd.read_csv('./predictions/lr_char_ngram.csv') print(accuracy_score(y_test, y_pred_char)) 0.80420055491
80.4% 的准确率!字符级 ngram 模型的性能要比词级的 ngram 更好。
3. 基于词级 ngram 和字符级 ngram 的词袋模型
与词级 ngram 的特征相比,字符级 ngram 特征似乎提供了更好的准确率。那么将字符级 ngram 和词级 ngram 结合效果又怎么样呢?
我们将两个 tf-idf 矩阵连接在一起,建立一个新的、混合 tf-idf 矩阵。该模型有助于学习单词形态结构以及与这个单词大概率相邻单词的形态结构。
将这些属性结合在一起。
tfidf_matrix_word_char_train=hstack((tfidf_matrix_word_train, tfidf_matrix_char_train)) tfidf_matrix_word_char_test=hstack((tfidf_matrix_word_test, tfidf_matrix_char_test)) lr_word_char=LogisticRegression(solver='sag', verbose=2) lr_word_char.fit(tfidf_matrix_word_char_train, y_train) y_pred_word_char=lr_word_char.predict(tfidf_matrix_word_char_test) joblib.dump(lr_word_char, './models/lr_word_char_ngram.pkl') pd.DataFrame(y_pred_word_char, columns=['y_pred']).to_csv('./predictions/lr_word_char_ngram.csv', index=False) y_pred_word_char=pd.read_csv('./predictions/lr_word_char_ngram.csv') print(accuracy_score(y_test, y_pred_word_char)) 0.81423845895
得到了 81.4% 的准确率。该模型只加了一个整体单元,但结果比之前的两个都要好。
关于词袋模型
现在要用到深度学习模型了。深度学习模型的表现优于词袋模型是因为深度学习模型能够捕捉到句子中单词间的顺序依赖关系。这可能要归功于循环神经网络这一特殊神经网络结构的出现了。
本文并未涵盖 RNN 的理论基础,但该链接(http://colah.github.io/posts/2015-08-Understanding-LSTMs/)中的内容值得一读。这篇文章来源于 Cristopher Olah 的博客,详细叙述了一种特殊的 RNN 模型:长短期记忆网络(LSTM)。
在开始之前,要先设置一个深度学习专用的环境,以便在 TensorFlow 上使用 Keras。诚实地讲,我试着在个人笔记本上运行这些代码,但考虑到数据集的大小和 RNN 架构的复杂程度,这是很不实际的。还有一个很好的选择是 AWS。我一般在 EC2 p2.xlarge 实例上用深度学习 AMI(https://aws.amazon.com/marketplace/pp/B077GCH38C?qid=1527197041958&sr=0-1&ref_=srh_res_product_title)。亚马逊 AMI 是安装了所有包(TensorFlow、PyTorch 和 Keras 等)的预先配置过的 VM 图。强烈推荐大家使用!
from keras.preprocessing.text import Tokenizer from keras.preprocessing.text import text_to_word_sequence from keras.preprocessing.sequence import pad_sequences from keras.models import Model from keras.models import Sequential from keras.layers import Input, Dense, Embedding, Conv1D, Conv2D, MaxPooling1D, MaxPool2D from keras.layers import Reshape, Flatten, Dropout, Concatenate from keras.layers import SpatialDropout1D, concatenate from keras.layers import GRU, Bidirectional, GlobalAveragePooling1D, GlobalMaxPooling1D from keras.callbacks import Callback from keras.optimizers import Adam from keras.callbacks import ModelCheckpoint, EarlyStopping from keras.models import load_model from keras.utils.vis_utils import plot_model
4. 没有预训练词嵌入的循环神经网络
RNN 可能看起来很可怕。尽管它们因为复杂而难以理解,但非常有趣。RNN 模型封装了一个非常漂亮的设计,以克服传统神经网络在处理序列数据(文本、时间序列、视频、DNA 序列等)时的短板。
RNN 是一系列神经网络的模块,它们彼此连接像锁链一样。每一个都将消息向后传递。强烈推荐大家从 Colah 的博客中深入了解它的内部机制,下面的图就来源于此。
我们要处理的序列类型是文本数据。对意义而言,单词顺序很重要。RNN 考虑到了这一点,它可以捕捉长期依赖关系。
为了在文本数据上使用 Keras,我们首先要对数据进行预处理。可以用 Keras 的 Tokenizer 类。该对象用 num_words 作为参数,num_words 是根据词频进行分词后保留下来的最大词数。
MAX_NB_WORDS=80000 tokenizer=Tokenizer(num_words=MAX_NB_WORDS) tokenizer.fit_on_texts(data['cleaned_text'])
当分词器适用于数据时,我们就可以用分词器将文本字符级 ngram 转换为数字序列。
这些数字表示每个单词在字典中的位置(将其视为映射)。
如下例所示:
x_train[15] 'breakfast time happy time'
这里说明了分词器是如何将其转换为数字序列的。
tokenizer.texts_to_sequences([x_train[15]]) [[530, 50, 119, 50]]
接下来在训练序列和测试序列中应用该分词器:
train_sequences=tokenizer.texts_to_sequences(x_train) test_sequences=tokenizer.texts_to_sequences(x_test)
将推文映射到整数列表中。但是由于长度不同,还是没法将它们在矩阵中堆叠在一起。还好 Keras 允许用 0 将序列填充至最大长度。我们将这个长度设置为 35(这是推文中的最大分词数)。
MAX_LENGTH=35 padded_train_sequences=pad_sequences(train_sequences, maxlen=MAX_LENGTH) padded_test_sequences=pad_sequences(test_sequences, maxlen=MAX_LENGTH) padded_train_sequences array([[ 0, 0, 0, ..., 2383, 284, 9], [ 0, 0, 0, ..., 13, 30, 76], [ 0, 0, 0, ..., 19, 37, 45231], ..., [ 0, 0, 0, ..., 43, 502, 1653], [ 0, 0, 0, ..., 5, 1045, 890], [ 0, 0, 0, ..., 13748, 38750, 154]]) padded_train_sequences.shape (1417523, 35)
现在就可以将数据传入 RNN 了。
以下是我将使用的架构的一些元素:
双向 GRU 的输出是有维度的(批尺寸、时间步和单元)。这意味着如果用的是经典的 256 的批尺寸,维度将会是 (256, 35, 200)。
def get_simple_rnn_model(): embedding_dim=300 embedding_matrix=np.random.random((MAX_NB_WORDS, embedding_dim)) inp=Input(shape=(MAX_LENGTH, )) x=Embedding(input_dim=MAX_NB_WORDS, output_dim=embedding_dim, input_length=MAX_LENGTH, weights=[embedding_matrix], trainable=True)(inp) x=SpatialDropout1D(0.3)(x) x=Bidirectional(GRU(100, return_sequences=True))(x) avg_pool=GlobalAveragePooling1D()(x) max_pool=GlobalMaxPooling1D()(x) conc=concatenate([avg_pool, max_pool]) outp=Dense(1, activation="sigmoid")(conc) model=Model(inputs=inp, outputs=outp) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) return model rnn_simple_model=get_simple_rnn_model()
该模型的不同层如下所示:
plot_model(rnn_simple_model, to_file='./images/article_5/rnn_simple_model.png', show_shapes=True, show_layer_names=True)
在训练期间使用了模型检查点。这样可以在每个 epoch 的最后将最佳模型(可以用准确率度量)自动存储(在硬盘上)。
filepath="./models/rnn_no_embeddings/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5" checkpoint=ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max') batch_size=256 epochs=2 history=rnn_simple_model.fit(x=padded_train_sequences, y=y_train, validation_data=(padded_test_sequences, y_test), batch_size=batch_size, callbacks=[checkpoint], epochs=epochs, verbose=1) best_rnn_simple_model=load_model('./models/rnn_no_embeddings/weights-improvement-01-0.8262.hdf5') y_pred_rnn_simple=best_rnn_simple_model.predict(padded_test_sequences, verbose=1, batch_size=2048) y_pred_rnn_simple=pd.DataFrame(y_pred_rnn_simple, columns=['prediction']) y_pred_rnn_simple['prediction']=y_pred_rnn_simple['prediction'].map(lambda p: 1 if p >=0.5 else 0) y_pred_rnn_simple.to_csv('./predictions/y_pred_rnn_simple.csv', index=False) y_pred_rnn_simple=pd.read_csv('./predictions/y_pred_rnn_simple.csv') print(accuracy_score(y_test, y_pred_rnn_simple)) 0.826219183127
准确率达到了 82.6%!这真是很不错的结果了!现在的模型表现已经比之前的词袋模型更好了,因为我们将文本的序列性质考虑在内了。
还能做得更好吗?
5. 用 GloVe 预训练词嵌入的循环神经网络
在最后一个模型中,嵌入矩阵被随机初始化了。那么如果用预训练过的词嵌入对其进行初始化又当如何呢?举个例子:假设在语料库中有「pizza」这个词。遵循之前的架构对其进行初始化后,可以得到一个 300 维的随机浮点值向量。这当然是很好的。这很好实现,而且这个嵌入可以在训练过程中进行调整。但你还可以使用在很大的语料库上训练出来的另一个模型,为「pizza」生成词嵌入来代替随机选择的向量。这是一种特殊的迁移学习。
使用来自外部嵌入的知识可以提高 RNN 的精度,因为它整合了这个单词的相关新信息(词汇和语义),而这些信息是基于大规模数据语料库训练和提炼出来的。
我们使用的预训练嵌入是 GloVe。
官方描述是这样的:GloVe 是一种获取单词向量表征的无监督学习算法。该算法的训练基于语料库全局词-词共现数据,得到的表征展示出词向量空间有趣的线性子结构。
本文使用的 GloVe 嵌入的训练数据是数据量很大的网络抓取,包括:
下载压缩文件要 2.03GB。请注意,该文件无法轻松地加载在标准笔记本电脑上。
GloVe 嵌入有 300 维。
GloVe 嵌入来自原始文本数据,在该数据中每一行都包含一个单词和 300 个浮点数(对应嵌入)。所以首先要将这种结构转换为 Python 字典。
def get_coefs(word, *arr): try: return word, np.asarray(arr, dtype='float32') except: return None, None embeddings_index=dict(get_coefs(*o.strip().split()) for o in tqdm_notebook(open('./embeddings/glove.840B.300d.txt'))) embed_size=300 for k in tqdm_notebook(list(embeddings_index.keys())): v=embeddings_index[k] try: if v.shape !=(embed_size, ): embeddings_index.pop(k) except: pass embeddings_index.pop(None)
一旦创建了嵌入索引,我们就可以提取所有的向量,将其堆叠在一起并计算它们的平均值和标准差。
values=list(embeddings_index.values()) all_embs=np.stack(values) emb_mean, emb_std=all_embs.mean(), all_embs.std()
现在生成了嵌入矩阵。按照 mean=emb_mean 和 std=emb_std 的正态分布对矩阵进行初始化。遍历语料库中的 80000 个单词。对每一个单词而言,如果这个单词存在于 GloVe 中,我们就可以得到这个单词的嵌入,如果不存在那就略过。
word_index=tokenizer.word_index
nb_words=MAX_NB_WORDS
embedding_matrix=np.random.normal(emb_mean, emb_std, (nb_words, embed_size))
oov=0
for word, i in tqdm_notebook(word_index.items()):
if i >=MAX_NB_WORDS: continue
embedding_vector=embeddings_index.get(word)
if embedding_vector is not None:
embedding_matrix[i]=embedding_vector
else:
oov +=1
print(oov)
def get_rnn_model_with_glove_embeddings():
embedding_dim=300
inp=Input(shape=(MAX_LENGTH, ))
x=Embedding(MAX_NB_WORDS, embedding_dim, weights=[embedding_matrix], input_length=MAX_LENGTH, trainable=True)(inp)
x=SpatialDropout1D(0.3)(x)
x=Bidirectional(GRU(100, return_sequences=True))(x)
avg_pool=GlobalAveragePooling1D()(x)
max_pool=GlobalMaxPooling1D()(x)
conc=concatenate([avg_pool, max_pool])
outp=Dense(1, activation="sigmoid")(conc)
model=Model(inputs=inp, outputs=outp)
model.compile(loss='binary_crossentropy',
optimizer='adam',
metrics=['accuracy'])
return model
rnn_model_with_embeddings=get_rnn_model_with_glove_embeddings()
filepath="./models/rnn_with_embeddings/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5"
checkpoint=ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max')
batch_size=256
epochs=4
history=rnn_model_with_embeddings.fit(x=padded_train_sequences,
y=y_train,
validation_data=(padded_test_sequences, y_test),
batch_size=batch_size,
callbacks=[checkpoint],
epochs=epochs,
verbose=1)
best_rnn_model_with_glove_embeddings=load_model('./models/rnn_with_embeddings/weights-improvement-03-0.8372.hdf5')
y_pred_rnn_with_glove_embeddings=best_rnn_model_with_glove_embeddings.predict(
padded_test_sequences, verbose=1, batch_size=2048)
y_pred_rnn_with_glove_embeddings=pd.DataFrame(y_pred_rnn_with_glove_embeddings, columns=['prediction'])
y_pred_rnn_with_glove_embeddings['prediction']=y_pred_rnn_with_glove_embeddings['prediction'].map(lambda p:
1 if p >=0.5 else 0)
y_pred_rnn_with_glove_embeddings.to_csv('./predictions/y_pred_rnn_with_glove_embeddings.csv', index=False)
y_pred_rnn_with_glove_embeddings=pd.read_csv('./predictions/y_pred_rnn_with_glove_embeddings.csv')
print(accuracy_score(y_test, y_pred_rnn_with_glove_embeddings))
0.837203100893
准确率达到了 83.7%!来自外部词嵌入的迁移学习起了作用!本教程剩余部分都会在嵌入矩阵中使用 GloVe 嵌入。
6. 多通道卷积神经网络
这一部分实验了我曾了解过的卷积神经网络结构(http://www.wildml.com/2015/11/understanding-convolutional-neural-networks-for-nlp/)。CNN 常用于计算机视觉任务。但最近我试着将其应用于 NLP 任务,而结果也希望满满。
简要了解一下当在文本数据上使用卷积网络时会发生什么。为了解释这一点,我从 wildm.com(一个很好的博客)中找到了这张非常有名的图(如下所示)。
了解一下使用的例子:I like this movie very much!(7 个分词)
背后的原理是什么?
检测到特殊模式会激活每一次卷积的结果。通过改变卷积核的大小和连接它们的输出,你可以检测多个尺寸(2 个、3 个或 5 个相邻单词)的模式。
模式可以是像是「我讨厌」、「非常好」这样的表达式(词级的 ngram?),因此 CNN 可以在不考虑其位置的情况下从句子中分辨它们。
def get_cnn_model(): embedding_dim=300 filter_sizes=[2, 3, 5] num_filters=256 drop=0.3 inputs=Input(shape=(MAX_LENGTH,), dtype='int32') embedding=Embedding(input_dim=MAX_NB_WORDS, output_dim=embedding_dim, weights=[embedding_matrix], input_length=MAX_LENGTH, trainable=True)(inputs) reshape=Reshape((MAX_LENGTH, embedding_dim, 1))(embedding) conv_0=Conv2D(num_filters, kernel_size=(filter_sizes[0], embedding_dim), padding='valid', kernel_initializer='normal', activation='relu')(reshape) conv_1=Conv2D(num_filters, kernel_size=(filter_sizes[1], embedding_dim), padding='valid', kernel_initializer='normal', activation='relu')(reshape) conv_2=Conv2D(num_filters, kernel_size=(filter_sizes[2], embedding_dim), padding='valid', kernel_initializer='normal', activation='relu')(reshape) maxpool_0=MaxPool2D(pool_size=(MAX_LENGTH - filter_sizes[0] + 1, 1), strides=(1,1), padding='valid')(conv_0) maxpool_1=MaxPool2D(pool_size=(MAX_LENGTH - filter_sizes[1] + 1, 1), strides=(1,1), padding='valid')(conv_1) maxpool_2=MaxPool2D(pool_size=(MAX_LENGTH - filter_sizes[2] + 1, 1), strides=(1,1), padding='valid')(conv_2) concatenated_tensor=Concatenate(axis=1)( [maxpool_0, maxpool_1, maxpool_2]) flatten=Flatten()(concatenated_tensor) dropout=Dropout(drop)(flatten) output=Dense(units=1, activation='sigmoid')(dropout) model=Model(inputs=inputs, outputs=output) adam=Adam(lr=1e-4, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0) model.compile(optimizer=adam, loss='binary_crossentropy', metrics=['accuracy']) return model cnn_model_multi_channel=get_cnn_model() plot_model(cnn_model_multi_channel, to_file='./images/article_5/cnn_model_multi_channel.png', show_shapes=True, show_layer_names=True)
filepath="./models/cnn_multi_channel/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5" checkpoint=ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max') batch_size=256 epochs=4 history=cnn_model_multi_channel.fit(x=padded_train_sequences, y=y_train, validation_data=(padded_test_sequences, y_test), batch_size=batch_size, callbacks=[checkpoint], epochs=epochs, verbose=1) best_cnn_model=load_model('./models/cnn_multi_channel/weights-improvement-04-0.8264.hdf5') y_pred_cnn_multi_channel=best_cnn_model.predict(padded_test_sequences, verbose=1, batch_size=2048) y_pred_cnn_multi_channel=pd.DataFrame(y_pred_cnn_multi_channel, columns=['prediction']) y_pred_cnn_multi_channel['prediction']=y_pred_cnn_multi_channel['prediction'].map(lambda p: 1 if p >=0.5 else 0) y_pred_cnn_multi_channel.to_csv('./predictions/y_pred_cnn_multi_channel.csv', index=False) y_pred_cnn_multi_channel=pd.read_csv('./predictions/y_pred_cnn_multi_channel.csv') print(accuracy_score(y_test, y_pred_cnn_multi_channel)) 0.826409655689
准确率为 82.6%,没有 RNN 那么高,但是还是比 BOW 模型要好。也许调整超参数(滤波器的数量和大小)会带来一些提升?
7. RNN + CNN
RNN 很强大。但有人发现可以通过在循环层上叠加卷积层使网络变得更强大。
这背后的原理在于 RNN 允许嵌入序列和之前单词的相关信息,CNN 可以使用这些嵌入并从中提取局部特征。这两个层一起工作可以称得上是强强联合。
更多相关信息请参阅:http://konukoii.com/blog/2018/02/19/twitter-sentiment-analysis-using-combined-lstm-cnn-models/
def get_rnn_cnn_model(): embedding_dim=300 inp=Input(shape=(MAX_LENGTH, )) x=Embedding(MAX_NB_WORDS, embedding_dim, weights=[embedding_matrix], input_length=MAX_LENGTH, trainable=True)(inp) x=SpatialDropout1D(0.3)(x) x=Bidirectional(GRU(100, return_sequences=True))(x) x=Conv1D(64, kernel_size=2, padding="valid", kernel_initializer="he_uniform")(x) avg_pool=GlobalAveragePooling1D()(x) max_pool=GlobalMaxPooling1D()(x) conc=concatenate([avg_pool, max_pool]) outp=Dense(1, activation="sigmoid")(conc) model=Model(inputs=inp, outputs=outp) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) return model rnn_cnn_model=get_rnn_cnn_model() plot_model(rnn_cnn_model, to_file='./images/article_5/rnn_cnn_model.png', show_shapes=True, show_layer_names=True)
filepath="./models/rnn_cnn/weights-improvement-{epoch:02d}-{val_acc:.4f}.hdf5" checkpoint=ModelCheckpoint(filepath, monitor='val_acc', verbose=1, save_best_only=True, mode='max') batch_size=256 epochs=4 history=rnn_cnn_model.fit(x=padded_train_sequences, y=y_train, validation_data=(padded_test_sequences, y_test), batch_size=batch_size, callbacks=[checkpoint], epochs=epochs, verbose=1) best_rnn_cnn_model=load_model('./models/rnn_cnn/weights-improvement-03-0.8379.hdf5') y_pred_rnn_cnn=best_rnn_cnn_model.predict(padded_test_sequences, verbose=1, batch_size=2048) y_pred_rnn_cnn=pd.DataFrame(y_pred_rnn_cnn, columns=['prediction']) y_pred_rnn_cnn['prediction']=y_pred_rnn_cnn['prediction'].map(lambda p: 1 if p >=0.5 else 0) y_pred_rnn_cnn.to_csv('./predictions/y_pred_rnn_cnn.csv', index=False) y_pred_rnn_cnn=pd.read_csv('./predictions/y_pred_rnn_cnn.csv') print(accuracy_score(y_test, y_pred_rnn_cnn)) 0.837882453033
这样可得到 83.8% 的准确率,这也是到现在为止最好的结果。
8. 总结
在运行了 7 个不同的模型后,我们对比了一下:
import seaborn as sns from sklearn.metrics import roc_auc_score sns.set_style("whitegrid") sns.set_palette("pastel") predictions_files=os.listdir('./predictions/') predictions_dfs=[] for f in predictions_files: aux=pd.read_csv('./predictions/{0}'.format(f)) aux.columns=[f.strip('.csv')] predictions_dfs.append(aux) predictions=pd.concat(predictions_dfs, axis=1) scores={} for column in tqdm_notebook(predictions.columns, leave=False): if column !='y_true': s=accuracy_score(predictions['y_true'].values, predictions[column].values) scores[column]=s scores=pd.DataFrame([scores], index=['accuracy']) mapping_name=dict(zip(list(scores.columns), ['Char ngram + LR', '(Word + Char ngram) + LR', 'Word ngram + LR', 'CNN (multi channel)', 'RNN + CNN', 'RNN no embd.', 'RNN + GloVe embds.'])) scores=scores.rename(columns=mapping_name) scores=scores[['Word ngram + LR', 'Char ngram + LR', '(Word + Char ngram) + LR', 'RNN no embd.', 'RNN + GloVe embds.', 'CNN (multi channel)', 'RNN + CNN']] scores=scores.T ax=scores['accuracy'].plot(kind='bar', figsize=(16, 5), ylim=(scores.accuracy.min()*0.97, scores.accuracy.max() * 1.01), color='red', alpha=0.75, rot=45, fontsize=13) ax.set_title('Comparative accuracy of the different models') for i in ax.patches: ax.annotate(str(round(i.get_height(), 3)), (i.get_x() + 0.1, i.get_height() * 1.002), color='dimgrey', fontsize=14)
我们可以很快地看出在这些模型的预测值之间的关联。
fig=plt.figure(figsize=(10, 5)) sns.heatmap(predictions.drop('y_true', axis=1).corr(method='kendall'), cmap="Blues", annot=True);
结论
以下是几条我认为值得与大家分享的发现:
这篇文章很长。希望本文能对大家有所帮助。
*请认真填写需求信息,我们会在24小时内与您取得联系。