创建您自己的数据集
有时,构建 NLP 应用程序所需的数据集并不存在,因此您需要自己创建它。在本节中,我们将向您展示如何创建 GitHub 问题 语料库,这些问题通常用于跟踪 GitHub 存储库中的错误或功能。此语料库可用于各种目的,包括
- 探索关闭开放问题或拉取请求所需的时间
- 训练一个可以根据问题描述(例如,“错误”、“增强”或“问题”)对问题进行元数据标记的 *多标签分类器*
- 创建语义搜索引擎以查找与用户查询匹配的问题
这里我们将重点介绍创建语料库,在下一节中,我们将解决语义搜索应用程序。为了保持一致性,我们将使用与流行的开源项目相关的 GitHub 问题:🤗 数据集!让我们看看如何获取数据并探索这些问题中包含的信息。
获取数据
您可以通过导航到存储库的 问题标签 来找到 🤗 数据集中的所有问题。如以下屏幕截图所示,在撰写本文时,有 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)
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}]
哇,这真是很多信息!我们可以看到有用的字段,例如 title
、body
和 number
,它们描述了问题,以及有关打开问题的人的信息。
✏️ **试试看!** 点击上面 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 对象。让我们使用此函数来获取 🤗 数据集的所有问题
# 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
})
太棒了,我们从头开始创建了第一个数据集!但是,为什么有几千个问题,而 🤗 数据集存储库的 问题标签 总共只显示了大约 1,000 个问题 🤔?如 GitHub 文档 所述,这是因为我们也下载了所有拉取请求
GitHub 的 REST API v3 将每个拉取请求都视为一个问题,但并非每个问题都是拉取请求。因此,“问题”端点可能会在响应中返回问题和拉取请求。您可以通过
pull_request
键来识别拉取请求。请注意,从“问题”端点返回的拉取请求的id
将是一个问题 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}
)
✏️ **试试看!** 计算关闭 🤗 数据集问题所需时间的平均值。您可能会发现 Dataset.filter()
函数对过滤掉拉取请求和开放问题很有用,并且可以使用 Dataset.set_format()
函数将数据集转换为 DataFrame
,以便您可以轻松地操作 created_at
和 closed_at
时间戳。为获得额外加分,请计算关闭拉取请求所需时间的平均值。
虽然我们可以通过删除或重命名某些列来进一步清理数据集,但通常的做法是在此阶段尽可能保持数据集的“原始”状态,以便它可以轻松地用于多个应用程序。
在我们把数据集推送到 Hugging Face Hub 之前,让我们处理一个它缺少的东西:与每个问题和拉取请求相关的评论。我们将在下一步添加它们——你猜对了——使用 GitHub REST API!
扩充数据集
如以下屏幕截图所示,与问题或拉取请求相关的评论提供了丰富的 信息来源,特别是如果我们有兴趣构建一个搜索引擎来回答用户关于库的查询。
GitHub REST API 提供了一个 Comments
端点,它返回与问题编号相关的所有评论。让我们测试一下端点,看看它返回了什么。
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
内容来返回与问题相关的所有评论。
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")
从这里,任何人都可以通过简单地向 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。有关如何执行此操作的详细信息,请参阅 🤗 数据集指南。
创建数据集卡片
记录良好的数据集更有可能对其他人(包括未来的自己)有用,因为它们提供了上下文,使用户能够决定数据集是否与其任务相关,并评估使用数据集时可能存在的任何偏差或风险。
在 Hugging Face Hub 上,此信息存储在每个数据集存储库的 README.md 文件中。在创建此文件之前,您应该执行两个主要步骤。
- 使用
datasets-tagging
应用程序 以 YAML 格式创建元数据标签。这些标签用于 Hugging Face Hub 上的各种搜索功能,并确保您的数据集可以轻松地被社区成员找到。由于我们在这里创建了一个自定义数据集,您需要克隆datasets-tagging
存储库并在本地运行该应用程序。以下是界面的样子。
- 阅读 🤗 数据集指南,了解如何创建信息丰富的 数据集卡片,并将其用作模板。
您可以在 Hub 上直接创建 README.md 文件,您可以在 lewtun/github-issues
数据集存储库中找到一个模板数据集卡片。以下是一张填写完整的数据集卡片的屏幕截图。
✏️ 试试吧!使用 dataset-tagging
应用程序和 🤗 数据集指南 来完成 GitHub 问题数据集的 README.md 文件。
就是这样!我们在本节中看到,创建一个好的数据集可能相当复杂,但幸运的是,上传它并与社区分享并不难。在下一节中,我们将使用我们的新数据集使用 🤗 Datasets 创建一个语义搜索引擎,它可以将问题与最相关的问题和评论进行匹配。
✏️ 试试吧!按照我们在本节中执行的步骤,为您最喜欢的开源库创建一个 GitHub 问题数据集(当然,选择 🤗 Datasets 以外的库)。作为额外奖励,微调一个多标签分类器来预测 labels
字段中存在的标签。