From 96c7c6e08c6b92ca293135e133fb8b36de77e1d2 Mon Sep 17 00:00:00 2001 From: LSaldyt Date: Thu, 5 Oct 2017 15:17:39 -0600 Subject: [PATCH] Cleans code, moving formula choice to copycat.py --- copycat/copycat.py | 50 +++--- copycat/temperature.py | 371 +---------------------------------------- multi-run.py | 52 ------ 3 files changed, 29 insertions(+), 444 deletions(-) delete mode 100755 multi-run.py diff --git a/copycat/copycat.py b/copycat/copycat.py index c14f1eb..3d0cf01 100644 --- a/copycat/copycat.py +++ b/copycat/copycat.py @@ -65,38 +65,30 @@ class Copycat(object): self.reporter.report_answer(answer) return answer - def run(self, initial, modified, target, iterations, testAdjFormulas=False): + def run(self, initial, modified, target, iterations): self.workspace.resetWithStrings(initial, modified, target) - if testAdjFormulas: - formulas = self.temperature.adj_formulas() - else: - formulas = ['inverse'] - #formulas = ['entropy'] + #self.temperature.useAdj('original') + #self.temperature.useAdj('entropy') + self.temperature.useAdj('inverse') # 100 weight + #self.temperature.useAdj('150-weight') + #self.temperature.useAdj('200-weight') + answers = {} + for i in range(iterations): + answer = self.runTrial() + d = answers.setdefault(answer['answer'], { + 'count': 0, + 'sumtemp': 0, # TODO: use entropy + 'sumtime': 0 + }) + d['count'] += 1 + d['sumtemp'] += answer['temp'] # TODO: use entropy + d['sumtime'] += answer['time'] - formulaList = [] - for formula in formulas: - self.temperature.useAdj(formula) - answers = {} - for i in range(iterations): - answer = self.runTrial() - d = answers.setdefault(answer['answer'], { - 'count': 0, - 'sumtemp': 0, # TODO: use entropy - 'sumtime': 0 - }) - d['count'] += 1 - d['sumtemp'] += answer['temp'] # TODO: use entropy - d['sumtime'] += answer['time'] - - for answer, d in answers.items(): - d['avgtemp'] = d.pop('sumtemp') / d['count'] - d['avgtime'] = d.pop('sumtime') / d['count'] - formulaList.append((formula, answers)) - if not testAdjFormulas: - return formulaList[0][1] - else: - return formulaList + for answer, d in answers.items(): + d['avgtemp'] = d.pop('sumtemp') / d['count'] + d['avgtime'] = d.pop('sumtime') / d['count'] + return answers def run_forever(self, initial, modified, target): self.workspace.resetWithStrings(initial, modified, target) diff --git a/copycat/temperature.py b/copycat/temperature.py index ea352dd..075a032 100644 --- a/copycat/temperature.py +++ b/copycat/temperature.py @@ -28,6 +28,11 @@ def _inverse_prob(temp, prob): iprob = 1 - prob return (temp / 100) * iprob + ((100 - temp) / 100) * prob +def _create_weighted_inverse_prob(weight): + def _inner_weighted_prob(temp, prob): + iprob = 1 - prob + return (temp / weight) * iprob + ((weight - temp) / weight) * prob + return _inner_weighted_prob class Temperature(object): def __init__(self): @@ -36,7 +41,9 @@ class Temperature(object): self._adjustmentFormulas = { 'original' : _original, 'entropy' : _entropy, - 'inverse' : _inverse_prob + 'inverse' : _inverse_prob, + '200-weight' : _create_weighted_inverse_prob(200), + '150-weight' : _create_weighted_inverse_prob(150) } def reset(self): @@ -78,365 +85,3 @@ class Temperature(object): def adj_formulas(self): return self._adjustmentFormulas.keys() - - ''' - def getAdjustedProbability(self, value): - if value == 0 or value == 0.5 or self.value() == 0: - return value - if value < 0.5: - return 1.0 - self.getAdjustedProbability(1.0 - value) - coldness = 100.0 - self.value() - a = math.sqrt(coldness) - c = (10 - a) / 100 - f = (c + 1) * value - return max(f, 0.5) - - def getAdjustedProbability(self, value): - if value == 0 or value == 0.5 or self.value() == 0: - return value - if value < 0.5: - return 1.0 - self.getAdjustedProbability(1.0 - value) - coldness = 100.0 - self.value() - a = math.sqrt(coldness) - c = (10 - a) / 100 - f = (c + 1) * value - # return max(f, 0.5) - # return max(f, 0.0) - # return (0 + (-f * math.log2(f))) - return -f * math.log2(f) - # TODO: use entropy - - """ - This function returns the probability for a decision. - Copied above. - - Please look at the last line of it. Strangely, it was - return max(f, 0.5). Does that make sense? Let's compare - some results. Where it was (0.5), we obtained, for example: - - iiijjjlll: 670 (avg time 1108.5, avg temp 23.6) - iiijjjd: 2 (avg time 1156.0, avg temp 35.0) - iiijjjkkl: 315 (avg time 1194.4, avg temp 35.5) - iiijjjkll: 8 (avg time 2096.8, avg temp 44.1) - iiijjjkkd: 5 (avg time 837.2, avg temp 48.0) - - wyz: 5 (avg time 2275.2, avg temp 14.9) - xyd: 982 (avg time 2794.4, avg temp 17.5) - yyz: 7 (avg time 2731.9, avg temp 25.1) - dyz: 2 (avg time 3320.0, avg temp 27.1) - xyy: 2 (avg time 4084.5, avg temp 31.1) - xyz: 2 (avg time 1873.5, avg temp 52.1) - - Now, let's see what return max(f, 0.0000) does: - - wyz: 7 (avg time 3192.9, avg temp 13.1) - xyd: 985 (avg time 2849.1, avg temp 17.5) - yyz: 6 (avg time 3836.7, avg temp 18.6) - xyy: 1 (avg time 1421.0, avg temp 19.5) - xyz: 1 (avg time 7350.0, avg temp 48.3) - - They *seem* better (in the strict sense that we've obtained both - lower T and more times of wyz.) But they're *not* statistically - significant (for 1000 runs). - - Now... looking at the code... it seems to be a mess... what does - function f() even mean in intuitive terms? - - Work it does, but dude... quite a hack. - - Another run, with return f @line89: - - wyz: 8 (avg time 4140.5, avg temp 13.3) - yyz: 6 (avg time 2905.2, avg temp 14.5) - xyd: 982 (avg time 3025.4, avg temp 17.6) - dyz: 4 (avg time 4265.0, avg temp 17.7) - - Does it even matter? Another (quick) run, I think with return (0.5): - - dyz: 1 (avg time 5198.0, avg temp 15.3) - wyz: 3 (avg time 4043.7, avg temp 17.1) - yyz: 9 (avg time 3373.6, avg temp 21.0) - xyd: 84 (avg time 5011.1, avg temp 23.3) - xyy: 3 (avg time 4752.0, avg temp 27.9) - - Compared to return(0.99): - - xyd: 1000 (avg time 1625.2, avg temp 17.3) - - Comparing to return f --> Statistically significant. - Comparing to return(0.5) --> same, so this return value does something. - - Now running return(0.0): - - xyz: 3 (avg time 3996.7, avg temp 81.1) - dyz: 46 (avg time 5931.7, avg temp 82.6) - xd: 17 (avg time 6090.3, avg temp 83.8) - xyd: 934 (avg time 7699.8, avg temp 88.1) - - It's bad overall, but at least it's statistically significant! - - return (-f * (math.log2(f))) # Entropy test #1 (global). - - wyz: 123 (avg time 5933.1, avg temp 16.5) - xyy: 200 (avg time 6486.7, avg temp 27.8) - yyz: 330 (avg time 6310.2, avg temp 38.5) - dyz: 75 (avg time 6393.3, avg temp 39.6) - yzz: 5 (avg time 4965.0, avg temp 59.3) - xyz: 160 (avg time 6886.2, avg temp 60.2) - xd: 4 (avg time 2841.0, avg temp 61.8) - dz: 3 (avg time 3721.0, avg temp 62.1) - xyd: 100 (avg time 5853.1, avg temp 67.5) - - Here we get an intuitive result: entropy/uncertainty seems better at - exploring a whole range of possible solutions. It even seems, at least - to me, better than the distribution obtained by the original copycat. - - instead of log2, trying ln --> return (-f * math.log(f)): - - wyz: 78 (avg time 7793.7, avg temp 16.6) - xyy: 202 (avg time 9168.5, avg temp 27.5) - wxz: 1 (avg time 3154.0, avg temp 33.4) - dyz: 63 (avg time 7950.3, avg temp 41.7) - yyz: 217 (avg time 8147.4, avg temp 41.7) - xyz: 201 (avg time 7579.7, avg temp 62.5) - xxy: 1 (avg time 7994.0, avg temp 64.8) - yzz: 8 (avg time 4672.6, avg temp 65.7) - xd: 9 (avg time 9215.2, avg temp 68.1) - xyd: 217 (avg time 7677.9, avg temp 73.8) - dz: 3 (avg time 20379.0, avg temp 77.3) - - (quickly) trying out (1-this_entropy_function): - - xyd: 100 (avg time 2984.3, avg temp 18.2) - - And that's beautiful! One wants an inverse function that punishes - exploration and creativity, that takes all the fluidity off - the system. - - But somehow this completely messes up with abc abd iijjkk: - - jijjkk: 66 (avg time 3200.1, avg temp 61.3) - iijjkk: 114 (avg time 5017.2, avg temp 63.5) - dijjkk: 23 (avg time 2209.0, avg temp 67.3) - iijjkl: 748 (avg time 3262.8, avg temp 70.0) - iijjkd: 49 (avg time 2315.9, avg temp 76.3) - - Which leads me to suspect that someone may have overfitted the - model for either xyz or iijjkk or some other problem, and one - improvement there means disaster here. - - Something tells me to invert again to 1-entropy... and bingo! - - iijjll: 59 (avg time 797.4, avg temp 19.8) - iijjkl: 41 (avg time 696.1, avg temp 28.5) - - My guess is that some code is prefering to find groups in the - opposite form that it likes finding the "symmetry/opposite" - concepts of the xyz problem. - - Sould compare & contrast the unhappiness and relevance of both - the opposite/symmetry codelets and the grouping/chunking codelets. - My hunch is the sameness group code: something there that - interacts with Temperature is wicked, and should be relatively - easy to find the error. - - Here's why: the following run was done on (1-entropy(f)): - - mrrlll: 77 (avg time 2195.7, avg temp 41.4) - mrrd: 2 (avg time 1698.0, avg temp 42.6) - mrrkkl: 20 (avg time 1317.8, avg temp 46.6) - mrrkkd: 1 (avg time 1835.0, avg temp 48.6) - - - If (1-entropy(f)) binds the system into a tight corridor of possibilities, - then why does it easily get the samenessGroup right? If this is right, - then running just entropy(f) should have big trouble with samenessGroup. - Let's see: - - nrrkkk: 11 (avg time 3637.8, avg temp 64.6) - drrkkk: 3 (avg time 5921.3, avg temp 66.2) - mrrkkd: 7 (avg time 6771.3, avg temp 74.6) - mrrkkl: 79 (avg time 3723.0, avg temp 74.9) - - So there we are: the system is unable to find that change samenessGroup - to next letterCategory, so there ought to be something very different - in the code that: - - * Interacts with Temperature (things like unhappiness, relevance, depth, - urgency, and whatever else interacts with T) - * something very close to samenessGroup... sameGroup, sameness, - sameNeighbors, etc... is encoded in a form that is *directly opposite* - to other concepts/categories/codlets, etc. - - - Need to play with this more... and WTF is f anyways? - - LSaldyt: - - Recall self.value(): - def value(self): - return 100.0 if self.clamped else self.actual_value - - f in terms of value() and value only - f = ((10 - sqrt(100 - self.value()))/100 + 1) * value - so, the function: (10 - sqrt(100 - temp)) / 100 - produces a scalar from the current temperature, - ranging from 1.0 when the temp is 0 to 1.1 when the temp is 100. - This is used to scale probablities in their inverse directions - - For the original LISP program: - ; This function is a filter: it inputs a value (from 0 to 100) and returns - ; a probability (from 0 - 1) based on that value and the temperature. - ********************************************************* - ; When the temperature is 0, the result is (/ value 100), but at higher - ; temperatures, values below 50 get raised and values above 50 get lowered - ; as a function of temperature. - ********************************************************** - ; I think this whole formula could probably be simplified. - ********************************************************** - - (defun get-temperature-adjusted-probability (prob &aux low-prob-factor - result) - (setq result - (cond ((= prob 0) 0) - ((<= prob .5) - (setq low-prob-factor (max 1 (truncate (abs (log prob 10))))) - (min (+ prob - (* (/ (- 10 (sqrt (fake-reciprocal *temperature*))) - 100) - (- (expt 10 (- (1- low-prob-factor))) prob))) - .5)) - - ((= prob .5) .5) - ((> prob .5) - (max (- 1 - (+ (- 1 prob) - (* (/ (- 10 (sqrt (fake-reciprocal *temperature*))) - 100) - (- 1 (- 1 prob))))) - .5)))) - result) - - Which was tested using: - - (defun test-get-temperature-adjusted-probability (prob) - (with-open-file (ostream "testfile" :direction :output - :if-does-not-exist :create - :if-exists :append) - (format ostream "prob: ~a~&" prob) - (loop for temp in '(0 10 20 30 40 50 60 70 80 90 100) do - (setq *temperature* temp) - (format ostream "Temperature: ~a; probability ~a~&" - temp (float (get-temperature-adjusted-probability prob)))) - (format ostream "~%"))) - - Interpretation: - - Importantly, the values of .5 in both the min and max correspond to the mid-cutoff of 50: - i.e. 'values below 50 get raised and values above 50 get lowered' - Still, it is interesting to note that changing 'return max(f, 0.0) to max(f, 0.5) has no significant effect on the distribution - - It looks like the function below preserves most of the functionality of the original lisp. - However, the comments themselves agree that the formula is overly complicated. - - """ - prob = value # Slightly more descriptive (and less ambiguous), will change argument - # Temperature, potentially clamped - temp = self.value() - iprob = 1 - prob - - - # This function does precisely what I think the original lisp comment describes, but provides crappy results - # return (temp / 200) * iprob + ((200 - temp) / 200) * prob - # However, this version preforms much better: - # Essentially, it weights probabilities towards their inverses when temperature is higher, and leaves them unaffected when it is lower. - # Some statistical analysis is needed of course - return (temp / 100) * iprob + ((100 - temp) / 100) * prob - - # This will give only xyd answers: - #return 1 - (temp / 100) * iprob + ((100 - temp) / 100) * prob - """ - lucas@infinity:~/projects/personal/copycat$ ./main.py abc abd xyz --iterations 10 --plot - Answered wyz (time 3865, final temperature 13.9) - Answered wyz (time 8462, final temperature 10.8) - Answered wyz (time 6062, final temperature 11.7) - Answered yyz (time 4022, final temperature 15.6) - Answered yyz (time 2349, final temperature 59.8) - Answered xyd (time 17977, final temperature 14.9) - Answered xyd (time 1550, final temperature 14.7) - Answered xyd (time 11755, final temperature 16.8) - Answered wyz (time 2251, final temperature 13.7) - Answered yyz (time 13007, final temperature 11.7) - wyz: 4 (avg time 5160.0, avg temp 12.5) - xyd: 3 (avg time 10427.3, avg temp 15.5) - yyz: 3 (avg time 6459.3, avg temp 29.0) - - However, it generally does worse on abc:abd::ijjkk:_ - - lucas@infinity:~/projects/personal/copycat$ ./main.py abc abd ijjkkk --iterations 10 --plot - Answered ijjlll (time 7127, final temperature 17.6) - Answered ijjlll (time 968, final temperature 17.5) - Answered ijjkkl (time 1471, final temperature 19.8) - Answered ijjlll (time 1058, final temperature 13.2) - Answered ijjkkl (time 489, final temperature 83.9) - Answered ijjkkl (time 1576, final temperature 18.7) - Answered ijjkkl (time 1143, final temperature 65.2) - Answered ijjkkl (time 3067, final temperature 19.6) - Answered ijjkkl (time 1973, final temperature 40.4) - Answered ijjkkl (time 1064, final temperature 50.6) - ijjlll: 3 (avg time 3051.0, avg temp 16.1) - ijjkkl: 7 (avg time 1540.4, avg temp 42.6) - """ - # unparameterized version - #curvedProb = (temp / 100) * iprob + (cold / 100) * prob - - #if prob < .5: - # # Curving small (<.5) probabilities to .5, but not over - # return max(curvedProb, .5) - #else: - # # Curving large (>.5) probabilities to .5, but not under - # return min(curvedProb, .5) - - # parameterized version : weights towards or away from probabilities - # alpha = 1.0 - # beta = 1.0 - # return ((alpha + temp / 100) * iprob + (beta + cold / 100) * prob) / (alpha + beta) - - """ - # A scaling factor (between 0 and infinity), based on temperature (i.e. 100/coldness) - if temp == 100: # Avoid dividing by zero - factor = float('inf') - else: - # My factor: - # factor = 100 / (100 - temp) - # SQRT: - # factor = math.sqrt(100 / (100 - temp)) - # Original factor: - # factor = (110 - math.sqrt(100 - temp)) / 100 - # Factor ^ 10: - # factor = ((110 - math.sqrt(100 - temp)) / 100) ** 10. - # Factor ^ 20 - # factor = ((110 - math.sqrt(100 - temp)) / 100) ** 20. - # No factor: - # factor = 1 - if prob == .5: - return .5 - elif prob > .5: - prob = prob / factor - elif prob < .5: - prob = prob * factor - return min(max(prob, 0), 1) # Normalize between 0 and 1. - - if value == 0 or value == 0.5 or self.value() == 0: - return value - if value < 0.5: - return 1.0 - self.getAdjustedProbability(1.0 - value) - coldness = 100.0 - self.value() - a = math.sqrt(coldness) - c = (10 - a) / 100 - f = (c + 1) * value - # return max(f, 0.5) - # return max(f, 0.0) - # return (0 + (-f * math.log2(f))) - return -f * math.log2(f) - ''' diff --git a/multi-run.py b/multi-run.py deleted file mode 100755 index 88b1b16..0000000 --- a/multi-run.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -import argparse, logging -from copycat import Copycat, Reporter, plot_answers, save_answers -from collections import defaultdict - -class SimpleReporter(Reporter): - """Reports results from a single run.""" - def report_answer(self, answer): - """Self-explanatory code.""" - print('Answered %s (time %d, final temperature %.1f)' % ( - answer['answer'], answer['time'], answer['temp'], - )) - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--iterations', type=int, default=1, help='Run the given case this many times.') - options = parser.parse_args() - - copycat = Copycat(reporter=SimpleReporter()) - - with open('input/reduced_problems.csv', 'r') as infile: - for line in infile: - line = line.replace('\n', '') - a, b, c = line.split(',') - answerList = copycat.run(a, b, c, options.iterations, True) - results = dict() - for formula, answers in answerList: - answers = sorted(answers.items(), key=lambda kv : kv[1]['count']) - keys = [k for k, v in answers] - counts = [v['count'] for k, v in answers] - results[formula] = (keys, counts) - - originalCounts = defaultdict(lambda : 0) - originalCounts.update(dict(zip(*results['original']))) - - for formula, (keys, counts) in results.items(): - if formula != 'original': - chi2 = 0 - for answer, count in zip(keys, counts): - originalCount = originalCounts[answer] - if originalCount != 0: - chi2 += (count + originalCount) ** 2 / originalCount - print('Chi^2 value for {}:{}'.format(formula, chi2)) - - #for answer, d in sorted(iter(answers.items()), key=lambda kv: kv[1]['avgtemp']): - #print('%s: %d (avg time %.1f, avg temp %.1f)' % (answer, d['count'], d['avgtime'], d['avgtemp'])) - - #filename = 'output/{}-{}-{}.csv'.format(a, b, c) - #save_answers(answers, filename) - -if __name__ == '__main__': - main()