第一次个人编程作业:论文查重
-
github项目链接:https://github.com/blacksheep107/041803101
1.计算模块接口的设计与实现过程
-
类:
1.readToString 包含方法 String readToString(String fileName) 读入文件名,把文件转为字符串。 2.getCos 包含方法 double getCos(String s1, String s2) 计算两个字符串的余弦值。 3.writeToAnswer 包含方法 void writeIntoAnswer(double sum,String ans) 将结果写入ans文件。
-
流程图
-
实现过程
大致的过程在类中已经说明,这里主要写一下核心算法——余弦相似性的应用。
先贴上参考资料: TF-IDF与余弦相似性的应用
资料中是利用词频计算余弦相似性,这里不再赘述。讲讲踩的坑,我先尝试用字频方法写了一下,结果相当不理想:只有rep的结果接近0.8,其他大部分都在0.98左右,dis的几个文件结果甚至达到1。打开文件一看发现dis文本基本就是原文换个顺序或者加个回车(顺便一提一开始写的是对比每行的重复率,发现dis文本的重复率都奇低,因为中间插了回车= =直接拉胯)。到这里我想着用词频判断会不会好一点,于是又把字换成词(感谢安利我hanlp的hxd)。but,情况没有改善……我冷静下来仔细想想,这整个文本去掉标点,还不计顺序,词频和字频的区别也确实不大……
总之待在坑里我想到另一个方法,原理还是算余弦,这次把所有词的出现位置用Map<String,Vector<Integer> >记录下来,于是每个词在两个文本中都对应有一个向量,也就是获得了这个词在两个文本中的位置向量,接下来可以计算它们的余弦值了。 -
例:
句子A:普通的DISCO我们普通的摇 句子B:旁边普通的路人在普通的瞧
如何计算以上两个句子的余弦值?
-
第一步:分词
在maven中配置hanlp,网上能搜到教程就不多说了,以及这里也踩到坑了,jdk版本太高居然还会配置失败……附个pom.xml文件的图吧
搞半天终于成功了,泪目。
-
第二步:列出所有词,存入它们出现的位置,注意只判断汉字
句子A:普通0 的1 我们2 普通3 的4 摇5
句子B:旁边0 普通1 的2 路人3 在4 普通5 的6 瞧7 -
第三步:得到两个文本中所有词的位置向量
举个栗子:
普通:句子A:[0,3] 句子B:[1,5]
- 第四步:计算两个文本中“普通”的余弦值:cosθ=\(\frac {0×1+3×5}{\sqrt {0+3^2}×\sqrt {1^2+5^2}}\)≈0.98
余弦值可以表示两个向量之间的夹角大小,夹角越小,也就是余弦值越大,两个向量就越相似。(只是我这个不是二维的向量,其实我也不知道能不能叫向量……
使用TreeMap存储词位置向量也是因为要确保是两个文本中相同的词对比,如果某个词只在一个文本中出现就不会被计算到。
- 第五步:计算所有词的余弦值,同时记录词数。所有余弦值相加除以词数就是最终结果了。
结果总算能看了。
2.计算模块接口部分的性能改进
主要的改进思路都在实现过程↑里说了,也就是把词频向量改成词位置向量。如果没用词位置而是字位置应该会更慢,也更耗内存。少踩了一个坑导致现在不知道性能改进怎么写。
- 效能工具用了预培训文档推荐的JProfiler。
性能分析
- Overview
- Live memory
毕竟每个词都要开两个Vector
- CPU views
没想到是matches消耗最大。主要是用来判断汉字了,也就是两个文本的每个字符都要判断一遍……这样想想也挺正常,但是这个好像无法避免啊……
3.计算模块部分单元测试展示
新增了重复率0和1的测试,其他几个用的是作业提供的样例。主要测试getCos方法。
- 测试代码
import org.junit.Assert;
import static org.junit.Assert.*;
public class mainTest {
@org.junit.Test
public void origAndOrig() {
String s1=readToString.readToString("testfile/orig.txt");
String s2=readToString.readToString("testfile/orig.txt");
double sum=getCos.getCos(s1,s2);
Assert.assertEquals(1.0,sum,0);
}
@org.junit.Test
public void completeDiff(){
String s1="乘风破浪会有时";
String s2="直挂云帆济沧海";
double sum=getCos.getCos(s1,s2);
Assert.assertEquals(0,sum,0);
}
@org.junit.Test
public void origAndAdd() {
String s1=readToString.readToString("testfile/orig.txt");
String s2=readToString.readToString("testfile/orig_0.8_add.txt");
double sum=getCos.getCos(s1,s2);
Assert.assertEquals(0.8,sum,0.2);
}
@org.junit.Test
public void origAndDel() {
String s1=readToString.readToString("testfile/orig.txt");
String s2=readToString.readToString("testfile/orig_0.8_del.txt");
double sum=getCos.getCos(s1,s2);
Assert.assertEquals(0.8,sum,0.2);
}
@org.junit.Test
public void origAndMix() {
String s1=readToString.readToString("testfile/orig.txt");
String s2=readToString.readToString("testfile/orig_0.8_mix.txt");
double sum=getCos.getCos(s1,s2);
Assert.assertEquals(0.8,sum,0.2);
}
后面的几个代码大同小异,就是改了个文件名,这里不放了。
- 测试结果
- 测试覆盖率
核心方法getCos覆盖率为100%,readIntoString没覆盖到的都是catch块。
4.计算模块部分异常处理说明
- 空文本异常
//测试
try{
throw new EmptyTextException("Empty Text!");
}catch(EmptyTextException e){
e.printStackTrace();
}
public class EmptyTextException extends Exception{
public EmptyTextException(){
super();
}
public EmptyTextException(String message){
super(message);
}
public EmptyTextException(String message,Throwable cause){
super(message,cause);
}
public EmptyTextException(Throwable cause){
super(cause);
}
}
- 测试结果
5.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 120 | 100 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 60 |
Development | 开发 | 420 | 1440 |
Analysis | · 需求分析 (包括学习新技术) | 60 | 360 |
· Design Spec | · 生成设计文档 | 60 | 120 |
· Design Review | · 设计复审 | 20 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 20 |
· Design | · 具体设计 | 60 | 45 |
· Coding | · 具体编码 | 180 | 300 |
· Code Review | · 代码复审 | 30 | 15 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 60 | 90 |
· Test Report | · 测试报告 | 40 | 30 |
· Size Measurement | · 计算工作量 | 10 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 45 |
· 合计 | 1230 | 2715 |
6.遇到的问题
-
问题1
创建测试的方法培训文档里讲的有点模糊,点击类名(我找了半天Solve在哪,原来是就是它的类名……),alt+enter就可以创建测试了。
-
问题2
照着预培训文档里面写,结果编译器报不能比较浮点数……? -
解决方法:应该用assertEquals(expected, actual, delta) 比较浮点数大小,只要期望值和声明值之间的差值小等于delta,那么就断言相等。Math.abs(expected - actual) <= delta
这样写就没问题了
-
问题3
导jar包是最费劲的……好几次导出来结果命令窗口中执行出错。
提示没有主清单解决方法:用WinRAR打开jar包,找到这个文件
打开添加Main-Class: main,注意冒号后有空格。
还有这个问题
至今没明白具体原因,搜索到的答案基本都是说路径配置出错,然而我编译器里就运行的好好的……这种情况一般是导出的时候没设置好,就猜+搜+搞,莫名其妙就行了。
7.总结
我变秃了,也变强了
- 第一次用jprofiler,第一次用git,第一次配置maven……学到了很多东西,主要是增强了查资料的能力……整个过程走下来写代码反而是最轻松的,学习新东西是最难的,单一个git的用法就研究了一下午。找到的教程总是走到一半就出问题,然后又去百度解决方法,怎么导jar包,怎么配置maven,怎么用git push,于是浏览器常常挂着好几个窗口。在过程中也意识到自己跟别人的差距,在别人眼里可能是常识的东西我得搞几小时,好在最后都顺利解决了。
- 编码过程中同样遇到了一些困难,因为Java语言还不太熟练,写代码的时候下意识的想用C的东西,有时候写到一半开始面向百度编程。比如存词向量的存储,开始写了个map<string,vector
>offset1,编译器直接报错,在编译器的提示下蒙出了正确写法Map<String, Vector > Offset1=new TreeMap<String,Vector >();
……害,积累的东西太少,现在开始吃亏了,好在这也是一个积累的过程,今后的学习和工作中总会派上用场吧。
“你的负担将变成礼物,你受的苦将照亮你的路。”