【nlp自然语言处理实战】案例---FastText模型文本分类

目录

1.案例简介

 2 代码

2.1 load_data.py

2.2 load_data_iter.py

2.3 FastText.py

2.4 train.py

2.5 predict.py

2.6 run.py

2.7 实验结果(部分

3 代码地址


1.案例简介

数据集从 THUCNews 上抽取 20 万条新闻标题,文本长度在 20~30 字,总计 10 个类别。每类 2 万条进行分类操作,并基于 PyTorch 完成 FastText 模型处理。

FastText模型是脸书开源的一个词向量与文本分类工具。其在2016年开源,典型应用场景是「带监督的文本分类问题」。其可以提供简单而高效的文本分类和表征学习的方法,性能比肩深度学习而且速度更快。

FastText模型结合了自然语言处理和机器学习中最成功的理念。我们另外采用了一个softmax层级(利用了类别不均衡分布的优势)来加速运算过程。

FastText模型是一个快速文本分类模型算法,与基于神经网络的分类模型算法相比有以下优点:


1)FastText模型在保持高精度的情况下提高了训练速度和测试速度;
2)FastText模型不需要预训练好的词向量,其可以自己训练词向量,
3)FastText模型两个重要的优化是Hierarchical softmax和N-gram。

FastText模型网络分为三层:

输入层:对文档插入之后的向量,包含有N-gram特征。
隐含层:对输入数据的求和平均。
输出层:文档对应标签。

 2 代码

2.1 load_data.py

将词转换为编号

比如:一开始文档中的词是长这个样子

内容  标签

 对文件中的中文进行分词处理,按照一个字一个字的分开:

['中','华','女','子','学','院',':','本','科','层','次','仅','1','专','业','招','男','生']

分开之后对文件中所有的词进行频率统计,按照频率高低进行排列:

比如: 

{'中': 17860, '华': 5053, '女': 8568, '子': 10246, '学': 11069, '院': 2833, ':': 28269, '本': 6649, '科': 4375, '层': 776, '次': 3159, '仅': 1793, '1': 40420, '专': 4134, '业': 9748, '招': 4648, '男': 5884, '生': 15370, '两': 4880, '天': 6662, '价': 11663, '网': 8573, '站': 1760, '背': 662, '后': 7306, '重': 5960, '迷': 1248, '雾': 81, '做': 1827, '个': 4075, '究': 1372, '竟': 541, '要': 4044, '多': 5170, '少': 2108, '钱': 1551, '东': 4555, '5': 18163, '环': 1898, '海': 6229, '棠': 65, '公': 10769, '社': 1165, '2': 31856, '3': 20291, '0': 60319, '-': 7944, '9': 15626, '平': 8207, '居': 5716, '准': 1859, '现': 7473, '房': 8181, '8': 13315, '折': 2990, '优': 1978, '惠': 1560, '卡': 3009, '佩': 467, '罗': 2536, '告': 2843, '诉': 1055, '你': 1456, '德': 3209, '国': 24079, '脚': 491, '猛': 379, '的': 8753, '原': 1782, '因': 2959, ' ': 80926, '不': 12798, '希': 1264, '望': 2504, '英': 5067, '战': 6391, '踢': 268, '点': 5623, '球': 5074, '岁': 2528, '老': 3982, '太': 1699, '为': 7095, '饭': 313, '扫': 299, '地': 7875, '4': 13832, '年': 18565, '获': 3876, '授': 381, '港': 3341, '大': 26024, '荣': 538, '誉': 265, '士': 2674, '记': 1858, '者': 3507, '回': 4644, '访': 1410, '震': 2040, '可': 5042, '乐': 2875, '孩': 1379, '将': 10546, '受': 3724, '邀': 440, '赴': 782, '美': 11941, '参': 1526, '观': 1635, '冯': 252, '伦': 831, '徐': 376, '若': 394, '�': 763, ..........}

排序之后再对词重新编号最后面两个添加<UNK>:字总数,<PAD>:字总数+1

{'中': 1, '华': 2, '女': 3, '子': 4,.......,<UNK>:字总数,<PAD>:字总数+1}

保存好这个词典

在建立数据集的时候,对每一行的数据都要进行填充或者删除就是用词典来进行的。

import os
import pickle as pkl
from tqdm import tqdm
MAX_VOCAB_SIZE = 10000 #词表长度限制
UNK,PAD = '<UNK>','<PAD>' #未知字,padding符号

# 编辑词典函数
def build_vocab(file_path,tokenizer,max_size,min_freq):
    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 中添加两个特殊单词的映射:UNK 表示未知单词,PAD 表示填充单词。UNK 的编号为词汇表大小,而 PAD 的编号为词汇表大小加 1。
        vocab_dic.update({UNK: len(vocab_dic), PAD: len(vocab_dic)+1})
    return vocab_dic

# 编辑建立数据集函数
def build_dataset(config,ues_word):
    # 根据 ues_word 变量的值在单词级别和字符级别之间切换分词方式
    if ues_word:
        tokenizer = lambda x: x.split('') # 以空格隔开,word-level  单词级别
    else:
        tokenizer = lambda x: [y for y in x] # char-level  字符级别

    # 如果存在 词典文件则直接读取
    if os.path.exists(config.vocab_path):
        vocab = pkl.load(open(config.vocab_path,'rb'))
    # 不存在则创建词典文件
    else:
        # config.train_path = './data/train.txt'
        vocab = build_vocab(config.train_path, tokenizer=tokenizer,max_size=MAX_VOCAB_SIZE, min_freq=1)
        pkl.dump(vocab, open(config.vocab_path, 'wb'))
        print(f"Vocab size:{len(vocab)}")

    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')
                words_line = []
                # 以字符方式进行分词处理
                token = tokenizer(content)
                # 记录所有文件中词的数量
                seq_len = len(token)
                # token = ['传', '凡', '客', '诚', '品', '裁', '员', '5', '%', ' ', '电', '商', '寒', '冬', '或', '提', '前', '到', '来']
                # 将token固定为同样长度
                if pad_size:
                    if len(token) < pad_size:
                        token.extend([PAD]*(pad_size - len(token)))
                    else:
                        token = token[:pad_size]
                        seq_len = pad_size
                # 讲统一填充的词完成词到编号的转换
                for word in token:
                    words_line.append(vocab.get(word, vocab.get(UNK)))

                contents.append((words_line, int(label), seq_len))
        return contents

    # 加载训练集
    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 vocab, train, dev, test

2.2 load_data_iter.py

将数据按照批量进行。

批量记载数据的原因:深度学习模型的参数非常多,为了得到模型的参数,需要用大量的数据对模型进行训练,所以数据量一般是相当大的,不可能一次性加载到内存中对所有数据进行向前传播和反向传播,因此需要分批次将数据加载到内存中对模型进行训练。使用数据加载器的目的就是方便分批次将数据加载到模型,以分批次的方式对模型进行迭代训练。

import torch

class DatasetIterater(object):
    def __init__(self, batches, batch_size, device):
        # 批次大小(在config中定义)
        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):
        # 讲数据集转为tensor
        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)
        return (x, seq_len), y

    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):
        return self

    def __len__(self):
        if self.residue:
            return self.n_batches + 1
        else:
            return self.n_batches


def build_iterator(dataset, config, predict):
    if predict is True:
        config.batch_size = 1
    iter = DatasetIterater(dataset, config.batch_size, config.device)
    return iter

2.3 FastText.py

代码里面有所有需要模型的配置参数,以及模型类

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

# 编写参数配置类
class Config(object):
    # 配置参数
    def __init__(self):
        self.model_name = 'FastText'
        self.train_path = './data/train.txt'
        # 训练集
        self.dev_path = './data/dev.txt'
        # 验证集
        self.test_path = './data/test.txt'
        # 测试集
        self.predict_path = './data/predict.txt'
        self.class_list = [x.strip() for x in open('./data/class.txt', encoding='utf-8').readlines()]
        self.vocab_path = './data/vocab.pkl'  # 词表
        # 模型训练结果
        self.save_path = './saved dict/' + self.model_name + '.ckpt'
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

        self.dropout = 0.5 #随机失活
        #若超过1000 patch效果还没提升,则提前结束训练
        self.require_improvement = 1000
        self.num_classes = len(self.class_list)#类别数
        self.n_vocab = 0 #词表大小,在运行时赋值
        self.num_epochs = 5 #epoch数
        self.batch_size = 32 #mini-batch大小
        self.pad_size = 32 #每句话处理成的长度(短填长切)
        self.learning_rate = 1e-3 #学习率
        self.embed = 300 #字向量维度
        self.hidden_size =256 #隐藏层大小
        self.filter_sizes = (2, 3, 4)  # 卷积核尺寸
        self.num_filters = 256  # 卷积核数量(channels数)

# 编写模型类
class Model(nn.Module):
    def __init__(self,config):
        super(Model,self).__init__()
        self.embedding = nn.Embedding(
            config.n_vocab,  # 词汇表达大小
            config.embed,    # 词向量维度
            padding_idx=config.n_vocab-1 # 填充
        )
        self.dropout = nn.Dropout(config.dropout)   # 丢弃
        self.fc1 = nn.Linear(config.embed, config.hidden_size)   # 全连接层
        self.dropout = nn.Dropout(config.dropout)  # 丢弃
        self.fc2 = nn.Linear(config.hidden_size, config.num_classes)   #全连接层

    # 前向传播计算
    def forward(self, x):
        # 词嵌入
        out_word = self.embedding(x[0])
        out = out_word.mean(dim=1)
        out = self.dropout(out)
        # print(out.shape)
        out = self.fc1(out)
        out = F.relu(out)
        out = self.fc2(out)
        return out

2.4 train.py

训练数据集,

import numpy as np
import torch
import torch.nn.functional as F
from sklearn import metrics
#编写训练函数
# 传入的是 测试集和验证集
def train(config,model,train_iter,dev_iter):
    print("begin")
    model.train()
    # 优化器
    optimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate)
    total_batch = 0 # 记录进行到多少batch
    dev_best_loss = float('inf')
    last_improve = 0 # 记录上次验证集loss下降的batch数
    flag = False # 记录是否很久没有效果提升

    for epoch in range(config.num_epochs):
        print('Epoch[{}/{}]'.format(epoch+1,config.num_epochs))
        # 批量训练
        for i ,(trains ,labels) in enumerate(train_iter):
            # 将训练集放在模型中
            outputs = model(trains)
            # 清空模型梯度信息  在每次迭代时,需要将参数的梯度清空,以免当前的梯度信息影响到下一次迭代
            model.zero_grad()
            # 计算损失函数
            loss = F.cross_entropy(outputs, labels)
            # 反向传播
            loss.backward()
            # 根据上面计算得到的梯度信息和学习率  对模型的参数进行优化
            optimizer.step()

            if total_batch % 100 == 0:
                # 每多少轮输出在训练集和验证集上的效果
                true = labels.data.cpu()
                predict = torch.max(outputs.data, 1)[1].cpu()
                train_acc = metrics.accuracy_score(true, predict)
                dev_acc, dev_loss = evaluate(config, model, dev_iter)
                if dev_loss < dev_best_loss:
                    dev_best_loss = dev_loss
                    # 存储模型
                    torch.save(model.state_dict(), config.save_path)
                    # 记录batch数
                    last_improve = total_batch

                # {2:6.2%} 是一个格式化字符串语法,表示将第三个参数格式化为一个百分数,并使用右对齐方式,并在左侧填充空格,总宽度为 6 个字符,保留两位小数。
                msg = 'Iter: {0:>6}, Train Loss: {1:>5.2}, Train Acc: {2:6.2%} ,''Val Loss :{3:>5.2}, Val Acc: {4:>6.2%}'
                print(msg.format(total_batch,loss.item(),train_acc,dev_loss,dev_acc))
                model.train()
            total_batch +=1

            if total_batch - last_improve > config.require_improvement:
                # 验证集1oss超过1000 batch没下降,结束训练
                print("No optimization for a long time,auto-stopping...")
                flag = True
                break
        if flag:
            break

# 编写评价函数
def evaluate(config,model,data_iter,test=False):
    # 将模型切换到评估模式 在评估模式下,模型的行为与训练模式下略有不同。具体来说,评估模式下模型会关闭一些对训练过程的辅助功能,例如 dropout 和 batch normalization 等,并且不会对模型的参数进行更新。
    model.eval()
    loss_total = 0
    predict_all = np.array([], dtype=int)
    labels_all = np.array([], dtype=int)

    # 防止模型参数更新
    with torch.no_grad():

        for texts, labels in data_iter:
            outputs = model(texts)
            # 损失函数
            loss = F.cross_entropy(outputs, labels)
            # 损失值累加
            loss_total += loss
            labels = labels.data.cpu().numpy()
            predict = torch.max(outputs.data, 1)[1].cpu().numpy()
            # labels_all 是所有样本的真实标签
            labels_all = np.append(labels_all, labels)
            # predict_all 是所有样本的预测标签
            predict_all = np.append(predict_all, predict)

        acc = metrics.accuracy_score(labels_all, predict_all)
        if test:
            # config.class_list 是所有可能的类别列表
            report = metrics.classification_report(labels_all, predict_all, target_names=config.class_list, digits=4)
            # 混淆矩阵
            confusion = metrics.confusion_matrix(labels_all, predict_all)
            return acc, loss_total / len(data_iter), report, confusion
        return acc, loss_total / len(data_iter)

2.5 predict.py

import torch
import numpy as np
from train import evaluate
MAX_VOCAB_SIZE = 10000
UNK,PAD = '<UNK>','<PAD>'
tokenizer = lambda x:[y for y in x]     #char-level

# 编写测试函数
def test(config,model,test_iter):
    # test
    # 加载训练好的模型
    model.load_state_dict(torch.load(config.save_path))
    model.eval()#开启评价模式
    test_acc,test_loss,test_report,test_confusion = evaluate(config,model,test_iter,test=True)
    msg = 'Test Loss:{0:>5.2},Test Acc:{1:>6.28}'
    print(msg.format(test_loss,test_acc))
    print("Precision,Recall and Fl-Score...")
    print(test_report)
    print("Confusion Matrix...")
    print(test_confusion)

# 编写加载数据函数
def load_dataset(text, vocab, config, pad_size=32):
    contents = []
    for line in text:
        lin = line.strip()
        if not lin:
            continue
        words_line = []
        token = tokenizer(line)
        seq_len = len(token)
        if pad_size:
            if len(token) < pad_size:
                token.extend([PAD](pad_size - len(token)))
            else:
                token = token[:pad_size]
                seq_len = pad_size
        # 单词到编号的转换
        for word in token:
            words_line.append(vocab.get(word, vocab.get(UNK)))
        contents.append((words_line, int(0), seq_len))
    return contents  # 数据格式为[([..],O),([.·],1),.]

# 编写标签匹配函数
def match_label(pred,config):
    label_list = config.class_list
    return label_list[pred]

# 编写预测函数
def final_predict(config, model, data_iter):
    map_location = lambda storage, loc: storage
    model.load_state_dict(torch.load(config.save_path, map_location=map_location))

    model.eval()
    predict_all = np.array([])
    with torch.no_grad():
        for texts, _ in data_iter:
            outputs = model(texts)
            pred = torch.max(outputs.data, 1)[1].cpu().numpy()
            pred_label = [match_label(i, config)for i in pred]
            predict_all = np.append(predict_all, pred_label)
    return predict_all

2.6 run.py

from FastText import Config
from FastText import Model
from load_data import build_dataset
from load_data_iter import build_iterator
from train import train
from predict import test,load_dataset,final_predict

# 测试文本
text = ['国考网上报名序号查询后务必牢记。报名参加2011年国家公务员考试的考生:如果您已通过资格审查,那么请于10月28日8:00后,登录考录专题网站查询自己的报名序号']
if __name__ == "__main__":
    config = Config()
    print("Loading data...")
    vocab, train_data, dev_data, test_data = build_dataset(config, False)
    #1,批量加载测试数据
    # 批量记载数据的原因:深度学习模型的参数非常多,为了得到模型的参数,需要用大量的数据对模型进行训练,所以数据量一般是相当大的,
    # 不可能一次性加载到内存中对所有数据进行向前传播和反向传播,因此需要分批次将数据加载到内存中对模型进行训练。使用数据加载器的
    # 目的就是方便分批次将数据加载到模型,以分批次的方式对模型进行迭代训练。
    train_iter = build_iterator(train_data, config, False)
    dev_iter = build_iterator(dev_data, config, False)
    test_iter = build_iterator(test_data, config, False)
    config.n_vocab = len(vocab)
    #2,加载模型结构
    model = Model(config).to(config.device)
    train(config, model, train_iter, dev_iter)
    #3.测试
    test(config, model, test_iter)
    print("+++++++++++++++++")
    #4.预测
    content = load_dataset(text, vocab, config)
    predict_iter = build_iterator(content, config, predict=True)
    result = final_predict(config, model, predict_iter)
    for i, j in enumerate(result):
        print('text:{}'.format(text[i]), '\t', 'label:{}'.format(j))

2.7 实验结果(部分

3 代码地址

代码来源:《自然语言处理应用与实战》 韩少云等编著 著

代码地址:c4d2/nlp_demo: nlp相关案例 (github.com)

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值