From ba94693f44d09cfecfa576bd86dc93d84fc988f7 Mon Sep 17 00:00:00 2001 From: jkunimune15 Date: Mon, 14 Mar 2016 19:04:30 -0400 Subject: [PATCH] Turning in my evolutionary algorithms toolbox --- approx000.png | Bin 0 -> 1911 bytes evolve_text.py | 40 ++++-- genetic_painting.py | 296 ++++++++++++++++++++++++++++++++++++++++++++ icon.jpg | Bin 0 -> 856 bytes results.txt | 3 + results.txt~ | 1 + 6 files changed, 327 insertions(+), 13 deletions(-) create mode 100644 approx000.png create mode 100644 genetic_painting.py create mode 100644 icon.jpg create mode 100644 results.txt create mode 100644 results.txt~ diff --git a/approx000.png b/approx000.png new file mode 100644 index 0000000000000000000000000000000000000000..c54b545ed86992d5a5cfe06212a975da52fb8d90 GIT binary patch literal 1911 zcmV--2Z;EIP)6H)`594y9qqIlMHgcmb68$Cy_-}U zD9MqKq?)LKIb!x~k}4U6g6LH2A}$ajNv0ryN+_F=ED24b{YB*lNwwgl2Ssa z)H!mFP-qgnP%`z*;#tiyJ#sfiZ-6oia}};dq)1dX)t<$uL>A7%P)=1yg%p%ZtxBDu zOyG$?X^BpFG8(5UalsQg!n>)`%%~GjYn?9B^kvdAWo;w$G#Q=qWbVvhWM&327p{fo znoG^4MbqF6^V|cUkrk*+t(A?T+pgEk?RvrWvTn02nWZ}&9*3Ls zQ_f?r&qu#=?*NfZ%qwBBoXi;>EDk+)Jx+ZZS&-7$^eFP!SpWQ&<)_>Ar!Tj!w-n;{PFzZN8tSI$PhT9XCNJp)TF{*b(A{V;-n!9%;fO$2-Uy+ z{c?M|-M+lNy?m+btxhXdGU+>>yYHW;P46-Ns_o<_e?PG&LSk@uL{EqpD%G`kWt4Pr zml1Mu9tFLok^brL*O%*;x7VLuUw_&zuj{gn!h`zV9`E<(^8G0LSxONd%8w_hhjBW^ zKx78ekP$fH#4JQD+lAU2SG49!HPx?wy=`w_uD7q3?dxTJ-O8rPHu}f$)c!!ZcX@Q8 ziq@$r3Zef_-`&C;NUF(=sH%w6A`{8fbgt7{wzW)p`Qz>7^~>w^?KZz{`nqCO=L5sW zvU7WoHc;iE+=v>e1?k8dqo_Shgy@77ht~I8a^dh+GSg$!< zvRquYPAg$Dq8P zuVva~nRp#^)w`}PEAzrU{XW70I)itQd$c>+gWIRncdj2if6}y5JK<*xA3g3WDdl@s z=Go<8dW71wx=t>$%i3twMpH#o)RLv9Qc4Np1W1E5s5q5QZAdk)#syr2O3B)@oK(hC zuz@aGE;ZJgt&udv)N*c^8cjNn(yC0sGt)%GL{4Z#3ZlVL#I+37!Nr7KRJa&bqtVfh z9D8^KsjK+JHnYuQliFOkwQMsNrJcQyl^h5m7s^b^Op;VV6>37W@GLwF7GY-zE{@j2 z`iSv?lXy?vW9q(mEaY?Xd9pT(F2Z{J$Q^j*sPm9Iz`{_8XdWKXBhHK`;*+9rs$deS zR4xe5w4igBUFj(0l*ut$OxD{t<}uY?=7Uqe|BCH+wrA0;&_Yv7<(i2YmVICh@?N3~ zb!I2cP!3iHB6y?@Z|)gU!H%_$~K$KJOY2rIX4%^@#GYcACW=@FJttUe`nGeYJVM&xPfBkMH`+FOTIW z+vm6K-s^RbZO*Cg!XFXe`u^X?$N!!`zMt>k{PUBJy;wJNl5`QrW#G~inZyt&%B7$- zyI`Lmq9b^WuK)Z+^!jc5`hH*cx2-K(rLByKo?;(3Kl38+L;56C2 x+F9(7JUEHiNV?R!wmQ#snd(#;<9vUP{(p6k(5FtUC-VRR002ovPDHLkV1kKevAO^N literal 0 HcmV?d00001 diff --git a/evolve_text.py b/evolve_text.py index e0202d2..7246914 100644 --- a/evolve_text.py +++ b/evolve_text.py @@ -92,8 +92,24 @@ def get_text(self): # Genetic operators #----------------------------------------------------------------------------- -# TODO: Implement levenshtein_distance function (see Day 9 in-class exercises) -# HINT: Now would be a great time to implement memoization if you haven't +def levenshtein_distance(a, b, memory): + if a == b: + return 0 + if len(a) == 0: + return len(b) + if len(b) == 0: + return len(a) + if memory.has_key((a,b)): + return memory[(a,b)] + + x = levenshtein_distance(a[1:], b, memory) + 1 + y = levenshtein_distance(a, b[1:], memory) + 1 + #z = levenshtein_distance(a[1:], b[1:], memory) + abs(ord(a[0])-ord(b[0])) + z = levenshtein_distance(a[1:], b[1:], memory) + (a[0]!=b[0]) + + memory[(a,b)] = min(x,y,z) + memory[(b,a)] = memory[(a,b)] + return memory[(b,a)] def evaluate_text(message, goal_text, verbose=VERBOSE): """ @@ -101,13 +117,14 @@ def evaluate_text(message, goal_text, verbose=VERBOSE): between the Message and the goal_text as a length 1 tuple. If verbose is True, print each Message as it is evaluated. """ - distance = levenshtein_distance(message.get_text(), goal_text) + mem = dict() + distance = levenshtein_distance(message.get_text(), goal_text, mem) if verbose: print "{msg:60}\t[Distance: {dst}]".format(msg=message, dst=distance) return (distance, ) # Length 1 tuple, required by DEAP -def mutate_text(message, prob_ins=0.05, prob_del=0.05, prob_sub=0.05): +def mutate_text(message, prob_ins=0.1, prob_del=0.1, prob_sub=0.1): """ Given a Message and independent probabilities for each mutation type, return a length 1 tuple containing the mutated Message. @@ -119,15 +136,12 @@ def mutate_text(message, prob_ins=0.05, prob_del=0.05, prob_sub=0.05): Substitution: Replace one character of the Message with a random (legal) character """ - if random.random() < prob_ins: - # TODO: Implement insertion-type mutation - pass - - # TODO: Also implement deletion and substitution mutations - # HINT: Message objects inherit from list, so they also inherit - # useful list methods - # HINT: You probably want to use the VALID_CHARS global variable + message.insert(int(random.random()*(len(message)+1)),random.choice(VALID_CHARS)) + if random.random() < prob_del: + message.remove(message[int(random.random()*len(message))]) + if random.random() < prob_sub: + message[int(random.random()*len(message))] = random.choice(VALID_CHARS) return (message, ) # Length 1 tuple, required by DEAP @@ -185,7 +199,7 @@ def evolve_string(text): toolbox, cxpb=0.5, # Prob. of crossover (mating) mutpb=0.2, # Probability of mutation - ngen=500, # Num. of generations to run + ngen=400, # Num. of generations to run stats=stats) return pop, log diff --git a/genetic_painting.py b/genetic_painting.py new file mode 100644 index 0000000..9d20ea3 --- /dev/null +++ b/genetic_painting.py @@ -0,0 +1,296 @@ +from math import * +from random import * +import numpy as np +from PIL import Image + + + +def generate_random_function(): + """ + generates a random function of x and y of depth 1 + return a list representing the new function + """ + varbles = ["x","y"] + singles = ["sin_pi","cos_pi","cos_30","sin_30","tan_pi/4","neg","square","cube","lnabs","abs"] + doubles = ["prod","avg","hypot"] + + if random() < float(len(varbles))/(len(varbles)+2*len(singles)+3*len(doubles)): + return [choice(varbles)] + elif random() < float(len(singles))/(len(singles)+2*len(doubles)): + return [choice(singles), [choice(varbles)]] + else: + return [choice(doubles), [choice(varbles)], [choice(varbles)]] + + +def mutate_function(function, rate=.03): + """ + mutates a function by recursively and randomly changing functions + function = a list representing the old function + rate = a float between 0 and 1 + return a new list representing the new function + """ + singles = ["sin_pi","cos_pi","cos_30","sin_30","tan_pi/4","neg","square","cube","lnabs","abs"] + doubles = ["prod","avg","hypot"] + + if random() < rate: + return generate_random_function() # it might just trash what it gets + elif random() < rate: + return [choice(singles), mutate_function(function)] # it might nest it inside a different function + elif random() < rate: + return [choice(doubles), mutate_function(function), generate_random_function()] # it might create a new double function + elif random() < rate: + return [choice(doubles), generate_random_function(), mutate_function(function)] + else: + mutant = [function[0]] # or it might mutate the stuff below it + for i in range(1,len(function)): + mutant.append(mutate_function(function[i])) + return mutant + +def clone_function(function): + """ + clones functions + function = a list representing the function + returns a new list, equivalent to function + + >>> f = ["hypot", ["x"], ["sin_pi", ["y"]]] + >>> g = clone_function(f) + >>> f[2] = ["cos_pi", ["x"]] + >>> g[2] + ['sin_pi', ['y']] + """ + newFunc = [function[0]] # copies the string part and turns to recursion to handle the rest + for i in range(1,len(function)): + newFunc.append(clone_function(function[i])) + return newFunc + + +def test_function(function, image, xes, yys): + """ + tests a function against an existing image + function = a list representing the function + image = a list of three numpy arrays of floats in range [-1.0,1.0] representing an image + return a tuple of ints representing how different the approximation is from the image in each channel + + >>> imgR = np.array([[1.0, 1.0],[0.0, -1.0]]) + >>> imgG = np.array([[-1.0, -0.5],[0.5, 1.0]]) + >>> imgB = np.array([[0.0, -1.0],[1.0, -1.0]]) + >>> x = np.array([[-1.0, 1.0],[-1.0, 1.0]]) + >>> y = np.array([[-1.0, -1.0],[1.0, 1.0]]) + >>> test_function(["x"], [imgR, imgG, imgB], x, y) + (5.0, 3.0, 7.0) + """ + + approximation = evaluate_function(function, xes, yys) + actualR = image[0] + actualG = image[1] + actualB = image[2] + + return ( + np.sum(np.absolute(np.subtract(approximation, actualR))), + np.sum(np.absolute(np.subtract(approximation, actualG))), + np.sum(np.absolute(np.subtract(approximation, actualB))) + ) + + +def evaluate_function(f, x, y): + """ + evaluates a function over a set of points + f = a list representing the function + x, y = a numpy array of floats representing a coordinate system + returns a numpy array of floats in range [-1.0,1.0] + + >>> evaluate_function(["avg", ["x"],["y"]], np.array([1.0, 0.0]), np.array([0.5, 0.5])) + array([ 0.75, 0.25]) + """ + if f[0] == "x": + ans = x + elif f[0] == "y": + ans = y + elif f[0] == "prod": + ans = evaluate_function(f[1],x,y) * evaluate_function(f[2],x,y) + elif f[0] == "avg": + ans = 0.5*(evaluate_function(f[1],x,y) + evaluate_function(f[2],x,y)) + elif f[0] == "cos_pi": + ans = np.cos(pi * evaluate_function(f[1],x,y)) + elif f[0] == "sin_pi": + ans = np.sin(pi * evaluate_function(f[1],x,y)) + elif f[0] == "cos_30": + ans = np.cos(30 * evaluate_function(f[1],x,y)) + elif f[0] == "sin_30": + ans = np.sin(30 * evaluate_function(f[1],x,y)) + elif f[0] == "tan_pi/4": + ans = np.tan(pi/4.0 * evaluate_function(f[1],x,y)) + elif f[0] == "neg": + ans = np.negative(evaluate_function(f[1],x,y)) + elif f[0] == "square": + ans = np.square(evaluate_function(f[1],x,y)) + elif f[0] == "cube": + ans = evaluate_function(f[1],x,y) + ans = ans * ans * ans + elif f[0] == "lnabs": + ans = np.log(np.absolute(evaluate_function(f[1],x,y))+1) / log(2) + elif f[0] == "abs": + ans = np.absolute(evaluate_function(f[1],x,y)) *2-1 + elif f[0] == "hypot": + ans = np.sqrt(np.square(evaluate_function(f[1],x,y)) + np.square(evaluate_function(f[2],x,y))) *sqrt(2)-1 + else: + raise Exception("That's not a function I recognize!") + + return np.around(ans, 5) # rounds to 5 digits to make it be in bounds + + +def save_art(r_function, g_function, b_function, xes, yys, filename): + """ + takes three functions as an image and saves it to disk + r_function, g_function, b_function = lists representing each function + xes, yys = numpy arrays of floats in range [-1.0, 1.0] + filename = a string, the name it will save it with + """ + + print "Red: ",r_function + print "Green:",g_function + print "Blue: ",b_function + + rArray = evaluate_function(r_function, xes, yys) + gArray = evaluate_function(g_function, xes, yys) + bArray = evaluate_function(b_function, xes, yys) + + w = len(xes[0]) + h = len(xes) + + img = Image.new("RGB", (w,h)) + pixels = img.load() + + for x in range(w): + for y in range(h): + pixels[x, y] = (int(127.5*(rArray[y,x]+1)), + int(127.5*(gArray[y,x]+1)), + int(127.5*(bArray[y,x]+1)) + ) + + img.save(filename) + + +def build_x_coordinates(w,h): + """ + bulids a numpy array representing a coordinate system + w,h = integers representing the dimensions of the array + returns a numpy array with dimensions w and h and values from -1.0 to 1.0 + + >>> build_x_coordinates(3,3) + array([[-1., 0., 1.], + [-1., 0., 1.], + [-1., 0., 1.]]) + + """ + basicArray = [] + for y in range(h): + row = [] + for x in range(w): + row.append(x*2.0/(w-1)-1) + basicArray.append(row) + return np.array(basicArray) + + +def build_y_coordinates(w,h): + """ + bulids a numpy array representing a coordinate system + w,h = integers representing the dimensions of the array + returns a numpy array with dimensions w and h and values from -1.0 to 1.0 + + >>> build_y_coordinates(3,3) + array([[-1., -1., -1.], + [ 0., 0., 0.], + [ 1., 1., 1.]]) + """ + basicArray = [] + for y in range(h): + row = [] + for x in range(w): + row.append(y*2.0/(h-1)-1) + basicArray.append(row) + return np.array(basicArray) + + +def convert_image(filename): + """ + converts an image into three numpy arrays + filename = a string, the name of the image file to be read + returns a list of three numpy arrays of floats in range [-1.0,1.0] representing the r, g, and b channels + """ + img = Image.open(filename) + pxl = img.load() + allChannels = [] + + for i in [0,1,2]: + basicArray = [] + for y in range(img.size[1]): + row = [] + for x in range(img.size[0]): + row.append(((pxl[x, y])[i])/127.5-1) + basicArray.append(row) + allChannels.append(np.array(basicArray)) + return allChannels + + + +def evolve_painting(filename, generations): + """ + approximates a function to match an image + filename = a string, the name of the image to be painted + generations = an int greater than 0 representing the number of generations to run + """ + generationSize = 100 + + source = convert_image(filename) + xes = build_x_coordinates(len(source[0][0]), len(source[0])) + yys = build_y_coordinates(len(source[0][0]), len(source[0])) + + generation = [] + for i in range(3*generationSize): # builds generation 0 + generation.append(generate_random_function()) + + bestRedScore = 1 + bestGreenScore = 1 + bestBlueScore = 1 + + t = 0 + while True: + g = 0 + while g < generations: # thes actual genetic algorithm + bestRedScore = 10**100 # for lack of an actual max value, I've arbitrarily picked Googol, because I'm lazy. + bestGreenScore = 10**100 + bestBlueScore = 10**100 + bestRedFunc = [] + bestGreenFunc = [] + bestBlueFunc = [] + + for f in generation: # pulls out the best function for each channel + scores = test_function(f, source, xes, yys) + if scores[0] < bestRedScore: + bestRedScore = scores[0] + bestRedFunc = f + if scores[1] < bestGreenScore: + bestGreenScore = scores[1] + bestGreenFunc = f + if scores[2] < bestBlueScore: + bestBlueScore = scores[2] + bestBlueFunc = f + + generation = [bestRedFunc, bestGreenFunc, bestBlueFunc] + for i in range(1,generationSize): # crafts a new generation in the winners' images (pun not intended) + generation.append(mutate_function(bestRedFunc)) + generation.append(mutate_function(bestGreenFunc)) + generation.append(mutate_function(bestBlueFunc)) + + g = g+1 + print g + + save_art(bestRedFunc, bestGreenFunc, bestBlueFunc, xes, yys, "approx{0:03d}.png".format(t)) + t = t+1 + + +if __name__ == '__main__': + import doctest + doctest.testmod() + evolve_painting("icon.jpg", 100) \ No newline at end of file diff --git a/icon.jpg b/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..443fc4d1931b283ebd1f8c73b949ad0337b86b85 GIT binary patch literal 856 zcmex=>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*Oh zL4iSmnNf*>Nsy6Qkn#T!1}O#xW(HAuraf;10^&BfyS^fGBdHmB!R{VvI;30 zGOz?D3KuFlHcs65;p6{X3_L)GFbOgXGT1XbopN)tnom{8O21v7*J)-)UGtiR*R+s@$_;_Kzt;LCG<$R^ zCEpdX3c8`v5X4`9;eBuBv&9}O)t}d?>{!=-((87F-EsfdcC8P?o@Dpk@0gx$@0Y8; zWlzigQ%3W3rmSZE{eDUA(V!J~N>7@u47hT8Qw&#-Ky%Au27broQ@fN+)=gX_rC?4EYM{nsY$@w;7; z)%W4j6^)Ne&UUAsk4ftZvHrBmDd;6W`1WZGFzHN6ohArrWtR*_PNo(4C~N{P~zIL(p7J zueB;NXKJ?Em41D~eCWBzn%pC^=V@{9m?As-W;czgHAjSfng}Te8dN#O~Hdt5#XOUZ-eK6)QApa{rFvukyYD zq28-xzk3AvOlG-!+slFL`Kx07XLj>z1=g4}eJ-1sZO&n*Y<)j3eQVon{_UC5J{L4U LGT!^``Tv^$+rKJ3 literal 0 HcmV?d00001 diff --git a/results.txt b/results.txt new file mode 100644 index 0000000..5239d57 --- /dev/null +++ b/results.txt @@ -0,0 +1,3 @@ +The first thing I found was that the mutation rates were way to low. By doubling all of them, I was able to get to the final outcome over a hundred generations sooner. I tried modifying my distance equation to check for distances between letters, but as it turns out, no matter how I weighed it, I ended up encouraging the program to delete letters and then add them back in rather than substituting existing letters, which took much longer and was not nearly as interesting to watch. + +On a completely unrelated note, I made a program based on Computation art that does genetics. diff --git a/results.txt~ b/results.txt~ new file mode 100644 index 0000000..098a8f6 --- /dev/null +++ b/results.txt~ @@ -0,0 +1 @@ +The first thing I found was that the mutation rates were way to low. By doubling all of them, I was able to get to the final outcome over a hundred generations sooner. I tried modifying my distance equation to check for distances between letters, but as it turns out, no matter how I weighed it, I ended up encouraging the program to delete letters and then add them back in rather than substituting existing letters, which took much longer and was not nearly as interesting to watch.