Fork me on GitHub

问句类型分类器

到目前为止,复旦的不倒翁问答系统只能回答事实类问题,不具备闲聊功能,为了真正做到“不倒问答”,我使用 fasttext 做了一个问句类型分类器,用来判断是事实类问题还是闲聊问题,效果好的出奇。

数据集

闲聊部分的数据集使用的是青云数据集,这个数据集很脏(里面有很多不文明的话),大概长这样:

1
2
3
4
5
6
7
南京在哪里 在这里了
咋死???红烧还是爆炒 哦了哦了哦了,咱聊点别的吧
你个小骚货,哥哥的巴操你爽不爽? 不要这样说嘛!很不文明哦
额麻麻怎么会有那玩意儿 无法理解您的话,获取帮助请发送 help
拿尿冲一冲 今天这天气很适合聊天的说
那重点是什么 好话不分轻重!
章子怡新宠 夜曲小三

前面是问题,后面是答案,这里我们只需要问题。

事实类问题是从学长们的遗留代码中找到的,也不知道是哪来的(咱也不敢问),大概长这样:

1
2
3
4
5
6
7
许嵩哪里人!
预期结果导向法的摘要主要写了什么?
功夫的产品类型有哪些?
有谁知道鞭打快牛这个词语怎样用
id设计的专业代码是多少?
西奈半岛属于哪个国家
西门社区的交通位置在哪?

好了,接下来要把数据处理成 fasttext 想要的样子,这里我用0表示闲聊,1表示事实类问题:

1
2
3
4
5
6
7
8
9
10
__label__0 , 不要 每次 都 是 这句 撒
__label__0 , 不要 你 抱
__label__1 , 女人 左手 无名指 戴 戒指 代表 什么
__label__1 , 请问 师范生 技能 适应 于 哪些 人群
__label__1 , 请问 连连看 5 是 什么 语言 的 游戏
__label__0 , 充满 了 我 的 想像
__label__0 , 算 算数
__label__1 , 昀 朵 是 谁 的 粉丝
__label__1 , 根据 声波 测距 原理 开发 出 的 水下 探测器 是
__label__1 , 张 老庄 村 对 战役 有 啥 影响

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import random
import jieba
data = []
with open('question_corpus.txt', 'r', encoding='utf-8') as f:
for line in f.readlines():
line = line.strip(' ?!,.?!,。\n\t\r')
data.append((' '.join(jieba.lcut(line)), 1))
# re = '' #是否只保留中文
# for ch in line:
# if '\u4e00' <= ch <= '\u9fff':
# re += ch
# if re:
# data.append((re, 1))
with open('qingyun.tsv', 'r', encoding='utf-8') as f:
for line in f.readlines():
line = line.split('\t')[0].strip(' ?!,.?!,。\n\t\r')
data.append((' '.join(jieba.lcut(line)), 0))
# re = ''
# for ch in line:
# if '\u4e00' <= ch <= '\u9fff':
# re += ch
# if re:
# data.append((re, 0))
random.shuffle(data)
with open('text_classification_data.txt', 'w', encoding='utf-8') as f:
for each in data:
text, label = each
f.write(text)
f.write('\t')
f.write(str(label))
f.write('\n')
with open('text_classification_data.txt', 'r', encoding='utf-8') as f:
with open('text_classification_data_train.txt', 'w', encoding='utf-8') as f1:
with open('text_classification_data_test.txt', 'w', encoding='utf-8') as f2:
data = f.readlines()
length = len(data)
train_length = int(length * 0.8)
for i in range(train_length):
text, label = data[i].split('\t')
f1.write(f'__label__{label[:-1]} , {text}\n')
for i in range(train_length, length):
text, label = data[i].split('\t')
f2.write(f'__label__{label[:-1]} , {text}\n')

分类器

这里使用的是 fasttext 进行文本分类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import fasttext.FastText as fasttext
import numpy as np
import os
import jieba
def train_model(ipt=None, opt=None, model='', dim=100, epoch=5, lr=0.1, loss='softmax'):
np.set_printoptions(suppress=True)
if os.path.isfile(model):
classifier = fasttext.load_model(model)
else:
classifier = fasttext.train_supervised(ipt, label='__label__',
minCount=1, dim=dim, epoch=epoch, lr=lr, wordNgrams=2, loss=loss)
"""
训练一个监督模型, 返回一个模型对象
@param input: 训练数据文件路径
@param lr: 学习率
@param dim: 向量维度
@param ws: cbow模型时使用
@param epoch: 次数
@param minCount: 词频阈值, 小于该值在初始化时会过滤掉
@param minCountLabel: 类别阈值,类别小于该值初始化时会过滤掉
@param minn: 构造subword时最小char个数
@param maxn: 构造subword时最大char个数
@param neg: 负采样
@param wordNgrams: n-gram个数
@param loss: 损失函数类型, softmax, ns: 负采样, hs: 分层softmax
@param bucket: 词扩充大小, [A, B]: A语料中包含的词向量, B不在语料中的词向量
@param thread: 线程个数, 每个线程处理输入数据的一段, 0号线程负责loss输出
@param lrUpdateRate: 学习率更新
@param t: 负采样阈值
@param label: 类别前缀
@param verbose: ??
@param pretrainedVectors: 预训练的词向量文件路径, 如果word出现在文件夹中初始化不再随机
@return model object
"""
classifier.save_model(opt)
return classifier
dim = 50
lr = 0.5
epoch = 5
model = f'data_dim{str(dim)}_lr0{str(lr)}_iter{str(epoch)}.model'
classifier = train_model(ipt='text_classification_data_train.txt',
opt=model,
model=model,
dim=dim, epoch=epoch, lr=lr
)
# 整体的结果为(测试数据量,precision,recall):
result = classifier.test('text_classification_data_test.txt')
print(result)
if __name__ == '__main__':
while True:
s = input('请输入问句:')
s = s.strip(' ?!,.?!,。\n\t\r')
print(classifier.predict(' '.join(jieba.lcut(s))))

训练的速度出奇的快,基本上都在半分钟以内,而且准确度和召回率都在98%以上。

唯一的缺点是保存下来的模型很大。

donate the author