这个数据集是由清华大学根据新浪新闻RSS订阅频道2005-2011年间的历史数据筛选过滤生成的,数据集包含50000个样本的训练集,5000个样本的验证集,10000个样本的测试集,词汇表5000个字/词,文本内容一共包含十个分类,包括:
‘体育’, ‘财经’, ‘房产’, ‘家居’, ‘教育’, ‘科技’, ’ 时尚’, ‘时政’, ‘游戏’, ‘娱乐’
数据集我也把它上传了,不需要积分和金币就能下载,地址如下:
cnews中文文本分类数据集
首先我们拿到这么一个数据集,我们想一下我们的目标是什么?对,就是文本分类。也就是说这是一个分类任务。想想咱之前已经做过的分类任务有哪些?cifar-10图像分类、mnist手写数字识别,那么相关的经验能不能借鉴呢?肯定是可以的,但是又有什么不同呢,之前都是图片,转化后是一个多维的tensor,最里层是一个32x32或28x28的像素矩阵,而现在我们拿到的是一篇篇长短不一的文本,我们要想按照之前的套路来,首先就要将文本转化为数值化的tensor,每一篇文本的类别也要转化为数值化的label,这样才能正常进行训练。其他的目前看起来区别不大,先开始吧,遇到问题再解决。
因为设计到的内容可能比较多,我们将预处理的各部分代码放置在不同的py文件中,供主程序调用。
对于文本的处理我们有这样一个思路,先给词汇表里的每一个字进行编号,5000个字那就是0-4999,编完号之后再去将我们的文本进行数值化:将文本里的每一个字转化为其在词汇表中的编号,如果某一个字不在词汇表中,那么它将被抛弃。这样我们就得到一个长短不一全是数值的文本列表,再将其进行长度统一化规范化处理,设置一个截断值(比如600),如果文本A超过600字,就将其截断成600的长度,如果它不足600字,我们就给她进行无意义字符的填充,让它长度达到我们的要求。同样的,对于十个类别,我们也可以直接给它们进行0-9的编号,这样我们就能得到一堆可训练数据的初始形态了,在进行必要的维度转化、类型转换,就可以得到我们训练所需的标准数据集了。嗯,差不多就是这么个思路,开干。
将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。。。。。}
# 读取分类目录,固定
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表示
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
#######################################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))=
#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))=
#构建标准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])
###############################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)
结果与调参过程就不具体展示了,因为感觉效果并不是特别好,还有很大优化空间。中间遇到好多问题,简单记录一下:
- GPU缓存不够的问题:在训练时刚开始BATCH_SIZE设置的1000,一开始训练就报错了,告诉我需要多少显存,但我不够。后来上云之后还是不够,于是降低了BATCH_SIZE到32,才能正常开始训练。
- 笔记本电脑不时报一下GPU不可用的错,在网上找了一下没有特别好的解决方案,可能是GPU电压不稳或者过热;反正有点玄学了,最后只能关机歇一会再开机就能正常,最后直接放弃使用云算力了。
- 目前模型使用的是双向的LSTM,注意如果换成单向的模型,f1中的nn.Linear(256,128)就得改变维度成(128,128)了,不然会报程序输入的尺寸不匹配。
- 模型定义部分在放在单独的py文件时,可能导致程序运行报错,一般报错是维度不匹配。可以加打印一层一层的看输入输出tensor。也可以将模型定义代码直接转入主程序中编译。
- 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中文文本分类,虽然最后训练结果不是特别理想,但是整个过程还是让人收益匪浅。从中我们至少可以学习到以下几点:
但还是做的不够,问题在哪还没找到,loss还很大,而且还不怎么变化,还需要调教。加油吧,调好后再会更新。
加油加油!做好自己的事,爱自己。你若盛开,蝴蝶自来!