Cleans code, moving formula choice to copycat.py

This commit is contained in:
LSaldyt
2017-10-05 15:17:39 -06:00
parent c2c5d24f0d
commit 96c7c6e08c
3 changed files with 29 additions and 444 deletions

View File

@ -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)

View File

@ -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)
'''

View File

@ -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()