可以在这里找到本文对应的Jupyter Notebook.
要运行这个例子,需要更新HarvestText到V0.7及以上版本
pip install --upgrade harvesttext
并且,在这里下载数据集,放到与本文件同一路径下。
from harvesttext import HarvestText
ht = HarvestText()
with open("all_comments.txt", encoding="utf-8") as f:
for i, line in enumerate(f):
if i >= 10:
break
print(f"{i}: {line}")
0: 特神牛逼
1: 请教下,叶尔凡去了哪里?也不见替补哇
2: 下场上!
3: 11号请在东看台26区找我不见不散!//<a href="https://home.zhibo8.cc/user.html?platform=mobile&uid=3348048">@江苏苏宁俱乐部</a>:下场上!
4: 这场看了20分钟就走了。还好是免费的![笑哭]
5: 心疼我的筱婷
6: 卡佩罗牛逼,敢上三个U23球员!为你****?
7: 恭喜特谢拉加入“我奶奶都能进”系列[滑稽][捂脸]
8: 视频裁判这么好用居然还有人喷,减少很多争议。耗时间只是刚开始不够专业需要时间来优化。
9: 就算耗时间得到公正判罚也值得了。
可以看到,这里的数据有以下特点:
处理1需要文本清洗,处理2需要挖掘出人物别名,HarvestText提供了能够方便处理这些问题的接口:
ht.clean_text的默认配置就可以处理这类评论和微博类的数据:
sample = '11号请在东看台26区找我不见不散!//<a href="https://home.zhibo8.cc/user.html?platform=mobile&uid=3348048">@江苏苏宁俱乐部</a>:下场上!'
print(ht.clean_text(sample))
11号请在东看台26区找我不见不散! 下场上!
上面的例子就演示了去除HTML代码,以及回复其他用户的特殊格式"//@user",这些信息一般是无用的干扰信息。
函数还提供了更多其他参数,来处理包括:网址;email;html代码中的 一类的特殊字符;网址内的%20一类的特殊字符等问题,可以使用help方法来看函数的注释,以及参照README上的例子
help(ht.clean_text)
Help on method clean_text in module harvesttext.harvesttext:
clean_text(text, remove_url=True, email=True, weibo_at=True, stop_terms=('转发微博',), emoji=True, weibo_topic=False, deduplicate_space=True, norm_url=False, norm_html=False, to_url=False, remove_puncts=False, remove_tags=True) method of harvesttext.harvesttext.HarvestText instance
进行各种文本清洗操作,微博中的特殊格式,网址,email,html代码,等等
:param text: 输入文本
:param remove_url: (默认使用)是否去除网址
:param email: (默认使用)是否去除email
:param weibo_at: (默认使用)是否去除微博的\@相关文本
:param stop_terms: 去除文本中的一些特定词语,默认参数为("转发微博",)
:param emoji: (默认使用)去除\[\]包围的文本,一般是表情符号
:param weibo_topic: (默认不使用)去除##包围的文本,一般是微博话题
:param deduplicate_space: (默认使用)合并文本中间的多个空格为一个
:param norm_url: (默认不使用)还原URL中的特殊字符为普通格式,如(%20转为空格)
:param norm_html: (默认不使用)还原HTML中的特殊字符为普通格式,如(\ 转为空格)
:param to_url: (默认不使用)将普通格式的字符转为还原URL中的特殊字符,用于请求,如(空格转为%20)
:param remove_puncts: (默认不使用)移除所有标点符号
:param remove_tags: (默认使用)移除所有html块
:return: 清洗后的文本
下面清洗所有文本并保存备用
processed_texts = []
with open("all_comments.txt", encoding="utf-8") as f:
for line in f:
line = ht.clean_text(line, remove_tags=True)
if len(line) > 0:
processed_texts.append(line)
print("\n".join(processed_texts[:10]))
特神牛逼
请教下,叶尔凡去了哪里?也不见替补哇
下场上!
11号请在东看台26区找我不见不散! 下场上!
这场看了20分钟就走了。还好是免费的!
心疼我的筱婷
卡佩罗牛逼,敢上三个U23球员!为你****?
恭喜特谢拉加入“我奶奶都能进”系列
视频裁判这么好用居然还有人喷,减少很多争议。耗时间只是刚开始不够专业需要时间来优化。
就算耗时间得到公正判罚也值得了。
实体的别名之所以会出现,有多种情况:
- 可能是拼写错误(“武磊”经常被写成“吴磊”)
- 是全名缩写等造成的长度变化(“广州恒大淘宝队”, “恒大淘宝队”, “恒大队”, “恒大”)
- 昵称(“特谢拉”, “特神”)
- 其他等等
从自然语言处理的角度来看,前两者是一些模式匹配的问题,而昵称的情况,则需要考虑语义来解决。
V0.7的HarvestText实现了一种我自己称为"NFL"的算法(NER+FastText+Louvain),参考Mining Entity Synonyms with Efficient Neural Set Generation一文中提出的其中一种baseline方法。虽然效果不是最佳的,但还算是一种相对快速且精度尚可的算法,而且很重要的是无监督。并且与原文不同的是,原文的实体发现需要基于知识库的entity linking,而这里我则使用NER来完成这一步,因而彻底摆脱了一切外部数据需求。我用这一算法来实现基于语义的人物别名挖掘。
另外,程序内也实现了一些常见的基于后缀和拼音近似的模式匹配。
ht.entity_discover的方法实现实体别名挖掘,设置参数method="NFL"
就会使用上述语义与模式结合的实体发现与别名挖掘,而使用method="NERP"
则单纯使用NER+Pattern匹配。
%%time
em_dict, et_dict = ht.entity_discover("\n".join(processed_texts), method="NFL", threshold=0.97)
all_mentions = set(x for enty, ments in em_dict.items() for x in ments)
print(f"Num entities: {len(em_dict)}, Num mentions: {len(all_mentions)}")
Doing NER
100%|██████████| 178290/178290 [01:27<00:00, 2043.79it/s]
Pattern matching
Training fasttext
Louvain clustering
Pattern matching
Num entities: 2284, Num mentions: 3795
Wall time: 2min 34s
在大约2分半的时间内,在178290句话里挖掘出了2240个实体的共3723个别名,还是比较高效的。
挑一些典型案例看看效果:
# 拼音的模式匹配得到
print(em_dict["武磊_人名"])
{'武磊', '吴磊'}
# 模式以及语义匹配得到
print(em_dict["恒大队_机构名"])
{'恒大队', '恒大!', '恒大', '恒大集团', '恒大淘宝队', '恒大女排'}
# 是错误的匹配,但是很有趣地体现出了语义/字面值的近似
print(em_dict["郑州_地名"])
print(em_dict["巴西国家队_机构名"])
{'郑州', '杭州', '苏州', '常州'}
{'巴西国家队', '阿根廷国家队', '韩国国家队', '西班牙国家队', '用国家队', '中国国家队', '罗国家队', '巴西国家', '国家队'}
尽管挖掘出了很多有意义的别名,但是错误也有很多,要用于后续的可靠挖掘,还可以手工调整。
ht提供了保存为易于阅读编辑的格式,并读取的API,用来帮助这个过程。
ht.save_entity_info('./entity_info_v1.txt', em_dict, et_dict)
print("\n".join(open("./entity_info_v1.txt", encoding="utf-8").readlines(100)))
放低_其他专名||其他专名 放低||其他专名
捧杀_其他专名||其他专名 捧杀||其他专名 专杀||其他专名
更容易_其他专名||其他专名 更早||其他专名 更爽||其他专名 更容易||其他专名
双冠王_其他专名||其他专名 双冠王||其他专名
格式:
entity||类别 mention||类别 mention||类别
entity||类别 mention||类别
每行第一个是实体名,其后都是对应的mention名,用一个空格分隔,每个名称后面都对应了其类别。
我们选取和编辑一部分的别名,得到entity_info_v2.txt用于后续分析
print(open("./entity_info_v2.txt", encoding="utf-8").read())
武磊_人名||人名 武磊||人名 吴磊||人名
郜林_人名||人名 郜林||人名
上港队_机构名||机构名 上港队||机构名 上港||机构名
恒大队_机构名||机构名 恒大||机构名 恒大队||机构名 恒大淘宝队||机构名
# 读取加载入模型
ht.load_entities("./entity_info_v2.txt")
print(ht.entity_mention_dict)
print(ht.entity_type_dict)
defaultdict(<class 'set'>, {'武磊_人名': {'武磊', '吴磊'}, '郜林_人名': {'郜林'}, '上港队_机构名': {'上港队', '上港'}, '恒大队_机构名': {'恒大淘宝队', '恒大队', '恒大'}})
{'武磊_人名': '人名', '郜林_人名': '人名', '上港队_机构名': '机构名', '恒大队_机构名': '机构名'}
有了挖掘出的实体别名和清洗好的文本,我们就可以相对放心的对其进行进一步的挖掘了。
ht提供了一些挖掘功能,下面以词频和情感分析为例。上面选取了两个球员和球队,哪个人/球队在球迷心目中更被热议,更受好评呢?
inv_index = ht.build_index(processed_texts)
print(ht.get_entity_counts(processed_texts, inv_index))
{'武磊_人名': 3565, '郜林_人名': 1492, '上港队_机构名': 6517, '恒大队_机构名': 13022}
# 使用默认的内置资源建立情感词典,最负面为-1,最正面为+1
senti_dict = ht.build_sent_dict(processed_texts, scale="+-1")
# 假设实体出现的句子的情感都是体现了对其的情感,所有句子的情感的平均值代表了总体好评度
for entity in ht.entity_type_dict:
entity_appeared_docs = ht.search_entity(entity, processed_texts, inv_index)
docs_senti = [ht.analyse_sent(doc) for doc in entity_appeared_docs]
avg_senti = sum(docs_senti) / len(docs_senti)
print(f"{entity}的好评度为:{avg_senti}")
武磊_人名的好评度为:0.3005335343653094
郜林_人名的好评度为:0.2780580786707832
上港队_机构名的好评度为:0.27413765970801507
恒大队_机构名的好评度为:0.26715797690707327
选取数据所在的赛季,武磊的表现确实比郜林更加出彩,而上港则是力压恒大获得了冠军,与我们的情感分析结果恰好吻合。看来我们做了一个基本正确的情感分析,其基础就是我们之前做的数据清洗以及实体别名发现。
进一步还可以实现这样的数据分析: 一文看评论里的中超风云
期待大家能够用HarvestText做出更多有趣有用的数据分析项目!