1. 数据集的默认加载机制
Hugging Face 的 datasets
支持公开数据集和本地数据集。默认机制包括以下几种情况:
(1) 加载公开数据集
- 公开数据集(如 IMDB、GLUE)通常预定义了子集(
train
、validation
、test
)。 - 可以通过
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")
总结
- 单文件数据集默认加载为
train
,可通过手动划分和重命名实现自定义子集。 - 多文件方式可以直接通过
data_files
定义子集名称,简化操作。 - 嵌套数据可以通过
field
加载指定层级,并通过扁平化方法处理复杂结构。
踩坑记录:
1. 假设我们想把一个文件分给多个子集,这里非常麻烦
- 初次划分后,子集名称为
train
和dec25
。 - 第二次划分,
dec25
被拆分为两个子集。 - 我们手动给新划分的子集命名为
validation
和test
,但原名称dec25
本身不会被修改。
- 初始数据集加载后,只有一个子集,默认名为
train
。 - 我们先将
train
按比例划分为train
和dec25
。 - 再将
dec25
进一步划分为dec25_validation
和test
。
修订后的代码示例
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)} 条样本")
子集名称的变化与来源
-
初次划分
train_test_split
返回的子集名称默认是train
和test
。- 我们手动将
test
子集重新命名为dec25
,表示临时划分出的验证集。
-
再次划分
- 对
dec25
执行train_test_split
,默认生成新的train
和test
子集。 - 我们重新命名:
- 原来的
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 条
规律总结
-
命名逻辑
- 初次划分:默认子集名称是
train
和test
,可以手动将test
改为其他名称(如dec25
)。 - 再次划分:新生成的子集名称仍是
train
和test
,需要根据具体用途重新命名。
- 初次划分:默认子集名称是
-
索引值的变化
- 每次执行
train_test_split
,默认生成两个子集:train
: 保留的数据部分。test
: 新划分出的小部分数据。
- 如果子集继续划分,索引值也会沿用
train
和test
。
- 每次执行
-
避免名称混淆
-
在多次划分时,及时重命名子集(如
dec25_validation
和test
),确保不同阶段的子集名称清晰、唯一。 -
代码中的
test_valid['test']
可能会让人误解为原来的dec25
被覆盖。需要明确说明:每次执行train_test_split
都会生成新的子集对象,原子集不会被覆盖。
-
2. Split = {} 而不是 Split = [ ]
在你的代码中,load_dataset
函数被用来加载数据集,但由于使用了参数split=["train[:1%]", "test[-1%:]"]
,导致返回的对象是一个列表(list
)而不是通常的DatasetDict
类型。下面详细解释这一现象的原因,以及如何正确理解和使用这段代码。
核心机制:
split
参数的默认行为是根据预定义的分割(如train
、test
、validation
)生成一个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]) # 第二部分数据,属于测试集切片
- 变量含义:
data_files
: 包含训练文件和测试文件的路径。delimiter="\t"
: 指定文件的分隔符为制表符。split=["train[:1%]", "test[-1%:]"]
: 将训练集取前1%,测试集取后1%。ds
: 加载后的数据,类型是list
,包含两个分割。
- 返回结果:
ds[0]
是训练集的切片(前1%)。ds[1]
是测试集的切片(后1%)。
如何使结果是 DatasetDict
需要通过dict
的方式命名 split={"train": "train[:1%]", "test": "test[-1%:]"}
: 指定分割后命名为train
和test
# 修改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
)
-
load_dataset
- 用于加载普通文件格式(如
csv
、json
、tsv
等)。 - 需要指定文件类型,例如
"csv"
。
- 用于加载普通文件格式(如
-
load_from_disk
- 用于加载 Hugging Face 本地保存的数据集文件夹。
- 文件夹中通常包含
.arrow
文件以及dataset_info.json
等元数据文件。
4. Map 和 Filter可以直接应用到总集但是访问不行
filter
和 map
的语法和调用
filter
和map
的作用:filter
:筛选符合条件的样本。map
:对每条样本或批量样本进行映射操作。
- 调用方式:
- 对总数据集(
DatasetDict
)操作:直接调用drug_dataset.filter()
或drug_dataset.map()
会对所有子数据集(如train
和test
)同时应用操作。 - 对单个子数据集(
Dataset
)操作:必须先通过键(如"train"
)获取子数据集,再调用对应的方法,如drug_dataset["train"].filter()
。
- 对总数据集(
DatasetDict
和Dataset
的区别:DatasetDict
是一个字典结构,包含多个子数据集(键是子数据集名称,如"train"
,值是具体的Dataset
对象)。Dataset
是具体的单个数据集,具有行和列的概念。
- 访问规则:
- 要访问列数据(如
"review"
),必须先进入具体的子数据集(如drug_dataset["train"]
),因为列是Dataset
的属性,而不是DatasetDict
的。
- 要访问列数据(如
特性 | DatasetDict | Dataset |
---|---|---|
筛选/映射 | 对所有子数据集操作(filter/map 方法) | 针对单个子数据集操作(filter/map 方法) |
列数据访问(如 "review" ) | ❌ 不支持 | ✅ 通过列名访问,如 drug_dataset["train"]["review"] |
行数据访问(如前 3 行) | ❌ 不支持 | ✅ 通过索引访问,如 drug_dataset["train"][:3] |
5 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])
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])
修正的要点
-
列表推导式处理每个字符串:
x["review"]
是一个列表(因为batched=True
),需要用[review.lower() for review in x["review"]]
遍历并处理每个字符串。- 如果不使用
batched=True
,则每次处理一行,x["review"]
会是单个字符串,可以直接调用.lower()
。
-
保留原始结构:
- 使用字典推导式创建新的数据集,保持
DatasetDict
的结构完整。 - 依然对
train
和test
两部分数据分别操作。
- 使用字典推导式创建新的数据集,保持
执行上述代码后,假设原始数据如下:
{
"train": {
"review": ["Great medicine", "Worked well", "Very effective"]
},
"test": {
"review": ["Highly recommended", "Works like a charm"]
}
}
输出:
['great medicine', 'worked well', 'very effective']