ciLin.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. '''
  4. @author: yaleimeng@sina.com
  5. @license: (C) Copyright 2018 under MIT License
  6. @desc: 按照2016年9月论文《基于路径与深度的同义词词林词语相似度计算》_陈宏朝、李飞、朱新华等,改进相似度算法。
  7. 单纯在词林范围内,在MC30上的皮尔逊系数达到了论文标称的0.856
  8. @DateTime: Created on 2018/2/28, at 下午 14:48 by PyCharm
  9. '''
  10. class CilinSimilarity(object):
  11. """ 基于哈工大同义词词林扩展版计算语义相似度 """
  12. def __init__(self):
  13. """
  14. 'code_word' 以编码为key,单词list为value的dict,一个编码有多个单词
  15. 'word_code' 以单词为key,编码为value的dict,一个单词可能有多个编码
  16. 'vocab' 所有不重复的单词,便于统计词汇总数。
  17. 'N' 为单词出现累计次数,含重复出现的。
  18. """
  19. self.N = 0
  20. self.code_word = {}
  21. self.word_code = {}
  22. self.vocab = set()
  23. self.read_cilin()
  24. def read_cilin(self):
  25. """
  26. 读入同义词词林,编码为key,词群为value,保存在self.code_word
  27. 单词为key,编码群为value,保存在self.word_code
  28. 所有单词保存在self.vocab
  29. """
  30. with open('./cilin/cilin.txt', 'r', encoding='gbk') as f:
  31. for line in f.readlines():
  32. res = line.split()
  33. code = res[0] # 词义编码
  34. words = res[1:] # 同组的多个词
  35. self.vocab.update(words) # 一组词更新到词汇表中
  36. self.code_word[code] = words # 字典,目前键是词义编码,值是一组单词。
  37. self.N += len(words) # 包含多义词的总义项数。
  38. for w in words:
  39. if w in self.word_code.keys(): # 最终目的:键是单词本身,值是词义编码。
  40. self.word_code[w].append(code) # 如果单词已经在,就把当前编码增加到字典中
  41. else:
  42. self.word_code[w] = [code] # 反之,则在字典中添加该项。
  43. def get_common_str(self, c1, c2):
  44. """ 获取两个字符的公共部分,注意有些层是2位数字 """
  45. res = ''
  46. for i, j in zip(c1, c2):
  47. if i == j:
  48. res += i
  49. else:
  50. break
  51. if 3 == len(res) or 6 == len(res):
  52. res = res[:-1]
  53. return res
  54. def get_layer(self, common_str):
  55. """
  56. 根据common_str返回两个编码【公共父节点的孩子】所在的层好。从下往上为0--5。
  57. 如果没有共同的str,则位于第5层;
  58. 如果第1个字符相同,则位于第4层;
  59. 注意:最底层用0表示。
  60. """
  61. length = len(common_str)
  62. table = {1: 4, 2: 3, 4: 2, 5: 1, 7: 0}
  63. if length in table.keys():
  64. return table[length]
  65. return 5
  66. def code_layer(self, c):
  67. """将编码按层次结构化: Aa01A01=
  68. 第三层和第五层是两个数字表示;
  69. 第一、二、四层分别是一个字母;
  70. 最后一个字符用来区分所有字符相同的情况。
  71. """
  72. return [c[0], c[1], c[2:4], c[4], c[5:7], c[7]]
  73. def get_k(self, c1, c2):
  74. """ 返回两个编码对应分支的距离,相邻距离为1 """
  75. if c1[0] != c2[0]:
  76. return abs(ord(c1[0]) - ord(c2[0]))
  77. elif c1[1] != c2[1]:
  78. return abs(ord(c1[1]) - ord(c2[1]))
  79. elif c1[2] != c2[2]:
  80. return abs(int(c1[2]) - int(c2[2]))
  81. elif c1[3] != c2[3]:
  82. return abs(ord(c1[3]) - ord(c2[3]))
  83. else:
  84. return abs(int(c1[4]) - int(c2[4]))
  85. def get_n(self, common_str):
  86. """
  87. 计算所在分支层的分支数; 即计算分支的父节点总共有多少个子节点
  88. 两个编码的common_str决定了它们共同处于哪一层
  89. 例如,它们的common_str为前两层,则它们共同处于第3层,则统计前两层为common_str的第三层编码个数就好了
  90. """
  91. if not common_str:
  92. return 14 # 如果没有公共子串,则第5层有14个大类。
  93. siblings = set()
  94. layer = self.get_layer(common_str) # 找到公共父节点的孩子所在层号
  95. for c in self.code_word.keys():
  96. if c.startswith(common_str): # 如果遇到一个编码是以公共子串开头的
  97. clayer = self.code_layer(c) # 找到该编码的一组编码。
  98. siblings.add(clayer[5 - layer]) # 将该公共子串后面的一个编码添加到集合当中。
  99. return len(siblings)
  100. def sim2016(self, w1, w2):
  101. """ 按照论文《基于路径与深度的同义词词林词语相似度计算》_陈宏朝、李飞、朱新华等 2016年9月 """
  102. # 如果有一个词不在词林中,则相似度为0
  103. if w1 not in self.vocab or w2 not in self.vocab:
  104. return 0
  105. # 获取两个词的编码
  106. code1 = self.word_code[w1]
  107. code2 = self.word_code[w2]
  108. sim_max = 0
  109. for c1 in code1: # 选取相似度最大值
  110. for c2 in code2:
  111. cur_sim = self.sim2016_by_code(c1, c2)
  112. sim_max = cur_sim if cur_sim > sim_max else sim_max
  113. return sim_max
  114. def sim2016_by_code(self, c1, c2):
  115. """ 根据编码计算相似度 """
  116. # 先把code的转换为一组类别编号。
  117. clayer1, clayer2 = self.code_layer(c1), self.code_layer(c2)
  118. # 找到公共字符串
  119. common_str = self.get_common_str(c1, c2)
  120. layer = self.get_layer(common_str) + 1 # 找到公共父节点所在层号
  121. Weights = [0, 0.5, 1.5, 4, 6, 8] # 在0号位置补0,方便层序号与下标一一对应。
  122. if len(common_str) >= 7: # 如果前7个字符相同,则第8个字符也相同,要么同为'=',要么同为'#''
  123. if common_str[-1] == '=':
  124. return 1
  125. elif common_str[-1] == '#':
  126. return 0.5
  127. k = self.get_k(clayer1, clayer2)
  128. n = self.get_n(common_str)
  129. Depth = 0.9 if layer > 5 else sum(Weights[layer:]) # 从layer层,累加到最后
  130. Path = 2 * sum(Weights[:layer]) # 要从1,累加到layer-1这一层
  131. # print(common_str, 'K=', k, 'N=', n, '公共父节点层号:', layer, end = '\t')
  132. beta = k / n * Weights[layer - 1] # 公共父节点所在层的权重
  133. return (Depth + 0.9) / (Depth + 0.9 + Path + beta) if layer <= 5 else 0.9 / (0.9 + Path + beta)