作者|Conner Brew
编译|VK
起源|Towards Data Science

介绍

在本文中,咱们将创立一个基于和平研究所(ISW)的结构化文档数据库。ISW为内政和情报业余人员提供信息产品,以加深对世界各地产生的抵触的理解。

要查看与本文相关联的原始代码和Notebook,请拜访以下链接:https://colab.research.google...

要拜访Kaggle上托管的最终结构化数据集,请拜访以下链接:https://www.kaggle.com/conner...

本文将是一个对于web抽取、自然语言解决(NLP)和命名实体辨认(NER)的练习。对于NLP,咱们将次要应用开源Python库NLTK和Spacy。

本文旨在演示web提取和NLP的一个用例,而不是对于这两种技术应用的全面初学者教程。如果你是NLP或web提取的老手,我倡议你遵循不同的教程,或者浏览Spacy、BeautifulSoup和NLTK文档页面。

# 导入库import requestsimport nltkimport mathimport reimport spacyimport regex as reimport pandas as pdimport numpy as npimport statistics as statsimport matplotlib.pyplot as pltimport matplotlib.cm as cmimport json# 你须要从NLTK下载一些包。from bs4 import BeautifulSoup from nltk import *nltk.download('stopwords')nltk.download('punkt')from nltk.corpus import stopwords# #在大多数环境中,你须要装置NER-D。!pip install ner-dfrom nerd import nerfrom sklearn.feature_extraction.text import CountVectorizerfrom sklearn.feature_extraction.text import TfidfTransformerfrom sklearn.cluster import MiniBatchKMeansfrom sklearn.feature_extraction.text import TfidfVectorizer

初始化变量

首先,咱们将初始化最终结构化数据中须要的数据字段。对于每个文档,我要提取题目、公布日期、人名、地名和其余各种信息。咱们还将加强文档中曾经存在的信息—例如,咱们将应用文档中的地名来获取相干的坐标,这对于当前可视化数据十分有用。

# 初始化最终数据集的数据字段dates=[]titles=[]locations=[]people=[]key_countries=[]content_text=[]links=[]coord_list=[]mentioned_countries=[]keywords=[]topic_categories=[]# 为前面的主题模型初始化簇变量cluster_keywords=[]cluster_number=[]# 应用SPACY库初始化NLP对象nlp = spacy.load("en_core_web_sm")

提取href

咱们将从ISW的生产库中提取文档。首先,咱们将抓取“浏览”页面以获取每个产品的独自href链接。而后咱们将这些链接存储在一个列表中,供提取函数稍后拜访。

# #从ISW浏览页面获取产品链接urls=['http://www.understandingwar.org/publications?page={}'.format(i) for i in range(179)]hrefs=[]def get_hrefs(page,class_name):  page=requests.get(page)  soup=BeautifulSoup(page.text,'html.parser')  container=soup.find_all('div',{'class':class_name})  container_a=container[0].find_all('a')  links=[container_a[i].get('href') for i in range(len(container_a))]  for link in links:    if link[0]=='/':      hrefs.append('http://www.understandingwar.org'+link)for url in urls:  get_hrefs(url,'view-content')

Web爬取

咱们将要编写的前几个函数是相当简略的文本提取。本教程不是对于BeautifulSoup用法的教程,要理解Python中的web爬取,请查看这里的文档:https://www.crummy.com/softwa...

取得日期

对于咱们的第一个函数,咱们将提取公布日期。它扫描从产品网页中提取的html文档,并找到一个类为“submitted”的字段。这是咱们的生产日期。

取得题目

接下来,咱们须要产品名称。同样,这个字段被不便地标记为“title”类。

获取所有文本

最初,咱们将提取文档的全文。当我提取文本时,我通常遵循“先提取,后过滤”的web提取形式。这意味着,在最后的文本提取中,我对文本执行起码的过滤和解决。我更违心在当前的剖析中进行解决,因为这是必要的。然而,如果你想更进一步,你可能心愿对提取的文本进行比上面函数演示的更多的预处理。

对于我的get_contents函数,我保持最根本的准则——我在黑名单中列出了一些不想被提取的文本。而后从页面中提取所有文本并将其附加到一个长期字符串中,该字符串又被附加到列表content_text中。

# 提取公布数据def get_date(soup):  try:    data=soup.find('span',{'class':'submitted'})    content=data.find('span')    date=content.get('content')    dates.append(date)  except Exception:    dates.append('')    pass# 提取产品题目def get_title(soup):  try:    title=soup.find('h1',{'class':'title'}).contents    titles.append(title[0])  except Exception:    titles.append('')    pass# 提取产品的文本内容def get_contents(soup):  try:    parents_blacklist=['[document]','html','head',                       'style','script','body',                       'div','a','section','tr',                       'td','label','ul','header',                       'aside',]    content=''    text=soup.find_all(text=True)      for t in text:      if t.parent.name not in parents_blacklist and len(t) > 10:        content=content+t+' '    content_text.append(content)  except Exception:    content_text.append('')    pass

自然语言解决

接下来,咱们将找出产品中援用了哪些国家。有很多API能够用于查看国家的文本内容,但这里咱们将应用一个简略的办法:列出世界上所有国家的列表。这个列表来自维基百科:https://en.wikipedia.org/wiki...

在函数失去all_mentioned_countries 后,它应用根本统计分析来确定哪些国家最突出——这些国家最有可能成为文件叙述的焦点。为此,该函数计算整个文档中提到一个国家的次数,而后查找比平均值提到次数多的国家。而后将这些国家追加到key_countries列表中。

# 在注释中援用所有国家的列表。# 如果文本中的一个单词与列表中的一个国家匹配,那么它将被增加到国家列表中。 def get_countries(content_list):  iteration=1  for i in range(len(content_list)):    print('Getting countries',iteration,'/',len(content_list))    temp_list=[]    for word in word_tokenize(content_list[i]):      for country in country_list:        if word.lower().strip() == country.lower().strip():          temp_list.append(country)    counted_countries=dict(Counter(temp_list))    temp_dict=dict.fromkeys(temp_list,0)    temp_list=list(temp_dict)    if len(temp_list)==0:      temp_list.append('Worldwide')    mentioned_countries.append(temp_list)    # 计算每个国家被提及的次数,而后对照平均值查看每次计数。     # 如果一个国家被提及的次数超过了均匀次数,它就会作为一个关键字被记录。    keywords=[]    for key in counted_countries.keys():      if counted_countries[key] > np.mean(list(counted_countries.values())):        keywords.append(key)    if len(keywords) != 0:      key_countries.append(keywords)    else:      key_countries.append(temp_list)    iteration+=1

命名实体辨认:地点

接下来,咱们要丰盛咱们的数据。最终,结构化数据的指标通常是执行某种剖析或可视化——在这种国内抵触信息的状况下,将信息按地理位置绘制进去是很有价值的。为此,咱们须要与文档对应的坐标。

找到地名

首先,咱们将应用自然语言解决(NLP)和命名实体辨认(NER)从文本中提取地名。

NLP是机器学习的一种模式,计算机算法应用语法和语法规定来学习文本中单词之间的关系。通过这种学习,NER可能了解某些单词在句子或段落中所起的作用。本教程并不打算全面介绍NLP—对于这样的资源,请查看:https://medium.com/@ODSC/an-i...

从内部API获取坐标

为了找到地名的坐标,咱们将应用 Open Cage API查问坐标;你能够在这里创立一个收费帐户并接管API密钥。还有许多其余风行的天文api可供选择,但通过重复试验,我发现Open-Cage在中东地区有着最好的性能。

首先,咱们迭代从文档中检索到的每个地名,并在Open Cage中查问它。一旦实现这项工作,咱们将比照Open Cage与先前创立的mentioned_countries 列表。这将确保咱们检索的查问后果位于正确的地位。

# 应用NLP提取地名,而后查问open-cage API以取得绘图所需的坐标# 插入你本人的OpenCage API key:geo_api_key='Insert Your API Key Here'def get_coords(content_list):  iteration=1  for i in range(len(content_list)):    print('Getting coordinates',iteration,'/',len(content_list))    temp_list=[]    text=content_list[i]    # 利用一个NER算法,从python库'ner-d'中查找地名。    doc=nlp(text)    location=[X.text for X in doc.ents if X.label_ == 'GPE']    location_dict=dict.fromkeys(location,0)    location=list(location_dict)    # 查问地位。        for l in location:      try:         request_url='https://api.opencagedata.com/geocode/v1/json?q={}&key={}'.format(l,geo_api_key)        page=requests.get(request_url)        data=page.json()        for n in range(len(data)):          # 这行代码查看查问后果中的国家是否与mentioned_countries之一相匹配。如果不是,那么查问后果很可能是假正例。          if data['results'][n]['components']['country'] in mentioned_countries[i]:            lat=data['results'][n]['geometry']['lat']            lng=data['results'][n]['geometry']['lng']            coordinates={'Location': l,                          'Lat': lat,                          'Lon': lng}            temp_list.append(coordinates)            break          else:            continue          except Exception:         continue    coord_list.append(temp_list)    iteration+=1

命名实体辨认:人

接下来,咱们将提取文档中提到的人的姓名。为此,咱们将再次应用NER-d python库中的NER算法。

获取全名

在最终的结构化数据中,我只想要全名。只找到“Jack”或“John”的数据,会不会令人困惑?为此,咱们将再次应用一些根本的统计数据。当提到全名时,函数将跟踪全名,通常是在文本的结尾。

当前面提到局部名称时,它将援用全名列表,以标识局部名称援用的是谁。例如,如果一篇新闻文章是这样写的:“乔·拜登正在竞选总统。乔是前总统奥巴马的副总统,咱们晓得乔指的是拜登,因为他的全名在文中早些时候曾经给出。此函数将以雷同的形式运行。

反复的名字

如果呈现了反复的状况,该函数将应用后面用于国家/地区函数的雷同统计数据。它将测量一个名字被提及的次数,并将其作为最有可能的名字。例如:乔·拜登和他的儿子亨特·拜登都是受欢迎的美国政治家。乔·拜登是前副总统。拜登当初正在与现任总统唐纳德·特朗普竞选总统”。依据文本的统计重点,这篇文章显然是对于乔·拜登,而不是亨特·拜登。

验证名字

一旦函数计算出所有提到的全名,它将把它们增加到一个列表中。而后,它将查问维基百科中的每个名字,以验证它是否是值得蕴含在结构化数据中的有影响力的人的名字。

def get_people(content_list):  iteration=1  # 应用NER在文本中查找人名。    for i in range(len(content_list)):    print('Getting people',iteration,'/',len(content_list))    temp_list=[]    text=content_list[i]    doc=nlp(text)    persons=[X.text for X in doc.ents if X.label_ == 'PERSON']    persons_dict=dict.fromkeys(persons,0)    persons=list(persons_dict)    full_names=[]    for person in persons:       if len(word_tokenize(person)) >= 2:        string_name=re.sub(r"[^a-zA-Z0-9]+", ' ', person).strip()        full_names.append(string_name)      final_names=[]    for person in persons:      for name in full_names:        tokens=word_tokenize(name)        for n in range(len(tokens)):          if person==tokens[n]:            final_names.append(name)    for name in full_names:      final_names.append(name)    name_dict=dict.fromkeys(final_names,0)    final_names=list(name_dict)    valid_names=[]    for name in final_names:      page=requests.get('https://en.wikipedia.org/wiki/'+name)      if page.status_code==200:        valid_names.append(name)    people.append(valid_names)    iteration+=1

关键词提取:TF-IDF

咱们的下一个工作是从文本中提取关键字。最常见的办法是应用一种称为TF-IDF的办法。TF-IDF模型测量单个文档中单词的应用频率,而后将其与整个文档语料库中的均匀使用率进行比拟。

如果一个术语在单个文档中频繁应用,并且很少在整个文档语料库中应用,那么该术语很可能示意该特定文档特有的关键字。这篇文章并不是一篇对于TF-IDF模型的全面概述。要理解更多信息,请查看这篇对于Medium的文章:https://medium.com/datadriven...

首先,咱们的函数将创立通常所说的“词袋”。这将跟踪每个文档中应用的每个单词。而后,它将计算每个文档中每个单词的每次应用次数—单词频率(TF)。而后,它计算逆文档频率(IDF)。而后将这些值写入矩阵中的坐标,而后对矩阵进行排序,以帮忙咱们找到最有可能示意文档的单词。

# 第一个函数通过升高字符大小写和删除特殊字符对文本进行预处理。def pre_process(text):    text=text.lower()    text=re.sub("</?.*?>"," <> ",text)    text=re.sub("(\\d|\\W)+"," ",text)    return text# 这个函数将矩阵映射到坐标。TF-IDF函数将频率分数映射到矩阵,而后须要对这些矩阵进行排序,以帮忙咱们找到关键字。 def sort_coo(coo_matrix):    tuples = zip(coo_matrix.col, coo_matrix.data)    return sorted(tuples, key=lambda x: (x[1], x[0]), reverse=True)# 与下面一样,这是一个帮忙函数,一旦频率映射到矩阵,它将帮忙排序和抉择关键字。 # 这个函数专门帮忙咱们依据TF-IDF统计数据抉择最相干的关键字def extract_topn_from_vector(feature_names, sorted_items, topn=10):    sorted_items = sorted_items[:topn]    score_vals = []    feature_vals = []    for idx, score in sorted_items:        fname = feature_names[idx]        score_vals.append(round(score, 3))        feature_vals.append(feature_names[idx])    results= {}    for idx in range(len(feature_vals)):        results[feature_vals[idx]]=score_vals[idx]    return results#最初一个函数蕴含了上述helper函数,它对注释利用TF-IDF算法,依据应用频率查找关键字。def get_keywords(content_list):  iteration=1  processed_text=[pre_process(text) for text in content_list]  stop_words=set(stopwords.words('english'))  cv=CountVectorizer(max_df=0.85,stop_words=stop_words)  word_count_vector=cv.fit_transform(processed_text)  tfidf_transformer=TfidfTransformer(smooth_idf=True,use_idf=True)  tfidf_transformer.fit(word_count_vector)  feature_names=cv.get_feature_names()  for i in range(len(processed_text)):    print('Getting Keywords',iteration,'/',len(content_list))    doc=processed_text[i]    tf_idf_vector=tfidf_transformer.transform(cv.transform([doc]))    sorted_items=sort_coo(tf_idf_vector.tocoo())    keys=extract_topn_from_vector(feature_names,sorted_items,10)    keywords.append(list(keys.keys()))    iteration+=1

主题模型

NLP中最常见的工作之一就是主题模型。这是一种聚类模式,它尝试依据文档的文本内容主动对文档进行分类。在这个具体的例子中,我想一眼就晓得ISW波及哪些主题。通过依据文本内容对文档进行分类,我能够轻松地对文档的次要思维有一个大抵的理解。

向量化

对于这个例子,我将应用k-means聚类算法来进行主题建模。首先,我将再次应用TF-IDF算法对每个文档进行向量化。向量化是一个机器学习术语,指的是将非数字数据转换成计算机能够用来执行机器学习工作的数字空间数据。

优化

一旦文档被向量化,helper函数就会查看簇的最佳数量。(k示意k-means的k)。在本例中,最佳数目是50。一旦我找到了最佳的数字,在这个例子中,我正文掉了这行代码,并手动将参数调整为等于50。这是因为我正在剖析的数据集不会常常更改,所以我能够冀望随着工夫的推移,最佳簇的数量会放弃不变。对于变动更频繁的数据,你应该返回最佳的簇数量作为变量-这将帮忙你的聚类算法主动设置其最佳参数。我在我的工夫序列剖析文章中展现了一个例子。

聚类

每个簇实现后,我将每个簇的编号(1–50)保留到簇编号的列表中,而组成每个簇的关键字保留到cluster_keywords的列表中。这些簇关键字稍后将用于向每个主题簇增加题目。

# 该函数依据各种“k”参数查看聚类算法,以找到“k”的最优值。def find_optimal_clusters(data, max_k):    iters = range(2, max_k+1, 2)        sse = []    for k in iters:        sse.append(MiniBatchKMeans(n_clusters=k,                                    init_size=1024,                                    batch_size=2048,                                   random_state=20).fit(data).inertia_)                print('Fit {} clusters'.format(k))            f, ax = plt.subplots(1, 1)    ax.plot(iters, sse, marker='o')    ax.set_xlabel('Cluster Centers')    ax.set_xticks(iters)    ax.set_xticklabels(iters)    ax.set_ylabel('SSE')    ax.set_title('SSE by Cluster Center Plot')# 从内容列表中获取关键词来帮忙对主题模型的分类def get_top_keywords(data, clusters, labels, n_terms):  df = pd.DataFrame(data.todense()).groupby(clusters).mean()  for i,r in df.iterrows():    cluster_keywords.append(','.join([labels[t] for t in np.argsort(r)[-n_terms:]]))    # 利用于主题建模的内容列表def get_topics(content_list):  processed_text=[pre_process(text) for text in content_list]  stop_words=set(stopwords.words('english'))  cv=CountVectorizer(max_df=0.85,stop_words=stop_words)  word_count_vector=cv.fit_transform(processed_text)  tfidf_transformer=TfidfTransformer(smooth_idf=True,use_idf=True)  tfidf_transformer.fit(word_count_vector)  feature_names=cv.get_feature_names()  vector=tfidf_transformer.transform(cv.transform(processed_text))  #find_optimal_clusters(vector,50)  clusters = MiniBatchKMeans(n_clusters=50, init_size=1024, batch_size=2048, random_state=20).fit_predict(vector)  for cluster in clusters:    cluster_number.append(int(cluster))    get_top_keywords(vector, clusters, cv.get_feature_names(), 20)

放在一起

最初,咱们将提取咱们的数据。应用咱们之前失去的href列表,当初是将所有提取函数利用于web内容的时候了。

# 遍历从“browse”中提取的href,提取相干内容iteration=1# 前几个函数依赖于原始提取的web内容作为参数。这些都是根本的web抓取技术。for href in hrefs:  print('Web scraping: iteration',iteration,'/',len(hrefs))  page=requests.get(href)  soup=BeautifulSoup(page.text,'html.parser')  links.append(href)  get_date(soup)  get_title(soup)  get_contents(soup)  iteration+=1# 上面这些函数依赖于文本主体作为参数。# 这些是基于nlp的函数。# 留神:因为查问内部API,# 须要一个超时来阻止服务器过载。# 这部分代码的运行工夫很长。get_countries(content_text)get_coords(content_text)get_people(content_text)get_keywords(content_text)get_topics(content_text)

丰盛主题模型

咱们的下一个问题是:咱们的簇为咱们提供了一个与每个簇相关联的单词列表,然而簇的名称仅仅是数字。这使咱们有机会绘制一个词云或其余乏味的可视化图,能够帮忙咱们了解每个簇,但对于结构化数据集中的高深莫测的了解来说,它并没有那么有用。另外,我认为有些文档可能属于多个主题类别。k-means不反对多重聚类,因而我必须手动辨认这些文档。首先,我将打印前几行关键字,以理解我正在解决的数据。

与每个主题相关联的一些关键字。咱们将应用这些关键字将簇分类到预约义的类别中。

在对各种技术进行了大量试验之后,我决定采纳一种非常简单的办法。我扫描了与每个簇相干的每个关键字列表,并在每个与特定主题相干的关键字中记录了重要的关键字。在这个阶段,畛域常识是要害。例如,我晓得,ISW文件中的阿勒颇简直必定提到了叙利亚内战。对于你的数据,如果你不足适当的畛域常识,你可能须要做进一步的钻研,征询你团队中的其他人,或者定义一个更高级的编程办法来命名簇。

然而,对于这个例子,简略的办法很无效。在记录了簇列表中存在的几个重要关键字之后,我本人制作了几个列表,其中蕴含了与结构化数据中我想要的最终主题类别相关联的关键字。该函数简略地将每个簇的关键字列表与我创立的列表进行比拟,而后依据列表中的匹配项调配主题名称。而后将这些最初的主题附加到主题类别列表中。

#搜寻与主题对应的关键词列表,与聚类词库穿插援用,为每篇文章调配一个主题类别。oir=['OIR Iraq','yezidis','mosul','peshmerga','isis','iraq','sinjar','baghdad','maliki',     'daquq','anbar','isf','abadi','malaki','ramadi','iraqi','fallujah','dabiq']terrorism=['Terrorism','jihadi','islamic','salafi','qaeda',           'caliphate','isis','terrorist','terrorism']syrian_conflict=['Syrian Conflict','sana','syria','assad',                 'idlib','afrin','aleppo']russia=['Russia','russia','belarus','slavic','kremlin','russian',        'minsk','ukraine','putin']iran=['Iran','iran','iranian','proxy','militias','militia','marjah']turkey=['Turkey','erdogan','turkish','turkey']ors=['ORS','kabul','ghani','pakistan','afghan','afghanistan',     'taliban','ansf','karzai','helmand']africa=['Africa','libya','libyan','egypt','egyptian','africa','african']cat_list=[oir,terrorism,syrian_conflict,russia,iran,turkey,ors,africa]topic_dict={}for i in range(len(cluster_keywords)):  temp_list=[]  for n in nltk.word_tokenize(cluster_keywords[i]):    for item in cat_list:      if n in item:        temp_list.append(item[0])    temp_dict=dict.fromkeys(temp_list,0)  temp_list=list(temp_dict)  topic_dict[i] = temp_listfor num in cluster_number:  topic_categories.append(topic_dict[num])

数据库创立

最初一步是将咱们提取的所有数据集中起来。对于这些数据,我更喜爱JSON格局。这是因为我想以不同的形式组织某些类型的数据—例如,locations字段将蕴含地名、纬度和经度的字典列表。在我看来,JSON格局是将这种格式化的数据存储到本地磁盘的最无效的办法。我还在文档数据库MongoDB中备份了这个数据库的正本,但这不是本文的重点。

#将一个空列表初始化db=[]for i in range(len(hrefs)):  countries={      'focus area': key_countries[i],      'all mentioned countries': mentioned_countries[i]  }  # 将函数中定义的所有列表增加到新的存储列表中  doc={       '_id': len(hrefs) - i,       'title': titles[i],       'date': dates[i],       'places': coord_list[i],       'people': people[i],       'keywords': keywords[i],       'countries': countries,       'full text': content_text[i],       'url': links[i],       'topic cluster': cluster_number[i],       'categories': topic_categories[i]  }  db.append(doc)# 将列表保留为谷歌驱动器内的.JSON数据存储文件(用于演示目标)with open ('/content/drive/My Drive/Colab Notebooks/isw_products.json', 'w') as fout:  json.dump(db, fout)

摘要

当初咱们完结了!咱们从网页中提取链接,而后应用这些链接从网站中提取更多内容。咱们应用这些内容,而后应用内部api、ML簇算法和NLP来提取和加强这些信息。TF-IDF向量化、关键字提取和主题模型,这些是NLP的基石。如果你有更多问题或须要信息,请分割咱们,祝你在将来的NLP中好运!

原文链接:https://towardsdatascience.co...

欢送关注磐创AI博客站:
http://panchuang.net/

sklearn机器学习中文官网文档:
http://sklearn123.com/

欢送关注磐创博客资源汇总站:
http://docs.panchuang.net/