#!/usr/bin/env python
from sys import argv
from svm import *
from math import log, exp
from string import atof
from random import randrange
#--[Baisc Function]---------------------------------------------------------------------
#input decision_values, real_labels{1,-1}, #positive_instances, #negative_instances
#output [A,B] that minimize sigmoid likilihood
#refer to Platt's Probablistic Output for Support Vector Machines
def SigmoidTrain(deci, label, prior1=None, prior0=None):

	#Count prior0 and prior1 if needed
	if prior1==None or prior0==None:
		prior1, prior0 = 0, 0
		for i in range(len(label)):
			if label[i] > 0:
				prior1+=1
			else:
				prior0+=1
	
	#Parameter Setting
	maxiter=100	#Maximum number of iterations
	minstep=1e-10	#Minimum step taken in line search
	sigma=1e-3	#For numerically strict PD of Hessian
	eps=1e-5
	
	#Construct Target Support
	hiTarget=(prior1+1.0)/(prior1+2.0)
	loTarget=1/(prior0+2.0)
	length=prior1+prior0
	t=[]

	for i in range(length):
		if label[i] > 0:
			t.append(hiTarget)
		else:
			t.append(loTarget)

	#Initial Point and Initial Fun Value
	A,B=0.0, log((prior0+1.0)/(prior1+1.0))
	fval = 0.0

	for i in range(length):
		fApB = deci[i]*A+B
		if fApB >= 0:
			fval += t[i]*fApB + log(1+exp(-fApB))
		else:
			fval += (t[i] - 1)*fApB +log(1+exp(fApB))

	for it in range(maxiter):
		#Update Gradient and Hessian (use H' = H + sigma I)
		h11=h22=sigma #Numerically ensures strict PD
		h21=g1=g2=0.0
		for i in range(length):
			fApB = deci[i]*A+B
			if (fApB >= 0):
				p=exp(-fApB)/(1.0+exp(-fApB))
				q=1.0/(1.0+exp(-fApB))
			else:
				p=1.0/(1.0+exp(fApB))
				q=exp(fApB)/(1.0+exp(fApB))
			d2=p*q
			h11+=deci[i]*deci[i]*d2
			h22+=d2
			h21+=deci[i]*d2
			d1=t[i]-p
			g1+=deci[i]*d1
			g2+=d1

		#Stopping Criteria
		if abs(g1)<eps and abs(g2)<eps:
			break

		#Finding Newton direction: -inv(H') * g
		det=h11*h22-h21*h21
		dA=-(h22*g1 - h21 * g2) / det
		dB=-(-h21*g1+ h11 * g2) / det
		gd=g1*dA+g2*dB

		#Line Search
		stepsize = 1
		while stepsize >= minstep:
			newA = A + stepsize * dA
			newB = B + stepsize * dB

			#New function value
			newf = 0.0
			for i in range(length):
				fApB = deci[i]*newA+newB
				if fApB >= 0:
					newf += t[i]*fApB + log(1+exp(-fApB))
				else:
					newf += (t[i] - 1)*fApB +log(1+exp(fApB))

			#Check sufficient decrease
			if newf < fval + 0.0001 * stepsize * gd:
				A, B, fval = newA, newB, newf
				break
			else:
				stepsize = stepsize / 2.0

		if stepsize < minstep:
			print "line search fails",A,B,g1,g2,dA,dB,gd
			return [A,B]

	if it>=maxiter-1:
		print "reaching maximal iterations",g1,g2
	return [A,B]

#reads decision_value and Platt parameter [A,B]
#outputs predicted probability
def SigmoidPredict(deci, AB):
	A, B = AB
	fApB = deci * A + B
	if (fApB >= 0):
		return exp(-fApB)/(1.0+exp(-fApB))
	else:
		return 1.0/(1+exp(fApB)) 

#given training data and param, output decision_values
def get_pos_deci(train_x, train_y, test_x, param):
	prob =  svm_problem(train_y, train_x)
	model = svm_model(prob, param)

	#check
	if len(model.get_labels())!=1 and (model.get_labels() != [1,-1] and model.get_labels() != [-1,1]):
		print "Binary Training is only applicable to binary classes dataset with labels 1, -1"
		print "You have:", model.get_labels()
		raise SystemExit
	
	if len(model.get_labels())==1:
		class unimodel:
			def __init__(self,x):
				self.def_label=x
			def predict_values(self,x):
				return {(1,-1):self.def_label}
		model = unimodel(model.get_labels()[0])

	deci = []
	#predict and grab decision value, assure deci>0 for label+
	for i in test_x:
		deci.append(model.predict_values(i)[1,-1])
	return deci, model

#get_cv_deci(prob_x[], prob_y[], svm_parameter param, nr_fold)
#input raw attributes, labels, param, cv_fold in decision value building
#output list of decision value
def get_cv_deci(prob_x, prob_y, param, nr_fold):
	if nr_fold == 1 or nr_fold==0:
		deci,model = get_pos_deci(prob_x, prob_y, prob_x, param)
		return deci
	prob_l = len(prob_y)

	#random permutation by swapping i and j instance
	for i in range(prob_l):
		j = randrange(i,prob_l)
		prob_x[i], prob_x[j] = prob_x[j], prob_x[i]
		prob_y[i], prob_y[j] = prob_y[j], prob_y[i]

	#cross training : folded CV
	deci = []
	for i in range(nr_fold):
		begin = i * prob_l / nr_fold
		end = (i + 1) * prob_l / nr_fold
		train_x = prob_x[:begin] + prob_x[end:]
		train_y = prob_y[:begin] + prob_y[end:]
		test_x = prob_x[begin:end]
		subdeci, submodel = get_pos_deci(train_x, train_y, test_x, param)
		deci += subdeci
	return deci

#read svm sparse format from file 'fname' (with labels only)
#output: [attributes[], labels[]]
def read_svm(fname):
	attrs = []
	labels = []
	for line in open(fname).readlines():
		label, lineattr = line.split(' ', 1)
		attr={}
		for e in lineattr.split():
			index,value= e.split(":")
			attr[int(index)] = float(value)
		attrs.append(attr)
		labels.append(float(label))	#float in case of regression
	return attrs, labels

#processing argv and set some global variables
def proc_argv(argv = argv):
	#The command line : ./plotroc -c 1 -g 0 -m 100 -v 5 -t 2 -T xxx.test xxx.train
    #default parameter
	fold = 5
	cost = 2**0
	kern = 2
	gma = 0
	cache_size = 200
	test_file = None
	i = 1
	while i < len(argv) - 1:
		if argv[i] == '-v':
			fold = int(argv[i+1])
		elif argv[i] == '-g':
			gma = atof(argv[i+1])
		elif argv[i] == '-c':
			cost = atof(argv[i+1])
		elif argv[i] == '-t':
			kern = atoi(argv[i+1])
		elif argv[i] == '-m':
			cache_size = atoi(argv[i+1])
		elif argv[i] == '-T':
			test_file = argv[i+1]
		else:
			i+=1
			continue
		i += 2 #skip len("-argname argval")

   	if gma!=0:
	   param = svm_parameter(svm_type=C_SVC, kernel_type=kern, gamma=gma, C=cost)
	else:
	   param = svm_parameter(svm_type=C_SVC, kernel_type=kern, C=cost)
	return param, fold, argv[-1], test_file

def getarg(argkey, argdft='x'):
	if argv.count(argkey):
		return argv[argv.index(argkey)+1]
	else:
		if argdft == 'x':
			print "Missing arg:",argkey
			raise SystemExit
		else:
			return argdft
	

def main():
	from random import seed
	seed(0)
	if len(argv)<2:
		print "%s [svm-train parameters][-o outfile][-T test_file] train_file" % argv[0]
		print "	output file consists of probability, decision for each test instance"
		raise SystemExit
	param, cv_fold, train_file, test_file = proc_argv()
	train_x, train_y = read_svm(train_file)
	outfile = getarg('-o',None)
	train_deci = get_cv_deci(train_x, train_y, param, cv_fold)
	AB=SigmoidTrain(train_deci, train_y)
	model = svm_model(svm_problem(train_y, train_x), param)
	#self testing?
	if test_file:
		pred_x, pred_y = read_svm(test_file)
	else:
		pred_x, pred_y = train_x, train_y

	#measure Performance
	if outfile:
		outfile = open(outfile,'w')
	correct = 0.	#ensure float division
	for i in range(len(pred_x)):
		ideci = model.predict_values(pred_x[i])[1,-1]
		prob = SigmoidPredict(ideci, AB)
		if (prob - 0.5)*pred_y[i] >= 0:
			correct += 1
		if outfile:
			outfile.write('%f %f\n' % (prob, ideci))
	if outfile:
		outfile.close()
	print "Accuracy %f" % (correct/len(pred_y))

if __name__ == '__main__':
	main()
