前言
余弦定理,这个在初中课本中就出现过的公式,恐怕没有人不知道的吧。但是另外一个概念,可能不是很多的人会听说过,他叫空间向量,一般用e表示,高中课本中有专门讲过这个东西,有了余弦定理和向量空间,我们就可以做许多有意思的事情了,利用余弦定理计算文本相似度的算法就是其中一个很典型的例子。当然这个话题太老,说的人太多,没有什么新意,恰巧周末阅读了吴军博士的<<数学之美>>这门书,书中讲到了利用余弦定理实现新闻分类,于是就索性完成这个算法的初步模型。感兴趣的可以继续往下看。
算法背景
在以往,如果对一则新闻进行归类,一般使用的都是人工分类的办法,大体上看一下标题和首尾两段文字,就能知道新闻是属于财经的,体育的又或者是健康类的。但是在当今信息爆炸的时代,这显然是不可能完成的任务,所以我们急切的相用机器自己帮我们”分类“。最好的形式是我给计算机提供大量的已分类好的数据,等强大的计算机大脑训练好了这个分类模型,后边的事情就是他来完成了。看起来这好像很高深,很困难的样子,但是其实我们自己也可以写一个,只是效果可能不会那么好。
分类器实现原理
新闻自动分类器实现的本质也是利用余弦定理比较文本的相似度,于是这个问题的难点就在于这个特征向量哪里来,怎么去获得。特征向量,特征向量,关键两个字在于特征,新闻的特征就在于他的关键词,我的简单理解就是专业性的词语,换句话说,就是属于某类新闻特有的词语,比如金融类的新闻,关键词一般就是股票啊,公司啊,上市啊等等词语。这些词的寻找可以通过统计词频的方式实现,最后统计出来的关键词,进行降序排列,一个关键词就代表一个新的维度。 那么新的问题又来了,我要统计词频,那么就得首先进行分词,要把每个新闻句子的主谓宾统统挖掘出来啊,好像这个工作比我整个算法还要复杂的样子。OK,其实已经有人已经帮我们把这个问题解决了,在这个算法中我使用的是中科大的ICTCLAS分词系统,效果非常棒,举个例子,下面是我原始的新闻内容:
- 教育部副部长:教育公平是社会公平重要基础
- 7月23日,教育部党组副书记、副部长杜玉波为全国学联全体代表作《教育综合改革与青年学生成长成才》的专题报告。中国青年网记者张炎良摄
- 人民网北京7月24日电(记者贺迎春实习生王斯慧
经过分词系统处理后的分词效果:
- 教育部/nt副/b部长/n:/wm教育/v公平/an是/vshi社会/n公平/a重要/a基础/n
- 7月/t23日/t,/wd教育部/nt党组/n副/b书记/n、/wn副/b部长/n杜玉波/nr为/p全国学联/nt全体/n代表作/n《/wkz教育/vn综合/vn改革/vn与/cc青年/n学生/n成长/vi成才/vi》/wky的/ude1专题/n报告/n。/wj中国/ns青年/n网/n记者/n张/q炎/ng良/d摄/vg
- 人民/n网/n北京/ns7月/t24日/t电/n(/wkz记者/n贺/vg迎春/n实习生/n王斯慧/nr)/wky昨日/t,/wd教育部/nt副/b部长
OK,有了这个分词的结果之后,后面的事情就水到渠成了。
算法的实现步骤
1、给定训练的新闻数据集。
2、通过分词系统统计词频的方式,统计词频最高的N位作为特征词,即特征向量
3、输入测试数据,同样统计词频,并于训练数据的进行商的操作,得到特征向量值
4、最后利用余弦定理计算相似度,并与最小阈值做比较。
算法的代码实现
ICTCLAS工具类ICTCLAS.java:
- packageNewsClassify;
- importjava.io.File;
- importjava.io.FileOutputStream;
- importjava.io.InputStream;
- importjava.util.StringTokenizer;
- publicclassICTCLAS50{
- static{
- try{
- Stringlibpath=System.getProperty("user.dir")+"\\lib";
- Stringpath=null;
- StringTokenizerst=newStringTokenizer(libpath,
- System.getProperty("path.separator"));
- if(st.hasMoreElements()){
- path=st.nextToken();
- }
- //copyalldllfilestojavalibpath
- FiledllFile=null;
- InputStreaminputStream=null;
- FileOutputStreamoutputStream=null;
- byte[]array=null;
- dllFile=newFile(newFile(path),"ICTCLAS50.dll");
- if(!dllFile.exists()){
- inputStream=ICTCLAS50.class.getResource("/lib/ICTCLAS50.dll")
- .openStream();
- outputStream=newFileOutputStream(dllFile);
- array=newbyte[1024];
- for(inti=inputStream.read(array);i!=-1;i=inputStream
- .read(array)){
- outputStream.write(array,0,i);
- }
- outputStream.close();
- }
- }catch(Exceptione){
- e.printStackTrace();
- }
- try{
- //loadJniCall.dll
- System.loadLibrary("ICTCLAS50");
- System.out.println("4444");
- }catch(Errore){
- e.printStackTrace();
- }
- }
- publicnativebooleanICTCLAS_Init(byte[]sPath);
- publicnativebooleanICTCLAS_Exit();
- publicnativeintICTCLAS_ImportUserDictFile(byte[]sPath,inteCodeType);
- publicnativeintICTCLAS_SaveTheUsrDic();
- publicnativeintICTCLAS_SetPOSmap(intnPOSmap);
- publicnativebooleanICTCLAS_FileProcess(byte[]sSrcFilename,
- inteCodeType,intbPOSTagged,byte[]sDestFilename);
- publicnativebyte[]ICTCLAS_ParagraphProcess(byte[]sSrc,inteCodeType,
- intbPOSTagged);
- publicnativebyte[]nativeProcAPara(byte[]sSrc,inteCodeType,
- intbPOStagged);
- }
- packageNewsClassify;
- /**
- *词语实体类
- *
- *@authorlyq
- *
- */
- publicclassWordimplementsComparable<Word>{
- //词语名称
- Stringname;
- //词频
- Integercount;
- publicWord(Stringname,Integercount){
- this.name=name;
- this.count=count;
- }
- @Override
- publicintcompareTo(Wordo){
- //TODOAuto-generatedmethodstub
- returno.count.compareTo(this.count);
- }
- }
- packageNewsClassify;
- importjava.io.BufferedReader;
- importjava.io.File;
- importjava.io.FileReader;
- importjava.io.IOException;
- importjava.util.ArrayList;
- importjava.util.Collections;
- /**
- *分类算法模型
- *
- *@authorlyq
- *
- */
- publicclassNewsClassifyTool{
- //余弦向量空间维数
- privateintvectorNum;
- //余弦相似度最小满足阈值
- privatedoubleminSupportValue;
- //当前训练数据的新闻类别
- privateStringnewsType;
- //训练新闻数据文件地址
- privateArrayList<String>trainDataPaths;
- publicNewsClassifyTool(ArrayList<String>trainDataPaths,StringnewsType,
- intvectorNum,doubleminSupportValue){
- this.trainDataPaths=trainDataPaths;
- this.newsType=newsType;
- this.vectorNum=vectorNum;
- this.minSupportValue=minSupportValue;
- }
- /**
- *从文件中读取数据
- */
- privateStringreadDataFile(StringfilePath){
- Filefile=newFile(filePath);
- StringBuilderstrBuilder=null;
- try{
- BufferedReaderin=newBufferedReader(newFileReader(file));
- Stringstr;
- strBuilder=newStringBuilder();
- while((str=in.readLine())!=null){
- strBuilder.append(str);
- }
- in.close();
- }catch(IOExceptione){
- e.getStackTrace();
- }
- returnstrBuilder.toString();
- }
- /**
- *计算测试数据的特征向量
- */
- privatedouble[]calCharacterVectors(StringfilePath){
- intindex;
- double[]vectorDimensions;
- double[]temp;
- Newsnews;
- NewstestNews;
- StringnewsCotent;
- StringtestContent;
- StringparseContent;
- //高频词汇
- ArrayList<Word>frequentWords;
- ArrayList<Word>wordList;
- testContent=readDataFile(filePath);
- testNews=newNews(testContent);
- parseNewsContent(filePath);
- index=filePath.indexOf('.');
- parseContent=readDataFile(filePath.substring(0,index)+"-split.txt");
- testNews.statWords(parseContent);
- vectorDimensions=newdouble[vectorNum];
- //计算训练数据集的类别的特征向量
- for(Stringpath:this.trainDataPaths){
- newsCotent=readDataFile(path);
- news=newNews(newsCotent);
- //进行分词操作
- index=path.indexOf('.');
- parseNewsContent(path);
- parseContent=readDataFile(path.substring(0,index)+"-split.txt");
- news.statWords(parseContent);
- wordList=news.wordDatas;
- //将词频统计结果降序排列
- Collections.sort(wordList);
- frequentWords=newArrayList<Word>();
- //截取出前vectorDimens的词语
- for(inti=0;i<vectorNum;i++){
- frequentWords.add(wordList.get(i));
- }
- temp=testNews.calVectorDimension(frequentWords);
- //将特征向量值进行累加
- for(inti=0;i<vectorDimensions.length;i++){
- vectorDimensions[i]+=temp[i];
- }
- }
- //最后取平均向量值作为最终的特征向量值
- for(inti=0;i<vectorDimensions.length;i++){
- vectorDimensions[i]/=trainDataPaths.size();
- }
- returnvectorDimensions;
- }
- /**
- *根据求得的向量空间计算余弦相似度值
- *
- *@paramvectorDimension
- *已求得的测试数据的特征向量值
- *@return
- */
- privatedoublecalCosValue(double[]vectorDimension){
- doubleresult;
- doublenum1;
- doublenum2;
- doubletemp1;
- doubletemp2;
- //标准的特征向量,每个维度上都为1
- double[]standardVector;
- standardVector=newdouble[vectorNum];
- for(inti=0;i<vectorNum;i++){
- standardVector[i]=1;
- }
- temp1=0;
- temp2=0;
- num1=0;
- for(inti=0;i<vectorNum;i++){
- //累加分子的值
- num1+=vectorDimension[i]*standardVector[i];
- //累加分母的值
- temp1+=vectorDimension[i]*vectorDimension[i];
- temp2+=standardVector[i]*standardVector[i];
- }
- num2=Math.sqrt(temp1)*Math.sqrt(temp2);
- //套用余弦定理公式进行计算
- result=num1/num2;
- returnresult;
- }
- /**
- *进行新闻分类
- *
- *@paramfilePath
- *测试新闻数据文件地址
- */
- publicvoidnewsClassify(StringfilePath){
- doubleresult;
- double[]vectorDimension;
- vectorDimension=calCharacterVectors(filePath);
- result=calCosValue(vectorDimension);
- //如果余弦相似度值满足最小阈值要求,则属于目标分类
- if(result>=minSupportValue){
- System.out.println(String.format("最终相似度结果为%s,大于阈值%s,所以此新闻属于%s类新闻",
- result,minSupportValue,newsType));
- }else{
- System.out.println(String.format("最终相似度结果为%s,小于阈值%s,所以此新闻不属于%s类新闻",
- result,minSupportValue,newsType));
- }
- }
- /**
- *利用分词系统进行新闻内容的分词
- *
- *@paramsrcPath
- *新闻文件路径
- */
- privatevoidparseNewsContent(StringsrcPath){
- //TODOAuto-generatedmethodstub
- intindex;
- StringdirApi;
- StringdesPath;
- dirApi=System.getProperty("user.dir")+"\\lib";
- //组装输出路径值
- index=srcPath.indexOf('.');
- desPath=srcPath.substring(0,index)+"-split.txt";
- try{
- ICTCLAS50testICTCLAS50=newICTCLAS50();
- //分词所需库的路径、初始化
- if(testICTCLAS50.ICTCLAS_Init(dirApi.getBytes("GB2312"))==false){
- System.out.println("InitFail!");
- return;
- }
- //将文件名string类型转为byte类型
- byte[]Inputfilenameb=srcPath.getBytes();
- //分词处理后输出文件名、将文件名string类型转为byte类型
- byte[]Outputfilenameb=desPath.getBytes();
- //文件分词(第一个参数为输入文件的名,第二个参数为文件编码类型,第三个参数为是否标记词性集1yes,0
- //no,第四个参数为输出文件名)
- testICTCLAS50.ICTCLAS_FileProcess(Inputfilenameb,0,1,
- Outputfilenameb);
- //退出分词器
- testICTCLAS50.ICTCLAS_Exit();
- }catch(Exceptionex){
- ex.printStackTrace();
- }
- }
- }
- packageNewsClassify;
- importjava.util.ArrayList;
- /**
- *新闻分类算法测试类
- *@authorlyq
- *
- */
- publicclassClient{
- publicstaticvoidmain(String[]args){
- StringtestFilePath1;
- StringtestFilePath2;
- StringtestFilePath3;
- Stringpath;
- StringnewsType;
- intvectorNum;
- doubleminSupportValue;
- ArrayList<String>trainDataPaths;
- NewsClassifyToolclassifyTool;
- //添加测试以及训练集数据文件路径
- testFilePath1="C:\\Users\\lyq\\Desktop\\icon\\test\\testNews1.txt";
- testFilePath2="C:\\Users\\lyq\\Desktop\\icon\\test\\testNews2.txt";
- testFilePath3="C:\\Users\\lyq\\Desktop\\icon\\test\\testNews3.txt";
- trainDataPaths=newArrayList<String>();
- path="C:\\Users\\lyq\\Desktop\\icon\\test\\trainNews1.txt";
- trainDataPaths.add(path);
- path="C:\\Users\\lyq\\Desktop\\icon\\test\\trainNews2.txt";
- trainDataPaths.add(path);
- newsType="金融";
- vectorNum=10;
- minSupportValue=0.45;
- classifyTool=newNewsClassifyTool(trainDataPaths,newsType,vectorNum,minSupportValue);
- classifyTool.newsClassify(testFilePath1);
- classifyTool.newsClassify(testFilePath2);
- classifyTool.newsClassify(testFilePath3);
- }
- }
- 最终相似度结果为0.39999999999999997,小于阈值0.45,所以此新闻不属于金融类新闻
- 最终相似度结果为0.4635393084189425,大于阈值0.45,所以此新闻属于金融类新闻
- 最终相似度结果为0.661835948543857,大于阈值0.45,所以此新闻属于金融类新闻