构建NLP数据集,分为如下几步:
1.单词分割形式
2.词汇表
3.利用词汇表word2idx映射,制作数据集
4.打包
✨ 1.单词分割
有两种表示方式,一种是word-level,另外一种是char-level。
🌊 1.1 word-level
tokenizer = lambda x: x.split(' ') # 传入x返回x.split(' ')
举一个例子:x=“你好啊 我是谁” **=》 ** return:[“你好啊”, “我是谁”]
🎈 1.2 char-level
tokenizer = lambda x: [y for y in x]
举一个例子:x=“你好啊 我是谁” **=》 ** return:[“你”, “好”, “啊”, " ", “我”, “是”, “谁”]
一个按照词分割,一个按照字分割
✨ 2.词汇表
有两种方式,一种是已经存在了直接导入,另外一种是没有需要制作。
🎃 2.1 直接导入
目前我遇到的就是pkl文件存储的词汇表,所以用pickle库的load函数导入。
import pickle as pkl
vocab = pkl.load(open(vocab_file_path, "rb"))
🎄 2.2 制作
这部分就用到了上面的单词分割**(制作的词汇表是以字符的形式还是词的形式)**。
制作词汇表的函数实现如下
UNK, PAD = '<UNK>', '<PAD>'
def build_vocab(file_path, tokenizer, max_size, min_freq):
"""
file_path: 一般为训练集文件路径
tokenizer: 按照什么方式制作词汇表(word-level或char-level),详细见第一小节
max_size: 词汇表中最多有多少词
min_freq: 若训练集中词或字符出现的次数小于这个,直接排除
return: 按照训练集中单词的出现次数由大到小进行排序得到的词汇表。举一个简单的例子,假设有单词"白", "三", "点",白出现2词,三出现3次,点出现1次。那么返回的词汇表为{"三": 1, "白": 2, "点": 1}。
"""
vocab_dic = {}
with open(file_path, 'r', encoding='UTF-8') as f:
for line in tqdm(f):
lin = line.strip()
if not lin:
continue
content = lin.split('\t')[0]
for word in tokenizer(content):
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_dic = {word_count[0]: idx for idx, word_count in enumerate(vocab_list)}
vocab_dic.update({UNK: len(vocab_dic), PAD: len(vocab_dic) + 1})
return vocab_dic
说了这么多,vocab最终有什么用呢?他是在制作制作数据集时,将中文或英文字符/单词表示为数字的形式
✨ 3 word2idx
总效果就是按照词汇分割,并按照词汇表,将字符映射为数字。
比如:我有词汇表vocab={“白”: 1, “三”:2, “点”:3},输入:
input = [
["白三"],
["三点"],
["白点"]
]
那么输出应该为:
output = [
[1, 2],
[2, 3],
[1, 3]
]
上面的例子是最简单的,还有一种是应用了n-gram模型的,下面做一个简单的总结。
3.1 原文展示
原文都是类似如上的,如果有label,将label用split分割出去。
🍿 3.2 分割映射
首先我们应该导入词汇表和训练数据:按照第二节的内容导入或这制作,唯一需要注意的是这里的词汇表是n-gram模型吗!!!影响到后面切片。
⛱️ 3.3 遍历
准备工作做完了,开始遍历,进行切割。就以代码进行总结了。
# 按照行遍历,即3.1节中第一行的内容,第二行的你内容...
for a_sentence,b_sentence in zip(text_a,text_b):
# 存储模型的输入内容,因为是双塔模型,所以两个输入
a,b=[],[]
# 效果是对每一行中的每一个切片,这里在下面进行详细的总结!!!3.4小节
for slice in lst_gram(a_sentence):
# 如果切片内容在词汇表中存在,就把映射到的数据放到结果列表
if slice in slice2idx.keys():
a.append(slice2idx[slice])
# 如果切片的内容不存在,就用映射到[UNK]的数字进行替换
else:
a.append(1) # {"[UNK]": 1,
# 这个和上面的for循环是一样的,如果不是多输入模型,其实就上面一个
for slice in lst_gram(b_sentence):
if slice in slice2idx.keys():
b.append(slice2idx[slice])
else:
b.append(1)
# 这里是由于我们的打包时要求tensor维度一致,因此我们把数据填充到相同大小。3.5小节详细总结!!!
a_list.append(a)
b_list.append(b)
🌭 3.4 切片
到这里总结的原因其实就是上面放不下!!!**因为这里分为很多种,普通的单字符切片,运用了n-gram模型的。**我目前遇到的就上面两个。
如果是单字符切片,其实就for char in sentence即可,其中sentence是一行的数据内容,参照3.1小节!!
但是如果是运用了n-gram,就需要一些代码进行处理了,见下面
def lst_gram(lst, n):
# 返回切片结果
s=[]
# 按照空格分割行数据,可能在别的数据集或按照词进行分割的时候有用,这里真没用!!!
for word in str(lst).lower().split():
# n_gram是主要函数
s.extend(n_gram(word))
# 返回结果
return s
lst为需要分割的行数据,n为n-gram模型的类型,比如2-gram模型,数据类型为int。
str(lst).lower().split()可能在别的数据集或按照词进行分割的时候有用,比如我们一行数据为[“白三 三点 白点”]这里就有用了,遇到再总结吧!!!
def n_gram(word,n=args.N):
s=[]
# 每行数据以#开头和结束
word='#'+word+'#'
# 三个字符分割
for i in range(len(word)-2):
s.append(word[i:i+3])
return s
这里进行的就是n-gram操作了 重点!!!
最终的效果大概如下(随便截的,与上面无关):
☃️3.5 padding
def padding(text,maxlen=args.SENTENCE_MAXLEN):
"""
text:按照行映射完成的数据
maxlen:约定的每行数据的固定长度
"""
# 存储结果
pad_text=[]
# 遍历,得到每行的数据(sentence)
for sentence in text:
# 创建一个固定长度的默认值为0的数组
pad_sentence = np.zeros(maxlen).astype('int64')
cnt=0
# 遍历一行中的每个字符数据,向创建好的固定长度的数组进行填充
for index in sentence:
pad_sentence[cnt]=index
cnt+=1
# 如果达到了最大长度,就结束,这一行后面的内容就省略了。
if cnt== maxlen:
break
# 将一行的数据添加到存储结果的列表中
pad_text.append(pad_sentence.tolist())
return pad_text
特别注意,这里为什么要用0来填充。原因是我们约定,如果长度不够,剩下的用[PAD]来补充,而这里认为[PAD]映射为数字即为0。
🎈 3.6 最终结果展示
这里,a_list代表了文本文件中所有数据的内容。而其中每一个位置都是一行的数据。
至此,文本任务制作数据集的操作就完成了,下面是打包的操作
✨ 4.打包
看过几个NLP项目了,总结一下其中遇到的数据集的创建。
首先,明确数据集最终的使用是以for循环进行遍历。因此最终是一个以batch_size大小可迭代的对象即可(其实pytorch的DateLoader应该就是做着这个工作)。
所以,接下来就有两种办法:
1.重载DataSet并用DataLoader打包
2.自定义迭代器
🎃 4.1 DataLoader打包
如果是DataLoader打包,从第1小节到第3小节的内容应该在重载的DataSet类中完成(该有的操作必须要)。如果是制作迭代器,就没有这个要求了。
这部分其实和图片分类任务是一致的,如果真的需要再来总结!!!
🍿 4.2 迭代器
这可是第一次。创建迭代器最基本的架构如下:
class DatasetIterater(object):
def __init__(self):
def __next__(self):
def __iter__(self):
return self
这里的__next__
和__inter__
见特殊实例总结!!!主要介绍一下init和next函数的工作。
# __init__
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
最重要的就是对batch_size的定义及其延申:
1.定义batch_size大小
2.得到batch_size的尺寸
3.记录batch_size是否为整数
# __next__
其实用DataSet这种现成的就好,支持多线程,速度快,还简单!!!