『NLP练习赛』中文新闻标题分类 baseline

试题说明
基于THUCNews数据集的文本分类, THUCNews是根据新浪新闻RSS订阅频道2005~2011年间的历史数据筛选过滤生成,包含74万篇新闻文档,参赛者需要根据新闻标题的内容用算法来判断该新闻属于哪一类别。

数据说明
THUCNews是根据新浪新闻RSS订阅频道2005~2011年间的历史数据筛选过滤生成,包含74万篇新闻文档(2.19 GB),均为UTF-8纯文本格式。在原始新浪新闻分类体系的基础上,重新整合划分出14个候选分类类别:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐。

训练集,验证集按照“原文标题+\t+标签”的格式抽取出来,可以直接根据新闻标题进行文本分类任务,希望答题者能够给出自己的解决方案。

测试集仅提供“原文标题”,答题者需要对其预测相应的分类类别。

训练集:data/train.txt

验证集:data/dev.txt

测试集:data/test.txt

提交答案
考试提交,需要提交模型代码项目版本和结果文件。结果文件为TXT文件格式,命名为result.txt,文件内的字段需要按照指定格式写入。

1.每个类别的行数和测试集原始数据行数应一一对应,不可乱序

2.输出结果应检查是否为83599行数据,否则成绩无效

3.输出结果文件命名为result.txt,一行一个类别,样例如下:

···

游戏

财经

时政

股票

家居

科技 ···

基线系统
数据处理
构建词汇表
在搭建模型之前,我们需要对整体语料构造词表。通过切词统计词频,去除低频词,从而完成构造词表。我们使用jieba作为中文切词工具。

停用词表,我们从网上直接获取:https://github.com/goto456/stopwords/blob/master/baidu_stopwords.txt

In [1]
!pip install --upgrade paddlenlp 
!pip install paddlepaddle
In [2]
import os
import time
from collections import Counter
from itertools import chain

import jieba


def sort_and_write_words(all_words, file_path):
    words = list(chain(*all_words))
    words_vocab = Counter(words).most_common()
    with open(file_path, "w", encoding="utf8") as f:
        f.write('[UNK]\n[PAD]\n')
        # filter the count of words below 5
        # 过滤低频词,词频<5
        for word, num in words_vocab:
            if num < 5:
                continue
            f.write(word + "\n")


(root, directory, files), = list(os.walk("./work/data"))
all_words = []
for file_name in files:
    with open(os.path.join(root, file_name), "r", encoding="utf8") as f:
        for line in f:
            if file_name in ["train.txt", "dev.txt"]:
                text, label = line.strip().split("\t")
            elif file_name == "test.txt":
                text = line.strip()
            else:
                continue
            words = jieba.lcut(text)
            words = [word for word in words if word.strip() !='']
            all_words.append(words)

# 写入词表
sort_and_write_words(all_words, "work/data/vocab.txt")
Building prefix dict from the default dictionary ...
Dumping model to file cache /tmp/jieba.cache
Loading model cost 0.812 seconds.
Prefix dict has been built successfully.
In [3]
# 词汇表大小
!wc -l work/data/vocab.txt
# 停用词表大小
!wc -l work/data/stop_words.txt
79441 work/data/vocab.txt
1395 work/data/stop_words.txt
加载自定义数据集
构建词汇表完毕之后,我们可以加载自定义数据集。加载自定义数据集可以通过继承paddle.io.Dataset完成。

更多自定义数据集方式参考:自定义数据集

同时,PaddleNLP提供了文本分类、序列标注、阅读理解等多种任务的常用数据集,一键即可加载,详细信息参考数据集:

In [4]
import paddle

class NewsData(paddle.io.Dataset):
    def __init__(self, data_path, mode="train"):
        is_test = True if mode == "test" else False
        self.label_map = { item:index for index, item in enumerate(self.label_list)}
        self.examples = self._read_file(data_path, is_test)

    def _read_file(self, data_path, is_test):
        examples = []
        with open(data_path, 'r', encoding='utf-8') as f:
            for line in f:
                if is_test:
                    text = line.strip()
                    examples.append((text,))
                else:
                    text, label = line.strip('\n').split('\t')
                    label = self.label_map[label]
                    examples.append((text, label))
        return examples

    def __getitem__(self, idx):
        return self.examples[idx]

    def __len__(self):
        return len(self.examples)

    @property
    def label_list(self):
        return ['财经', '彩票', '房产', '股票', '家居', '教育', '科技', '社会', '时尚', '时政', '体育', '星座', '游戏', '娱乐']
In [5]
# Loads dataset.
train_ds = NewsData("work/data/train.txt", mode="train")
dev_ds = NewsData("work/data/dev.txt", mode="dev")
test_ds = NewsData("work/data/test.txt", mode="test")

print("Train data:")
for text, label in train_ds[:5]:
    print(f"Text: {text}; Label ID {label}")

print()
print("Test data:")
for text, in test_ds[:5]:
    print(f"Text: {text}")
Train data:
Text: 网易第三季度业绩低于分析师预期; Label ID 6
Text: 巴萨1年前地狱重现这次却是天堂 再赴魔鬼客场必翻盘; Label ID 10
Text: 美国称支持向朝鲜提供紧急人道主义援助; Label ID 9
Text: 增资交银康联 交行夺参股险商首单; Label ID 3
Text: 午盘:原材料板块领涨大盘; Label ID 3

Test data:
Text: 北京君太百货璀璨秋色 满100353020元
Text: 教育部:小学高年级将开始学习性知识
Text: 专业级单反相机 佳能7D单机售价9280元
Text: 星展银行起诉内地客户 银行强硬客户无奈
Text: 脱离中国的实际 强压人民币大幅升值只能是梦想
读入数据
加载数据集之后,还需要将原始文本转化为word id,读入数据。

PaddleNLP提供了许多关于NLP任务中构建有效的数据pipeline的常用API

API	简介
paddlenlp.data.Stack	堆叠N个具有相同shape的输入数据来构建一个batch
paddlenlp.data.Pad	将长度不同的多个句子padding到统一长度,取N个输入数据中的最大长度
paddlenlp.data.Tuple	将多个batchify函数包装在一起
更多数据处理操作详见: https://github.com/PaddlePaddle/PaddleNLP/blob/develop/docs/data.md

In [6]
from paddlenlp.data import Stack, Pad, Tuple
a = [1, 2, 3, 4]
b = [3, 4, 5, 6]
c = [5, 6, 7, 8]
result = Stack()([a, b, c])
print("Stacked Data: \n", result)
print()

a = [1, 2, 3, 4]
b = [5, 6, 7]
c = [8, 9]
result = Pad(pad_val=0)([a, b, c])
print("Padded Data: \n", result)
print()

data = [
        [[1, 2, 3, 4], [1]],
        [[5, 6, 7], [0]],
        [[8, 9], [1]],
       ]
batchify_fn = Tuple(Pad(pad_val=0), Stack())
ids, labels = batchify_fn(data)
print("ids: \n", ids)
print()
print("labels: \n", labels)
print()
Stacked Data: 
 [[1 2 3 4]
 [3 4 5 6]
 [5 6 7 8]]

Padded Data: 
 [[1 2 3 4]
 [5 6 7 0]
 [8 9 0 0]]

ids: 
 [[1 2 3 4]
 [5 6 7 0]
 [8 9 0 0]]

labels: 
 [[1]
 [0]
 [1]]

本基线将对数据作以下处理:

将原始数据处理成模型可以读入的格式。首先使用jieba切词,之后将jieba切完后的单词映射词表中单词id。

使用paddle.io.DataLoader接口多线程异步加载数据。

In [7]
from functools import partial

import paddlenlp
from paddlenlp.datasets import MapDataset

from utils import convert_example, read_vocab, write_results


def create_dataloader(dataset,
                      trans_fn=None,
                      mode='train',
                      batch_size=1,
                      use_gpu=False,
                      batchify_fn=None):
    if trans_fn:
        dataset = MapDataset(dataset)
        dataset = dataset.map(trans_fn)

    if mode == 'train' and use_gpu:
        sampler = paddle.io.DistributedBatchSampler(
            dataset=dataset, batch_size=batch_size, shuffle=True)
    else:
        shuffle = True if mode == 'train' else False
        sampler = paddle.io.BatchSampler(
            dataset=dataset, batch_size=batch_size, shuffle=shuffle)
    dataloader = paddle.io.DataLoader(
        dataset,
        batch_sampler=sampler,
        return_list=True,
        collate_fn=batchify_fn)
    return dataloader
In [8]
vocab = read_vocab("work/data/vocab.txt")
stop_words = read_vocab("work/data/stop_words.txt")

batch_size = 128
epochs = 2

trans_fn = partial(convert_example, vocab=vocab, stop_words=stop_words, is_test=False)
batchify_fn = lambda samples, fn=Tuple(
    Pad(axis=0, pad_val=vocab.get('[PAD]', 0)),  # input_ids
    Stack(dtype="int64"),  # seq len
    Stack(dtype="int64")  # label
): [data for data in fn(samples)]
train_loader = create_dataloader(
    train_ds,
    trans_fn=trans_fn,
    batch_size=batch_size,
    mode='train',
    use_gpu=True,
    batchify_fn=batchify_fn)
dev_loader = create_dataloader(
    dev_ds,
    trans_fn=trans_fn,
    batch_size=batch_size,
    mode='validation',
    use_gpu=True,
    batchify_fn=batchify_fn)
组网、配置、训练
定义模型结构
读入了数据之后,即可定义模型结构。此处,我们选择BiLSTM作为baseline。

PaddleNLP提供了序列化建模模块paddlenlp.seq2vec模块,该模块可以将文本抽象成一个携带语义的文本向量。

关于seq2vec模块更多信息参考:paddlenlp.seq2vec是什么?快来看看如何用它完成情感分析任务

本基线模型选用LSTMencoder搭建一个BiLSTM模型用于文本分类任务。

paddle.nn.Embedding组建word-embedding层
paddlenlp.seq2vec.LSTMEncoder组建句子建模层
paddle.nn.Linear构造二分类器
In [9]
import paddle.nn as nn
import paddle.nn.functional as F

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.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 = paddlenlp.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):
        # Shape: (batch_size, num_tokens, embedding_dim)
        embedded_text = self.embedder(text)

        # 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)


        # Shape: (batch_size, fc_hidden_size)
        fc_out = paddle.tanh(self.fc(text_repr))

        # Shape: (batch_size, num_classes)
        logits = self.output_layer(fc_out)
        return logits

model= LSTMModel(
        len(vocab),
        len(train_ds.label_list),
        direction='bidirectional',
        padding_idx=vocab['[PAD]'])
model = paddle.Model(model)
训练
数据读入模型构建完毕,定义优化器,选择学习率和评价指标,我们即可开始训练。

根据比赛评价规则,此处选用准确率Accuracy作为评价指标。

模型训练模型之后模型参数会自动保存在ckpt文件夹下。

In [10]
optimizer = paddle.optimizer.Adam(
    parameters=model.parameters(), learning_rate=5e-4)

# Defines loss and metric.
criterion = paddle.nn.CrossEntropyLoss()
metric = paddle.metric.Accuracy()

model.prepare(optimizer, criterion, metric)

# Starts training and evaluating.
model.fit(train_loader, dev_loader, epochs=epochs, save_dir='./ckpt')


预测
训练模型之后,我们可以利用当前训练的模型对测试集数据进行预测,并写入预测结果至result.txt文件中。

之后将结果文件提交至课程或比赛区即可看到成绩噢!

In [11]
import numpy as np

test_batchify_fn = lambda samples, fn=Tuple(
    Pad(axis=0, pad_val=vocab.get('[PAD]', 0)),  # input_ids
    Stack(dtype="int64"),  # seq len
): [data for data in fn(samples)]
test_loader = create_dataloader(
    test_ds,
    trans_fn=partial(convert_example, vocab=vocab, stop_words=stop_words, is_test=True),
    batch_size=batch_size,
    mode='test',
    use_gpu=True,
    batchify_fn=test_batchify_fn)

# Does predict.
results = model.predict(test_loader)
inverse_lable_map = {value:key for key, value in test_ds.label_map.items()}
all_labels = []
for batch_results in results[0]:
    label_ids = np.argmax(batch_results, axis=1).tolist()
    labels = [inverse_lable_map[label_id] for label_id in label_ids]
    all_labels.extend(labels)

write_results(all_labels, "./result.txt")
进阶优化
可以尝试paddlenlp.seq2vec中其它模型,观察模型效果。

预训练模型: 鉴于目前预训练模型ERNIE/BERT对语义有着更强大的表征意义,我们可以通过更换为预训练模型完成该分类任务。

PaddleNLP提供了许多中文预训练模型如ERNIE、ERNIE-Tiny、BERT、RoBERTa、Electra等预训练模型,也内置了各种预训练模型用于文本分类Fine-tune常用网络。可参考PaddleNLP AI Studio项目学习试用预训练模型。

如:使用ERNIE Fine-tune文本分类任务:参考项目

此外,还可调用paddlenlp.embeddings接口,使用预训练好的词向量初始化embedding,加快模型收敛速度:参考项目

In [12]
from paddlenlp.transformers import ErnieForSequenceClassification, ErnieTokenizer

model = ErnieForSequenceClassification.from_pretrained("ernie-1.0", num_classes=len(train_ds.label_list))
tokenizer = ErnieTokenizer.from_pretrained("ernie-1.0")
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值