AI练手系列(四)—— cnews中文文本分类(RNN实现)

数据集介绍

这个数据集是由清华大学根据新浪新闻RSS订阅频道2005-2011年间的历史数据筛选过滤生成的,数据集包含50000个样本的训练集,5000个样本的验证集,10000个样本的测试集,词汇表5000个字/词,文本内容一共包含十个分类,包括:

‘体育’, ‘财经’, ‘房产’, ‘家居’, ‘教育’, ‘科技’, ’ 时尚’, ‘时政’, ‘游戏’, ‘娱乐’

数据集我也把它上传了,不需要积分和金币就能下载,地址如下:

cnews中文文本分类数据集

我们应该怎样开始

首先我们拿到这么一个数据集,我们想一下我们的目标是什么?对,就是文本分类。也就是说这是一个分类任务。想想咱之前已经做过的分类任务有哪些?cifar-10图像分类、mnist手写数字识别,那么相关的经验能不能借鉴呢?肯定是可以的,但是又有什么不同呢,之前都是图片,转化后是一个多维的tensor,最里层是一个32x32或28x28的像素矩阵,而现在我们拿到的是一篇篇长短不一的文本,我们要想按照之前的套路来,首先就要将文本转化为数值化的tensor,每一篇文本的类别也要转化为数值化的label,这样才能正常进行训练。其他的目前看起来区别不大,先开始吧,遇到问题再解决。

因为设计到的内容可能比较多,我们将预处理的各部分代码放置在不同的py文件中,供主程序调用。

准备工作:文本读取及文本信息ID化

对于文本的处理我们有这样一个思路,先给词汇表里的每一个字进行编号,5000个字那就是0-4999,编完号之后再去将我们的文本进行数值化:将文本里的每一个字转化为其在词汇表中的编号,如果某一个字不在词汇表中,那么它将被抛弃。这样我们就得到一个长短不一全是数值的文本列表,再将其进行长度统一化规范化处理,设置一个截断值(比如600),如果文本A超过600字,就将其截断成600的长度,如果它不足600字,我们就给她进行无意义字符的填充,让它长度达到我们的要求。同样的,对于十个类别,我们也可以直接给它们进行0-9的编号,这样我们就能得到一堆可训练数据的初始形态了,在进行必要的维度转化、类型转换,就可以得到我们训练所需的标准数据集了。嗯,差不多就是这么个思路,开干。

词汇表ID化

将5000个字进行编号,每个字都有一个独一无二的编号,编号与字一一对应,放置于字典中,代码如下:

import numpy as np
import tensorflow.contrib.keras as kr
import os

# 读取词汇表,词汇表转化成ID
def read_vocab(vocab_dir):
    #以只读方式打开
    with open(vocab_dir, 'r', encoding='utf-8', errors='ignore') as fp:
        words = [_.strip() for _ in fp.readlines()]
    word_to_id = dict(zip(words, range(len(words))))
    return words, word_to_id

得到:

word_to_id={’’: 0, ‘,’: 1, ‘的’: 2, ‘。’: 3, ‘一’: 4, ‘是’: 5, ‘在’: 6, ‘0’: 7, ‘有’: 8, ‘不’: 9, ‘了’: 10。。。。。}

文本标签ID化

# 读取分类目录,固定
def read_category():
    categories = ['体育', '财经', '房产', '家居', '教育', '科技', '时尚', '时政', '游戏', '娱乐']
    categories = [x for x in categories]
    cat_to_id = dict(zip(categories, range(len(categories)))) 
    return categories, cat_to_id

得到:

cat_to_id = {‘体育’: 0, ‘财经’: 1, ‘房产’: 2, ‘家居’: 3, ‘教育’: 4, ‘科技’: 5, ‘时尚’: 6, ‘时政’: 7, ‘游戏’: 8, ‘娱乐’: 9}

文本内容ID化并进行裁剪

# 将文件转换为id表示
def process_file(filename, word_to_id, cat_to_id, max_length=600):
    contents, labels = [], []
    with open(filename, 'r', encoding='utf-8', errors='ignore') as f:
        for line in f:
            #line:财经	(新闻内容)          
            try:
                #前后空白及以tab为分隔符
                label, content = line.strip().split('\t')
                if content:
                    #构建双重列表contents,及列表labels
                    contents.append(list(content))
                    labels.append(label)
            except:
                pass
        #print(type(contents),type(labels))
    data_id, label_id = [], []
    for i in range(len(contents)):
        #将每一篇内容都ID化
        data_id.append([word_to_id[x] for x in contents[i] if x in word_to_id])#将每句话id化
        label_id.append(cat_to_id[labels[i]])#id化后将对应的id添加至对应列表                                                                                                                                                                                                       
    
    # 使用keras提供的pad_sequences来将文本pad为固定长度
    #可以理解为将每一篇新闻内容id化后规范为固定长度,长度不够的前面补0,长度多余的截取前面的
    x_pad = kr.preprocessing.sequence.pad_sequences(data_id, max_length)
   
    #每一段600字都有其对应的标签
    y_pad = kr.utils.to_categorical(label_id, num_classes=len(cat_to_id))  # 将标签转换为one-hot表示
    # print(type(x_pad),type(y_pad))
    # print(y_pad)
    #x_pad:50000个len=600的段子(numpy):[[1609  659   56    8   14 1190    1  108 1135  121  244...] []]
    #print(len(y_pad))
    #y_pad:50000个len=10的标签(numpy)[[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.][]]
    return x_pad, y_pad

这样我们的第一步就完成了,可以将这些代码放在同一个文件下,暂且命名cnews_loader.py,待会要用到的。

下面开始我们正式的流程吧。

导包定参

import torch
import torchvision
import numpy as np
import torch.nn as nn
import torchvision.datasets as datasets
import torchvision.transforms as  transforms
from torch.utils.data import DataLoader
import torch.utils.data as Data
#自己定义的函数导入
from cnews_loader import read_vocab, read_category, process_file
from model import TextRNN #模型定义放在model.py这个文件中

#超参数定义
BATCH_SIZE = 32
EPOCH = 100
LR = 0.001

文本读取与ID化

#######################################01 数据加载############################
# 获取文本的类别及其对应id的字典
categories, cat_to_id = read_category()

# 获取训练文本中所有出现过的字及其所对应的id
words, word_to_id = read_vocab('cnews.vocab.txt')

#获取字数,5000字
vocab_size = len(words)

# 获取训练数据每个字的id和对应标签的one-hot形式
x_train, y_train = process_file('cnews.train.txt', word_to_id, cat_to_id, 600)
#验证集处理同上
x_val, y_val = process_file('cnews.val.txt', word_to_id, cat_to_id, 600)
#测试集
x_test, y_test = process_file('cnews.test.txt', word_to_id, cat_to_id, 600)
print('x_train=', x_train[0])

数据预处理

###############################02 数据预处理###################################
# print(type(x_train))=<class 'numpy.ndarray'>
#x_train转化为64位的LongTensor,y_train维持32位的FloatTensor
x_train,y_train = torch.LongTensor(x_train),torch.Tensor(y_train)
x_val,y_val = torch.LongTensor(x_val),torch.Tensor(y_val)
x_test,y_test = torch.LongTensor(x_test),torch.Tensor(y_test)
# print(type(x_train))=<class 'torch.Tensor'>

#构建标准datasets
train_dataset = Data.TensorDataset(x_train, y_train)
#数据分批
train_loader = Data.DataLoader(
    dataset=train_dataset,# torch TensorDataset format
    batch_size=BATCH_SIZE,      # 最新批数据
    shuffle=True,         # 是否随机打乱数据
    num_workers=2,        # 用于加载数据的子进程
)

val_dataset = Data.TensorDataset(x_val,y_val)
val_loader = Data.DataLoader(
    dataset = val_dataset,
    batch_size = BATCH_SIZE,
    shuffle = True,
    num_workers = 2,
)
test_dataset = Data.TensorDataset(x_test,y_test)
test_loader = Data.DataLoader(
    dataset = test_dataset,
    batch_size = BATCH_SIZE,
    shuffle = True,
    num_workers = 2,
)

网络模型定义

###############################03 定义网络模型#############################
# TextRNN-> 单独放于model.py
import torch
from torch import nn
import torch.nn.functional as F
 
# 文本分类,RNN模型
class TextRNN(nn.Module):   
    def __init__(self):
        super(TextRNN, self).__init__()
        # 进行词嵌入,5000个中文单词,每个单词64维,是特征数量
        self.embedding = nn.Embedding(5000, 64) 
         #双向RNN,input_size为每个字的向量维度大小,hidden_size、num_layers自选,bidirectional=True表示双向
        self.rnn = nn.LSTM(input_size=64, hidden_size=128, num_layers=2, bidirectional=False)
        #self.rnn = nn.GRU(input_size=64, hidden_size=128, num_layers=2, bidirectional=True)
        #全连接层+dropout+激活
        self.f1 = nn.Sequential(nn.Linear(256,128),
                                nn.Dropout(0.8),
                                nn.ReLU())
        self.f2 = nn.Sequential(nn.Linear(128,10),
                                nn.Softmax())
 	#自定义前向传播过程
    def forward(self, x):
        #print(x.size())
        x = self.embedding(x)
        #print(x.size())
        x,_ = self.rnn(x)
        #print(x.size())
        x = F.dropout(x,p=0.8)
        x = self.f1(x[:,-1,:])
        #print(x.size())
        return self.f2(x)

上面的各个阶段的x.size打印如下:

torch.Size([32, 600])
torch.Size([32, 600, 64])
torch.Size([32, 600, 256])
torch.Size([32, 128])

设置使用GPU

###############################04 设置使用GPU#############################
#设置使用GPU
cuda = torch.device('cuda')

#使用定义好的RNN,将模型导入GPU
model = TextRNN()
model = model.cuda()

定义损失函数及优化器

###############################05 定义损失函数及优化器#####################
#定义损失函数,MultiLabelSoftMarginLoss是适用于多分类且每个样本只能属于一个类的分类场景
criterion = nn.MultiLabelSoftMarginLoss()

#选用Adam来做优化器
optimizer = torch.optim.Adam(model.parameters(),lr= LR)

训练模型及验证

###############################06 训练模型###############################
#训练过程保存
from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter('cnews_train')

# 加载之前训练过的网络参数,注意换完模型这一块加载旧的会出错
if os.path.exists('model_params.pkl'):
    model.load_state_dict(torch.load('model_params.pkl'))
    
best_val_acc = 0

for epoch in range(EPOCH):
    for step,data in enumerate(train_loader):
        #取出数据及标签
        inputs,labels = data
        
        #将数据及标签送入GPU
        inputs,labels = inputs.cuda(),labels.cuda()
        
        #前向传播
        outputs = model(inputs)
        
        #计算损失函数
        loss = criterion(outputs,labels)
        
        #清空上一轮梯度
        optimizer.zero_grad()
        
        #反向传播
        loss.backward()
        
        #参数更新
        optimizer.step()
        
        #在每次预测中,输出向量最大值得下标索引如果和目标值(标签)相同,则认为预测结果是对的。
        accuracy = np.mean((torch.argmax(outputs, 1) == torch.argmax(labels, 1)).cpu().numpy())

        if  step%100 == 0:
            print('epoch{} loss:{:.4f}'.format(epoch+1,loss.item()))
            writer.add_scalar("Train_Loss", loss.item(), epoch*len(train_loader)+step)
            
    #对模型进行验证
    if (epoch+1)%3 == 0:
        for step, data in enumerate(val_loader):
            #取出数据及标签
            inputs,labels = data
            #将数据及标签送入GPU
            inputs,labels = inputs.cuda(),labels.cuda()
            #前向传播
            outputs = model(inputs)

            accuracy = np.mean((torch.argmax(outputs, 1) == torch.argmax(labels, 1)).cpu().numpy())
            writer.add_scalar("Accuracy", 100.0*accuracy, (epoch+1)//3)
            if accuracy > best_val_acc:
                # 如果准确率有提升则保存模型参数
                torch.save(model.state_dict(), 'model_params.pkl')
                best_val_acc = accuracy
                print('model_params.pkl saved')
            print(accuracy)

结果与调参过程就不具体展示了,因为感觉效果并不是特别好,还有很大优化空间。中间遇到好多问题,简单记录一下:

  1. GPU缓存不够的问题:在训练时刚开始BATCH_SIZE设置的1000,一开始训练就报错了,告诉我需要多少显存,但我不够。后来上云之后还是不够,于是降低了BATCH_SIZE到32,才能正常开始训练。
  2. 笔记本电脑不时报一下GPU不可用的错,在网上找了一下没有特别好的解决方案,可能是GPU电压不稳或者过热;反正有点玄学了,最后只能关机歇一会再开机就能正常,最后直接放弃使用云算力了。
  3. 目前模型使用的是双向的LSTM,注意如果换成单向的模型,f1中的nn.Linear(256,128)就得改变维度成(128,128)了,不然会报程序输入的尺寸不匹配。
  4. 模型定义部分在放在单独的py文件时,可能导致程序运行报错,一般报错是维度不匹配。可以加打印一层一层的看输入输出tensor。也可以将模型定义代码直接转入主程序中编译。
  5. loss波动较小,准确率不高。(待后期解决)

测试集测试

第一种想法,先在测试集上测试一下整体的准确率和某些个别样本:

###############################07 测试集上测试模型###############################
model.load_state_dict(torch.load('model_params.pkl'))

for test_step,test_data in enumerate(test_loader):
    inputs,labels = test_data
    #将数据及标签送入GPU
    inputs,labels = inputs.cuda(),labels.cuda()
    #前向传播
    outputs = model(inputs)
    if(0==test_step%30):
        label_index = torch.argmax(labels[0]).item()
        output_index = torch.argmax(outputs[0]).item()
        
        #print(outputs[0],labels[0])
        print('实际类别{}:{}'.format(test_step//30,categories[label_index]))
        print('模型归类{}:{}'.format(test_step//30,categories[output_index]))
    accuracy = np.mean((torch.argmax(outputs, 1) == torch.argmax(labels, 1)).cpu().numpy())
print('准确率:{:.4f}%'.format(100.0*accuracy))

结果如下:

实际类别0:体育 模型归类0:体育

实际类别1:娱乐 模型归类1:时政

实际类别2:房产 模型归类2:房产

实际类别3:时尚 模型归类3:家居

实际类别4:家居 模型归类4:家居

实际类别5:娱乐 模型归类5:家居

实际类别6:娱乐 模型归类6:娱乐

实际类别7:娱乐 模型归类7:娱乐

实际类别8:体育 模型归类8:家居

实际类别9:时尚 模型归类9:家居

实际类别10:游戏 模型归类10:家居

准确率:56.2500%

准确率还是很低,我想看在这种情况下随便给一段文本模型的泛化能力,于是我在网上随便找两段新闻看看它到底属于哪一个类别:

import tensorflow.contrib.keras as kr

 
vocab_file = 'cnews.vocab.txt'
 
class RnnModel:
    def __init__(self):
        # 获取文本的类别及其对应id的字典
        self.categories, self.cat_to_id = read_category()
        self.words, self.word_to_id = read_vocab(vocab_file)
        
        self.model = TextRNN()
        self.model.load_state_dict(torch.load('model_params.pkl'))
 
    def predict(self, message):  
        content = message
        data = [self.word_to_id[x] for x in content if x in self.word_to_id]
        data = kr.preprocessing.sequence.pad_sequences([data], 600)
        data = torch.LongTensor(data)
        y_pred_cls = self.model(data)
        class_index = torch.argmax(y_pred_cls[0]).item()
        return self.categories[class_index]
 
 
if __name__ == '__main__':
    model = RnnModel()
    test_demo = [['据台湾媒体报道,三浦春马18日惊传在东京自宅轻生死亡,享年30岁,消息不只震撼演艺圈,粉丝也心碎不已,格外引起讨论的是,好友贺来贤人4小时前在Instagram以黑底白字发了一则限时动态,“真的希望SNS(社群网站)上可以多一点正能量。”此文目前已悄悄发酵且引发不少网友联想是否和他的离世有关系。贺来贤人与三浦春马同样都是Amuse事务所的旗下艺人,他18日早上11点多更新IG动态,他无奈表示人们总会嘲笑别人喜欢的东西、拼命努力去做的事情,是那么简单,否定他人、说喜欢讨厌也是那么简单,实在太简单,比绑鞋带还简单,他期盼未来在各大社群网站上能多一点正面能量。事实上,三浦春马2019年8月来台时也有提到贺来贤人,他透露如果要组团的话,会找贺来贤人、新田真剑佑一起组3人男团,之所以没选佐藤健是因为“佐藤健比较适合一枝独秀”,显见他们私底下的好交情。还记得三浦春马去年来台时感性地说,真心觉得成为艺人是一件很棒的事,粉丝的存在也让他能够得到力量,“今后希望大家能继续关注我,我也会继续努力下去,带给大家更好的面貌。”事隔不到1年,却传来不幸消息,让粉丝相当难过。(ETtoday/文)'],['北京时间7月17日消息,29岁的前德国国脚安德烈-许尔勒在接受采访时宣布退役,他刚刚和多特蒙德解除了合同。2014年世界杯决赛,他的助攻帮助格策打入制胜球,德国1-0击败阿根廷夺得冠军。接受采访时,许尔勒确认自己决定退役:“我已经不再需要掌声了。”许尔勒出生于1990年11月6日,职业生涯开始于美因茨,2010-11赛季,许尔勒、霍尔特比和绍洛伊组成的美因茨三叉戟表现出色,许尔勒那个赛季打入了15个进球,2011年许尔勒加盟勒沃库森,在这里效力了两个赛季后,他在2013年夏天加盟切尔西,转会费达到1800万英镑。2014年3月,许尔勒在对阵富勒姆的英超联赛中上演帽子戏法,帮助切尔西3-1赢下德比战。两周后,许尔勒在切尔西6-0大胜阿森纳的经典战役中传射建功,当选全场最佳。效力切尔西期间,他一共出场65次打进14球,成为切尔西2014-15赛季英超冠军的成员。2015年2月,许尔勒以2200万英镑的转会费离开切尔西加盟沃尔夫斯堡,在这里他出场63次打进13球,夺得德国杯和德国超级杯的冠军。2016年夏天,他离开沃尔夫斯堡加盟多特,在多特他一共上场51次打进8球,过去两个赛季他都被外租。上赛季租借去了富勒姆,本赛季则是租借去了莫斯科斯巴达。几天前,多特提前结束和许尔勒的合同,他之前的合同还剩下一年。作为一名德国国脚,许尔勒最高光的时刻毫无疑问是2014年世界杯决赛,他助攻格策在加时赛完成绝杀,帮助德国1-0力克阿根廷,时隔24年后再次捧起了大力神杯。在德国国家队,许尔勒出场57次打进22球。']]
    for i in test_demo:
        print(i,":",model.predict(i))

最后运行的结果是两段文本都被归为财经类,但实际应该是”娱乐“和”体育“。说明模型真的不咋地,待后期优化吧,这个月的云算力资源用完了。。

总结

cnews中文文本分类,虽然最后训练结果不是特别理想,但是整个过程还是让人收益匪浅。从中我们至少可以学习到以下几点:

  • 文本数据的处理(文本读取、文本内容及标签ID化、数据规范化)
  • RNN网络模型构建与定义(包括各层之间的连接与调试、维度理解)
  • GPU使用相关的幺蛾子,一般和硬件、框架版本关系较大
  • 损失函数的多样化使用与注意事项
  • 验证集与测试集的使用,模型测试的多样性
  • pytorch框架的使用,常用函数与方法的使用

但还是做的不够,问题在哪还没找到,loss还很大,而且还不怎么变化,还需要调教。加油吧,调好后再会更新。


加油加油!做好自己的事,爱自己。你若盛开,蝴蝶自来!


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值