Hugging Face 数据集加载机制,自定义数据集的方法和注意事项 全面整合(含代码和细节解释)


1. 数据集的默认加载机制

Hugging Face 的 datasets 支持公开数据集和本地数据集。默认机制包括以下几种情况:

(1) 加载公开数据集
  • 公开数据集(如 IMDB、GLUE)通常预定义了子集(trainvalidationtest)。
  • 可以通过 split 参数选择加载特定的子集。

代码示例

from datasets import load_dataset

# 加载公开数据集(IMDB)
dataset = load_dataset("imdb")

# 查看数据集的子集
print(dataset.keys())  # 输出:dict_keys(['train', 'test'])

# 加载训练集
train_data = load_dataset("imdb", split="train")
print(train_data[:5])  # 查看训练集前 5 条记录

(2) 加载本地数据集
  • 单文件:默认加载为 train 子集。
  • 多文件:通过 data_files 参数指定多个文件,同时自定义子集名称。

代码示例:单文件

dataset = load_dataset("csv", data_files="data.csv")
print(dataset.keys())  # 输出:dict_keys(['train'])
print(dataset["train"][:5])  # 查看数据内容

代码示例:多文件

data_files = {
    "train": "train.csv",
    "validation": "validation.csv",
    "test": "test.csv"
}
dataset = load_dataset("csv", data_files=data_files)
print(dataset.keys())  # 输出:dict_keys(['train', 'validation', 'test'])
print(dataset["train"][:5])  # 查看训练集内容

2. 数据集的自定义加载与子集命名

(1) 自定义单文件的子集名称

通过指定 data_files 的键名,为单文件加载的默认子集重新命名。

代码示例

dataset = load_dataset("csv", data_files={"custom_subset": "data.csv"})
print(dataset.keys())  # 输出:dict_keys(['custom_subset'])
print(dataset["custom_subset"][:5])  # 查看自定义子集内容

(2) 多文件加载直接定义子集名称

可以通过 data_files 指定每个文件对应的子集名称。

代码示例

data_files = {
    "deck22": "file1.csv",
    "deck23": "file2.csv"
}
dataset = load_dataset("csv", data_files=data_files)
print(dataset.keys())  # 输出:dict_keys(['deck22', 'deck23'])
print(dataset["deck22"][:5])  # 查看 deck22 子集内容

(3) 通过手动划分创建自定义子集

当加载的文件只有一个时,可以手动划分子集,并自定义子集名称。

代码示例

from datasets import load_dataset

# 加载单文件
dataset = load_dataset("csv", data_files="data.csv")["train"]

# 按比例划分为 train 和 custom_subset
split = dataset.train_test_split(test_size=0.3)
custom_splits = {
    "train": split["train"],
    "custom_subset": split["test"]
}

print(custom_splits.keys())  # 输出:dict_keys(['train', 'custom_subset'])
print(custom_splits["custom_subset"][:5])  # 查看自定义子集内容

3. 嵌套数据的加载与处理

(1) 加载嵌套 JSON 数据

如果数据文件是嵌套 JSON,可以通过 field 参数指定加载的层级。

示例 JSON 数据

{
  "user": {
    "id": 1,
    "profile": {"name": "Alice", "age": 25},
    "posts": [{"title": "First Post", "content": "Hello World"}]
  },
  "metadata": {"source": "app", "timestamp": "2023-12-25"}
}

代码示例

dataset = load_dataset("json", data_files="nested.json")

# 查看顶层键
print(dataset["train"].column_names)  # 输出:['user', 'metadata']

# 加载特定字段(如 user)
user_data = load_dataset("json", data_files="nested.json", field="user")
print(user_data["train"][:5])  # 查看 user 部分内容
(2) 扁平化嵌套数据

如果嵌套层次过深,可以将数据扁平化以便后续处理。

代码示例

def flatten(record):
    return {
        "user_id": record["user"]["id"],
        "user_name": record["user"]["profile"]["name"],
        "user_age": record["user"]["profile"]["age"],
        "first_post_title": record["user"]["posts"][0]["title"]
    }

# 扁平化数据
flattened_dataset = dataset["train"].map(flatten)
print(flattened_dataset[:5])  # 查看扁平化后的数据

4. 按比例划分与数据检查

(1) 按比例划分子集

train_test_split 可以轻松按比例划分数据集,并生成新的子集。

代码示例

split = dataset["train"].train_test_split(test_size=0.2)

print(split.keys())  # 输出:dict_keys(['train', 'test'])
print(len(split["train"]), len(split["test"]))  # 查看每个子集的样本数量
(2) 检查数据集的结构和内容

通过以下方法快速检查数据集的结构:

  • 列名
    print(dataset["train"].column_names)  # 查看列名
    
  • 数据类型
    print(dataset["train"].features)  # 查看每列数据的类型
    
  • 样本内容
    print(dataset["train"][:5])  # 查看前 5 条记录
    

5. 数据保存与重新加载

(1) 保存处理后的数据集

处理后的数据集可以保存到本地:

processed_dataset = dataset["train"].map(lambda x: {"new_column": x["column1"] * 2})
processed_dataset.save_to_disk("processed_dataset")
(2) 重新加载保存的数据集
from datasets import load_from_disk

loaded_dataset = load_from_disk("processed_dataset")
print(loaded_dataset[:5])  # 查看重新加载的数据

6. 整合示例:多文件加载与自定义子集

以下是一个完整的加载和处理流程,涵盖多文件加载、自定义子集划分与保存操作:

from datasets import load_dataset

# Step 1: 加载多个文件并自定义子集名称
data_files = {
    "train": "train.csv",
    "validation": "validation.csv",
    "test": "test.csv"
}
dataset = load_dataset("csv", data_files=data_files)

# Step 2: 按比例划分 train 子集
split = dataset["train"].train_test_split(test_size=0.2)
dataset["train"] = split["train"]
dataset["extra_validation"] = split["test"]

# Step 3: 检查数据集内容
print(dataset.keys())  # 输出:dict_keys(['train', 'validation', 'test', 'extra_validation'])

# Step 4: 保存处理后的数据集
dataset["extra_validation"].save_to_disk("extra_validation")

总结

  1. 单文件数据集默认加载为 train,可通过手动划分和重命名实现自定义子集。
  2. 多文件方式可以直接通过 data_files 定义子集名称,简化操作。
  3. 嵌套数据可以通过 field 加载指定层级,并通过扁平化方法处理复杂结构。


踩坑记录:

1. 假设我们想把一个文件分给多个子集,这里非常麻烦

  • 初次划分后,子集名称为 traindec25
  • 第二次划分,dec25 被拆分为两个子集。
  • 我们手动给新划分的子集命名为 validationtest,但原名称 dec25 本身不会被修改。
  1. 初始数据集加载后,只有一个子集,默认名为 train
  2. 我们先将 train 按比例划分为 traindec25
  3. 再将 dec25 进一步划分为 dec25_validationtest

修订后的代码示例

from datasets import load_dataset

# Step 1: 加载整个 CSV 文件
dataset = load_dataset('csv', data_files='dataset.csv')['train']  # 默认所有数据归为 train

# Step 2: 按比例划分为 train 和 dec25
train_dec25_split = dataset.train_test_split(test_size=0.3)  # 按 7:3 分为 train 和 dec25
train_dec25 = {
    'train': train_dec25_split['train'],  # 保留 70% 数据作为 train
    'dec25': train_dec25_split['test']   # 剩下 30% 数据作为 dec25
}

# Step 3: 将 dec25 再划分为 dec25_validation 和 test
dec25_split = train_dec25['dec25'].train_test_split(test_size=0.5)  # 按 1:1 再分
final_dataset = {
    'train': train_dec25['train'],               # 原来的 train 保留不变
    'dec25_validation': dec25_split['train'],    # dec25 的一部分作为 validation
    'test': dec25_split['test']                  # dec25 的另一部分作为 test
}

# Step 4: 查看最终划分结果
print(final_dataset.keys())  # 输出:dict_keys(['train', 'dec25_validation', 'test'])

# 查看每个子集的样本数量
for subset_name, subset_data in final_dataset.items():
    print(f"{subset_name} 子集包含 {len(subset_data)} 条样本")

子集名称的变化与来源

  1. 初次划分

    • train_test_split 返回的子集名称默认是 traintest
    • 我们手动将 test 子集重新命名为 dec25,表示临时划分出的验证集。
  2. 再次划分

    • dec25 执行 train_test_split,默认生成新的 traintest 子集。
    • 我们重新命名:
      • 原来的 train 改为 dec25_validation
      • 原来的 test 保留为 test

示例数据与划分比例

假设初始数据集有 1000 条记录,划分过程如下:

(1) 初次划分
  • train_dec25_split['train']:占总数据的 70%,即 700 条。
  • train_dec25_split['test'](重命名为 dec25):占总数据的 30%,即 300 条。
(2) 第二次划分
  • dec25_split['train'](重命名为 dec25_validation):占 dec25 的 50%,即 150 条。
  • dec25_split['test'](保持为 test):占 dec25 的 50%,即 150 条。

最终数据集分布:

  • train: 700 条
  • dec25_validation: 150 条
  • test: 150 条

规律总结

  1. 命名逻辑

    • 初次划分:默认子集名称是 traintest,可以手动将 test 改为其他名称(如 dec25)。
    • 再次划分:新生成的子集名称仍是 traintest,需要根据具体用途重新命名。
  2. 索引值的变化

    • 每次执行 train_test_split,默认生成两个子集:
      • train: 保留的数据部分。
      • test: 新划分出的小部分数据。
    • 如果子集继续划分,索引值也会沿用 traintest
  3. 避免名称混淆

    • 在多次划分时,及时重命名子集(如 dec25_validationtest),确保不同阶段的子集名称清晰、唯一。

    • 代码中的 test_valid['test'] 可能会让人误解为原来的 dec25 被覆盖。需要明确说明:每次执行 train_test_split 都会生成新的子集对象,原子集不会被覆盖。


2. Split = {} 而不是 Split = [ ]

在你的代码中,load_dataset函数被用来加载数据集,但由于使用了参数split=["train[:1%]", "test[-1%:]"],导致返回的对象是一个列表(list)而不是通常的DatasetDict类型。下面详细解释这一现象的原因,以及如何正确理解和使用这段代码。


核心机制:

  • split 参数的默认行为是根据预定义的分割(如traintestvalidation)生成一个DatasetDict
  • 但当传入列表时,它认为用户想要的结果是“多个分割片段的集合”,因此返回list
from datasets import load_dataset

# 加载TSV格式的文件作为数据集
data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}  # 定义训练和测试文件
# 加载数据集并指定分割
ds = load_dataset(
    "csv",  # 数据集的格式为CSV(这里也兼容TSV)
    data_files=data_files,  # 指定文件路径
    delimiter="\t",  # TSV文件以制表符分隔
    split=["train[:1%]", "test[-1%:]"]  # 只取训练集的前1%和测试集的后1%
)

# 查看加载后的数据类型和结构
print(type(ds))  # <class 'list'>
print(len(ds))   # 2
print(ds[0])     # 第一部分数据,属于训练集切片
print(ds[1])     # 第二部分数据,属于测试集切片
  1. 变量含义:
    • data_files: 包含训练文件和测试文件的路径。
    • delimiter="\t": 指定文件的分隔符为制表符。
    • split=["train[:1%]", "test[-1%:]"]: 将训练集取前1%,测试集取后1%。
    • ds: 加载后的数据,类型是list,包含两个分割。
  2. 返回结果:
    • ds[0] 是训练集的切片(前1%)。
    • ds[1] 是测试集的切片(后1%)。

如何使结果是 DatasetDict

需要通过dict的方式命名 split={"train": "train[:1%]", "test": "test[-1%:]"}: 指定分割后命名为traintest

# 修改split参数,使其返回DatasetDict
ds = load_dataset(
    "csv", 
    data_files=data_files, 
    delimiter="\t", 
    split={"train": "train[:1%]", "test": "test[-1%:]"}
)

# 查看类型和内容
print(type(ds))  # <class 'datasets.dataset_dict.DatasetDict'>
print(ds.keys())  # dict_keys(['train', 'test'])
print(ds["train"])  # 训练集切片
print(ds["test"])   # 测试集切片


3. load_from_disk 是用来加载 Hugging Face 数据集目录的(例如 .arrow 格式的文件夹),而不能直接加载普通的文件格式(如 .csv.tsv

  1. load_dataset

    • 用于加载普通文件格式(如 csvjsontsv 等)。
    • 需要指定文件类型,例如 "csv"
  2. load_from_disk

    • 用于加载 Hugging Face 本地保存的数据集文件夹。
    • 文件夹中通常包含 .arrow 文件以及 dataset_info.json 等元数据文件。

4. Map 和 Filter可以直接应用到总集但是访问不行

 filtermap 的语法和调用

  • filtermap 的作用
    • filter:筛选符合条件的样本。
    • map:对每条样本或批量样本进行映射操作。
  • 调用方式
    • 对总数据集(DatasetDict)操作:直接调用 drug_dataset.filter()drug_dataset.map() 会对所有子数据集(如 traintest)同时应用操作。
    • 对单个子数据集(Dataset)操作:必须先通过键(如 "train")获取子数据集,再调用对应的方法,如 drug_dataset["train"].filter()
  • DatasetDictDataset 的区别
    • DatasetDict 是一个字典结构,包含多个子数据集(键是子数据集名称,如 "train",值是具体的 Dataset 对象)。
    • Dataset 是具体的单个数据集,具有行和列的概念。
  • 访问规则
    • 要访问列数据(如 "review"),必须先进入具体的子数据集(如 drug_dataset["train"]),因为列是 Dataset 的属性,而不是 DatasetDict 的。
特性DatasetDictDataset
筛选/映射对所有子数据集操作(filter/map 方法)针对单个子数据集操作(filter/map 方法)
列数据访问(如 "review"❌ 不支持✅ 通过列名访问,如 drug_dataset["train"]["review"]
行数据访问(如前 3 行)❌ 不支持✅ 通过索引访问,如 drug_dataset["train"][:3]

batched=True 时,x["列名"] 是一个列表,直接调用 .lower() 导致错误

在HG教程练习中遇到的问题 https://huggingface.co/learn/nlp-course/zh-CN/chapter5/3

#当前结构

DatasetDict({
    train: Dataset({
        features: ['Unnamed: 0', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'input_ids', 'token_type_ids', 'attention_mask', 'code'],
        num_rows: 1613
    })
    test: Dataset({
        features: ['Unnamed: 0', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'input_ids', 'token_type_ids', 'attention_mask', 'code'],
        num_rows: 538
    })
})


# 假设 ds 是原始 DatasetDict
filtered_ds = {
    a: ds[a].filter(lambda x: x['review'] is not None)  # 保留 'review' 不为 None 的数据
    for a in ds.keys()  # 遍历 train 和 test
}

# 将 review 列小写化,正确处理 batched=True 的情况
lowercased_ds = {
    a: filtered_ds[a].map(
        lambda x: {"review": [review.lower() for review in x["review"]]},  # 对批次内的每个字符串小写化
        batched=True
    )
    for a in filtered_ds.keys()
}

# 检查结果:输出 train 集中的 review 列
print(lowercased_ds["train"]["review"][:3])
  1. batched=True 的影响:当 map 方法设置 batched=True 时,x 是一个批量数据(即一个字典,其中每列的值是一个列表)。因此,x["review"] 返回的是一个字符串列表,而不是单个字符串。未正确处理批量数据x["review"].lower() 试图对整个列表调用字符串的方法,导致报错。

解决方法

需要对每个批次中的字符串列表进行逐个处理。通过列表推导式来实现:

# 假设 ds 是原始 DatasetDict
filtered_ds = {
    a: ds[a].filter(lambda x: x['review'] is not None)  # 保留 'review' 不为 None 的数据
    for a in ds.keys()  # 遍历 train 和 test
}

# 将 review 列小写化,正确处理 batched=True 的情况
lowercased_ds = {
    a: filtered_ds[a].map(
        lambda x: {"review": [review.lower() for review in x["review"]]},  # 对批次内的每个字符串小写化
        batched=True
    )
    for a in filtered_ds.keys()
}

# 检查结果:输出 train 集中的 review 列
print(lowercased_ds["train"]["review"][:3])

修正的要点

  1. 列表推导式处理每个字符串

    • x["review"] 是一个列表(因为 batched=True),需要用 [review.lower() for review in x["review"]] 遍历并处理每个字符串。
    • 如果不使用 batched=True,则每次处理一行,x["review"] 会是单个字符串,可以直接调用 .lower()
  2. 保留原始结构

    • 使用字典推导式创建新的数据集,保持 DatasetDict 的结构完整。
    • 依然对 traintest 两部分数据分别操作。

执行上述代码后,假设原始数据如下:

{
    "train": {
        "review": ["Great medicine", "Worked well", "Very effective"]
    },
    "test": {
        "review": ["Highly recommended", "Works like a charm"]
    }
}

输出:

['great medicine', 'worked well', 'very effective']


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值