如何识别、抓取和构建高质量机器学习数据集(上)

2019年05月01日 由 sunlei 发表 325144 0


本文介绍

数据是任何机器学习问题的核心。如果没有相关数据的访问,机器学习目前所取得的所有进步都是不可能的。话虽如此,如今大多数机器学习爱好者都专注于获取方法论知识(这是一个好的开始,但不能超越)。

在对方法达到一定熟悉程度之后,仅解决数据集已经可用的问题方面它的潜力是有限的。

幸运的是,我们生活在一个网络上有大量数据的时代;我们所需要的只是识别和提取有意义数据集的技能。因此,让我们开始看看如何识别、抓取和构建一个高质量的机器学习数据集。

本文的重点是解释如何通过实际示例和代码片段构建高质量的数据集。

在整篇文章中,我将引用我收集到的三个高质量的数据集,分别是服装尺寸推荐Fit数据集,新闻类数据集,讽刺检测数据集来解释各个点。为了做好准备,接下来我将简要解释每个数据集的内容。你可以找到他们的详细描述,下面是他们的数据集。

服装尺寸推荐Fit数据集

服装尺码的推荐和合身度的预测对于改善顾客的购物体验和降低退货率至关重要。从某家服饰购物网站收集的数据集包含顾客对购买的衣服的合身程度反馈,以及诸如评分、评论、类别信息、顾客尺寸等其他方面的信息。该数据集在确定服装产品适合客户的关键特征时非常有用。

新闻类别数据集

该数据集包含从《赫芬顿邮报》 (HuffPost)获得的2012至2018年约20万条新闻标题。它包含诸如新闻类别、新闻标题、新闻故事的简短描述、出版日期等详细信息。数据集可以用于多种用途,如识别未跟踪的新闻文章的标签、识别不同新闻类别中使用的语言类型等。

讽刺检测数据集

以往关于挖苦检测的研究大多使用基于hashtag的监控收集的Twitter数据集,但这些数据集在标签和语言方面存在噪声。为了克服这些限制,这个数据集从两个新闻网站收集:洋葱和赫芬顿邮报。《洋葱》(美国洋葱新闻网站)对时事进行讽刺,而《赫芬顿邮报》(HuffPost)则报道真实和非讽刺新闻。

分享一个有趣的事实:这些数据集在Kaggle上共有超过250个以上的upvote, 50k以上的view, 6000+的download和50多个kerkernel。

第1阶段-搜索数据

这个阶段需要耐心,因为你可能需要广泛地在网络上搜索。但别担心。在这里,我将根据我的经验提供一些可以使你的搜索更加系统和有效的建议。

如果你希望收集和构建一个高质量的数据集,你可能会遇到以下两种情况之一:

你正在寻找能够解决特定问题的数据集。(问题已知)

你正在寻找可用于解决有趣问题的数据集。(问题未知)

根据你所处的情况,以下几点将会很有帮助。

已知的问题

收集服装合身度和讽刺度检测数据集,以解决特定问题。因此,他们可以很好的解释这种情况。

以下步骤可能有助于在这种情况下搜索数据集:

找出问题的关键在于识别数据信号来解决问题:这是最重要的一步。在尺寸推荐问题中,我们想要向客户推荐服装尺寸,关键的数据信号将是用户id、产品id、购买的尺寸以及该购买客户的合身反馈。其他信号,如产品类别,客户测量等,虽然很好,但不是必要的。

在网上搜索一个提供所有必要信号的来源:在这里,你的搜索技能将派上用场。用它来浏览几个网站,看看它们是否提供了必要的数据信号。对于服装匹配数据集,像Zappos(美国一家出售鞋子的网站)这样的网站似乎很有前途,但缺少了购买的基本尺寸信号,而ModCloth(美国一家服装网站)提供了所有必要的数据信号(尽管需要一些额外的调整;稍后将详细介绍)。

如果找不到单个数据源,请查看是否可以将多个数据源的数据组合起来构建数据集:讽刺检测数据集是将多个数据源的数据组合起来构建完整且高质量数据集的完美示例。既然我们知道问题所在(检测讽刺)和我们想要的数据类型(讽刺和非讽刺文本),我们就不必拘泥于一个来源来提供所有信息。我认为《洋葱》是获取讽刺文本的来源,而对于非讽刺文本,我选择了一个真实的新闻报道网站《赫芬顿邮报》(HuffPost)。

查看源文件中是否包含足够的历史数据,以便构建足够大的数据集:在开始收集数据之前,这也是非常重要的一点。如果一个网站没有包含足够的数据,例如,一个在线零售商没有提供大量的产品集合,或者一个新闻网站没有包含对旧故事的存档,即使你收集了数据,也不会给你带来多大的好处。因此,需要寻找一个提供足够数据来构建足够大的数据集的源。

如何改进数据集?你能将来自其他来源的数据组合起来使其更有趣吗?这是一个开放式指针。选中上述所有框后,请查看如何进一步改进数据集。考虑一下,你是否可以通过不同的源组合关于某些属性的更多信息,从而帮助人们为他们的模型构建信息特性。

未知的问题

新闻类别数据集是解释这类情况的一个很好的候选数据集,因为它没有收集特定的问题。不知道我们在寻找什么确实会使情况变得有点复杂,但是,当你在网上冲浪时记下以下几点可以帮助你识别下一个有趣的数据集:

  1. 源是否包含任何值得估计/预测的数据信号?在分析一个网站的时候,考虑一下这个网站是否提供了一些值得评估的有趣信息。它可以是一些简单的东西,或者与网站上的信息类型有关的东西。举个简单的例子,我在《赫芬顿邮报》(HuffPost)上注意到,每个故事都被标注了一些类别(比如体育、政治等),我认为这是一个有趣的数据信号,可以用来预测。对于信息类型的情况,我将《赫芬顿邮报》HuffPost的新闻标题在挖苦检测数据集中视为非挖苦句(考虑到它们报道的是真实的新闻),而《洋葱》的标题则视为挖苦句。

  2. 源包含足够的元数据来预测结果吗?:一旦你决定了一个值得预测的数据信号,你将必须确保网站提供足够的侧信息来预测该数据信号,如果没有,你是否可以使用其他来源来将该信息带入数据集中。例如,如果我们没有关于商品的元数据,那么在电子商务平台上预测产品的价格可能就不是很好。要使数据集成为一个好的数据集,需要足够多的侧信息。

  3. 站点是否包含足够的历史数据,允许你能够构建足够大的数据集?这与问题已知部分的第4点相同。

  4. 预测结果有什么重要的意义或应用吗?高质量数据集的另一个标志是,它可以用来解决有趣的和实际的问题,或者能够对一些现象提供有趣的见解。例如,建立在新闻类别数据集上的分类器可以帮助识别任何散文的写作风格(无论是政治的、幽默的,等等),帮助标记未跟踪的新闻文章,为不同类型的新闻提供写作风格如何不同的见解,等等。

  5. 交叉检查,看看是否已经有这种类型的数据可用。如果是,你的数据集是否在现有数据集上添加了任何内容?当前位置这一步很重要,这样你就知道你在做一些独特的事情,而不是那些已经存在的事情,它们可能不会很好地利用时间。从这个步骤开始,在谷歌上进行简单的搜索就足够了。

  6. 如何改进数据集?你能将来自其他来源的数据组合起来使其更有趣吗?这与问题已知部分的第5点相同。


第2阶段-提取数据

一旦我们缩小了源的范围,就可以开始提取数据了。

在抓取数据之前,请仔细阅读网站的条款和条件,以确保你不会通过抓取和公开分发数据而违反任何规则。

由于没有实际的例子很难解释这一节,所以我将引用我在从ModCloth中抓取数据时使用的脚本作为例子来说明不同的观点。

了解网站的结构

首先要做的是熟悉站点的结构。



在ModCloth上,我们看到在顶部我们有不同的服装类别:连衣裙,上衣,下装等等。如果单击其中一个类别(如上图中的top),就会看到产品以网格格式显示。图片中的页面显示了100个产品,其余的产品可以通过右上角的页面滚动器访问。

接下来,我们单击其中一个产品,观察每个产品的页面。在顶部,我们有与项目相关的元数据,在底部,我们有产品评论。



我们注意到,每个页面最多包含10个评论。如果有超过10个评论,我们会在右下角看到一个NEXT按钮。



当我们点击NEXT按钮时,我们会看到接下来的10条评论。但是,你可能注意到链接没有变化,这意味着除了单击NEXT按钮之外,没有其他方法访问后续的评论。我们还看到,在随后的评论页面中,还会出现一个PREVIOUS(上一个)按钮。稍后我们将看到为什么注意到这些事情对于数据提取很重要。



我们现在对网站的结构有了一定的了解。重申一下,我们的目标是从每个类别中提取每个产品的评论。

提取产品链接

由于类别的数量有限,没有必要编写脚本来提取它们的链接;它们可以手工收集。在本节中,我们将重点从服装类别之一:上衣中提取产品链接。

我们还将使用Selenium (Web浏览器自动化工具)进行数据提取。

那么,让我们开始吧:

到目前为止,我们知道在每个类别中,产品以100组的形式呈现,我们可以使用一个页面滚动器来访问所有的产品。首先,我们需要了解不同页面的链接是如何变化的。通常情况下,下面的图片表明链接遵循的模式。

[caption id="attachment_39712" align="aligncenter" width="920"] top类别的第1页[/caption]

[caption id="attachment_39713" align="aligncenter" width="920"] top类别的第2页[/caption]

[caption id="attachment_39714" align="aligncenter" width="920"] top类别的第3页[/caption]

然后,对于每个页面,我们需要提取到单个项目页面的链接。要得到它,转到其中一项,右键单击它,然后转到“inspect”选项。滚动一点以识别包含item链接的元素,并注意它的CSS类。在下面的图片中,我们看到在我们的例子中类是thumbu -link。很可能,所有其他产品链接也将使用相同的类进行样式化(只需验证一次)。



有了这些信息,我们可以编写下面的代码来提取top类别中所有产品的链接:
from bs4 import BeautifulSoup
from selenium import webdriver

# download driver from http://chromedriver.chromium.org/downloads
path_to_chromedriver = './chromedriver2.exe'
browser = webdriver.Chrome(executable_path = path_to_chromedriver)

urls = []; counter = 0; tops_link = []
## Since Tops category has 7 pages, link to each following a specific pattern,
## we can create links to pages in following way.
for i in range(7):
urls.append('https://www.modcloth.com/shop/tops?sz=102&start='+str(counter))
counter += 102

## Extracting links for products in each page
for url in urls:
## open the url
browser.get(url)

## purposeful wait time to allow website to get fully loaded
time.sleep(4)

## get page content
content = browser.page_source
soup = BeautifulSoup(content, "lxml")

product_links = []

## extract all the "a" elements with "thumb-link" class from the page
data_links = soup.find_all("a", {"class":"thumb-link"})

## from each element, extract the URL
for i in data_links:
product_links.append(i['href'])

tops_link.extend(product_links)

## purposeful wait time to avoid sending requests in quick succession
time.sleep(10)

正如你可能已经注意到的,脚本有目的地等待时间,以确保我们没有向站点发送太多频繁的请求。通常,每秒一个请求是好的,但是考虑到ModCloth是一个小站点(可能没有Amazon那么大),我们可以延长等待时间。你可以在这方面运用你的判断。

提取评论

既然我们有了每个产品的链接,那么我们就可以更深入地了解并提取每个产品的评论。首先,我们将检查每个评论对应的HTML。同样,右键单击review并单击“inspect”。



我们注意到每个评论都包含在一个
元素中。让我们研究一下
元素中的内容。我们可以通过单击元素旁边的箭头来实现这一点。当我们将鼠标悬停在
标记内的各种元素上时,相应的视图将在页面上突出显示。

例如,在上面的图像中,具有名为pr-rd-content-block pr-accordion pr-accordion-collapsed的类的
元素折叠对应于fit feedback和与客户测量相关的数据。一旦你研究了
标记内的所有不同元素,请参阅下面的脚本,以了解如何提取所有相关的详细信息。
from selenium.common.exceptions import NoSuchElementException, WebDriverException
import numpy as np
import random
## helper function to consolidate two dictionaries
def merge_two_dicts(x, y):
z = x.copy() # start with x's keys and values
z.update(y) # modifies z with y's keys and values & returns None
return z
scraped_data = []
## for each product in Tops category
for iterr in range(0,len(tops_link)):
init = 0
url = tops_link[iterr]
## open the URL in browser
try:
browser.get(url)
time.sleep(4)
except WebDriverException: ## when extracted URL is invalid
print('invalid url', iterr)
continue
## get the webpage content
content = browser.page_source
soup = BeautifulSoup(content, "lxml")
## repeat until we run of review pages
while(True):
## get the webpage content
content = browser.page_source
soup = BeautifulSoup(content, "lxml")
## extract reviewer details
reviewer_details = soup.find_all("div", {"class": "pr-rd-reviewer-details pr-rd-inner-side-content-block"})
## extract reviewers' name
reviewers_name = []
for reviewer in reviewer_details:
## In ModCloth, reviewer name appears as "By REVIEWER_NAME"
## Splitting at the end is to remove "By" to get only the actual reviewer name
reviewer_name = reviewer.find("p", {"class":"pr-rd-details pr-rd-author-nickname"}).text.split('\n')[-1].strip()
reviewers_name.append(reviewer_name)
## extract "isVerified" information
isVerified = soup.find_all("span", {"class": "pr-rd-badging-text"})
## extract the fit feedback and customer measurements data (review_metadata)
review_data = soup.find_all("article", {"class": "pr-review"})
review_metadata_raw = []
for i in range(len(review_data)):
review_metadata_raw.append(review_data[i].find("div", {"class": "pr-accordion-content"}))
## extract HTML elements which contain review metadata
review_metadata_elements = [review_metadata_raw[i].find_all("dl", {"class", "pr-rd-def-list"})
if review_metadata_raw[i] is not None else None
for i in range(len(review_metadata_raw))]
## extract actual data from HTML elements
review_metadata = []
for element in review_metadata_elements:
if element is None:
review_metadata.append(None)
continue
##
elements contain metadata field name like "fit", "length" etc
##
elements contain reviewer's response for those metadata fields like "small", "just right" etc
review_metadata.append([(element[i].find("dt").text.lower(), element[i].find("dd").text.lower())
if element is not None else ""
for i in range(len(element))])
## extract review text
review_text = [txt.text for txt in soup.find_all("p", {"class": "pr-rd-description-text"})]
review_summary = [txt.text for txt in soup.find_all("h2", {"class": "pr-rd-review-headline"})]
## extract item id
item_id = soup.find("div", {"class": "product-number"}).find("span").text
## extract item category
try:
category = soup.find("a", {"class":"breadcrumb-element"}).text.lower()
except AttributeError: ## if category not present, item is not available
time.sleep(15 + random.randint(0,10))
break
## extract available product sizes
product_sizes = [i.text.strip().lower() for i in soup.find("ul", {"class": "swatches size"})
.find_all("li", {"class": "selectable variation-group-value"})]
item_info = {"category": category, "item_id": item_id, "product_sizes": product_sizes}
## consolidate all the extracted data
## ignore records which don't have any review metadata as fit feedback is an essential signal for us
scraped_data.extend([merge_two_dicts({"review_text": review_text[j], "review_summary": review_summary[j]},
merge_two_dicts(merge_two_dicts({"user_name":reviewers_name[j]},
{data[0]:data[1] for data in review_metadata[j]})
,item_info))
for j in range(len(reviewer_details)) if review_metadata_raw[j] is not None])
## if current page is the initial one, it contains only NEXT button (PREVIOUS is missing)
if init == 0:
try:
init = 1
## execute click on NEXT by utilizing the xpath of NEXT
browser.execute_script("arguments[0].click();",
browser.find_element_by_xpath('//*[@id="pr-review-display"]/footer/div/aside/button'))
time.sleep(10 + random.randint(0,5))
except NoSuchElementException: ## No NEXT button present, less than 10 reviews
time.sleep(15 + random.randint(0,10))
break
else:
try:
## execute click on NEXT by utilizing the xpath of NEXT
## if you notice, the xpath of NEXT is different here since PREVIOUS button is also present now
browser.execute_script("arguments[0].click();",
browser.find_element_by_xpath('//*[@id="pr-review-display"]/footer/div/aside/button[2]'))
time.sleep(10 + random.randint(0,5))
except NoSuchElementException: ## No NEXT button, no more pages left
time.sleep(15 + random.randint(0,10))
break
## save the extracted data locally
np.save('./scraped_data_tops.npy',scraped_data)

有几件事需要注意:

我们在很多地方都做过异常处理。这些是在运行脚本时遇到问题时逐步添加的。

第30-97行负责将感兴趣的数据提取并解析为字典格式。通常,人们更喜欢将提取的数据存储在本地并离线解析,然而,由于我的笔记本电脑存储空间有限,我更喜欢在运行中进行解析。

Selenium在第99-119行中派上用场。由于URL不会在不同的评论页面之间更改,所以导航的惟一方法是模拟单击按钮。我们使用了NEXT按钮的xpath来做同样的事情。

XPath可用于导航XML文档中的元素和属性。要识别元素的xpath,转到inspect screen,右键单击HTML代码并复制xpath,如下图所示。

[caption id="attachment_39720" align="aligncenter" width="920"] 获取HTML元素XPath的方法;在本例中,为NEXT按钮[/caption]

这就完成了数据的提取和解析过程,之后我们的数据中的记录如下:
{
'bra size': '42',
'category': 'tops',
'cup size': 'd',
'fit': 'slightly small',
'height': '5ft 6in',
'hips': '46.0',
'item_id': '149377',
'length': 'just right',
'original_size': 'xxxl',
'product_sizes': {'l', 'm', 's', 'xl', 'xs', 'xxl', 'xxxl'},
'quality': '4rated 4 out of 5 stars',
'review_summary': 'I love love love this shi',
'review_text': "I love love love this shirt. I ordered up because it looked a little more fitted in the picture and I'm glad I did; if I had ordered my normal size it would probably have been snugger than I prefer. The material is good qualityit's semithick. And the design is just so hilariously cute! I'm going to see if this brand has other tees and order more.",
'shoe size': '8.00',
'shoe width': 'average',
'size': 16,
'user_name': 'erinmbundren'
}

从表面上看,我们的工作已经完成了。然而,构建最终的数据集还有几个步骤,我们下次继续学习。

 
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
写评论取消
回复取消