文本分类(一) | (9) 项目组织结构

项目Github地址

在学习某个深度学习框架时,掌握其基本知识和接口固然重要,但如何合理组织代码,使得代码具有良好的可读性和可扩展性也必不可少。本文不会深入讲解过多知识性的东西,更多的则是传授一些经验,关于如何使得自己的程序更pythonic,更符合pytorch的设计理念。这些内容可能有些争议,因其受我个人喜好和coding风格影响较大,你可以将这部分当成是一种参考或提议,而不是作为必须遵循的准则。归根到底,都是希望你能以一种更为合理的方式组织自己的程序。

在做深度学习实验或项目时,为了得到最优的模型结果,中间往往需要很多次的尝试和修改(也就是所谓地调参)。根据我的个人经验,在从事大多数深度学习研究时,程序都需要实现以下几个功能:

1)模型定义

2)数据处理和加载

3)训练模型(Train&Validate)

4)训练过程的可视化或相关指标的计算

5)测试/预测(Test/Inference)

另外程序还应该满足以下几个要求:

1)模型需具有高度可配置性,便于修改参数、修改模型,反复实验

2)代码应具有良好的组织结构,使人一目了然

3)代码应具有良好的说明,使其他人能够理解

接下来我将应用这些内容,并结合实际的例子,来讲解如何合理组织我们的文本分类项目。

目录

1. 文件组织结构

2. 数据预处理和加载

3. 模型定义

4. 配置文件

5. main.py

6. 使用方式

7. 实验结果与分析

8. 预测与网页Demo

9. 程序所依赖的环境

10. 总结


1. 文件组织结构

首先来看程序文件的组织结构:

文本分类(一) | (9) 项目组织结构_第1张图片

其中:

1)checkpoints/: 用于保存训练好的模型,可使程序在异常退出后仍能重新载入模型,恢复训练

2)data/:数据相关操作,包括数据预处理、dataset实现等

3)models/:模型定义,可以有多个模型,例如上面的FastText、TextCNN等,一个模型对应一个文件

4)config.py:配置文件,所有可配置的变量都集中在此,并提供默认值

5)main.py:主文件,训练和预测程序的入口,可通过不同的命令来指定不同的操作和参数

6)load_word_vector.py:定义加载预训练词向量的函数

7).idea/、static/、templates/、settings.py、urls.py、views.py、wsgi.py、db.sqlite3、manage.py:网页Demo运行支撑文件。

8)requirements.txt:程序依赖的第三方库

9)README.pdf:项目说明文档

 

2. 数据预处理和加载

数据的相关预处理函数主要保存在data/dataset.py中。关于数据加载的相关操作,其基本原理就是使用Dataset进行数据集的封装,再使用Dataloader实现数据并行加载。

具体的预处理过程和实现细节在文本分类专栏的第(2)篇博客中已经详细介绍了。

使用时,我们可通过dataloader加载数据:

#读取之前预处理过程 保存的处理好的训练集、验证集和测试集
    X_train = torch.load('./data/X_train.pt')
    y_train = torch.load('./data/y_train.pt')
    X_val = torch.load('./data/X_val.pt')
    y_val = torch.load('./data/y_val.pt')
    X_test = torch.load('./data/X_test.pt')
    y_test = torch.load('./data/y_test.pt')
    
    #封装成DataSet
    trainset = Data.TensorDataset(X_train,y_train)
    valset = Data.TensorDataset(X_val,y_val)
    testset = Data.TensorDataset(X_test,y_test)

    #使用DataLoader并行加载数据
    train_iter = Data.DataLoader(trainset,opt.batch_size,shuffle=True,num_workers=opt.num_workers)
    val_iter = Data.DataLoader(valset,opt.batch_size)
    test_iter = Data.DataLoader(testset,opt.batch_size)
  • 加载预训练词向量

load_word_vector.py定义了加载预训练词向量的函数:

def read_word_vector(path): #path为 下载的预训练词向量 解压后的文件所在的路径
    #读取预训练词向量
    with open(path, 'r') as f:
        words = set()  # 定义一个words集合
        word_to_vec_map = {}  # 定义词到向量的映射字典
        for line in f:  #跳过文件的第一行 
            break

        for line in f:  # 遍历f中的每一行
            line = line.strip().split()  # 去掉首尾空格,每一行以空格切分  返回一个列表  第一项为单词 其余为单词的嵌入表示
            curr_word = line[0]  # 取出单词
            words.add(curr_word)  # 加到集合/词典中
            # 定义词到其嵌入表示的映射字典
            word_to_vec_map[curr_word] = np.array(line[1:], dtype=np.float64)

    return words, word_to_vec_map



def load_pretrained_embedding(word2index, word2vector):#word2index是构建的词典(单词到索引的映射),word2vector是预训练词向量(单词到词向量的映射)
  
    embed = torch.zeros(len(word2index), opt.embed_size) # 初始化词嵌入矩阵为0
    oov_count = 0 # 找不到预训练词向量的词典中单词的个数

    for word, index in word2index.items(): #遍历词典中的每个单词 及其在词典中的索引
        try: #如果单词有对应的预训练词向量 则用预训练词向量对词嵌入矩阵的对应行进行赋值
            embed[index, :] = torch.from_numpy(word2vector[word])
        except KeyError:
            oov_count += 1

    if oov_count > 0:
        print("There are %d oov words."%oov_count)
    return embed #返回词嵌入矩阵

在主程序main.py 的train函数中调用:

 

  #加载预训练词向量
    if opt.use_pretrained_word_vector:
        words,word2vec = read_word_vector(opt.word_vector_path) #opt.word_vector_path为下载的预训练词向量 解压后的文件所在的路径
        print("预训练词向量读取完毕!")
        #读取之前预处理过程保存的词典(词到索引的映射)
        with open('./data/word2index.json') as f:
            word2index = json.load(f)

        model.embedding.weight.data.copy_(load_pretrained_embedding(word2index, word2vec)) #使用加载完预训练词向量的词嵌入矩阵 对embdding层的词嵌入矩阵赋值
        print("预训练词向量加载完毕!")
        if opt.frozen: #冻结还是finetuning
            model.embedding.weight.requires_grad = False

3. 模型定义

各个模型的定义主要保存在models/目录下,其中BasicModule是对nn.Module的简易封装,提供快速加载(可以处理GPU训练、CPU加载的情况)和保存模型(提供多GPU训练时的模型保存方法)的接口,其他模型都继承自BasicModule。

class BasicModule(nn.Module):
   '''
   封装了nn.Module,主要提供save和load两个方法
   '''

   def __init__(self,opt=None):
       super(BasicModule,self).__init__()
       self.model_name = str(type(self)) # 模型的默认名字

   def load(self, path):
       '''
       加载模型
       可指定路径
       '''
       self.load_state_dict(torch.load(path))

   def load_map(self, path,device): #如果在GPU上训练 在CPU上加载 可以调用这个函数
       '''
       加载模型
       可指定路径
       '''
       self.load_state_dict(torch.load(path,map_location=device))

   def save(self, name=None):
       '''
       保存模型,默认使用“模型名字_best”作为文件名,
       '''
       if name is None:
           prefix = 'checkpoints/' + self.model_name.split('.')[-2] + '_best.pth'
           #name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
       torch.save(self.state_dict(), prefix) #只保存模型的参数
       return name

   def save_multiGPU(self, name=None):  #如果使用多GPU训练,保存模型时,可以调用这个函数。
       '''
       保存模型,默认使用“模型名字_best”作为文件名,
       '''
       if name is None:
           prefix = 'checkpoints/' + self.model_name.split('.')[-2] + '_best.pth'
           # name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
       torch.save(self.module.state_dict(), prefix)  # 只保存模型的参数
       return name

在实际使用中,直接调用model.save()及model.load(opt.load_path)即可,我们已经对保存和加载做了封装。

其它自定义模型一般继承BasicModule,然后实现自己的模型。其中TextCNN.py实现了TextCNN,FastText.py实现了FastText等。在models/__init__py中,代码如下:

#本项目可选择的模型:
from .FastText import FastText
from .TextCNN import TextCNN
from .MulBiLSTM import MulBiLSTM
from .MulBiLSTM_Atten import MulBiLSTM_Atten
from .RCNN import RCNN
from .DPCNN import DPCNN

这样在主函数中就可以写成:

from models import TextCNN

或:

import models
model = models.TextCNN()

或:

import models
model = getattr(models, 'TextCNN')()

其中最后一种写法最为关键,这意味着我们可以通过字符串直接指定使用的模型,而不必使用判断语句,也不必在每次新增加模型后都修改代码。新增模型后只需要在models/__init__.py中加上

from .new_module import NewModule

各个模型的原理和实现细节在文本分类专栏博客的第(3)-(8)篇博客中已经详细介绍过了。 

4. 配置文件

在模型定义、数据处理和训练等过程都有很多变量,这些变量应提供默认值,并统一放置在配置文件中,这样在后期调试、修改代码或迁移程序时会比较方便,在这里我们将所有可配置项放在config.py中。

class DefaultConfig(object):

    model = 'FastText'  # 使用的模型,名字必须与models/__init__.py中的名字一致

    load_model_path = None  # 加载预训练的模型的路径,为None代表不加载

    batch_size = 256  # batch size
    num_workers = 4  # 加载数据使用的线程数

    #下载数据集 解压缩后得到的文件夹所在的路径
    data_root = '/Users/apple/Downloads/THUCNews-1'


    max_epoch = 20
    lr = 0.01  # initial learning rate
    weight_decay = 1e-4  # 损失函数 正则化
    embed_size = 100 #词嵌入维度
    drop_prop = 0.5 #丢弃率
    classes = 14  #分类类别数
    max_len = 500 #序列最大长度

    #学习率衰减相关超参数
    use_lrdecay = True #是否使用学习率衰减
    lr_decay = 0.95  # 衰减率
    n_epoch = 1  #每隔n_epoch个epoch衰减一次 lr = lr * lr_decay

	
    #TextCNN相关的超参数
    kernel_sizes = [3,4,5] #一维卷积核的大小
    num_channels = [100,100,100] #一维卷积核的数量

    #FastText相关的超参数
    linear_hidden_size =512  #隐层单元数

    #MulBiLSTM/MulBiLSTM_Atten相关超参数
    recurrent_hidden_size = 128 #循环层 单元数
    num_layers = 2      #循环层 层数
    
    #RCNN相关超参数
    num_layers_rcnn = 1 #循环层 层数
    drop_prop_rcnn = 0.0 #1个循环层设置为0 丢弃率  
	
    #DPCNN相关超参数
    channel_size = 250
    drop_prop_dpcnn = 0.2

    #梯度裁剪相关超参数
    use_rnn = False
    norm_type = 1
    max_norm = 5

    #预训练词向量相关超参数
    use_pretrained_word_vector = False
    word_vector_path = '/Users/apple/Downloads/sgns.sogou.word'
    frozen = False

    #待分类文本
    text="众所周知,一支球队想要夺冠,超级巨星必不可少,不过得到超级巨星并不简单,方式无非两种,一是自己培养,这种方式适用于所有球队,二是交易,这种方式基本只适用于大市场球队——事实就是,30支球队之间并非完全公平,超级巨星依然更愿意前往大城市。"

    #预测时是否对文本进行填充或截断
    predict_pad = False

可配置的参数主要包括:

1)训练参数(学习率、训练epoch等)

2)各个模型相关的参数

这样我们在程序中就可以这样使用:

import models
from config import DefaultConfig

opt = DefaultConfig()
lr = opt.lr
model = getattr(models, opt.model)

这些都只是默认参数(如果后续不在命令行指定的话,就是用默认参数),在这里还提供了更新函数(根据命令行中指定的参数进行更新),根据字典更新配置参数。

def parse(self, kwargs):
    '''
    根据字典kwargs 更新 默认的config参数
    '''
    # 更新配置参数
    for k, v in kwargs.items():
        if not hasattr(self, k):
            # 警告还是报错,取决个人喜好
            warnings.warn("Warning: opt has not attribut %s" % k)
        setattr(self, k, v)

    # 打印配置信息
    print('user config:')
    for k, v in self.__class__.__dict__.items(): #python3 中iteritems()已经废除了
        if not k.startswith('__'):
            print(k, getattr(self, k))

这样我们在实际使用时,并不需要每次都修改config.py(默认配置),只需要通过命令行传入所需参数,覆盖默认配置即可。

opt = DefaultConfig()
new_config = {'lr':0.1,'use_gpu':False}
opt.parse(new_config)
opt.lr == 0.1

5. main.py

在讲解主程序main.py之前,我们先来看看2017年3月谷歌开源的一个命令行工具fire ,通过pip install fire即可安装。下面来看看fire的基础用法,假设example.py文件内容如下:

import fire
def add(x, y):
 return x + y
 
def mul(**kwargs):
   a = kwargs['a']
   b = kwargs['b']
   return a * b

if __name__ == '__main__':
 fire.Fire()

那么我们可以使用:

python example.py add 1 2 # 执行add(1, 2)
python example.py mul --a=1 --b=2 # 执行mul(a=1, b=2),kwargs={'a':1, 'b':2}
python example.py add --x=1 --y=2 # 执行add(x=1, y=2)

可见,只要在程序中运行fire.Fire(),即可使用命令行参数python file [args,] {--kwargs,}。fire还支持更多的高级功能,具体请参考官方指南 。

在主程序main.py中,主要包含四个函数,其中三个需要命令行执行,main.py的代码组织结构如下:

def train(**kwargs):
   '''
   训练
   '''
   pass
    
def evaluate_accuracy(data_iter, net,flag=False,labels=None):
   '''
   计算模型在验证集/测试集上的准确率等信息,用以辅助训练
   '''
   pass

def predict(**kwargs):
   '''
   对新样本进行预测
   '''
   pass

def help():
   '''
   打印帮助的信息 
   '''
   print('help')

if __name__=='__main__':
   import fire
   fire.Fire()
  • 训练

训练的主要步骤如下:

1)定义网络

2)定义数据

3)定义损失函数和优化器

4)计算重要指标

5)开始训练

6)训练网络

7)计算在验证集上的指标

训练函数的代码如下:

def train(**kwargs):

    # 根据命令行参数更新配置 否则使用默认配置
    opt.parse(kwargs)

    # step1: 数据
    #词典大小
    with open('./data/vocabsize.json') as f:
        vocab_size = json.load(f)
    print("词典大小:",vocab_size)
    #标签
    with open('./data/labels.json') as f:
        labels = json.load(f)

    #读取之前预处理过程 保存的处理好的训练集、验证集和测试集
    X_train = torch.load('./data/X_train.pt')
    y_train = torch.load('./data/y_train.pt')
    X_val = torch.load('./data/X_val.pt')
    y_val = torch.load('./data/y_val.pt')
    X_test = torch.load('./data/X_test.pt')
    y_test = torch.load('./data/y_test.pt')

    #封装成DataSet
    trainset = Data.TensorDataset(X_train,y_train)
    valset = Data.TensorDataset(X_val,y_val)
    testset = Data.TensorDataset(X_test,y_test)

    #使用DataLoader并行加载数据
    train_iter = Data.DataLoader(trainset,opt.batch_size,shuffle=True,num_workers=opt.num_workers)
    val_iter = Data.DataLoader(valset,opt.batch_size)
    test_iter = Data.DataLoader(testset,opt.batch_size)

    # step2: 模型
    model = getattr(models, opt.model)(vocab_size,opt)
    if opt.load_model_path:
        model.load(opt.load_model_path)

    #加载预训练词向量
    if opt.use_pretrained_word_vector:
        words,word2vec = read_word_vector(opt.word_vector_path) #opt.word_vector_path为下载的预训练词向量 解压后的文件所在的路径
        print("预训练词向量读取完毕!")
        #读取之前预处理过程保存的词典(词到索引的映射)
        with open('./data/word2index.json') as f:
            word2index = json.load(f)

        model.embedding.weight.data.copy_(load_pretrained_embedding(word2index, word2vec)) #使用加载完预训练词向量的词嵌入矩阵 对embdding层的词嵌入矩阵赋值
        print("预训练词向量加载完毕!")
        if opt.frozen: #冻结还是finetuning
            model.embedding.weight.requires_grad = False


    print("使用设备:",device)
    if torch.cuda.device_count() > 1: #使用多GPU进行训练
        print("Let's use", torch.cuda.device_count(), "GPUs!")
        model = torch.nn.DataParallel(model)

    model.to(device)

    # step3: 目标函数和优化器
    criterion = torch.nn.CrossEntropyLoss()
    optimizer = torch.optim.Adam(model.parameters(),
                             lr = opt.lr,
                             weight_decay = opt.weight_decay)
    scheduler = lr_scheduler.StepLR(optimizer,opt.n_epoch,opt.lr_decay)
    # 训练
    batch_count = 0
    best_f1_val = 0.0


    for epoch in range(opt.max_epoch):
        train_l_sum, train_acc_sum, n, start = 0.0, 0.0, 0, time.time()
        if opt.use_lrdecay:
            scheduler.step()
        for X, y in train_iter:
            X = X.to(device)
            y = y.to(device)
            y_hat = model(X)
            loss = criterion(y_hat, y)
            optimizer.zero_grad()
            loss.backward()
            if opt.use_rnn: #梯度裁剪
                nn.utils.clip_grad_norm_(model.parameters(), max_norm=opt.max_norm, norm_type=opt.norm_type)
            optimizer.step()
            train_l_sum += loss.cpu().item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().cpu().item()
            n += y.shape[0]
            batch_count += 1


        #一个epoch后在验证集上做一次验证
        val_f1,val_acc = evaluate_accuracy(val_iter, model)
        if val_f1 > best_f1_val:
            best_f1_val = val_f1
            # 保存在验证集上weighted average f1最高的参数(最好的参数)
            if torch.cuda.device_count() > 1: #多GPU训练时保存参数
                print("Saving on ", torch.cuda.device_count(), "GPUs!")
                model.save_multiGPU()
            else:
                print("Saving on one GPU!")#单GPU训练时保存参数
                model.save()
            #使用当前最好的参数,在测试集上再跑一遍
            best_f1_test,best_acc_test = evaluate_accuracy(test_iter,model,True,labels)

        print('epoch %d, lr %.6f,loss %.4f, train acc %.3f, val acc %.3f,val weighted f1 %.3f, val best_weighted f1 %.3f,test best_acc %.3f,test best_weighted f1 %.3f,time %.1f sec'
              % (epoch + 1, optimizer.state_dict()['param_groups'][0]['lr'],train_l_sum / batch_count, train_acc_sum / n, val_acc,val_f1, best_f1_val,best_acc_test,best_f1_test,time.time() - start))
  • 验证

验证相对来说比较简单,但要注意需将模型置于验证模式(model.eval()),验证完成后还需要将其置回为训练模式(model.train()),这两句代码会影响BatchNorm和Dropout等层的运行模式。

多分类我们使用 weighed average f1-score作为评估指标,主要使用sklearn中的指标计算函数。

代码如下:

def evaluate_accuracy(data_iter, net,flag=False,labels=None):
    #计算模型在验证集上的相关指标 多分类我们使用 weighed average f1-score

    acc_sum, n = 0.0, 0
    net.eval()  # 评估模式, 这会关闭dropout
    y_pred_total = []
    y_total = []
    with torch.no_grad():
        for X, y in data_iter:
            #acc_sum += (net(X.to(device)).argmax(dim=1) == y.to(device)).float().sum().cpu().item()
            #n += y.shape[0]
            y_pred = net(X.to(device)).argmax(dim=1).cpu().numpy()
            y_pred_total.append(y_pred)
            y_total.append(y.numpy())

    y_pred = np.concatenate(y_pred_total)
    y_label = np.concatenate(y_total)
    weighted_f1 = f1_score(y_label,y_pred,average='weighted') #weighed average f1-score

    accuracy = accuracy_score(y_label,y_pred) #准确率
    if flag: #当在测试集上验证时 flag设置为True  额外打印分类报告和混淆矩阵
        print(classification_report(y_label,y_pred,digits=4,target_names = labels))
        cm = confusion_matrix(y_label,y_pred)
        print(cm)
    net.train()  # 改回训练模式

    return weighted_f1,accuracy
  • 预测

对于新的输入文本,我们加载训练好的模型进行预测,输出类别标签:

def predict(**kwargs):
    # 根据命令行参数更新配置 否则使用默认配置
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    print("使用设备:", device)
    opt.parse(kwargs)
    text = opt.text #待分类文本

    # 词典大小
    with open('./data/vocabsize.json') as f:
        vocab_size = json.load(f)
    print(vocab_size)
    
    #创建指定的模型对象
    model = getattr(models, opt.model)(vocab_size, opt)
    
    #加载训练好的模型参数
    if device.type=='cpu': #GPU训练 CPU预测 加载参数时需要对参数进行映射
        model.load_map('./checkpoints/'+opt.model+'_best.pth',device)
    else:
        model.load('./checkpoints/' + opt.model + '_best.pth')
    
    #加载之前预处理过程 保存的词到索引的映射字典
    with open('./data/word2index.json') as f:
        word2index = json.load(f)

    #device = list(model.parameters())[0].device
    if opt.predict_pad: #预测时对文本进行填充(若文本长度opt.max_len)
        sentence = [word2index.get(word, 1) for word in jieba.lcut(text)]
        sentence = sentence[:opt.max_len] if len(sentence) > opt.max_len else sentence + [0] * (opt.max_len - len(sentence))
        sentence = torch.tensor(sentence,device=device)
    else:
        sentence = torch.tensor([word2index.get(word,1) for word in jieba.lcut(text)],device=device)
    print(sentence)
    #预测
    with torch.no_grad():
        model.eval()
        label = torch.argmax(model(sentence.view((1,-1))),dim=1)
    
    # 加载之前预处理过程 保存的索引到类别标签的映射字典
    with open('./data/index2labels.json') as f:
        index2labels = json.load(f)
    #输出新文本的类别标签
    print(index2labels[str(label.item())])
  • 帮助函数

为了方便他人使用, 程序中还应当提供一个帮助函数,用于说明函数是如何使用。程序的命令行接口中有众多参数,如果手动用字符串表示不仅复杂,而且后期修改config文件时,还需要修改对应的帮助信息,十分不便。这里使用了Python标准库中的inspect方法,可以自动获取config的源代码。help的代码如下:

def help():

    '''
    打印帮助的信息: python file.py help
     '''

    print('''
   usage : python {0}  [--args=value,]
    := train | test | help
   example: 
           python {0} train --model='TextCNN' --lr=0.01
           python {0} test --text='xxxxx'
           python {0} help
   avai    able
    args: '''.format(__file__))

    from inspect import getsource
    source = (getsource(opt.__class__))
    print(source)

 

6. 使用方式

正如help函数的打印信息所述,可以通过命令行参数指定变量名.下面是三个使用例子,fire会将包含-的命令行参数自动转层下划线_,也会将非数值的值转成字符串。所以--train-data-root=data/train和--train_data_root='data/train'是等价的。

下载THUCnews数据集(完整数据集压缩包下载),解压缩,并修改config.py:

#下载数据集 解压缩后得到的文件夹所在的路径
data_root = '/Users/apple/Downloads/THUCNews-1'

下载预训练词向量(项目所使用预训练词向量),解压缩,并修改config.py:(更多预训练词向量下载(中文))

 #预训练词向量相关超参数
word_vector_path = '/Users/apple/Downloads/sgns.sogou.word' #下载的预训练词向量 解压后的文件所在的路径

进入data目录下,运行dataset.py,对数据进行预处理并生成必要的中间文件(数据集非常大,此过程需要3-4小时):

python dataset.py

之后便可以训练模型(在main.py所在的目录下运行):

可以在命令行指定新的超参数覆盖默认超参数配置 不然将使用config.py中的默认超参数
# 训练模型 单GPU  
CUDA_VISIBLE_DEVICES=5 nohup python -u main.py train 
        --model='TextCNN' 
        --lr=0.01
        --batch-size=256  
        --max-epoch = 20 >zdz.log 2>&1 &

# 训练模型 多GPU
CUDA_VISIBLE_DEVICES=0,1,2,5 nohup python -u main.py train 
        --model='DPCNN'
        --drop_prop_dpcnn=0.2
        --batch-size=256  
        --max-epoch = 10 >zdz.log 2>&1 &


# 打印帮助信息
python main.py help

7. 实验结果与分析

  • 实验结果

训练各个模型的超参数均采用默认超参数,具体的配置在config.py中。各个模型在测试集上的weight f1-score值如下表所示:

文本分类(一) | (9) 项目组织结构_第2张图片

  • 实验分析

FastText模型是我们的Baseline,在测试集上的weighted f1-score为92.6%。

DPCNN模型在测试集上取得了最好的性能,其weighted f1-score为95.4%。

Attention机制在分类问题上的效果不是很明显,多层双向LSTM和多层双向LSTM with Attention的性能几乎差不多。

比较意外地是RCNN模型(结合RNN与CNN)并没有取得预期的性能,相比FastText提升不大,可能模型细节还需要细。

TextCNN是"性价比"最高的模型,模型比较简单,训练很容易,但效果非常不错。

上述实验结果均基于默认的超参数配置,调参可能会取得更好的性能。

基于RNN的模型相对基于CNN的模型更难训练(相同配置下,训练时间更长),但效果并没有显著优势。基于RNN的模型训练一定轮数后可能会出现梯度爆炸,注意使用梯度剪切技巧。

深层网络(DPCNN)相对于浅层网络(TextCNN)更难训练,但效果提升比较明显。深层网络要注意缓解梯度消失现象(比如,使用残差连结)。

 

8. 预测与网页Demo

  • 通过命令行执行预测

模型训练完成后,便可以对新闻文本执行预测过程,预测其对应的主题标签(在main.py所在的目录下运行):

# 通过命令行运行预测过程
python main.py predict
       --model='RCNN'  #指定预测所使用的model
       --text='众所周知,一支球队想要夺冠,超级巨星必不可少,不过得到超级巨星并不简单,方式无非两种,一是自己培养,这种方式适用于所有球队,二是交易。'  #待分类文本
  • 通过网页Demo进行预测

1)进入项目目录,运行manage.py(在manage.py所在的目录下运行):

 python manage.py runserver

文本分类(一) | (9) 项目组织结构_第3张图片

 2)在浏览器打开网址:

http://127.0.0.1:8000/index  #不要忘了index

3)界面如下所示:

文本分类(一) | (9) 项目组织结构_第4张图片

 把待分类的文本粘贴到上图中的红框中,在蓝框的下拉列表中选择模型,点击上传分析后,在下方的绿框中便可显示分类结果。

4)支持的新闻类别有14个:财经、彩票、房产、股票、家居、教育、科技、社会、时尚、时政、体育、星座、游戏、娱乐。

以下是一些示例(注意:我们的系统是对篇章进行分类,最长不要超过500个词。):

a. 体育:“众所周知,一支球队想要夺冠,超级巨星必不可少,不过得到超级巨星并不简单,方式无非两种,一是自己培养,这种方式适用于所有球队,二是交易,这种方式基本只适用于大市场球队——事实就是,30支球队之间并非完全公平,超级巨星依然更愿意前往大城市。”

b. 体育:“德国球星萨内告知拜仁慕尼黑,他希望在一月份转会加盟,但据英国媒体报道,曼城方面对他的要价高达1亿英镑。尽管萨内自从8月份以来就一直伤停,但拜仁仍对他感兴趣。曼城方面也清楚萨内想走,但他们的立场是,买家出价合适才会放人,而瓜迪奥拉对他的要价是1亿英镑,这笔钱将用来买进新球员补充阵容。”

c. 娱乐:“有网友晒出了范冰冰现身好莱坞华裔导演温子仁新作《恶毒》的杀青晚宴现场的照片。照片中范冰冰戴着暗绿色帽子,穿着黑色皮衣,留着大波浪长发,五官精致笑容甜美,与众主创合照站C位,很有排面。”

d. 娱乐:“北京时间12月20日消息,据香港媒体报道,电影《急先锋》日前在北京举行发布会,导演唐季礼联同成龙、杨洋、艾伦、朱正廷、母其弥雅(MIYA)等主角齐齐亮相,并分享台前幕后的故事。其中导演唐季礼特别提到,在拍摄过程中快艇不慎被石头掀翻,一下将成龙扣在水下,把自己吓哭了!今年成龙大哥又有新作,与老拍档唐季礼导演连手打造新片《急先锋》,并且找来一班新血组成中国版“复仇者”,在大年初一与观众贺岁,让影迷万分期待,《急先锋》日前在北京举行发布会。”

e. 游戏:“尽管新英雄厄斐琉斯还没有正式登陆各大服务器,但拳头已经通过邮件向国外网友发送了另一名新英雄的神秘技能卡片,卡片描绘出其他的英雄被某个技能打中的效果,但是这个打击效果的来源却是未知的。之前拳头曾在英雄制作大纲写道:“在厄斐琉斯之后的英雄会是一位来自艾欧尼亚的斗士。这名英雄在打斗中茁壮成长,在受到过对方强烈的击打后,他(她)会狂笑,并且将所有受到的挑衅全部释放到对方的脸上。如果你喜欢用拳头说话,喜欢致命搏击,或者喜欢在激烈的战斗中把对手的头打得粉碎,他(她)可能是你的本命英雄。”这是铁拳要进入联盟了吗”

9. 程序所依赖的环境

numpy    >=1.16.2
json    >=2.0.9
jieba    >=0.39
torch    >=1.1.0
torchtext    >=0.4.0
sklearn    >=0.20.3
django    >=3.0.1
fire    >=0.2.1

10. 总结

1)本项目并没有解决训练集样本类别分布不均衡的问题,之后会考虑解决这个问题。

2)尽管各个模型在测试集上的准确率都在90%以上,但在预测时,准确率会有一定程度的下降,原因在于真实(预测)数据的分布和训练集数据的分布不同。

3)本项目主要实现了一些基于CNN、RNN(Attention)的文本分类模型,并没有实现一些基于预训练语言模型(如Bert、XLNet等)的分类模型,之后会逐步完善。

你可能感兴趣的:(文本分类(一),文本分类,项目组织结构)