首先说明一下,这些句子均来自一个特定的领域(如教育,娱乐明星,游戏),且无标签。
先讲一下大体流程。首先对句子分词并进行词语过滤,并根据word2vec的思想,对句子进行了向量化,接着对高纬度向量表征的句子进行了PCA降维,然后用kmeans对句子进行了聚类,再利用相似度阈值的思想过滤掉了一些“噪音”句子,最后根据聚起来的相同类别的句子的高频共现词得到该类的几个标签。
最终我们保留了这些聚类中心及对应的标签,以便于对于每一个新的句子,我们都能找到与它相似度最高的中心,并且找到其相对应的标签。
句子分词
句子分词用的是现成的工具,分词过后对句子进行了停用词和关键词(各有一个词典)的过滤。停用词就是字面意思,所谓关键词就是我们只保留分词后我们所关心的那一部分词,即专业领域中我们关心的那一部分词语。
word2vec
大概用了50w句语料,总字数为几千万到亿计。采用的word2vec的工具包是python的gensim.word2vec,保存的词向量是150维的,最后大概得到了15w字的词典。(但最后实际用到的只有关键词的词向量)
句子向量化
相较于词向量化有经典的word2vec方法,句子向量化并没有比较统一的方法。可以直接把词向量相加求平均,可以用和word2vec相似的思路进行doc2vec,也可以采用神经网络自编码的思路进行处理。
我这里采用的是知乎上一名答者提供的方法:
如何用 word2vec 计算两个句子之间的相似度? - 鲁灵犀的回答 - 知乎https://www.zhihu.com/question/29978268/answer/55338644
他的回答比较简略,经过代码实现后我在此将更加清晰的步骤进行记录:
假设我们词典里有M个词(不关心的词就直接丢掉,只保留一些我们关心的词)。
那么我们的句子也要相应的向量化为M维的向量(不要担心,这个句子会是个稀疏向量)。
每个位置和词典里的词相对应。假设对句子分词和停词后只剩下N个词了,现在我们就将这N个词的集合看成句子本身。
现在遍历这M维的每个位置对应的词,分别与句子的N个词进行相似度计算(可采用cos),每次取N个相似度中最大的值作为当前位置的最终值,并记录此次是哪个词达到的最大相似度。
遍历结束后我们的M维句子就被相似度最大值填满了,此时他并不是稀疏的。
然后我们对N个词的每个词,分别取其在句子中相似度前10大的值,其余全部舍弃,这样我们得到的句子的非零元素个数一定<=10*N.
这样得到的句子就是一个稀疏的向量。不过我还是将其进行了PCA降维。
这个方法的缺点就是训练每个句子的时间花费时间很长,以及依然没有考虑词语的语序。
有个极大减少训练句子向量时间的方法,就是将所有词对的相似度存为一个字典的形式(如果词语过w的话,这个词典会有上亿个对,会相当大)。
KMeans聚类
在说聚类之前,首先介绍一下衡量两个句子向量相似度的两种基本的思路:一是比较二者的欧式距离,越小代表二者越相似;二是比较二者的余弦值,越大代表二者越相似。
首先我也不清楚哪个衡量标准更好,但既然KMeans聚类天然的衡量标准就是欧式距离最小化,那就先从第一个衡量标准进行尝试,这里使用的是sklearn的包(之后想将KMeans的衡量标准改成余弦值最大化,自己也写了个KMeans的模型试了下,速度差了估计有几十倍,只能暂时作罢)。聚类的个数是通过聚类自身的类里相似度、类间相似度、sklearn用于评价聚类结果的silhouette_score及calinski_harabaz_score综合比较过选择得到的。
这样的得到的聚类结果并不理想,很多类里面的句子在形势和语义上都相去甚远,这时候首先想到排除其中的噪音句子(因为用于聚类的句子数量并不多,所以很多类型的句子可能就只出现了1 2次,自然很难给它分到某一个类里面,这时候它就成了噪音),即那些离自己聚类中心比较远的句子。通过调整阈值,我丢掉了大约1/5的句子,最终对留下来的句子再次进行评价。遗憾的是,这次的结果也不尽如人意。
排除了噪音句子还不行,那么还有两个重要的环节对结果影响也十分重要:1.句子向量化是否合理。2.句子相似度的衡量标准是否合理。
对于第一点,我其实一开始采用的是直接将词向量相加做平均,后来实在觉得这样过于简陋,就去搜索了下才有了上面的方法,个人感觉挺合理的,而且看了下评论大家也说用着不错,所以我就将注意力转移到了第二点上。
欧氏距离衡量不好使,那就自然而然想到了余弦值。我将KMeans的欧氏距离改为余弦值并进行类似的算法推导,直到最后一步之前都还挺顺利的,但是到了最后更新聚类中心点的公式那一步遇到了障碍:我没法根据余弦值最小化得到的结果,将需要更新的聚类中心显示的写出来(可以隐式表示出来),由于时间关系也没再多去思考(纯数学问题,但感觉应该是能够解决的,大家有兴趣的可以尝试一下)。
既然无法改进聚类时的衡量标准,我退而求其次,在筛选噪音点的时候采用了句子和类中心的余弦值作为衡量标准,丢掉了大概1/7的句子。然后再对留下来的聚好类的句子进行了观察,发现这次结果居然比之前好了许多,大概10个类里只有1个类聚的完全风马牛不相及。
迭代优化
上面说到即使效果好了很多,但总体来说仍然不容乐观,仔细观察后,发现很多类是由一些泛泛的词聚起来,如“时间”,“流程”等。我们想将这些词也加入我们的停用词列表里,也就是黑名单,问题的关键就是如何找出这些词。一个比较简单的思想就是我们先把所有idf值比较低(指的是在整个大的语料环境中出现频率高的词,而非领域专业词汇),又在关键词列表中出现的词全部选出来,再人工过滤一波。当然也有其他很多方法,这就需要经验和耐心了,虽然还没有完善这个过程,但是个人预期结果会好很多。
另一个可以优化的就是刚才提到的,如果能够根据余弦值作为相似度进行聚类就更好了,目前还是使用的欧式距离聚类,余弦值筛选过滤一波噪音点来得到的最终聚类结果。
————————5.24更新————————-
之前谈到sklearn的KM聚类只有欧式距离的聚类,后来又发现了更牛逼的faiss支持的KM聚类,速度更快,而且支持基于向量点乘大小的聚类哦,特地在此推荐一下。