点击左上方蓝字关注我们
课程简介:
“跟着雨哥学AI”是百度飞桨开源框架近期针对高层API推出的系列课。本课程由多位资深飞桨工程师精心打造,不仅提供了从数据处理、到模型组网、模型训练、模型评估和推理部署全流程讲解;还提供了丰富的趣味案例,旨在帮助开发者更全面清晰地掌握百度飞桨框架的用法,并能够举一反三、灵活使用飞桨框架进行深度学习实践。
嗨,同学们好久不见!我是雨哥,之前的知识大家都有好好掌握吗?本节课我们将学习自然语言处理领域的相关知识。大家都知道,深度学习模型的内部包含了各种各样的数据运算,但是这些运算都是通过数字来进行的;而在自然语言处理的任务中,输入数据都是文本类型。那么我们如何将文本类型的数据转变成模型可以识别的内容,这就是我们本节课要学习的知识啦。针对不同的任务和数据集,数据处理的细节上可能会有所不同,但是大致的流程相似。我们将以NLP任务中的文本分类和命名实体识别任务作为示范,大家可以通过本节课的学习举一反三。
本次课程链接:
https://aistudio.baidu.com/aistudio/projectdetail/1579435
1. 文本分类
文本分类即给定一段文本(也可能是文档级别,在此只讨论句子级别),然后将文本归为N个类别中的一个或者多个。文本分类常见的应用有垃圾邮件识别、情感分析等等。根据类别个数的不同,可以分为二分类和多分类问题。我们今天使用的数据集只包含0、1标签,是一个二分类的任务。多分类任务的过程与此类似,可以参考本教程稍作改动。
1.1 数据集下载
我们选用微博评论数据集为例,正负向评论均包含五万多条。
首先我们对数据集进行解压:
!unzip -q -o data/data69383/weibo_senti_100k.zip
解压后我们可以看到,该数据集包含一个csv文件,里面包含评论句子以及其对应的标签,1表示正向积极的评论,0表示负向消极的评论。
import pandas as pd
import paddle
paddle.set_device('gpu')
content = pd.read_csv('weibo_senti_100k.csv')
content = content.dropna() # 去掉有缺失值的行
content.head(5)
label review
0 1 更博了,爆照了,帅的呀,就是越来越爱你!生快傻缺[爱你][爱你][爱你]
1 1 @张晓鹏jonathan 土耳其的事要认真对待[哈哈],否则直接开除。@丁丁看世界 很是细心...
2 1 姑娘都羡慕你呢…还有招财猫高兴……//@爱在蔓延-JC:[哈哈]小学徒一枚,等着明天见您呢/...
3 1 美~~~~~[爱你]
4 1 梦想有多大,舞台就有多大![鼓掌]
读取数据后,我们将数据处理成[sentence, label]的格式存在列表中,并将数据集打乱。由于此数据集未划分训练集、验证集和测试集,所以我们需要手动划分,作为案例,在这里只取1000条作为训练集,100条作为验证集,大家可以根据自己的需求进行划分。
import random
# 指定seed让每次打乱顺序一样
random.seed(123)
label = content.iloc[:, 0]
text = content.iloc[:, 1]
data = []
for i in range(len(text)):
data.append([text[i], label[i]])
random.shuffle(data)
print('数据集句子数:{}'.format(len(data)))
train_data = data[:1000]
dev_data = data[-100:]
print('训练集句子数:{}'.format(len(train_data)))
print('验证集句子数:{}'.format(len(dev_data)))
数据集句子数:119988
训练集句子数:1000
验证集句子数:100
1.2 构建词表
前面提到了,模型计算使用的都是数字,而我们现在获取到的数据集还是文本类型,如何将文本映射到数据呢?我们将通过这一步构建的词表来进行映射。
# 下载词汇表文件word_dict.txt,用于构造词-id映射关系。
!wget https://paddlenlp.bj.bcebos.com/data/senta_word_dict.txt
# 加载词表
def load_vocab(path):
vocab = {}
with open(path, 'r') as f:
tokens = f.readlines()
for idx, token in enumerate(tokens):
token = token.rstrip("\n").split("\t")[0]
vocab[token] = idx
return vocab
vocab = load_vocab('senta_word_dict.txt')
print('词表大小:{}'.format(len(vocab)))
# 展示词表内容
for i, (k, v) in enumerate(vocab.items()):
if i in range(0, 10):
print(k, v)
词表大小:1256608
[PAD] 0
[UNK] 1
一斤三 2
意面屋 3
11点25分 4
2.0三厢 5
上杭路 6
意大利菜用料 7
菲拉斯 8
還么 9
1.3 数据加载
读取数据之后,需要自定义数据集,实现一个新的Dataset类,继承父类paddle.io.Dataset,并实现父类中的两个抽象方法:__getitem__和__len__。
在__getitem__方法中,我们根据上一步构建的词表,进行了一个词-id的映射,并且根据给定的max_len对句子进行了padding或截断。
import jieba
from paddle.io import Dataset
class TextDataset(Dataset):
def __init__(self, data, vocab, max_len):
super(TextDataset, self).__init__()
self.data = data
self.vocab = vocab
self.max_len = max_len
def __getitem__(self, idx):
sent = self.data[idx][0]
label = self.data[idx][1]
# 利用jieba对中文进行分词,再映射到id
sent_idx = [self.vocab[word] if word in self.vocab else vocab['[UNK]'] for word in jieba.cut(sent)]
# 不够max_len长度的补0,超出的截掉
if len(sent_idx) <= self.max_len:
sent_idx += [vocab['[PAD]'] for _ in range(self.max_len - len(sent_idx))]
else:
sent_idx = sent_idx[:self.max_len]
return sent_idx, label
def __len__(self):
return len(self.data)
def get_labels(self):
return ['0', '1']
train_ds = TextDataset(train_data, vocab, max_len=100)
dev_ds = TextDataset(dev_data, vocab, max_len=100)
print(train_ds[0])
print(dev_ds[0])
([1, 1203981, 269746, 620612, 358475, 340363, 421393, 147537, 115030, 535777, 269746, 300363, 358475, 828868, 828868, 327208, 865881, 661652, 1, 62211, 828868, 828868, 327208, 1, 459120, 62211, 553315, 1057229, 409314, 4783, 828868, 828868, 327208, 346505, 733784, 1231390, 1, 62211, 1, 877224, 1106339, 850865, 389733, 1093154, 1106328, 1, 666731, 932352, 237839, 428598, 147537, 823066, 1106326, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 0)
([1, 202379, 173188, 705655, 749968, 823066, 1106328, 1124478, 991056, 1084488, 1, 340521, 173188, 1, 348895, 1106339, 382479, 421393, 166145, 1093154, 136954, 269746, 365925, 358475, 4783, 977896, 511894, 823116, 1208194, 1211275, 115414, 173188, 489131, 667149, 1106339, 489131, 1023964, 1106328, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 0)
1.4 模型训练
在数据处理好之后,我们就可以根据自己定义的模型进行训练啦,在这里仅讨论数据处理部分,模型的组建大家可以参考我们之前的教程。
# 省略模型组建及实例化过程
# 模型训练
model.prepare(optimizer, loss, metrics)
model.fit(train_ds, dev_ds, epochs=50, batch_size=32, verbose=1)
2. 命名实体识别
命名实体识别(Named Entity Recognition,NER)是NLP中一项非常基础的任务,是信息提取、问答系统、句法分析、机器翻译等众多NLP任务的重要基础工具,其准确度决定了下游任务的效果,是NLP中非常重要的一个基础问题。首先,我们需要了解实体的概念,包括人名、地名、组织结构名以及其他专有名词。根据数据集的不同,实体类别的个数也不相同。例如,本例中使用的CoNLL2003数据集只包含4种实体类别,而另一个NER任务的常用数据集OntoNotes5.0则包含18种实体类别。
2.1 数据集下载
我们采用命名实体识别常用数据集CoNLL2003,该数据集已经为我们划分好训练集、验证集以及测试集。数据集内格式为[word, label]:
SOCCER O
- O
JAPAN B-LOC
GET O
LUCKY O
WIN O
, O
CHINA B-PER
IN O
SURPRISE O
DEFEAT O
. O
此数据集包含四中实体类别,分别为人名(PER)、地名(LOC)、组织机构名(ORG)、其他(MISC)。并且采用BIO的标注方式,B表示实体的起始单词,I表示实体内部单词,O表示非实体。
import paddle
import numpy as np
def load_data(path):
data = list()
with open(path, 'r') as f:
words, labels = list(), list()
for line in f:
line = line.strip()
if line:
w, l = line.split()
words.append(w)
labels.append(l)
else:
data.append([words, labels])
words, labels = list(), list()
if words:
data.append([words, labels])
return data
train_data = load_data('data/data7933/train.txt')
dev_data = load_data('data/data7933/dev.txt')
print('训练集句子数:{}'.format(len(train_data)))
print(train_data[0])
print('验证集句子数:{}'.format(len(dev_data)))
print(dev_data[0])
训练集句子数:14986
[['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.'], ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']]
验证集句子数:3465
[['CRICKET', '-', 'LEICESTERSHIRE', 'TAKE', 'OVER', 'AT', 'TOP', 'AFTER', 'INNINGS', 'VICTORY', '.'], ['O', 'O', 'B-ORG', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O']]
2.2 构建词表
这里我们采用手动构建词表的方式,读取训练集中的数据,分别构建词-id、标签-id的映射关系。
# 根据训练集的数据构建词表
def create_vocab(data, save_path):
word_list = ['[PAD]', '[UNK]']
label_list = []
for i in range(len(data)):
for word in data[i][0]:
if word not in word_list:
word_list.append(word)
for label in data[i][1]:
if label not in label_list:
label_list.append(label)
with open(save_path + 'word_dict.txt', 'w') as file1:
for i, w in enumerate(word_list):
file1.write(w + ' ' + str(i) + '\n')
with open(save_path + 'label_dict.txt', 'w') as file2:
for i, l in enumerate(label_list):
file2.write(l + ' ' + str(i) + '\n')
create_vocab(train_data, 'data/data7933/')
def load_vocab(word_dict_path, label_dict_path):
word_dict, label_dict = {}, {}
with open(word_dict_path,'r') as f1:
for line in f1:
word, idx = line.strip().split()
word_dict[word] = idx
with open(label_dict_path, 'r') as f2:
for line in f2:
label, idx = line.strip().split()
label_dict[label] = idx
return word_dict, label_dict
word_dict, label_dict = load_vocab('data/data7933/word_dict.txt', 'data/data7933/label_dict.txt')
print('词表大小:{}'.format(len(word_dict)))
print('标签个数:{}'.format(len(label_dict)))
词表大小:23626
标签个数:9
2.3 数据加载
PaddleNLP中提供了很多用于文本处理的接口,这里我们结合PaddleNLP进行数据集的构建以及加载。
# 首先安装paddlenlp
!pip install paddlenlp==2.0.0b
import paddlenlp
from paddlenlp.data import Stack, Tuple, Pad
# 将词或标签转换为id
def convert_tokens_to_ids(tokens, vocab, unk_token=None):
token_ids = []
unk_id = vocab.get(unk_token) if unk_token else None
for token in tokens:
token_id = vocab.get(token, unk_id)
token_ids.append(token_id)
return token_ids
# 自定义数据集
class NERDataset(paddle.io.Dataset):
def __init__(self, data, word_dict, label_dict):
self.data = data
self.word_dict = word_dict
self.label_dict = label_dict
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
words = self.data[idx][0]
labels = self.data[idx][1]
word_ids = convert_tokens_to_ids(words, self.word_dict, unk_token='[UNK]')
label_ids = convert_tokens_to_ids(labels, self.label_dict)
return word_ids, len(word_ids), label_ids
train_ds = NERDataset(train_data, word_dict, label_dict)
dev_ds = NERDataset(dev_data, word_dict, label_dict)
print(train_ds[0])
(['2', '3', '4', '5', '6', '7', '8', '9', '10'], 9, ['0', '1', '2', '1', '1', '1', '2', '1', '1'])
# 构建dataloader
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=train_ds.word_dict['[PAD]']), # word
Stack(), # seq_len
Pad(axis=0, pad_val=train_ds.word_dict['[PAD]']) # label
): fn(samples)
train_loader = paddle.io.DataLoader(
dataset=train_ds,
batch_size=32,
shuffle=True,
drop_last=True,
return_list=True,
collate_fn=batchify_fn
)
dev_loader = paddle.io.DataLoader(
dataset=dev_ds,
batch_size=32,
drop_last=True,
return_list=True,
collate_fn=batchify_fn
)
print(len(train_loader))
print(len(dev_loader))
468
108
2.4 模型训练
同样的,我们处理好数据之后,根据自己的需要组建模型,然后就可以进行训练啦。
# 省略模型组建及实例化过程
# 模型训练
model.prepare(optimizer, loss, metrics)
model.fit(train_loader, dev_loader, epochs=50, verbose=1)
总结
本节课和大家一起学习了如何对NLP任务中的数据进行预处理,下节课同学们想实现什么趣味案例呢?欢迎大家在评论区告诉我,我们将会在后续的课程中给大家安排上哈,今天的课程到这里就结束了,我是雨哥,下节课再见啦~
欢迎关注飞桨框架高层API官方账号:飞桨PaddleHapi
https://aistudio.baidu.com/aistudio/personalcenter/thirdview/564527
有任何问题可以在本项目中评论或到飞桨Github仓库提交Issue。
同时欢迎扫码加入飞桨框架高层API技术交流群
回顾往期:
『跟着雨哥学AI』系列之五:快速上手趣味案例FashionMNIST
『跟着雨哥学AI』系列之六:趣味案例——基于U-Net的宠物图像分割
如果您想详细了解更多飞桨的相关内容,请参阅以下文档。
·飞桨官网地址·
https://www.paddlepaddle.org.cn/
·飞桨开源框架项目地址·
GitHub: https://github.com/PaddlePaddle/Paddle
Gitee: https://gitee.com/paddlepaddle/Paddle
????长按上方二维码立即star!????
飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,是中国首个开源开放、技术领先、功能完备的产业级深度学习平台,包括飞桨开源平台和飞桨企业版。飞桨开源平台包含核心框架、基础模型库、端到端开发套件与工具组件,持续开源核心能力,为产业、学术、科研创新提供基础底座。飞桨企业版基于飞桨开源平台,针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。
END