├── .gitignore ├── local ├── database.db └── config.php ├── .github └── FUNDING.yml ├── sources ├── Db.php ├── autoload.php └── Engine.php ├── save.php ├── api └── v1 │ └── objects.php ├── CONTRIBUTING.md ├── LICENSE ├── SCHEMA.sql ├── README.md ├── harvestFrom20Q.php ├── index.php ├── TECHNICAL.md └── play.php /.gitignore: -------------------------------------------------------------------------------- 1 | local/config.php 2 | local/database.db 3 | -------------------------------------------------------------------------------- /local/database.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fulldecent/19-questions/HEAD/local/database.db -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [fulldecent] 4 | custom: ["https://www.paypal.me/fulldecent", "https://amazon.com/hz/wishlist/ls/EE78A23EEGQB"] 5 | -------------------------------------------------------------------------------- /local/config.php: -------------------------------------------------------------------------------- 1 | false, 13 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, 14 | ]; 15 | 16 | try { 17 | parent::__construct(constant('DB_DSN'), '', '', $options); 18 | } catch (\PDOException $e) { 19 | trigger_error($e->getMessage()); 20 | return false; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /save.php: -------------------------------------------------------------------------------- 1 | getObjectByName($_GET['objectname']); 13 | } else { 14 | die('Invalid object'); 15 | } 16 | list($objectName, $objectSubName) = $nq->getObject($objectID); 17 | if (empty($objectName)) die('Object database error'); 18 | //$nq->teach($objectID); 19 | header('Location: index.php?playagain=1'); 20 | 21 | //TODO: use POST to access this page instead of GET -------------------------------------------------------------------------------- /api/v1/objects.php: -------------------------------------------------------------------------------- 1 | prepare($sql); 24 | $statement->execute(['%' . $query . '%']); 25 | 26 | echo json_encode($statement->fetchAll(\PDO::FETCH_OBJ)); 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Project scope 4 | 5 | Please see the README to understand what is in project scope and what contributions are likely to be accepted. If you're not sure, just ask! Please feel free to create an issue to discuss changes you would like to see for the project. 6 | 7 | ## Release process 8 | 9 | - [ ] Perform manual user testing to ensure the project is working 10 | * Use PHP 8.1 on latest macOS 11 | * Play a game 12 | * Save results 13 | * Try using the object search on the "I won" page 14 | - [ ] Tag the release and use GitHub releases 15 | - [ ] Follow distribution list (see below) 16 | 17 | ## Promoting / distribution list 18 | 19 | Following are communities that have shown an interest in this project. After each major release or new feature, reach out or post on each of the following: 20 | 21 | - https://news.ycombinator.com/ 22 | - https://www.reddit.com/r/MachineLearning/ 23 | 24 | HELP: need more ideas on how we should promote new releases 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 William Entriken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SCHEMA.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `questions` ( 2 | `questionid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 3 | `name` TEXT NOT NULL, 4 | `subname` TEXT 5 | ); 6 | CREATE TABLE "logs" ( 7 | `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 8 | `host` TEXT NOT NULL, 9 | `datetime` TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | `objectid` INTEGER NOT NULL, 11 | `answers` TEXT NOT NULL 12 | ); 13 | CREATE TABLE "objects" ( 14 | `objectid` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, 15 | `name` TEXT NOT NULL, 16 | `subname` TEXT, 17 | `hits` INTEGER NOT NULL DEFAULT 0, 18 | `visible` INTEGER NOT NULL DEFAULT 0, 19 | `calc_logl` REAL NOT NULL 20 | ); 21 | CREATE TABLE "answers" ( 22 | `objectid` INTEGER NOT NULL, 23 | `questionid` INTEGER NOT NULL, 24 | `yes` INTEGER NOT NULL, 25 | `no` INTEGER NOT NULL, 26 | `skip` INTEGER NOT NULL, 27 | `calc_y3lmin1` REAL NOT NULL, 28 | `calc_n3lmin1` REAL NOT NULL, 29 | `calc_s3lmin1` REAL NOT NULL, 30 | `calc_y3lll` REAL NOT NULL, 31 | `calc_n3lll` REAL NOT NULL, 32 | `calc_s3lll` REAL NOT NULL, 33 | `calc_y3ll` REAL NOT NULL, 34 | `calc_n3ll` REAL NOT NULL, 35 | `calc_s3ll` REAL NOT NULL, 36 | PRIMARY KEY(`objectid`,`questionid`) 37 | ); 38 | CREATE UNIQUE INDEX `questions_name` ON `questions` (`name` ,`subname` ); 39 | CREATE UNIQUE INDEX `objects_name` ON `objects` (`name` ,`subname` ) 40 | -------------------------------------------------------------------------------- /sources/autoload.php: -------------------------------------------------------------------------------- 1 | referrer) . $url; 15 | } 16 | $ch = curl_init(); 17 | curl_setopt($ch, CURLOPT_URL, $url); 18 | curl_setopt($ch, CURLOPT_REFERER, $this->referrer); 19 | curl_setopt($ch, CURLOPT_HEADER, 1); 20 | curl_setopt($ch, CURLOPT_VERBOSE, 1); 21 | if (!empty($post)) { 22 | curl_setopt($ch, CURLOPT_POST, true); 23 | curl_setopt($ch, CURLOPT_POSTFIELDS, $post); 24 | } 25 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 26 | curl_setopt($ch,CURLOPT_USERAGENT,'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13'); 27 | $this->referrer = $url; 28 | $this->page = curl_exec($ch); 29 | echo "##\n## LOADED URL {$url}\n##\n{$this->page}\n\n"; 30 | } 31 | 32 | function preg_extract_first($pattern) 33 | { 34 | if (!preg_match($pattern, $this->page, $matches)) return null; 35 | if (count($matches) < 2) return null; 36 | echo "##\n## EXTRACTED: {$pattern}\n## GOT: {$matches[1]}\n##\n\n"; 37 | return $matches[1]; 38 | } 39 | } 40 | 41 | ################################################################################ 42 | ## Now get to work 43 | ################################################################################ 44 | $scraper = new Scraper(); 45 | $scraper->referrer = 'http://y.20q.net/play'; 46 | 47 | // Load login page 48 | $scraper->loadUrl('http://y.20q.net/gsq-en'); 49 | $actionUrl = $scraper->preg_extract_first('/
/'); 50 | 51 | // Post form to start game 52 | $scraper->loadUrl($actionUrl, ['submit' => ' Play ']); 53 | 54 | $i = 1; 55 | do { 56 | $scraper->page = preg_replace('|page); 57 | $buttons = []; 58 | $buttons[] = $scraper->preg_extract_first('|]*>Unknown|'); 59 | $buttons[] = $scraper->preg_extract_first('|]*> Yes |'); 60 | $buttons[] = $scraper->preg_extract_first('|]*>  No  |'); 61 | $buttons[] = $scraper->preg_extract_first('|]*>Right|'); 62 | $buttons = array_filter($buttons); 63 | if (!count($buttons)) break; 64 | $url = $buttons[array_rand($buttons)]; 65 | $scraper->loadUrl($url); 66 | $i++; 67 | } while($i < 25); 68 | 69 | #TODO: Output the stuff we learned, like: 70 | # 71 | # You were thinking of gunpowder.
72 | # You said it's classified as Unknown, 20Q was taught by other players that the answer is Mineral.
73 | # Is it shiny? You said Yes, 20Q was taught by other players that the answer is No.
74 | # Do you use it when it rains? You said No, 20Q was taught by other players that the answer is Yes.
75 | # 76 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | query('SELECT COUNT(*) FROM questions')->fetchColumn(); 7 | $oCount = $db->query('SELECT COUNT(*) FROM objects')->fetchColumn(); 8 | $cCount = $db->query('SELECT COUNT(*) FROM answers')->fetchColumn(); 9 | $aCount = $db->query('SELECT COALESCE(SUM(yes+no+skip), 0) FROM answers')->fetchColumn(); 10 | $oHits = $db->query('SELECT COALESCE(SUM(hits), 0) FROM objects')->fetchColumn(); 11 | $newObjects = $db->query('SELECT COUNT(*) FROM objects WHERE visible=0')->fetchColumn(); 12 | 13 | // Calculate entropy of objects 14 | // Iterative calculation of entropy (the "with bits" flavor) 15 | // https://fulldecent.blogspot.com/2009/12/interesting-properties-of-entropy.html 16 | 17 | $sumFrequencies = 0; 18 | $logBase = 2; // bits 19 | $sumFLogF = 0; 20 | //$productFPowerF = 1; 21 | $entropy = 0; // in bits 22 | 23 | $query = $db->query('SELECT hits FROM objects WHERE hits > 0'); 24 | $hitCounts = $query->fetchAll(PDO::FETCH_COLUMN); 25 | foreach ($hitCounts as $frequency) { 26 | //TODO: Don't use frequency, use freq+1, also use precalculated values 27 | $sumFrequencies += $frequency; 28 | $sumFLogF += $frequency * log($frequency, $logBase); 29 | //$productFPowerF *= pow($frequency, $frequency); 30 | } 31 | if ($sumFrequencies > 0) { 32 | $entropy = log($sumFrequencies, $logBase) - $sumFLogF / $sumFrequencies; 33 | } 34 | ?> 35 | 36 | 37 | 38 | 39 | 40 | 19 Questions 41 | 42 | 43 | 44 | 45 |
46 | 47 |

48 | Thank you for playing! Please tell everyone you know about me. That's how I learn. 49 |

50 | 51 |
52 |

19 Questions

53 |

Think of something a common person would know about, then I ask you questions and guess what it is.

54 |

Play

55 |
56 | 57 |
58 |
59 |

Background

60 |

61 | 19 Questions is a machine-learning game that finds out about things by playing games with users. The algorithm is based on Bayesian statistics, compared to other similar games use neural networks. 62 |

63 |

64 | See also: 65 | notes on the the Bayesian/entropy approach 66 | and 67 | the GitHub project. 68 |

69 |
70 |
71 |

Statistics

72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Questions in knowledge base:
Objects in knowledge base:
Connections in knowledge base:
Games played:
New user-submitted objects:
Total object entropy:
80 |
81 |
82 |
83 | 84 |
85 |
86 | No cookies. No analytics. 87 |
88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /TECHNICAL.md: -------------------------------------------------------------------------------- 1 | TODO: fix LaTeX transcription errors in formulas, see blog post and double check 2 | 3 | # The math 4 | 5 | You have a bag of marbles {3 red, 2 blue, 1 black}. What is the probability of choosing the black one with a blindfold on? $\frac{1}{6}$. This is BASIC PROBABILITY (we wont be using this). 6 | 7 | You have a bag of some purple, green, and white marbles. You pick & replace a marble several times, the observation results are {4 purple, 2 green, 6 white}. What is the probability of picking purple next time? The PROBABILITY is unknown, but you can ESTIMATE it as $\frac{(4+1)}{(4+1) + (2+1) + (6+1)}$. This is MAXIMUM LIKELIHOOD ESTIMATION. 8 | 9 | (Another ESTIMATE could be "there are so many colors out there, the odds of getting exactly green again are zero". There are even more ESTIMATION techniques, but we are using MLE today.) 10 | 11 | Before somebody picks a marble, we may want to calculate ENTROPY, which is how much uncertainty we have about which marble they will pick. ENTROPY requires knowledge of PROBABILITY which we don't have, but we can ESTIMATE using MAXIMUM LIKELIHOOD ESTIMATION. For more on ENTROPY, see http://fulldecent.blogspot.com/2009/12/interesting-properties-of-entropy.html 12 | 13 | Someone reaches in that bag and pull out a "dark" marble. Here everyone agrees that purple and green are dark but white isn't. We don't know the PROBABILITY it is purple or green, but can use MLE to get $\frac{(4+1)}{(4+1) + (2+1)}$ chance of purple and $\frac{(2+1)}{(4+1) + (2+1)}$ chance of green. This is MAXIMUM A POSTERIORI ESTIMATION. 14 | 15 | In the real world, someone picks a marble and tells us it's "dark". We can't be certain on the definition of "dark", but three people said purple is dark, 2 said green was dark, 1 said green was not dark, and nobody said anything about white. Likelihood of each marble in this situation is ESTIMATED with MLE and 16 | MAP. 17 | 18 | - Purple: $\frac{(4+1)}{(4+1) + (2+1) + (6+1)}\frac{(3+1)}{((3+1) + (0+1))total}$ 19 | 20 | - Green: $\frac{(2+1)}{(4+1) + (2+1) + (6+1)}\frac{(2+1)}{((2+1) + (1+1))total}$ 21 | 22 | - White: $\frac{(6+1)}{(4+1) + (2+1) + (6+1)}\frac{(0+1)}{((0+1) + (0+1))total}$ 23 | 24 | Those likelihoods above can be used to ESTIMATE the ENTROPY of the situation. 25 | 26 | Someone just chose a marble, what is the PROBABILITY of them agreeing it is "dark"? We don't know what marbles there are and we are we're not even sure about colors now, but we can use MLE and MAP to ESTIMATE. So let's compare the ESTIMATES of "dark" and not "dark". The LIKELIHOOD of them agreeing "dark" will be the sum of the first three divided by all six options. 27 | 28 | **Situations they would agree it is "dark" are (using MLE):** 29 | 30 | - Purple: $\frac{(4+1)}{(4+1) + (2+1) + (6+1)}\frac{(3+1)}{(3+1) + (0+1)}$ 31 | 32 | - Green: $\frac{(2+1)}{(4+1) +(2+1) + (6+1)}\frac{(2+1)}{(2+1) + (1+1)}$ 33 | 34 | - White: $\frac{(6+1)}{(4+1) + (2+1) + (6+1)}\frac{(0+1)}{(0+1) + (0+1)}$ 35 | 36 | **Situations they would not agree it is "dark" are (using MLE):** 37 | 38 | - Purple: $\frac{(4+1)}{(4+1) + (2+1) + (6+1)}\frac{(0+1)}{(3+1) + (0+1)}$ 39 | 40 | - Green: $\frac{(2+1)}{(4+1) + (2+1) + (6+1)}\frac{(1+1)}{(2+1) + (1+1)}$ 41 | 42 | - White: $\frac{(6+1)}{(4+1) + (2+1) + (6+1)}\frac{(0+1)}{(0+1) + (0+1)}$ 43 | 44 | After they answer we can calculate ENTROPY of the marbles using MAP. But since we have estimated the likelihood of both possible results and the ENTROPY under each, we can superpose before asking the question to calculate the EXPECTED VALUE of ENTROPY. Comparing ENTROPY before we ask to EXPECTED VALUE after we ask gives us the EXPECTED ENTROPY reduction of the question. 45 | 46 | The goal of this game is to choose the best questions (i.e. that are ESTIMATED to reduce EXPECTED ENTROPY the most). 47 | 48 | ## What’s going on here 49 | 50 | The algorithm is: 51 | 52 | 1. Use past answers and bayesian median likelihood formula to assess "probabilities" of objects being correct. 53 | 54 | 2. Choose questions which minimize the expected entropy of this set of probabilities. (We use "expected" here: a weighted average based on likelihood of each question response) 55 | 56 | Calculating entropy in bits is slow: 57 | 58 | $$-\sum{\frac{x_i}{\sum x_i} log_2(\frac{x_i}{\sum{x_i}})}$$ 59 | 60 | or: 61 | 62 | $$log_2(\prod{\frac{x_i}{\sum{x_i}}^\frac{-x_i}{\sum{x_i}}})$$ 63 | 64 | because you need to find the total likelihood and then go revisit each outcome to perform the calculation, and you need to do logarithms or exponents. This needs to be done for each question for each object for each round. 65 | 66 | **Optimization:** 67 | 68 | $$log_2(\frac{\sum{x_i-\sum(x_i log_2 x_i)}}{\sum{x_i}})$$ 69 | 70 | here you can pre-calculate most of the math AND you only need to visit outcomes once. 71 | 72 | http://fulldecent.blogspot.com/2009/12/interesting-properties-of-entropy.html 73 | -------------------------------------------------------------------------------- /play.php: -------------------------------------------------------------------------------- 1 | askedQuestions); 7 | ?> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 Questions 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |

19 Questions you think of something, we guess it

26 | 27 | = 40): ?> 28 |

You win this round!

29 |

Were you thinking of one of these:

30 | getTopHunches() as $hunch) { 32 | $nameHtml = htmlspecialchars($hunch->name); 33 | if (!empty($hunch->subname)) $nameHtml .= '(' . htmlspecialchars($hunch->subname) . ')'; 34 | echo "objectId}&q=".$nq->state."\">{$nameHtml}\n"; 35 | } 36 | ?> 37 |
38 |

If not, please type the thing here:

39 | 40 | 41 | 42 | 43 | 44 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | = 19) { 70 | echo "
You've won this round! You can continue answering a few more questions or state."\" class=\"btn btn-info\">tell me what you were thinking of so I can learn.
\n"; 71 | } 72 | 73 | echo "

#". ($numQuestions+1) ." "; 74 | 75 | list($text, $subtext, $choices) = $nq->getNextQuestion(); 76 | if (strlen($subtext)) $text .= " ($subtext)"; 77 | echo "$text? "; 78 | 79 | foreach ($choices as $choice) { 80 | if (preg_match('/w([0-9]+)/',$choice[1],$regs)) 81 | $prefix = "save.php?obj={$regs[1]}&q="; 82 | else 83 | $prefix = basename($_SERVER['PHP_SELF']).'?'.(isset($_GET['debug'])?'debug&':'')."q="; 84 | echo "".$choice[0]." "; 85 | } 86 | echo "


"; 87 | 88 | foreach (array_reverse($nq->askedQuestions) as $pastQuestion) { 89 | list($name, $subtext, $answer) = $pastQuestion; 90 | if (strlen($subtext)) $name .= " ($subtext)"; 91 | echo "

".htmlentities($name)." — $answer

"; 92 | } 93 | ?> 94 | 95 | 96 |
97 | 98 | 99 |
100 |
101 |

Hunches

102 | 103 | getTopHunches() as $hunch) { 105 | $htmlName = htmlspecialchars($hunch->name); 106 | if (!empty($hunch->subname)) { 107 | $htmlName .= ' (' . htmlspecialchars($hunch->subname) . ')'; 108 | } 109 | echo '
' . $htmlName . '' . number_format($hunch->likelihood*100 / $nq->objectSumLikelihood, 3) . '%'; 110 | echo '
'; 111 | echo '
'; 112 | echo '
'; 113 | echo '
'; 114 | } 115 | echo '
Total entropy:' . number_format($nq->objectEntropy, 2) . ' bits'; 116 | ?> 117 |
118 |
119 |
120 |

Top questions

121 | 122 | getBestQuestions(10) as $question) { 124 | list($score, $id, $name, $sub, $yes, $no) = $question; 125 | if (strlen($sub)) $name .= " ($sub)"; 126 | echo "
$name" . number_format($score, 3) . " bits"; 127 | echo "
"; 128 | echo '
'; 129 | echo '
'; 130 | echo '
'; 131 | echo '
'; 132 | echo '
'; 133 | } 134 | ?> 135 |
136 |
137 |
138 |
139 | 140 | 141 |
142 |
143 | No cookies. No analytics. 144 |
145 |
146 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /sources/Engine.php: -------------------------------------------------------------------------------- 1 | database = new \NineteenQ\Db(); 35 | $questionStatement = $this->database->prepare('SELECT name, subname FROM questions WHERE questionid=?'); 36 | $objectStatement = $this->database->prepare('SELECT name, subname FROM objects WHERE objectid=?'); 37 | 38 | ## 39 | ## Parse the STATE string 40 | ## 41 | $this->yesses = $this->noes = $this->skips = $this->guesses = []; 42 | $this->askedQuestions = []; 43 | preg_match_all('/([ynsg])(\d+)/', $state, $regs, PREG_SET_ORDER); 44 | foreach ($regs as $reg) { 45 | switch ($reg[1]) { 46 | case 'y': 47 | $questionStatement->execute([$reg[2]]); 48 | list($name, $sub) = $questionStatement->fetch(\PDO::FETCH_NUM); 49 | $this->yesses[] = $reg[2]; 50 | $this->askedQuestions[] = array($name, $sub, 'yes', $reg[2]); 51 | break; 52 | case 'n': 53 | $questionStatement->execute([$reg[2]]); 54 | list($name, $sub) = $questionStatement->fetch(\PDO::FETCH_NUM); 55 | $this->noes[] = $reg[2]; 56 | $this->askedQuestions[] = array($name, $sub, 'no', $reg[2]); 57 | break; 58 | case 's': 59 | $questionStatement->execute([$reg[2]]); 60 | list($name, $sub) = $questionStatement->fetch(\PDO::FETCH_NUM); 61 | $this->skips[] = $reg[2]; 62 | $this->askedQuestions[] = array($name, $sub, 'skip', $reg[2]); 63 | break; 64 | case 'g': 65 | $objectStatement->execute([$reg[2]]); 66 | list($name, $sub) = $objectStatement->fetch(\PDO::FETCH_NUM); 67 | $this->guesses[] = $reg[2]; 68 | $this->askedQuestions[] = array('I am guessing that it is ' . $name, $sub, 'wrong', $reg[2]); 69 | break; 70 | default: 71 | break; 72 | } 73 | $this->state .= $reg[1] . $reg[2]; 74 | } 75 | $this->debug[__FUNCTION__] = number_format(microtime(true) - $start, 3) . ' SECONDS'; 76 | } 77 | 78 | /** 79 | * estimateObjectLikelihoods 80 | * 81 | * Create temporary table `evidence` storing object IDs and likelihood. 82 | * Likelihood that the user believes OBJECT matches given yes/no/skip predicates. 83 | * 84 | * If we have no past knowledge regarding QUESTION, then we estimate likelihood 85 | * that user agrees QUESTION is YES for an object is 1/3. (Because there are 86 | * three options, yes/no/skip). 87 | * 88 | * To simplify this default value, we instead calculate the log (base 2) of 89 | * three times the likelihood. Now the default is log(3*1/3)=0. Much nicer. 90 | */ 91 | function estimateObjectLikelihoods() 92 | { 93 | $start = microtime(true); 94 | if (!empty($this->_didEstimateObjectLikelihoods)) return; 95 | $this->_didEstimateObjectLikelihoods = 1; 96 | 97 | $placeholdersY = implode(',' , array_fill(0, count($this->yesses), '?')); 98 | $placeholdersN = implode(',' , array_fill(0, count($this->noes), '?')); 99 | $placeholdersS = implode(',' , array_fill(0, count($this->skips), '?')); 100 | $placeholdersG = implode(',' , array_fill(0, count($this->guesses), '?')); 101 | $sql = <<yesses, $this->noes, $this->skips, $this->guesses); 120 | $statement = $this->database->prepare($sql); 121 | $statement->execute($binds); 122 | 123 | //TODO: I'd rather do this transformation in SQL but SQLite math functions are limited. Any tricks? 124 | $this->database->beginTransaction(); 125 | $this->database->exec('CREATE TEMPORARY TABLE object_likelihood(objectid PRIMARY KEY, l REAL, lll REAL)'); 126 | $insertStatement = $this->database->prepare('INSERT INTO object_likelihood VALUES(?,?,?)'); 127 | $this->objectEntropy = 0; 128 | $this->objectSumLikelihood = 0; 129 | $this->objectSumLLL = 0; 130 | while($row = $statement->fetch(\PDO::FETCH_NUM)) { 131 | list($objectId, $logL) = $row; 132 | $l = pow(2, $logL); 133 | $values = [$objectId, $l, $l*$logL]; 134 | $insertStatement->execute($values); 135 | $this->objectSumLikelihood += $l; 136 | $this->objectSumLLL += $l*$logL; 137 | } 138 | $this->objectEntropy = log($this->objectSumLikelihood, 2) - $this->objectSumLLL / $this->objectSumLikelihood; 139 | $this->database->commit(); 140 | $this->debug[__FUNCTION__] = number_format(microtime(true) - $start, 3) . ' SECONDS'; 141 | } 142 | 143 | // Returns [(object)[objectId=>..., name=>..., subname=>..., likelihood=>...]] 144 | // Sorted in order of best guess first 145 | function getTopHunches() 146 | { 147 | if (!empty($this->hunches)) return $this->hunches; 148 | $this->estimateObjectLikelihoods(); 149 | $sql = <<database->query($sql); 157 | $this->hunches = $statement->fetchAll(\PDO::FETCH_OBJ); 158 | return $this->hunches; 159 | } 160 | 161 | /** 162 | * getBestQuestions 163 | * 164 | * Each question we could ask has a YES/NO/SKIP answer. We can estimate 165 | * likelihood of each response and the entropy of system state given responses. 166 | * So we pick the question that are expected to reduce entropy the most. 167 | * 168 | * @return [[score, questionid, name, subname, yesLikelihood, noLikelihood]] 169 | */ 170 | function getBestQuestions() 171 | { 172 | if (!empty($this->_bestQuestions)) return $this->_bestQuestions; 173 | $start = microtime(true); 174 | $this->estimateObjectLikelihoods(); 175 | $binds = array_merge($this->yesses, $this->noes, $this->skips); 176 | $placeholdersSkipQuestions = implode(',', array_fill(0, count($binds), '?')); 177 | $sql = <<database->prepare($sql); 198 | $statement->execute($binds); 199 | $questions = []; 200 | while ($row = $statement->fetch(\PDO::FETCH_NUM)) { 201 | #var_dump($row); 202 | #die(); 203 | list($questionId, $name, $subname, $yesDeltaL, $yesDeltaLLL, $noDeltaL, $noDeltaLLL, $skipDeltaL, $skipDeltaLLL) = $row; 204 | $yesSumL = $yesDeltaL + $this->objectSumLikelihood; 205 | $noSumL = $noDeltaL + $this->objectSumLikelihood; 206 | $skipSumL = $skipDeltaL + $this->objectSumLikelihood; 207 | $yesSumLLL = $yesDeltaLLL + $this->objectSumLLL; 208 | $noSumLLL = $noDeltaLLL + $this->objectSumLLL; 209 | $skipSumLLL = $skipDeltaLLL + $this->objectSumLLL; 210 | $denom = $yesSumL + $noSumL + $skipSumL; 211 | $yesLikelihood = $yesSumL / $denom; 212 | $noLikelihood = $noSumL / $denom; 213 | $skipLikelihood = $skipSumL / $denom; 214 | $yesEntropy = log($yesSumL, 2) - $yesSumLLL / $yesSumL; 215 | $noEntropy = log($noSumL, 2) - $noSumLLL / $noSumL; 216 | $skipEntropy = log($skipSumL, 2) - $skipSumLLL / $skipSumL; 217 | $score = $this->objectEntropy - ($yesLikelihood*$yesEntropy + $noLikelihood*$noEntropy + $skipLikelihood*$skipEntropy); 218 | $questions[] = [$score, $questionId, $name, $subname, $yesLikelihood, $noLikelihood]; 219 | } 220 | rsort($questions); 221 | $this->debug[__FUNCTION__] = number_format(microtime(true) - $start, 3) . ' SECONDS'; 222 | $this->_bestQuestions = array_slice($questions, 0, 10); 223 | return $this->_bestQuestions; 224 | } 225 | 226 | // Returns array(score of question "i am thinking of OBJ", top object name, top object subtext) 227 | // [score, questionid, name, subname, yesLikelihood, noLikelihood] 228 | function getGuessQuestion() 229 | { 230 | $hunches = $this->getTopHunches(); 231 | $topHunch = $hunches[0]; 232 | 233 | $entropyIfGuessCorrect = 0; 234 | $sumLikelihoodIfGuessWrong = $this->objectSumLikelihood - $topHunch->likelihood; 235 | $sumLLLIfGuessWrong = $this->objectSumLLL - $topHunch->likelihood * log($topHunch->likelihood, 2); 236 | $entropyIfGuessWrong = log($sumLikelihoodIfGuessWrong, 2) - $sumLLLIfGuessWrong / $sumLikelihoodIfGuessWrong; 237 | $expectedEntropy = $entropyIfGuessCorrect * ($topHunch->likelihood/$this->objectSumLikelihood) + 238 | $entropyIfGuessWrong * (1 - $topHunch->likelihood/$this->objectSumLikelihood); 239 | $score = $this->objectEntropy - $expectedEntropy; 240 | 241 | //TODO remove this fudge factor when we have some reliable data in ANSWERS table 242 | $score = $score / 3; 243 | 244 | return [$score, $topHunch->objectId, 'I am guessing that it is ' . $topHunch->name, $topHunch->subname]; 245 | } 246 | 247 | // [name, subname, [response, token]] 248 | function getNextQuestion() 249 | { 250 | $answers = []; 251 | $questionNumber = count($this->askedQuestions) + 1; 252 | $doGuessQuestion = false; 253 | if (($questionNumber == 19) || ($questionNumber > 21 && $questionNumber % 4 == 0)) { 254 | $doGuessQuestion = true; 255 | } 256 | 257 | $questions = $this->getBestQuestions(); 258 | $guessQuestion = $this->getGuessQuestion(); 259 | if ($guessQuestion[0] > $questions[0][0]) { // rank by ->score 260 | $doGuessQuestion = true; 261 | } 262 | 263 | if ($doGuessQuestion) { 264 | list($gscore, $gid, $gname, $gsub) = $this->getGuessQuestion(); 265 | $answers[] = ['Right', $this->state.'w'.$gid]; 266 | $answers[] = ['Wrong', $this->state.'g'.$gid]; 267 | return [$gname, $gsub, $answers]; 268 | } 269 | 270 | if (!count($questions)) return ['no questions', '', []]; 271 | $choice = 0; 272 | if ($questions[4][0] > $questions[0][0] * 0.90) { 273 | # Have some fun here, the top questions are pretty close, pick one at random 274 | $choice = rand(0,4); 275 | } 276 | 277 | list($nscore, $nid, $nname, $nsub) = $questions[$choice]; 278 | $answers[] = ['Yes', $this->state.'y'.$nid]; 279 | $answers[] = ['No', $this->state.'n'.$nid]; 280 | $answers[] = ['Skip this question', $this->state.'s'.$nid]; 281 | return [$nname, $nsub, $answers]; 282 | } 283 | 284 | function getObject($objectId) 285 | { 286 | $statement = $this->database->prepare('SELECT name, subname FROM objects WHERE objectid = ?'); 287 | $statement->execute([$objectId]); 288 | return $statement->fetch(\PDO::FETCH_NUM); 289 | } 290 | 291 | function getObjectByName($name) 292 | { 293 | $name = preg_replace('/[^a-z0-9_, ]/','', $name); 294 | $sql = 'SELECT objectid FROM objects WHERE name = ?'; 295 | $statement = $this->database->prepare($sql); 296 | $statement->execute([$name]); 297 | if ($objectId = $statement->fetchColumn()) { 298 | return $objectId; 299 | } 300 | 301 | $sql = 'INSERT INTO objects (name, hits, calc_logl) VALUES (?,?,?)'; 302 | $statement = $this->database->prepare($sql); 303 | $statement->execute([$name, 1, log(1+1, 2)]); 304 | return $this->database->lastInsertId(); 305 | } 306 | 307 | // commit answers to the database using the current state 308 | function teach($objectId) 309 | { 310 | $sql1 = 'SELECT hits FROM objects WHERE objectid = ?'; 311 | $statement1 = $this->database->prepare($sql1); 312 | $statement1->execute([$objectId]); 313 | $hits = $statement1->fetchColumn(); 314 | 315 | $sql2 = 'UPDATE objects SET hits = hits + 1, calc_logl = ? WHERE objectid = ?'; 316 | $statement2 = $this->database->prepare($sql2); 317 | $statement2->execute([log($hits + 1, 2), $objectId]); 318 | 319 | $sql3 = 'INSERT OR IGNORE INTO answers (objectid, questionid) VALUES (?,?)'; 320 | $statement3 = $this->database->prepare($sql3); 321 | $sql4y = 'UPDATE answers SET yes = yes+1 WHERE objectid = ? AND questionid = ?'; 322 | $statement4y = $this->database->prepare($sql4y); 323 | $sql4n = 'UPDATE answers SET no = no+1 WHERE objectid = ? AND questionid = ?'; 324 | $statement4n = $this->database->prepare($sql4n); 325 | $sql4s = 'UPDATE answers SET skip = skip+1 WHERE objectid = ? AND questionid = ?'; 326 | $statement4s = $this->database->prepare($sql4s); 327 | 328 | $sql5 = 'SELECT yes, no, skip FROM answers WHERE objectid = ? AND questionid = ?'; 329 | $statement5 = $this->database->prepare($sql5); 330 | $sql6 = 'UPDATE answers SET calc_y3lmin1=?, calc_n3lmin1=?, calc_s3lmin1=?, calc_y3lll=?, calc_n3lll=?, calc_s3lll=?, calc_y3ll=?, calc_n3ll=?, calc_s3ll=? WHERE objectid = ? AND questionid = ?'; 331 | $statement6 = $this->database->prepare($sql6); 332 | 333 | foreach ($this->askedQuestions as $question) { 334 | list($name, $subname, $answer, $questionId) = $question; 335 | $statement4 = null; 336 | switch ($answer) { 337 | case 'yes': 338 | $statement4 = $statement4y; 339 | break; 340 | case 'no': 341 | $statement4 = $statement4n; 342 | break; 343 | case 'skip': 344 | $statement4 = $statement4s; 345 | break; 346 | } 347 | if (!empty($statement4)) { 348 | $statement4->execute([$objectId, $questionId]); 349 | $statement5->execute([$objectId, $questionId]); 350 | list($yes, $no, $skip) = $statement5->fetch(\PDO::FETCH_NUM); 351 | $binds = []; 352 | $binds[] = 3*($yes+1)/($yes+$no+$skip+3)-1; // calc_y3min1 353 | $binds[] = 3*($no+1)/($yes+$no+$skip+3)-1; // calc_n3min1 354 | $binds[] = 3*($skip+1)/($yes+$no+$skip+3)-1; // calc_s3min1 355 | $binds[] = $binds[0] * log($binds[0], 2); // calc_y3lll 356 | $binds[] = $binds[1] * log($binds[1], 2); // calc_n3lll 357 | $binds[] = $binds[2] * log($binds[2], 2); // calc_s3lll 358 | $binds[] = log($binds[0], 2); // calc_y3ll 359 | $binds[] = log($binds[1], 2); // calc_n3ll 360 | $binds[] = log($binds[2], 2); // calc_s3ll 361 | $statement6->execute($binds); 362 | } 363 | } 364 | } 365 | } 366 | 367 | ?> 368 | --------------------------------------------------------------------------------