mahout 0.9 seq2sparse 流程和源码分析

起始的类是 /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);
        }
    }
}

step1 .

分词的过程。这个是一个map only的作业。输入是 <文件路径,文件类容> 输出是<文件路径 ,分词后的一个单词>  , 输出的时候是<Text,StringTuple>, 并且是序列化文件。生成在了我们的output/tokenized-documents下面。

step2 .

这过程又分成很多步骤:

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),也就是这个单词在多少的文件中出现的次数。

step3 .

有两个比较重要的方法来进行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这样的文件,然后会返回文件的路径集合。

step4

剪枝的过程还真没看懂,也是在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);

step5。

剪完枝就到了最后一步。创建逆向文件频率,这个过程还是,先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是不一样的。

mahout 0.9 seq2sparse 流程和源码分析

mahout 0.9 seq2sparse 流程和源码分析

这就告诉我们,要用Mhout训练出来的分类器,那你的文本一定要和训练分类器的时候的训练集一起向量化。否则他们用的不是同一个dictionary.就算用了同一个dictionary,那在形成tfidf的时候还要参考df文件。简单的说就是mahout提供了训练和测试分类器的方法,但没办法让你自己调用分类器,让你一篇篇来分类。要做到这一点,我们要自己写程序,去读dictionary.file和frequency.file ,然后自己算。这导致了我上篇博文扯着蛋了,不得不作废。

你可能感兴趣的:(mahout 0.9 seq2sparse 流程和源码分析)