mosesdecoder/contrib/tmcombine/tmcombine.py

1954 lines
91 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: Rico Sennrich <sennrich [AT] cl.uzh.ch>
# This program handles the combination of Moses phrase tables, either through
# linear interpolation of the phrase translation probabilities/lexical weights,
# or through a recomputation based on the (weighted) combined counts.
#
# It also supports an automatic search for weights that minimize the cross-entropy
# between the model and a tuning set of word/phrase alignments.
# for usage information, run
# python tmcombine.py -h
# you can also check the docstrings of Combine_TMs() for more information and find some example commands in the function test()
# Some general things to note:
# - Different combination algorithms require different statistics. To be on the safe side, use the option `-write-lexical-counts` when training models.
# - The script assumes that phrase tables are sorted (to allow incremental, more memory-friendly processing). sort with LC_ALL=C.
# - Some configurations require additional statistics that are loaded in memory (lexical tables; complete list of target phrases). If memory consumption is a problem, use the option --lowmem (slightly slower and writes temporary files to disk), or consider pruning your phrase table before combining (e.g. using Johnson et al. 2007).
# - The script can read/write gzipped files, but the Python implementation is slow. You're better off unzipping the files on the command line and working with the unzipped files.
# - The cross-entropy estimation assumes that phrase tables contain true probability distributions (i.e. a probability mass of 1 for each conditional probability distribution). If this is not true, the results are skewed.
# - Unknown phrase pairs are not considered for the cross-entropy estimation. A comparison of models with different vocabularies may be misleading.
# - Don't directly compare cross-entropies obtained from a combination with different modes. Depending on how some corner cases are treated, linear interpolation does not distribute full probability mass and thus shows higher (i.e. worse) cross-entropies.
from __future__ import division, unicode_literals
import sys
import os
import gzip
import argparse
import copy
import re
from math import log, exp
from collections import defaultdict
from operator import mul
from tempfile import NamedTemporaryFile
from subprocess import Popen
try:
from itertools import izip
except:
izip = zip
try:
from lxml import etree as ET
except:
import xml.etree.cElementTree as ET
try:
from scipy.optimize.lbfgsb import fmin_l_bfgs_b
optimizer = 'l-bfgs'
except:
optimizer = 'hillclimb'
class Moses():
"""Moses interface for loading/writing models
to support other phrase table formats, subclass this and overwrite the relevant functions
"""
def __init__(self,models,number_of_features):
self.number_of_features = number_of_features
self.models = models
#example item (assuming mode=='counts' and one feature): phrase_pairs['the house']['das haus'] = [[[10,100]],['0-0 1-1']]
self.phrase_pairs = defaultdict(lambda: defaultdict(lambda: [[[0]*len(self.models) for i in range(self.number_of_features)],[]]))
self.phrase_source = defaultdict(lambda: [0]*len(self.models))
self.phrase_target = defaultdict(lambda: [0]*len(self.models))
self.reordering_pairs = defaultdict(lambda: defaultdict(lambda: [[0]*len(self.models) for i in range(self.number_of_features)]))
self.word_pairs_e2f = defaultdict(lambda: defaultdict(lambda: [0]*len(self.models)))
self.word_pairs_f2e = defaultdict(lambda: defaultdict(lambda: [0]*len(self.models)))
self.word_source = defaultdict(lambda: [0]*len(self.models))
self.word_target = defaultdict(lambda: [0]*len(self.models))
self.require_alignment = False
def open_table(self,model,table,mode='r'):
"""define which paths to open for lexical tables and phrase tables.
we assume canonical Moses structure, but feel free to overwrite this
"""
if table == 'reordering-table':
table = 'reordering-table.wbe-msd-bidirectional-fe'
filename = os.path.join(model,'model',table)
fileobj = handle_file(filename,'open',mode)
return fileobj
def load_phrase_features(self,line,priority,i,mode='interpolate',store='pairs',filter_by=None,filter_by_src=None,filter_by_target=None,inverted=False,flags=None):
"""take single phrase table line and store probablities in internal data structure"""
src = line[0]
target = line[1]
if inverted:
src,target = target,src
if (store == 'all' or store == 'pairs') and (priority < 10 or (src in self.phrase_pairs and target in self.phrase_pairs[src])) and not (filter_by and not (src in filter_by and target in filter_by[src])):
self.store_info(src,target,line)
scores = line[2].split()
if len(scores) <self.number_of_features:
sys.stderr.write('Error: model only has {0} features. Expected {1}.\n'.format(len(scores),self.number_of_features))
exit()
scores = scores[:self.number_of_features]
model_probabilities = map(float,scores)
phrase_probabilities = self.phrase_pairs[src][target][0]
if mode == 'counts' and not priority == 2: #priority 2 is MAP
try:
counts = map(float,line[-1].split())
try:
target_count,src_count,joint_count = counts
joint_count_e2f = joint_count
joint_count_f2e = joint_count
except ValueError:
# possibly old-style phrase table with 2 counts in last column, or phrase table produced by tmcombine
# note: since each feature has different weight vector, we may have two different phrase pair frequencies
target_count,src_count = counts
i_e2f = flags['i_e2f']
i_f2e = flags['i_f2e']
joint_count_e2f = model_probabilities[i_e2f] * target_count
joint_count_f2e = model_probabilities[i_f2e] * src_count
except:
sys.stderr.write(str(b" ||| ".join(line))+b'\n')
sys.stderr.write('ERROR: counts are missing or misformatted. Maybe your phrase table is from an older Moses version that doesn\'t store counts or word alignment?\n')
raise
i_e2f = flags['i_e2f']
i_f2e = flags['i_f2e']
model_probabilities[i_e2f] = joint_count_e2f
model_probabilities[i_f2e] = joint_count_f2e
for j,p in enumerate(model_probabilities):
phrase_probabilities[j][i] = p
# mark that the src/target phrase has been seen.
# needed for re-normalization during linear interpolation
if (store == 'all' or store == 'source') and not (filter_by_src and not src in filter_by_src):
if mode == 'counts' and not priority == 2: #priority 2 is MAP
try:
self.phrase_source[src][i] = float(line[-1].split()[1])
except:
sys.stderr.write(str(line)+'\n')
sys.stderr.write('ERROR: Counts are missing or misformatted. Maybe your phrase table is from an older Moses version that doesn\'t store counts or word alignment?\n')
raise
else:
self.phrase_source[src][i] = 1
if (store == 'all' or store == 'target') and not (filter_by_target and not target in filter_by_target):
if mode == 'counts' and not priority == 2: #priority 2 is MAP
try:
self.phrase_target[target][i] = float(line[-1].split()[0])
except:
sys.stderr.write(str(line)+'\n')
sys.stderr.write('ERROR: Counts are missing or misformatted. Maybe your phrase table is from an older Moses version that doesn\'t store counts or word alignment?\n')
raise
else:
self.phrase_target[target][i] = 1
def load_reordering_probabilities(self,line,priority,i,**unused):
"""take single reordering table line and store probablities in internal data structure"""
src = line[0]
target = line[1]
model_probabilities = map(float,line[2].split())
reordering_probabilities = self.reordering_pairs[src][target]
try:
for j,p in enumerate(model_probabilities):
reordering_probabilities[j][i] = p
except IndexError:
sys.stderr.write('\nIndexError: Did you correctly specify the number of reordering features? (--number_of_features N in command line)\n')
exit()
def traverse_incrementally(self,table,models,load_lines,store_flag,mode='interpolate',inverted=False,lowmem=False,flags=None):
"""hack-ish way to find common phrase pairs in multiple models in one traversal without storing it all in memory
relies on alphabetical sorting of phrase table.
"""
increment = -1
stack = ['']*len(self.models)
while increment:
self.phrase_pairs = defaultdict(lambda: defaultdict(lambda: [[[0]*len(self.models) for i in range(self.number_of_features)],[]]))
self.reordering_pairs = defaultdict(lambda: defaultdict(lambda: [[0]*len(self.models) for i in range(self.number_of_features)]))
self.phrase_source = defaultdict(lambda: [0]*len(self.models))
if lowmem:
self.phrase_target = defaultdict(lambda: [0]*len(self.models))
for model,priority,i in models:
if stack[i]:
if increment != stack[i][0]:
continue
else:
load_lines(stack[i],priority,i,mode=mode,store=store_flag,inverted=inverted,flags=flags)
stack[i] = ''
for line in model:
line = line.rstrip().split(b' ||| ')
if increment != line[0]:
stack[i] = line
break
load_lines(line,priority,i,mode=mode,store=store_flag,inverted=inverted,flags=flags)
yield 1
#calculate which source phrase to process next
lines = [line[0] + b' |' for line in stack if line]
if lines:
increment = min(lines)[:-2]
else:
increment = None
def load_word_probabilities(self,line,side,i,priority,e2f_filter=None,f2e_filter=None):
"""process single line of lexical table"""
a, b, prob = line.split(b' ')
if side == 'e2f' and (not e2f_filter or a in e2f_filter and b in e2f_filter[a]):
self.word_pairs_e2f[a][b][i] = float(prob)
elif side == 'f2e' and (not f2e_filter or a in f2e_filter and b in f2e_filter[a]):
self.word_pairs_f2e[a][b][i] = float(prob)
def load_word_counts(self,line,side,i,priority,e2f_filter=None,f2e_filter=None,flags=None):
"""process single line of lexical table"""
a, b, ab_count, b_count = line.split(b' ')
if side == 'e2f':
if priority == 2: #MAP
if not e2f_filter or a in e2f_filter:
if not e2f_filter or b in e2f_filter[a]:
self.word_pairs_e2f[a][b][i] = float(ab_count)/float(b_count)
self.word_target[b][i] = 1
else:
if not e2f_filter or a in e2f_filter:
if not e2f_filter or b in e2f_filter[a]:
self.word_pairs_e2f[a][b][i] = float(ab_count)
self.word_target[b][i] = float(b_count)
elif side == 'f2e':
if priority == 2: #MAP
if not f2e_filter or a in f2e_filter and b in f2e_filter[a]:
if not f2e_filter or b in f2e_filter[a]:
self.word_pairs_f2e[a][b][i] = float(ab_count)/float(b_count)
self.word_source[b][i] = 1
else:
if not f2e_filter or a in f2e_filter and b in f2e_filter[a]:
if not f2e_filter or b in f2e_filter[a]:
self.word_pairs_f2e[a][b][i] = float(ab_count)
self.word_source[b][i] = float(b_count)
def load_lexical_tables(self,models,mode,e2f_filter=None,f2e_filter=None):
"""open and load lexical tables into data structure"""
if mode == 'counts':
files = ['lex.counts.e2f','lex.counts.f2e']
load_lines = self.load_word_counts
else:
files = ['lex.e2f','lex.f2e']
load_lines = self.load_word_probabilities
j = 0
for f in files:
models_prioritized = [(self.open_table(model,f),priority,i) for (model,priority,i) in priority_sort_models(models)]
for model,priority,i in models_prioritized:
for line in model:
if not j % 100000:
sys.stderr.write('.')
j += 1
load_lines(line,f[-3:],i,priority,e2f_filter=e2f_filter,f2e_filter=f2e_filter)
def store_info(self,src,target,line):
"""store alignment info and comment section for re-use in output"""
if len(line) == 5:
self.phrase_pairs[src][target][1] = line[3:5]
# assuming that alignment is empty
elif len(line) == 4:
if self.require_alignment:
sys.stderr.write('Error: unexpected phrase table format. Your current configuration requires alignment information. Make sure you trained your model with -phrase-word-alignment (default in newer Moses versions)\n')
exit()
self.phrase_pairs[src][target][1] = [b'',line[3].lstrip(b'| ')]
else:
sys.stderr.write('Error: unexpected phrase table format. Are you using a very old/new version of Moses with different formatting?\n')
exit()
def get_word_alignments(self,src,target,cache=False,mycache={}):
"""from the Moses phrase table alignment info in the form "0-0 1-0",
get the aligned word pairs / NULL alignments
"""
if cache:
if (src,target) in mycache:
return mycache[(src,target)]
try:
alignment = self.phrase_pairs[src][target][1][0]
except:
return None,None
src_list = src.split(b' ')
target_list = target.split(b' ')
textual_e2f = [[s,[]] for s in src_list]
textual_f2e = [[t,[]] for t in target_list]
for pair in alignment.split(b' '):
s,t = pair.split('-')
s,t = int(s),int(t)
textual_e2f[s][1].append(target_list[t])
textual_f2e[t][1].append(src_list[s])
for s,t in textual_e2f:
if not t:
t.append('NULL')
for s,t in textual_f2e:
if not t:
t.append('NULL')
#tupelize so we can use the value as dictionary keys
for i in range(len(textual_e2f)):
textual_e2f[i][1] = tuple(textual_e2f[i][1])
for i in range(len(textual_f2e)):
textual_f2e[i][1] = tuple(textual_f2e[i][1])
if cache:
mycache[(src,target)] = textual_e2f,textual_f2e
return textual_e2f,textual_f2e
def write_phrase_table(self,src,target,weights,features,mode,flags):
"""convert data to string in Moses phrase table format"""
# if one feature value is 0 (either because of loglinear interpolation or rounding to 0), don't write it to phrasetable
# (phrase pair will end up with probability zero in log-linear model anyway)
if 0 in features:
return ''
# information specific to Moses model: alignment info and comment section with target and source counts
alignment,comments = self.phrase_pairs[src][target][1]
if alignment:
extra_space = b' '
else:
extra_space = b''
if mode == 'counts':
i_e2f = flags['i_e2f']
i_f2e = flags['i_f2e']
srccount = dot_product(self.phrase_source[src],weights[i_f2e])
targetcount = dot_product(self.phrase_target[target],weights[i_e2f])
comments = b"%s %s" %(targetcount,srccount)
features = b' '.join([b'%.6g' %(f) for f in features])
if flags['add_origin_features']:
origin_features = map(lambda x: 2.718**bool(x),self.phrase_pairs[src][target][0][0]) # 1 if phrase pair doesn't occur in model, 2.718 if it does
origin_features = b' '.join([b'%.4f' %(f) for f in origin_features]) + ' '
else:
origin_features = b''
line = b"%s ||| %s ||| %s 2.718 %s||| %s%s||| %s\n" %(src,target,features,origin_features,alignment,extra_space,comments)
return line
def write_lexical_file(self,direction, path, weights,mode):
if mode == 'counts':
bridge = '.counts'
else:
bridge = ''
fobj = handle_file("{0}{1}.{2}".format(path,bridge,direction),'open',mode='w')
sys.stderr.write('Writing {0}{1}.{2}\n'.format(path,bridge,direction))
if direction == 'e2f':
word_pairs = self.word_pairs_e2f
marginal = self.word_target
elif direction == 'f2e':
word_pairs = self.word_pairs_f2e
marginal = self.word_source
for x in sorted(word_pairs):
for y in sorted(word_pairs[x]):
xy = dot_product(word_pairs[x][y],weights)
fobj.write(b"%s %s %s" %(x,y,xy))
if mode == 'counts':
fobj.write(b" %s\n" %(dot_product(marginal[y],weights)))
else:
fobj.write(b'\n')
handle_file("{0}{1}.{2}".format(path,bridge,direction),'close',fobj,mode='w')
def write_reordering_table(self,src,target,features):
"""convert data to string in Moses reordering table format"""
# if one feature value is 0 (either because of loglinear interpolation or rounding to 0), don't write it to reordering table
# (phrase pair will end up with probability zero in log-linear model anyway)
if 0 in features:
return ''
features = b' '.join([b'%.6g' %(f) for f in features])
line = b"%s ||| %s ||| %s\n" %(src,target,features)
return line
def create_inverse(self,fobj,tempdir=None):
"""swap source and target phrase in the phrase table, and then sort (by target phrase)"""
inverse = NamedTemporaryFile(prefix='inv_unsorted',delete=False,dir=tempdir)
swap = re.compile(b'(.+?) \|\|\| (.+?) \|\|\|')
# just swap source and target phrase, and leave order of scores etc. intact.
# For better compatibility with existing codebase, we swap the order of the phrases back for processing
for line in fobj:
inverse.write(swap.sub(b'\\2 ||| \\1 |||',line,1))
inverse.close()
inverse_sorted = sort_file(inverse.name,tempdir=tempdir)
os.remove(inverse.name)
return inverse_sorted
def merge(self,pt_normal, pt_inverse, pt_out, mode='interpolate'):
"""merge two phrasetables (the latter having been inverted to calculate p(s|t) and lex(s|t) in sorted order)
Assumes that p(s|t) and lex(s|t) are in first table half, p(t|s) and lex(t|s) in second"""
for line,line2 in izip(pt_normal,pt_inverse):
line = line.split(b' ||| ')
line2 = line2.split(b' ||| ')
#scores
mid = int(self.number_of_features/2)
scores1 = line[2].split()
scores2 = line2[2].split()
line[2] = b' '.join(scores2[:mid]+scores1[mid:])
# marginal counts
if mode == 'counts':
src_count = line[-1].split()[1]
target_count = line2[-1].split()[0]
line[-1] = b' '.join([target_count,src_count]) + b'\n'
pt_out.write(b' ||| '.join(line))
pt_normal.close()
pt_inverse.close()
pt_out.close()
class TigerXML():
"""interface to load reference word alignments from TigerXML corpus.
Tested on SMULTRON (http://kitt.cl.uzh.ch/kitt/smultron/)
"""
def __init__(self,alignment_xml):
"""only argument is TigerXML file
"""
self.treebanks = self._open_treebanks(alignment_xml)
self.word_pairs = defaultdict(lambda: defaultdict(int))
self.word_source = defaultdict(int)
self.word_target = defaultdict(int)
def load_word_pairs(self,src,target):
"""load word pairs. src and target are the itentifiers of the source and target language in the XML"""
if not src or not target:
sys.stderr.write('Error: Source and/or target language not specified. Required for TigerXML extraction.\n')
exit()
alignments = self._get_aligned_ids(src,target)
self._textualize_alignments(src,target,alignments)
def _open_treebanks(self,alignment_xml):
"""Parallel XML format references monolingual files. Open all."""
alignment_path = os.path.dirname(alignment_xml)
align_xml = ET.parse(alignment_xml)
treebanks = {}
treebanks['aligned'] = align_xml
for treebank in align_xml.findall('//treebank'):
treebank_id = treebank.get('id')
filename = treebank.get('filename')
if not os.path.isabs(filename):
filename = os.path.join(alignment_path,filename)
treebanks[treebank_id] = ET.parse(filename)
return treebanks
def _get_aligned_ids(self,src,target):
"""first step: find which nodes are aligned."""
alignments = []
ids = defaultdict(dict)
for alignment in self.treebanks['aligned'].findall('//align'):
newpair = {}
if len(alignment) != 2:
sys.stderr.write('Error: alignment with ' + str(len(alignment)) + ' children. Expected 2. Skipping.\n')
continue
for node in alignment:
lang = node.get('treebank_id')
node_id = node.get('node_id')
newpair[lang] = node_id
if not (src in newpair and target in newpair):
sys.stderr.write('Error: source and target languages don\'t match. Skipping.\n')
continue
# every token may only appear in one alignment pair;
# if it occurs in multiple, we interpret them as one 1-to-many or many-to-1 alignment
if newpair[src] in ids[src]:
idx = ids[src][newpair[src]]
alignments[idx][1].append(newpair[target])
elif newpair[target] in ids[target]:
idx = ids[target][newpair[target]]
alignments[idx][0].append(newpair[src])
else:
idx = len(alignments)
alignments.append(([newpair[src]],[newpair[target]]))
ids[src][newpair[src]] = idx
ids[target][newpair[target]] = idx
alignments = self._discard_discontinuous(alignments)
return alignments
def _discard_discontinuous(self,alignments):
"""discard discontinuous word sequences (which we can't use for phrase-based SMT systems)
and make sure that sequence is in correct order.
"""
new_alignments = []
for alignment in alignments:
new_pair = []
for sequence in alignment:
sequence_split = [t_id.split('_') for t_id in sequence]
#check if all words come from the same sentence
sentences = [item[0] for item in sequence_split]
if not len(set(sentences)) == 1:
#sys.stderr.write('Warning. Word sequence crossing sentence boundary. Discarding.\n')
#sys.stderr.write(str(sequence)+'\n')
continue
#sort words and check for discontinuities.
try:
tokens = sorted([int(item[1]) for item in sequence_split])
except ValueError:
#sys.stderr.write('Warning. Not valid word IDs. Discarding.\n')
#sys.stderr.write(str(sequence)+'\n')
continue
if not tokens[-1]-tokens[0] == len(tokens)-1:
#sys.stderr.write('Warning. Discontinuous word sequence(?). Discarding.\n')
#sys.stderr.write(str(sequence)+'\n')
continue
out_sequence = [sentences[0]+'_'+str(token) for token in tokens]
new_pair.append(out_sequence)
if len(new_pair) == 2:
new_alignments.append(new_pair)
return new_alignments
def _textualize_alignments(self,src,target,alignments):
"""Knowing which nodes are aligned, get actual words that are aligned."""
words = defaultdict(dict)
for text in [text for text in self.treebanks if not text == 'aligned']:
#TODO: Make lowercasing optional
for terminal in self.treebanks[text].findall('//t'):
words[text][terminal.get('id')] = terminal.get('word').lower()
for (src_ids, target_ids) in alignments:
try:
src_text = ' '.join((words[src][src_id] for src_id in src_ids))
except KeyError:
#sys.stderr.write('Warning. ID not found: '+ str(src_ids) +'\n')
continue
try:
target_text = ' '.join((words[target][target_id] for target_id in target_ids))
except KeyError:
#sys.stderr.write('Warning. ID not found: '+ str(target_ids) +'\n')
continue
self.word_pairs[src_text][target_text] += 1
self.word_source[src_text] += 1
self.word_target[target_text] += 1
class Moses_Alignment():
"""interface to load reference phrase alignment from corpus aligend with Giza++
and with extraction heuristics as applied by the Moses toolkit.
"""
def __init__(self,alignment_file):
self.alignment_file = alignment_file
self.word_pairs = defaultdict(lambda: defaultdict(int))
self.word_source = defaultdict(int)
self.word_target = defaultdict(int)
def load_word_pairs(self,src_lang,target_lang):
"""main function. overwrite this to import data in different format."""
fileobj = handle_file(self.alignment_file,'open','r')
for line in fileobj:
line = line.split(b' ||| ')
src = line[0]
target = line[1]
self.word_pairs[src][target] += 1
self.word_source[src] += 1
self.word_target[target] += 1
def dot_product(a,b):
"""calculate dot product from two lists"""
# optimized for PyPy (much faster than enumerate/map)
s = 0
i = 0
for x in a:
s += x * b[i]
i += 1
return s
def priority_sort_models(models):
"""primary models should have priority before supplementary models.
zipped with index to know which weight model belongs to
"""
return [(model,priority,i) for (i,(model,priority)) in sorted(zip(range(len(models)),models),key=lambda x: x[1][1])]
def cross_entropy(model_interface,reference_interface,weights,score,mode,flags):
"""calculate cross entropy given all necessary information.
don't call this directly, but use one of the Combine_TMs methods.
"""
weights = normalize_weights(weights,mode,flags)
if 'compare_cross-entropies' in flags and flags['compare_cross-entropies']:
num_results = len(model_interface.models)
else:
num_results = 1
cross_entropies = [[0]*num_results for i in range(model_interface.number_of_features)]
oov = [0]*num_results
oov2 = 0
other_translations = [0]*num_results
ignored = [0]*num_results
n = [0]*num_results
total_pairs = 0
for src in reference_interface.word_pairs:
for target in reference_interface.word_pairs[src]:
c = reference_interface.word_pairs[src][target]
for i in range(num_results):
if src in model_interface.phrase_pairs and target in model_interface.phrase_pairs[src]:
if ('compare_cross-entropies' in flags and flags['compare_cross-entropies']) or ('intersected_cross-entropies' in flags and flags['intersected_cross-entropies']):
if 0 in model_interface.phrase_pairs[src][target][0][0]: #only use intersection of models for comparability
# update unknown words statistics
if model_interface.phrase_pairs[src][target][0][0][i]:
ignored[i] += c
elif src in model_interface.phrase_source and model_interface.phrase_source[src][i]:
other_translations[i] += c
else:
oov[i] += c
continue
if ('compare_cross-entropies' in flags and flags['compare_cross-entropies']):
tmp_weights = [[0]*i+[1]+[0]*(num_results-i-1)]*model_interface.number_of_features
elif ('intersected_cross-entropies' in flags and flags['intersected_cross-entropies']):
tmp_weights = weights
features = score(tmp_weights,src,target,model_interface,flags)
else:
features = score(weights,src,target,model_interface,flags)
#if weight is so low that feature gets probability zero
if 0 in features:
#sys.stderr.write('Warning: 0 probability in model {0}: source phrase: {1!r}; target phrase: {2!r}\n'.format(i,src,target))
#sys.stderr.write('Possible reasons: 0 probability in phrase table; very low (or 0) weight; recompute lexweight and different alignments\n')
#sys.stderr.write('Phrase pair is ignored for cross_entropy calculation\n\n')
continue
n[i] += c
for j in range(model_interface.number_of_features):
cross_entropies[j][i] -= log(features[j],2)*c
elif src in model_interface.phrase_source and not ('compare_cross-entropies' in flags and flags['compare_cross-entropies']):
other_translations[i] += c
else:
oov2 += c
total_pairs += c
oov2 = int(oov2/num_results)
for i in range(num_results):
try:
for j in range(model_interface.number_of_features):
cross_entropies[j][i] /= n[i]
except ZeroDivisionError:
sys.stderr.write('Warning: no matching phrase pairs between reference set and model\n')
for j in range(model_interface.number_of_features):
cross_entropies[j][i] = 0
if 'compare_cross-entropies' in flags and flags['compare_cross-entropies']:
return [tuple([ce[i] for ce in cross_entropies]) + (other_translations[i],oov[i],ignored[i],n[i],total_pairs) for i in range(num_results)], (n[0],total_pairs,oov2)
else:
return tuple([ce[0] for ce in cross_entropies]) + (other_translations[0],oov2,total_pairs)
def cross_entropy_light(model_interface,reference_interface,weights,score,mode,flags,cache):
"""calculate cross entropy given all necessary information.
don't call this directly, but use one of the Combine_TMs methods.
Same as cross_entropy, but optimized for speed: it doesn't generate all of the statistics,
doesn't normalize, and uses caching.
"""
weights = normalize_weights(weights,mode,flags)
cross_entropies = [0]*model_interface.number_of_features
for (src,target,c) in cache:
features = score(weights,src,target,model_interface,flags,cache=True)
if 0 in features:
#sys.stderr.write('Warning: 0 probability in model {0}: source phrase: {1!r}; target phrase: {2!r}\n'.format(i,src,target))
#sys.stderr.write('Possible reasons: 0 probability in phrase table; very low (or 0) weight; recompute lexweight and different alignments\n')
#sys.stderr.write('Phrase pair is ignored for cross_entropy calculation\n\n')
continue
for i in range(model_interface.number_of_features):
cross_entropies[i] -= log(features[i],2)*c
return cross_entropies
def _get_reference_cache(reference_interface,model_interface):
"""creates a data structure that allows for a quick access
to all relevant reference set phrase/word pairs and their frequencies.
"""
cache = []
n = 0
for src in reference_interface.word_pairs:
for target in reference_interface.word_pairs[src]:
if src in model_interface.phrase_pairs and target in model_interface.phrase_pairs[src]:
c = reference_interface.word_pairs[src][target]
cache.append((src,target,c))
n += c
return cache,n
def _get_lexical_filter(reference_interface,model_interface):
"""returns dictionaries that store the words and word pairs needed
for perplexity optimization. We can use these dicts to load fewer data into memory for optimization."""
e2f_filter = defaultdict(set)
f2e_filter = defaultdict(set)
for src in reference_interface.word_pairs:
for target in reference_interface.word_pairs[src]:
if src in model_interface.phrase_pairs and target in model_interface.phrase_pairs[src]:
e2f_alignment,f2e_alignment = model_interface.get_word_alignments(src,target)
for s,t_list in e2f_alignment:
for t in t_list:
e2f_filter[s].add(t)
for t,s_list in f2e_alignment:
for s in s_list:
f2e_filter[t].add(s)
return e2f_filter,f2e_filter
def _hillclimb_move(weights,stepsize,mode,flags):
"""Move function for hillclimb algorithm. Updates each weight by stepsize."""
for i,w in enumerate(weights):
yield normalize_weights(weights[:i]+[w+stepsize]+weights[i+1:],mode,flags)
for i,w in enumerate(weights):
new = w-stepsize
if new >= 1e-10:
yield normalize_weights(weights[:i]+[new]+weights[i+1:],mode,flags)
def _hillclimb(scores,best_weights,objective,model_interface,reference_interface,score_function,mode,flags,precision,cache,n):
"""first (deprecated) implementation of iterative weight optimization."""
best = objective(best_weights)
i = 0 #counts number of iterations with same stepsize: if greater than 10, it is doubled
stepsize = 512 # initial stepsize
move = 1 #whether we found a better set of weights in the current iteration. if not, it is halfed
sys.stderr.write('Hillclimb: step size: ' + str(stepsize))
while stepsize > 0.0078:
if not move:
stepsize /= 2
sys.stderr.write(' ' + str(stepsize))
i = 0
move = 1
continue
move = 0
for w in _hillclimb_move(list(best_weights),stepsize,mode,flags):
weights_tuple = tuple(w)
if weights_tuple in scores:
continue
scores[weights_tuple] = cross_entropy_light(model_interface,reference_interface,[w for m in range(model_interface.number_of_features)],score_function,mode,flags,cache)
if objective(weights_tuple)+precision < best:
best = objective(weights_tuple)
best_weights = weights_tuple
move = 1
if i and not i % 10:
sys.stderr.write('\nIteration '+ str(i) + ' with stepsize ' + str(stepsize) + '. current cross-entropy: ' + str(best) + '- weights: ' + str(best_weights) + ' ')
stepsize *= 2
sys.stderr.write('\nIncreasing stepsize: '+ str(stepsize))
i = 0
i += 1
return best_weights
def optimize_cross_entropy_hillclimb(model_interface,reference_interface,initial_weights,score_function,mode,flags,precision=0.000001):
"""find weights that minimize cross-entropy on a tuning set
deprecated (default is now L-BFGS (optimize_cross_entropy)), but left in for people without SciPy
"""
scores = {}
best_weights = tuple(initial_weights[0])
cache,n = _get_reference_cache(reference_interface,model_interface)
# each objective is a triple: which score to minimize from cross_entropy(), which weights to update accordingly, and a comment that is printed
objectives = [(lambda x: scores[x][i]/n,[i],'minimize cross-entropy for feature {0}'.format(i)) for i in range(model_interface.number_of_features)]
scores[best_weights] = cross_entropy_light(model_interface,reference_interface,initial_weights,score_function,mode,flags,cache)
final_weights = initial_weights[:]
final_cross_entropy = [0]*model_interface.number_of_features
for i,(objective, features, comment) in enumerate(objectives):
best_weights = min(scores,key=objective)
sys.stderr.write('Optimizing objective "' + comment +'"\n')
best_weights = _hillclimb(scores,best_weights,objective,model_interface,reference_interface,score_function,feature_specific_mode(mode,i,flags),flags,precision,cache,n)
sys.stderr.write('\nCross-entropy:' + str(objective(best_weights)) + ' - weights: ' + str(best_weights)+'\n\n')
for j in features:
final_weights[j] = list(best_weights)
final_cross_entropy[j] = objective(best_weights)
return final_weights,final_cross_entropy
def optimize_cross_entropy(model_interface,reference_interface,initial_weights,score_function,mode,flags):
"""find weights that minimize cross-entropy on a tuning set
Uses L-BFGS optimization and requires SciPy
"""
if not optimizer == 'l-bfgs':
sys.stderr.write('SciPy is not installed. Falling back to naive hillclimb optimization (instead of L-BFGS)\n')
return optimize_cross_entropy_hillclimb(model_interface,reference_interface,initial_weights,score_function,mode,flags)
cache,n = _get_reference_cache(reference_interface,model_interface)
# each objective is a triple: which score to minimize from cross_entropy(), which weights to update accordingly, and a comment that is printed
objectives = [(lambda w: cross_entropy_light(model_interface,reference_interface,[[1]+list(w) for m in range(model_interface.number_of_features)],score_function,feature_specific_mode(mode,i,flags),flags,cache)[i],[i],'minimize cross-entropy for feature {0}'.format(i)) for i in range(model_interface.number_of_features)] #optimize cross-entropy for p(s|t)
final_weights = initial_weights[:]
final_cross_entropy = [0]*model_interface.number_of_features
for i,(objective, features, comment) in enumerate(objectives):
sys.stderr.write('Optimizing objective "' + comment +'"\n')
initial_values = [1]*(len(model_interface.models)-1) # we leave value of first model at 1 and optimize all others (normalized of course)
best_weights, best_point, data = fmin_l_bfgs_b(objective,initial_values,approx_grad=True,bounds=[(0.000000001,None)]*len(initial_values))
best_weights = normalize_weights([1]+list(best_weights),feature_specific_mode(mode,i,flags),flags)
sys.stderr.write('Cross-entropy after L-BFGS optimization: ' + str(best_point/n) + ' - weights: ' + str(best_weights)+'\n')
for j in features:
final_weights[j] = list(best_weights)
final_cross_entropy[j] = best_point/n
return final_weights,final_cross_entropy
def feature_specific_mode(mode,i,flags):
"""in mode 'counts', only the default Moses features can be recomputed from raw frequencies;
all other features are interpolated by default.
This fucntion mostly serves optical purposes (i.e. normalizing a single weight vector for logging),
since normalize_weights also handles a mix of interpolated and recomputed features.
"""
if mode == 'counts' and i not in [flags['i_e2f'],flags['i_e2f_lex'],flags['i_f2e'],flags['i_f2e_lex']]:
return 'interpolate'
else:
return mode
def redistribute_probability_mass(weights,src,target,interface,flags,mode='interpolate'):
"""the conditional probability p(x|y) is undefined for cases where p(y) = 0
this function redistributes the probability mass to only consider models for which p(y) > 0
"""
i_e2f = flags['i_e2f']
i_e2f_lex = flags['i_e2f_lex']
i_f2e = flags['i_f2e']
i_f2e_lex = flags['i_f2e_lex']
new_weights = weights[:]
if flags['normalize_s_given_t'] == 's':
# set weight to 0 for all models where target phrase is unseen (p(s|t)
new_weights[i_e2f] = map(mul,interface.phrase_source[src],weights[i_e2f])
if flags['normalize-lexical_weights']:
new_weights[i_e2f_lex] = map(mul,interface.phrase_source[src],weights[i_e2f_lex])
elif flags['normalize_s_given_t'] == 't':
# set weight to 0 for all models where target phrase is unseen (p(s|t)
new_weights[i_e2f] = map(mul,interface.phrase_target[target],weights[i_e2f])
if flags['normalize-lexical_weights']:
new_weights[i_e2f_lex] = map(mul,interface.phrase_target[target],weights[i_e2f_lex])
# set weight to 0 for all models where source phrase is unseen (p(t|s)
new_weights[i_f2e] = map(mul,interface.phrase_source[src],weights[i_f2e])
if flags['normalize-lexical_weights']:
new_weights[i_f2e_lex] = map(mul,interface.phrase_source[src],weights[i_f2e_lex])
return normalize_weights(new_weights,mode,flags)
def score_interpolate(weights,src,target,interface,flags,cache=False):
"""linear interpolation of probabilites (and other feature values)
if normalized is True, the probability mass for p(x|y) is redistributed to models with p(y) > 0
"""
model_values = interface.phrase_pairs[src][target][0]
scores = [0]*len(model_values)
if 'normalized' in flags and flags['normalized']:
normalized_weights = redistribute_probability_mass(weights,src,target,interface,flags)
else:
normalized_weights = weights
if 'recompute_lexweights' in flags and flags['recompute_lexweights']:
e2f_alignment,f2e_alignment = interface.get_word_alignments(src,target,cache=cache)
if not e2f_alignment or not f2e_alignment:
sys.stderr.write('Error: no word alignments found, but necessary for lexical weight computation.\n')
lst = 0
lts = 0
else:
scores[flags['i_e2f_lex']] = compute_lexicalweight(normalized_weights[flags['i_e2f_lex']],e2f_alignment,interface.word_pairs_e2f,None,mode='interpolate')
scores[flags['i_f2e_lex']] = compute_lexicalweight(normalized_weights[flags['i_f2e_lex']],f2e_alignment,interface.word_pairs_f2e,None,mode='interpolate')
for idx,prob in enumerate(model_values):
if not ('recompute_lexweights' in flags and flags['recompute_lexweights'] and (idx == flags['i_e2f_lex'] or idx == flags['i_f2e_lex'])):
scores[idx] = dot_product(prob,normalized_weights[idx])
return scores
def score_loglinear(weights,src,target,interface,flags,cache=False):
"""loglinear interpolation of probabilites
warning: if phrase pair does not occur in all models, resulting probability is 0
this is usually not what you want - loglinear scoring is only included for completeness' sake
"""
scores = []
model_values = interface.phrase_pairs[src][target][0]
for idx,prob in enumerate(model_values):
try:
scores.append(exp(dot_product(map(log,prob),weights[idx])))
except ValueError:
scores.append(0)
return scores
def score_counts(weights,src,target,interface,flags,cache=False):
"""count-based re-estimation of probabilites and lexical weights
each count is multiplied by its weight; trivial case is weight 1 for each model, which corresponds to a concatentation
"""
i_e2f = flags['i_e2f']
i_e2f_lex = flags['i_e2f_lex']
i_f2e = flags['i_f2e']
i_f2e_lex = flags['i_f2e_lex']
# if we have non-default number of weights, assume that we might have to do a mix of count-based and interpolated scores.
if len(weights) == 4:
scores = [0]*len(weights)
else:
scores = score_interpolate(weights,src,target,interface,flags,cache=cache)
try:
joined_count = dot_product(interface.phrase_pairs[src][target][0][i_e2f],weights[i_e2f])
target_count = dot_product(interface.phrase_target[target],weights[i_e2f])
scores[i_e2f] = joined_count / target_count
except ZeroDivisionError:
scores[i_e2f] = 0
try:
joined_count = dot_product(interface.phrase_pairs[src][target][0][i_f2e],weights[i_f2e])
source_count = dot_product(interface.phrase_source[src],weights[i_f2e])
scores[i_f2e] = joined_count / source_count
except ZeroDivisionError:
scores[i_f2e] = 0
e2f_alignment,f2e_alignment = interface.get_word_alignments(src,target,cache=cache)
if not e2f_alignment or not f2e_alignment:
sys.stderr.write('Error: no word alignments found, but necessary for lexical weight computation.\n')
scores[i_e2f_lex] = 0
scores[i_f2e_lex] = 0
else:
scores[i_e2f_lex] = compute_lexicalweight(weights[i_e2f_lex],e2f_alignment,interface.word_pairs_e2f,interface.word_target,mode='counts',cache=cache)
scores[i_f2e_lex] = compute_lexicalweight(weights[i_f2e_lex],f2e_alignment,interface.word_pairs_f2e,interface.word_source,mode='counts',cache=cache)
return scores
def score_interpolate_reordering(weights,src,target,interface):
"""linear interpolation of reordering model probabilities
also normalizes model so that
"""
model_values = interface.reordering_pairs[src][target]
scores = [0]*len(model_values)
for idx,prob in enumerate(model_values):
scores[idx] = dot_product(prob,weights[idx])
#normalizes first half and last half probabilities (so that each half sums to one).
#only makes sense for bidirectional configuration in Moses. Remove/change this if you want a different (or no) normalization
scores = normalize_weights(scores[:int(interface.number_of_features/2)],'interpolate') + normalize_weights(scores[int(interface.number_of_features/2):],'interpolate')
return scores
def compute_lexicalweight(weights,alignment,word_pairs,marginal,mode='counts',cache=False,mycache=[0,defaultdict(dict)]):
"""compute the lexical weights as implemented in Moses toolkit"""
lex = 1
# new weights: empty cache
if cache and mycache[0] != weights:
mycache[0] = weights
mycache[1] = defaultdict(dict)
for x,translations in alignment:
if cache and translations in mycache[1][x]:
lex_step = mycache[1][x][translations]
else:
lex_step = 0
for y in translations:
if mode == 'counts':
lex_step += dot_product(word_pairs[x][y],weights) / dot_product(marginal[y],weights)
elif mode == 'interpolate':
lex_step += dot_product(word_pairs[x][y],weights)
lex_step /= len(translations)
if cache:
mycache[1][x][translations] = lex_step
lex *= lex_step
return lex
def normalize_weights(weights,mode,flags=None):
"""make sure that probability mass in linear interpolation is 1
for weighted counts, weight of first model is set to 1
"""
if mode == 'interpolate' or mode == 'loglinear':
if type(weights[0]) == list:
new_weights = []
for weight_list in weights:
total = sum(weight_list)
try:
weight_list = [weight/total for weight in weight_list]
except ZeroDivisionError:
sys.stderr.write('Error: Zero division in weight normalization. Are some of your weights zero? This might lead to undefined behaviour if a phrase pair is only seen in model with weight 0\n')
new_weights.append(weight_list)
else:
total = sum(weights)
try:
new_weights = [weight/total for weight in weights]
except ZeroDivisionError:
sys.stderr.write('Error: Zero division in weight normalization. Are some of your weights zero? This might lead to undefined behaviour if a phrase pair is only seen in model with weight 0\n')
elif mode == 'counts_pure':
if type(weights[0]) == list:
new_weights = []
for weight_list in weights:
ratio = 1/weight_list[0]
new_weights.append([weight * ratio for weight in weight_list])
else:
ratio = 1/weights[0]
new_weights = [weight * ratio for weight in weights]
# make sure that features other than the standard Moses features are always interpolated (since no count-based computation is defined)
elif mode == 'counts':
if type(weights[0]) == list:
norm_counts = normalize_weights(weights,'counts_pure')
new_weights = normalize_weights(weights,'interpolate')
for i in [flags['i_e2f'],flags['i_e2f_lex'],flags['i_f2e'],flags['i_f2e_lex']]:
new_weights[i] = norm_counts[i]
return new_weights
else:
return normalize_weights(weights,'counts_pure')
return new_weights
def handle_file(filename,action,fileobj=None,mode='r'):
"""support reading/writing either from/to file, stdout or gzipped file"""
if action == 'open':
if mode == 'r':
mode = 'rb'
if mode == 'rb' and not filename == '-' and not os.path.exists(filename):
if os.path.exists(filename+'.gz'):
filename = filename+'.gz'
else:
sys.stderr.write('Error: unable to open file. ' + filename + ' - aborting.\n')
if 'counts' in filename and os.path.exists(os.path.dirname(filename)):
sys.stderr.write('For a weighted counts combination, we need statistics that Moses doesn\'t write to disk by default.\n')
sys.stderr.write('Repeat step 4 of Moses training for all models with the option -write-lexical-counts.\n')
exit()
if filename.endswith('.gz'):
fileobj = gzip.open(filename,mode)
elif filename == '-' and mode == 'w':
fileobj = sys.stdout
else:
fileobj = open(filename,mode)
return fileobj
elif action == 'close' and filename != '-':
fileobj.close()
def sort_file(filename,tempdir=None):
"""Sort a file and return temporary file"""
cmd = ['sort', filename]
env = {}
env['LC_ALL'] = 'C'
if tempdir:
cmd.extend(['-T',tempdir])
outfile = NamedTemporaryFile(delete=False,dir=tempdir)
sys.stderr.write('LC_ALL=C ' + ' '.join(cmd) + ' > ' + outfile.name + '\n')
p = Popen(cmd,env=env,stdout=outfile.file)
p.wait()
outfile.seek(0)
return outfile
class Combine_TMs():
"""This class handles the various options, checks them for sanity and has methods that define what models to load and what functions to call for the different tasks.
Typically, you only need to interact with this class and its attributes.
"""
#some flags that change the behaviour during scoring. See init docstring for more info
flags = {'normalized':False,
'recompute_lexweights':False,
'intersected_cross-entropies':False,
'normalize_s_given_t':None,
'normalize-lexical_weights':True,
'add_origin_features':False,
'lowmem': False,
'i_e2f':0,
'i_e2f_lex':1,
'i_f2e':2,
'i_f2e_lex':3
}
# each model needs a priority. See init docstring for more info
_priorities = {'primary':1,
'map':2,
'supplementary':10}
def __init__(self,models,weights=None,
output_file=None,
mode='interpolate',
number_of_features=4,
model_interface=Moses,
reference_interface=Moses_Alignment,
reference_file=None,
lang_src=None,
lang_target=None,
output_lexical=None,
**flags):
"""The whole configuration of the task is done during intialization. Afterwards, you only need to call your intended method(s).
You can change some of the class attributes afterwards (such as the weights, or the output file), but you should never change the models or mode after initialization.
See unit_test function for example configurations
models: list of tuples (path,priority) that defines which models to process. Path is usually the top directory of a Moses model. There are three priorities:
'primary': phrase pairs with this priority will always be included in output model. For most purposes, you'll want to define all models as primary.
'map': for maximum a-posteriori combination (Bacchiani et al. 2004; Foster et al. 2010). for use with mode 'counts'. stores c(t) = 1 and c(s,t) = p(s|t)
'supplementary': phrase pairs are considered for probability computation, but not included in output model (unless they also occur in at least one primary model)
useful for rescoring a model without changing its vocabulary.
weights: accept two types of weight declarations: one weight per model, and one weight per model and feature
type one is internally converted to type two. For 2 models with four features, this looks like: [0.1,0.9] -> [[0.1,0.9],[0.1,0.9],[0.1,0.9],[0.1,0.9]]
default: uniform weights (None)
output_file: filepath of output phrase table. If it ends with .gz, file is automatically zipped.
output_lexical: If defined, also writes combined lexical tables. Writes to output_lexical.e2f and output_lexical.f2e, or output_lexical.counts.e2f in mode 'counts'.
mode: declares the basic mixture-model algorithm. there are currently three options:
'counts': weighted counts (requires some statistics that Moses doesn't produce. Repeat step 4 of Moses training with the option -write-lexical-counts to obtain them.)
Only the standard Moses features are recomputed from weighted counts; additional features are linearly interpolated
(see number_of_features to allow more features, and i_e2f etc. if the standard features are in a non-standard position)
'interpolate': linear interpolation
'loglinear': loglinear interpolation (careful: this creates the intersection of phrase tables and is often of little use)
number_of_features: could be used to interpolate models with non-default Moses features. 4 features is currently still hardcoded in various places
(e.g. cross_entropy calculations, mode 'counts')
i_e2f,i_e2f_lex,i_f2e,i_f2e_lex: Index of the (Moses) phrase table features p(s|t), lex(s|t), p(t|s) and lex(t|s).
Relevant for mode 'counts', and if 'recompute_lexweights' is True in mode 'interpolate'. In mode 'counts', any additional features are combined through linear interpolation.
model_interface: class that handles reading phrase tables and lexical tables, and writing phrase tables. Currently only Moses is implemented.
default: Moses
reference_interace: class that deals with reading in reference phrase pairs for cross-entropy computation
Moses_Alignment: Word/phrase pairs as computed by Giza++ and extracted through Moses heuristics. This corresponds to the file model/extract.gz if you train a Moses model on your tuning set.
TigerXML: TigerXML data format
default: Moses_Alignment
reference_file: path to reference file. Required for every operation except combination of models with given weights.
lang_src: source language. Only required if reference_interface is TigerXML. Identifies which language in XML file we should treat as source language.
lang_target: target language. Only required if reference_interface is TigerXML. Identifies which language in XML file we should treat as target language.
intersected_cross-entropies: compute cross-entropies of intersection of phrase pairs, ignoring phrase pairs that do not occur in all models.
If False, algorithm operates on union of phrase pairs
default: False
add_origin_features: For each model that is being combined, add a binary feature to the final phrase table, with values of 1 (phrase pair doesn't occur in model) and 2.718 (it does).
This indicates which model(s) a phrase pair comes from and can be used during MERT to additionally reward/penalize translation models
lowmem: low memory mode: instead of loading target phrase counts / probability (when required), process the original table and its inversion (source and target swapped) incrementally, then merge the two halves.
tempdir: temporary directory (for low memory mode).
there are a number of further configuration options that you can define, which modify the algorithm for linear interpolation. They have no effect in mode 'counts'
recompute_lexweights: don't directly interpolate lexical weights, but interpolate word translation probabilities instead and recompute the lexical weights.
default: False
normalized: for interpolation of p(x|y): if True, models with p(y)=0 will be ignored, and probability mass will be distributed among models with p(y)>0.
If False, missing entries (x,y) are always interpreted as p(x|y)=0.
default: False
normalize_s_given_t: How to we normalize p(s|t) if 'normalized' is True? Three options:
None: don't normalize p(s|t) and lex(s|t) (only p(t|s) and lex(t|s))
t: check if p(t)==0 : advantage: theoretically sound; disadvantage: slower (we need to know if t occcurs in model); favours rare target phrases (relative to default choice)
s: check if p(s)==0 : advantage: relevant for task; disadvantage: no true probability distributions
default: None
normalize-lexical_weights: also normalize lex(s|t) and lex(t|s) if 'normalized' ist True:
reason why you might want to disable this: lexical weights suffer less from data sparseness than probabilities.
default: True
"""
self.mode = mode
self.output_file = output_file
self.lang_src = lang_src
self.lang_target = lang_target
self.loaded = defaultdict(int)
self.output_lexical = output_lexical
self.flags = copy.copy(self.flags)
self.flags.update(flags)
self.flags['i_e2f'] = int(self.flags['i_e2f'])
self.flags['i_e2f_lex'] = int(self.flags['i_e2f_lex'])
self.flags['i_f2e'] = int(self.flags['i_f2e'])
self.flags['i_f2e_lex'] = int(self.flags['i_f2e_lex'])
if reference_interface:
self.reference_interface = reference_interface(reference_file)
if mode not in ['interpolate','loglinear','counts']:
sys.stderr.write('Error: mode must be either "interpolate", "loglinear" or "counts"\n')
sys.exit()
models,number_of_features,weights = self._sanity_checks(models,number_of_features,weights)
self.weights = weights
self.models = models
self.model_interface = model_interface(models,number_of_features)
if mode == 'interpolate':
self.score = score_interpolate
elif mode == 'loglinear':
self.score = score_loglinear
elif mode == 'counts':
self.score = score_counts
def _sanity_checks(self,models,number_of_features,weights):
"""check if input arguments make sense (correct number of weights, valid model priorities etc.)
is only called on initialization. If you change weights afterwards, better know what you're doing.
"""
number_of_features = int(number_of_features)
for (model,priority) in models:
assert(priority in self._priorities)
models = [(model,self._priorities[p]) for (model,p) in models]
# accept two types of weight declarations: one weight per model, and one weight per model and feature
# type one is internally converted to type two: [0.1,0.9] -> [[0.1,0.9],[0.1,0.9],[0.1,0.9],[0.1,0.9]]
if weights:
if type(weights[0]) == list:
assert(len(weights)==number_of_features)
for sublist in weights:
assert(len(sublist)==len(models))
else:
assert(len(models) == len(weights))
weights = [weights for i in range(number_of_features)]
else:
if self.mode == 'loglinear' or self.mode == 'interpolate':
weights = [[1/len(models)]*len(models) for i in range(number_of_features)]
elif self.mode == 'counts':
weights = [[1]*len(models) for i in range(number_of_features)]
sys.stderr.write('Warning: No weights defined: initializing with uniform weights\n')
new_weights = normalize_weights(weights,self.mode,self.flags)
if weights != new_weights:
if self.mode == 'interpolate' or self.mode == 'loglinear':
sys.stderr.write('Warning: weights should sum to 1 - ')
elif self.mode == 'counts':
sys.stderr.write('Warning: normalizing weights so that first model has weight 1 (for features that are recomputed from counts) - ')
sys.stderr.write('normalizing to: '+ str(new_weights) +'\n')
weights = new_weights
return models,number_of_features,weights
def _ensure_loaded(self,data):
"""load data (lexical tables; reference alignment; phrase table), if it isn't already in memory"""
if 'lexical' in data:
self.model_interface.require_alignment = True
if 'reference' in data and not self.loaded['reference']:
sys.stderr.write('Loading word pairs from reference set...')
self.reference_interface.load_word_pairs(self.lang_src,self.lang_target)
sys.stderr.write('done\n')
self.loaded['reference'] = 1
if 'lexical' in data and not self.loaded['lexical']:
sys.stderr.write('Loading lexical tables...')
self.model_interface.load_lexical_tables(self.models,self.mode)
sys.stderr.write('done\n')
self.loaded['lexical'] = 1
if 'pt-filtered' in data and not self.loaded['pt-filtered']:
models_prioritized = [(self.model_interface.open_table(model,'phrase-table'),priority,i) for (model,priority,i) in priority_sort_models(self.models)]
for model,priority,i in models_prioritized:
sys.stderr.write('Loading phrase table ' + str(i) + ' (only data relevant for reference set)')
j = 0
for line in model:
if not j % 1000000:
sys.stderr.write('...'+str(j))
j += 1
line = line.rstrip().split(b' ||| ')
self.model_interface.load_phrase_features(line,priority,i,store='all',mode=self.mode,filter_by=self.reference_interface.word_pairs,filter_by_src=self.reference_interface.word_source,filter_by_target=self.reference_interface.word_target,flags=self.flags)
sys.stderr.write(' done\n')
self.loaded['pt-filtered'] = 1
if 'lexical-filtered' in data and not self.loaded['lexical-filtered']:
e2f_filter, f2e_filter = _get_lexical_filter(self.reference_interface,self.model_interface)
sys.stderr.write('Loading lexical tables (only data relevant for reference set)...')
self.model_interface.load_lexical_tables(self.models,self.mode,e2f_filter=e2f_filter,f2e_filter=f2e_filter)
sys.stderr.write('done\n')
self.loaded['lexical-filtered'] = 1
if 'pt-target' in data and not self.loaded['pt-target']:
models_prioritized = [(self.model_interface.open_table(model,'phrase-table'),priority,i) for (model,priority,i) in priority_sort_models(self.models)]
for model,priority,i in models_prioritized:
sys.stderr.write('Loading target information from phrase table ' + str(i))
j = 0
for line in model:
if not j % 1000000:
sys.stderr.write('...'+str(j))
j += 1
line = line.rstrip().split(b' ||| ')
self.model_interface.load_phrase_features(line,priority,i,mode=self.mode,store='target',flags=self.flags)
sys.stderr.write(' done\n')
self.loaded['pt-target'] = 1
def _inverse_wrapper(self,weights,tempdir=None):
"""if we want to invert the phrase table to better calcualte p(s|t) and lex(s|t), manage creation, sorting and merging of inverted phrase tables"""
sys.stderr.write('Processing first table half\n')
models = [(self.model_interface.open_table(model,'phrase-table'),priority,i) for (model,priority,i) in priority_sort_models(self.model_interface.models)]
pt_half1 = NamedTemporaryFile(prefix='half1',delete=False,dir=tempdir)
self._write_phrasetable(models,pt_half1,weights)
pt_half1.seek(0)
sys.stderr.write('Inverting tables\n')
models = [(self.model_interface.create_inverse(self.model_interface.open_table(model,'phrase-table'),tempdir=tempdir),priority,i) for (model,priority,i) in priority_sort_models(self.model_interface.models)]
sys.stderr.write('Processing second table half\n')
pt_half2_inverted = NamedTemporaryFile(prefix='half2',delete=False,dir=tempdir)
self._write_phrasetable(models,pt_half2_inverted,weights,inverted=True)
pt_half2_inverted.close()
for model,priority,i in models:
model.close()
os.remove(model.name)
pt_half2 = sort_file(pt_half2_inverted.name,tempdir=tempdir)
os.remove(pt_half2_inverted.name)
sys.stderr.write('Merging tables: first half: {0} ; second half: {1} ; final table: {2}\n'.format(pt_half1.name,pt_half2.name,self.output_file))
output_object = handle_file(self.output_file,'open',mode='w')
self.model_interface.merge(pt_half1,pt_half2,output_object,self.mode)
os.remove(pt_half1.name)
os.remove(pt_half2.name)
handle_file(self.output_file,'close',output_object,mode='w')
def _write_phrasetable(self,models,output_object,weights,inverted=False):
"""Incrementally load phrase tables, calculate score for increment and write it to output_object"""
# define which information we need to store from the phrase table
# possible flags: 'all', 'target', 'source' and 'pairs'
# interpolated models without re-normalization only need 'pairs', otherwise 'all' is the correct choice
store_flag = 'all'
if self.mode == 'interpolate' and not self.flags['normalized']:
store_flag = 'pairs'
i = 0
sys.stderr.write('Incrementally loading and processing phrase tables...')
for block in self.model_interface.traverse_incrementally('phrase-table',models,self.model_interface.load_phrase_features,store_flag,mode=self.mode,inverted=inverted,lowmem=self.flags['lowmem'],flags=self.flags):
for src in sorted(self.model_interface.phrase_pairs, key = lambda x: x + b' |'):
for target in sorted(self.model_interface.phrase_pairs[src], key = lambda x: x + b' |'):
if not i % 1000000:
sys.stderr.write(str(i) + '...')
i += 1
features = self.score(weights,src,target,self.model_interface,self.flags)
outline = self.model_interface.write_phrase_table(src,target,weights,features,self.mode, self.flags)
output_object.write(outline)
sys.stderr.write('done\n')
def combine_given_weights(self,weights=None):
"""write a new phrase table, based on existing weights"""
if not weights:
weights = self.weights
data = []
if self.mode == 'counts':
data.append('lexical')
if not self.flags['lowmem']:
data.append('pt-target')
elif self.mode == 'interpolate':
if self.flags['recompute_lexweights']:
data.append('lexical')
if self.flags['normalized'] and self.flags['normalize_s_given_t'] == 't' and not self.flags['lowmem']:
data.append('pt-target')
self._ensure_loaded(data)
if self.flags['lowmem'] and (self.mode == 'counts' or self.flags['normalized'] and self.flags['normalize_s_given_t'] == 't'):
self._inverse_wrapper(weights,tempdir=self.flags['tempdir'])
else:
models = [(self.model_interface.open_table(model,'phrase-table'),priority,i) for (model,priority,i) in priority_sort_models(self.model_interface.models)]
output_object = handle_file(self.output_file,'open',mode='w')
self._write_phrasetable(models,output_object,weights)
handle_file(self.output_file,'close',output_object,mode='w')
if self.output_lexical:
sys.stderr.write('Writing lexical tables\n')
self._ensure_loaded(['lexical'])
self.model_interface.write_lexical_file('e2f',self.output_lexical,weights[1],self.mode)
self.model_interface.write_lexical_file('f2e',self.output_lexical,weights[3],self.mode)
def combine_given_tuning_set(self):
"""write a new phrase table, using the weights that minimize cross-entropy on a tuning set"""
data = ['reference','pt-filtered']
if self.mode == 'counts' or (self.mode == 'interpolate' and self.flags['recompute_lexweights']):
data.append('lexical-filtered')
self._ensure_loaded(data)
best_weights,best_cross_entropy = optimize_cross_entropy(self.model_interface,self.reference_interface,self.weights,self.score,self.mode,self.flags)
sys.stderr.write('Best weights: ' + str(best_weights) + '\n')
sys.stderr.write('Cross entropies: ' + str(best_cross_entropy) + '\n')
sys.stderr.write('Executing action combine_given_weights with -w "{0}"\n'.format('; '.join([', '.join(str(w) for w in item) for item in best_weights])))
self.loaded['pt-filtered'] = False # phrase table will be overwritten
self.combine_given_weights(weights=best_weights)
def combine_reordering_tables(self,weights=None):
"""write a new reordering table, based on existing weights."""
if not weights:
weights = self.weights
data = []
if self.mode != 'interpolate':
sys.stderr.write('Error: only linear interpolation is supported for reordering model combination')
output_object = handle_file(self.output_file,'open',mode='w')
models = [(self.model_interface.open_table(model,'reordering-table'),priority,i) for (model,priority,i) in priority_sort_models(self.models)]
i = 0
sys.stderr.write('Incrementally loading and processing phrase tables...')
for block in self.model_interface.traverse_incrementally('reordering-table',models,self.model_interface.load_reordering_probabilities,'pairs',mode=self.mode,lowmem=self.flags['lowmem'],flags=self.flags):
for src in sorted(self.model_interface.reordering_pairs):
for target in sorted(self.model_interface.reordering_pairs[src]):
if not i % 1000000:
sys.stderr.write(str(i) + '...')
i += 1
features = score_interpolate_reordering(weights,src,target,self.model_interface)
outline = self.model_interface.write_reordering_table(src,target,features)
output_object.write(outline)
sys.stderr.write('done\n')
handle_file(self.output_file,'close',output_object,mode='w')
def compare_cross_entropies(self):
"""print cross-entropies for each model/feature, using the intersection of phrase pairs.
analysis tool.
"""
self.flags['compare_cross-entropies'] = True
data = ['reference','pt-filtered']
if self.mode == 'counts' or (self.mode == 'interpolate' and self.flags['recompute_lexweights']):
data.append('lexical-filtered')
self._ensure_loaded(data)
results, (intersection,total_pairs,oov2) = cross_entropy(self.model_interface,self.reference_interface,self.weights,self.score,self.mode,self.flags)
padding = 90
num_features = self.model_interface.number_of_features
print('\nResults of model comparison\n')
print('{0:<{padding}}: {1}'.format('phrase pairs in reference (tokens)',total_pairs, padding=padding))
print('{0:<{padding}}: {1}'.format('phrase pairs in model intersection (tokens)',intersection, padding=padding))
print('{0:<{padding}}: {1}\n'.format('phrase pairs in model union (tokens)',total_pairs-oov2, padding=padding))
for i,data in enumerate(results):
cross_entropies = data[:num_features]
(other_translations,oov,ignored,n,total_pairs) = data[num_features:]
print('model ' +str(i))
for j in range(num_features):
print('{0:<{padding}}: {1}'.format('cross-entropy for feature {0}'.format(j), cross_entropies[j], padding=padding))
print('{0:<{padding}}: {1}'.format('phrase pairs in model (tokens)', n+ignored, padding=padding))
print('{0:<{padding}}: {1}'.format('phrase pairs in model, but not in intersection (tokens)', ignored, padding=padding))
print('{0:<{padding}}: {1}'.format('phrase pairs in union, but not in model (but source phrase is) (tokens)', other_translations, padding=padding))
print('{0:<{padding}}: {1}\n'.format('phrase pairs in union, but source phrase not in model (tokens)', oov, padding=padding))
self.flags['compare_cross-entropies'] = False
return results, (intersection,total_pairs,oov2)
def compute_cross_entropy(self):
"""return cross-entropy for a tuning set, a set of models and a set of weights.
analysis tool.
"""
data = ['reference','pt-filtered']
if self.mode == 'counts' or (self.mode == 'interpolate' and self.flags['recompute_lexweights']):
data.append('lexical-filtered')
self._ensure_loaded(data)
current_cross_entropy = cross_entropy(self.model_interface,self.reference_interface,self.weights,self.score,self.mode,self.flags)
sys.stderr.write('Cross entropy: ' + str(current_cross_entropy) + '\n')
return current_cross_entropy
def return_best_cross_entropy(self):
"""return the set of weights and cross-entropy that is optimal for a tuning set and a set of models."""
data = ['reference','pt-filtered']
if self.mode == 'counts' or (self.mode == 'interpolate' and self.flags['recompute_lexweights']):
data.append('lexical-filtered')
self._ensure_loaded(data)
best_weights,best_cross_entropy = optimize_cross_entropy(self.model_interface,self.reference_interface,self.weights,self.score,self.mode,self.flags)
sys.stderr.write('Best weights: ' + str(best_weights) + '\n')
sys.stderr.write('Cross entropies: ' + str(best_cross_entropy) + '\n')
sys.stderr.write('You can apply these weights with the action combine_given_weights and the option -w "{0}"\n'.format('; '.join([', '.join(str(w) for w in item) for item in best_weights])))
return best_weights,best_cross_entropy
def test():
"""test (and illustrate) the functionality of the program based on two test phrase tables and a small reference set,"""
# linear interpolation of two models, with fixed weights. Output uses vocabulary of model1 (since model2 is supplementary)
# command line: (currently not possible to define supplementary models through command line)
sys.stderr.write('Regression test 1\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary'],[os.path.join('test','model2'),'supplementary']],[0.5,0.5],os.path.join('test','phrase-table_test1'))
Combiner.combine_given_weights()
# linear interpolation of two models, with fixed weights (but different for each feature).
# command line: python tmcombine.py combine_given_weights test/model1 test/model2 -w "0.1,0.9;0.1,1;0.2,0.8;0.5,0.5" -o test/phrase-table_test2
sys.stderr.write('Regression test 2\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary'],[os.path.join('test','model2'),'primary']],[[0.1,0.9],[0.1,1],[0.2,0.8],[0.5,0.5]],os.path.join('test','phrase-table_test2'))
Combiner.combine_given_weights()
# count-based combination of two models, with fixed weights
# command line: python tmcombine.py combine_given_weights test/model1 test/model2 -w "0.1,0.9;0.1,1;0.2,0.8;0.5,0.5" -o test/phrase-table_test3 -m counts
sys.stderr.write('Regression test 3\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary'],[os.path.join('test','model2'),'primary']],[[0.1,0.9],[0.1,1],[0.2,0.8],[0.5,0.5]],os.path.join('test','phrase-table_test3'),mode='counts')
Combiner.combine_given_weights()
# output phrase table should be identical to model1
# command line: python tmcombine.py combine_given_weights test/model1 -w 1 -o test/phrase-table_test4 -m counts
sys.stderr.write('Regression test 4\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary']],[1],os.path.join('test','phrase-table_test4'),mode='counts')
Combiner.combine_given_weights()
# count-based combination of two models with weights set through perplexity minimization
# command line: python tmcombine.py combine_given_tuning_set test/model1 test/model2 -o test/phrase-table_test5 -m counts -r test/extract
sys.stderr.write('Regression test 5\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary'],[os.path.join('test','model2'),'primary']],output_file=os.path.join('test','phrase-table_test5'),mode='counts',reference_file='test/extract')
Combiner.combine_given_tuning_set()
# loglinear combination of two models with fixed weights
# command line: python tmcombine.py combine_given_weights test/model1 test/model2 -w 0.1,0.9 -o test/phrase-table_test6 -m loglinear
sys.stderr.write('Regression test 6\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary'],[os.path.join('test','model2'),'primary']],weights=[0.1,0.9],output_file=os.path.join('test','phrase-table_test6'),mode='loglinear')
Combiner.combine_given_weights()
# cross-entropy analysis of two models through a reference set
# command line: python tmcombine.py compare_cross_entropies test/model1 test/model2 -m counts -r test/extract
sys.stderr.write('Regression test 7\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary'],[os.path.join('test','model2'),'primary']],mode='counts',reference_file='test/extract')
f = open(os.path.join('test','phrase-table_test7'),'w')
f.write(str(Combiner.compare_cross_entropies()))
f.close()
# maximum a posteriori combination of two models (Bacchiani et al. 2004; Foster et al. 2010) with weights set through cross-entropy minimization
# command line: (currently not possible through command line)
sys.stderr.write('Regression test 8\n')
Combiner = Combine_TMs([[os.path.join('test','model1'),'primary'],[os.path.join('test','model2'),'map']],output_file=os.path.join('test','phrase-table_test8'),mode='counts',reference_file='test/extract')
Combiner.combine_given_tuning_set()
# count-based combination of two non-default models, with fixed weights. Same as test 3, but with the standard features moved back
# command line: python tmcombine.py combine_given_weights test/model3 test/model4 -w "0.5,0.5;0.5,0.5;0.5,0.5;0.5,0.5;0.1,0.9;0.1,1;0.2,0.8;0.5,0.5" -o test/phrase-table_test9 -m counts --number_of_features 8 --i_e2f 4 --i_e2f_lex 5 --i_f2e 6 --i_f2e_lex 7 -r test/extract
sys.stderr.write('Regression test 9\n')
Combiner = Combine_TMs([[os.path.join('test','model3'),'primary'],[os.path.join('test','model4'),'primary']],[[0.5,0.5],[0.5,0.5],[0.5,0.5],[0.5,0.5],[0.1,0.9],[0.1,1],[0.2,0.8],[0.5,0.5]],os.path.join('test','phrase-table_test9'),mode='counts',number_of_features=8,i_e2f=4,i_e2f_lex=5,i_f2e=6,i_f2e_lex=7)
Combiner.combine_given_weights()
# count-based combination of two non-default models, with fixed weights. Same as test 5, but with the standard features moved back
# command line: python tmcombine.py combine_given_tuning_set test/model3 test/model4 -o test/phrase-table_test10 -m counts --number_of_features 8 --i_e2f 4 --i_e2f_lex 5 --i_f2e 6 --i_f2e_lex 7 -r test/extract
sys.stderr.write('Regression test 10\n')
Combiner = Combine_TMs([[os.path.join('test','model3'),'primary'],[os.path.join('test','model4'),'primary']],output_file=os.path.join('test','phrase-table_test10'),mode='counts',number_of_features=8,i_e2f=4,i_e2f_lex=5,i_f2e=6,i_f2e_lex=7,reference_file='test/extract')
Combiner.combine_given_tuning_set()
#convert weight vector passed as a command line argument
class to_list(argparse.Action):
def __call__(self, parser, namespace, weights, option_string=None):
if ';' in weights:
values = [[float(x) for x in vector.split(',')] for vector in weights.split(';')]
else:
values = [float(x) for x in weights.split(',')]
setattr(namespace, self.dest, values)
def parse_command_line():
parser = argparse.ArgumentParser(description='Combine translation models. Check DOCSTRING of the class Combine_TMs() and its methods for a more in-depth documentation and additional configuration options not available through the command line. The function test() shows examples.')
group1 = parser.add_argument_group('Main options')
group2 = parser.add_argument_group('More model combination options')
group1.add_argument('action', metavar='ACTION', choices=["combine_given_weights","combine_given_tuning_set","combine_reordering_tables","compute_cross_entropy","return_best_cross_entropy","compare_cross_entropies"],
help='What you want to do with the models. One of %(choices)s.')
group1.add_argument('model', metavar='DIRECTORY', nargs='+',
help='Model directory. Assumes default Moses structure (i.e. path to phrase table and lexical tables).')
group1.add_argument('-w', '--weights', dest='weights', action=to_list,
default=None,
help='weight vector. Format 1: single vector, one weight per model. Example: \"0.1,0.9\" ; format 2: one vector per feature, one weight per model: \"0.1,0.9;0.5,0.5;0.4,0.6;0.2,0.8\"')
group1.add_argument('-m', '--mode', type=str,
default="interpolate",
choices=["counts","interpolate","loglinear"],
help='basic mixture-model algorithm. Default: %(default)s. Note: depending on mode and additional configuration, additional statistics are needed. Check docstring documentation of Combine_TMs() for more info.')
group1.add_argument('-r', '--reference', type=str,
default=None,
help='File containing reference phrase pairs for cross-entropy calculation. Default interface expects \'path/model/extract.gz\' that is produced by training a model on the reference (i.e. development) corpus.')
group1.add_argument('-o', '--output', type=str,
default="-",
help='Output file (phrase table). If not specified, model is written to standard output.')
group1.add_argument('--output-lexical', type=str,
default=None,
help=('Not only create a combined phrase table, but also combined lexical tables. Writes to OUTPUT_LEXICAL.e2f and OUTPUT_LEXICAL.f2e, or OUTPUT_LEXICAL.counts.e2f in mode \'counts\'.'))
group1.add_argument('--lowmem', action="store_true",
help=('Low memory mode: requires two passes (and sorting in between) to combine a phrase table, but loads less data into memory. Only relevant for mode "counts" and some configurations of mode "interpolate".'))
group1.add_argument('--tempdir', type=str,
default=None,
help=('Temporary directory in --lowmem mode.'))
group2.add_argument('--i_e2f', type=int,
default=0, metavar='N',
help=('Index of p(f|e) (relevant for mode counts if phrase table has custom feature order). (default: %(default)s)'))
group2.add_argument('--i_e2f_lex', type=int,
default=1, metavar='N',
help=('Index of lex(f|e) (relevant for mode counts or with option recompute_lexweights if phrase table has custom feature order). (default: %(default)s)'))
group2.add_argument('--i_f2e', type=int,
default=2, metavar='N',
help=('Index of p(e|f) (relevant for mode counts if phrase table has custom feature order). (default: %(default)s)'))
group2.add_argument('--i_f2e_lex', type=int,
default=3, metavar='N',
help=('Index of lex(e|f) (relevant for mode counts or with option recompute_lexweights if phrase table has custom feature order). (default: %(default)s)'))
group2.add_argument('--number_of_features', type=int,
default=4, metavar='N',
help=('Combine models with N + 1 features (last feature is constant phrase penalty). (default: %(default)s)'))
group2.add_argument('--normalized', action="store_true",
help=('for each phrase pair x,y: ignore models with p(y)=0, and distribute probability mass among models with p(y)>0. (default: missing entries (x,y) are always interpreted as p(x|y)=0). Only relevant in mode "interpolate".'))
group2.add_argument('--recompute_lexweights', action="store_true",
help=('don\'t directly interpolate lexical weights, but interpolate word translation probabilities instead and recompute the lexical weights. Only relevant in mode "interpolate".'))
return parser.parse_args()
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.stderr.write("no command specified. use option -h for usage instructions\n")
elif sys.argv[1] == "test":
test()
else:
args = parse_command_line()
#initialize
combiner = Combine_TMs([(m,'primary') for m in args.model],
weights=args.weights,
mode=args.mode,
output_file=args.output,
reference_file=args.reference,
output_lexical=args.output_lexical,
lowmem=args.lowmem,
normalized=args.normalized,
recompute_lexweights=args.recompute_lexweights,
tempdir=args.tempdir,
number_of_features=args.number_of_features,
i_e2f=args.i_e2f,
i_e2f_lex=args.i_e2f_lex,
i_f2e=args.i_f2e,
i_f2e_lex=args.i_f2e_lex)
# execute right method
f_string = "combiner."+args.action+'()'
exec(f_string)