Reference:https://github.com/649453932/Bert-Chinese-Text-Classification-Pytorch
模型代码学习-CLS文本分类-Bert-Chinese-Text-Classification-Pytorch代码学习-构建数据集类
baseDir: Bert-Chinese-Text-Classification-Pytorch/utils.py
目录
def load_dataset(path, pad_size=32):
class DatasetIterater(object):
def __init__(self, batches, batch_size, device):
def build_iterator(dataset, config):
./utils.py学习
utils.py中主要是对于数据集的预处理,最终目标是构造能用于训练的batch和iter
全局
- Tqdm 是一个快速,可扩展的Python进度条,可以在 Python 长循环中添加一个进度提示信息,用户只需要封装任意的迭代器 tqdm(iterator),使用方法可见:https://blog.csdn.net/zkp_987/article/details/81748098
import torch
from tqdm import tqdm
import time
from datetime import timedelta
PAD, CLS = '[PAD]', '[CLS]' # padding符号, bert中综合信息符号
作者提供的数据集示例
def build_dataset(config):
def load_dataset(path, pad_size=32):
- 读取作者提供的txt文件为f迭代器,for line in tqdm f可能可以指定一个进度条,通过strip方法去掉每行的空格,之后如果该行不存在了,则continue继续处理下一行
- 由于数据集中两个内容中间以\t分割,于是通过split方法拆分出content和label
- config.tokenizer.tokenize(content),其中config来自上层build_dataset方法的入参,run.py作为最终的运行文件进行调用train_data, dev_data, test_data = build_dataset(config),其中config再进一步来源于x = import_module('models.' + model_name) config = x.Config(dataset),来自于model bert.py中的class Config,最终config类中包括了self.tokenizer = BertTokenizer.from_pretrained(self.bert_path),于是综合来说config.tokenizer.tokenize(content)可以理解为了BertTokenizer.from_pretrained(self.bert_path).tokenize(content)
- token最开始前边手动拼接[CLS],根据一些讨论个人理解[CLS]首先是bert用作分类任务必须需要的一个字符,参考该篇博客中的说法https://blog.csdn.net/qq_42189083/article/details/102641087,[CLS]就是classification的意思,可以理解为用于下游分类的任务,主要用于以下两种任务:1)单文本分类任务:对于文本分类任务,BERT模型在文本前插入一个[CLS]符号,并将与该符号对应的输出向量作为整篇文本的语义表示,用于文本分类。可以理解为:与本文中已有的其他字词相比,这个无明显语义信息的符号会更“公平”的融合文本中各个字/词的语义信息。2)语句对分类任务:该任务的实际应用场景包括:问答(判断一个问题与一个答案是否匹配)、语句匹配(两句话是否表达同一个意思)等。对于该任务,BERT模型除了添加[CLS]符号并将对应的输出作为文本的语义表示,还对输入两句话用一个[SEP]符号作分割,并分别对两句话附加两个不同的文本向量以作区分。
- token_ids的作用需要打印后查看,猜测应该是一个与vocab.txt中进行角标对应的过程,不过为什么要进行这个对应->为了输入过程中的进一步输入进入bert进行位置embedding等
- pad_size指定了希望的最长文本长度,并对不足的文本进行pad补充,于是在该分支内进行判断,如果token的长度小于pad_size超参,首先对mask进行拼接,拼接为前边token_ids长度个数的1和最后补齐pad_size的0,由于token_ids的后半部分没有补东西,现在也把token_ids的最后补上0,这里为什么把token_ids的最后补上0,是否和词表中的对应关系有关?->vocat.txt中角标是0的位置对应的是[PAD]->个人感觉一般来说vocab.txt中的第0位应该都是[PAD]
- 如果token的长度已经等于或超过了pad_size超参了,则mask中不设置任何忽略,为pad_size长度的1,同时把token_ids进行截取,并重置seq_len
- 把每一条数据放入contents中,每一条为(token_ids, int(label), seq_len, mask),依次是:vocab.txt中的角标、类别int类型,文本长度,一个待使用的mask
def load_dataset(path, pad_size=32):
contents = []
with open(path, 'r', encoding='UTF-8') as f:
for line in tqdm(f):
lin = line.strip()
if not lin:
continue
content, label = lin.split('\t')
token = config.tokenizer.tokenize(content)
token = [CLS] + token
seq_len = len(token)
mask = []
token_ids = config.tokenizer.convert_tokens_to_ids(token)
if pad_size:
if len(token) < pad_size:
mask = [1] * len(token_ids) + [0] * (pad_size - len(token))
token_ids += ([0] * (pad_size - len(token)))
else:
mask = [1] * pad_size
token_ids = token_ids[:pad_size]
seq_len = pad_size
contents.append((token_ids, int(label), seq_len, mask))
return contents
- 这里执行完token = config.tokenizer.tokenize(content)后,打印输出当前的token,也希望把token_ids打印输出->但这里其实根据后文datas的打印结果就可以进行如下的猜测了
datas中的token_ids字段,之前的tokenizer.tokenize应该就是把中文文本split一下,如果用英文数据这里应该还需要变通一下
[101, 1367, 2349, 7566, 2193, 782, 7028, 4509, 2703, 680, 5401, 1744, 2190, 6413, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
- 对于mask拼接的实验尝试如下,希望验证[1] * 100这样在python中的输出打印效果->如下
>>> print([1]*10)
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
在def build_dataset()这一上层函数中对编写的load_dataset进行数据调用,经过学长提醒这里可能存在不能区分训练测试过程,导致训练测试过程都需要进行数据加载,虽然数据大小应该不大,但是批量加载也是一个过程。
train = load_dataset(config.train_path, config.pad_size)
dev = load_dataset(config.dev_path, config.pad_size)
test = load_dataset(config.test_path, config.pad_size)
return train, dev, test
class DatasetIterater(object):
从名称上猜测DatasetIterater类应该是把数据的dataset变为可迭代形式的,或者说batch形式的
def __init__(self, batches, batch_size, device):
- 在后续的def build_iterator(dataset, config):函数中对该类进行了实例化,iter = DatasetIterater(dataset, config.batch_size, config.device),可以看到传入的参数是经过def build_dataset()后的dataset(多条(token_ids, int(label), seq_len, mask)的集合),期望的batch_size,还有config的device。
- 在该初始化中定义了batch_size,数据集的batches(dataset传入),n_batches代表batch的数目,如果不能正好n_batches等分,则置self.residue为true,self.index和device的用处需要后文
- 看了后文,index应该是来标记走到了第几个iter的
def __init__(self, batches, batch_size, device):
self.batch_size = batch_size
self.batches = batches
self.n_batches = len(batches) // batch_size
self.residue = False # 记录batch数量是否为整数
if len(batches) % self.n_batches != 0:
self.residue = True
self.index = 0
self.device = device
def _to_tensor(self, datas):
- 前边这个短下划线有没有什么特殊的考虑,在一些材料中看到如果加个短下划线不会被import到->一种较为习惯性的写法,在实际上只要保证不冲突即可
- datas中应该是一条条的(token_ids, int(label), seq_len, mask)(有待print验证),torch.LongTensor() Long类型的张量,对于BERT经过这样的转化就可以进行输入到模型中吗,因为如果理解没错的话此时的token_ids只是一个列表向量,代表了对应词汇的index标签,而且从表达形式来看,x就是数据,y就是标签
- 在return的时候把x seqlen mask组合了一下,不知道有什么考虑
def _to_tensor(self, datas):
x = torch.LongTensor([_[0] for _ in datas]).to(self.device)
y = torch.LongTensor([_[1] for _ in datas]).to(self.device)
# pad前的长度(超过pad_size的设为pad_size)
seq_len = torch.LongTensor([_[2] for _ in datas]).to(self.device)
mask = torch.LongTensor([_[3] for _ in datas]).to(self.device)
return (x, seq_len, mask), y
对这里的datas进行打印,一个datas包含了一个batch的数据,这里展示一个batch中的一条数据,可以看到总长度为32,也就是超参中指定的长度
_to_tensor datas [
(
[101, 1367, 2349, 7566, 2193, 782, 7028, 4509, 2703, 680, 5401, 1744, 2190, 6413, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
6,
14,
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
),
(
[101, 4125, 3215, 1520, 840, 2357, 7371, 1778, 1079, 1355, 4385, 3959, 3788, 3295, 2100, 1762, 6395, 2945, 113, 1745, 114, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
4,
21,
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
),
…
]
希望对这里的LongTensor进行打印查看(显示数据的打印查看,也打印查看维度),如下所示:
x.shape torch.Size([128, 32]) y.shape torch.Size([128]) seq_len torch.Size([128]) mask torch.Size([128, 32])
x tensor([[ 101, 837, 5855, ..., 0, 0, 0],
[ 101, 2343, 3173, ..., 0, 0, 0],
[ 101, 2512, 6228, ..., 0, 0, 0],
...,
[ 101, 6122, 4495, ..., 0, 0, 0],
[ 101, 1849, 1164, ..., 0, 0, 0],
[ 101, 860, 7741, ..., 0, 0, 0]], device='cuda:0')
y tensor([2, 9, 3, 6, 1, 9, 6, 7, 6, 8, 1, 6, 3, 3, 7, 7, 2, 0, 9, 2, 2, 2, 2, 0,
9, 4, 0, 3, 1, 1, 5, 0, 7, 6, 0, 6, 4, 5, 0, 5, 1, 4, 3, 3, 1, 2, 7, 9,
4, 4, 2, 2, 0, 6, 3, 1, 7, 8, 4, 4, 7, 7, 4, 6, 6, 9, 0, 7, 4, 8, 1, 9,
6, 4, 7, 6, 8, 0, 5, 2, 6, 9, 7, 1, 3, 1, 4, 9, 9, 9, 9, 3, 8, 7, 1, 9,
1, 9, 0, 4, 2, 0, 0, 4, 4, 7, 0, 4, 7, 4, 7, 7, 7, 7, 6, 6, 8, 7, 1, 3,
1, 3, 7, 6, 5, 0, 6, 3], device='cuda:0')
seq_len tensor([16, 20, 20, 21, 19, 20, 16, 23, 21, 17, 21, 16, 18, 19, 25, 22, 16, 13,
18, 15, 22, 21, 20, 11, 23, 17, 15, 17, 23, 21, 20, 9, 23, 17, 17, 20,
14, 19, 20, 15, 21, 19, 20, 20, 22, 14, 27, 22, 20, 19, 21, 14, 16, 19,
13, 21, 23, 17, 17, 12, 23, 25, 19, 16, 22, 21, 12, 24, 19, 16, 21, 23,
21, 15, 23, 17, 19, 21, 20, 16, 18, 19, 24, 16, 19, 19, 14, 22, 17, 20,
18, 18, 16, 23, 21, 22, 22, 22, 20, 15, 16, 18, 18, 20, 22, 22, 16, 15,
23, 18, 24, 24, 23, 25, 15, 16, 17, 24, 22, 16, 21, 21, 22, 19, 21, 22,
21, 18], device='cuda:0')
mask tensor([[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
...,
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0]], device='cuda:0')
def __next__(self):
- 怎么理解next前后的双横线,看起来next是为了走到下一个iter,或许在调用的时候会根据iter自动往后边一个走?->__next__ __iter__ __len__ 都是为了构造一个可迭代的对象,即一个数据的iter class,在写法上一般可以模仿类似的数据预处理过程
- 如果self.residue(不是正好能分成n个batch),并且现在的index已经达到了n个batch,也就是说剩下那一小部分没法归为一个正好的batch了,拆分出一个batches为self.batches[self.index * self.batch_size: len(self.batches)],也就是从最后上一个batch到结尾的,作为一个新的batch,并使得self.index += 1。并把这个batches进行_to_tensor()操作
- elif情况(即在不满足如果self.residue(不是正好能分成n个batch),并且现在的index已经达到了n个batch的情况下),如果现在的index超过并且等于(这个是应对正好的情况)了,重置self.index=0为下一个Epoch进行准备,并且raise StopIteration
- 其他情况(可以理解为不遇到终止等特殊情况,正常往后迭代的情况):切分batches:batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size]
def __next__(self):
if self.residue and self.index == self.n_batches:
batches = self.batches[self.index * self.batch_size: len(self.batches)]
self.index += 1
batches = self._to_tensor(batches)
return batches
elif self.index >= self.n_batches:
self.index = 0
raise StopIteration
else:
batches = self.batches[self.index * self.batch_size: (self.index + 1) * self.batch_size]
self.index += 1
batches = self._to_tensor(batches)
return batches
def __iter__(self):
- 这个怎么理解,是可以作为一个迭代器?->__next__ __iter__ __len__ 共同构成一个可迭代的类
def __iter__(self):
return self
def __len__(self):
- 返回batch的长度,也就是说有多少个batch
def __len__(self):
if self.residue:
return self.n_batches + 1
else:
return self.n_batches
def build_iterator(dataset, config):
- 把dataset转化为一个DatasetIterater类的iter
- 这个iter是否是一个可迭代的->是
def build_iterator(dataset, config):
iter = DatasetIterater(dataset, config.batch_size, config.device)
return iter
def get_time_dif(start_time):
def get_time_dif(start_time):
"""获取已使用时间"""
end_time = time.time()
time_dif = end_time - start_time
return timedelta(seconds=int(round(time_dif)))