快来选一顿好吃的年夜饭:看看如何自定义数据集,实现文本分类中的情感分析任务
情感分析是自然语言处理领域一个老生常谈的任务。句子情感分析目的是为了判别说者的情感倾向,比如在某些话题上给出的的态度明确的观点,或者反映的情绪状态等。情感分析有着广泛应用,比如电商评论分析、舆情分析等。
题目:
将lstm网络替换成其他网络。可参考seq2vec介绍
提示位置:self.lstm_encoder = ppnlp.seq2vec.LSTMEncoder()
环境介绍
-
PaddlePaddle框架,AI Studio平台已经默认安装最新版2.0。
-
PaddleNLP,深度兼容框架2.0,是飞桨框架2.0在NLP领域的最佳实践。
这里使用的是beta版本,马上也会发布rc版哦。AI Studio平台后续会默认安装PaddleNLP,在此之前可使用如下命令安装。
# 下载paddlenlp
!pip install --upgrade paddlenlp==2.0.0b4
查看安装的版本
import paddle
import paddlenlp
print(paddle.__version__, paddlenlp.__version__)
2.0.0 2.0.0b4
PaddleNLP和Paddle框架是什么关系?
- Paddle框架是基础底座,提供深度学习任务全流程API。PaddleNLP基于Paddle框架开发,适用于NLP任务。
PaddleNLP中数据处理、数据集、组网单元等API未来会沉淀到框架paddle.text
中。
- 代码中继承
class TSVDataset(paddle.io.Dataset)
使用飞桨完成深度学习任务的通用流程
-
数据集和数据处理
paddle.io.Dataset
paddle.io.DataLoader
paddlenlp.data -
组网和网络配置
paddle.nn.Embedding
paddlenlp.seq2vec
paddle.nn.Linear
paddle.tanh
paddle.nn.CrossEntropyLoss
paddle.metric.Accuracy
paddle.optimizer
model.prepare
-
网络训练和评估
model.fit
model.evaluate -
预测
model.predict
注意:建议在GPU下运行。
import numpy as np
from functools import partial
import paddle.nn as nn
import paddle.nn.functional as F
import paddlenlp as ppnlp
from paddlenlp.data import Pad, Stack, Tuple
from paddlenlp.datasets import MapDatasetWrapper
from utils import load_vocab, convert_example
数据集和数据处理
自定义数据集
映射式(map-style)数据集需要继承paddle.io.Dataset
-
__getitem__
: 根据给定索引获取数据集中指定样本,在 paddle.io.DataLoader 中需要使用此函数通过下标获取样本。 -
__len__
: 返回数据集样本个数, paddle.io.BatchSampler 中需要样本个数生成下标序列。
class SelfDefinedDataset(paddle.io.Dataset):
def __init__(self, data):
super(SelfDefinedDataset, self).__init__()
self.data = data
def __getitem__(self, idx):
return self.data[idx]
def __len__(self):
return len(self.data)
def get_labels(self):
return ["0", "1"]
def txt_to_list(file_name):
res_list = []
for line in open(file_name):
res_list.append(line.strip().split('\t'))
return res_list
trainlst = txt_to_list('train.txt')
devlst = txt_to_list('dev.txt')
testlst = txt_to_list('test.txt')
# 通过get_datasets()函数,将list数据转换为dataset。
# get_datasets()可接收[list]参数,或[str]参数,根据自定义数据集的写法自由选择。
# train_ds, dev_ds, test_ds = ppnlp.datasets.ChnSentiCorp.get_datasets(['train', 'dev', 'test'])
train_ds, dev_ds, test_ds = SelfDefinedDataset.get_datasets([trainlst, devlst, testlst])
看看数据长什么样
label_list = train_ds.get_labels()
print(label_list)
for i in range(10):
print (train_ds[i])
['0', '1']
['赢在心理,输在出品!杨枝太酸,三文鱼熟了,酥皮焗杏汁杂果可以换个名(九唔搭八)', '0']
['服务一般,客人多,服务员少,但食品很不错', '1']
['東坡肉竟然有好多毛,問佢地點解,佢地仲話係咁架\ue107\ue107\ue107\ue107\ue107\ue107\ue107冇天理,第一次食東坡肉有毛,波羅包就幾好食', '0']
['父亲节去的,人很多,口味还可以上菜快!但是结账的时候,算错了没有打折,我也忘记拿清单了。说好打8折的,收银员没有打,人太多一时自己也没有想起。不知道收银员忘记,还是故意那钱露入自己钱包。。', '0']
['吃野味,吃个新鲜,你当然一定要来广州吃鹿肉啦*价格便宜,量好足,', '1']
['味道几好服务都五错推荐鹅肝乳鸽飞鱼', '1']
['作为老字号,水准保持算是不错,龟岗分店可能是位置问题,人不算多,基本不用等位,自从抢了券,去过好几次了,每次都可以打85以上的评分,算是可以了~粉丝煲每次必点,哈哈,鱼也不错,还会来帮衬的,楼下还可以免费停车!', '1']
['边到正宗啊?味味都咸死人啦,粤菜讲求鲜甜,五知点解感多人话好吃。', '0']
['环境卫生差,出品垃圾,冇下次,不知所为', '0']
['和苑真是精致粤菜第一家,服务菜品都一流', '1']
数据处理
为了将原始数据处理成模型可以读入的格式,本项目将对数据作以下处理:
- 首先使用jieba切词,之后将jieba切完后的单词映射词表中单词id。
- 使用
paddle.io.DataLoader
接口多线程异步加载数据。
其中用到了PaddleNLP中关于数据处理的API。PaddleNLP提供了许多关于NLP任务中构建有效的数据pipeline的常用API
API | 简介 |
---|---|
paddlenlp.data.Stack | 堆叠N个具有相同shape的输入数据来构建一个batch,它的输入必须具有相同的shape,输出便是这些输入的堆叠组成的batch数据。 |
paddlenlp.data.Pad | 堆叠N个输入数据来构建一个batch,每个输入数据将会被padding到N个输入数据中最大的长度 |
paddlenlp.data.Tuple | 将多个组batch的函数包装在一起 |
更多数据处理操作详见: https://github.com/PaddlePaddle/PaddleNLP/blob/develop/docs/data.md
# 下载词汇表文件word_dict.txt,用于构造词-id映射关系。
!wget https://paddlenlp.bj.bcebos.com/data/senta_word_dict.txt
# 加载词表
vocab = load_vocab('./senta_word_dict.txt')
for k, v in vocab.items():
print(k, v)
break
构造dataloder
下面的create_data_loader
函数用于创建运行和预测时所需要的DataLoader
对象。
-
paddle.io.DataLoader
返回一个迭代器,该迭代器根据batch_sampler
指定的顺序迭代返回dataset数据。异步加载数据。 -
batch_sampler
:DataLoader通过 batch_sampler 产生的mini-batch索引列表来 dataset 中索引样本并组成mini-batch -
collate_fn
:指定如何将样本列表组合为mini-batch数据。传给它参数需要是一个callable对象,需要实现对组建的batch的处理逻辑,并返回每个batch的数据。在这里传入的是prepare_input
函数,对产生的数据进行pad操作,并返回实际长度等。
# Reads data and generates mini-batches.
def create_dataloader(dataset,
trans_function=None,
mode='train',
batch_size=1,
pad_token_id=0,
batchify_fn=None):
if trans_function:
dataset = dataset.apply(trans_function, lazy=True)
# return_list 数据是否以list形式返回
# collate_fn 指定如何将样本列表组合为mini-batch数据。传给它参数需要是一个callable对象,需要实现对组建的batch的处理逻辑,并返回每个batch的数据。在这里传入的是`prepare_input`函数,对产生的数据进行pad操作,并返回实际长度等。
dataloader = paddle.io.DataLoader(
dataset,
return_list=True,
batch_size=batch_size,
collate_fn=batchify_fn)
return dataloader
# python中的偏函数partial,把一个函数的某些参数固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
trans_function = partial(
convert_example,
vocab=vocab,
unk_token_id=vocab.get('[UNK]', 1),
is_test=False)
# 将读入的数据batch化处理,便于模型batch化运算。
# batch中的每个句子将会padding到这个batch中的文本最大长度batch_max_seq_len。
# 当文本长度大于batch_max_seq时,将会截断到batch_max_seq_len;当文本长度小于batch_max_seq时,将会padding补齐到batch_max_seq_len.
batchify_fn = lambda samples, fn=Tuple(
Pad(axis=0, pad_val=vocab['[PAD]']), # input_ids
Stack(dtype="int64"), # seq len
Stack(dtype="int64") # label
): [data for data in fn(samples)]
train_loader = create_dataloader(
train_ds,
trans_function=trans_function,
batch_size=128,
mode='train',
batchify_fn=batchify_fn)
dev_loader = create_dataloader(
dev_ds,
trans_function=trans_function,
batch_size=128,
mode='validation',
batchify_fn=batchify_fn)
test_loader = create_dataloader(
test_ds,
trans_function=trans_function,
batch_size=128,
mode='test',
batchify_fn=batchify_fn)
模型搭建
使用LSTMencoder
搭建一个BiLSTM模型用于进行句子建模,得到句子的向量表示。
然后接一个线性变换层,完成二分类任务。
paddle.nn.Embedding
组建word-embedding层ppnlp.seq2vec.LSTMEncoder
组建句子建模层paddle.nn.Linear
构造二分类器
- 除LSTM外,
seq2vec
还提供了许多语义表征方法,详细可参考:seq2vec介绍
class LSTMModel(nn.Layer):
def __init__(self,
vocab_size,
num_classes,
emb_dim=128,
padding_idx=0,
lstm_hidden_size=198,
direction='forward',
lstm_layers=1,
dropout_rate=0,
pooling_type=None,
fc_hidden_size=96):
super().__init__()
# 首先将输入word id 查表后映射成 word embedding
self.embedder = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=emb_dim,
padding_idx=padding_idx)
# 将word embedding经过LSTMEncoder变换到文本语义表征空间中
self.lstm_encoder = ppnlp.seq2vec.LSTMEncoder(
emb_dim,
lstm_hidden_size,
num_layers=lstm_layers,
direction=direction,
dropout=dropout_rate,
pooling_type=pooling_type)
# LSTMEncoder.get_output_dim()方法可以获取经过encoder之后的文本表示hidden_size
self.fc = nn.Linear(self.lstm_encoder.get_output_dim(), fc_hidden_size)
# 最后的分类器
self.output_layer = nn.Linear(fc_hidden_size, num_classes)
def forward(self, text, seq_len):
# text shape: (batch_size, num_tokens)
# print('input :', text.shape)
# Shape: (batch_size, num_tokens, embedding_dim)
embedded_text = self.embedder(text)
# print('after word-embeding:', embedded_text.shape)
# Shape: (batch_size, num_tokens, num_directions*lstm_hidden_size)
# num_directions = 2 if direction is 'bidirectional' else 1
text_repr = self.lstm_encoder(embedded_text, sequence_length=seq_len)
# print('after lstm:', text_repr.shape)
# Shape: (batch_size, fc_hidden_size)
fc_out = paddle.tanh(self.fc(text_repr))
# print('after Linear classifier:', fc_out.shape)
# Shape: (batch_size, num_classes)
logits = self.output_layer(fc_out)
# print('output:', logits.shape)
# probs 分类概率值
probs = F.softmax(logits, axis=-1)
# print('output probability:', probs.shape)
return probs
model= LSTMModel(
len(vocab),
len(label_list),
direction='bidirectional',
padding_idx=vocab['[PAD]'])
model = paddle.Model(model)
模型配置和训练
模型配置
optimizer = paddle.optimizer.Adam(
parameters=model.parameters(), learning_rate=5e-5)
loss = paddle.nn.CrossEntropyLoss()
metric = paddle.metric.Accuracy()
model.prepare(optimizer, loss, metric)
# 设置visualdl路径
log_dir = './visualdl'
callback = paddle.callbacks.VisualDL(log_dir=log_dir)
模型训练
训练过程中会输出loss、acc等信息。这里设置了10个epoch,在训练集上准确率约97%。
model.fit(train_loader, dev_loader, epochs=20, save_dir='./checkpoints', save_freq=5, callbacks=callback)
启动VisualDL查看训练过程可视化结果
启动步骤:
- 1、切换到本界面左侧「可视化」
- 2、日志文件路径选择 ‘visualdl’
- 3、点击「启动VisualDL」后点击「打开VisualDL」,即可查看可视化结果:
Accuracy和Loss的实时变化趋势如下:
results = model.evaluate(dev_loader)
print("Finally test acc: %.5f" % results['acc'])
Finally test acc: 0.96628
预测
label_map = {0: 'negative', 1: 'positive'}
results = model.predict(test_loader, batch_size=128)[0]
predictions = []
for batch_probs in results:
# 映射分类label
idx = np.argmax(batch_probs, axis=-1)
idx = idx.tolist()
labels = [label_map[i] for i in idx]
predictions.extend(labels)
# 看看预测数据前5个样例分类结果
for idx, data in enumerate(test_ds.data[:10]):
for idx, data in enumerate(test_ds.data[:10]):
print('Data: {} \t Label: {}'.format(data[0], predictions[idx]))
Predict begin...
step 42/42 [==============================] - ETA: 4s - 103ms/st - ETA: 3s - 101ms/st - ETA: 3s - 97ms/step - ETA: 3s - 99ms/ste - ETA: 3s - 97ms/ste - ETA: 2s - 92ms/ste - ETA: 2s - 88ms/ste - ETA: 2s - 85ms/ste - ETA: 1s - 82ms/ste - ETA: 1s - 80ms/ste - ETA: 1s - 78ms/ste - ETA: 1s - 76ms/ste - ETA: 1s - 75ms/ste - ETA: 1s - 74ms/ste - ETA: 0s - 73ms/ste - ETA: 0s - 73ms/ste - ETA: 0s - 73ms/ste - ETA: 0s - 72ms/ste - ETA: 0s - 70ms/ste - ETA: 0s - 68ms/ste - 65ms/step
Predict samples: 5353
Data: 楼面经理服务态度极差,等位和埋单都差,楼面小妹还挺好 Label: negative
Data: 欺负北方人没吃过鲍鱼是怎么着?简直敷衍到可笑的程度,团购连青菜都是两人份?!难吃到死,菜色还特别可笑,什么时候粤菜的小菜改成拍黄瓜了?!把团购客人当傻子,可这满大厅的傻子谁还会再来?! Label: negative
Data: 如果大家有时间而且不怕麻烦的话可以去这里试试,点一个饭等左2个钟,没错!是两个钟!期间催了n遍都说马上到,结果?呵呵。乳鸽的味道,太咸,可能不新鲜吧……要用重口味盖住异味。上菜超级慢!中途还搞什么表演,麻烦有人手的话就上菜啊,表什么演?!?!要大家饿着看表演吗?最后结账还算错单,我真心服了……有一种店叫不会有下次,大概就是指它吧 Label: negative
Data: 偌大的一个大厅就一个人点菜,点菜速度超级慢,菜牌上多个菜停售,连续点了两个没标停售的菜也告知没有,粥上来是凉的,榴莲酥火大了,格格肉超级油腻而且咸?????? Label: negative
Data: 泥撕雞超級好吃!!!吃了一個再叫一個還想打包的節奏! Label: positive
Data: 作为地道的广州人,从小就跟着家人在西关品尝各式美食,今日带着家中长辈来这个老字号泮溪酒家真实失望透顶,出品差、服务差、洗手间邋遢弥漫着浓郁尿骚味、丢广州人的脸、丢广州老字号的脸。 Label: negative
Data: 辣味道很赞哦!猪肚鸡一直是我们的最爱,每次来都必点,服务很给力,环境很好,值得分享哦!西洋菜 Label: positive
Data: 第一次吃到這麼脏的火鍋:吃着吃著吃出一條尾指粗的黑毛毛蟲——惡心!脏!!!第一次吃到這麼無誠信的火鍋服務:我們呼喚人員時,某女部長立即使服務員迅速取走蟲所在的碗,任我們多次叫「放下」論理,她們也置若罔聞轉身將蟲毁屍滅跡,還嘻皮笑臉辯稱只是把碗換走,態度行為惡劣——奸詐!毫無誠信!!爛!!!當然還有剛坐下時的情形:第一次吃到這樣的火鍋:所有肉食熟食都上桌了,鍋底遲遲沒上,足足等了半小時才姍姍來遲;---差!!第一次吃到這樣的火鍋:1元雞鍋、1碟6塊小牛肉、1碟小腐皮、1碟5塊裝的普通肥牛、1碟數片的細碎牛肚結帳便2百多元;---不值!!以下省略千字差評......白云路的稻香是最差、最失禮的稻香,天河城、華廈的都比它好上過萬倍!!白云路的稻香是史上最差的餐廳!!! Label: negative
Data: 文昌鸡份量很少且很咸,其他菜味道很一般!服务态度差差差!还要10%的服务费、 Label: negative
Data: 这个网站的评价真是越来越不可信了,搞不懂为什么这么多好评。真的是很一般,不要迷信什么哪里回来的大厨吧。环境和出品若是当作普通茶餐厅来看待就还说得过去,但是价格又不是茶餐厅的价格,这就很尴尬了。。服务也是有待提高。 Label: negative
这里只采用了一个基础的模型,就得到了较高的的准确率。
可以试试预训练模型,能得到更好的效果!参考如何通过预训练模型Fine-tune下游任务
PaddleNLP 更多项目
- 瞧瞧怎么使用PaddleNLP内置数据集-基于seq2vec的情感分析
- 如何通过预训练模型Fine-tune下游任务
- 使用BiGRU-CRF模型完成快递单信息抽取
- 使用预训练模型ERNIE优化快递单信息抽取
- 使用Seq2Seq模型完成自动对联
- 使用预训练模型ERNIE-GEN实现智能写诗
- 使用TCN网络完成新冠疫情病例数预测
- 使用预训练模型完成阅读理解
加入交流群,一起学习吧
现在就加入PaddleNLP的QQ技术交流群,一起交流NLP技术吧!