通过计算并比较文档的摘要,可实现文本的相似度比较。
下面先阐述向量类(Vector) 和文档摘要类(Sketch)的设计与实现,然后使用文档摘要类来比较文档的相似度。
向量是一种数学抽象,n维向量可以使用一个n个实数的有序列表: (x0, x1,…,xn-1)。向量支持基本的四则算数运算,故可通过运算符重载来实现。
向量的基本运算
包括:两个向量的加法、一个向量乘以一个标量(一个实数)、计算两个向量的点积、计算向量大小和方向。
基本的向量(Vector) 类设计思路如下:
_coords
。def __init__(self, a):
"""构造函数:切片拷贝列表参数a到对象实例变量_coords"""
self._coords = a[:]
self._n = len(a)
__getitem__
,返回第 i 个元素,即第 i 维坐标。def __getitem__(self, i):
"""返回第i个元素,即第i维坐标"""
return self._coords[i]
__len__
, 返回向量的维度。def __len__(self):
"""返回向量的维度"""
return self._n
__str__
,返回向量的字符串表示。.def __str__(self):
"""返回向量的字符串表示"""
return str(self._coords)
__add__
、__sub__
,实现向量的运算:加法、减法。加法:x+y = (x0+y0, x1+y1, …, xn-1+yn-1)
减法:x-y = (x0-y0,x1-y1,…,xn-1-yn-1)
def __add__(self, other):
"""返回2个向量之和"""
result = []
for i in range(self._n):
result.append(self._coords[i] + other._coords[i])
return Vector(result)
def __sub__(self, other):
"""返回2个向量之差"""
result = []
for i in range(self._n):
result.append(self._coords[i] - other._coords[i])
return Vector(result)
标量积:ax = (ax0,ax1, …,axn-1)
def scale(self, n):
"""返回向量与数值的乘积(标量积)"""
result = []
for i in range(self._n):
result.append(self._coords[i] * n)
return Vector(result)
点积:xy = x0y0 + x1y1 +… + xn-1yn-1
def dot(self, other):
"""返回2向量的内积(点积)"""
result = 0
for i in range(self._n):
result += self._coords[i] * other._coords[i]
return result
__abs__
,实现向量的运算:大小(模)。大小:|x| = (x02 + x12 + … + xn-12)1/2
def __abs__(self):
"""返回向量的模"""
return math.sqrt(self.dot(self))
方向:x / |x| = ( x0 / |x|,x1 / |x|,…,xn-1 / |x| )
def direction(self):
"""返回向量的单位向量"""
return self.scale(1.0 / abs(self))
vector.py
import math
class Vector:
"""笛卡尔坐标系向量"""
def __init__(self, a):
"""构造函数:切片拷贝列表参数a到对象实例变量_coords"""
self._coords = a[:]
self._n = len(a)
def __getitem__(self, i):
"""返回第i个元素,即第i维坐标"""
return self._coords[i]
def __add__(self, other):
"""返回2个向量之和"""
result = []
for i in range(self._n):
result.append(self._coords[i] + other._coords[i])
return Vector(result)
def __sub__(self, other):
"""返回2个向量之差"""
result = []
for i in range(self._n):
result.append(self._coords[i] - other._coords[i])
return Vector(result)
def scale(self, n):
"""返回向量与数值的乘积(标量积)"""
result = []
for i in range(self._n):
result.append(self._coords[i] * n)
return Vector(result)
def dot(self, other):
"""返回2向量的内积(点积)"""
result = 0
for i in range(self._n):
result += self._coords[i] * other._coords[i]
return result
def __abs__(self):
"""返回向量的模"""
return math.sqrt(self.dot(self))
def direction(self):
"""返回向量的单位向量"""
return self.scale(1.0 / abs(self))
def __str__(self):
"""返回向量的字符串表示"""
return str(self._coords)
def __len__(self):
"""返回向量的维度"""
return self._n
# 测试代码
def main():
xCoords = [2.0, 2.0, 2.0]
yCoords = [5.0, 5.0, 0.0]
x = Vector(xCoords)
y = Vector(yCoords)
print('x = {}, y = {}'.format(x, y))
print()
print('x + y = {}'.format(x + y))
print()
print('10x = {}'.format(x.scale(10.0)))
print()
print('|x| = {}'.format(abs(x)))
print()
print(' = {}' .format(x.dot(y)))
print()
print('|x - y| = {}'.format(abs(x-y)))
if __name__ == '__main__':
main()
运行示例:
x = [2.0, 2.0, 2.0], y = [5.0, 5.0, 0.0]
x + y = [7.0, 7.0, 2.0]
10x = [20.0, 20.0, 20.0]
|x| = 3.4641016151377544
= 20.0
|x - y| = 4.69041575982343
文档摘要类(Sketch) 用于封装文档的摘要信息。
设计思路如下:
定义带3个列表参数(text (文本)、k (k-grams) 、d (文档摘要向量的维度) )的构造函数。
def __init__(self, text, k, d):
使用列表解析创建一个包含d个元素的列表freq (初始值为0),用于存储k-grams的频率。
freq = [0 for i in range(d)]
循环抽取文本的所有k-grams,并使用hash函数映射到0-d之间的整数,从而更新对应的列表freq的元素(递增)。
for i in range(len(text) - k):
kgram = text[i:i+k]
freq[hash(kgram) % d] += 1
然后使用freq创建Vector对象vector,并调用向量对象的Direction方法进行归一化。
vector = Vector(freq)
最后把文档摘要向量vector并保存到实例对象属性:_sketch
。
self._sketch = vector.direction()
similarTo()
,计算两个文档摘要向量的余弦相似度。abs(x - y)
, 余弦相似性度的计算方法为: x.dot(y)
。def similarTo(self, other):
"""比较两个文档摘要对象Sketch的余弦相似度"""
return self._sketch.dot(other._sketch)
__str__
,返回向量的字符串表示。def __str__(self):
return str(self._sketch)
sketch.py
import sys
from vector import Vector
class Sketch:
"""计算文本text的k-grams的文档摘要向量(d维) """
def __init__(self, text, k, d):
"""初始化函数:计算文本text的文档摘要向量"""
freq = [0 for i in range(d)] #创建长度为d的列表,初始值0
for i in range(len(text) - k): #循环抽取k- grams,计算频率
kgram = text[i:i+k]
freq[hash(kgram) % d] += 1
vector = Vector(freq) #创建文档摘要向量
self._sketch = vector.direction() #归一化并赋值给对象实例变量
def similarTo(self, other):
"""比较两个文档摘要对象Sketch的余弦相似度"""
return self._sketch.dot(other._sketch)
def __str__(self):
return str(self._sketch)
# 测试代码
def main():
with open("华夏.txt", "r", encoding='UTF-8') as f:
text = f.read()
sketch = Sketch(text, 5, 100)
print(sketch)
if __name__ == '__main__':main()
因为我打开的文本“华夏.txt”的编码方式是UTF-8,所以需要指定第三个参数:open("华夏.txt", "r", encoding='UTF-8')
而且,上述代码的输出是列表,列表中的每个值是文本循环输出的的k-grams(k个连续字符)的向量方向(即余弦值),就不将运行结果放出来了。
补充说明:哈希函数基于一个数值“种子”计算,在Python 3中,哈希种子会改变(缺省情况下),即给定对象的哈希值可能每次运行结果都不一样。因而,程序输出结果可能不同。
document_compare.py
import sys
from vector import Vector
from sketch import Sketch
# 测试文档列表
filenames = ['华夏.txt', '中国.txt', '炎黄子孙.txt']
k = 5 #k-grams
d = 10000 #文档摘要维度
sketches = [0 for i in filenames]
for i in range(len(filenames)):
with open(filenames[i], 'r', encoding='UTF-8') as f:
text = f.read()
sketches[i] = Sketch(text, k, d)
# 输出结果标题
print(' ' * 20, end = ' ')
for filename in filenames:
print('{:>25}'.format(filename), end = ' ')
print()
# 输出结果比较明细
for i in range(len(filenames)):
print('{:10}'.format(filenames[i]), end = ' ')
for j in range(len(filenames)):
print('{: <22}'.format(sketches[i].similarTo(sketches[j])), end = ' ')
print()
运行结果:
华夏.txt 中国.txt 炎黄子孙.txt
华夏.txt 0.9999999999999785 0.5011577874531165 0.42527486798147096
中国.txt 0.5011577874531165 1.0000000000000089 0.536571712819202
炎黄子孙.txt 0.42527486798147096 0.536571712819202 1.0000000000000757
结果表明,相同文档的相似度为1,相同类型的文档(华夏.txt和中国.txt)相似度尚可,而不同类型的文档(华夏.txt和炎黄子孙.txt)的相似度则稍微低些。