第一次个人编程作业

论文相似度计算系统

相关链接

GItHub项目仓库

GitHub博客

博客园

算法及流程图

  1. 在不断地使用搜索引擎和翻看各路大佬的博客之后,决定使用比较简单的算法(老懒狗了),直接用jieba分词(关于jieba的使用,GitHub上有详细的说明)并计算各个词的TF-IDF值(停用词表采用的是jieba自带的,TF-IDF语料库也是采用jieba自带的),再用TF-IDF值构造词向量,最后计算余弦相似度(虽然算法简单,但貌似效果还不错)。

    有关 TF-IDF 具体可以参考阮一峰老师的两篇博客:

    我和博客中的算法稍稍有些不一样,博客中的相似度计算的方法,是先从两篇文章中各提取出TF-IDF值最高的20个关键字,取并集,然后根据这些关键词的出现频率生成两个词向量。

  2. 流程图

计算模块接口的设计与实现过程

(这么头疼的玩意当然是用 Python 比较爽了)

  1. 类的定义

    只定义了一个 Document 类,用来存放读取后的文本,分词的结果及每个词对应的 TF-IDF 的字典。

    • 初始化的时候从给定的路径中读取文本,然后调用 self.__analyse( ) 方法分析文本
        def __init__(self, path):
    
            self.__path = path
            self.__words = []
            self.__tfidf = {}
    
            # read txt file
            try:
                self.__file = open(path, encoding='utf-8')
                self.__text = self.__file.read()
                self.__file.close()
            except:
                print("无法读取 %s"%(self.__path))
                raise
    
            # analyse text
            self.__analyse()
    
    • self.__analyse( ) 方法通过调用 jieba.analyse.extract_tags( ) 分词并获取各个词的TF-IDF值
        # analyse text
        def __analyse(self):
            for word,tfidf in jieba.analyse.extract_tags(self.__text, topK=0, withWeight=True):
                self.__words.append(word)
                self.__tfidf[word] = tfidf
            # print(self.__words)
    
    • 提供了两个接口函数

      get_words( ) 用来获取文本中的词汇,返回的是一个列表。

    # get words list
    def get_words(self) -> list:
        return self.__words
    

    get_tfidf( ) 用来查询某个词在该文本中的TF-IDF值,如果文本中没有该词则返回0。

    # get TF-IDF
    def get_tfidf(self, word) -> float:
        return self.__tfidf.get(word, 0)
    
  2. 相似度计算函数

    参数为两个 Document 对象,返回两个文本的余弦相似度。

    # caculate similiarity
    def caculate_similarity(doc_1:Document, doc_2:Document) -> float:
    
        keywords_1 = set(doc_1.get_words())
        keywords_2 = set(doc_2.get_words())
        if len(keywords_1) == 0 or len(keywords_2) == 0:
            print("文本无有效词汇,请输入包含有效词汇的文本路径")
            raise
    
        keywords = keywords_1 and keywords_2
    
        # build vector
        vector = []
        for keyword in keywords:
            vector.append((doc_1.get_tfidf(keyword), doc_2.get_tfidf(keyword)))
    
        # caculate cosine_similiarity
        v_1 = 0
        v_2 = 0
        v = 0
        for i in vector:
            v_1 += i[0] ** 2
            v_2 += i[1] ** 2
            v += (i[0] * i[1])
    
        similiarity = v / math.sqrt(v_1 * v_2)
    
    
        # return caculate result
        return similiarity
    
    

计算模块接口部分的性能改进

  1. 用pycharm自带的 profile (pycharm永远滴神)分析了性能,生成了以下图片。

    放大之后可以看到

    总共耗时4秒多,说实话挺慢的。

    但再细看各个部分


    基本上是jieba占据了大部分时间开销。

    jieba.analyse.extract_tags(self.__text, topK=0, withWeight=True)
    

    这个函数可以用来分词(自动去除了标点、停用词等)并返回各个词的TF-IDF值的,最开始我是采用分词后直接用词频率构造词向量去算相似度,虽然在效果上可能没有TF-IDF来的好,但明显快多了。(写完了才知道有gensim这玩意,应该会比这个快)

    可以看到计算的部分其实耗时是非常少的:

计算模块部分单元测试展示

  • 看着别人的博客照葫芦画瓢写了几个测试,第一次搞这玩意属实很懵。

    一共测试了提供的九个文本和两个异常处理,一个是文本为空的时候抛出 RuntimeError,还有一个是路径不存在时抛出 FileNotFoundError

C:Users小明venvScriptspython.exe "H:/Workspace/Software Engineering/paper_check_system/ut.py"
test add
0.8601768754747804
test over!

test del
..0.8985891244539704
test over!

test dis_1
.0.9808112866546317
test over!

test dis_10
.0.9233929083371283
test over!

test dis_15
0.7122391923479036
test over!

test dis_3
..0.9613218437864147
test over!

test dis_7
..0.9511418312250954
test over!

test mix
..0.9368555273793027
test over!

test rep
0.7963167032716033
test over!

test err_1
无法读取 123
test over!

test err_2
文本无有效词汇,请输入包含有效词汇的文本路径
test over!


.
----------------------------------------------------------------------
Ran 11 tests in 6.066s

OK


Process finished with exit code 0
  • 生成的 coverage 报告 可以看出 document.py已经100%覆盖

  • 下面是单元测试的代码:

import unittest
import document
import logging
import jieba

class MyClassTest(unittest.TestCase):
    def setUp(self) -> None:
        jieba.setLogLevel(logging.INFO)


    def tearDown(self) -> None:
        print("test over!")


    def test_add(self):
        print('test add')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_add.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)
    def test_del(self):
        print('test del')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_del.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)

    def test_dis_1(self):
        print('test dis_1')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_dis_1.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)

    def test_dis_3(self):
        print('test dis_3')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_dis_3.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)

    def test_dis_7(self):
        print('test dis_7')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_dis_7.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)
    def test_dis_10(self):
        print('test dis_10')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_dis_10.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)
    def test_dis_15(self):
        print('test dis_15')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_dis_15.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)
    def test_mix(self):
        print('test mix')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_mix.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)
    def test_rep(self):
        print('test rep')
        doc_1 = document.Document(r"sim_0.8/orig.txt")
        doc_2 = document.Document(r"sim_0.8/orig_0.8_rep.txt")
        ans = document.caculate_similarity(doc_1, doc_2)
        print(ans)
        self.assertGreaterEqual(ans,0)
        self.assertLessEqual(ans,1)

    def test_err_1(self):
        print('test err_1')
        self.assertRaises(FileNotFoundError, document.Document, '123')

    def test_err_2(self):
        print('test err_2')
        doc_1 = document.Document(r"1.txt")
        doc_2 = document.Document(r"2.txt")
        self.assertRaises(RuntimeError, document.caculate_similarity, doc_1, doc_2)


if __name__ == '__main__':
    unittest.main()

计算模块异常处理

  1. 计算模块只写了一个文本为空的时候抛出异常,因为会导致计算余弦相似度的时候出现除以零的情况(想不到别的了)。

        keywords_1 = set(doc_1.get_words())
        keywords_2 = set(doc_2.get_words())
        if len(keywords_1) == 0 or len(keywords_2) == 0:
            print("文本无有效词汇,请输入包含有效词汇的文本路径")
            raise
    

PSP 表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 60 40
Estimate 估计这个任务需要多少时间 30 30
Development 开发 180 240
Analysis 需求分析 (包括学习新技术) 480 600
Design Spec 生成设计文档 60 40
Design Review 设计复审 30 40
Coding Standard 代码规范 (为目前的开发制定合适的规范) 30 15
Design 具体设计 120 120
Coding 具体编码 180 240
Code Review 代码复审 30 30
Test 测试(自我测试,修改代码,提交修改) 120 240
Reporting 报告 20 20
Test Report 测试报告 30 40
Size Measurement 计算工作量 20 20
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 30 40
合计 1420 2025

总结

讲实话学算法和写代码的时间好像花的也并不是很多,到是折腾 git、单元测试、性能分析啥的折腾了挺久(都是第一次用,研究了好长时间)。为了写这次作业,疯狂的使用搜索引擎,各种看博客,才慢慢磨出了这结果,老折磨了。u1s1还是自己学得太少,才写一次作业就得各种补漏(要是以前没事的时候多学点东西多好)。虽然累,但是靠着百度的力量,不仅学到了很多从来没接触过的知识,很大程度上也提高自己解决问题的能力,也算是一次很不错的体验。还是希望以后别当懒狗了,积极一点,多学点知识,以后总用得上的。

原文地址:https://www.cnblogs.com/lin-xm/p/13670481.html