基于fasttext的文本分类
前段时间使用bert参与了一场竞赛,回过头来看fasttext,感觉这个工具,就一个特点,快,很快,非常快,所以回过头来就整理一下。
工具下载,进入链接后选择合适的版本,下载即可。
进入后点击左侧下载到本地,通过pip install命令安装
数据
这里使用的是新闻数据,每条格式都是“类别+标题+正文”,训练与测试数据自行下载。
数据读取
import pandas as pd
df = pd.read_csv('./cnews/cnews_train.txt',
sep='\t',
error_bad_lines=False,header=None)
df.columns = ['labels', 'str']
数据展示
df.head(3)
labels str
0 体育 商瑞华首战复仇心切 中国玫瑰要用美国方式攻克...
1 体育 冠军球队迎新欢乐派对 黄旭获大奖张军赢下PK赛...
2 体育 辽足签约危机引注册难关 高层威逼利诱合同笑里...
数据标准格式
__label__Shares , 中铁 物资 最快 今年底 A H股 上市 募资 120 亿……
fasttext模型训练时使用的标准数据分为两块,第一块的组成是“__label__”+“类别名称”,这里的类别名称可以是中文也可以是英文,值得一提的是,这个标签其实可以填写多个,但是非常不推荐。
第二块是文章主体,并且主体已经完成分词,并使用空格分割。
第一块与第二块之间使用西文逗号分割。
所以,想要非常简单直接的使用fasttext,首先,得把数据改好。
数据加工
标签加工
# 标签加工
def create_label(x):
return '__label__' + str(x)
df['label_type'] = df['labels'].apply(lambda x: create_label(x))
效果展示
df['label_type'].head(3)
0 __label__体育
1 __label__体育
2 __label__体育
Name: label_type, dtype: object
文章主体加工
文章主体的加工其实也非常简单,只是相对于标签加工而言多了一个分词的步骤,详细的分析原理可以研究研究结巴分词,不得不说的是结巴真的挺强大的,至于到底应该如何选择分词工具、分词模式,仁者见仁智者见智,文中就简单使用,不做详细甄别。
import jieba
def str_cut(x):
x = ' '.join(i for i in jieba.cut(x))
return x
df['text'] = df['str'].apply(lambda x: str_cut(x))
效果展示
df['text'].head(3)
0 商瑞华 首战 复仇 心切 中国 玫瑰 要 用 美国 方式 攻...
1 冠军 球队 迎新 欢乐 派对 黄旭获 大奖 张军 赢 下...
2 辽足 签约 危机 引 注册 难关 高层 威逼利诱 合同 笑里藏刀 新浪...
Name: text, dtype: object
数据合并
df['model_text'] = df['label_type'] + "," + df['text']
效果展示
df['model_text'].head(3)
0 __label__体育,商瑞华 首战 复仇 心切 中国 玫瑰 要 用 美国 方式 攻克 ...
1 __label__体育,冠军 球队 迎新 欢乐 派对 黄旭获 大奖 张军 赢 下 PK ...
2 __label__体育,辽足 签约 危机 引 注册 难关 高层 威逼利诱 合同 笑里藏刀...
Name: model_text, dtype: object
训练数据写出
在这里有个很不解的地方,就是fasttext训练的时候是通过外置文件进行读取训练的,我一直没找到应该怎么样去用脚本内的数据集进行模型的训练,每次都是文件转存好后,通过模型去调用这个文件,离离原上谱……
抽一部分数据作为训练数据进行写出,留一部分数据预测
import numpy as np
from sklearn.model_selection import train_test_split
x_train,x_text = train_test_split(df, train_size=0.1, random_state=18)
np.savetxt('./data_set',
x_train.values,
delimiter=' ',
fmt="%s",
encoding='utf-8')
此处抽取了10%的数据进行训练,剩下90%都作为预测。
模型
模型构建与训练
import fasttext
model = fasttext.train_supervised('data_set',
epoch = 1000,
lr = 0.9,
dim = 5,
wordNgrams = 1,
loss='softmax')
为了使得模型尽可能简化,我在这里选用了一组计算消耗低的参数用于模型训练,常见参数如下:
| 参数 | 含义 |
|---|---|
| lr | 学习率 |
| dim | 向量维度 |
| ws | cbow模型时使用 |
| epoch | 训练轮数 |
| minCount | 词频阈值, 小于该值在初始化时会过滤掉 |
| minCountLabel | 类别阈值,类别小于该值初始化时会过滤掉 |
| minn | 构造subword时最小char个数 |
| maxn | 构造subword时最大char个数 |
| neg | 负采样 |
| wordNgrams | n-gram个数 |
| loss | 损失函数类型[softmaxns--负采样, hs--分层softmax] |
| bucket | 词扩充大小, [A, B]A语料中包含的词向量, B不在语料中的词向量 |
| thread | 线程个数, 每个线程处理输入数据的一段, 0号线程负责loss输出 |
| lrUpdateRate | 学习率更新 |
| t | 负采样阈值 |
模型保存
# model.save_model("./model/fasttext.bin")
# model = fasttext.load_model("./model/fasttext.bin")
模型预测
def x_pre(x):
f = model.predict(x)[0][0].split(',')[0].replace('__label__','')
return f
df['pred'] = df['text'].apply(lambda x: x_pre(x))
模型检验
acc_lst = []
for i in range(len(df)):
if df['labels'][i] == df['pred'][i]:
acc_lst.append(1)
else:
acc_lst.append(0)
sum(acc_lst)/len(acc_lst)
效果展示
sum(acc_lst)/len(acc_lst)
0.84718
可以看到,我们使用了新闻数据集10%的数据来进行训练,训练好的模型在整个数据集上的精度是84%,相对而言效果还是不错的。
说一些题外话
之前有一个省份的项目,是工单相关的,原来的模型用了一种让人很不理解的方法:
1 分词
2 tf-idf提取词汇
3 词汇编码构建词向量
4 词向量使用lightgbm构建分类模型
这个模型很帅,有自定义业务分词,有逆文档词频,有编码,有集成学习。啥都有,就是效果缺了点。值得一提的是,这个模型全程没有注释,里边还掺杂了大量的业务逻辑,我看了一天,人都傻了。在这里恳求大家,一定要养成代码配注释的好习惯。
后来项目组到交付期限了,模型有,但是一直达不到测试要求,于是转了一圈最后到了研发中心,我和项目人员沟通的时候,得到的方案是:能不能优化一下?我痛定思痛后果断拒绝,然后用正文里看起来很小清新的方法,把这个项目的核心部分全部重构了,后来反馈也挺好,顺利交付。
在这里就得说一些项目上的思考,很多时候,项目会在追逐”算法高大上“的路上越走越远,但是如果真的静下心来看一看,就会发现,其实有很多看起来不起眼但是效果真的、不不错的方法可以选择,就像上边的例子里,是有机会和有条件去进行bert训练的,但是为什么最后会选择效果相对偏差的fasttext呢?
主要还是因为一点:算力,工单方面想要实现快速响应,尽管bert的效果辗压群雄,但是慢,今天接到客户需求说,近期整理好了一批数据,想加工测试一下看看模型会不会有提升,明天给个报告。能说做不了嘛?不能。
所以,前沿的技术不一定要用但是要会,并且实际项目中一定要务实,不然,你永远不知道,会不会在什么时候坑到自己,或者是坑到一个素不相识的小倒霉蛋。