PyTorch数据预处理

用深度学习来解决现实世界的问题,我们经常从预处理原始数据开始,而不是从那些准备好的张量格式数据开始。

读取数据集

为了方便数据的读取,我们需要将使用的数据包装为Dataset类,自定义的Dataset需要继承它并且实现两个成员方法:

  • __getitem__() 该方法定义用索引(0 到 len(self))获取一条数据或一个样本
  • __len__() 该方法返回数据集的总长度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    from torch.utils.data import Dataset

    class MyDataset(Dataset):
    def __init__(self, input_file):
    """ 实现初始化方法,可以从文件中读取到列表或DataFrame中 """
    self.data = create_dataset() # 后文有实现
    self.labels = pd.read_csv()

    def __len__(self):
    """ 返回数据长度 """
    return len(self.data)
    # return len(self.df)

    def __getitem__(self, idx):
    """ 根据idx返回数据 """
    return self.data[idx], self.labels.iloc[idx]

自定义的数据集创建好后,就可以使用官方提供的数据载入器来读取数据,数据载入器提供了设置batch_size、是否进行shuffle、加载数据的子进程树(num_workers)等参数:

1
2
3
4
5
6
7
8
9
dataset = MyDataset(input_file)
dl = torch.utils.data.DataLoader(dataset, batch_size=10, shuffle=True, num_workers=0)

# dl是一个迭代器,可以对其循环遍历实现对数据的访问
idata = iter(dl)
print(next(idata))

for i, data in enumerate(dl):
print(i, data)

处理缺失值

在结构化的表格数据中,“NaN” 项代表缺失值。为了处理缺失的数据,典型的方法包括插值删除,其中插值用替代值代替缺失值,而删除则忽略缺失值。

1
2
3
4
5
6
7
df = pd.read_csv(input_file)

df.fillna(0) # 用0值替换缺失值,也常用均值df.mean()
df.fillna(axis=1, method='ffill') # 横向用缺失值前面的值替换缺失值
df.dropna(how='all') # 所有值全为缺失值才删除
df.dropna(thresh=2) # 至少出现过两个缺失值才删除
df.dropna(subset=['name', 'born']) # 删除subset中的含有缺失值的行或列

在文本数据中,通常会引入一个特殊的token“”来表征未登录词。

文本序列化

处理文本数据的第一步是生成词典,即为每个词进行编号,从而可以将文本转换成词对应编号的序列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# tokenizer是对文本进行分词的分词器,通常会指定词典词的最大个数以及最小词频
def build_vocab(file_path, tokenizer, max_size, min_freq):
vocab_dic = {}
with open(file_path, 'r', encoding='UTF-8') as fin:
for sline in fin:
for word in tokenizer(sline.strip()):
vocab_dic[word] = vocab_dic.get(word, 0) + 1
vocab_list = sorted([_ for _ in vocab_dic.items() if _[1] >= min_freq], key=lambda x: x[1], reverse=True)[
:max_size]
vocab_list.insert(0, ('<pad>', min_freq)) # 引入补全标识(通常编号为0)
vocab_list.append(('<unk>', min_freq)) # 引入未登录词标识
word2idx = {word_count[0]: idx for idx, word_count in enumerate(vocab_list)}
idx2word = {idx : word_count[0] for idx, word_count in enumerate(vocab_list)}
return word2idx, idx2word

有了词典以后还需要再次遍历文档,生成文档中文本对应的编号序列,我们可以在这一步就对数据进行补齐,使得批量文本具有相同个数的词(序列长度相等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def create_dataset(file_path, tokenizer, word2idx, pad_size=32):
contents = []
with open(file_path, 'r', encoding='UTF-8') as fin:
for sline in fin:
words_line = []
token = tokenizer(sline.strip())
seq_len = len(token)
if pad_size:
if len(token) < pad_size:
token.extend('<pad>' * (pad_size - len(token)))
else:
token = token[:pad_size]
seq_len = pad_size
for word in token:
words_line.append(word2idx.get(word, word2idx.get('<unk>')))
contents.append((words_line, seq_len)) # 带有文本实际长度
return contents

批量化补齐

当然,PyTorch也提供在载入器中进行批量化补齐的接口,我们可以实现一个collate_fn函数,作为参数传入DataLoader中:

1
2
3
4
5
6
7
8
9
10
def collate_fn(batch_data, pad=0):
data, labels = list(zip(*batch_data)) # batch_data的结构是[([data_1],[label_1]),([data_2],[label_2]),...],所以需要使用zip函数对它解压
max_len = max([len(seq) for seq in data]) # 以批量数据中的最长序列为基准补齐

data = [seq+[pad]*(max_len-len(seq)) for seq in data]
data = torch.LongTensor(data)
label = torch.FloatTensor(label)
return (data, label)

dl = torch.utils.data.DataLoader(dataset, batch_size=10, shuffle=True, collate_fn=collate_fn)

当然,如上一节的实例那样预先把数据补齐,然后使用数据载入器只进行打乱与批量读取。

TorchText工具

对于处理 NLP 任务的数据,我们可以使用高级工具 TorchText,在 pip 安装时需要确保版本与你所使用的 torch 版本配套(本文使用的接口是 0.8 及其以下版本的,自 0.9 版本起接口变化较大)。

TorchText 的核心是 Field、Dataset、Iterator 这三个类:

  • Field:定义对于数据预处理的配置信息,包括设置是否以序列表示、是否使用词典对象、起始字符(如 “\“)、结尾字符(如”\“)、每条数据的长度、分词前的预处理、分词后的后处理、转小写、分词器、是否以 batch_size 作为第一轴、padding 字符、unk 字符等信息:

    • torchtext.data.Field(sequential=True, use_vocab=True, init_token=None, eos_token=None, fix_length=None, dtype=torch.int64, preprocessing=None, postprocessing=None, lower=False, tokenize=None, tokenizer_language=’en’, include_lengths=False, batch_first=False, pad_token=’\‘, unk_token=’\‘, pad_first=False, truncate_first=False, stop_words=None, is_target=False)
  • Dataset:按照 Feild 声明的配置信息,从原始数据构造数据集,常用的 Dataset 类包括:TabularDataset 用来处理结构化的原始数据(tsv、csv、json);TranslationDataset 用来处理机器翻译的原始数据(包含源语言与目标语言);LanguageModelingDataset 用来处理语言模型的原始数据;SequenceTaggingDataset 用来处理序列标注的原始数据。

  • Iterator:对 Dataset 进行迭代输出,支持对输出的张量进行 shuffle、batching 与 packaging。其中,BucketIterator 类可以将长度相似的输入序列存储在一起,提升运算效率。

    • torchtext.data.BucketIterator(dataset, batch_size, sort_key=None, device=None, batch_size_fn=None, train=True, repeat=False, shuffle=None, sort=None, sort_within_batch=None)

使用上述三类组件进行 NLP 数据处理的步骤是:声明数据的 Field -> 将 Field 映射到原始数据上并构建 Dataset 划分数据 -> 使用某个数据集上的 Field 创建词汇表 vocab -> 生成批量的迭代器,我们通过下面的实例来掌握 TorchText 工具的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 声明 Field
from torchtext.data import Field

tokenize = lambda x: x.split()
TEXT = Field(sequential=True, tokenize=tokenize, lower=True)
LABEL = Field(sequential=False, use_vocab=False)


# 构建Dataset
from torchtext.data import TabularDataset

tv_datafields = [("id", None),
("comment_text", TEXT), ("toxic", LABEL),
("severe_toxic", LABEL), ("threat", LABEL),
("obscene", LABEL), ("insult", LABEL),
("identity_hate", LABEL)]
trn, vld = TabularDataset.splits(
path="data",
train='torchtext_train.csv', validation="torchtext_valid.csv",
format='csv',
skip_header=True,
fields=tv_datafields)

tst_datafields = [("id", None),
("comment_text", TEXT)]
tst = TabularDataset(
path="./data/torchtext_test.csv",
format='csv',
skip_header=True,
fields=tst_datafields)

# 使用训练数据的TEXT字段构建词表
TEXT.build_vocab(trn)
# pickle.dump(TEXT.vocab, vocab_file_path) # 输出保存词表
# TEXT.vocab.stoi (基于词表词转id) / TEXT.vocab.itos (基于词表id转词)


# 创建迭代器
from torchtext.data import Iterator, BucketIterator

train_iter, val_iter = BucketIterator.splits(
(trn, vld),
batch_sizes=(64, 64),
device=-1,
sort_key=lambda x: len(x.comment_text),
sort_within_batch=False,
repeat=False
)
test_iter = Iterator(tst, batch_size=64, device=-1, sort=False, sort_within_batch=False, repeat=False)

我们读取的原始数据是如下所示的结构化表格数据:

id comment_text toxic severe_toxic obscene threat insult identity_hate
0…7bf “Explanation Why the edits ….” 0 0 0 0 0 0

首先将声明的 Field 指定到具体的列名上, “comment_text” 的评论文本需要分词并从训练数据的该字段中构建词表;除了 “id” 列不被使用,其余各列都是被声明为了标签。本例中的训练集与验证集是通过指定文件加载,调用的是对象方法 splits(),我们也可以调用 Dataset 对象的方法 split() 指定划分比例将一个整体数据划分为训练集与验证集。在划分数据的过程中就自动按照 Field 里声明的处理方法对数据进行了预处理。使用训练数据的 TEXT 字段构建词汇表,也可以通过设置 build_vector() 中的 vectors 参数,引入已经预训练好的词向量(word2vec等)。