写在前边:目前已经通过爬虫等手段获取了千万级的文章类数据,但是目前这些数据是只是简单的基于表层的应用,相对粗粒度的统计,文本之间的很多信息并没有被良好的利用起来。为了提高数据的使用率并获取更多有用信息,尝试用常见的NLP来计算文章之间的相似度,探索文章与文章之间的关联关系,了解文章的传播范围,为后续灵活使用数据提供技术基础。
1、设计思路
1.1、 数据分析
- 数据样式上复杂。在数据探索阶段发现除了有汉语、英语,同时也有不少藏文、特殊符号构成的文章,样式上相对多样,对数据处理有更高的要求
- 数据长度相对较长,大部分都是长文本类数据,一篇文章长的有上万个词语,对数据处理时候的速度影响较大,且在词语数量过多的情况下,会有不少无效词语对整片文章造成“中庸”的影响,即钝化的文章的特征
- 文章内夹杂大量emoji符号,在对文章进行分析的时候,很难去定义符号的含义,如果不考虑这类数据的特征时,实际上有大量的信息被忽略了
- 语义分析复杂,在长文本的特征提取时,使用常规的句法分析工具,提取“主+谓+宾”之类的,作为文本的切分结果。但是句法分析的耗时比较高,适合数据量小的场景,在精度要求没那么高的时候不考虑句向量分析。
1.2、流程设计
-
存量数据相似度计算
-
新增数据相似度计算
2、前期准备
2.1、 表设计
-
word_vec
字段名称 字段英文名 字段类型 文章id id int 文章向量 vec varchar -
similarity_results
字段名称 字段英文名 字段类型 文章id id int 与id进行对比的文章id hit_id int 相似度 similarity float
2.2、 腾讯词向量包下载
考虑到有实时需求,不再通过tf-idf来提取特征。采用腾讯词向量包来计算向量
2.3、 用到的python包
gensim 提取词向量
jieba 分词
mysql.connector mysql连接器
numpy 计算包
json 解析json
time 获取时间
kafka 消费topic
sklearn 机器学习包
2.4、 MysqlBinlog实时读取
需要提前安装部署debezium+kafka,实时的获取mysql的数据
3、开发流程
3.1、导入所需的python包
from gensim.models import KeyedVectors
import jieba
import mysql.connector as mysql
import numpy as np,json
import time
from kafka import KafkaConsumer
3.2、将腾讯词向量包加载进内存,生成字典
def load_tencent_packet():
starttime = time.time()
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + ":开始加载词向量包")
wv_from_text = KeyedVectors.load_word2vec_format("./Tencent_AILab_ChineseEmbedding.txt", binary=False)
#wv_from_text = KeyedVectors.load_word2vec_format(r"F:\5000-small.txt", binary=False)
wv_from_text.init_sims(replace=True)
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + ":词向量包加载完毕")
print("加载词向量包耗时:", time.time() - starttime, "秒")
return wv_from_text
3.3、 创建mysql连接
def getDB():
db = mysql.connect(host='192.168.5.219', user='root', passwd='123456', database='test', charset='utf8')
return db
3.4、 进行中文分词:“我爱北京天安门” --> “我 爱 北京 天安门”,设置停用词并删除非中文字符
def cut_word(text,stop_list):
word_list = []
data_list = jieba.lcut(text.strip())
for word in data_list:
if word not in stop_list:
if u'\u4e00' <= word <= u'\u9fff':
word_list.append(word)
return word_list
3.5、 在词典中找该词语,获取向量
def word2vec(word, wv_from_text, min_n=1, max_n=3):
# 确认词向量维度
word_size = wv_from_text.vector_size
# 计算word的ngrams词组
ngrams = compute_ngrams(word, min_n=min_n, max_n=max_n)
# 如果在词典之中,直接返回词向量
if word in wv_from_text.index_to_key:
return wv_from_text[word]
else:
# 不在词典的情况下,计算与词相近的词向量
word_vec = np.zeros(word_size, dtype=np.float32)
ngrams_found = 0
ngrams_single = [ng for ng in ngrams if len(ng) == 1]
ngrams_more = [ng for ng in ngrams if len(ng) > 1]
# 先只接受2个单词长度以上的词向量
for ngram in ngrams_more:
if ngram in wv_from_text.index_to_key:
word_vec += wv_from_text[ngram]
ngrams_found += 1
# print(ngram)
# 如果,没有匹配到,那么最后是考虑单个词向量
if ngrams_found == 0:
for ngram in ngrams_single:
if ngram in wv_from_text.index_to_key:
word_vec += wv_from_text[ngram]
ngrams_found += 1
if word_vec.any(): # 只要有一个不为0
return word_vec / max(1, ngrams_found)
else:
#print('all ngrams for word %s absent from model' % word)
return 0
3.6、 字典中不存在的词语就找对应的近义词
def compute_ngrams(word, min_n, max_n):
extended_word = word
ngrams = []
for ngram_length in range(min_n, min(len(extended_word), max_n) + 1):
for i in range(0, len(extended_word) - ngram_length + 1):
ngrams.append(extended_word[i:i + ngram_length])
return list(set(ngrams))
3.7、 余弦相似度计算
def cosVector(x,y):
if(len(x)!=len(y)):
print('error input,x and y is not in the same space')
return
result1=0.0
result2=0.0
result3=0.0
for i in range(len(x)):
result1+=x[i]*y[i] #sum(X*Y)
result2+=x[i]**2 #sum(X*X)
result3+=y[i]**2 #sum(Y*Y)
resultnum=result1/((result2*result3)**0.5)
return resultnum
3.8、 执行程序
db = getDB()
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + ":连接数据库成功")
stop_list = get_stop_list("./stop_word.txt")
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + ":获取停用词列表成功")
cursor = db.cursor()
count=0
insert_vec = "replace into word_vec (id,vec) Values ( %s, %s);"
consumer = KafkaConsumer('mysql.sentiment.stm_information',
auto_offset_reset='earliest',
bootstrap_servers=['risen-dn01:9092'])
for message in consumer:
title=json.loads(message.value)["after"]["title"]
message_id=json.loads(message.value)["after"]["id"]
count +=1
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())," id:",message_id," 第",count,"行")
word_list=cut_word(title,stop_list)
vec=get_vec(word_list,wv_from_text)
if vec.size < 200:
content=json.loads(message.value)["after"]["content"]
content_word_list=cut_word(content,stop_list)
if len(content_word_list) >2000:
continue
vec=get_vec(content_word_list,wv_from_text)
if content_vec.size < 200:
continue
m = [str(j) for j in vec]
#将新的向量存储
id_list.append(message_id)
vec_list.append(vec)
cursor.execute(insert_vec, [message_id, ",".join(m)])
#开始计算余弦相似度
allnum = 0
cosnum=0
insert_result="replace into similarity_results (id,hit_id,similarity) Values ( %s, %s,%s);"
cos_reslut=cosine_similarity(vec_list[-30000:])
re_id_list = id_list[-30000:]
cos_list = cos_reslut[-1]
for index, cosR in enumerate(cos_list):
if 0.99 > cosR >0.85 :
print(message_id,"-",re_id_list[index],"-",cosR)
cursor.execute(insert_result, [message_id,re_id_list[index],float(cosR)])
db.commit()
#while(allnum<1000 and cosnum<2):
# print(allnum)
# allnum +=1
# num = random.randint(0, len(vec_list)-1)
# cosDU=cosVector(vec,vec_list[num])
# print(message_id,id_list[num],":",cosDU,"=======",cosnum)
# if 0.8<cosDU<0.99:
# cosnum += 1
# print("======================",cosnum,":",cosDU)
db.close
4、步骤说明
4.1、余弦相似度计算原理
在余弦定理中,两个向量间的余弦值可以通过使用欧几里得点积公式求出:
又因为余弦值的范围在[-1,1]之间,值越趋近于1,代表两个向量的方向越接近
所以将两个文本根据他们词,建立两个向量,计算这两个向量的余弦值,就可以知道两个文本在统计学方法中他们的相似度情况
4.2、 词语转化为向量
本质上就是将无法计算的文本量化成可以计算的向量,在这个过程需要有一套行之有效的计算规则。在设计之初考虑采用TF-IDF进行文本特征的抽取,但是由于可能会有实时的需求,当每来一条数据都会对向量维度造成影响的时候,就舍弃了这种方法,这里我们采用的是腾讯的词向量包。
腾讯词向量包本质上就是腾讯提供的一个 词语->向量 一一对应的数据包。
前边的汉字是词语,后边的数字即这个词语所对应的向量,该向量有200个维度,总共有8824330个词在这个包中有一一对应的向量。
在使用时只需要对文章进行分词,再把一个个词语在数据包中找到对应的向量即可实现文本的量化。
5、小结及后续优化思路
5.1、小结
- 目前的这种设计相对简陋,是因为在文本太长的情况下,分出来的词就会很多,相应的各类计算量就太大了,很多精细化的计算没办法进行,不然对效率的影响非常大,在精简了很多操作之后,现在的效率其实也不高,平均在1.5秒/条。与目前采集数据每秒数十条的速度相比还要进行调整才能达到实时要求。
- 精准度在之前的测试中,要在85%及以上的才可以看到明显的相似,因为为了提升效率,在精度上牺牲了很多,在正式进行运行的时候,因为数据量过大,没办法将每一条数据都去和所有的数据进行匹配,只能筛选一部分来计算,所以计算出来相似的这些文章不一定是最优解。
- 目前采用的是不再分析文章内容,而是从标题入手,当标题不满足计算条件的时候再考虑解析文章内容,可以明显提升效率,因为在很多时候,文章标题就是文章内容的高度总结
5.2、 后续优化思路
- 采用pyflink集群运行模式,提高并行度的情况下,满足我们目前采集速度应该是可以达到的。
- 建立近义词库,每篇文章取词频最高的几个词语,去词库中找相应的文章进行计算相似度,可以缩小匹配范围,既可以提高准确率也可以明显提升效率。
- 增加匹配不到的处理逻辑。当匹配不到相似文章的时候,推荐热度最高的文章