Alink's Talk (6): Реализация алгоритма TF-IDF

машинное обучение

0x00 сводка

Alink — это платформа алгоритмов машинного обучения нового поколения, разработанная Alibaba на основе вычислительного движка реального времени Flink, и первая в отрасли платформа машинного обучения, которая поддерживает как пакетные, так и потоковые алгоритмы. TF-IDF (термин частота — обратная частота документа) — широко используемый метод взвешивания для поиска информации и интеллектуального анализа данных. Эта статья покажет вам, как Alink реализует TF-IDF.

0x01 TF-IDF

TF-IDF (термин частота — обратная частота документа) — это статистический метод, распространенный метод взвешивания, используемый при поиске информации и анализе данных.

TF — частота терминов, а IDF — частота обратных документов.

Зачем использовать TF-IDF?Поскольку компьютер может распознавать только числа, компьютер не может понимать слова одно за другим., не говоря уже о предложении или статье.TF-IDF используется для преобразования текста в язык, понятный компьютерам, или в набор данных, который могут изучать и обучать модели машинного обучения или глубокого обучения..

1.1 Принцип

TF-IDF используется для оценки важности слова для набора документов или одного из документов в корпусе. Важность слова увеличивается пропорционально количеству раз, которое оно встречается в документе, но уменьшается обратно пропорционально частоте его появления в корпусе.

Основная идея TF-IDF заключается в следующем: если слово или фраза часто встречается в TF в одной статье и редко появляется в других статьях, считается, что слово или фраза имеет хорошую способность различать категории и подходит для использования в классифицировать.

TF-IDF на самом деле: TF*IDF, частота термина TF, обратная частота документа IDF.

частота слов(частота терминов, TF) относится к частоте, с которой данный термин появляется в документе. этот номер правильныйколичество слов(количество терминов) для предотвращения предпочтения длинных файлов (один и тот же термин может иметь большее количество терминов в длинном файле, чем в коротком файле, независимо от того, важен термин или нет).

в то время как **IDF обратная частота документа (IDF)реагирует на слово вчастота вхождений во всем тексте (всем документе)**,Если слово встречается в большом количестве текста, его значение IDF должно быть низким. И наоборот, если слово появляется в меньшем количестве текста, его значение IDF должно быть высоким..比如一些专业的名词如“Machine Learning”。这样的词IDF值应该高。一个极端的情况,如果一个词在所有的文本中都出现,那么它的IDF值应该为0。

Если для вычисления важности слова используются только TF или IDF, это односторонне, поэтому TF-IDF сочетает в себе преимущества TF и ​​IDF для оценки.одно словоВажность документа в наборе документов или корпусе. Важность слова увеличивается пропорционально количеству раз, которое оно встречается в документе, но уменьшается обратно пропорционально частоте его появления в корпусе. Приведенная выше цитата резюмирует:Чем больше раз слово появляется в статье и чем меньше раз оно появляется во всех документах, тем лучше оно может представлять статью и тем лучше ее можно отличить от других статей.

1.2 Метод расчета

Формула расчета ТФ выглядит следующим образом:

где N_w — количество раз, когда термин w появляется в тексте, а N — общее количество терминов в тексте.

Формула расчета IDF выглядит следующим образом:

Где Y — общее количество документов в корпусе, Y_w — количество документов, содержащих термин w, а знаменатель увеличивается на единицу, чтобы избежать ситуации, когда w не появляется ни в одном документе, а знаменатель равен 0.

TF-IDF умножает TF и ​​IDF:

Из приведенной выше формулы расчета видно, что высокая частота слов в конкретном документе и низкая частота слова в документе во всем наборе документов могут генерировать TF-IDF с высоким весом. следовательно,TF-IDF имеет тенденцию отфильтровывать общие слова и оставлять важные слова.

0x02 Образец кода Alink

2.1 Пример кода

Сначала мы даем пример кода, далее нужно обучить модель через некоторый корпус, а затем использовать эту модель для прогнозирования:

public class DocCountVectorizerExample {

    AlgoOperator getData(boolean isBatch) {
        Row[] rows = new Row[]{
                Row.of(0, "二手旧书:医学电磁成像"),
                Row.of(1, "二手美国文学选读( 下册 )李宜燮南开大学出版社 9787310003969"),
                Row.of(2, "二手正版图解象棋入门/谢恩思主编/华龄出版社"),
                Row.of(3, "二手中国糖尿病文献索引"),
                Row.of(4, "二手郁达夫文集( 国内版 )全十二册馆藏书")
        };

        String[] schema = new String[]{"id", "text"};

        if (isBatch) {
            return new MemSourceBatchOp(rows, schema);
        } else {
            return new MemSourceStreamOp(rows, schema);
        }
    }

    public static void main(String[] args) throws Exception {
        DocCountVectorizerExample test = new DocCountVectorizerExample();
        BatchOperator batchData = (BatchOperator) test.getData(true);

         // 分词
        SegmentBatchOp segment = new SegmentBatchOp() 
                                                .setSelectedCol("text")
                                                .linkFrom(batchData);
        // TF-IDF训练
        DocCountVectorizerTrainBatchOp model = new DocCountVectorizerTrainBatchOp()
                                                .setSelectedCol("text")
                                                .linkFrom(segment);
        // TF-IDF预测
        DocCountVectorizerPredictBatchOp predictBatch = new 
                              DocCountVectorizerPredictBatchOp()
                                                .setSelectedCol("text")
                                                .linkFrom(model, segment);
        model.print();
        predictBatch.print();
    }
}

2.2 Модель TF-IDF

Модель TF-IDF распечатывается следующим образом:

model_id|model_info
--------|----------
0|{"minTF":"1.0","featureType":"\"WORD_COUNT\""}
1048576|{"f0":"二手","f1":0.0,"f2":0}
2097152|{"f0":"/","f1":1.0986122886681098,"f2":1}
3145728|{"f0":"出版社","f1":0.6931471805599453,"f2":2}
4194304|{"f0":")","f1":0.6931471805599453,"f2":3}
5242880|{"f0":"(","f1":0.6931471805599453,"f2":4}
6291456|{"f0":"入门","f1":1.0986122886681098,"f2":5}
......
36700160|{"f0":"美国","f1":1.0986122886681098,"f2":34}
37748736|{"f0":"谢恩","f1":1.0986122886681098,"f2":35}
38797312|{"f0":"象棋","f1":1.0986122886681098,"f2":36}

2.3 Прогнозирование TF-IDF

Результаты прогнозирования TF-IDF следующие:

id|text
--|----
0|$37$0:1.0 6:1.0 10:1.0 25:1.0 26:1.0 28:1.0
1|$37$0:1.0 1:1.0 2:1.0 4:1.0 11:1.0 15:1.0 16:1.0 19:1.0 20:1.0 32:1.0 34:1.0
2|$37$0:1.0 3:2.0 4:1.0 5:1.0 8:1.0 22:1.0 23:1.0 24:1.0 29:1.0 35:1.0 36:1.0
3|$37$0:1.0 12:1.0 27:1.0 31:1.0 33:1.0
4|$37$0:1.0 1:1.0 2:1.0 7:1.0 9:1.0 13:1.0 14:1.0 17:1.0 18:1.0 21:1.0 30:1.0

0x03 Сегмент сегментации

Сегментация китайских слов относится к разделению последовательности китайских иероглифов на отдельные слова. Сегментация слов — это процесс рекомбинации последовательных последовательностей слов в последовательности слов в соответствии с определенными спецификациями.

В примере кода часть сегментации слов выглядит следующим образом:

    SegmentBatchOp segment = new SegmentBatchOp() 
                                            .setSelectedCol("text")
                                            .linkFrom(batchData);

Сегментация слов в основном делится на следующие две категории, и ее функция состоит в том, чтобы разделить китайский документ на слова.

public final class SegmentBatchOp extends MapBatchOp <SegmentBatchOp>
 implements SegmentParams <SegmentBatchOp> {

 public SegmentBatchOp(Params params) {
  super(SegmentMapper::new, params);
 }
}

public class SegmentMapper extends SISOMapper {
 private JiebaSegmenter segmentor;
}

3.1 Причастия заикания

Опытные ученики улыбнутся, увидев это: заикающиеся причастия.

Сегментация слов Jieba является наиболее используемым инструментом сегментации китайских слов в Китае.GitHub.com/flighttimeabout/streetfighter…

  • Точный режим, который пытается наиболее точно сократить предложение, подходящий для анализа текста;
  • Полный режим, сканирует все слова, которые могут быть сформированы в слова в предложении, скорость очень высокая, но неоднозначность не может быть устранена;
  • Режим поисковой системы на основе точного режима снова разделяет длинные слова, чтобы улучшить скорость отзыва, что подходит для сегментации слов в поисковых системах.
  • Режим лопасти использует платформу глубокого обучения PaddlePaddle для обучения сетевой модели маркировки последовательностей (двунаправленный GRU) для достижения сегментации слов.

Алинк используетсяcom.alibaba.alink.operator.common.nlp.jiebasegment.viterbi.FinalSeg;чтобы закончить причастие. В частности, вGitHub.com/лепесток/белый…

public class JiebaSegmenter implements Serializable {
    private static FinalSeg finalSeg = FinalSeg.getInstance();
    private WordDictionary wordDict;
    ......
    private Map<Integer, List<Integer>> createDAG(String sentence) 
}

Из кода Alink реализованы два режима сегментации индекса и сегментации запросов, которые следует разделить на гранулярность сегментации слов.

Роль функции createDAG заключается в следующем: в процессе обработки предложений на основе словаря префиксов для достижения эффективного сканирования графа слов и создания направленного ациклического графа (DAG), состоящего из всех возможных словообразований китайских иероглифов в предложении.

Сегментация заикающихся слов Для незарегистрированных слов используется модель НММ, основанная на способности китайских иероглифов образовывать слова, и применяется алгоритм Витерби.

3.2 Процесс сегментации слов

Процесс сегментации слов в основном выполняется в функции SegmentMapper.mapColumn.Когда входными данными является «Подержанная книга: Медицинская электромагнитная визуализация», сегментация заикающихся слов делит предложение на шесть слов. Подробнее см. ниже:

input = "二手旧书:医学电磁成像"
tokens = {ArrayList@9619}  size = 6
 0 = {SegToken@9630} "[二手, 0, 2]"
 1 = {SegToken@9631} "[旧书, 2, 4]"
 2 = {SegToken@9632} "[:, 4, 5]"
 3 = {SegToken@9633} "[医学, 5, 7]"
 4 = {SegToken@9634} "[电磁, 7, 9]"
 5 = {SegToken@9635} "[成像, 9, 11]"

mapColumn:44, SegmentMapper (com.alibaba.alink.operator.common.nlp)
apply:-1, 35206803 (com.alibaba.alink.common.mapper.SISOMapper$Lambda$646)
handleMap:75, SISOColsHelper (com.alibaba.alink.common.mapper)
map:52, SISOMapper (com.alibaba.alink.common.mapper)
map:21, MapperAdapter (com.alibaba.alink.common.mapper)
map:11, MapperAdapter (com.alibaba.alink.common.mapper)
collect:79, ChainedMapDriver (org.apache.flink.runtime.operators.chaining)
collect:35, CountingCollector (org.apache.flink.runtime.operators.util.metrics)
invoke:196, DataSourceTask (org.apache.flink.runtime.operators)

0x04 обучение

Обучение проводится в классе DocCountVectorizerTrainBatchOp, который завершает построение модели через linkFrom. На самом деле расчет TF IDF относительно прост, а сложность заключается в последующей масштабной сортировке.

public DocCountVectorizerTrainBatchOp linkFrom(BatchOperator<?>... inputs) {
        BatchOperator<?> in = checkAndGetFirst(inputs);

        DataSet<DocCountVectorizerModelData> resDocCountModel = generateDocCountModel(getParams(), in);

        DataSet<Row> res = resDocCountModel.mapPartition(new MapPartitionFunction<DocCountVectorizerModelData, Row>() {
            @Override
            public void mapPartition(Iterable<DocCountVectorizerModelData> modelDataList, Collector<Row> collector) {
                new DocCountVectorizerModelDataConverter().save(modelDataList.iterator().next(), collector);
            }
        });
        this.setOutput(res, new DocCountVectorizerModelDataConverter().getModelSchema());
        return this;
}

4.1 Расчет ИДФ

Работа по вычислению IDF выполняется в generateDocCountModel Конкретные шаги следующие:

Первый шаг получается за счет смешанного использования DocWordSplitCount и UDTF.Количество слов в документе docWordCnt.

BatchOperator<?> docWordCnt = in.udtf(
        params.get(SELECTED_COL),
        new String[] {WORD_COL_NAME, DOC_WORD_COUNT_COL_NAME},
        new DocWordSplitCount(NLPConstant.WORD_DELIMITER),
        new String[] {});

DocWordSplitCount.evalВходные данные представляют собой предложение, которое было сегментировано, затем сегментировано пробелами и подсчитано по словам. результат:

map = {HashMap@9816}  size = 6
 "医学" -> {Long@9833} 1
 "电磁" -> {Long@9833} 1
 ":" -> {Long@9833} 1
 "成像" -> {Long@9833} 1
 "旧书" -> {Long@9833} 1
 "二手" -> {Long@9833} 1

Второй шаг получилКоличество документов docCnt

BatchOperator docCnt = in.select("COUNT(1) AS " + DOC_COUNT_COL_NAME);

Этот номер будет транслироваться.withBroadcastSet(docCnt.getDataSet(), "docCnt");,Следующий CalcIdf будет по-прежнему использоваться для подсчета количества строк.

Третий шаг будет передавать CalcIdfВычислите DF и IDF каждого слова.

docCnt будет получен при открытии. Затем сокращение рассчитает IDF, конкретный расчет выглядит следующим образом:

double idf = Math.log((1.0 + docCnt) / (1.0 + df));
collector.collect(Row.of(featureName, -wordCount, idf));

В частности, получено следующим образом

df = 1.0
wordCount = 1.0
featureName = "中国"
idf = 1.0986122886681098
docCnt = 5

Важный момент: возвращаемое значение равно -wordCount, потому что чем больше слов, тем меньше вес, поэтому для сравнения он отрицательный.

4.2 Сортировка

После получения ИДФ всех слов получается словарь ИДФ, в это время словарь нужно отсортировать по весу. Сортировка делится на два этапа.

4.2.1 SortUtils.pSort

Первый шаг — это SortUtils.pSort, массивно-параллельная выборочная сортировка.

Tuple2<DataSet<Tuple2<Integer, Row>>, DataSet<Tuple2<Integer, Long>>> partitioned = SortUtils.pSort(sortInput, 1);

Этот шаг очень сложен.Алинк ссылается на статью.Если вам интересно, вы можете узнать об этом подробнее.

* reference: Yang, X. (2014). Chong gou da shu ju tong ji (1st ed., pp. 25-29).
* Note: This algorithm is improved on the base of the parallel sorting by regular sampling(PSRS).

Возвращаемое значение pSort:

* @return f0: dataset which is indexed by partition id, f1: dataset which has partition id and count.

pSort разделен на следующие шаги

SampleSampleSplitPoint

SortUtils.SampleSplitPoint.mapPartition завершает здесь выборку.

DataSet <Tuple2 <Object, Integer>> splitPoints = input
   .mapPartition(new SampleSplitPoint(index))
   .reduceGroup(new SplitPointReducer());

Входная строка здесь является возвращаемым значением IDF выше.

Используйте allValues, чтобы записать, сколько слов в предложении, которое в данный момент обрабатывается этой задачей.

Выборка осуществляется с помощью точек разделения. Как его выбрать, через функцию genSampleIndex.

public static Long genSampleIndex(Long splitPointIdx, Long count, Long splitPointSize) {
   splitPointIdx++;
   splitPointSize++;

   Long div = count / splitPointSize;
   Long mod = count % splitPointSize;

   return div * splitPointIdx + ((mod > splitPointIdx) ? splitPointIdx : mod) - 1;
}

Последующие операции также используют для выбора ту же функцию genSampleIndex, которая гарантирует, что одна и та же точка выборки может быть выбрана во всех последовательностях.

allValues = {ArrayList@10264}  size = 8  //本task有多少单词
 0 = {Double@10266} -2.0
 1 = {Double@10271} -1.0
 2 = {Double@10272} -1.0
 3 = {Double@10273} -1.0
 4 = {Double@10274} -1.0
 5 = {Double@10275} -1.0
 6 = {Double@10276} -1.0
 7 = {Double@10277} -1.0

splitPoints = {ArrayList@10265}  size = 7 //采样了7个
 0 = {Double@10266} -2.0
 1 = {Double@10271} -1.0
 2 = {Double@10272} -1.0
 3 = {Double@10273} -1.0
 4 = {Double@10274} -1.0
 5 = {Double@10275} -1.0
 6 = {Double@10276} -1.0

Наконец, выборочные данные возвращаются, и при возврате прикрепляется текущий идентификатор задачи.new Tuple2 <Object, Integer>(obj,taskId).

Вот уловка

  for (Object obj : splitPoints) {
     Tuple2 <Object, Integer> cur
        = new Tuple2 <Object, Integer>(
        obj,
        taskId); //这里返回的是类似 (-5.0,2) :其中2就是task id,-5.0是-wordcount。
     out.collect(cur);
  }

  out.collect(new Tuple2(
     getRuntimeContext().getNumberOfParallelSubtasks(),
     -taskId - 1));//这里返回的是一个特殊元素,类似(4,-2) :其中4是本应用中并行task数目,-2是当前-taskId - 1。这个task数目后续就会用到。

См. также следующие конкретные данные:

row = {Row@10211} "中国,-1.0,1.0986122886681098"
 fields = {Object[3]@10214} 

cur = {Tuple2@10286} "(-5.0,2)" // 返回采样数据,返回时候附带当前taskID
 f0 = {Double@10285} -5.0 // -wordcount。
 f1 = {Integer@10300} 2 // 当前taskID
Объединить SplitPointReducer

Объедините образцы, сгенерированные всеми задачами. Затем повторите выборку и сформируйте данные выборки в блок данных.Принцип выбора этого блока данных таков: постарайтесь выбрать как можно больше выборок для каждой задачи.

На самом деле здесь происходит преобразование, то есть от выборки нормальных слов к выборке определенного типа слов, значение этого типа например: количество вхождений равно одному, или количество вхождений равно пяти таким слова.

Здесь все — все выборочные данные, пример содержимого одного элемента (-5.0,2): где 2 — это id задачи, а -5.0 — это -wordcount.

использовать здесьCollections.sort(all, new PairComparator());для сортировки всех выборочных данных. Основа сортировки — сначала по количеству слов, а затем по идентификатору задачи.

Выборочные значения, возвращаемые SplitPointReducer, сохраняются как широковещательные переменные:.withBroadcastSet(splitPoints, "splitPoints");

Хитрость вот в чем:

for (Tuple2 <Object, Integer> value : values) {
   if (value.f1 < 0) { 
      instanceCount = (int) value.f0;  // 特殊数据,类似(4,-2) :其中4是本应用中task数目,这个就是后续选择哪些taskid的基准
      continue;
   }
   all.add(new Tuple2 <>(value.f0, value.f1)); // (-5.0,2) 正常数据
}

выберите образец индексаsplitPoints.add(allValues.get(index));Также используется тот же genSampleIndex.

Конкретные данные в расчете следующие:

for (int i = 0; i < splitPointSize; ++i) {
  int index = genSampleIndex(
     Long.valueOf(i),
     Long.valueOf(count),
     Long.valueOf(splitPointSize))
     .intValue();
  spliters.add(all.get(index));
}
for (Tuple2 <Object, Integer> spliter : spliters) {
  out.collect(spliter);
}

count = 33
all = {ArrayList@10245}  size = 33 // 所有采样数据,
0 = {Tuple2@10256} "(-5.0,2)"// 2就是task id,-5.0是-wordcount。
1 = {Tuple2@10285} "(-2.0,0)"
......
6 = {Tuple2@10239} "(-1.0,0)"
7 = {Tuple2@10240} "(-1.0,0)"
8 = {Tuple2@10241} "(-1.0,0)"
9 = {Tuple2@10242} "(-1.0,0)"
10 = {Tuple2@10243} "(-1.0,0)"
11 = {Tuple2@10244} "(-1.0,1)"
......
16 = {Tuple2@10278} "(-1.0,1)"
......
24 = {Tuple2@10279} "(-1.0,2)"
......
32 = {Tuple2@10313} "(-1.0,3)"

// spliters是返回结果,这里分别选取了all中index为8,16,24这个三个record。每个task都选择了一个元素。
spliters = {HashSet@10246}  size = 3
 0 = {Tuple2@10249} "(-1.0,0)" // task 0 被选择。就是说,这里从task 0中选择了一个count是1的元素,具体选择哪个单词其实不重要,就是为了选择count是1的这种即可。
 1 = {Tuple2@10250} "(-1.0,1)" // task 1 被选择。具体同上。
 2 = {Tuple2@10251} "(-1.0,2)" // task 2 被选择。具体同上。
SplitData вставляет реальные данные IDF

используйте бинарный поиск для разделения данных на отсортированные подмножества. Предыдущая функция дает количество слов, но не IDF. Здесь мы будем использовать бинарный поиск, чтобы найти IDF, а затем вставить IDF в данные раздела.

Первое, что нужно отметить: ввод splitData — это исходный ввод ввода, который совпадает с вводом splitPoints.

DataSet <Tuple2 <Integer, Row>> splitData = input
   .mapPartition(new SplitData(index))
   .withBroadcastSet(splitPoints, "splitPoints");

Широковещательная переменная splitPoints будет вынесена в функцию открытия.

splitPoints = {ArrayList@10248}  size = 3
 0 = {Tuple2@10257} "(-1.0,0)"
 1 = {Tuple2@10258} "(-1.0,1)"
 2 = {Tuple2@10259} "(-1.0,2)"

Пример ввода этой функции

row = {Row@10232} "入门,-1.0,1.0986122886681098"

Двоичный поиск будет выполняться в точках разбиения, чтобы получить реальный IDF, соответствующий каждому образцу в разбиениях. Тогда отправьте его.

Здесь требуется специальное объяснение.Этот двоичный поиск ищет значение IDF.Например,значение IDFсоответствует слову, счет которого равен 1, многие слова могут иметь количество 1, поэтому вы можете найти IDF такого слова ..

splitPoints = {ArrayList@10223}  size = 3
 0 = {Tuple2@10229} "(-1.0,0)"
 1 = {Tuple2@10230} "(-1.0,1)"
 2 = {Tuple2@10231} "(-1.0,2)"
curTuple.f0 = {Double@10224} -1.0

int bsIndex = Collections.binarySearch(splitPoints, curTuple, new PairComparator());

  int curIndex;
  if (bsIndex >= 0) {
   curIndex = bsIndex;
  } else {
   curIndex = -bsIndex - 1;
  }

// 假设单词是 "入门",则发送的是 "入门" 这类单词在本partition的index,和 "入门" 的单词本身
// 其实,从调试过程看,是否发送单词信息本身并不重要,因为接下来的那一步操作中,并没有用到单词本身信息
out.collect(new Tuple2 <>(curIndex, row)); 
reduceGroup подсчитывает количество слов одного типа

Здесь указано количество слов определенного вида в разделе. Например, при счете 1 сколько всего слов?.

последуетnew Tuple2 <>(id, count)Сохранить как широковещательную переменную partitionCnt.

id — это индекс этого типа слова в середине раздела, мы временно называем его индексом раздела. count — количество таких слов в этом разделе.

// 输入举例
value = {Tuple2@10312} "(0,入门,-1.0,1.0986122886681098)"
 f0 = {Integer@10313} 0

// 计算数目
for (Tuple2 <Integer, Row> value : values) {
  id = value.f0;
  count++;
}

out.collect(new Tuple2 <>(id, count));  

// 输出举例,假如是序号为0的这类单词,其总体数目是12。这个序号0就是这类单词在某一partition中的序号。就是上面的 curIndex。
id = {Integer@10313} 0
count = {Long@10338} 12

4.2.2 localSort

Второй шаг — локальная сортировка. Отсортируйте секционированный набор данных. Окончательная сортировка вернет окончательное значение, например (29, "Главный редактор, -1.0,1.0986122886681098"), 29 — порядковый номер слова "Главный редактор" в словарь ИДФ.

DataSet<Tuple2<Long, Row>> ordered = localSort(partitioned.f0, partitioned.f1, 1);

PartitionCnt будет получен в функции open. Затем подсчитайте количество слов определенного типа во всех разделах до этого раздела.

public void open(Configuration parameters) throws Exception {
  List <Tuple2 <Integer, Long>> bc = getRuntimeContext().getBroadcastVariable("partitionCnt");
  startIdx = 0L;
  int taskId = getRuntimeContext().getIndexOfThisSubtask();
  for (Tuple2 <Integer, Long> pcnt : bc) {
   if (pcnt.f0 < taskId) {
     startIdx += pcnt.f1;
   }
  }
}

bc = {ArrayList@10303}  size = 4
 0 = {Tuple2@10309} "(0,12)"  // 就是task0里面,这种单词有12个
 1 = {Tuple2@10310} "(2,9)"// 就是task1里面,这种单词有2个
 2 = {Tuple2@10311} "(1,7)"// 就是task2里面,这种单词有1个
 3 = {Tuple2@10312} "(3,9)"// 就是task3里面,这种单词有3个
// 如果本task id是4,则其startIdx为30。就是所有partition之中,它前面index所有单词的和。  

Потом сортировать.Collections.sort(valuesList, new RowComparator(field));

valuesList = {ArrayList@10405}  size = 9
 0 = {Row@10421} ":,-1.0,1.0986122886681098"
 1 = {Row@10422} "主编,-1.0,1.0986122886681098"
 2 = {Row@10423} "国内,-1.0,1.0986122886681098"
 3 = {Row@10424} "文献,-1.0,1.0986122886681098"
 4 = {Row@10425} "李宜燮,-1.0,1.0986122886681098"
 5 = {Row@10426} "糖尿病,-1.0,1.0986122886681098"
 6 = {Row@10427} "美国,-1.0,1.0986122886681098"
 7 = {Row@10428} "谢恩,-1.0,1.0986122886681098"
 8 = {Row@10429} "象棋,-1.0,1.0986122886681098"

// 最后返回时候,就是  (29, "主编,-1.0,1.0986122886681098"),29就是“主编”这个单词在最终字典中的序号。
// 这个序号是startIdx + cnt,startIdx是某一种类单词,其在本partition之前所有partition中,这类单词数目。比如在本partition之前,这类单词有28个,则本partition中,从29开始计数。就是最终序列号
 for (Row row : valuesList) {
  out.collect(Tuple2.of(startIdx + cnt, row));
  cnt++; // 这里就是在某一类单词中,单调递增,然后赋值一个字典序列而已
 }  
cnt = 1
row = {Row@10336} "主编,-1.0,1.0986122886681098"
 fields = {Object[3]@10339} 
startIdx = 28

4.3 Фильтрация

Наконец, выполняется фильтрация: если количество символов превышает размер словаря, лишние символы отбрасываются.

ordered.filter(new FilterFunction<Tuple2<Long, Row>>() {
    @Override
    public boolean filter(Tuple2<Long, Row> value) {
        return value.f0 < vocabSize;
    }
})

0x05 Создать модель

Конкретный сгенерированный код модели выглядит следующим образом.

DataSet<DocCountVectorizerModelData> resDocCountModel = ordered.filter(new FilterFunction<Tuple2<Long, Row>>() {
    @Override
    public boolean filter(Tuple2<Long, Row> value) {
        return value.f0 < vocabSize;
    }
}).mapPartition(new BuildDocCountModel(params)).setParallelism(1);
return resDocCountModel;

Ключевыми классами являются доцентвекторермодельдата и BuildDoccoutModel.

5.1 DocCountVectorizerModelData

Это векторная информация.

/**
 * Save the data for DocHashIDFVectorizer.
 *
 * Save a HashMap: index(MurMurHash3 value of the word), value(Inverse document frequency of the word).
 */
public class DocCountVectorizerModelData {
    public List<String> list;
    public String featureType;
    public double minTF;
}

5.2 BuildDocCountModel

Окончательная сгенерированная информация о модели выглядит следующим образом: это результат, полученный в предыдущем примере кода.

modelData = {DocCountVectorizerModelData@10411} 
 list = {ArrayList@10409}  size = 37
  0 = "{"f0":"9787310003969","f1":1.0986122886681098,"f2":19}"
  1 = "{"f0":"下册","f1":1.0986122886681098,"f2":20}"
  2 = "{"f0":"全","f1":1.0986122886681098,"f2":21}"
  3 = "{"f0":"华龄","f1":1.0986122886681098,"f2":22}"
  4 = "{"f0":"图解","f1":1.0986122886681098,"f2":23}"
  5 = "{"f0":"思","f1":1.0986122886681098,"f2":24}"
  6 = "{"f0":"成像","f1":1.0986122886681098,"f2":25}"
  7 = "{"f0":"旧书","f1":1.0986122886681098,"f2":26}"
  8 = "{"f0":"索引","f1":1.0986122886681098,"f2":27}"
  9 = "{"f0":":","f1":1.0986122886681098,"f2":28}"
  10 = "{"f0":"主编","f1":1.0986122886681098,"f2":29}"
  11 = "{"f0":"国内","f1":1.0986122886681098,"f2":30}"
  12 = "{"f0":"文献","f1":1.0986122886681098,"f2":31}"
  13 = "{"f0":"李宜燮","f1":1.0986122886681098,"f2":32}"
  14 = "{"f0":"糖尿病","f1":1.0986122886681098,"f2":33}"
  15 = "{"f0":"美国","f1":1.0986122886681098,"f2":34}"
  16 = "{"f0":"谢恩","f1":1.0986122886681098,"f2":35}"
  17 = "{"f0":"象棋","f1":1.0986122886681098,"f2":36}"
  18 = "{"f0":"二手","f1":0.0,"f2":0}"
  19 = "{"f0":")","f1":0.6931471805599453,"f2":1}"
  20 = "{"f0":"/","f1":1.0986122886681098,"f2":2}"
  21 = "{"f0":"出版社","f1":0.6931471805599453,"f2":3}"
  22 = "{"f0":"(","f1":0.6931471805599453,"f2":4}"
  23 = "{"f0":"入门","f1":1.0986122886681098,"f2":5}"
  24 = "{"f0":"医学","f1":1.0986122886681098,"f2":6}"
  25 = "{"f0":"文集","f1":1.0986122886681098,"f2":7}"
  26 = "{"f0":"正版","f1":1.0986122886681098,"f2":8}"
  27 = "{"f0":"版","f1":1.0986122886681098,"f2":9}"
  28 = "{"f0":"电磁","f1":1.0986122886681098,"f2":10}"
  29 = "{"f0":"选读","f1":1.0986122886681098,"f2":11}"
  30 = "{"f0":"中国","f1":1.0986122886681098,"f2":12}"
  31 = "{"f0":"书","f1":1.0986122886681098,"f2":13}"
  32 = "{"f0":"十二册","f1":1.0986122886681098,"f2":14}"
  33 = "{"f0":"南开大学","f1":1.0986122886681098,"f2":15}"
  34 = "{"f0":"文学","f1":1.0986122886681098,"f2":16}"
  35 = "{"f0":"郁达夫","f1":1.0986122886681098,"f2":17}"
  36 = "{"f0":"馆藏","f1":1.0986122886681098,"f2":18}"
 featureType = "WORD_COUNT"
 minTF = 1.0

0x06 предсказание

Бизнес-логика прогнозирования — DocCountVectorizerModelMapper.

Сначала мы можем увидеть FeatureType, который можно использовать для настройки того, какую информацию выводить. Например, может быть выведено следующее:

public enum FeatureType implements Serializable {
    /**
     * IDF type, the output value is inverse document frequency.
     */
    IDF(
        (idf, termFrequency, tokenRatio) -> idf
    ),
    /**
     * WORD_COUNT type, the output value is the word count.
     */
    WORD_COUNT(
        (idf, termFrequency, tokenRatio) -> termFrequency
    ),
    /**
     * TF_IDF type, the output value is term frequency * inverse document frequency.
     */
    TF_IDF(
        (idf, termFrequency, tokenRatio) -> idf * termFrequency * tokenRatio
    ),
    /**
     * BINARY type, the output value is 1.0.
     */
    BINARY(
        (idf, termFrequency, tokenRatio) -> 1.0
    ),
    /**
     * TF type, the output value is term frequency.
     */
    TF(
        (idf, termFrequency, tokenRatio) -> termFrequency * tokenRatio
    );
}

Во-вторых, в функции open будет загружена модель, например:

wordIdWeight = {HashMap@10838}  size = 37
 "医学" -> {Tuple2@10954} "(6,1.0986122886681098)"
 "选读" -> {Tuple2@10956} "(11,1.0986122886681098)"
 "十二册" -> {Tuple2@10958} "(14,1.0986122886681098)"
...
 "华龄" -> {Tuple2@11022} "(22,1.0986122886681098)"
 "索引" -> {Tuple2@11024} "(27,1.0986122886681098)"
featureType = {DocCountVectorizerModelMapper$FeatureType@10834} "WORD_COUNT"

Наконец, при прогнозировании вызовите функцию predictSparseVector, которая будет ориентироваться на входные данные.二手 旧书 : 医学 电磁 成像соответствовать. Создайте разреженный вектор SparseVector.

0|$37$0:1.0 6:1.0 10:1.0 25:1.0 26:1.0 28:1.0

Вышеизложенное указывает на то, что слова соответствуют словам с соответствующими порядковыми номерами в словарях 0 6 10 25 26 28, и соответствующее количество вхождений в этом предложении равно единице.

ссылка 0x07

Подробное объяснение и применение Tf-Idf

github.com/fxsjy/jieba

★★★★★★Думая о жизни и технологиях★★★★★★

Публичный аккаунт WeChat: мысли Росси

Если вы хотите получать своевременные новости о статьях, написанных отдельными лицами, или хотите видеть технические материалы, рекомендованные отдельными лицами, обратите внимание.

В этой статье используетсяmdniceнабор текста