2018-07-11 15:36:27 +00:00
|
|
|
|
using BotSharp.Core.Abstractions;
|
|
|
|
|
|
using BotSharp.Core.Agents;
|
2018-08-01 20:25:50 +00:00
|
|
|
|
using BotSharp.MachineLearning.NLP;
|
2018-08-15 22:37:23 +00:00
|
|
|
|
using BotSharp.NLP.Tokenize;
|
2018-08-09 21:06:40 +00:00
|
|
|
|
using DotNetToolkit;
|
2018-07-11 15:36:27 +00:00
|
|
|
|
using EntityFrameworkCore.BootKit;
|
|
|
|
|
|
using Microsoft.Extensions.Configuration;
|
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
|
|
using System;
|
2018-08-01 20:25:50 +00:00
|
|
|
|
using System.Collections;
|
2018-07-11 15:36:27 +00:00
|
|
|
|
using System.Collections.Generic;
|
2018-08-09 21:06:40 +00:00
|
|
|
|
using System.Diagnostics;
|
2018-08-01 20:25:50 +00:00
|
|
|
|
using System.IO;
|
2018-08-10 20:10:53 +00:00
|
|
|
|
using System.Linq;
|
2018-07-11 15:36:27 +00:00
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Text.RegularExpressions;
|
2018-08-01 20:25:50 +00:00
|
|
|
|
using System.Threading;
|
2018-08-09 21:06:40 +00:00
|
|
|
|
using System.Threading.Tasks;
|
2018-07-11 15:36:27 +00:00
|
|
|
|
|
2018-08-11 18:17:15 +00:00
|
|
|
|
namespace BotSharp.Core.Engines.NERs
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
2018-08-13 04:14:02 +00:00
|
|
|
|
public class CRFsuiteEntityRecognizer : INlpTrain, INlpPredict, INlpNer
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
|
|
|
|
|
public IConfiguration Configuration { get; set; }
|
2018-08-09 21:54:52 +00:00
|
|
|
|
public PipeSettings Settings { get; set; }
|
2018-07-11 15:36:27 +00:00
|
|
|
|
|
2018-08-11 18:17:15 +00:00
|
|
|
|
public List<OntologyEnum> Ontologies
|
|
|
|
|
|
{
|
|
|
|
|
|
get
|
|
|
|
|
|
{
|
|
|
|
|
|
return new List<OntologyEnum>
|
|
|
|
|
|
{
|
2018-08-12 18:42:27 +00:00
|
|
|
|
OntologyEnum.Location,
|
2018-08-11 18:17:15 +00:00
|
|
|
|
OntologyEnum.DateTime
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2018-08-10 20:10:53 +00:00
|
|
|
|
public async Task<bool> Train(Agent agent, NlpDoc doc, PipeModel meta)
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
|
|
|
|
|
var dc = new DefaultDataContextLoader().GetDefaultDc();
|
2018-08-01 20:25:50 +00:00
|
|
|
|
var corpus = agent.Corpus;
|
2018-08-09 21:06:40 +00:00
|
|
|
|
|
|
|
|
|
|
meta.Model = "ner-crf.model";
|
|
|
|
|
|
|
2018-08-01 20:25:50 +00:00
|
|
|
|
List<TrainingIntentExpression<TrainingIntentExpressionPart>> userSays = corpus.UserSays;
|
2018-08-06 18:39:26 +00:00
|
|
|
|
List<List<TrainingData>> list = new List<List<TrainingData>>();
|
2018-07-11 15:36:27 +00:00
|
|
|
|
|
2018-08-15 22:37:23 +00:00
|
|
|
|
string rawTrainingDataFileName = Path.Combine(Settings.TempDir, "ner-crf.corpus.txt");
|
|
|
|
|
|
string parsedTrainingDataFileName = Path.Combine(Settings.TempDir, "ner-crf.parsed.txt");
|
|
|
|
|
|
string modelFileName = Path.Combine(Settings.ModelDir, meta.Model);
|
2018-07-11 15:36:27 +00:00
|
|
|
|
|
2018-08-09 21:06:40 +00:00
|
|
|
|
using (FileStream fs = new FileStream(rawTrainingDataFileName, FileMode.Create))
|
2018-08-01 20:25:50 +00:00
|
|
|
|
{
|
2018-08-09 21:06:40 +00:00
|
|
|
|
using (StreamWriter sw = new StreamWriter(fs))
|
|
|
|
|
|
{
|
2018-08-10 20:10:53 +00:00
|
|
|
|
for (int i = 0; i < doc.Sentences.Count; i++)
|
2018-08-09 21:06:40 +00:00
|
|
|
|
{
|
2018-08-10 20:10:53 +00:00
|
|
|
|
List<TrainingData> curLine = Merge(doc.Sentences[i].Tokens, userSays[i].Entities);
|
2018-08-09 21:06:40 +00:00
|
|
|
|
curLine.ForEach(trainingData =>
|
|
|
|
|
|
{
|
|
|
|
|
|
string[] wordParams = { trainingData.Entity, trainingData.Token, trainingData.Pos, trainingData.Chunk };
|
|
|
|
|
|
string wordStr = string.Join(" ", wordParams);
|
|
|
|
|
|
sw.Write(wordStr + "\n");
|
|
|
|
|
|
});
|
|
|
|
|
|
list.Add(curLine);
|
|
|
|
|
|
sw.Write("\n");
|
|
|
|
|
|
}
|
|
|
|
|
|
sw.Flush();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2018-07-11 15:36:27 +00:00
|
|
|
|
|
2018-08-09 21:06:40 +00:00
|
|
|
|
var fields = Configuration.GetValue<String>($"CRFsuiteEntityRecognizer:fields");
|
|
|
|
|
|
var uniFeatures = Configuration.GetValue<String>($"CRFsuiteEntityRecognizer:uniFeatures");
|
|
|
|
|
|
var biFeatures = Configuration.GetValue<String>($"CRFsuiteEntityRecognizer:biFeatures");
|
|
|
|
|
|
|
|
|
|
|
|
new MachineLearning.CRFsuite.Ner()
|
2018-08-15 22:37:23 +00:00
|
|
|
|
.NerStart(rawTrainingDataFileName, parsedTrainingDataFileName, fields, uniFeatures.Split(' '), biFeatures.Split(' '));
|
2018-08-01 20:25:50 +00:00
|
|
|
|
|
2018-08-15 22:37:23 +00:00
|
|
|
|
var algorithmDir = Path.Combine(AppDomain.CurrentDomain.GetData("ContentRootPath").ToString(), "Algorithms");
|
2018-08-09 21:06:40 +00:00
|
|
|
|
|
2018-08-15 22:37:23 +00:00
|
|
|
|
CmdHelper.Run(Path.Combine(algorithmDir, "crfsuite"), $"learn -m {modelFileName} {parsedTrainingDataFileName}", false); // --split=3 -x
|
2018-08-09 21:06:40 +00:00
|
|
|
|
Console.WriteLine($"Saved model to {modelFileName}");
|
|
|
|
|
|
meta.Meta = new JObject();
|
|
|
|
|
|
meta.Meta["fields"] = fields;
|
|
|
|
|
|
meta.Meta["uniFeatures"] = uniFeatures;
|
|
|
|
|
|
meta.Meta["biFeatures"] = biFeatures;
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
2018-08-01 20:25:50 +00:00
|
|
|
|
}
|
2018-08-09 21:06:40 +00:00
|
|
|
|
|
2018-08-15 22:37:23 +00:00
|
|
|
|
public List<TrainingData> Merge(List<Token> tokens, List<TrainingIntentExpressionPart> entities)
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
|
|
|
|
|
List<TrainingData> trainingTuple = new List<TrainingData>();
|
|
|
|
|
|
HashSet<String> entityWordBag = new HashSet<String>();
|
2018-08-01 20:25:50 +00:00
|
|
|
|
int wordCandidateCount = 0;
|
|
|
|
|
|
|
2018-08-09 21:06:40 +00:00
|
|
|
|
for (int i = 0; i < tokens.Count; i++)
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
2018-08-01 20:25:50 +00:00
|
|
|
|
TrainingIntentExpressionPart curEntity = null;
|
|
|
|
|
|
if (entities != null)
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
2018-08-01 20:25:50 +00:00
|
|
|
|
bool entityFinded = false;
|
|
|
|
|
|
entities.ForEach(entity => {
|
|
|
|
|
|
if (!entityFinded)
|
|
|
|
|
|
{
|
2018-08-15 22:37:23 +00:00
|
|
|
|
string[] words = entity.Value.Split(' ');
|
2018-08-01 20:25:50 +00:00
|
|
|
|
for (int j = 0; j < words.Length; j++)
|
|
|
|
|
|
{
|
2018-08-09 21:06:40 +00:00
|
|
|
|
if (tokens[i + j].Text == words[j])
|
2018-08-01 20:25:50 +00:00
|
|
|
|
{
|
|
|
|
|
|
wordCandidateCount++;
|
|
|
|
|
|
if (j == words.Length - 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
curEntity = entity;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
wordCandidateCount = 0;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2018-08-14 22:21:09 +00:00
|
|
|
|
if (wordCandidateCount != 0) // && entity.Start == tokens[i].Offset)
|
2018-08-01 20:25:50 +00:00
|
|
|
|
{
|
|
|
|
|
|
String entityName = curEntity.Entity.Contains(":")? curEntity.Entity.Substring(curEntity.Entity.IndexOf(":") + 1): curEntity.Entity;
|
|
|
|
|
|
foreach(string s in words)
|
|
|
|
|
|
{
|
2018-08-09 21:06:40 +00:00
|
|
|
|
trainingTuple.Add(new TrainingData(entityName, s, tokens[i].Pos, "I"));
|
2018-08-01 20:25:50 +00:00
|
|
|
|
}
|
|
|
|
|
|
entityFinded = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2018-07-11 15:36:27 +00:00
|
|
|
|
}
|
2018-08-01 20:25:50 +00:00
|
|
|
|
if (wordCandidateCount == 0)
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
2018-08-09 21:06:40 +00:00
|
|
|
|
trainingTuple.Add(new TrainingData("O", tokens[i].Text, tokens[i].Pos, "O"));
|
2018-07-11 15:36:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
2018-08-01 20:25:50 +00:00
|
|
|
|
{
|
|
|
|
|
|
i = i + wordCandidateCount - 1;
|
|
|
|
|
|
}
|
2018-07-11 15:36:27 +00:00
|
|
|
|
}
|
2018-07-12 21:22:42 +00:00
|
|
|
|
|
2018-08-09 21:06:40 +00:00
|
|
|
|
return trainingTuple;
|
2018-07-11 15:36:27 +00:00
|
|
|
|
}
|
2018-08-08 14:45:35 +00:00
|
|
|
|
|
2018-08-10 20:10:53 +00:00
|
|
|
|
public async Task<bool> Predict(Agent agent, NlpDoc doc, PipeModel meta)
|
2018-08-09 21:06:40 +00:00
|
|
|
|
{
|
2018-08-10 14:52:31 +00:00
|
|
|
|
var uniFeatures = meta.Meta["uniFeatures"].ToString();
|
|
|
|
|
|
var biFeatures = meta.Meta["biFeatures"].ToString();
|
|
|
|
|
|
string field = meta.Meta["fields"].ToString();
|
2018-08-15 22:37:23 +00:00
|
|
|
|
string[] fields = field.Split(' ');
|
2018-08-09 21:59:04 +00:00
|
|
|
|
|
2018-08-15 22:37:23 +00:00
|
|
|
|
string rawPredictingDataFileName = Path.Combine(Settings.TempDir, "ner-crf.corpus.predict.txt");
|
|
|
|
|
|
string parsedPredictingDataFileName = Path.Combine(Settings.TempDir, "ner-crf.parsed.predict.txt");
|
|
|
|
|
|
string modelFileName = Path.Combine(Settings.ModelDir, meta.Model);
|
2018-08-09 21:59:04 +00:00
|
|
|
|
|
|
|
|
|
|
using (FileStream fs = new FileStream(rawPredictingDataFileName, FileMode.Create))
|
|
|
|
|
|
{
|
|
|
|
|
|
using (StreamWriter sw = new StreamWriter(fs))
|
|
|
|
|
|
{
|
|
|
|
|
|
List<string> curLine = new List<string>();
|
2018-08-10 20:10:53 +00:00
|
|
|
|
foreach (NlpDocSentence sentence in doc.Sentences)
|
2018-08-09 21:59:04 +00:00
|
|
|
|
{
|
2018-08-15 22:37:23 +00:00
|
|
|
|
foreach (Token token in sentence.Tokens)
|
2018-08-09 21:59:04 +00:00
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0 ; i < fields.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (fields[i] == "y") {
|
|
|
|
|
|
curLine.Add("");
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (fields[i] == "w") {
|
|
|
|
|
|
curLine.Add(token.Text);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (fields[i] == "pos") {
|
|
|
|
|
|
curLine.Add(token.Tag);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (fields[i] == "chk") {
|
|
|
|
|
|
curLine.Add("");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
sw.Write(string.Join(" ", curLine) + "\n");
|
2018-08-10 14:52:31 +00:00
|
|
|
|
curLine.Clear();
|
2018-08-09 21:59:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
sw.Write("\n");
|
2018-08-10 14:52:31 +00:00
|
|
|
|
|
2018-08-09 21:59:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
sw.Flush();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2018-08-10 20:10:53 +00:00
|
|
|
|
|
2018-08-09 21:59:04 +00:00
|
|
|
|
new MachineLearning.CRFsuite.Ner()
|
2018-08-15 22:37:23 +00:00
|
|
|
|
.NerStart(rawPredictingDataFileName, parsedPredictingDataFileName, field, uniFeatures.Split(' '), biFeatures.Split(' '));
|
2018-08-09 21:59:04 +00:00
|
|
|
|
|
2018-08-15 22:37:23 +00:00
|
|
|
|
var output = CmdHelper.Run(Path.Combine(Settings.AlgorithmDir, "crfsuite"), $"tag -i -m {modelFileName} {parsedPredictingDataFileName}", false);
|
2018-08-10 14:52:31 +00:00
|
|
|
|
|
|
|
|
|
|
var entities = new List<NlpEntity>();
|
2018-08-10 20:10:53 +00:00
|
|
|
|
|
2018-08-15 22:37:23 +00:00
|
|
|
|
string[] entityProbabilityPairs = output.Split(new string[] { Environment.NewLine }, StringSplitOptions.None).Where(x => !String.IsNullOrEmpty(x)).ToArray();
|
2018-08-10 20:10:53 +00:00
|
|
|
|
for (int i = 0; i < entityProbabilityPairs.Length; i++)
|
2018-08-10 15:42:00 +00:00
|
|
|
|
{
|
|
|
|
|
|
string entityProbabilityPair = entityProbabilityPairs[i];
|
2018-08-15 22:37:23 +00:00
|
|
|
|
string entity = entityProbabilityPair.Split(':')[0];
|
|
|
|
|
|
decimal probability = decimal.Parse(entityProbabilityPair.Split(':')[1]);
|
2018-08-10 20:10:53 +00:00
|
|
|
|
entities.Add(new NlpEntity
|
|
|
|
|
|
{
|
|
|
|
|
|
Entity = entity,
|
2018-08-10 22:13:59 +00:00
|
|
|
|
Start = doc.Sentences[0].Tokens[i].Offset,
|
2018-08-10 20:10:53 +00:00
|
|
|
|
Value = doc.Sentences[0].Tokens[i].Text,
|
|
|
|
|
|
Confidence = probability
|
|
|
|
|
|
});
|
2018-08-10 15:42:00 +00:00
|
|
|
|
}
|
2018-08-10 14:52:31 +00:00
|
|
|
|
|
2018-08-10 22:13:59 +00:00
|
|
|
|
List<NlpEntity> unionedEntities = MergeEntity(entities);
|
|
|
|
|
|
|
|
|
|
|
|
doc.Sentences[0].Entities = unionedEntities.Where(x => x.Entity != "O").ToList();
|
2018-08-10 20:10:53 +00:00
|
|
|
|
|
2018-08-10 14:52:31 +00:00
|
|
|
|
if(File.Exists(rawPredictingDataFileName))
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Delete(rawPredictingDataFileName);
|
|
|
|
|
|
}
|
|
|
|
|
|
if(File.Exists(parsedPredictingDataFileName))
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Delete(parsedPredictingDataFileName);
|
|
|
|
|
|
}
|
2018-08-10 20:10:53 +00:00
|
|
|
|
|
2018-08-09 21:06:40 +00:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
2018-08-10 22:13:59 +00:00
|
|
|
|
|
|
|
|
|
|
public List<NlpEntity> MergeEntity (List<NlpEntity> tokens)
|
|
|
|
|
|
{
|
|
|
|
|
|
List<NlpEntity> res = new List<NlpEntity>();
|
|
|
|
|
|
for (int i = 0; i < tokens.Count ; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
NlpEntity nlpEntity = new NlpEntity();
|
|
|
|
|
|
StringBuilder unionValue = new StringBuilder(tokens[i].Value);
|
|
|
|
|
|
StringBuilder unionEntity = new StringBuilder(tokens[i].Entity);
|
|
|
|
|
|
decimal unoinConfidence = tokens[i].Confidence;
|
|
|
|
|
|
|
|
|
|
|
|
int j = i + 1;
|
|
|
|
|
|
while (j < tokens.Count && tokens[j].Entity == tokens[i].Entity && tokens[i].Entity != "O")
|
|
|
|
|
|
{
|
|
|
|
|
|
unionValue.Append(" " + tokens[j].Value);
|
|
|
|
|
|
j++;
|
|
|
|
|
|
}
|
|
|
|
|
|
nlpEntity.Entity = unionEntity.ToString();
|
|
|
|
|
|
nlpEntity.Start = tokens[i].Start;
|
|
|
|
|
|
nlpEntity.Value = unionValue.ToString();
|
|
|
|
|
|
nlpEntity.Confidence = unoinConfidence;
|
|
|
|
|
|
res.Add(nlpEntity);
|
|
|
|
|
|
i = j - 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
|
|
|
}
|
2018-07-11 15:36:27 +00:00
|
|
|
|
}
|
2018-08-01 21:40:51 +00:00
|
|
|
|
|
2018-07-11 15:36:27 +00:00
|
|
|
|
public class TrainingData
|
|
|
|
|
|
{
|
|
|
|
|
|
public String Token { get; set; }
|
|
|
|
|
|
public String Entity { get; set; }
|
2018-08-09 21:06:40 +00:00
|
|
|
|
public String Pos { get; set; }
|
2018-08-01 20:25:50 +00:00
|
|
|
|
public String Chunk { get; set; }
|
2018-07-11 15:36:27 +00:00
|
|
|
|
|
2018-08-09 21:06:40 +00:00
|
|
|
|
public TrainingData(string entity, string token, string pos, string chunk)
|
2018-07-11 15:36:27 +00:00
|
|
|
|
{
|
2018-08-09 21:06:40 +00:00
|
|
|
|
Token = token;
|
|
|
|
|
|
Entity = entity;
|
|
|
|
|
|
Pos = pos;
|
|
|
|
|
|
Chunk = chunk;
|
2018-07-11 15:36:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|