From cd2b4cf394506cdaa8fba4cc1e165228f03839b0 Mon Sep 17 00:00:00 2001 From: Alexis Nasr <alexis.nasr@lif.univ-mrs.fr> Date: Wed, 29 Jun 2016 14:31:22 -0400 Subject: [PATCH] added maca_crf_tagger --- CMakeLists.txt | 1 + maca_crf_tagger/CMakeLists.txt | 24 ++ .../src/apply_template_crfsuite.cc | 72 ++++ maca_crf_tagger/src/crf_barebones_decoder.cc | 90 ++++ maca_crf_tagger/src/crf_binlexicon.hh | 365 ++++++++++++++++ maca_crf_tagger/src/crf_binmodel.hh | 406 ++++++++++++++++++ maca_crf_tagger/src/crf_decoder.hh | 147 +++++++ maca_crf_tagger/src/crf_features.hh | 100 +++++ maca_crf_tagger/src/crf_lexicon.hh | 128 ++++++ maca_crf_tagger/src/crf_model.hh | 170 ++++++++ maca_crf_tagger/src/crf_tagger | Bin 0 -> 100190 bytes maca_crf_tagger/src/crf_tagger.cc | 60 +++ maca_crf_tagger/src/crf_template.hh | 146 +++++++ maca_crf_tagger/src/crf_utils.hh | 75 ++++ maca_crf_tagger/src/lemmatizer.cc | 19 + maca_crf_tagger/src/lemmatizer.h | 51 +++ .../src/maca_crf_convert_binlexicon.cc | 46 ++ .../src/maca_crf_convert_binmodel.cc | 23 + maca_crf_tagger/src/maca_crf_tagger_main.cc | 259 +++++++++++ maca_crf_tagger/src/maca_crf_tagger_utils.cc | 31 ++ maca_crf_tagger/src/simple_tagger.cc | 21 + maca_crf_tagger/src/simple_tagger.hh | 40 ++ maca_crf_tagger/src/test_simple_tagger.cc | 27 ++ maca_crf_tagger/src/utf8 | Bin 0 -> 8718 bytes maca_crf_tagger/src/utf8.c | 33 ++ 25 files changed, 2334 insertions(+) create mode 100644 maca_crf_tagger/CMakeLists.txt create mode 100644 maca_crf_tagger/src/apply_template_crfsuite.cc create mode 100644 maca_crf_tagger/src/crf_barebones_decoder.cc create mode 100644 maca_crf_tagger/src/crf_binlexicon.hh create mode 100644 maca_crf_tagger/src/crf_binmodel.hh create mode 100644 maca_crf_tagger/src/crf_decoder.hh create mode 100644 maca_crf_tagger/src/crf_features.hh create mode 100644 maca_crf_tagger/src/crf_lexicon.hh create mode 100644 maca_crf_tagger/src/crf_model.hh create mode 100755 maca_crf_tagger/src/crf_tagger create mode 100644 maca_crf_tagger/src/crf_tagger.cc create mode 100644 maca_crf_tagger/src/crf_template.hh create mode 100644 maca_crf_tagger/src/crf_utils.hh create mode 100644 maca_crf_tagger/src/lemmatizer.cc create mode 100644 maca_crf_tagger/src/lemmatizer.h create mode 100644 maca_crf_tagger/src/maca_crf_convert_binlexicon.cc create mode 100644 maca_crf_tagger/src/maca_crf_convert_binmodel.cc create mode 100644 maca_crf_tagger/src/maca_crf_tagger_main.cc create mode 100644 maca_crf_tagger/src/maca_crf_tagger_utils.cc create mode 100644 maca_crf_tagger/src/simple_tagger.cc create mode 100644 maca_crf_tagger/src/simple_tagger.hh create mode 100644 maca_crf_tagger/src/test_simple_tagger.cc create mode 100755 maca_crf_tagger/src/utf8 create mode 100644 maca_crf_tagger/src/utf8.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 85d4e4b..a94a43c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,5 +8,6 @@ include_directories(maca_common/include) add_subdirectory(maca_common) add_subdirectory(maca_lemmatizer) add_subdirectory(maca_trans_parser) +add_subdirectory(maca_crf_tagger) #set(CMAKE_INSTALL_PREFIX ../) diff --git a/maca_crf_tagger/CMakeLists.txt b/maca_crf_tagger/CMakeLists.txt new file mode 100644 index 0000000..6df7df6 --- /dev/null +++ b/maca_crf_tagger/CMakeLists.txt @@ -0,0 +1,24 @@ +include_directories(src) + +#compiling, linking and installing executables + +add_executable(crf_barebones_decoder ./src/crf_barebones_decoder.cc) +target_compile_options(crf_barebones_decoder PRIVATE -std=c++11) +install (TARGETS crf_barebones_decoder DESTINATION bin) + +#add_executable(test_simple_tagger ./src/test_simple_tagger.cc) +#target_compile_options(test_simple_tagger PRIVATE -std=c++11) +#install (TARGETS test_simple_tagger DESTINATION bin) + +add_executable(apply_template_crfsuite ./src/apply_template_crfsuite.cc) +target_compile_options(apply_template_crfsuite PRIVATE -std=c++11) +install (TARGETS apply_template_crfsuite DESTINATION bin) + +add_executable(maca_crf_convert_binmodel ./src/maca_crf_convert_binmodel.cc) +target_compile_options(maca_crf_convert_binmodel PRIVATE -std=c++11) +install (TARGETS maca_crf_convert_binmodel DESTINATION bin) + +add_executable(maca_crf_convert_binlexicon ./src/maca_crf_convert_binlexicon.cc) +target_compile_options(maca_crf_convert_binlexicon PRIVATE -std=c++11) +install (TARGETS maca_crf_convert_binlexicon DESTINATION bin) + diff --git a/maca_crf_tagger/src/apply_template_crfsuite.cc b/maca_crf_tagger/src/apply_template_crfsuite.cc new file mode 100644 index 0000000..eba51ba --- /dev/null +++ b/maca_crf_tagger/src/apply_template_crfsuite.cc @@ -0,0 +1,72 @@ +#include <string> +#include <vector> +#include <iostream> +#include <fstream> +#include "crf_template.hh" + +// http://www.oopweb.com/CPP/Documents/CPPHOWTO/Volume/C++Programming-HOWTO-7.html +static void tokenize(const std::string& str, std::vector<std::string>& tokens, const std::string& delimiters = " ") +{ + std::string::size_type lastPos = str.find_first_not_of(delimiters, 0); + std::string::size_type pos = str.find_first_of(delimiters, lastPos); + while (std::string::npos != pos || std::string::npos != lastPos) + { + tokens.push_back(str.substr(lastPos, pos - lastPos)); + lastPos = str.find_first_not_of(delimiters, pos); + pos = str.find_first_of(delimiters, lastPos); + } +} + +static void replace(std::string& str, const std::string &search, const std::string &replacement) { + std::string::size_type pos = 0; + while ((pos = str.find(search, pos)) != std::string::npos) { + str.replace(pos, search.size(), replacement); + pos += replacement.size(); + } +} + +int main(int argc, char** argv) { + if(argc != 2) { + std::cerr << "usage: cat <input> | " << argv[0] << " <template>\n"; + return 1; + } + std::vector<macaon::CRFPPTemplate> templates; + std::ifstream templateFile(argv[1]); + while(!templateFile.eof()) { + std::string line; + std::getline(templateFile, line); + if(templateFile.eof()) break; + macaon::CRFPPTemplate current(line.c_str()); + if(current.type != macaon::CRFPPTemplate::BIGRAM) templates.push_back(current); + //std::cerr << templates.back() << std::endl; + } + std::vector<std::vector<std::string> > lines; + while(!std::cin.eof()) { + std::string line; + std::getline(std::cin, line); + if(std::cin.eof()) break; + std::vector<std::string> tokens; + tokenize(line, tokens, " \t"); + if(tokens.size() == 0) { + for(int position = 0; position < (int) lines.size(); position++) { + std::string label = lines[position][lines[position].size() - 1]; + replace(label, "\\", "\\\\"); + replace(label, ":", "\\:"); + std::cout << label; + for(std::vector<macaon::CRFPPTemplate>::const_iterator i = templates.begin(); i != templates.end(); i++) { + std::string feature = i->apply(lines, position); + replace(feature, "\\", "\\\\"); + replace(feature, ":", "\\:"); + std::cout << "\t" << feature; + } + /*if(position == 0) std::cout << "\t__BOS__"; + if(position == (int) lines.size() - 1) std::cout << "\t__EOS__";*/ + std::cout << std::endl; + } + std::cout << std::endl; + lines.clear(); + } else { + lines.push_back(tokens); + } + } +} diff --git a/maca_crf_tagger/src/crf_barebones_decoder.cc b/maca_crf_tagger/src/crf_barebones_decoder.cc new file mode 100644 index 0000000..229868f --- /dev/null +++ b/maca_crf_tagger/src/crf_barebones_decoder.cc @@ -0,0 +1,90 @@ +#include <vector> +#include "crf_decoder.hh" +#include "crf_binlexicon.hh" +#include "crf_features.hh" + +/* This is a sample decoder for the crf tagger. + compile with: + g++ -O3 -Wall -o barebones_decoder barebones_decoder.cc + + example usage: + echo -e "I\nam\nyour\nfather\n\njhon\neats\npotatoes\n" | ./barebones_decoder en/bin/crf_tagger.model.bin en/bin/crf_tagger.wordtag.lexicon + */ + +void tag_sentence(macaon::Decoder& decoder, macaon::BinaryLexicon* lexicon, const std::vector<std::vector<std::string> >& lines, int wordField, bool isConll07) { + + std::vector<std::vector<std::string> > features; + for(size_t i = 0; i < lines.size(); i++) { + std::vector<std::string> word_features; + macaon::FeatureGenerator::get_pos_features(lines[i][wordField], word_features); + features.push_back(word_features); + //for(size_t j = 0; j < word_features.size(); j++) std::cout << word_features[j] << " "; + //std::cout << "\n"; + } + std::vector<std::string> tagged; + decoder.decodeString(features, tagged, lexicon); + for(size_t i = 0; i < tagged.size(); i++) { + if(isConll07) { + for(size_t j = 0; j < lines[i].size(); j++) { + if(j != 0) std::cout << "\t"; + if(j == 3 || j == 4) std::cout << tagged[i]; + else std::cout << lines[i][j]; + } + std::cout << "\n"; + } else { + std::cout << lines[i][wordField] << "\t" << tagged[i] << "\n"; + } + } + std::cout << "\n"; +} + +void usage(const char* argv0) { + std::cerr << "usage: " << argv0 << " [--conll07] <model> [lexicon]\n"; + exit(1); +} + +int main(int argc, char** argv) { + bool isConll07 = false; // warning: no verification of conll07 format + int word_offset = 0; + std::string modelName = ""; + std::string lexiconName = ""; + + for(int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if(arg == "-h" || arg == "--help") { + usage(argv[0]); + } else if(arg == "--conll07") { + isConll07 = true; + word_offset = 1; + } else if(modelName == "") { + modelName = arg; + } else if(lexiconName =="") { + lexiconName = arg; + } else { + usage(argv[0]); + } + } + if(modelName == "") usage(argv[0]); + + macaon::Decoder decoder(modelName); + macaon::BinaryLexicon *lexicon = NULL; + if(lexiconName != "") lexicon = new macaon::BinaryLexicon(lexiconName, decoder.getTagset()); + + std::string line; + std::vector<std::vector<std::string> > lines; + while(std::getline(std::cin, line)) { + if(line == "") { + tag_sentence(decoder, lexicon, lines, word_offset, isConll07); + lines.clear(); + } else { + std::vector<std::string> tokens; + macaon::Tokenize(line, tokens, "\t"); + lines.push_back(tokens); + } + } + if(!lines.empty()) { + tag_sentence(decoder, lexicon, lines, word_offset, isConll07); + } + if(lexicon) delete lexicon; + return 0; +} diff --git a/maca_crf_tagger/src/crf_binlexicon.hh b/maca_crf_tagger/src/crf_binlexicon.hh new file mode 100644 index 0000000..8294860 --- /dev/null +++ b/maca_crf_tagger/src/crf_binlexicon.hh @@ -0,0 +1,365 @@ +#pragma once + +#include <stdio.h> +#include <string.h> +#include <stdint.h> +#include "crf_model.hh" +#include "crf_template.hh" +#include "crf_lexicon.hh" + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> + +#include <limits.h> +#ifdef CHAR_BIT +#if CHAR_BIT != 8 +#error CHAR_BIT != 8 not supported +#endif +#endif + +namespace macaon { + const uint32_t lexiconMagic = 0xbffe1253; + + class BinaryLexicon : public Lexicon { + // disable alignment in MSVC++ +#pragma pack(push, 1) + struct ModelInfo { + uint32_t magic; + uint32_t dataLocation; + uint32_t tableSize; + uint32_t numLabels; + } __attribute__((packed)); + + struct TableElement { + uint32_t hashValue; + uint8_t keySize; + uint8_t dataSize; + uint32_t location; + } __attribute__((packed)); // disable alignment in g++ +#define lexicon_tag_t uint8_t +#define sizeof_LexiconTableElement (sizeof(uint32_t) + sizeof(uint8_t) + sizeof(uint8_t) + sizeof(uint32_t)) + +#pragma pack(pop) + + private: + bool isBinary; + int fd; + const char* data; + size_t dataLength; + const ModelInfo* info; + const TableElement* table; + + // copied from https://smhasher.googlecode.com/svn-history/r136/trunk/MurmurHash3.cpp (MIT license) + + static inline uint32_t rotl32 ( uint32_t x, int8_t r ) + { + return (x << r) | (x >> (32 - r)); + } + + static inline uint64_t rotl64 ( uint64_t x, int8_t r ) + { + return (x << r) | (x >> (64 - r)); + } + +#define ROTL32(x,y) rotl32(x,y) +#define ROTL64(x,y) rotl64(x,y) + +#define BIG_CONSTANT(x) (x##LLU) +#define FORCE_INLINE inline + + static FORCE_INLINE uint32_t getblock ( const uint32_t * p, int i ) + { + return p[i]; + } + + static FORCE_INLINE uint64_t getblock ( const uint64_t * p, int i ) + { + return p[i]; + } + + static FORCE_INLINE uint32_t fmix ( uint32_t h ) + { + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + + return h; + } + + //---------- + + static FORCE_INLINE uint64_t fmix ( uint64_t k ) + { + k ^= k >> 33; + k *= BIG_CONSTANT(0xff51afd7ed558ccd); + k ^= k >> 33; + k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); + k ^= k >> 33; + + return k; + } + + static void MurmurHash3_x86_32 ( const void * key, int len, uint32_t seed, void * out ) + { + const uint8_t * data = (const uint8_t*)key; + const int nblocks = len / 4; + + uint32_t h1 = seed; + + uint32_t c1 = 0xcc9e2d51; + uint32_t c2 = 0x1b873593; + + //---------- + // body + + const uint32_t * blocks = (const uint32_t *)(data + nblocks*4); + + for(int i = -nblocks; i; i++) + { + uint32_t k1 = getblock(blocks,i); + + k1 *= c1; + k1 = ROTL32(k1,15); + k1 *= c2; + + h1 ^= k1; + h1 = ROTL32(h1,13); + h1 = h1*5+0xe6546b64; + } + + //---------- + // tail + + const uint8_t * tail = (const uint8_t*)(data + nblocks*4); + + uint32_t k1 = 0; + + switch(len & 3) + { + case 3: k1 ^= tail[2] << 16; + case 2: k1 ^= tail[1] << 8; + case 1: k1 ^= tail[0]; + k1 *= c1; k1 = ROTL32(k1,15); k1 *= c2; h1 ^= k1; + //std::cerr << k1 << " " << h1 << " " << seed << "\n"; + }; + + //---------- + // finalization + + h1 ^= len; + + h1 = fmix(h1); + + *(uint32_t*)out = h1; + } + + static uint32_t Hash(const char *k, size_t length) { + uint32_t output = 0; + MurmurHash3_x86_32(k, length, BinaryModelConstants::magic, &output); + + // java hash function + /*uint32_t output = 0; + for(size_t i = 0; i < length; i++) { + output = 31 * output + k[i]; + }*/ + return output; + } + + public: + BinaryLexicon() : Lexicon(), isBinary(false) {} + BinaryLexicon(const std::string &filename, Symbols* _tagSymbols = NULL) : Lexicon(), isBinary(false), fd(-1), data((const char*) MAP_FAILED) { + tagSymbols = _tagSymbols; + Load(filename); + } + + ~BinaryLexicon() { + if(data != MAP_FAILED) munmap((void*) data, dataLength); + if(fd != -1) close(fd); + } + + bool Convert(const std::string& from, const std::string& to) { + std::cerr << "loading\n"; + Lexicon::Load(from); + std::cerr << "writing\n"; + return Write(to); + } + + bool Write(const std::string & filename) { + FILE* output = fopen(filename.c_str(), "w"); + // magic + fwrite(&lexiconMagic, sizeof(lexiconMagic), 1, output); // magic + + // features + uint32_t dataLocation = 0; + uint32_t dataLocationOffset = (uint32_t) ftell(output); + fwrite(&dataLocation, sizeof(dataLocation), 1, output); + uint32_t tableSize = (uint32_t) wordSymbols.NumSymbols() * 2; + fwrite(&tableSize, sizeof(tableSize), 1, output); + uint32_t numLabels = tagsForWord[kUnknownWordTags].size(); + fwrite(&numLabels, sizeof(numLabels), 1, output); + + // create table + TableElement* table = (TableElement*) malloc(sizeof(TableElement) * tableSize); + memset(table, 0, sizeof(TableElement) * tableSize); + + // write entries + int num = 0; + int totalNumCollisions = 0; + int numTags = 0; + int sizeOfKeys = 0; + for(SymbolsIterator siter(wordSymbols); !siter.Done(); siter.Next()) { + std::string word = siter.Symbol(); + if(tagsForWordEntry.find(siter.Value()) == tagsForWordEntry.end()) { + continue; + } + int64 id = tagsForWordEntry[siter.Value()]; + + num++; + TableElement element; + element.hashValue = Hash(word.c_str(), word.length()) % tableSize; + element.keySize = (uint8_t) word.length(); + element.dataSize = tagsForWord[id].size(); + numTags += element.dataSize; + element.location = (uint32_t) ftell(output); + fwrite(word.c_str(), element.keySize, 1, output); + sizeOfKeys += element.keySize; + for(size_t tag = 0; tag < tagsForWord[id].size(); tag++) { + lexicon_tag_t packed = (lexicon_tag_t) tagsForWord[id][tag]; + fwrite(&packed, sizeof(packed), 1, output); + } + if(element.dataSize > 0) { + uint32_t hash = element.hashValue % tableSize; + int numCollisions = 0; + while(table[hash].location != 0) { + numCollisions++; + hash = (hash + 1) % tableSize; + } + totalNumCollisions += numCollisions; + table[hash] = element; + } + } + std::cerr << "avg collisions: " << 1.0 * totalNumCollisions / (double) wordSymbols.NumSymbols() << "\n"; + std::cerr << "sizeof (keys) = " << sizeOfKeys << "\n"; + std::cerr << "sizeof (tags) = " << sizeof(lexicon_tag_t) << " * " << numTags << "\n"; + std::cerr << "sizeof (entry in table) = " << sizeof_LexiconTableElement << " * " << tableSize << "\n"; + + // write table + dataLocation = (uint32_t) ftell(output); + for(uint32_t i = 0; i < tableSize; i++) { + fwrite(&table[i], sizeof(table[i]), 1, output); + } + free(table); + + // set feature locations + fseek(output, dataLocationOffset, SEEK_SET); + fwrite(&dataLocation, sizeof(dataLocation), 1, output); + + fclose(output); + return true; + } + + bool Load(const std::string& filename) { + isBinary = false; + + struct stat sb; + fd = open(filename.c_str(), O_RDONLY); + if(fd == -1) { + std::cerr << "ERROR: could not open crf lexicon \"" << filename << "\"\n"; + return false; + } + if (fstat(fd, &sb) == -1) { + std::cerr << "ERROR: could not fstat crf lexicon \"" << filename << "\"\n"; + return false; + } + dataLength = sb.st_size; + data = (const char*) mmap(NULL, dataLength, PROT_READ, MAP_PRIVATE, fd, 0); + if(data == MAP_FAILED) { + perror("mmap"); + std::cerr << "ERROR: could mmap() crf lexicon \"" << filename << "\"\n"; + return false; + } + + info = (const ModelInfo*) data; + + // read magic + if(info->magic != lexiconMagic) { + bool result = Lexicon::Load(filename); + if(result == false) { + std::cerr << "ERROR: invalid magic or unsupported version in binary crf lexicon. Please reconvert it from text model.\n"; + } + return result; + } + + // read table + table = (const TableElement*) &data[info->dataLocation]; + + loaded = true; + isBinary = true; + return true; + } + + bool GetTagsForWord(int64 word, std::vector<int64>& output) const { + if(!isBinary) { + return Lexicon::GetTagsForWord(word, output); + } + std::cerr << "ERROR: GetTagsForWord() not supported on binary models\n"; + abort(); + return false; + } + + int NumLabels() const { + if(!isBinary) return Lexicon::NumLabels(); + return info->numLabels; + } + + bool GetTagsForWord(const std::string& word, std::vector<int64>& output) const { + if(!isBinary) { + return Lexicon::GetTagsForWord(word, output); + //std::cerr << "ERROR: called GetTagsForWord() on a non binary model\n"; + //return false; + } + if(word == "<eps>") { + output.clear(); + output.push_back(0); + } + size_t keySize = word.length(); + uint32_t hashValue = Hash(word.c_str(), keySize); // % info->tableSize; + uint32_t offset = 0; + while(offset < info->tableSize) { + uint32_t location = (hashValue + offset) % info->tableSize; + const TableElement& element = table[location]; + /*std::cerr << word << " " << location << " " << hashValue << " " << offset << " " << info->tableSize << + "|" << element.hashValue << " " << (int) element.keySize << " " << (int) element.dataSize << " " << element.location << + "\n";*/ + if(element.location == 0) break; + if(element.keySize == keySize) { + char key[keySize + 1]; + strncpy(key, &data[element.location], keySize); + key[keySize] = '\0'; + if(std::string(key) == word) { + //std::cerr << "h:" << element.hashValue << " k:" << element.keySize << " d:" << element.dataSize << "\n"; + output.clear(); + const lexicon_tag_t* tags = (const lexicon_tag_t*) &data[element.location + keySize]; + for(int i = 0; i < element.dataSize; i++) { + output.push_back(tags[i]); + } + return true; + } + } + offset++; + } + // unknown word + output.clear(); + for(int i = 1; i < (int) info->numLabels + 1; i++) output.push_back(i); + return false; + } + + }; +} diff --git a/maca_crf_tagger/src/crf_binmodel.hh b/maca_crf_tagger/src/crf_binmodel.hh new file mode 100644 index 0000000..33deb53 --- /dev/null +++ b/maca_crf_tagger/src/crf_binmodel.hh @@ -0,0 +1,406 @@ +#pragma once + +#include <stdio.h> +#include <string.h> +#include <stdint.h> +#include "crf_model.hh" +#include "crf_template.hh" + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> + +#include <limits.h> +#ifdef CHAR_BIT +#if CHAR_BIT != 8 +#error CHAR_BIT != 8 not supported +#endif +#endif + +namespace macaon { +// disable alignment in MSVC++ +#pragma pack(push, 1) + struct ModelInfo { + uint32_t magic; + uint32_t templateLocation; + uint32_t numTemplates; + uint32_t labelLocation; + uint32_t numLabels; + uint32_t featureLocation; + uint32_t tableSize; + } __attribute__((packed)); + + struct TableElement { + uint32_t hashValue; + uint16_t keySize; + uint16_t dataSize; + uint32_t location; + } __attribute__((packed)); // disable alignment in g++ +#define sizeof_TableElement (sizeof(uint32_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t)) + + struct LabelWeight { + uint16_t label; + float weight; + } __attribute__((packed)); +#define sizeof_LabelWeight (sizeof(uint16_t) + sizeof(float)) + + struct LabelPairWeight { + uint16_t previous; + uint16_t label; + float weight; + } __attribute__((packed)); +#define sizeof_LabelPairWeight (sizeof(uint16_t) + sizeof(uint16_t) + sizeof(float)) +#pragma pack(pop) + + namespace BinaryModelConstants { + const uint32_t magic = 0x132a0ab5; + } + + class BinaryModel : public CRFModel { + private: + bool isBinary; + int fd; + const char* data; + size_t dataLength; + const ModelInfo* info; + const TableElement* table; + std::vector<double> B_weights; // cache for B template + + // java hash function + uint32_t Hash(const char *k, size_t length) { + uint32_t output = 0; + for(size_t i = 0; i < length; i++) { + output = 31 * output + k[i]; + } + return output; + } + + public: + BinaryModel() : CRFModel(), isBinary(false) {} + BinaryModel(const std::string &filename) : CRFModel(), isBinary(false), fd(-1), data((const char*) MAP_FAILED) { + Load(filename); + } + + ~BinaryModel() { + if(data != MAP_FAILED) munmap((void*) data, dataLength); + if(fd != -1) close(fd); + } + + bool Convert(const std::string& from, const std::string& to) { + std::cerr << "loading\n"; + CRFModel::Load(from); + std::cerr << "writing\n"; + return Write(to); + } + + // trimming is already performed when writing the bin model + void TrimModel() { + std::unordered_map<std::string, int> newFeatures; + for(std::unordered_map<std::string, int>::const_iterator feature = features.begin(); feature != features.end(); feature++) { + if(feature->first[0] != 'B') { + int numNonNull = 0; + for(size_t i = 0; i < labels.size(); i++) { + float weight = weights[feature->second + i]; + if(weight != 0) numNonNull++; + } + if(numNonNull > 0) { + newFeatures[feature->first] = feature->second; + } + } else { + newFeatures[feature->first] = feature->second; + } + } + std::cerr << "trim: " << features.size() << " -> " << newFeatures.size() << "\n"; + features = newFeatures; + } + + bool Write(const std::string & filename) { + FILE* output = fopen(filename.c_str(), "w"); + // magic + fwrite(&BinaryModelConstants::magic, sizeof(BinaryModelConstants::magic), 1, output); // magic + + // templates + uint32_t templateLocation = 0; + uint32_t templateLocationOffset = (uint32_t) ftell(output); + fwrite(&templateLocation, sizeof(templateLocation), 1, output); + uint32_t numTemplates = (uint32_t) templates.size(); + fwrite(&numTemplates, sizeof(numTemplates), 1, output); + + // labels + uint32_t labelLocation = 0; + uint32_t labelLocationOffset = (uint32_t) ftell(output); + fwrite(&labelLocation, sizeof(labelLocation), 1, output); + uint32_t numLabels = (uint32_t) labels.size(); + fwrite(&numLabels, sizeof(numLabels), 1, output); + + // features + uint32_t featureLocation = 0; + uint32_t featureLocationOffset = (uint32_t) ftell(output); + fwrite(&featureLocation, sizeof(featureLocation), 1, output); + uint32_t tableSize = (uint32_t) features.size() * 3; + fwrite(&tableSize, sizeof(tableSize), 1, output); + + // create table + TableElement* table = (TableElement*) malloc(sizeof(TableElement) * tableSize); + memset(table, 0, sizeof(TableElement) * tableSize); + + // write templates + templateLocation = (uint32_t) ftell(output); + for(std::vector<CRFPPTemplate>::const_iterator i = templates.begin(); i != templates.end(); i++) { + fprintf(output, "%s\n", i->text.c_str()); + } + + // write labels + std::vector<std::string> labelVector(labels.size()); + for(std::unordered_map<std::string, int>::const_iterator label = labels.begin(); label != labels.end(); label++) { + labelVector[label->second] = label->first; + } + labelLocation = (uint32_t) ftell(output); + for(size_t i = 0; i < labelVector.size(); i++) { + fprintf(output, "%s\n", labelVector[i].c_str()); + } + + // write weights + int num = 0; + int totalNumCollisions = 0; + int numUnigram = 0; + int numBigram = 0; + for(std::unordered_map<std::string, int>::const_iterator feature = features.begin(); feature != features.end(); feature++) { + num++; + TableElement element; + element.hashValue = Hash(feature->first.c_str(), feature->first.length()) % tableSize; + element.keySize = (uint16_t) feature->first.length(); + element.dataSize = 0; + element.location = (uint32_t) ftell(output); + fwrite(feature->first.c_str(), element.keySize, 1, output); + if(feature->first[0] == 'B') { + for(uint16_t label = 0; label < numLabels; label++) { + for(uint16_t previous = 0; previous < numLabels; previous++) { + float weight = weights[feature->second + label + numLabels * previous]; + if(weight != 0) { + LabelPairWeight item; + item.previous = previous; + item.label = label; + item.weight = weight; + fwrite(&item, sizeof(item), 1, output); + element.dataSize ++; + } + } + } + numBigram++; + } else { + for(uint16_t label = 0; label < numLabels; label++) { + float weight = weights[feature->second + label]; + if(weight != 0) { + LabelWeight item; + item.label = label; + item.weight = weight; + fwrite(&item, sizeof(item), 1, output); + element.dataSize ++; + } + } + numUnigram++; + } + if(element.dataSize > 0) { + uint32_t hash = element.hashValue % tableSize; + int numCollisions = 0; + while(table[hash].location != 0) { + numCollisions++; + hash = (hash + 1) % tableSize; + } + totalNumCollisions += numCollisions; + //std::cout << element.hashValue << " " << feature->first << "\n"; + table[hash] = element; + } + } + std::cerr << "avg collisions: " << 1.0 * totalNumCollisions / (double) features.size() << "\n"; + std::cerr << "sizeof (label+weight) = " << sizeof_LabelWeight << " * " << numUnigram << "\n"; + std::cerr << "sizeof (label+label+weight) = " << sizeof_LabelPairWeight << " * " << numBigram << "\n"; + std::cerr << "sizeof (entry in table) = " << sizeof_TableElement << " * " << tableSize << "\n"; + + // write table + featureLocation = (uint32_t) ftell(output); + for(uint32_t i = 0; i < tableSize; i++) { + fwrite(&table[i], sizeof(table[i]), 1, output); + } + free(table); + + // set section locations + fseek(output, templateLocationOffset, SEEK_SET); + fwrite(&templateLocation, sizeof(templateLocation), 1, output); + + // set label locations + fseek(output, labelLocationOffset, SEEK_SET); + fwrite(&labelLocation, sizeof(labelLocation), 1, output); + + // set feature locations + fseek(output, featureLocationOffset, SEEK_SET); + fwrite(&featureLocation, sizeof(featureLocation), 1, output); + + fclose(output); + return true; + } + + bool Load(const std::string& filename) { + isBinary = false; + + struct stat sb; + fd = open(filename.c_str(), O_RDONLY); + if(fd == -1) { + std::cerr << "ERROR: could not open crf model \"" << filename << "\"\n"; + return false; + } + if (fstat(fd, &sb) == -1) { + std::cerr << "ERROR: could not fstat crf model \"" << filename << "\"\n"; + return false; + } + dataLength = sb.st_size; + data = (const char*) mmap(NULL, dataLength, PROT_READ, MAP_PRIVATE, fd, 0); + if(data == MAP_FAILED) { + perror("mmap"); + std::cerr << "ERROR: could mmap() crf model \"" << filename << "\"\n"; + return false; + } + name = filename; + + info = (const ModelInfo*) data; + + // read magic + if(info->magic != BinaryModelConstants::magic) { + //std::cerr << "WARNING: binary crf model format not recognized, trying text model\n"; + return CRFModel::Load(filename); + } + + size_t lineSize = 0; + + // read templates + templates.clear(); + const char* line = (const char*) &data[info->templateLocation]; + for(size_t i = 0; i < info->numTemplates; i++) { + lineSize = strchr(line, '\n') - line; + char content[lineSize + 1]; + strncpy(content, line, lineSize); + content[lineSize] = '\0'; + //std::cerr << "TEMPLATE[" << content << "]\n"; + templates.push_back(CRFPPTemplate(content)); + line += lineSize + 1; + } + + // read labels + labels.clear(); + reverseLabels.clear(); + line = (const char*) &data[info->labelLocation]; + for(uint32_t i = 0; i < info->numLabels; i++) { + lineSize = strchr(line, '\n') - line; + char content[lineSize + 1]; + strncpy(content, line, lineSize); + content[lineSize] = '\0'; + //std::cerr << "LABEL[" << content << "]\n"; + labels[std::string(content)] = (int) i; + reverseLabels.push_back(std::string(content)); + line += lineSize + 1; + } + + // read table + table = (const TableElement*) &data[info->featureLocation]; + + ComputeWindowOffset(); + loaded = true; + isBinary = true; + GetWeights("B", B_weights); + return true; + } + + bool GetWeights(const std::string& feature, std::vector<double>& output) { + if(!isBinary) { + std::cerr << "ERROR: called GetWeights() on a non binary model\n"; + return false; + } + size_t keySize = feature.length(); + uint32_t hashValue = Hash(feature.c_str(), keySize) % info->tableSize; + uint32_t offset = 0; + size_t numLabels = labels.size(); + while(offset < info->tableSize) { + uint32_t location = (hashValue + offset) % info->tableSize; + const TableElement& element = table[location]; + if(element.location == 0) return false; + if(element.keySize == keySize) { + char key[keySize + 1]; + strncpy(key, &data[element.location], keySize); + key[keySize] = '\0'; + if(std::string(key) == feature) { + //std::cerr << "h:" << element.hashValue << " k:" << element.keySize << " d:" << element.dataSize << "\n"; + if(feature[0] == 'B') { + output.assign(numLabels * numLabels, 0); + const LabelPairWeight* items = (const LabelPairWeight*) &data[element.location + keySize]; + for(int i = 0; i < element.dataSize; i++) { + output[items[i].label + numLabels * items[i].previous] = items[i].weight; + } + } else { + output.assign(numLabels, 0); + const LabelWeight* items = (const LabelWeight*) &data[element.location + keySize]; + for(int i = 0; i < element.dataSize; i++) { + output[items[i].label] = items[i].weight; + } + } + return true; + } + } + offset++; + } + return false; + } + + /* note: this function can use bigram templates conditionned on observations */ + double rescore(const std::vector<std::vector<std::string> > &input, const std::vector<int> &context, const std::vector<int> &context_tags) { + if(!isBinary) { + return CRFModel::rescore(input, context, context_tags); + } + double output = 0; + if((int) context.size() != window_length) return 0; + if(context[window_offset] < 0) return 0; + const int label = context_tags[window_offset]; //ilabels[input[context[window_offset]][input[context[window_offset]].size() - 1]]; + int previous = -1; + if(window_length > 1 && context[window_offset - 1] >=0) previous = context_tags[window_offset - 1]; + for(std::vector<CRFPPTemplate>::const_iterator i = templates.begin(); i != templates.end(); i++) { + std::string feature = i->applyToClique(input, context, window_offset); + std::vector<double> feature_weights; + if(GetWeights(feature, feature_weights)) { + if(i->type == CRFPPTemplate::UNIGRAM) output += feature_weights[label]; + else if(previous != -1) output += feature_weights[label + labels.size() * previous]; + } + } + return output; + } + + /* note: this function CANNOT use bigram templates conditionned on observations */ + double transition(int previous, int label) { + if(!isBinary) return CRFModel::transition(previous, label); + return B_weights[label + info->numLabels * previous]; + } + + void emissions(const std::vector<std::vector<std::string> > &input, const std::vector<int> &context, std::vector<double>& output) { + if(!isBinary) { + CRFModel::emissions(input, context, output); + return; + } + output.assign(labels.size(), 0); + if((int) context.size() != window_length) return; + if(context[window_offset] == -1) return; + for(std::vector<CRFPPTemplate>::const_iterator i = templates.begin(); i != templates.end(); i++) { + if(i->type == CRFPPTemplate::UNIGRAM) { + std::string feature = i->applyToClique(input, context, window_offset); + std::vector<double> feature_weights; + if(GetWeights(feature, feature_weights)) { + for(size_t label = 0; label < labels.size(); label++) + output[label] += feature_weights[label]; + } + } + } + } + }; +} diff --git a/maca_crf_tagger/src/crf_decoder.hh b/maca_crf_tagger/src/crf_decoder.hh new file mode 100644 index 0000000..0c89aeb --- /dev/null +++ b/maca_crf_tagger/src/crf_decoder.hh @@ -0,0 +1,147 @@ +#pragma once +#include <list> +#ifdef __APPLE__ +#include "../../../third_party/unordered_map/unordered_map.hpp" +#else +#include <unordered_map> +#endif +#include "crf_binmodel.hh" +#include "crf_utils.hh" +#include "crf_binlexicon.hh" + +namespace macaon { + + struct Decoder { + + //CRFModel model; + BinaryModel model; + Symbols tagSet; + + Decoder() : tagSet("tagset") { } + Decoder(const std::string &filename) : tagSet("tagset") { + model.Load(filename); + tagSet.AddSymbol("<eps>", 0); + for(std::unordered_map<std::string, int>::const_iterator label = model.labels.begin(); label != model.labels.end(); label++) { + tagSet.AddSymbol(label->first, label->second + 1); + } + } + + Symbols* getTagset() { + return &tagSet; + } + + bool IsLoaded() const { + return model.IsLoaded(); + } + + /* Faster decoder for simple sequences. + * This function supports an optional lexicon to specify allowed word/tags. And attional option sets the location of the word in the feature vector. + * */ + void decodeString(const std::vector<std::vector<std::string> > &features, std::vector<std::string> &predictions, const BinaryLexicon* lexicon=NULL, int wordFeatureLocation=0) { + int length = features.size(); + int numLabels = model.labels.size(); + + /*if(lexicon->NumLabels() != numLabels) { + std::cerr << "ERROR: num label mismatch between model and lexicon\n"; + return; + }*/ + + // store score and backtrack matrices (TODO: size matrices according to possible word/tag assoc) + std::vector<std::vector<double> > scores(length, std::vector<double>(numLabels, 0.0)); + std::vector<std::vector<int> > backtrack(length, std::vector<int>(numLabels, 0)); + /*double** scores = new double*[length]; + int** backtrack = new int*[length]; + + for(int i = 0; i < length; i++) { + scores[i] = new double[numLabels]; + backtrack[i] = new int[numLabels]; + for(int j = 0; j < numLabels; j++) { + backtrack[i][j] = -1; + scores[i][j] = 0.0; + } + }*/ + + // possible tags for each word: use lexicon if provided + std::vector<std::vector<int64> > wordTags(length); + std::vector<int64> allTags(numLabels); + for(int label = 0; label < numLabels; label++) allTags[label] = label + 1; // warning: there is an offset of one for epsilon transitions + + // perform viterbi search for the maximum scoring labeling + for(int current = 0; current < length; current++) { + // honor lexicon or allow all tags + if(lexicon != NULL) lexicon->GetTagsForWord(features[current][wordFeatureLocation], wordTags[current]); + else wordTags[current] = allTags; + + // create context vector (offset of features for current word) + std::vector<int> context(model.window_length); + for(int i = 0; i < model.window_length; i++) + if(current + i - model.window_offset >= 0 && current + i - model.window_offset < length) context[i] = (current + i - model.window_offset); + else context[i] = -1; + + // compute emissions and find highest scoring transition pair + if(current == 0) { + // TODO: compute emissions only for valid word/tags pairs + std::vector<double> emissions; + model.emissions(features, context, emissions); + for(int e = 0; e < numLabels; e++) scores[current][e] = emissions[e]; + } else { + std::vector<double> emissions; + model.emissions(features, context, emissions); + for(int e = 0; e < numLabels; e++) scores[current][e] = emissions[e]; + for(size_t i = 0; i < wordTags[current].size(); i++) { + int label = wordTags[current][i] - 1; + if(label < 0 || label >= numLabels) { + std::cerr << "ERROR: unexpected label (" << label << ") from lexicon, please check that it is compatible with model.\n"; + return; + } + double max = 0; + int argmax = -1; + for(size_t j = 0; j < wordTags[current - 1].size(); j++) { + int previous = wordTags[current - 1][j] - 1; + if(previous < 0 || previous >= numLabels) { + std::cerr << "ERROR: unexpected label (" << previous << ") from lexicon, please check that it is compatible with model.\n"; + return; + } + double score = scores[current][label] + scores[current - 1][previous] + model.transition(previous, label); + if(argmax == -1 || max < score) { + max = score; + argmax = previous; + } + } + scores[current][label] = max; + backtrack[current][label] = argmax; + } + } + } + // find last label + double max = 0; + int argmax = -1; + if(length > 0) { + for(size_t i = 0; i < wordTags[length - 1].size(); i++) { + int label = wordTags[length - 1][i] - 1; + if(argmax == -1 || scores[length - 1][label] > max) { + max = scores[length - 1][label]; + argmax = label; + } + } + } + + // backtrack solution + int current = length - 1; + predictions.clear(); + predictions.resize(length); + while(current >= 0) { + predictions[current] = model.reverseLabels[argmax]; + argmax = backtrack[current][argmax]; + current --; + } + + /*for(int i = 0; i < length; i++) { + delete scores[i]; + delete backtrack[i]; + } + delete scores; + delete backtrack;*/ + } + }; +} diff --git a/maca_crf_tagger/src/crf_features.hh b/maca_crf_tagger/src/crf_features.hh new file mode 100644 index 0000000..917a395 --- /dev/null +++ b/maca_crf_tagger/src/crf_features.hh @@ -0,0 +1,100 @@ +#pragma once +#include <vector> +#include <string> + +namespace macaon { + class FeatureGenerator { + static void prefixesUtf8(const std::string &word, int n, std::vector<std::string> &output) { + size_t offset = 0; + while(offset < word.length() && n > 0) { + if((unsigned char)word[offset] >> 7 == 1) { // 1xxxxxxx (length of utf8 character) + offset++; + while(offset < word.length() && (unsigned char)word[offset] >> 6 == 2) { // 10xxxxxx (continuation of character) + offset++; + } + } else { + offset++; + } + + output.push_back(word.substr(0, offset)); + n--; + } + while(n > 0) { + output.push_back("__nil__"); + n--; + } + } + + static void suffixesUtf8(const std::string &word, int n, std::vector<std::string> &output) { + std::vector<int> char_starts; + size_t offset = 0; + while(offset < word.length()) { + char_starts.push_back(offset); + if((unsigned char)word[offset] >> 7 == 1) { // 1xxxxxxx (length of utf8 character) + offset++; + while(offset < word.length() && (unsigned char)word[offset] >> 6 == 2) { // 10xxxxxx (continuation of character) + offset++; + } + } else { + offset++; + } + } + for(int i = char_starts.size() - 1; i > 0 && n > 0; i--) { + //std::cerr << "s=[" << word.substr(offsets[i]) << "]\n"; + output.push_back(word.substr(char_starts[i])); + n--; + } + while(n > 0) { + output.push_back("__nil__"); + n--; + } + } + + + static void prefixes(const std::string &word, int n, std::vector<std::string> &output) { + int length = word.length(); + for(int i = 1; i <= n; i++) { + if(length >= i) output.push_back(word.substr(0, i)); + else output.push_back("__nil__"); + } + } + static void suffixes(const std::string &word, int n, std::vector<std::string> &output) { + int length = word.length(); + for(int i = 1; i <= n; i++) { + if(length >= i) output.push_back(word.substr(length - i, i)); + else output.push_back("__nil__"); + } + } + static void wordClasses(const std::string &word, std::vector<std::string> &output) { + bool containsNumber = false; + bool containsSymbol = false; + for(int i = 0; i < (int) word.length(); i++) { + if(!containsNumber && word.at(i) >= '0' && word.at(i) <= '9') containsNumber = true; + if(!containsSymbol && !((word.at(i) >= '0' && word.at(i) <= '9') || (word.at(i) >= 'a' && word.at(i) <= 'z') || (word.at(i) >= 'A' && word.at(i) <= 'Z'))) containsSymbol = true; + } + if(containsNumber) output.push_back("Y"); + else output.push_back("N"); + if(word.length() >= 2 && word.at(0) >= 'A' && word.at(0) <= 'Z' && word.at(1) >= 'a' && word.at(1) <= 'z') output.push_back("Y"); + else output.push_back("N"); + if(containsSymbol) output.push_back("Y"); + else output.push_back("N"); + } + public: + static void get_pos_features(const std::string &word, std::vector<std::string> &output, bool utf8=true) { + output.push_back(word); + wordClasses(word, output); + if(utf8) { + prefixesUtf8(word, 4, output); + suffixesUtf8(word, 4, output); + } else { + prefixes(word, 4, output); + suffixes(word, 4, output); + } + } + static std::vector<std::string> get_pos_features(const std::string &word, bool utf8=true) { + std::vector<std::string> output; + get_pos_features(word, output, utf8); + return output; + } + }; +} diff --git a/maca_crf_tagger/src/crf_lexicon.hh b/maca_crf_tagger/src/crf_lexicon.hh new file mode 100644 index 0000000..5182c1f --- /dev/null +++ b/maca_crf_tagger/src/crf_lexicon.hh @@ -0,0 +1,128 @@ +#pragma once + +#include <iostream> +#include <fstream> +#include <string> +#include <stdint.h> +#ifdef __APPLE__ +#include "../../../third_party/unordered_map/unordered_map.hpp" +#else +#include <unordered_map> +#endif +#include "crf_utils.hh" + +namespace macaon { + const int kEpsilonTags = 0; + const int kUnknownWordTags = 1; + + class Lexicon { + protected: + bool loaded; + + Symbols wordSymbols; + Symbols* tagSymbols; + + std::vector<std::vector<int64> > tagsForWord; + std::unordered_map<int64, int> tagsForWordEntry; + + + public: + Lexicon() : loaded(false), wordSymbols("words"), tagSymbols(NULL) { + wordSymbols.AddSymbol("<eps>", 0); + } + + Lexicon(const std::string& filename, Symbols* _tagSymbols) : loaded(false), wordSymbols("words"), tagSymbols(_tagSymbols) { + wordSymbols.AddSymbol("<eps>", 0); + Load(filename); + } + + virtual ~Lexicon() { + } + + bool NumLabels() const { + return tagSymbols->NumSymbols() - 1; // account for epsilon + } + + bool Load(const std::string &filename) { + tagsForWord.push_back(std::vector<int64>()); // keep space for epsilon tags + tagsForWord.push_back(std::vector<int64>()); // keep space for unk word tags + loaded = false; + std::unordered_map<std::string, int> known; + std::ifstream input(filename.c_str()); + if(!input.is_open()) { + std::cerr << "ERROR: could not open " << filename << " in Lexicon::Load()" << std::endl; + return false; + } + while(!input.eof()) { + std::string line; + std::getline(input, line); + if(input.eof()) break; + std::string word; + std::string::size_type end_of_word = line.find('\t'); + if(end_of_word == std::string::npos) { + return false; + } + word = line.substr(0, end_of_word); + int64 wordId = wordSymbols.AddSymbol(word); + std::string signature = line.substr(end_of_word + 1); + std::unordered_map<std::string, int>::const_iterator found = known.find(signature); + if(found == known.end()) { + int id = tagsForWord.size(); + known[signature] = id; + tagsForWordEntry[wordId] = id; + std::vector<std::string> tokens; + Tokenize(signature, tokens, "\t"); + std::vector<int64> tagset; + for(std::vector<std::string>::const_iterator i = tokens.begin(); i != tokens.end(); i++) { + int64 tagId = tagSymbols->Find(*i); + if(tagId != -1) tagset.push_back(tagId); + } + tagsForWord.push_back(tagset); + } else { + tagsForWordEntry[wordId] = found->second; + } + } + tagsForWord[kEpsilonTags].push_back(0); // epsilon + for(SymbolsIterator siter(*tagSymbols); !siter.Done(); siter.Next()) { // unknown word + if(siter.Value() != 0) tagsForWord[kUnknownWordTags].push_back(siter.Value()); + } + loaded = true; + return loaded; + } + + virtual bool GetTagsForWord(const std::string& word, std::vector<int64>& output) const { + return GetTagsForWord(wordSymbols.Find(word), output); + } + + virtual bool GetTagsForWord(int64 word, std::vector<int64>& output) const { + if(!IsLoaded()) { + std::cerr << "ERROR: Lexicon::GetTagsForWord(" << wordSymbols.Find(word) << ") called on empty lexicon" << std::endl; + return false; + } + if(word == -1) { + output = tagsForWord[kUnknownWordTags]; + return true; + } + if(word == 0) { + output = tagsForWord[kEpsilonTags]; + return true; + } + std::unordered_map<int64, int>::const_iterator found = tagsForWordEntry.find(word); + if(found == tagsForWordEntry.end()) { + output = tagsForWord[kUnknownWordTags]; + } else { + if(tagsForWord[found->second].size() == 0) { + std::cerr << "WARNING: inconsistancy between word/tag lexicon and model, word no " << word << " has no tags => treat as unknown word\n"; + output = tagsForWord[kUnknownWordTags]; + } + output = tagsForWord[found->second]; + } + return true; + } + + bool IsLoaded() const { + return loaded; + } + + }; +} diff --git a/maca_crf_tagger/src/crf_model.hh b/maca_crf_tagger/src/crf_model.hh new file mode 100644 index 0000000..6547460 --- /dev/null +++ b/maca_crf_tagger/src/crf_model.hh @@ -0,0 +1,170 @@ +#pragma once +#include <string> +#include <vector> +#ifdef __APPLE__ +#include "../../../third_party/unordered_map/unordered_map.hpp" +#else +#include <unordered_map> +#endif +#include <stdio.h> +#include <errno.h> +#include "crf_template.hh" + +namespace macaon { + class CRFModel { + protected: + std::string name; + std::vector<CRFPPTemplate> templates; + int version; + double cost_factor; + int maxid; + int xsize; + std::unordered_map<std::string, int> features; + std::vector<float> weights; + bool loaded; + int bigramWeightLocation; + public: + std::unordered_map<std::string, int> labels; + std::vector<std::string> reverseLabels; + int window_offset; + int window_length; + CRFModel() : loaded(false) {} + CRFModel(const std::string &filename) : loaded(false) { Load(filename); } + + bool Load(const std::string &filename) { + name = filename; + FILE* fp = fopen(filename.c_str(), "r"); + if(!fp) { + fprintf(stderr, "ERROR: %s, %s\n", filename.c_str(), strerror(errno)); + return false; + } + char line[1024]; + int section = 0; + int header_num = 0; + int line_num = 0; + int num_non_null = 0; + while(NULL != fgets(line, 1024, fp)) { + line_num ++; + if(line[0] == '\n') { + section ++; + } else { + line[1023] = '\0'; + line[strlen(line) - 1] = '\0'; // chomp + if(section == 0) { // header + char* space = line; + while(*space != ' ' && *space != '\0') space ++; + if(header_num == 0) version = strtol(space + 1, NULL, 10); + else if(header_num == 1) cost_factor = strtod(space + 1, NULL); + else if(header_num == 2) maxid = strtol(space + 1, NULL, 10); + else if(header_num == 3) xsize = strtol(space + 1, NULL, 10); + else { + fprintf(stderr, "ERROR: unexpected header line %d in %s\n", line_num, filename.c_str()); + fclose(fp); + return false; + } + header_num ++; + } else if (section == 1) { // labels + int next_id = labels.size(); + labels[std::string(line)] = next_id; + reverseLabels.push_back(std::string(line)); + } else if (section == 2) { // templates + templates.push_back(CRFPPTemplate(line)); + } else if (section == 3) { // feature indexes + char* space = line; + while(*space != ' ' && *space != '\0') space ++; + *space = '\0'; + int index = strtol(line, NULL, 10); + features[std::string(space + 1)] = index; + } else if (section == 4) { // weights + float weight = (float) strtod(line, NULL); + if(weight != 0) num_non_null++; + weights.push_back(weight); + } else { + fprintf(stderr, "ERROR: too many sections in %s\n", filename.c_str()); + fclose(fp); + return false; + } + } + } + //std::cerr << "weights: " << num_non_null << "/" << weights.size() << "\n"; + fclose(fp); + + ComputeWindowOffset(); + + std::unordered_map<std::string, int>::const_iterator found = features.find("B"); + if(found != features.end()) { + bigramWeightLocation = found->second; + } + loaded = true; + return true; + } + + void ComputeWindowOffset() { + int max_template_offset = 0; + int min_template_offset = 9; + for(std::vector<CRFPPTemplate>::const_iterator i = templates.begin(); i != templates.end(); i++) { + if(i->type == CRFPPTemplate::BIGRAM && min_template_offset > -1) min_template_offset = -1; // account for label bigram + for(std::vector<TemplateItem>::const_iterator j = i->items.begin(); j != i->items.end(); j++) { + if(j->line < min_template_offset) min_template_offset = j->line; + if(j->line > max_template_offset) max_template_offset = j->line; + } + } + window_offset = - min_template_offset; + window_length = max_template_offset - min_template_offset + 1; + } + + bool IsLoaded() const { + return loaded; + } + + /* note: this function can use bigram templates conditionned on observations */ + virtual double rescore(const std::vector<std::vector<std::string> > &input, const std::vector<int> &context, const std::vector<int> &context_tags) { + double output = 0; + if((int) context.size() != window_length) return 0; + //std::cerr << context[window_offset] << std::endl; + if(context[window_offset] < 0) return 0; + const int label = context_tags[window_offset]; //ilabels[input[context[window_offset]][input[context[window_offset]].size() - 1]]; + int previous = -1; + if(window_length > 1 && context[window_offset - 1] >=0) previous = context_tags[window_offset - 1]; //labels[input[context[window_offset - 1]][input[context[window_offset - 1]].size() - 1]]; + for(std::vector<CRFPPTemplate>::const_iterator i = templates.begin(); i != templates.end(); i++) { + std::string feature = i->applyToClique(input, context, window_offset); + //std::cerr << "feature: " << feature << std::endl; + std::unordered_map<std::string, int>::const_iterator found = features.find(feature); + if(found != features.end()) { + if(found->second >= 0 && found->second < (int) weights.size()) { + if(i->type == CRFPPTemplate::UNIGRAM) output += weights[found->second + label]; + else if(previous != -1) output += weights[found->second + label + labels.size() * previous]; + } + } + } + return output; + } + + /* note: this function CANNOT use bigram templates conditionned on observations */ + virtual double transition(int previous, int label) { + if(bigramWeightLocation < 0) return 0; + return weights[bigramWeightLocation + label + labels.size() * previous]; + } + + virtual void emissions(const std::vector<std::vector<std::string> > &input, const std::vector<int> &context, std::vector<double>& output) { + output.clear(); + output.resize(labels.size()); + if((int) context.size() != window_length) return; + if(context[window_offset] == -1) return; + for(std::vector<CRFPPTemplate>::const_iterator i = templates.begin(); i != templates.end(); i++) { + std::string feature = i->applyToClique(input, context, window_offset); + //std::cerr << " " << feature; + std::unordered_map<std::string, int>::const_iterator found = features.find(feature); + if(found != features.end()) { + if(found->second >= 0 && found->second < (int) weights.size()) { + if(i->type == CRFPPTemplate::UNIGRAM) + for(size_t label = 0; label < labels.size(); label++) + output[label] += weights[found->second + label]; + } + } + //else std::cerr << "*"; + } + //std::cerr << "\n"; + } + }; +} diff --git a/maca_crf_tagger/src/crf_tagger b/maca_crf_tagger/src/crf_tagger new file mode 100755 index 0000000000000000000000000000000000000000..48867b39b0ef4bffed7927ba5b51a0cbe81cb1e2 GIT binary patch literal 100190 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 literal 0 HcmV?d00001 diff --git a/maca_crf_tagger/src/crf_tagger.cc b/maca_crf_tagger/src/crf_tagger.cc new file mode 100644 index 0000000..a9fba5a --- /dev/null +++ b/maca_crf_tagger/src/crf_tagger.cc @@ -0,0 +1,60 @@ +#include <vector> +#include "crf_decoder.hh" +#include "crf_binlexicon.hh" +#include "crf_features.hh" + +void tag_sentence(macaon::Decoder& decoder, macaon::BinaryLexicon* lexicon, const std::vector<std::string>& words) { + + std::vector<std::vector<std::string> > features; + for(size_t i = 0; i < words.size(); i++) { + std::vector<std::string> word_features; + macaon::FeatureGenerator::get_pos_features(words[i], word_features); + features.push_back(word_features); + /*for(size_t j = 0; j < word_features.size(); j++) std::cout << word_features[j] << " "; + std::cout << "\n";*/ + } + std::vector<std::string> tagged; + decoder.decodeString(features, tagged, lexicon); + for(size_t i = 0; i < tagged.size(); i++) { + if(i > 0) std::cout << " "; + std::cout << words[i] << "/" << tagged[i]; + } + std::cout << "\n"; +} + +void usage(const char* argv0) { + std::cerr << "usage: " << argv0 << " <model> [lexicon]\n"; + exit(1); +} + +int main(int argc, char** argv) { + std::string modelName = ""; + std::string lexiconName = ""; + + for(int i = 1; i < argc; i++) { + std::string arg = argv[i]; + if(arg == "-h" || arg == "--help") { + usage(argv[0]); + } else if(modelName == "") { + modelName = arg; + } else if(lexiconName =="") { + lexiconName = arg; + } else { + usage(argv[0]); + } + } + if(modelName == "") usage(argv[0]); + + macaon::Decoder decoder(modelName); + macaon::BinaryLexicon *lexicon = NULL; + if(lexiconName != "") lexicon = new macaon::BinaryLexicon(lexiconName, decoder.getTagset()); + + std::string line; + while(std::getline(std::cin, line)) { + std::vector<std::string> words; + macaon::Tokenize(line, words, " "); + tag_sentence(decoder, lexicon, words); + } + if(lexicon) delete lexicon; + return 0; +} diff --git a/maca_crf_tagger/src/crf_template.hh b/maca_crf_tagger/src/crf_template.hh new file mode 100644 index 0000000..7307f14 --- /dev/null +++ b/maca_crf_tagger/src/crf_template.hh @@ -0,0 +1,146 @@ +#pragma once +#include <vector> +#include <string> +#include <iostream> +#include <sstream> +#include <string.h> +#include <stdlib.h> +#include <algorithm> + +namespace macaon { + // from http://www.jb.man.ac.uk/~slowe/cpp/itoa.html + static std::string number_to_string(const int value) { + const int base = 10; + std::string buf; + buf.reserve(35); + int quotient = value; + do { + buf += "0123456789abcdef"[ abs( quotient % base ) ]; + quotient /= base; + } while (quotient); + if (value < 0) buf += '-'; + reverse( buf.begin(), buf.end() ); + return buf; + } + + struct TemplateItem { + int line; + int column; + std::string prefix; + TemplateItem(const int _line, const int _column, const std::string &_prefix) : line(_line), column(_column), prefix(_prefix) { } + //friend std::ostream &operator<<(std::ostream &, const TemplateItem & ); + }; + + struct CRFPPTemplate { + enum TemplateType { + UNIGRAM, + BIGRAM, + }; + std::string text; + TemplateType type; + int size; + std::string suffix; + std::vector<TemplateItem> items; + CRFPPTemplate() {} + CRFPPTemplate(const char* input) { read(input); } + friend std::ostream &operator<<(std::ostream &, const CRFPPTemplate & ); + + std::string apply(const std::vector<std::vector<std::string> > &clique, int offset) const { + std::ostringstream output; + for(std::vector<TemplateItem>::const_iterator i = items.begin(); i != items.end(); i++) { + output << i->prefix; + int column = i->column; + int line = i->line + offset; + if(line >= 0 && line < (int) clique.size()) { + if(column >= 0 && column < (int) clique[line].size()) { + output << clique[line][column]; + } else { + std::cerr << "ERROR: invalid column " << column << " in template \"" << text << "\"\n"; + return ""; + } + } else { + output << "_B"; + output << number_to_string(line); + } + } + output << suffix; + return output.str(); + } + + std::string applyToClique(const std::vector<std::vector<std::string> > &features, const std::vector<int> &clique, int offset) const { + std::string output; + for(std::vector<TemplateItem>::const_iterator i = items.begin(); i != items.end(); i++) { + output += i->prefix; + int column = i->column; + int line = i->line; + if(line + offset >= 0 && line + offset < (int) clique.size() && clique[line + offset] >=0) { + if(column >= 0 && column < (int) features[clique[line + offset]].size()) { + output += features[clique[line + offset]][column]; + } else { + std::cerr << "ERROR: invalid column " << column << " in template \"" << text << "\"\n"; + return ""; + } + } else { + output += "_B"; + output += number_to_string(line); + } + } + output += suffix; + return output; + } + + void read(const char* input) { + text = input; + size = 0; + const char* current = input; + const char* gap_start = NULL, *gap_end = NULL, *line_start = NULL, *column_start = NULL; + int state = 0; + gap_start = current; + /* template is a succession of %x[-?\d+,\d+] which must be replaced by corresponding + * features at the given line, column relative to the current example. + * They are parsed with a rudimentary state machine, and stored in the template. + */ + if(*current == 'U') type = UNIGRAM; + else if(*current == 'B') type = BIGRAM; + else { + std::cerr << "ERROR: unexpected template type \"" << input << "\"\n"; + return; + } + while(*current != '\0') { + if(state == 0 && *current == '%') { state ++; gap_end = current; } + else if(state == 1 && *current == 'x') { state ++; } + else if(state == 2 && *current == '[') state ++; + else if(state == 3 && (*current == '-' || (*current >= '0' && *current <= '9'))) { state ++; line_start = current; } + else if(state == 4 && (*current >= '0' && *current <= '9')); + else if(state == 4 && *current == ',') { state ++; } + else if(state == 5 && (*current >= '0' && *current <= '9')) { state ++; column_start = current; } + else if(state == 6 && (*current >= '0' && *current <= '9')); + else if(state == 6 && *current == ']') { + state = 0; + std::string gap = std::string(gap_start, gap_end - gap_start); + int column = strtol(column_start, NULL, 10); + int line = strtol(line_start, NULL, 10); + items.push_back(TemplateItem(line, column, gap)); + size++; + gap_start = current + 1; + } else state = 0; + current ++; + } + suffix = gap_start; // add trailing text + } + }; + + /*std::ostream &operator<<(std::ostream &output, const macaon::TemplateItem &item) { + output << item.prefix << "%x[" << item.line << "," << item.column << "]"; + return output; + } + + std::ostream &operator<<(std::ostream &output, const macaon::CRFPPTemplate &featureTemplate) { + for(std::vector<macaon::TemplateItem>::const_iterator i = featureTemplate.items.begin(); i != featureTemplate.items.end(); i++) { + output << (*i); + } + output << featureTemplate.suffix; + return output; + }*/ + +} diff --git a/maca_crf_tagger/src/crf_utils.hh b/maca_crf_tagger/src/crf_utils.hh new file mode 100644 index 0000000..8a33ab7 --- /dev/null +++ b/maca_crf_tagger/src/crf_utils.hh @@ -0,0 +1,75 @@ +#pragma once + +#include <string> +#include <vector> + +#define int64 int + +namespace macaon { + class Symbols { + protected: + std::string name; + std::unordered_map<std::string, int> word2int; + std::unordered_map<int, std::string> int2word; + public: + Symbols(std::string _name) : name(_name) {} + int AddSymbol(const std::string& symbol, int value = -1) { + if(value == -1) value = word2int.size(); + word2int[symbol] = value; + int2word[value] = symbol; + return value; + } + int Find(const std::string& word) const { + std::unordered_map<std::string, int>::const_iterator found = word2int.find(word); + if(found != word2int.end()) return found->second; + return -1; + } + const std::string Find(const int64 id) const { + std::unordered_map<int, std::string>::const_iterator found = int2word.find(id); + if(found != int2word.end()) return found->second; + return ""; + } + int NumSymbols() const { + return word2int.size(); + } + friend class SymbolsIterator; + }; + class SymbolsIterator { + const Symbols& symbols; + std::unordered_map<std::string, int>::const_iterator iter; + public: + SymbolsIterator(const Symbols& _symbols) : symbols(_symbols) { + iter = symbols.word2int.begin(); + } + bool Done() { + return iter == symbols.word2int.end(); + } + void Next() { + iter++; + } + const std::string Symbol() { + return iter->first; + } + int Value() { + return iter->second; + } + }; + + // http://www.oopweb.com/CPP/Documents/CPPHOWTO/Volume/C++Programming-HOWTO-7.html + static void Tokenize(const std::string& str, std::vector<std::string>& tokens, const std::string& delimiters = " ", bool strict = false) + { + std::string::size_type lastPos = str.find_first_not_of(delimiters, 0); + std::string::size_type pos = str.find_first_of(delimiters, lastPos); + tokens.clear(); + while (std::string::npos != pos || std::string::npos != lastPos) + { + tokens.push_back(str.substr(lastPos, pos - lastPos)); + if(strict) { + if(pos == std::string::npos) break; + lastPos = pos + 1; + } else lastPos = str.find_first_not_of(delimiters, pos); + pos = str.find_first_of(delimiters, lastPos); + } + } + +} diff --git a/maca_crf_tagger/src/lemmatizer.cc b/maca_crf_tagger/src/lemmatizer.cc new file mode 100644 index 0000000..70c019f --- /dev/null +++ b/maca_crf_tagger/src/lemmatizer.cc @@ -0,0 +1,19 @@ +#include "lemmatizer.h" + +int main(int argc, char** argv) { + if(argc != 2) { + std::cerr << "usage: " << argv[0] << " <fplm-dictionary>\n"; + return 1; + } + macaon::Lemmatizer lemmatizer(argv[1]); + std::string line; + while(std::getline(std::cin, line)) { + std::vector<std::string> tokens; + macaon::Tokenize(line, tokens, " "); + for(size_t i = 0; i < tokens.size(); i++) { + if(i > 0) std::cout << " "; + std::cout << lemmatizer.lemmatize(tokens[i]); + } + std::cout << "\n"; + } +} diff --git a/maca_crf_tagger/src/lemmatizer.h b/maca_crf_tagger/src/lemmatizer.h new file mode 100644 index 0000000..5966c7e --- /dev/null +++ b/maca_crf_tagger/src/lemmatizer.h @@ -0,0 +1,51 @@ +#pragma once + +#include <string> +#include <unordered_map> +#include <vector> +#include <fstream> +#include <iostream> + +#include "crf_utils.hh" + +namespace macaon { + class Lemmatizer { + std::unordered_map<std::string, std::string> dictionary; + public: + Lemmatizer(const std::string& filename) { + std::ifstream input(filename); + if(input) { + std::string line; + int line_num = 1; + while(std::getline(input, line)) { + std::vector<std::string> tokens; + macaon::Tokenize(line, tokens, "\t", true); + if(tokens.size() != 4) { + std::cerr << "ERROR: unexpected input in " << filename << ", line " << line_num << ": \"" << line << "\"\n"; + break; + } + std::string word = tokens[0]; + std::string tag = tokens[1]; + std::string lemma = tokens[2]; + std::string morpho = tokens[3]; + dictionary[word + "/" + tag] = lemma; + line_num ++; + } + } else { + std::cerr << "ERROR: loading " << filename << "\n"; + } + } + std::string lemmatize(const std::string& word, const std::string& tag) const { + std::string key = word + "/" + tag; + return lemmatize(key); + } + std::string lemmatize(const std::string& word_tag) const { + std::unordered_map<std::string, std::string>::const_iterator found = dictionary.find(word_tag); + if(found != dictionary.end()) { + return found->second; + } + return word_tag.substr(0, word_tag.rfind('/')); + } + }; +} + diff --git a/maca_crf_tagger/src/maca_crf_convert_binlexicon.cc b/maca_crf_tagger/src/maca_crf_convert_binlexicon.cc new file mode 100644 index 0000000..4704208 --- /dev/null +++ b/maca_crf_tagger/src/maca_crf_convert_binlexicon.cc @@ -0,0 +1,46 @@ +#include "crf_decoder.hh" +#include "crf_binlexicon.hh" + +int main(int argc, char** argv) { + if(argc != 4 && argc != 3) { + std::cerr << "convert: " << argv[0] << " <crf-model> <lexicon.in> <lexicon.out>\n"; + std::cerr << "test: cat <text-lexicon> | " << argv[0] << " <crf-model> <bin-lexicon>\n"; + return 1; + } + if(argc == 4) { + macaon::Decoder decoder(argv[1]); + macaon::BinaryLexicon lexicon(argv[2], decoder.getTagset()); + lexicon.Write(argv[3]); + } else if(argc == 3) { + macaon::Decoder decoder(argv[1]); + macaon::BinaryLexicon lexicon(argv[2], decoder.getTagset()); + std::string line; + int line_num = 0; + while(std::getline(std::cin, line)) { + line_num ++; + std::vector<int64> tags; + std::vector<std::string> tokens; + macaon::Tokenize(line, tokens, "\t "); + if(lexicon.GetTagsForWord(tokens[0], tags) == false) { + std::cerr << "WARNING: word not found \"" << tokens[0] << "\", using all tags\n"; + } + if(tags.size() != tokens.size() - 1) { + std::cerr << "ERROR: wrong number of tags for entry " << line_num << "\n"; + std::cerr << " TXT: " << line << "\n"; + std::cerr << " BIN: " << tokens[0]; + for(size_t i = 0; i < tags.size(); i++) { + std::cerr << " " << decoder.getTagset()->Find(tags[i]); + } + std::cerr << "\n"; + } else { + for(size_t i = 0; i < tags.size(); i++) { + if(decoder.getTagset()->Find(tags[i]) != tokens[i + 1]) { + std::cerr << "ERROR: wrong tag \"" << tokens[i + 1] << "\" => \"" << decoder.getTagset()->Find(tags[i]) << "\", entry " << line_num << "\n"; + } + } + } + } + } + return 0; +} + diff --git a/maca_crf_tagger/src/maca_crf_convert_binmodel.cc b/maca_crf_tagger/src/maca_crf_convert_binmodel.cc new file mode 100644 index 0000000..4c6cc55 --- /dev/null +++ b/maca_crf_tagger/src/maca_crf_convert_binmodel.cc @@ -0,0 +1,23 @@ +#include "crf_binmodel.hh" + +int main(int argc, char** argv) { + if(argc != 3 && argc != 2) { + std::cerr << "usage: " << argv[0] << " <from> <to> or <binmodel>\n"; + return 1; + } + macaon::BinaryModel model; + if(argc == 3) { + model.Convert(argv[1], argv[2]); + } else { + model.Load(argv[1]); + std::vector<double> weights; + model.GetWeights("U18=a/jamais", weights); + for(size_t i = 0; i < weights.size(); i++) { + std::cout << weights[i] << " "; + } + std::cout << "\n"; + //model.Dump(); + } + return 0; +} + diff --git a/maca_crf_tagger/src/maca_crf_tagger_main.cc b/maca_crf_tagger/src/maca_crf_tagger_main.cc new file mode 100644 index 0000000..0371581 --- /dev/null +++ b/maca_crf_tagger/src/maca_crf_tagger_main.cc @@ -0,0 +1,259 @@ +/*************************************************************************** + Copyright (C) 2011 by xxx <xxx@lif.univ-mrs.fr> + This file is part of maca_crf_tagger. + + Maca_crf_tagger is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Maca_crf_tagger is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with maca_crf_tagger. If not, see <http://www.gnu.org/licenses/>. +**************************************************************************/ + +#include "maca_crf_tagger.hh" +#include "crf_decoder.hh" +#include "crf_features.hh" +#include "crf_lexicon.hh" +#include "crf_tclexdet.hh" + +void crf_tagger(fst::StdVectorFst &input, maca_crf_tagger_ctx *ctx, bool debug=false) +{ + if(debug) input.Write("debug.crf_tagger.input"); + gfsmStateId start; + maca_ht_structure * ht = ctx->ms->xml_nodes_ht; + xmlNodePtr seg; + char *tokens = NULL; + std::vector<std::vector<std::string> >features; + std::vector<int>ilabels; + fst::StdVectorFst output; + if(ctx->model_filename == NULL) { + std::cerr << "ERROR: crf_tagger model file not specified, exiting\n"; + exit(1); // ERROR + } + if(ctx->lexicon_filename == NULL) { + std::cerr << "ERROR: crf_tagger lexicon file not specified, exiting\n"; + exit(1); // ERROR + } + fst::SymbolTable inputSymbols("words"); + inputSymbols.AddSymbol("<eps>", 0); + + // extract features + for(start=0; start < input.NumStates(); start++){ + for(fst::MutableArcIterator<fst::StdVectorFst> aiter(&input, start); !aiter.Done(); aiter.Next()) { + const fst::StdArc &arc = aiter.Value(); + seg = (xmlNodePtr)maca_ht_index2adr(ht, arc.ilabel); + tokens = maca_sentence_get_segment_tokens_value(ctx->ms, seg); + ilabels.push_back(arc.ilabel); + inputSymbols.AddSymbol(tokens, ilabels.size()); + aiter.SetValue(fst::StdArc(ilabels.size(), arc.olabel, arc.weight, arc.nextstate)); + std::vector<std::string>word_features; + macaon::FeatureGenerator::get_pos_features(tokens, word_features); + features.push_back(word_features); + free(tokens); + } + } + if(debug) input.Write("debug.crf_tagger.features"); + + int64 isString = input.Properties(fst::kString, true); + if(isString & fst::kString && ctx->n == 1) { + if(ctx->verbose_flag > 0) std::cerr << "INFO: using linear tagger\n"; + // faster pipeline for linear automata + std::vector<std::string> tags; + ctx->decoder->decodeString(features, tags, ctx->lexicon); + output.AddState(); + output.SetStart(0); + for(int64 state = 0; state < input.NumStates() - 1; state++){ + const fst::StdArc &arc = fst::ArcIterator<fst::StdVectorFst>(input, state).Value(); + output.AddState(); + output.AddArc(state, fst::StdArc(ilabels[arc.ilabel-1], ctx->tag_mapping[ctx->decoder->getTagset()->Find(tags[state])], arc.weight, state + 1)); + } + output.SetFinal(output.NumStates() - 1, 0); + input = output; + + } else { + // add possible tag labels + input.SetInputSymbols(&inputSymbols); + ctx->lexicon->AddTags(input); + if(debug) input.Write("debug.crf_tagger.tags"); + + // rescore with CRF + ctx->decoder->decode(features, input, output, true); + if(debug) output.Write("debug.crf_tagger.decoded"); + + // convert to macaon + fst::RmEpsilon(&output); + + input = output; + for(start=0; start < input.NumStates(); start++){ + for(fst::MutableArcIterator<fst::StdVectorFst> aiter(&input, start); !aiter.Done(); aiter.Next()) { + const fst::StdArc &arc = aiter.Value(); + aiter.SetValue(fst::StdArc(ilabels[arc.ilabel-1], ctx->tag_mapping[arc.olabel], arc.weight, arc.nextstate)); + } + } + } + if(debug) input.Write("debug.crf_tagger.output"); +} + +void traverse_segments(maca_section *section, maca_crf_tagger_ctx *ctx) +{ + xmlNodePtr segs = section->xml_node_segs; + xmlNodePtr seg; + xmlChar *ulex_id; + maca_ht_structure * ht = ctx->ms->xml_nodes_ht; + int n; + int index; + char *tokens = NULL; + + + for(seg=segs->children, n=0; seg ; seg=seg->next, n++) + { + ulex_id = xmlGetProp(seg, BAD_CAST "id"); + index = maca_ht_adr2index(ht, seg); + tokens = maca_sentence_get_segment_tokens_value(ctx->ms, seg); + fprintf(stderr, "index = %d n = %d id = %s tokens = %s\n", index, n, ulex_id, tokens); + } +} + +void traverse_automaton(gfsmAutomaton *a, maca_crf_tagger_ctx *ctx) +{ + gfsmStateId i; + gfsmArcIter ai; + gfsmArc *t; + xmlNodePtr n; + maca_ht_xmlnode *ht = ctx->ms->xml_nodes_ht; + xmlChar *ulex_id; + xmlChar *ulex_lex_id; + + for(i=0; i<gfsm_automaton_n_states(a); i++){ + for (gfsm_arciter_open_ptr(&ai,a,gfsm_automaton_find_state(a, i)); gfsm_arciter_ok(&ai); gfsm_arciter_next(&ai)){ + t = gfsm_arciter_arc(&ai); + n = (xmlNodePtr)maca_ht_index2adr(ht, gfsm_arc_lower(t)); + ulex_id = xmlGetProp(n, BAD_CAST "id"); + ulex_lex_id = xmlGetProp(n, BAD_CAST "lex_id"); + fprintf(stderr,"index = %d ulex id = %s lex_id = %s\n",gfsm_arc_lower(t), ulex_id, ulex_lex_id); + } + } + /* creation de segment et d'une section */ +} + + +maca_section *create_morpho_section(gfsmAutomaton *a, maca_crf_tagger_ctx *ctx) +{ + gfsmStateId i; + gfsmArc *t; + gfsmArcIter ai; + maca_ht_structure * ht = ctx->ms->xml_nodes_ht; + char id_pos[500]; + xmlNodePtr posNode = NULL; + xmlNodePtr lexNode = NULL; + maca_section * section = NULL; + GHashTable* segments_created = g_hash_table_new_full(g_str_hash, g_str_equal,free, NULL); + char * prefix_id; + char * temp; + + //section = maca_section_create_section(MACA_POSS_SECTION); + //maca_sentence_add_section(ctx->ms, section); + section = maca_sentence_new_section(ctx->ms,MACA_MORPHO_SECTION); + prefix_id = (char*)malloc(sizeof(char)*(strlen(ctx->ms->id_sentence) +3)); + sprintf(prefix_id,"%s_M",ctx->ms->id_sentence); + + for(i=0; i<gfsm_automaton_n_states(a); i++){ + for (gfsm_arciter_open(&ai,a,i); gfsm_arciter_ok(&ai); gfsm_arciter_next(&ai)){ + t = gfsm_arciter_arc(&ai); + if(gfsm_arc_lower(t) != gfsmEpsilon){ + lexNode = (xmlNodePtr)maca_ht_index2adr(ht,gfsm_arc_lower(t)); + if(lexNode){ + temp = (char*)xmlGetProp(lexNode, BAD_CAST "id"); + sprintf(id_pos, "%s_%s",temp, maca_tags_get_str(ctx->cfg, "morpho", "stype", gfsm_arc_upper(t))); + free(temp); + // printf("key = %s\n", id_pos); + if(posNode = (xmlNodePtr)g_hash_table_lookup(segments_created, id_pos)){ + t->lower = maca_ht_adr2index(ht,posNode); + // printf("segment %s already created\n", id_pos); + } + else{ + // printf("add segment %s %s (%s)\n", xmlGetProp(lexNode, BAD_CAST "id"), maca_tags_get_str(ctx->cfg, "morpho", "stype", gfsm_arc_upper(t)), id_pos); + posNode = maca_sentence_add_segment(ctx->ms, MACA_MORPHO_SECTION, MACA_CAT_TYPE, prefix_id); + t->lower = maca_ht_adr2index(ht,posNode); + xmlNewProp(posNode, BAD_CAST "stype", BAD_CAST maca_tags_get_str(ctx->cfg, "morpho", "stype", gfsm_arc_upper(t))); + maca_segment_add_elt_from_node(posNode, lexNode, 0); + g_hash_table_insert(segments_created, strdup(id_pos), posNode); + } + } + } + } + } + free(prefix_id); + // maca_section_add_automaton(section, a); + maca_sentence_update_xml_automaton(ctx->ms, MACA_MORPHO_SECTION,a); + // section->xml_node_fsm = xmlAddChild(section->xml_node, fsm2xml(a, ht)); + + g_hash_table_destroy(segments_created); + // fsm_affiche(a, ht); + //maca_section_update_xml_automaton(section, ht, a); + return section; +} + + + +int maca_crf_tagger_ProcessSentence(maca_sentence * ms, maca_crf_tagger_ctx * ctx) +{ + maca_section * prelex_section; + maca_section * lex_section; + gfsmAutomaton *lex_automaton; + fst::StdVectorFst automaton; + + ctx->ms = ms; + + if(!maca_sentence_is_section_loaded(ctx->ms,MACA_PRELEX_SECTION)) + { + prelex_section = maca_sentence_load_section_by_type(ctx->ms,MACA_PRELEX_SECTION); + } + else prelex_section = maca_sentence_get_section(ctx->ms, MACA_PRELEX_SECTION); + if(prelex_section == NULL){ + maca_msg(ctx->module, MACA_ERROR); + fprintf(stderr,"sentence : %s no prelex section\n", ctx->ms->id_sentence); + return -1; + } + + if(!maca_sentence_is_section_loaded(ctx->ms,MACA_LEX_SECTION)) + { + lex_section = maca_sentence_load_section_by_type(ctx->ms,MACA_LEX_SECTION); + } + else lex_section = maca_sentence_get_section(ctx->ms, MACA_LEX_SECTION); + if(lex_section == NULL){ + maca_msg(ctx->module, MACA_ERROR); + fprintf(stderr,"sentence : %s no lex section\n", ctx->ms->id_sentence); + return -1; + } + lex_automaton = maca_sentence_get_section_automaton(ctx->ms, MACA_LEX_SECTION); + if(lex_automaton == NULL){ + maca_msg(ctx->module, MACA_ERROR); + fprintf(stderr,"sentence : %s no lex automaton\n", ctx->ms->id_sentence); + return -1; + } + gfsm2fst(lex_automaton, automaton); + crf_tagger(automaton, ctx, ctx->verbose_flag > 4); + if(ctx->n > 0){ + fst::StdVectorFst nbest; + fst::ShortestPath(automaton, &nbest, ctx->n); + create_morpho_section(fst2gfsm(nbest), ctx); + } else if(ctx->n == -1) { + create_morpho_section(fst2gfsm(automaton), ctx); + } else if(ctx->n == -2) { + macaon::DeterminizeTCLex(&automaton); + create_morpho_section(fst2gfsm(automaton), ctx); + } else { + fprintf(stderr, "error: unknown -n value (%d)\n", ctx->n); + return -1; + } + return 1; +} + + diff --git a/maca_crf_tagger/src/maca_crf_tagger_utils.cc b/maca_crf_tagger/src/maca_crf_tagger_utils.cc new file mode 100644 index 0000000..93ed95d --- /dev/null +++ b/maca_crf_tagger/src/maca_crf_tagger_utils.cc @@ -0,0 +1,31 @@ +/*************************************************************************** + Copyright (C) 2011 by xxx <xxx@lif.univ-mrs.fr> + This file is part of maca_crf_tagger. + + Maca_crf_tagger is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Maca_crf_tagger is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with maca_crf_tagger. If not, see <http://www.gnu.org/licenses/>. +**************************************************************************/ + +#include "maca_crf_tagger.hh" + +char * maca_crf_tagger_GetVersion() +{ + return MACA_CRF_TAGGER_VERSION; +} + +void maca_crf_tagger_add_stamp(xmlNodePtr node) +{ + add_maca_stamp(node,MACA_CRF_TAGGER_NAME,MACA_CRF_TAGGER_VERSION); +} + + diff --git a/maca_crf_tagger/src/simple_tagger.cc b/maca_crf_tagger/src/simple_tagger.cc new file mode 100644 index 0000000..1446829 --- /dev/null +++ b/maca_crf_tagger/src/simple_tagger.cc @@ -0,0 +1,21 @@ +#include "simple_tagger.hh" + +macaon::Tagger* Tagger_new(const char* modelName, const char* lexiconName) { + return new macaon::Tagger(modelName, lexiconName ? lexiconName : ""); +} + +void Tagger_free(macaon::Tagger* tagger) { + delete tagger; +} + +bool Tagger_ProcessSentence(macaon::Tagger* tagger, int num_words, const char** words, const char** tags) { + std::vector<std::string> word_vector, tag_vector; + for(int i = 0; i < num_words; i++) { + word_vector.push_back(words[i]); + } + bool result = tagger->ProcessSentence(word_vector, tag_vector); + for(int i = 0; i < num_words; i++) { + tags[i] = strdup(tag_vector[i].c_str()); + } + return result; +} diff --git a/maca_crf_tagger/src/simple_tagger.hh b/maca_crf_tagger/src/simple_tagger.hh new file mode 100644 index 0000000..2185338 --- /dev/null +++ b/maca_crf_tagger/src/simple_tagger.hh @@ -0,0 +1,40 @@ +#include <vector> +#include "crf_decoder.hh" +#include "crf_binlexicon.hh" +#include "crf_features.hh" + +namespace macaon { + class Tagger { + private: + macaon::Decoder decoder; + macaon::BinaryLexicon *lexicon; + public: + Tagger(const std::string modelName, const std::string lexiconName = "") : decoder(modelName), lexicon(NULL) { + if(lexiconName != "") lexicon = new macaon::BinaryLexicon(lexiconName, decoder.getTagset()); + } + + ~Tagger() { + if(lexicon != NULL) delete lexicon; + } + + bool ProcessSentence(const std::vector<std::string>& words, std::vector<std::string>& tags) { + std::vector<std::vector<std::string> > features; + for(size_t i = 0; i < words.size(); i++) { + std::vector<std::string> word_features; + macaon::FeatureGenerator::get_pos_features(words[i], word_features); + features.push_back(word_features); + } + tags.clear(); + decoder.decodeString(features, tags, lexicon); + return true; + } + }; +} + +extern "C" { + macaon::Tagger* Tagger_new(const char* modelName, const char* lexiconName); + + void Tagger_free(macaon::Tagger* tagger); + + bool Tagger_ProcessSentence(macaon::Tagger* tagger, int num_words, const char** words, const char** tags); +} diff --git a/maca_crf_tagger/src/test_simple_tagger.cc b/maca_crf_tagger/src/test_simple_tagger.cc new file mode 100644 index 0000000..ac3f021 --- /dev/null +++ b/maca_crf_tagger/src/test_simple_tagger.cc @@ -0,0 +1,27 @@ +#include <stdio.h> + +#include "simple_tagger.hh" + +int main(int argc, char** argv) { + int num_words = 6; + const char* words[] = {"le", "petit", "chat", "boit", "du", "lait"}; + const char* tags[6]; + int i; + + if(argc != 3) { + fprintf(stderr, "usage: %s <tagger-model> <tagger-lexicon>\n", argv[0]); + return 1; + } + + macaon::Tagger* tagger = Tagger_new(argv[1], argv[2]); + + Tagger_ProcessSentence(tagger, num_words, words, tags); + + for(i = 0; i < num_words; i++) { + printf("%s %s\n", words[i], tags[i]); + } + + Tagger_free(tagger); + + return 0; +} diff --git a/maca_crf_tagger/src/utf8 b/maca_crf_tagger/src/utf8 new file mode 100755 index 0000000000000000000000000000000000000000..4dee4508ec27a0923a86e9384cf41e7758a38d30 GIT binary patch literal 8718 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 Hk-z`|v62B9 literal 0 HcmV?d00001 diff --git a/maca_crf_tagger/src/utf8.c b/maca_crf_tagger/src/utf8.c new file mode 100644 index 0000000..4e1ea27 --- /dev/null +++ b/maca_crf_tagger/src/utf8.c @@ -0,0 +1,33 @@ +#include <stdio.h> +#include <stdlib.h> + +const char *byte_to_binary(int x) +{ + static char b[9]; + b[0] = '\0'; + + int z; + for (z = 128; z > 0; z >>= 1) + { + strcat(b, ((x & z) == z) ? "1" : "0"); + } + + return b; +} + +int main() { + char word[1024]; + fgets(word, 1024, stdin); + printf("%s\n", word); + int offset = 0; + while(word[offset] != 0) { + printf("%3d %s [%s]\n", offset, byte_to_binary(word[offset]), &word[offset]); + if((unsigned char)word[offset] >> 7 == 1) { + offset++; + while((unsigned char)word[offset] >> 6 == 2) offset++; + } else { + offset++; + } + } + return 0; +} -- GitLab