00241 NLP Course - Creating your own dataset


前言

有时,构建一个自然语言处理(NLP)应用程序所需的数据集并不存在,因此你需要自己创建它。在本节中,我们将向你展示如何创建一个GitHub问题的语料库,这些问题通常用于跟踪GitHub仓库中的错误或特性。这个语料库可以用于多种目的,包括:

  • 探索关闭开放问题或拉取请求所需的时间
  • 训练一个多标签分类器,根据问题的描述给问题打上元数据标签(例如,“错误”,“增强”或“问题”)
  • 创建一个语义搜索引擎,以找到与用户查询相匹配的问题

在这里,我们将重点创建语料库,在下一节中我们将处理语义搜索应用程序。为了保持元数据的一致性,我们将使用一个流行的开源项目:🤗 Datasets!的GitHub问题。让我们来看看如何获取数据并探索这些问题中包含的信息。

src link: https://huggingface.co/learn/nlp-course/chapter5/5

Operating System: Ubuntu 22.04.4 LTS

参考文档

  1. NLP Course - Creating your own dataset

获取数据

您可以通过导航到仓库的“问题”标签来找到🤗 Datasets中的所有问题。如下图所示,在撰写本文时,有331个待解决的问题和668个已解决的问题。

如果您点击这些问题中的一个,您会发现它包含一个标题、一个描述以及一组用于描述该问题的标签。下面截图展示了一个例子。

要下载仓库中的所有问题,我们将使用GitHub REST API来轮询问题端点。这个端点返回一个JSON对象列表,每个对象包含大量字段,包括标题和描述,以及关于问题状态等元数据。

下载问题的一种便捷方式是通过requests库,这是在Python中发起HTTP请求的标准方法。您可以通过运行以下命令来安装这个库:

!pip install requests

一旦安装了库,您可以通过调用requests.get()函数向问题端点发起GET请求。例如,您可以运行以下命令来获取第一页上的第一个问题:

import requests

url = "https://api.github.com/repos/huggingface/datasets/issues?page=1&per_page=1"
response = requests.get(url)

响应对象包含了关于请求的大量有用信息,其中包括HTTP状态码:

response.status_code
200

其中200状态码表示请求成功(您可以在 这里 找到可能的HTTP状态码列表)。然而,我们真正感兴趣的是有效载荷(payload),它可以以字节、字符串或JSON等不同格式访问。由于我们知道我们的问题是JSON格式的,让我们以下面的方式检查有效载荷:

response.json()
[{'url': 'https://api.github.com/repos/huggingface/datasets/issues/2792',
  'repository_url': 'https://api.github.com/repos/huggingface/datasets',
  'labels_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792/labels{/name}',
  'comments_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792/comments',
  'events_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792/events',
  'html_url': 'https://github.com/huggingface/datasets/pull/2792',
  'id': 968650274,
  'node_id': 'MDExOlB1bGxSZXF1ZXN0NzEwNzUyMjc0',
  'number': 2792,
  'title': 'Update GooAQ',
  'user': {'login': 'bhavitvyamalik',
   'id': 19718818,
   'node_id': 'MDQ6VXNlcjE5NzE4ODE4',
   'avatar_url': 'https://avatars.githubusercontent.com/u/19718818?v=4',
   'gravatar_id': '',
   'url': 'https://api.github.com/users/bhavitvyamalik',
   'html_url': 'https://github.com/bhavitvyamalik',
   'followers_url': 'https://api.github.com/users/bhavitvyamalik/followers',
   'following_url': 'https://api.github.com/users/bhavitvyamalik/following{/other_user}',
   'gists_url': 'https://api.github.com/users/bhavitvyamalik/gists{/gist_id}',
   'starred_url': 'https://api.github.com/users/bhavitvyamalik/starred{/owner}{/repo}',
   'subscriptions_url': 'https://api.github.com/users/bhavitvyamalik/subscriptions',
   'organizations_url': 'https://api.github.com/users/bhavitvyamalik/orgs',
   'repos_url': 'https://api.github.com/users/bhavitvyamalik/repos',
   'events_url': 'https://api.github.com/users/bhavitvyamalik/events{/privacy}',
   'received_events_url': 'https://api.github.com/users/bhavitvyamalik/received_events',
   'type': 'User',
   'site_admin': False},
  'labels': [],
  'state': 'open',
  'locked': False,
  'assignee': None,
  'assignees': [],
  'milestone': None,
  'comments': 1,
  'created_at': '2021-08-12T11:40:18Z',
  'updated_at': '2021-08-12T12:31:17Z',
  'closed_at': None,
  'author_association': 'CONTRIBUTOR',
  'active_lock_reason': None,
  'pull_request': {'url': 'https://api.github.com/repos/huggingface/datasets/pulls/2792',
   'html_url': 'https://github.com/huggingface/datasets/pull/2792',
   'diff_url': 'https://github.com/huggingface/datasets/pull/2792.diff',
   'patch_url': 'https://github.com/huggingface/datasets/pull/2792.patch'},
  'body': '[GooAQ](https://github.com/allenai/gooaq) dataset was recently updated after splits were added for the same. This PR contains new updated GooAQ with train/val/test splits and updated README as well.',
  'performed_via_github_app': None}]

哇,这里有大量的信息!我们可以看到像标题(title)、正文(body)和编号(number)这样的有用字段来描述问题,以及关于打开问题的GitHub用户的信息。

✏️ 尝试一下!点击上面JSON有效载荷中的几个URL,感受一下每个GitHub问题链接了哪些类型的信息。

正如GitHub文档所描述的,未经身份验证的请求每小时限制为60次。尽管您可以通过增加per_page查询参数来减少您发起的请求数量,但如果任何仓库的问题超过几千个,您仍然会达到速率限制。因此,您应该按照GitHub的说明创建一个个人访问令牌,这样您就可以将速率限制提高到每小时5000次请求。一旦您有了令牌,您可以将它作为请求头的一部分包含进去:

GITHUB_TOKEN = xxx  # Copy your GitHub token here
headers = {"Authorization": f"token {GITHUB_TOKEN}"}

⚠️ 不要与他人分享粘贴了GITHUB_TOKEN的笔记本。我们建议您在执行完最后一个单元格后删除它,以避免意外泄露这些信息。更好的做法是将令牌存储在一个.env文件中,并使用python-dotenv库自动将其作为环境变量加载。

现在我们有了访问令牌,让我们创建一个函数,它可以从一个GitHub仓库中下载所有问题:

import time
import math
from pathlib import Path
import pandas as pd
from tqdm.notebook import tqdm


def fetch_issues(
    owner="huggingface",
    repo="datasets",
    num_issues=10_000,
    rate_limit=5_000,
    issues_path=Path("."),
):
    if not issues_path.is_dir():
        issues_path.mkdir(exist_ok=True)

    batch = []
    all_issues = []
    per_page = 100  # Number of issues to return per page
    num_pages = math.ceil(num_issues / per_page)
    base_url = "https://api.github.com/repos"

    for page in tqdm(range(num_pages)):
        # Query with state=all to get both open and closed issues
        query = f"issues?page={page}&per_page={per_page}&state=all"
        issues = requests.get(f"{base_url}/{owner}/{repo}/{query}", headers=headers)
        batch.extend(issues.json())

        if len(batch) > rate_limit and len(all_issues) < num_issues:
            all_issues.extend(batch)
            batch = []  # Flush batch for next time period
            print(f"Reached GitHub rate limit. Sleeping for one hour ...")
            time.sleep(60 * 60 + 1)

    all_issues.extend(batch)
    df = pd.DataFrame.from_records(all_issues)
    df.to_json(f"{issues_path}/{repo}-issues.jsonl", orient="records", lines=True)
    print(
        f"Downloaded all the issues for {repo}! Dataset stored at {issues_path}/{repo}-issues.jsonl"
    )

现在当我们调用fetch_issues()时,它将分批下载所有问题,以避免超过GitHub每小时请求次数的限制;结果将存储在一个名为repository_name-issues.jsonl的文件中,其中每一行都是一个代表问题的JSON对象。让我们使用这个函数从🤗 Datasets中抓取所有问题:

# Depending on your internet connection, this can take several minutes to run...
fetch_issues()

一旦问题被下载,我们可以使用第二部分中学到的新技能在本地加载它们:

issues_dataset = load_dataset("json", data_files="datasets-issues.jsonl", split="train")
issues_dataset
Dataset({
    features: ['url', 'repository_url', 'labels_url', 'comments_url', 'events_url', 'html_url', 'id', 'node_id', 'number', 'title', 'user', 'labels', 'state', 'locked', 'assignee', 'assignees', 'milestone', 'comments', 'created_at', 'updated_at', 'closed_at', 'author_association', 'active_lock_reason', 'pull_request', 'body', 'timeline_url', 'performed_via_github_app'],
    num_rows: 3019
})

太棒了,我们从头开始创建了我们的第一个数据集!但是为什么会有几千个问题,而🤗 Datasets仓库的问题标签页总共只显示大约1,000个问题呢?正如GitHub文档所描述的,那是因为我们还下载了所有的拉取请求(pull requests):

“GitHub’s REST API v3 considers every pull request an issue, but not every issue is a pull request. For this reason, “Issues” endpoints may return both issues and pull requests in the response. You can identify pull requests by the pull_request key. Be aware that the id of a pull request returned from “Issues” endpoints will be an issue id.”

由于问题和拉取请求的内容相当不同,让我们进行一些轻微的预处理,以便能够区分它们。

清理数据

GitHub文档中的上述片段告诉我们,可以使用pull_request列来区分问题和拉取请求。让我们看看一个随机样本,看看区别在哪里。就像我们在第3部分所做的那样,我们将使用Dataset.shuffle()和Dataset.select()来创建一个随机样本,然后将html_url和pull_request列进行打包,这样我们就可以比较各种URL了:

sample = issues_dataset.shuffle(seed=666).select(range(3))

# Print out the URL and pull request entries
for url, pr in zip(sample["html_url"], sample["pull_request"]):
    print(f">> URL: {url}")
    print(f">> Pull request: {pr}\n")
>> URL: https://github.com/huggingface/datasets/pull/850
>> Pull request: {'url': 'https://api.github.com/repos/huggingface/datasets/pulls/850', 'html_url': 'https://github.com/huggingface/datasets/pull/850', 'diff_url': 'https://github.com/huggingface/datasets/pull/850.diff', 'patch_url': 'https://github.com/huggingface/datasets/pull/850.patch'}

>> URL: https://github.com/huggingface/datasets/issues/2773
>> Pull request: None

>> URL: https://github.com/huggingface/datasets/pull/783
>> Pull request: {'url': 'https://api.github.com/repos/huggingface/datasets/pulls/783', 'html_url': 'https://github.com/huggingface/datasets/pull/783', 'diff_url': 'https://github.com/huggingface/datasets/pull/783.diff', 'patch_url': 'https://github.com/huggingface/datasets/pull/783.patch'}

在这里我们可以看到,每个拉取请求都与各种URL相关联,而普通问题则有None条目。我们可以使用这种区别来创建一个新的is_pull_request列,该列检查pull_request字段是否为None:

issues_dataset = issues_dataset.map(
    lambda x: {"is_pull_request": False if x["pull_request"] is None else True}
)

✏️ 尝试一下!计算🤗 Datasets中关闭问题所需的平均时间。您可能会发现Dataset.filter()函数很有用,可以用来过滤掉拉取请求和未解决的问题。您可以使用Dataset.set_format()函数将数据集转换为DataFrame,这样您可以更容易地操作created_at和closed_at时间戳。为了获得额外积分,计算关闭拉取请求所需的平均时间。

虽然我们可以通过删除或重命名一些列来进一步清理数据集,但在这个阶段,通常一个好做法是保持数据集尽可能“原始”,这样它可以更容易地在多个应用中使用。

在我们将数据集推送到 Hugging Face Hub 之前,让我们先处理数据集中缺失的一件事:与每个问题和拉取请求相关联的评论。接下来我们将使用 GitHub REST API 添加它们!

增强数据集

如以下屏幕截图所示,与问题或拉取请求相关联的评论提供了丰富的信息来源,特别是如果我们想构建一个搜索引擎来回答用户关于库的查询。

GitHub REST API 提供了一个评论端点(Comments endpoint),用于返回与一个特定问题编号相关联的所有评论。让我们测试一下这个端点,看看它会返回什么内容。

issue_number = 2792
url = f"https://api.github.com/repos/huggingface/datasets/issues/{issue_number}/comments"
response = requests.get(url, headers=headers)
response.json()
[{'url': 'https://api.github.com/repos/huggingface/datasets/issues/comments/897594128',
  'html_url': 'https://github.com/huggingface/datasets/pull/2792#issuecomment-897594128',
  'issue_url': 'https://api.github.com/repos/huggingface/datasets/issues/2792',
  'id': 897594128,
  'node_id': 'IC_kwDODunzps41gDMQ',
  'user': {'login': 'bhavitvyamalik',
   'id': 19718818,
   'node_id': 'MDQ6VXNlcjE5NzE4ODE4',
   'avatar_url': 'https://avatars.githubusercontent.com/u/19718818?v=4',
   'gravatar_id': '',
   'url': 'https://api.github.com/users/bhavitvyamalik',
   'html_url': 'https://github.com/bhavitvyamalik',
   'followers_url': 'https://api.github.com/users/bhavitvyamalik/followers',
   'following_url': 'https://api.github.com/users/bhavitvyamalik/following{/other_user}',
   'gists_url': 'https://api.github.com/users/bhavitvyamalik/gists{/gist_id}',
   'starred_url': 'https://api.github.com/users/bhavitvyamalik/starred{/owner}{/repo}',
   'subscriptions_url': 'https://api.github.com/users/bhavitvyamalik/subscriptions',
   'organizations_url': 'https://api.github.com/users/bhavitvyamalik/orgs',
   'repos_url': 'https://api.github.com/users/bhavitvyamalik/repos',
   'events_url': 'https://api.github.com/users/bhavitvyamalik/events{/privacy}',
   'received_events_url': 'https://api.github.com/users/bhavitvyamalik/received_events',
   'type': 'User',
   'site_admin': False},
  'created_at': '2021-08-12T12:21:52Z',
  'updated_at': '2021-08-12T12:31:17Z',
  'author_association': 'CONTRIBUTOR',
  'body': "@albertvillanova my tests are failing here:\r\n```\r\ndataset_name = 'gooaq'\r\n\r\n    def test_load_dataset(self, dataset_name):\r\n        configs = self.dataset_tester.load_all_configs(dataset_name, is_local=True)[:1]\r\n>       self.dataset_tester.check_load_dataset(dataset_name, configs, is_local=True, use_local_dummy_data=True)\r\n\r\ntests/test_dataset_common.py:234: \r\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \r\ntests/test_dataset_common.py:187: in check_load_dataset\r\n    self.parent.assertTrue(len(dataset[split]) > 0)\r\nE   AssertionError: False is not true\r\n```\r\nWhen I try loading dataset on local machine it works fine. Any suggestions on how can I avoid this error?",
  'performed_via_github_app': None}]

我们可以看到评论存储在 body 字段中,所以让我们编写一个简单的函数,通过提取 response.json() 中每个元素的正文内容,返回与一个问题相关联的所有评论。

def get_comments(issue_number):
    url = f"https://api.github.com/repos/huggingface/datasets/issues/{issue_number}/comments"
    response = requests.get(url, headers=headers)
    return [r["body"] for r in response.json()]


# Test our function works as expected
get_comments(2792)
["@albertvillanova my tests are failing here:\r\n```\r\ndataset_name = 'gooaq'\r\n\r\n    def test_load_dataset(self, dataset_name):\r\n        configs = self.dataset_tester.load_all_configs(dataset_name, is_local=True)[:1]\r\n>       self.dataset_tester.check_load_dataset(dataset_name, configs, is_local=True, use_local_dummy_data=True)\r\n\r\ntests/test_dataset_common.py:234: \r\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \r\ntests/test_dataset_common.py:187: in check_load_dataset\r\n    self.parent.assertTrue(len(dataset[split]) > 0)\r\nE   AssertionError: False is not true\r\n```\r\nWhen I try loading dataset on local machine it works fine. Any suggestions on how can I avoid this error?"]

这看起来不错,所以让我们使用 Dataset.map() 方法在我们的数据集中为每个问题添加一个新的 comments 列。

# Depending on your internet connection, this can take a few minutes...
issues_with_comments_dataset = issues_dataset.map(
    lambda x: {"comments": get_comments(x["number"])}
)

最后一步是将我们的数据集推送到 Hub。让我们来看看如何做到这一点。

将数据集上传到 Hugging Face Hub

现在我们有了增强后的数据集,是时候将其推送到 Hub 了,这样我们就可以与社区共享了!上传数据集非常简单:就像来自 🤗 Transformers 的模型和分词器一样,我们可以使用 push_to_hub() 方法来推送一个数据集。要做到这一点,我们需要一个认证令牌,可以通过首先使用 notebook_login() 函数登录 Hugging Face Hub 来获得:

from huggingface_hub import notebook_login

notebook_login()

这将创建一个窗口小部件,你可以输入你的用户名和密码,并且一个 API 令牌将被保存在 ~/.huggingface/token。如果你在终端中运行代码,你也可以通过 CLI 登录:

huggingface-cli login

一旦我们完成了这个步骤,我们可以通过运行以下命令来上传我们的数据集:

issues_with_comments_dataset.push_to_hub("github-issues")

从这里,任何人都可以通过简单地使用存储库 ID 作为路径参数提供给 load_dataset() 函数来下载数据集:

remote_dataset = load_dataset("lewtun/github-issues", split="train")
remote_dataset
Dataset({
    features: ['url', 'repository_url', 'labels_url', 'comments_url', 'events_url', 'html_url', 'id', 'node_id', 'number', 'title', 'user', 'labels', 'state', 'locked', 'assignee', 'assignees', 'milestone', 'comments', 'created_at', 'updated_at', 'closed_at', 'author_association', 'active_lock_reason', 'pull_request', 'body', 'performed_via_github_app', 'is_pull_request'],
    num_rows: 2855
})

太棒了,我们已经将我们的数据集推送到 Hub,其他人现在可以使用它了!还有一件重要的事情要做:添加一个数据集卡片,解释语料库是如何创建的,并为社区提供其他有用的信息。

💡 你也可以使用 huggingface-cli 和一些 Git 技巧直接从终端将数据集上传到 Hugging Face Hub。有关如何做到这一点的详细信息,请参阅 🤗 Datasets 指南

创建一个数据集卡片

文档齐全的数据集对其他人(包括未来的你!)更有可能有用,因为它们提供了上下文,使用户能够决定数据集是否与他们的任务相关,并评估使用数据集可能存在的任何潜在偏见或风险。

在 Hugging Face Hub 上,这些信息存储在每个数据集仓库的 README.md 文件中。在创建这个文件之前,你应该采取两个主要步骤:

  1. 使用 datasets-tagging 应用程序以 YAML 格式创建元数据标签。这些标签用于 Hugging Face Hub 上的各种搜索功能,确保社区成员可以轻松找到你的数据集。由于我们在这里创建了一个自定义数据集,你需要克隆 datasets-tagging 仓库并在本地运行应用程序。这是界面的样子:

  1. 阅读 🤗 Datasets 关于创建信息丰富的数据集卡片的指南,并使用它作为模板。

你可以在Hub上直接创建README.md文件,你可以在lewtun/github-issues数据集仓库中找到一个模板数据集卡片。下面是填写好的数据集卡片的截图。

✏️ 尝试一下!使用数据集标记应用程序和🤗 Datasets指南来完成你的GitHub问题数据集的README.md文件。

这就是了!我们在本节中看到,创建一个好的数据集可能相当复杂,但幸运的是,上传它并与社区分享却不是。在下一节中,我们将使用我们的新数据集创建一个语义搜索引擎,利用🤗 Datasets将问题与最相关的问题和评论相匹配。

✏️ 尝试一下!按照我们在本节中采取的步骤,为你的最喜欢的开源库创建一个GitHub问题数据集(当然,选择除了🤗 Datasets之外的东西!)。为了获得额外分数,微调一个多标签分类器来预测标签字段中存在的标签。

结语

第二百四十一篇博文写完,开心!!!!

今天,也是充满希望的一天。


文章作者: LuYF-Lemon-love
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LuYF-Lemon-love !
  目录