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 word2id; List 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(); vocab = new List(); 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 { 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(); vocab = new List(); 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 wordIdList = new List(); //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 threadList = new List(); 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); } } }