训练数据集处理
列子数据:
-- https://archive.ics.uci.edu/ml/machine-learning-databases/00462/drugsCom_raw.zip
由于 TSV 只是 CSV 的一种变体,它使用制表符而不是逗号作为分隔符,我们可以通过使用csv
加载脚本并delimiter
在load_dataset()
函数中指定参数来加载这些文件,如下所示:
from datasets import load_dataset
data_files = {"train": "drugsComTrain_raw.tsv", "test": "drugsComTest_raw.tsv"}
# \t is the tab character in Python
drug_dataset = load_dataset("csv", data_files=data_files, delimiter="\t")
在进行任何类型的数据分析时,一个好的做法是随机抽取一个小样本,以快速了解您正在处理的数据类型。Dataset.shuffle()
在 Datasets 中,我们可以通过将和函数链接在一起来创建随机样本Dataset.select()
:
drug_sample = drug_dataset[ "train" ].shuffle(seed= 42 ).select( range ( 1000 ))
# 查看前几个例子
drug_sample[: 3 ]
{'Unnamed: 0': [87571, 178045, 80482],
'drugName': ['Naproxen', 'Duloxetine', 'Mobic'],
'condition': ['Gout, Acute', 'ibromyalgia', 'Inflammatory Conditions'],
'review': ['"like the previous person mention, I'm a strong believer of aleve, it works faster for my gout than the prescription meds I take. No more going to the doctor for refills.....Aleve works!"',
'"I have taken Cymbalta for about a year and a half for fibromyalgia pain. It is great\r\nas a pain reducer and an anti-depressant, however, the side effects outweighed \r\nany benefit I got from it. I had trouble with restlessness, being tired constantly,\r\ndizziness, dry mouth, numbness and tingling in my feet, and horrible sweating. I am\r\nbeing weaned off of it now. Went from 60 mg to 30mg and now to 15 mg. I will be\r\noff completely in about a week. The fibro pain is coming back, but I would rather deal with it than the side effects."',
'"I have been taking Mobic for over a year with no side effects other than an elevated blood pressure. I had severe knee and ankle pain which completely went away after taking Mobic. I attempted to stop the medication however pain returned after a few days."'],
'rating': [9.0, 3.0, 10.0],
'date': ['September 2, 2015', 'November 7, 2011', 'June 5, 2013'],
'usefulCount': [36, 13, 128]}
一、 对训练数据,进行清洗。
该训练数据中,数据需要清洗一下:
- 该
Unnamed: 0
列看起来很像每位患者的匿名 ID。 - 该
condition
列包含大写和小写标签的混合。 - 评论的长度各不相同,并且混合使用 Python 行分隔符 (
\r\n
) 以及 HTML 字符代码,例如&\#039;
.
a、要测试该Unnamed: 0
列的患者 ID 假设,我们可以使用该Dataset.unique()
函数来验证 ID 的数量是否与每个拆分中的行数相匹配:
for split in drug_dataset.keys():
assert len(drug_dataset[split]) == len(drug_dataset[split].unique("Unnamed: 0"))
通过将列重命名为更易于解释的名称来稍微清理一下数据集Unnamed: 0,
可以使用该DatasetDict.rename_column()
函数:
drug_dataset = drug_dataset.rename_column(
original_column_name="Unnamed: 0", new_column_name="patient_id"
)
drug_dataset
DatasetDict({
train: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
num_rows: 161297
})
test: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount'],
num_rows: 53766
})
})
2、该condition
列包含大写和小写标签的混合 ,转换为小写
def lowercase_condition(example):
return {"condition": example["condition"].lower()}
drug_dataset.map(lowercase_condition)
会报如下错误:
AttributeError: 'NoneType' object has no attribute 'lower'
说明有的数据,condition列为空,需要过滤下空,
可以使用 lambda 函数来定义简单的映射和过滤操作:
drug_dataset = drug_dataset.filter(lambda x: x["condition"] is not None)
删除条目后None
,我们可以规范化我们的condition
列,并取前三条检查下数据:
drug_dataset = drug_dataset.map(lowercase_condition)
# Check that lowercasing worked
drug_dataset["train"]["condition"][:3]
['left ventricular dysfunction', 'adhd', 'birth control']
二、对训练数据,添加新的列
例如:我要加一列,统计 用户评论 review 字段的长度,并加到训练集中
定义一个简单的函数来计算每条评论中的单词数:
def compute_review_length(example):
return {"review_length": len(example["review"].split())}
ompute_review_length()
它返回一个字典,其键不对应于数据集中的列名之一。在这种情况下,当compute_review_length()
传递给时Dataset.map()
,它将应用于数据集中的所有行以创建一个新review_length
列:
drug_dataset = drug_dataset.map(compute_review_length)
# Inspect the first training example
drug_dataset["train"][0]
{'patient_id': 206461,
'drugName': 'Valsartan',
'condition': 'left ventricular dysfunction',
'review': '"It has no side effect, I take it in combination of Bystolic 5 Mg and Fish Oil"',
'rating': 9.0,
'date': 'May 20, 2012',
'usefulCount': 27,
'review_length': 17}
正如预期的那样,我们可以看到一review_length
列已添加到我们的训练集中。我们可以对这个新列进行排序,Dataset.sort()
看看极值是什么样的:
drug_dataset["train"].sort("review_length")[:3]
{'patient_id': [103488, 23627, 20558],
'drugName': ['Loestrin 21 1 / 20', 'Chlorzoxazone', 'Nucynta'],
'condition': ['birth control', 'muscle spasm', 'pain'],
'review': ['"Excellent."', '"useless"', '"ok"'],
'rating': [10.0, 1.0, 6.0],
'date': ['November 4, 2008', 'March 24, 2017', 'August 20, 2016'],
'usefulCount': [5, 2, 10],
'review_length': [1, 1, 1]}
对于短的评论对于我们训练没有意义的话,我们使用该Dataset.filter()
函数删除包含少于 30 个单词的评论。与我们对专栏所做的类似condition
,我们可以通过要求评论的长度超过此阈值来过滤掉非常短的评论:
drug_dataset = drug_dataset.filter(lambda x: x["review_length"] > 30)
print(drug_dataset.num_rows)
训练集数据,和测试集数据条数:
{'train': 138514, 'test': 46108}
三、还有一个问题,就是评论中存在 HTML 字符代码。
可以使用 Python 的html
模块来对这些字符进行转义,如下所示:
import html
text = "I'm a transformer called BERT"
html.unescape(text)
"I'm a transformer called BERT"
我们将使用Dataset.map()和
unescape 方法 来转义
语料库中的所有 HTML 字符:
drug_dataset = drug_dataset.map(lambda x: {"review": html.unescape(x["review"])})
四、该map()
方法的批处理能力
该Dataset.map()
方法采用一个batched
参数,如果设置为True
,则会导致它立即将一批示例发送到 map 函数(批大小是可配置的,但默认为 1,000)。例如,之前未转义所有 HTML 的地图函数需要一些时间来运行(您可以从进度条中读取所用时间)。我们可以通过使用列表理解同时处理多个元素来加快速度。
当您指定batched=True
该函数时,它会接收一个包含数据集字段的字典,但每个值现在都是一个值列表,而不仅仅是一个值。的返回值Dataset.map()
应该是相同的:一个包含我们要更新或添加到数据集中的字段的字典,以及一个值列表。例如,这是另一种取消转义所有 HTML 字符的方法,但使用batched=True
:
new_drug_dataset = drug_dataset.map(
lambda x: {"review": [html.unescape(o) for o in x["review"]]}, batched=True
)
使用Dataset.map()
遇到的“快速”分词器的速度至关重要,它可以快速分词大文本列表。例如,要使用快速分词器对所有药物评论进行分词,我们可以使用如下函数:
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-cased")
def tokenize_function(examples):
return tokenizer(examples["review"], truncation=True)
我们可以将一个或多个示例传递给分词器,因此我们可以在有或没有 的情况下使用此函数batched=True
。让我们借此机会比较不同选项的性能。%time
在笔记本中,您可以通过在要测量的代码行之前添加来为单行指令计时:
%time tokenized_dataset = drug_dataset.map(tokenize_function, batched=True)
使用和不使用 执行相同的指令batched=True
,然后使用慢速分词器(use_fast=False
在AutoTokenizer.from_pretrained()
方法中添加)进行尝试,这样您就可以看到您在硬件上获得的数字:
Options | Fast tokenizer | Slow tokenizer |
---|---|---|
batched=True | 10.8s | 4min41s |
batched=False | 59.2s | 5min3s |
这意味着使用带batched=True
选项的快速分词器比没有批处理的慢分词器快 30 倍——这真是太神奇了!这就是为什么快速标记器在使用时是默认的(以及为什么它们被称为“快速”)的主要原因AutoTokenizer
。他们之所以能够实现这样的加速,是因为在幕后,标记化代码是在 Rust 中执行的,Rust 是一种可以轻松并行化代码执行的语言。
并行化也是快速分词器通过批处理实现近 6 倍加速的原因:你不能并行化单个分词操作,但是当你想同时对大量文本进行分词时,你可以将执行拆分到多个进程中,每个负责自己的文本。
Dataset.map()
也有一些自己的并行化能力。由于它们不受 Rust 的支持,它们不会让慢的分词器赶上快的分词器,但它们仍然有用(特别是如果您使用的分词器没有快速版本)。要启用多处理,请使用num_proc
参数并指定要在调用中使用的进程数Dataset.map()
:
slow_tokenizer = AutoTokenizer.from_pretrained("bert-base-cased", use_fast=False)
def slow_tokenize_function(examples):
return slow_tokenizer(examples["review"], truncation=True)
tokenized_dataset = drug_dataset.map(slow_tokenize_function, batched=True, num_proc=8)
您可以对时间进行一些试验以确定要使用的最佳进程数;在我们的例子中,8 似乎产生了最好的速度增益。以下是我们在使用和不使用多处理时得到的数字:
选项 | 快速分词器 | 慢分词器 |
---|---|---|
batched=True | 10.8s | 4分41秒 |
batched=False | 59.2秒 | 5分3秒 |
batched=True ,num_proc=8 | 6.52s | 41.3s |
batched=False ,num_proc=8 | 9.49s | 45.2秒 |
五、输入一组特征,被截断的情况
机器学习中,示例通常被定义为我们提供给模型的一组特征。在某些情况下,这些特征将是 a 中的一组列Dataset
,但在其他情况下(如此处和用于问答),可以从单个示例中提取多个特征并属于单个列(解释就是:一句话,因为分词器设置的最大长度,被截断为多段。)
我们将标记我们的示例并将它们截断为最大长度 128,但我们将要求标记器返回文本的所有块(所有的分段),而不仅仅是第一个。这可以通过以下方式完成return_overflowing_tokens=True
:
def tokenize_and_split(examples):
return tokenizer(
examples["review"],
truncation=True,
max_length=128,
return_overflowing_tokens=True,
)
Dataset.map()
在对整个数据集使用之前,让我们先在一个示例上对此进行测试:
result = tokenize_and_split(drug_dataset["train"][0])
[len(inp) for inp in result["input_ids"]]
一个输入,由于设置最大长度 128 ,截成了2段(2个特征数据),这个输入的长度是(128+49):
[128, 49]
对所有元素执行此操作数据集:
tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
会出现一下错误( 1463 行大于 默认 操作1000行 ) 1,000 个示例给出了 1,463 个新特征:
ArrowInvalid: Column 1 named condition expected length 1463 but got length 1000
- writer_batch_size (
int
, 默认为1000
) — 缓存文件写入器每次写入操作的行数。该值是处理期间内存使用和处理速度之间的良好折衷。较高的值使处理执行较少的查找,较低的值在运行时消耗较少的临时内存map
。
我们提到我们还可以通过使旧列与新列的大小相同来处理长度不匹配的问题。为此,我们需要overflow_to_sample_mapping
设置时分词器返回的字段return_overflowing_tokens=True
。它为我们提供了从新特征索引到其来源样本索引的映射。使用这个,我们可以通过重复每个示例的值与生成新功能的次数一样多的次数,将原始数据集中存在的每个键与正确大小的值列表相关联:
def tokenize_and_split(examples):
result = tokenizer(
examples["review"],
truncation=True,
max_length=128,
return_overflowing_tokens=True,
)
# Extract mapping between new and old indices
sample_map = result.pop("overflow_to_sample_mapping")
for key, values in examples.items():
result[key] = [values[i] for i in sample_map]
return result
tokenized_dataset = drug_dataset.map(tokenize_and_split, batched=True)
tokenized_dataset
DatasetDict({
train: Dataset({
features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
num_rows: 206772
})
test: Dataset({
features: ['attention_mask', 'condition', 'date', 'drugName', 'input_ids', 'patient_id', 'rating', 'review', 'review_length', 'token_type_ids', 'usefulCount'],
num_rows: 68876
})
})
六 创建验证集
Datasets 提供了一个Dataset.train_test_split()
基于scikit-learn
. 让我们用它来将我们的训练集分成train
和validation
拆分(我们设置了seed
可重复性的参数):
drug_dataset_clean = drug_dataset["train"].train_test_split(train_size=0.8, seed=42)
# Rename the default "test" split to "validation"
drug_dataset_clean["validation"] = drug_dataset_clean.pop("test")
# Add the "test" set to our `DatasetDict`
drug_dataset_clean["test"] = drug_dataset["test"]
drug_dataset_clean
DatasetDict({
train: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
num_rows: 110811
})
validation: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
num_rows: 27703
})
test: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length', 'review_clean'],
num_rows: 46108
})
})
七、保存数据集
如下表所示,Datasets 提供了三个主要功能来以不同的格式保存您的数据集:
Data format | Function |
---|---|
Arrow | Dataset.save_to_disk() |
CSV | Dataset.to_csv() |
JSON | Dataset.to_json() |
Arrow:
drug_dataset_clean.save_to_disk("drug-reviews")
这将创建一个具有以下结构的目录:
drug-reviews/
├── dataset_dict.json
├── test
│ ├── dataset.arrow
│ ├── dataset_info.json
│ └── state.json
├── train
│ ├── dataset.arrow
│ ├── dataset_info.json
│ ├── indices.arrow
│ └── state.json
└── validation
├── dataset.arrow
├── dataset_info.json
├── indices.arrow
└── state.json
保存数据集后,我们可以使用load_from_disk()
以下函数加载它:
from datasets import load_from_disk
drug_dataset_reloaded = load_from_disk("drug-reviews")
drug_dataset_reloaded
DatasetDict({
train: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
num_rows: 110811
})
validation: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
num_rows: 27703
})
test: Dataset({
features: ['patient_id', 'drugName', 'condition', 'review', 'rating', 'date', 'usefulCount', 'review_length'],
num_rows: 46108
})
})
对于 CSV 和 JSON 格式,我们必须将每个拆分存储为一个单独的文件。一种方法是遍历对象中的键和值DatasetDict
:
for split, dataset in drug_dataset_clean.items():
dataset.to_json(f"drug-reviews-{split}.jsonl")
加载 JSON 文件,如下所示:
data_files = {
"train": "drug-reviews-train.jsonl",
"validation": "drug-reviews-validation.jsonl",
"test": "drug-reviews-test.jsonl",
}
drug_dataset_reloaded = load_dataset("json", data_files=data_files)