Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- public class NewFieldTaggingManager
- {
- /// <summary>
- /// Назнвание игнорируемой технической сущности.
- /// </summary>
- public const string Other = "<Other>";
- /// <summary>
- /// Порог для отбора достоверных значений.
- /// </summary>
- public const double threshold = 0.6;
- /// <summary>
- /// результат сравнения двух фарз.
- /// </summary>
- private enum CompareStatus
- {
- None,
- Better,
- Worse,
- Questionable
- }
- /// <summary>
- /// Представляет диапазон индексов.
- /// </summary>
- private class IndexRange
- {
- public int Start { get; }
- public int Length { get; set; }
- public IndexRange(int start, int length)
- {
- Start = start;
- Length = length;
- }
- public override string ToString() => $"{Start};{Length}";
- }
- /// <summary>
- /// Содержит данные о фразе.
- /// </summary>
- private class WordsPhrase
- {
- /// <summary>
- /// Родительская фраза в процессе разбора.
- /// </summary>
- public WordsPhrase Parent { get; set; }
- /// <summary>
- /// Текст фарзы.
- /// </summary>
- public string Phrase { get; }
- /// <summary>
- /// Вес (уверенность)
- /// </summary>
- public double Weight { get; }
- /// <summary>
- /// Метка (имя сущности).
- /// </summary>
- public string Label { get; }
- /// <summary>
- /// Токены которые принадлежат к фразе.
- /// </summary>
- public List<Token> Tokens { get; }
- /// <summary>
- /// Топовые сущности.
- /// </summary>
- public List<Tuple<string, double>> TopLabels { get; }
- public CompareStatus Status { get; set; }
- /// <summary>
- /// Поколение разметки на котором сгенерирована фраза.
- /// </summary>
- public int Generation { get; set; }
- public WordsPhrase(string phrase, string label, double weight, List<Token> tokens, List<Tuple<string, double>> topLabels)
- {
- Phrase = phrase;
- Label = label;
- Weight = weight;
- Tokens = tokens;
- TopLabels = topLabels;
- }
- public WordsPhrase Clone()
- {
- return (WordsPhrase)MemberwiseClone();
- }
- public override string ToString() => $"{Phrase} => {Label} ({Weight:N3})";
- }
- /// <summary>
- /// Выполняет определение принадлежности токенов к определенной сущности.
- /// </summary>
- /// <param name="tokens"></param>
- public void Parse(List<Token> tokens, ClassifierEngineData engineData)
- {
- var groups = TagTokenHelper.GetMaxGroups(tokens,
- t => t.TokenType == TokenType.Undefined,
- t => t.TokenType == TokenType.Ignore
- );
- foreach (var group in groups)
- {
- // фразы в скобочках обрабатываем отдельно
- if (group.Count > 1 && IsGroupInParentheses(group, tokens))
- AnalizeAndTagSentenceInParentheses(group, tokens, engineData);
- else
- AnalizeAndTagSentence(group, tokens, engineData);
- }
- }
- /// <summary>
- /// Проверяет находится ли группа в скобках.
- /// </summary>
- /// <param name="group"></param>
- /// <param name="tokens"></param>
- /// <returns></returns>
- private bool IsGroupInParentheses(List<Token> group, List<Token> tokens)
- {
- // находим левый токен и пропускаем игнорируемые
- int firstGroupTokenIdx = tokens.IndexOf(group[0]);
- while (firstGroupTokenIdx - 1 >= 0 && tokens[firstGroupTokenIdx - 1].TokenType == TokenType.Ignore)
- firstGroupTokenIdx--;
- if (firstGroupTokenIdx == 0)
- return false;
- // находим правый токен и пропускаем игнорируемые
- int lastGroupTokenIdx = tokens.IndexOf(group[group.Count - 1]);
- while (lastGroupTokenIdx + 1 < tokens.Count && tokens[lastGroupTokenIdx + 1].TokenType == TokenType.Ignore)
- lastGroupTokenIdx++;
- if (lastGroupTokenIdx == tokens.Count - 1)
- return false;
- // проверяем что дальше стоят скобочки
- if (tokens[firstGroupTokenIdx - 1].Word == "(" && tokens[lastGroupTokenIdx + 1].Word == ")")
- return true;
- return false;
- }
- /// <summary>
- /// Выполняет анализ списка токенов, которые расположены в скобках и являются (скорей всего) одной сущностью.
- /// </summary>
- /// <param name="group"></param>
- /// <param name="tokens"></param>
- private void AnalizeAndTagSentenceInParentheses(List<Token> group, List<Token> tokens, ClassifierEngineData engineData)
- {
- WordsPhrase phrase = TokensToPhrase(group, tokens, engineData);
- group.ForEach(t => { t.MatchWord = phrase.TopLabels[0].Item1; t.TokenType = TokenType.Param; });
- }
- /// <summary>
- /// Выполняет анализ списка токенов для определения принадлежности к сущности.
- /// </summary>
- /// <param name="tokens"></param>
- private void AnalizeAndTagSentence(List<Token> tokens, List<Token> sourceTokens, ClassifierEngineData engineData)
- {
- // генерируем стартовый набор фраз
- List<WordsPhrase> phrases = GenerateInitialPhrases(tokens, sourceTokens, engineData);
- // циклически повторяем процесс присоединения соседних токенов к фразам
- int gen = 1; // поколение генерации+обработки новых фраз
- const int MaxGenCount = 5; // максимальное кол-во повторений для избегания очень долгих циклов на говно-текстах.
- bool done = false; // флаг остановки процесса
- while (gen <= MaxGenCount && !done)
- {
- phrases = ProcessExpandPhrases(phrases, sourceTokens, engineData, gen, out done);
- gen++;
- }
- // помечаем токены для найденых фраз
- foreach (var phrase in phrases)
- {
- foreach (var token in phrase.Tokens)
- {
- if (phrase.Label != Other && phrase.Weight > threshold)
- {
- token.TokenType = TokenType.Param;
- token.MatchWord = phrase.Label;
- }
- else
- {
- //token.MatchWord = "Other";
- }
- }
- }
- }
- /// <summary>
- /// Генерирует фразу со всеми характеристиками из списка токенов.
- /// </summary>
- /// <param name="phraseTokens"></param>
- /// <returns></returns>
- private static WordsPhrase TokensToPhrase(List<Token> phraseTokens, List<Token> sourceTokens, ClassifierEngineData engineData)
- {
- int takeCount = phraseTokens.Count;
- List<Token> allPhraseTokens = GetAllTokensWithSeparators(phraseTokens, sourceTokens);
- string sumWord = TagTokenHelper.RecollectTagsWord(allPhraseTokens);
- var res = engineData.Predict(sumWord);
- double degreOfConfidence = res.Weight;
- List<Tuple<string, double>> topCfSum = GetTopFields(engineData.AllLabels, res.Score, 3);
- WordsPhrase phrase = new WordsPhrase(sumWord, res.Result, res.Weight, phraseTokens, topCfSum);
- return phrase;
- }
- /// <summary>
- /// Генерирует стартовый набор фраз.
- /// </summary>
- /// <param name="tokens"></param>
- /// <param name="sourceTokens"></param>
- /// <param name="engineData"></param>
- /// <returns></returns>
- private List<WordsPhrase> GenerateInitialPhrases(List<Token> tokens, List<Token> sourceTokens, ClassifierEngineData engineData)
- {
- List<WordsPhrase> wordsPhrases = new List<WordsPhrase>();
- // определяем и учитываем токены написанные слитно друг с другом.
- List<int> continiousTokensIndexes = DetectContiniousTokens(tokens, sourceTokens);
- var continiousTokensRanges = CollectContiniousRanges(continiousTokensIndexes).ToDictionary(r => r.Start);
- for (int i = 0; i < tokens.Count;)
- {
- int start = i;
- int len = 1;
- if (continiousTokensRanges.TryGetValue(start, out var range))
- {
- len = range.Length;
- }
- List<Token> phraseTokens = tokens.Skip(start).Take(len).ToList();
- WordsPhrase phrase = TokensToPhrase(phraseTokens, sourceTokens, engineData);
- //phrase.Status = CompareStatus.Better;
- wordsPhrases.Add(phrase);
- i += len;
- }
- return wordsPhrases;
- }
- /// <summary>
- /// Выполняет расширение фраз и их анализ.
- /// </summary>
- /// <param name="phrases"></param>
- /// <param name="sourceTokens"></param>
- /// <param name="engineData"></param>
- /// <param name="generation"></param>
- /// <param name="done"></param>
- /// <returns></returns>
- private List<WordsPhrase> ProcessExpandPhrases(List<WordsPhrase> phrases, List<Token> sourceTokens, ClassifierEngineData engineData, int generation, out bool done)
- {
- List<WordsPhrase> childPhrases = new List<WordsPhrase>();
- bool newPhrasesGenerated = false;
- for (int i = 0; i < phrases.Count; i++)
- {
- var phrase = phrases[i];
- // если фраза была сгенерирована ранее чем на прошлом поколении, то нет смысла обрабатывать, т.к. она точно сгенерирует изменений.
- if (phrase.Generation < generation - 1)
- {
- childPhrases.Add(phrase);
- continue;
- }
- bool anyChildrenAdded = false;
- // после 1го поколения расширяемся только на Other
- // расширяем фразу влево
- if (i > 0 && (generation == 1 || phrases[i - 1].Label == Other))
- {
- WordsPhrase combinedToLeft = ConcatenatePhrases(phrases[i - 1], phrase, sourceTokens, engineData);
- var compareStatus = ComparePhrases(combinedToLeft, phrase);
- combinedToLeft.Parent = phrase;
- combinedToLeft.Status = compareStatus;
- combinedToLeft.Generation = generation;
- childPhrases.Add(combinedToLeft);
- anyChildrenAdded = true;
- }
- // расширяем фразу вправо
- if (i < phrases.Count - 1 && (generation == 1 || phrases[i + 1].Label == Other))
- {
- WordsPhrase combinedToRight = ConcatenatePhrases(phrase, phrases[i + 1], sourceTokens, engineData);
- var compareStatus = ComparePhrases(combinedToRight, phrase);
- combinedToRight.Parent = phrase;
- combinedToRight.Status = compareStatus;
- combinedToRight.Generation = generation;
- childPhrases.Add(combinedToRight);
- anyChildrenAdded = true;
- }
- // если расширение не произошло, то просто добавляем фразу в результат
- if (!anyChildrenAdded)
- childPhrases.Add(phrase);
- else
- newPhrasesGenerated = true;
- }
- // если нового ничего не сгенерировано, то сигнализируем об этом
- if (!newPhrasesGenerated)
- {
- done = true;
- return childPhrases;
- }
- // выбираем лучшие фразы из одинаковых или родственных вариантов
- var res1 = ResolveSamePhrasePairs(childPhrases);
- var res2 = ResolveSiblings(res1);
- // циклически выбираем лучшие фразы из пересекающихся вариантов
- var res = res2;
- bool hasChanges;
- do
- {
- res = ResolveOverlaid(res, out hasChanges);
- } while (hasChanges);
- done = false;
- return res;
- }
- /// <summary>
- /// Анализирует соседние фразы совпадающие по тексту.
- /// </summary>
- /// <param name="phrases"></param>
- /// <returns></returns>
- private List<WordsPhrase> ResolveSamePhrasePairs(List<WordsPhrase> phrases)
- {
- List<WordsPhrase> result = new List<WordsPhrase>();
- for (int i = 0; i < phrases.Count;)
- {
- if (i == phrases.Count - 1)
- {
- result.Add(phrases[i]);
- i += 1;
- continue;
- }
- var phrase1 = phrases[i];
- var phrase2 = phrases[i + 1];
- if (phrase1.Phrase != phrase2.Phrase)
- {
- result.Add(phrases[i]);
- i += 1;
- continue;
- }
- ResolvePair(result, phrase1, phrase2);
- i += 2;
- }
- RemoveFullDuplicates(result);
- return result;
- }
- /// <summary>
- /// Анализирует соседние фразы с общим родителем.
- /// </summary>
- /// <param name="phrases"></param>
- /// <returns></returns>
- private List<WordsPhrase> ResolveSiblings(List<WordsPhrase> phrases)
- {
- List<WordsPhrase> result = new List<WordsPhrase>();
- for (int i = 0; i < phrases.Count;)
- {
- if (i == phrases.Count - 1)
- {
- result.Add(phrases[i]);
- i += 1;
- continue;
- }
- var phrase1 = phrases[i];
- var phrase2 = phrases[i + 1];
- if (phrase1.Parent == null || phrase1.Parent != phrase2.Parent)
- {
- result.Add(phrases[i]);
- i += 1;
- continue;
- }
- ResolvePair(result, phrase1, phrase2);
- i += 2;
- }
- RemoveFullDuplicates(result);
- return result;
- }
- /// <summary>
- /// Анализирует соседние фразы учитывая наложение токенов.
- /// </summary>
- /// <param name="phrases"></param>
- /// <param name="hasChanges"></param>
- /// <returns></returns>
- private List<WordsPhrase> ResolveOverlaid(List<WordsPhrase> phrases, out bool hasChanges)
- {
- hasChanges = false;
- List<WordsPhrase> result = new List<WordsPhrase>();
- for (int i = 0; i < phrases.Count;)
- {
- if (i == phrases.Count - 1)
- {
- result.Add(phrases[i]);
- i += 1;
- continue;
- }
- var phrase1 = phrases[i];
- var phrase2 = phrases[i + 1];
- if (!IsTokensOverlaids(phrase1.Tokens, phrase2.Tokens))
- {
- result.Add(phrase1);
- i += 1;
- continue;
- }
- hasChanges = true;
- ResolvePair(result, phrase1, phrase2);
- i += 2;
- }
- RemoveFullDuplicates(result);
- return result;
- }
- /// <summary>
- /// Анализирует 2 фразы и результат их сравнения кладет в <paramref name="result"/>.
- /// </summary>
- /// <param name="result"></param>
- /// <param name="phrase1"></param>
- /// <param name="phrase2"></param>
- private static void ResolvePair(List<WordsPhrase> result, WordsPhrase phrase1, WordsPhrase phrase2)
- {
- var status1 = phrase1.Status;
- var status2 = phrase2.Status;
- if (phrase1.Status == phrase2.Status)
- {
- if (status1 == CompareStatus.Worse)
- {
- // откатываем
- result.Add(phrase1.Parent);
- result.Add(phrase2.Parent);
- }
- else if (status1 == CompareStatus.Questionable)
- {
- // берем с лучшим весом, если превышает порог или откатываем
- double maxWeight = Math.Max(phrase1.Weight, phrase2.Weight);
- if (maxWeight > threshold)
- {
- var tmpPhrase = GetWhere(phrase1, phrase2, p => p.Weight == maxWeight);
- result.Add(tmpPhrase);
- }
- else
- {
- result.Add(phrase1.Parent);
- result.Add(phrase2.Parent);
- }
- }
- else if (status1 == CompareStatus.Better)
- {
- // кладем с большим весом
- var tmpPhrase = phrase1.Weight >= phrase2.Weight ? phrase1 : phrase2;
- result.Add(tmpPhrase);
- }
- else // None
- {
- // ничего не меняем
- result.Add(phrase1);
- result.Add(phrase2);
- }
- }
- else // status1 != status2
- {
- if (IsOneEquals(status1, status2, CompareStatus.Questionable))
- {
- if (IsOneEquals(status1, status2, CompareStatus.Better))
- {
- // просто берем лучше
- var betterPhrase = GetWhere(phrase1, phrase2, s => s.Status == CompareStatus.Better);
- result.Add(betterPhrase);
- }
- else if (IsOneEquals(status1, status2, CompareStatus.Worse))
- {
- // сразу откат
- result.Add(phrase1.Parent ?? phrase1);
- result.Add(phrase2.Parent ?? phrase2);
- }
- else
- {
- // если Questionable переваливает за порог, то берем
- var questionablePhrase = GetWhere(phrase1, phrase2, s => s.Status == CompareStatus.Questionable);
- if (questionablePhrase.Weight > threshold)
- {
- result.Add(questionablePhrase);
- }
- else
- {
- result.Add(phrase1.Parent ?? phrase1);
- result.Add(phrase2.Parent ?? phrase2);
- }
- }
- }
- else if (IsOneEquals(status1, status2, CompareStatus.Better))
- {
- // просто берем учший
- var betterPhrase = GetWhere(phrase1, phrase2, s => s.Status == CompareStatus.Better);
- result.Add(betterPhrase);
- }
- else // worse или none - откат
- {
- result.Add(phrase1.Parent ?? phrase1);
- result.Add(phrase2.Parent ?? phrase2);
- }
- }
- }
- /// <summary>
- /// Удаляет полные дубликаты соседних токенов.
- /// </summary>
- /// <param name="phrases"></param>
- private void RemoveFullDuplicates(List<WordsPhrase> phrases)
- {
- for (int i = 0; i < phrases.Count - 1; i++)
- {
- var phrase1 = phrases[i];
- var phrase2 = phrases[i + 1];
- if (phrase1 == phrase2)
- {
- phrases.RemoveAt(i + 1);
- }
- }
- }
- /// <summary>
- /// Возвращает содержатся ли пересекающиеся токены.
- /// </summary>
- /// <param name="tokens1"></param>
- /// <param name="tokens2"></param>
- /// <returns></returns>
- private static bool IsTokensOverlaids(List<Token> tokens1, List<Token> tokens2)
- {
- for (int i = 0; i < tokens1.Count; i++)
- {
- for (int j = 0; j < tokens2.Count; j++)
- {
- if (tokens1[i] == tokens2[j])
- return true;
- }
- }
- return false;
- }
- /// <summary>
- /// Из 2х объектов выбирает удовлетворяющий <paramref name="predicate"/>.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="obj1"></param>
- /// <param name="obj2"></param>
- /// <param name="predicate"></param>
- /// <returns></returns>
- private static T GetWhere<T>(T obj1, T obj2, Predicate<T> predicate)
- where T : class
- {
- if (predicate(obj1))
- return obj1;
- if (predicate(obj2))
- return obj2;
- return null;
- }
- /// <summary>
- /// Совпадает ли что либо с <paramref name="toCompare"/>.
- /// </summary>
- /// <param name="status1"></param>
- /// <param name="status2"></param>
- /// <param name="toCompare"></param>
- /// <returns></returns>
- private static bool IsOneEquals(CompareStatus status1, CompareStatus status2, CompareStatus toCompare)
- {
- return status1 == toCompare || status2 == toCompare;
- }
- const double epsilon = 0;//-0.002;
- /// <summary>
- /// Сравнивает новую и старую фарзы
- /// </summary>
- /// <param name="newPhrase"></param>
- /// <param name="oldPhrase"></param>
- /// <returns></returns>
- private CompareStatus ComparePhrases(WordsPhrase newPhrase, WordsPhrase oldPhrase)
- {
- string newName = newPhrase.Label;
- string oldName = oldPhrase.Label;
- if (newName != Other && oldName != Other)
- {
- if (newName == oldName)
- {
- double newConf = newPhrase.Weight;
- double oldConf = oldPhrase.Weight;
- double delta = newConf - oldConf;
- return delta > epsilon ? CompareStatus.Better : CompareStatus.Worse; // однозначно лучше, если уверенность больше
- }
- else
- return CompareStatus.Questionable; // questionable
- }
- if (newName == Other && oldName == Other)
- {
- return CompareStatus.Worse; // оба Other - нет смысла обрабатывать
- }
- return CompareStatus.Questionable; // если новый не Other то под вопросом
- }
- /// <summary>
- /// Склеивает две фразы в одну.
- /// </summary>
- /// <param name="phrase1"></param>
- /// <param name="phrase2"></param>
- /// <param name="sourceTokens"></param>
- /// <param name="engineData"></param>
- /// <returns></returns>
- private static WordsPhrase ConcatenatePhrases(WordsPhrase phrase1, WordsPhrase phrase2, List<Token> sourceTokens, ClassifierEngineData engineData)
- {
- List<Token> sumTokens = phrase1.Tokens.Concat(phrase2.Tokens).ToList();
- return TokensToPhrase(sumTokens, sourceTokens, engineData);
- }
- /// <summary>
- /// Собирает все токены из <paramref name="phraseTokens"/> вместе с пропущенными (игнорируемыми) используя все токены предложения <paramref name="sourceTokens"/>.
- /// </summary>
- /// <param name="phraseTokens"></param>
- /// <param name="sourceTokens"></param>
- /// <returns></returns>
- private static List<Token> GetAllTokensWithSeparators(List<Token> phraseTokens, List<Token> sourceTokens)
- {
- if (phraseTokens.Count == 1)
- return phraseTokens;
- bool takeIt = false;
- List<Token> result = new List<Token>();
- for (int i = 0; i < sourceTokens.Count; i++)
- {
- if (!takeIt)
- {
- if (sourceTokens[i] == phraseTokens[0])
- {
- takeIt = true;
- result.Add(sourceTokens[i]);
- }
- }
- else
- {
- result.Add(sourceTokens[i]);
- if (sourceTokens[i] == phraseTokens[phraseTokens.Count - 1])
- break;
- }
- }
- return result;
- }
- /// <summary>
- /// Возвращает список из наиболее вероятных полей для данного слова.
- /// </summary>
- /// <param name="wordVec"></param>
- /// <param name="topN"></param>
- /// <returns></returns>
- private static List<Tuple<string, double>> GetTopFields(string[] allLabels, float[] wordVec, int topN)
- {
- List<Tuple<string, double>> result = new List<Tuple<string, double>>();
- for (int i = 0; i < wordVec.Length; i++)
- {
- if (wordVec[i] > 0.0)
- result.Add(new Tuple<string, double>(allLabels[i], wordVec[i]));
- }
- return result.OrderByDescending(w => w.Item2).Take(topN).ToList();
- }
- // предлоги которые "приклеиваем" к следующим словам
- private static HashSet<string> prepositions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "with", "without", "w/", "w/o" };
- /// <summary>
- /// Определяет токены написанные слитно друг с другом.
- /// </summary>
- /// <param name="tokens"></param>
- /// <param name="sourceTokens"></param>
- /// <returns></returns>
- private static List<int> DetectContiniousTokens(List<Token> tokens, List<Token> sourceTokens)
- {
- List<int> result = new List<int>();
- if (tokens.Count <= 1)
- return result;
- List<Token> twoTokens = new List<Token>();
- for (int i = 0; i < tokens.Count - 1; i++)
- {
- if (prepositions.Contains(tokens[i].Word))
- {
- result.Add(i);
- continue;
- }
- // сравниваем попарно
- twoTokens.Add(tokens[i]);
- twoTokens.Add(tokens[i + 1]);
- // склеиваем с разделителями
- List<Token> allPhraseTokens = GetAllTokensWithSeparators(twoTokens, sourceTokens);
- string sumWord = TagTokenHelper.RecollectTagsWord(allPhraseTokens);
- // если не содержит пробелов - значит слитно
- bool containsWhitespace = IsContainsWhitespace(sumWord);
- if (!containsWhitespace)
- result.Add(i);
- twoTokens.Clear();
- }
- return result;
- }
- /// <summary>
- /// Собирает токены написанные слитно в диапазоны.
- /// </summary>
- /// <param name="indexes"></param>
- /// <returns></returns>
- private static List<IndexRange> CollectContiniousRanges(List<int> indexes)
- {
- List<IndexRange> result = new List<IndexRange>();
- if (indexes.Count == 0)
- return result;
- int startIdx = indexes[0];
- int len = 1;
- for (int i = 1; i < indexes.Count; i++)
- {
- if (indexes[i] - len == startIdx)
- len++;
- else
- {
- result.Add(new IndexRange(startIdx, len + 1));
- startIdx = indexes[i];
- len = 1;
- }
- }
- result.Add(new IndexRange(startIdx, len + 1));
- return result;
- }
- /// <summary>
- /// Проверяет содержатся ли пробелы в тексте.
- /// </summary>
- /// <param name="sumWord"></param>
- /// <returns></returns>
- private static bool IsContainsWhitespace(string sumWord)
- {
- foreach (char ch in sumWord)
- {
- if (char.IsWhiteSpace(ch))
- return true;
- }
- return false;
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement