NLP 课程文档

创建自己的数据集

Hugging Face's logo
加入 Hugging Face 社区

并获得增强文档体验的访问权限

开始

创建自己的数据集

Ask a Question Open In Colab Open In Studio Lab

有时,您需要构建 NLP 应用程序的数据集不存在,因此您需要自己创建它。在本节中,我们将向您展示如何创建一个 GitHub 问题 语料库,这些问题通常用于跟踪 GitHub 存储库中的错误或功能。此语料库可用于各种目的,包括

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

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

获取数据

您可以通过导航到存储库的“问题”选项卡来查找 🤗 Datasets 中的所有问题。如下面的屏幕截图所示,在撰写本文时,有 331 个打开的问题和 668 个关闭的问题。

The GitHub issues associated with 🤗 Datasets.

如果您点击其中一个问题,您会发现它包含标题、描述和一组表征问题的标签。以下屏幕截图显示了一个示例。

A typical GitHub issue in the 🤗 Datasets repository.

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

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

!pip install requests

安装库后,您可以通过调用requests.get() 函数向Issues 端点发出 GET 请求。例如,您可以运行以下命令来检索第一页上的第一个问题

import requests

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

response 对象包含许多关于请求的有用信息,包括 HTTP 状态代码

response.status_code
200

其中200 状态表示请求成功(您可以在此处找到可能的 HTTP 状态代码列表)。但是,我们真正感兴趣的是有效负载,它可以以各种格式访问,例如字节、字符串或 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}]

哇,信息量很大!我们可以看到有用的字段,如titlebodynumber,它们描述了该问题,以及关于打开该问题的 GitHub 用户的信息。

✏️ 试一试!点击上面 JSON 有效负载中的几个 URL,以了解每个 GitHub 问题链接到的信息类型。

如 GitHub文档中所述,未经身份验证的请求每小时限于 60 个请求。尽管您可以增加per_page 查询参数以减少发出的请求数量,但您仍然会在任何拥有超过几千个问题的存储库上达到速率限制。因此,您应该按照 GitHub 的说明创建个人访问令牌,以便您可以将速率限制提高到每小时 5,000 个请求。获得令牌后,您可以将其作为请求标头的一部分包含在内

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()

下载问题后,我们可以使用我们在第 2 节中学到的新技能在本地加载它们

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文档中所述,这是因为我们也下载了所有拉取请求

GitHub 的 REST API v3 将每个拉取请求视为一个问题,但并非每个问题都是拉取请求。“问题”端点可能会在响应中返回问题和拉取请求。您可以通过pull_request 键识别拉取请求。请注意,从“问题”端点返回的拉取请求的id 将是问题 ID。

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

清理数据

GitHub 文档中的以上代码片段告诉我们,pull_request 列可用于区分问题和拉取请求。让我们查看一个随机样本以了解差异。正如我们在第 3 节中所做的那样,我们将Dataset.shuffle()Dataset.select() 连接起来以创建一个随机样本,然后压缩html_urlpull_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'}

在这里我们可以看到每个 Pull Request 都与各种 URL 相关联,而普通 Issue 则有一个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 中关闭 Issue 的平均时间。你可能会发现Dataset.filter()函数对于过滤掉 Pull Request 和未关闭的 Issue 很有用,并且可以使用Dataset.set_format()函数将数据集转换为DataFrame,以便你可以轻松地操作created_atclosed_at时间戳。作为奖励,计算关闭 Pull Request 的平均时间。

虽然我们可以继续清理数据集,例如删除或重命名一些列,但在这一阶段,通常最好保持数据集尽可能“原始”,以便它可以轻松地用于多个应用程序。

在我们把数据集推送到 Hugging Face Hub 之前,让我们处理一个缺失的部分:与每个 Issue 和 Pull Request 相关的评论。接下来我们将使用——你猜对了——GitHub REST API 添加它们!

扩充数据集

如下面的截图所示,与 Issue 或 Pull Request 相关的评论提供了丰富的的信息来源,特别是如果我们有兴趣构建一个搜索引擎来回答用户关于库的查询。

Comments associated with an issue about 🤗 Datasets.

GitHub REST API 提供了一个Comments端点,它返回与 Issue 编号相关联的所有评论。让我们测试一下该端点以查看它返回什么。

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()中每个元素的body内容来返回与 Issue 相关联的所有评论。

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()为数据集中每个 Issue 添加一个新的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")

从此处,任何人都可以通过简单地向load_dataset()提供存储库 ID 作为path参数来下载数据集。

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存储库并在本地运行该应用程序。以下是界面的样子。
The 'datasets-tagging' interface.
  1. 阅读 🤗 Datasets 指南,了解如何创建信息丰富的 dataset card 并将其用作模板。

您可以在 Hub 上直接创建README.md文件,并且可以在lewtun/github-issues数据集存储库中找到一个模板数据集卡片。下面显示了填写后的数据集卡片的屏幕截图。

A dataset card.

✏️ 试试看!使用dataset-tagging应用程序和 🤗 Datasets 指南完成 GitHub Issue 数据集的README.md文件。

就是这样!我们在本节中了解到,创建良好的数据集可能需要相当多的工作,但幸运的是,上传和与社区共享它并不困难。在下一节中,我们将使用我们新的数据集来使用 🤗 Datasets 创建一个语义搜索引擎,该引擎可以将问题与最相关的 Issue 和评论匹配。

✏️ 试试看!按照我们在本节中采取的步骤,为您最喜欢的开源库创建 GitHub Issue 数据集(当然,选择 🤗 Datasets 之外的其他库)。作为奖励,微调一个多标签分类器来预测labels字段中存在的标签。