起始的类是 /mahout-mrlegacy/src/main/java/org/apache/mahout/vectorizer/SparseVectorsFromSequenceFiles.java。主要的流程在这里,这不是源码,我挑了重要的放这里了。
public final class SparseVectorsFromSequenceFiles extends AbstractJob{ public void run(){ //step1 对文章进行分词 DocumentProcessor.tokenizeDocuments(inputDir, analyzerClass, tokenizedPath, conf); //step2 创建向量 DictionaryVectorizer.createTermFrequencyVectors(tokenizedPath, outputDir, tfDirName, conf, minSupport, maxNGramSize, minLLRValue, -1.0f, false, reduceTasks, chunkSize, sequentialAccessOutput, namedVectors); //step 3是否需要剪枝。如果需要,建立单词的倒排频率索引,记录索引文件的路径 //这个判断是或 有点不明白 Pair<Long[], List<Path>> docFrequenciesFeatures = null; if (shouldPrune || processIdf) { docFrequenciesFeatures = TFIDFConverter.calculateDF(new Path(outputDir, tfDirName), outputDir, conf, chunkSize); } //step 4剪枝 if (processIdf) { HighDFWordsPruner.pruneVectors(tfDir, prunedTFDir, prunedPartialTFDir, maxDFThreshold, minDf, conf, docFrequenciesFeatures, -1.0f, false, reduceTasks); } //step 5最终生成向量文件 if (processIdf) { TFIDFConverter.processTfIdf( new Path(outputDir, DictionaryVectorizer.DOCUMENT_VECTOR_OUTPUT_FOLDER), outputDir, conf, docFrequenciesFeatures, minDf, maxDF, norm, logNormalize, sequentialAccessOutput, namedVectors, reduceTasks); } } }
分词的过程。这个是一个map only的作业。输入是 <文件路径,文件类容> 输出是<文件路径 ,分词后的一个单词> , 输出的时候是<Text,StringTuple>, 并且是序列化文件。生成在了我们的output/tokenized-documents下面。
这过程又分成很多步骤:
if (maxNGramSize == 1) { startWordCounting(input, dictionaryJobPath, baseConf, minSupport); dictionaryChunks = createDictionaryChunks(dictionaryJobPath, output, baseConf, chunkSizeInMegabytes, ……); } else { //如果我们设置了几个词连在一起作为一个特征,那么wordcount将会被分为两个工作 //第一个工作是遍历文件,把词语连接起来,变成特征,generateCollocations(……); //然后再统计computeNGramsPruneByLLR(……) 这两个工作写在了CollocDriver里面。 CollocDriver.generateAllGrams(input, dictionaryJobPath, baseConf, maxNGramSize, minSupport, minLLRValue, numReducers); dictionaryChunks = createDictionaryChunks(……) } //读dictionary的文件, for (Path dictionaryChunk : dictionaryChunks) { makePartialVectors(input, baseConf, maxNGramSize, dictionaryChunk, partialVectorOutputPath, maxTermDimension[0], sequentialAccess, namedVectors, numReducers); } //最后一步,合并 PartialVectorMerger.mergePartialVectors(partialVectorPaths, outputDir, conf, normPower, logNormalize,maxTermDimension[0], sequentialAccess, namedVectors, numReducers); HadoopUtil.delete(conf, partialVectorPaths);
wordcounting,就是统计每个单词出现的次数。在map里统计每篇文章的<单词,次数>, 在reduce里,对次数求和,支持度低于我们要的水平的单词也是在这里reduce时过滤掉的,支持度默认为2。这个过程会生成wordcount目录。createDictionaryChunks目的是遍历wordcounting产生的文件,给每一个单词或者是特征,分一个id,坑爹的地方就在这里,这个id就是简单的i++。这一步产生的文件是dictionary.file-index啥的文件,文件可能有多个,按大小分成了一块块的,然后方法返回了这些块的路径。同时统计了总共多少个单词,这在后面一步用到了。
makePartialVectors是个mapreduced任务。把dictionary.file-x放到distributedCache里,然后在reduce中把cache读成一个大map,然后新建了一个向量表示这个文本,向量的容量就是上一步算出来的单词总数。然后遍历单词,读到一个单词,就会去map里找单词的id,然后往向量里写。向量里的内容其实是:单词的id对应向量的下标,该下标上存的是单词的出现次数。这一步产生了partial-vectors-x这样的文件。这里不太懂为什么不在map里搞,非要到reduce里做,难道是要对文件排序?
最后一步合并了所有的文件,生成到tf-vectors目录里,如果要剪枝的就生成到tf-vectors-toprune里。我理解是这样的,makePartialVectors每次就处理一个dictionary.file-x,但如果这个file有多个,那么一次不可能把文本内所有词都变成{id:count}的形式,所以这里还要做合并。理解的不对欢迎大家拍砖。normalize也是在reduce的时候进行的。在向量生成之后:
if (logNormalize) { vector = vector.logNormalize(normPower); } else { vector = vector.normalize(normPower); }
step2结束之后就有了, path:{wordid1:count1,wordid2:count2}这样的键值对了,是一个序列化文件,名称是tf-vectors 或者tf-vectors-toprune目录下。 上面的步骤就已经计算出了词频(term-frequency),也就是单词在一篇文章中出现的次数。 接下来要开始统计文件频率(document frequency),也就是这个单词在多少的文件中出现的次数。
有两个比较重要的方法来进行document frequency统计,
startDFCounting(input, wordCountPath, baseConf); return createDictionaryChunks(wordCountPath, output, baseConf, chunkSizeInMegabytes);
startDFCounting。在map阶段 每个文件的输出是一堆<单词id(也就是下标),1> ,每次写完一个文件,就再输出<-1,1> 。reduce的时候把value数一下,就知道每个单词,在所有文档中出现了多少次,而把-1对应的value数一下,就知道有多少个文件。这样df就统计出来了。这个保存在df-count文件夹下。createDictionaryChunks主要是把df-count的文件切分成很多块,方便放到cache里,这个和dictionary-file类似,生成的是frequency.file-index这样的文件,然后会返回文件的路径集合。
剪枝的过程还真没看懂,也是在reduce方法里做的,不知道为什么不放map里。在我们的命令行里有参数
--maxDFSigma
这个参数决定了要不要去掉一些频率非常高的词,这个数字的意思是所有单词的df的标准差的倍数,默认是-1也就是不去掉任何词,官方推荐3.0。
// Calculate the standard deviation
double stdDev = BasicStats.stdDevForGivenMean(dfDir, stdCalcDir, 0.0, conf);
maxDF = (int) (100.0 * maxDFSigma * stdDev / vectorCount);
之后maxDf 和minDf(默认值是-1) 会被放到下面的job里,一个单词要是不在这之间,就要被踢出去。
剪枝的时候读入的是tf-vectors,也就是词频向量。和上面将单词或特征换成int类型的id的过程有点像。先把frequency.file-x放到cache里,在reduce里变成一个大map,然后对每条向量 在reduce里 去读map里有没有对应的单词,没有的话,把单词的出现次数变成0.0,如果有,但大于maxDf或者小于minDf,也变成0.0. 不知道为什么不直接删除这个词,也不知道为什么不去map里做这些事情。
//首先在外面计算了要求的最大的document Frequency。最小的是我们在命令里指定的,不指定就是1. long vectorCount = docFrequenciesFeatures.getFirst()[1]; if (maxDFSigma >= 0.0) { Path dfDir = new Path(outputDir, TFIDFConverter.WORDCOUNT_OUTPUT_FOLDER); Path stdCalcDir = new Path(outputDir, HighDFWordsPruner.STD_CALC_DIR); // Calculate the standard deviation double stdDev = BasicStats.stdDevForGivenMean(dfDir, stdCalcDir, 0.0, conf); maxDF = (int) (100.0 * maxDFSigma * stdDev / vectorCount); } long maxDFThreshold = (long) (vectorCount * (maxDF / 100.0f)); for (Path path : docFrequenciesFeatures.getSecond()) { Path partialVectorOutputPath = new Path(prunedPartialTFDir, "partial-" + partialVectorIndex++); partialVectorPaths.add(partialVectorOutputPath); pruneVectorsPartial(tfDir, partialVectorOutputPath, path, maxDF, minDF, baseConf); } mergePartialVectors(partialVectorPaths, prunedTFDir, baseConf, normPower, logNormalize, numReducers); HadoopUtil.delete(new Configuration(baseConf), prunedPartialTFDir);
剪完枝就到了最后一步。创建逆向文件频率,这个过程还是,先partial,再merge. 不过看到代码里有和剪枝一样的代码,不知道是不是写重复了。这个就比较简单了,读每个term-frequency文件,把df 的dictionary放到cache里,然后,读cache,算idf.最终生成的文件就是
key:文件路径 value{word1:tfidf1,word2:tfidf2……}。
为了模拟mahout坑爹的地方,我先打开终端export MAHOUT_LOCAL=true. 在我的主目录下有两个文件夹,是一摸一样的,文件夹下有两个子文件夹,每个子文件夹中有两篇文本。然后我将第二个文件夹下的文本中随便填一些没用的单词。之后对这两个文件夹分别:
mahout seqdirectory mahout seq2sparse mahout seqdumper -i outdir/dictionary.file-0 -o dict1 mahout seqdumper -i outdir/dictionary.file-0 -o dict2
把两次向量化之后的dictionary.file-0导出来. dict1 和 dict2 中同一个单词,id是不一样的。
这就告诉我们,要用Mhout训练出来的分类器,那你的文本一定要和训练分类器的时候的训练集一起向量化。否则他们用的不是同一个dictionary.就算用了同一个dictionary,那在形成tfidf的时候还要参考df文件。简单的说就是mahout提供了训练和测试分类器的方法,但没办法让你自己调用分类器,让你一篇篇来分类。要做到这一点,我们要自己写程序,去读dictionary.file和frequency.file ,然后自己算。这导致了我上篇博文扯着蛋了,不得不作废。