BotSharp/BotSharp.NLP/Txt2Vec/Encoder.cs

901 lines
29 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Threading;
//using AdvUtils;
namespace Txt2Vec
{
public enum WORD_SOURCE
{
CORPUS,
PRETRAINED_MODEL
}
public class vocab_word
{
public string word;
public int cnt;
public WORD_SOURCE source;
}
public class Encoder
{
const int EXP_TABLE_SIZE = 1000;
const int MAX_EXP = 6;
const int MAX_CODE_LENGTH = 40;
StreamReader srTrainCorpus = null;
Dictionary<string, int> word2id;
List<vocab_word> vocab;
int vocab_size = 0;
long train_words = 0;
long word_count_actual = 0;
long next_save_step = 10000000;
long next_save_trained_words = 10000000;
long sentence_count = 0;
public long iter = 5;
public int layer1_size = 200;
public double starting_alpha = 0.025;
public double sample = 0;
public int min_count = 5;
public int num_threads = 1;
public int cbow = 1, window = 5;
public int classes = 1;
public int debug_mode = 0;
public long savestep = 100000000;
public int negative = 5;
public string strPreTrainedModelFileName = null;
public int onlyUpdateCorpusWord = 0;
double[] syn0;
double[] syn1;
double[] totalNeu_e;
double[] expTable;
object[] syn0Locker;
object[] syn1Locker;
Random rand = new Random(DateTime.Today.Millisecond);
public int[] accFreqTable;
public int accTotalFreq = 0;
public int accFactor = 1;
void InitAccTermFreq()
{
//Logger.WriteLine("Initializing acculumate term frequency...");
accFreqTable = new int[vocab_size];
accTotalFreq = 0;
//Keep accTotalFreq is less than int.MaxValue
accFactor = 1 + (int)(train_words / int.MaxValue);
//Logger.WriteLine("Acculumate factor: {0}", accFactor);
int i = 0;
foreach (vocab_word word in vocab)
{
accTotalFreq += (word.cnt / accFactor);
accFreqTable[i] = accTotalFreq;
i++;
}
//Logger.WriteLine("Acculumated total frequency : {0}", accTotalFreq);
}
int SearchAccTermTable(int freq)
{
int mid = vocab_size >> 1;
int left = 0, right = vocab_size - 1;
while (true)
{
if (accFreqTable[mid] < freq)
{
left = mid + 1;
}
else if (accFreqTable[mid] > freq)
{
if (mid == 0)
{
return 0;
}
if (accFreqTable[mid - 1] < freq)
{
return mid;
}
right = mid - 1;
}
else
{
return mid;
}
mid = (left + right) >> 1;
}
}
public Encoder()
{
word2id = new Dictionary<string, int>();
vocab = new List<vocab_word>();
expTable = new double[EXP_TABLE_SIZE + 1];
for (int i = 0; i < EXP_TABLE_SIZE; i++)
{
expTable[i] = Math.Exp((i / (double)EXP_TABLE_SIZE * 2 - 1) * MAX_EXP); // Precompute the exp() table
expTable[i] = expTable[i] / (expTable[i] + 1); // Precompute f(x) = x / (x + 1)
}
}
// Returns position of a word in the vocabulary; if the word is not found, returns -1
int SearchVocab(string word)
{
if (word2id.ContainsKey(word) == false)
{
return -1;
}
return word2id[word];
}
public class VocabComparer : IComparer<vocab_word>
{
public int Compare(vocab_word x, vocab_word y)
{
return y.cnt.CompareTo(x.cnt);
}
}
// Shrink the vocabulary by frequency using word counts
void ShrinkVocab()
{
// Sort the vocabulary
vocab.Sort(new VocabComparer());
word2id.Clear();
int size = vocab_size;
train_words = 0;
for (int a = 0; a < size; a++)
{
// Words occuring less than min_count times will be discarded from the vocab
if (vocab[a].cnt < min_count)
{
vocab_size--;
vocab[a].word = null;
}
else
{
word2id.Add(vocab[a].word, a);
train_words += vocab[a].cnt;
}
}
vocab.RemoveRange(vocab_size, vocab.Count - vocab_size);
}
void LoadPreTrainModelSyn(string strModelFileName, double[] syn)
{
Model preTrainedModel = new Model();
preTrainedModel.LoadModel(strModelFileName, false);
if (preTrainedModel.VectorSize != layer1_size)
{
throw new Exception("The layer size is inconsistent between given parameter and pre-trained model.");
}
string[] allTerms = preTrainedModel.GetAllTerms();
foreach (string strTerm in allTerms)
{
int wordId = SearchVocab(strTerm);
if (wordId < 0)
{
//Ingore the dropped term
continue;
}
float[] vector = preTrainedModel.GetVector(strTerm);
for (int i = 0; i < layer1_size;i++)
{
syn[i + wordId * layer1_size] = vector[i];
}
}
}
void LoadVocabFromPreTrainModel(string strModelFileName)
{
Model preTrainedModel = new Model();
preTrainedModel.LoadModel(strModelFileName, false);
layer1_size = preTrainedModel.VectorSize;
//Logger.WriteLine("Apply the following options from pr-trained model file {0}", preTrainedModel);
//Logger.WriteLine("Vector Size: {0}", layer1_size);
string[] allTerms = preTrainedModel.GetAllTerms();
foreach (string strTerm in allTerms)
{
//Add terms in pre-trained model into vocabulary
//If the term is already added from corpus or given dictionary, we ignore it
if (word2id.ContainsKey(strTerm) == false)
{
Term term = preTrainedModel.GetTerm(strTerm);
vocab_word word = new vocab_word();
word.word = strTerm;
word.cnt = 0;
word.source = WORD_SOURCE.PRETRAINED_MODEL;
word2id.Add(word.word, vocab_size);
vocab.Add(word);
vocab_size++;
}
}
}
public void LoadVocabFromFile(string vocab_file)
{
StreamReader sr = new StreamReader(vocab_file);
string strLine = null;
word2id = new Dictionary<string, int>();
vocab = new List<vocab_word>();
vocab_size = 0;
while ((strLine = sr.ReadLine()) != null)
{
string[] items = strLine.Split('\t');
vocab_word word = new vocab_word();
word.word = items[0];
word.source = WORD_SOURCE.CORPUS;
word2id.Add(word.word, vocab_size);
vocab.Add(word);
vocab_size++;
}
sr.Close();
}
private void GetTrainWordSize(string train_file)
{
StreamReader fin = new StreamReader(train_file);
string strLine = null;
train_words = 0;
foreach (vocab_word vw in vocab)
{
vw.cnt = 0;
}
while ((strLine = fin.ReadLine()) != null)
{
//Append the end of sentence
strLine = strLine.Trim();
string[] items = strLine.Split();
foreach (string item in items)
{
int wordId = SearchVocab(item);
if (wordId >= 0)
{
vocab[wordId].cnt++;
if (vocab[wordId].source == WORD_SOURCE.PRETRAINED_MODEL && onlyUpdateCorpusWord == 1)
{
continue;
}
train_words++;
if (debug_mode > 0 && train_words % 1000000 == 0)
{
//Logger.WriteLine("{0}M... ", train_words / 1000000);
}
}
}
}
fin.Close();
}
public void LearnVocabFromTrainFile(string train_file)
{
StreamReader fin = new StreamReader(train_file);
string strLine = null;
vocab_size = 0;
int i = 0;
while ((strLine = fin.ReadLine()) != null)
{
//Append the end of sentence
strLine = strLine.Trim();
string[] items = strLine.Split();
foreach (string word in items)
{
//This term is normal word
train_words++;
if (debug_mode > 0 && train_words % 1000000 == 0)
{
//Logger.WriteLine("{0}M... ", train_words / 1000000);
}
i = SearchVocab(word);
if (i == -1)
{
word2id.Add(word, vocab_size);
vocab_word voc_word = new vocab_word();
voc_word.word = word;
voc_word.cnt = 1;
voc_word.source = WORD_SOURCE.CORPUS;
vocab.Add(voc_word);
vocab_size++;
}
else
{
vocab[i].cnt++;
}
}
}
fin.Close();
}
public void SaveVocab(string save_vocab_file)
{
StreamWriter fo = new StreamWriter(save_vocab_file);
for (int i = 0; i < vocab_size; i++)
{
fo.WriteLine("{0}\t{1}", vocab[i].word, vocab[i].cnt);
}
fo.Close();
}
void InitNet()
{
syn0Locker = new object[vocab_size];
syn1Locker = new object[vocab_size];
for (int i = 0; i < vocab_size; i++)
{
syn0Locker[i] = new object();
syn1Locker[i] = new object();
}
totalNeu_e = new double[layer1_size];
syn0 = new double[vocab_size * layer1_size];
for (long b = 0; b < layer1_size; b++)
{
for (long a = 0; a < vocab_size; a++)
{
syn0[a * layer1_size + b] = (rand.NextDouble() - 0.5) / layer1_size;
}
}
syn1 = new double[vocab_size * layer1_size];
}
object rdlocker = new object();
object wrlocker = new object();
object locker_rand = new object();
int RandNext(int max)
{
lock (locker_rand)
{
return rand.Next(max);
}
}
double RandNextDouble()
{
lock (locker_rand)
{
return rand.NextDouble();
}
}
void TrainModelThread()
{
int word_count = 0, last_word_count = 0;
double alpha = starting_alpha * (1 - word_count_actual / (double)(iter * train_words + 1));
while (true)
{
if (word_count - last_word_count > 10000)
{
last_word_count = word_count;
if (debug_mode > 0)
{
double sumErr = 0;
for (int i = 0; i < layer1_size; i++)
{
sumErr += (totalNeu_e[i] / word_count_actual);
}
//Logger.WriteLine("Alpha: {0:0.0000} Prog: {1:0.00}% Words: {2}K Sent: {3}K Error: {4}", alpha,
//word_count_actual / (double)(iter * train_words + 1) * 100, word_count_actual / 1024, sentence_count / 1024, sumErr);
}
if (word_count_actual > next_save_trained_words)
{
long old_next_save_trained_words = next_save_trained_words;
lock (rdlocker)
{
if (old_next_save_trained_words == next_save_trained_words)
{
//Logger.WriteLine("Saving temporary word vector into file...");
Model.SaveModel("vector_tmp.bin", vocab_size, layer1_size, vocab, syn0);
Model.SaveModel("vector_tmp_bin.syn1", vocab_size, layer1_size, vocab, syn1);
next_save_trained_words += next_save_step;
}
}
}
}
alpha = starting_alpha * (1 - word_count_actual / (double)(iter * train_words + 1));
if (alpha < starting_alpha * 0.0001)
{
alpha = starting_alpha * 0.0001;
}
//Read a line from training corpus
string strLine = "";
lock (rdlocker)
{
strLine = srTrainCorpus.ReadLine();
}
if (strLine == null)
{
break;
}
Interlocked.Increment(ref sentence_count);
//Parse each word in current sentence
string[] strWords = strLine.Split();
bool bIgnore = true;
for (int i = 0; i < strWords.Length; i++)
{
int wordId = SearchVocab(strWords[i]);
if (wordId < 0)
{
continue;
}
if (vocab[wordId].source == WORD_SOURCE.CORPUS)
{
bIgnore = false;
break;
}
if (vocab[wordId].source == WORD_SOURCE.PRETRAINED_MODEL && onlyUpdateCorpusWord == 0)
{
bIgnore = false;
break;
}
}
if (bIgnore == true)
{
continue;
}
for (int sentence_position = 0; sentence_position < strWords.Length; sentence_position++)
{
string strPredictedWord = strWords[sentence_position];
int word = SearchVocab(strPredictedWord);
if (word < 0)
{
//Ingore the dropped term
continue;
}
if (vocab[word].source == WORD_SOURCE.CORPUS ||
(vocab[word].source == WORD_SOURCE.PRETRAINED_MODEL && onlyUpdateCorpusWord == 0))
{
word_count++;
Interlocked.Increment(ref word_count_actual);
}
int rnd_window = RandNext(window);
if (cbow != 0)
{
TrainByCBOW(sentence_position, rnd_window, strWords, alpha, word);
}
else
{
TrainBySkipGram(sentence_position, rnd_window, strWords, alpha, word);
}
}
}
}
private void TrainBySkipGram(int sentence_position, int b, string[] sen, double alpha, int word)
{
double[] neu1e = new double[layer1_size];
//train skip-gram
for (int a = b; a < window * 2 + 1 - b; a++)
{
int c = sentence_position - window + a;
if (c < 0 || c >= sen.Length || c == sentence_position)
{
//Invalidated position. out of sentence boundary
continue;
}
string strNGram = sen[c];
int wordId = SearchVocab(strNGram);
if (wordId == -1)
{
continue;
}
int l1 = wordId * layer1_size;
for (c = 0; c < layer1_size; c++)
{
neu1e[c] = 0;
}
lock (syn0Locker[wordId])
{
//Negative sampling
int target = 0;
int label = 1;
for (int d = 0; d < negative + 1; d++)
{
if (d == 0)
{
target = word;
label = 1;
}
else
{
target = SearchAccTermTable(RandNext(accTotalFreq));
if (target == word)
{
continue;
}
label = 0;
}
long l2 = target * layer1_size;
double f = 0;
double g;
lock (syn1Locker[target])
{
for (c = 0; c < layer1_size; c++)
{
f += syn0[c + l1] * syn1[c + l2];
}
if (f > MAX_EXP)
{
g = (label - 1) * alpha;
}
else if (f < -MAX_EXP)
{
g = (label - 0) * alpha;
}
else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
for (c = 0; c < layer1_size; c++)
{
neu1e[c] += g * syn1[c + l2];
}
if (onlyUpdateCorpusWord == 1 && vocab[target].source == WORD_SOURCE.PRETRAINED_MODEL)
{
continue;
}
for (c = 0; c < layer1_size; c++)
{
syn1[c + l2] += g * syn0[c + l1];
}
}
}
if (onlyUpdateCorpusWord == 1 && vocab[wordId].source == WORD_SOURCE.PRETRAINED_MODEL)
{
continue;
}
// Learn weights input -> hidden
for (c = 0; c < layer1_size; c++)
{
syn0[c + l1] += neu1e[c];
}
}
for (int i = 0; i < neu1e.Length; i++)
{
totalNeu_e[i] += Math.Abs(neu1e[i] / (window * 2 + 1 - b * 2));
}
}
}
private void TrainByCBOW(int sentence_position, int b, string[] sen, double alpha, int word)
{
double[] neu1 = new double[layer1_size];
double[] neu1e = new double[layer1_size];
int cw = 0;
List<int> wordIdList = new List<int>();
//train the cbow architecture
// in -> hidden
for (int a = b; a < window * 2 + 1 - b; a++)
{
int c = sentence_position - window + a;
if (c < 0 || c >= sen.Length || c == sentence_position)
{
//Invalidated position. out of sentence boundary
continue;
}
//Generate ngram string and word id
string strNGram = null;
int wordId = -1;
strNGram = sen[c];
wordId = SearchVocab(strNGram);
if (wordId < 0)
{
//Ingore the dropped term
continue;
}
//The subsampling randomly discards frequent words while keeping the ranking same
if (sample > 0)
{
double ran = (Math.Sqrt(vocab[wordId].cnt / (sample * train_words)) + 1) * (sample * train_words) / vocab[wordId].cnt;
if (ran < RandNextDouble())
{
continue;
}
}
if (onlyUpdateCorpusWord == 0 || (onlyUpdateCorpusWord == 1 && vocab[wordId].source == WORD_SOURCE.CORPUS))
{
//Terms that need to update their syn0
wordIdList.Add(wordId);
}
lock (syn0Locker[wordId])
{
for (int t = 0; t < layer1_size; t++)
{
neu1[t] += syn0[t + wordId * layer1_size];
}
}
cw++;
}
if (wordIdList.Count == 0)
{
//No term need to update its syn, return
return;
}
double synUpdateFactor = (double)(cw) / (double)(wordIdList.Count);
for (int c = 0; c < layer1_size; c++)
{
neu1[c] /= cw;
}
int target = 0;
int label = 1;
for (int d = 0; d < negative + 1; d++)
{
if (d == 0)
{
target = word;
label = 1;
}
else
{
target = SearchAccTermTable(RandNext(accTotalFreq));
if (target == word)
{
continue;
}
label = 0;
}
long l2 = target * layer1_size;
double f = 0;
lock (syn1Locker[target])
{
for (int c = 0; c < layer1_size; c++)
{
f += neu1[c] * syn1[c + l2];
}
double g = 0;
if (f > MAX_EXP) g = (label - 1) * alpha;
else if (f < -MAX_EXP) g = (label - 0) * alpha;
else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;
for (int c = 0; c < layer1_size; c++)
{
neu1e[c] += g * syn1[c + l2];
}
if (onlyUpdateCorpusWord == 1 && vocab[target].source == WORD_SOURCE.PRETRAINED_MODEL)
{
continue;
}
for (int c = 0; c < layer1_size; c++)
{
syn1[c + l2] += g * neu1[c];
}
}
}
// hidden -> in
foreach (int wordId in wordIdList)
{
lock (syn0Locker[wordId])
{
for (int c = 0; c < layer1_size; c++)
{
syn0[c + wordId * layer1_size] += (neu1e[c] * synUpdateFactor);
}
}
}
for (int i = 0; i < neu1e.Length; i++)
{
totalNeu_e[i] += Math.Abs(neu1e[i]);
}
}
public void TrainModel(string train_file, string output_file, string vocab_file)
{
if (debug_mode > 0)
{
//Logger.WriteLine("Starting training using file {0}", train_file);
}
if ((vocab_file != null && File.Exists(vocab_file) == true) ||
strPreTrainedModelFileName != null)
{
if (vocab_file != null && File.Exists(vocab_file) == true)
{
//Logger.WriteLine("Loading vocabulary {0} from file...", vocab_file);
LoadVocabFromFile(vocab_file);
}
if (strPreTrainedModelFileName != null)
{
//Logger.WriteLine("Load vocabulary from pre-trained model file {0}", strPreTrainedModelFileName);
LoadVocabFromPreTrainModel(strPreTrainedModelFileName);
}
//Vocaburary is loaded from given dict, then we need to calculate how many words need to be train
//Logger.WriteLine("Calculating how many words need to be train...");
GetTrainWordSize(train_file);
//Logger.WriteLine("Total training words : {0}", train_words);
}
else
{
//We have no input vocabulary, so we get vocabulary from training corpus
//Logger.WriteLine("Generate vocabulary from training corpus {0}...", train_file);
LearnVocabFromTrainFile(train_file);
}
//filter out words which frequenct is lower
ShrinkVocab();
//If vocabulary is specified in parameter list, but not existed in folder, we need to create it
if (vocab_file != null && vocab_file.Length > 0 && File.Exists(vocab_file) == false)
{
if (debug_mode > 0)
{
//Logger.WriteLine("Saving vocabulary into file...");
}
SaveVocab(vocab_file);
}
next_save_step = savestep;
next_save_trained_words = next_save_step;
if (output_file == null)
{
//Logger.WriteLine("No specified output file name");
return;
}
//Initialize neural network
InitNet();
//Generate word's frequency distribution for negative samping
InitAccTermFreq();
//Load pre-trained model syn0
if (strPreTrainedModelFileName != null)
{
//Logger.WriteLine("Loading syn0 from pre-trained model...");
LoadPreTrainModelSyn(strPreTrainedModelFileName, syn0);
//Logger.WriteLine("Loading syn1 from pre-trained model...");
LoadPreTrainModelSyn(strPreTrainedModelFileName + ".syn1", syn1);
}
if (File.Exists(train_file) == true)
{
string strCurTrainFile = train_file;
for (int j = 0; j < iter; j++)
{
totalNeu_e = new double[layer1_size];
//Logger.WriteLine("Starting training iteration {0}/{1}...", j + 1, iter);
srTrainCorpus = new StreamReader(strCurTrainFile, Encoding.UTF8, true, 102400000);
List<Thread> threadList = new List<Thread>();
for (int i = 0; i < num_threads; i++)
{
Thread thread = new Thread(new ThreadStart(TrainModelThread));
thread.Start();
threadList.Add(thread);
}
//Wait all threads finish their jobs
for (int i = 0; i < num_threads; i++)
{
threadList[i].Join();
}
srTrainCorpus.Close();
double sumErr = 0;
for (int i = 0; i < layer1_size; i++)
{
sumErr += (totalNeu_e[i] / word_count_actual);
}
//Logger.WriteLine("Error: {0}", sumErr);
}
}
else
{
//Logger.WriteLine("Train train file isn't existed.");
return;
}
Model.SaveModel(output_file, vocab_size, layer1_size, vocab, syn0);
Model.SaveModel(output_file + ".syn1", vocab_size, layer1_size, vocab, syn1);
}
}
}