一:文本相似度比较概述
通过计算并比较文档的摘要,可实现文本的相似度比较。
- 文档摘要的最简单形式可以使用文档中的k-grams(k个连续字符)的相对频率的向量来表示。
- 假设字符的取值可能有128种不同的值(ASCII码) ,则向量的维度d为128k;对于Unicode编码,这更是天文数字。
- 因此,一般使用哈希函数hash(s) % d把k-grams字符串s映射到0到d-1之间的整数,从而使得文档摘要向量的维度为d。.
- 创建文档摘要向量之后,可通过比较两个文档摘要向量的距离的方法来判断两个文档的相似度。
下面先阐述向量类(Vector) 和文档摘要类(Sketch)的设计与实现,然后使用文档摘要类来比较文档的相似度。
二:向量(Vector) 类设计和实现
向量是一种数学抽象,n维向量可以使用一个n个实数的有序列表: (x0, x1,…,xn-1)。向量支持基本的四则算数运算,故可通过运算符重载来实现。
向量的基本运算
包括:两个向量的加法、一个向量乘以一个标量(一个实数)、计算两个向量的点积、计算向量大小和方向。
- 加法:x+y = (x0+y0, x1+y1, …, xn-1+yn-1)
- 减法:x-y = (x0-y0,x1-y1,…,xn-1-yn-1)
- 标量积:ax = (ax0,ax1, …,axn-1)
- 点积:xy = x0y0 + x1y1 +… + xn-1yn-1
- 大小:|x| = (x02 + x12 + … + xn-12)1/2
- 方向:x / |x| = ( x0 / |x|,x1 / |x|,…,xn-1 / |x| )
基本的向量(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)
- 定义方法scale(),实现向量的运算:标量积。
标量积: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)
- 定义方法 dot(),实现向量的运算:点积。
点积: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))
- 定义方法direction(),实现向量的运算:方向。
方向: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('<x, y> = {}'.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
<x, y> = 20.0
|x - y| = 4.69041575982343
三:文档摘要类(Sketch) 的设计和实现
文档摘要类(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()
,计算两个文档摘要向量的余弦相似度。
比较两个向量的常用方法包括欧几里得距离和余弦相似性度。
给定向量x和y,其欧几里得距离定义为:|x-y| = ((x0 - y0)2 + (x1 - y1)2 +… + (xn-1 - yn-1)2)1/2
余弦相似性度定义为:xy = x0y0 + x1y1 + … + xn-1yn-1
基于Vector对象,给定向量x和y,其欧几里得距离为: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)的相似度则稍微低些。