目录
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 代码地址
代码来源:《自然语言处理应用与实战》 韩少云等编著 著