赞
踩
目录
数据集从 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特征。
隐含层:对输入数据的求和平均。
输出层:文档对应标签。
将词转换为编号
比如:一开始文档中的词是长这个样子
内容 标签
对文件中的中文进行分词处理,按照一个字一个字的分开:
['中','华','女','子','学','院',':','本','科','层','次','仅','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
将数据按照批量进行。
批量记载数据的原因:深度学习模型的参数非常多,为了得到模型的参数,需要用大量的数据对模型进行训练,所以数据量一般是相当大的,不可能一次性加载到内存中对所有数据进行向前传播和反向传播,因此需要分批次将数据加载到内存中对模型进行训练。使用数据加载器的目的就是方便分批次将数据加载到模型,以分批次的方式对模型进行迭代训练。
- 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
代码里面有所有需要模型的配置参数,以及模型类
- 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
- 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)
- 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
- 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))
代码来源:《自然语言处理应用与实战》 韩少云等编著 著
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。