By Luong
This is the extension of the generics model using gradable adjectives. What this means is rather than just using the generic statement for traits creatures either have or do not have, such as having claws or legs, it will now also be able to apply to traits that are not so cut and dry. In this case, the trait is tallness.
Before getting into the listener and speaker, we have to get into the biggest difference between this code and the previous code, theWorld. Rather than reasoning over our actual real Earth and actual real creatures, this code is going to be reasoning over a set of autogenerated creatures. Here we can see three different creatures, feps, wugs, and glippets, and each creature will be generated with different traits. In this code, the traits are whether or not they have wings, legs, or claws, and the gradable trait, height, will be a number from 0 to 1, with 0 being the shortest and 1 being the tallest.
/// fold:
var round = function(x){
var rounded = Math.round(10*x)/10
return rounded == 0 ? 0.01 : rounded
}
var makeHistogram = function(prevalences){
return map(function(s){
return reduce(function(x, i){
var k = x == s ? 1 : 0
return i + k
}, 0.001, prevalences)
}, stateBins)
}
var altBeta = function(g, d){
var a = g * d;
var b = (1-g) * d;
return beta(a, b)
}
var fep = function() {
return {
kind: "fep",
wings: flip(0.5),
legs: flip(0.01),
claws: flip(0.01),
height: altBeta(0.5, 10)
}
}
var wug = function() {
return {
kind: "wug",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.3),
height: altBeta(0.2, 10)
}
}
var glippet = function() {
return {
kind: "glippet",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.2),
height: altBeta(0.8, 10)
}
}
///
var stateBins = [0.01,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]
var theWorld = _.flatten([repeat(10, fep), repeat(10, wug), repeat(10, glippet)])
var kinds = _.uniq(_.map(theWorld, "kind"));
var allKinds = _.uniq(_.map(theWorld, "kind"))
var prevalence = function(world, kind, property){
var members = _.filter(world, {kind: kind})
return round(listMean(_.map(members, property)))
}
var prevalencePrior = function(property, world){
var p = map(function(k){return prevalence(world, k, property)}, allKinds)
return makeHistogram(p)
}
print('height distribution over all creatures')
viz.density(_.map(theWorld, "height"))
var rs = map(function(k){
print('height distribution for ' + k)
viz.density(_.map(_.filter(theWorld,{kind: k}), "height"), {bounds:[0,1]})
}, kinds)
print('leg prevalence over all creatures')
viz.bar(stateBins, prevalencePrior("legs", theWorld))
print('wing prevalence over all creatures')
viz.bar(stateBins, prevalencePrior("wings", theWorld))
theWorld
Running the code will show graphs showing the height distributions of the world and then the height distributions of each species of creature. Comparing the height graphs to the code, you can see that the highest peak of each graph corresponds to the first number in the height code of each creature. This number will determine the mean height of each creature. Also, as the world is randomly generated, running the code again will generate completely different graphs. However, you can see that the highest peak of each graph stays generally the same. Then there are the graphs of the prevalences of the other traits. The bars of these graphs correspond to the number being flipped for the other traits. For example, looking at the leg graph, you can see a bar at the bottom, the flip(0.01) of feps, and either one taller bar or two bars at the top, the flip(0.99) of the other two creatures. The wings graph shows a different picture. Due to the fact all the creatures have a 0.5 chance of having wings, the bar will vary wildly. Running the code multiple times will show very different graphs, compared to the mostly similar graphs of the legs category. Finally there is theWorld. Uncommenting it and running the code will show a list of all the creatures in the world and the traits they have. The ungradable traits will be a boolean, while the height will be a number from 0 to 1.
Here are the priors of the code, the knobs and sliders of the model. To keep the code simple, prevalences and thresholds will be set to the nearest tenths, as can be seen in stateBins and thresholdBins. And since it is needed, the alpha of this model is set to 5.
Here in the priors we can see the split in how the code handles ungradable traits and gradable traits. This can be seen in the inclusion of both a prevalencePrior and a propertyPrior. This split is needed since a gradable trait does not really have a prevalence, as all the creatures have a height for example. Then there is the split in both the utterance and meaning functions. Since it is the only gradable adjective, the code gets split here when the utterance relates to height. Why this is necessary will be explained later. Otherwise, these two functions are essentially unchanged from the base model. The utterance is either the generic statement or the null statement, and the meaning function is true for the generic statement when the state is more than the threshold.
///fold:
// discretized range between 0 - 1
var stateBins = [0.01,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]
var thresholdBins = [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
var alpha_1 = 5
var round = function(x){
var rounded = Math.round(10*x)/10
return rounded == 0 ? 0.01 : rounded
}
var makeHistogram = function(prevalences){
return map(function(s){
return reduce(function(x, i){
var k = x == s ? 1 : 0
return i + k
}, 0.001, prevalences)
}, stateBins)
}
var altBeta = function(g, d){
var a = g * d;
var b = (1-g) * d;
return beta(a, b)
}
var fep = function() {
return {
kind: "fep",
wings: flip(0.5),
legs: flip(0.01),
claws: flip(0.01),
height: round(altBeta(0.5, 10))
}
}
var wug = function() {
return {
kind: "wug",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.3),
height: round(altBeta(0.2, 10))
}
}
var glippet = function() {
return {
kind: "glippet",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.2),
height: round(altBeta(0.8, 10))
}
}
///
var theWorld = _.flatten([repeat(10, fep), repeat(10, wug), repeat(10, glippet)])
var allKinds = _.uniq(_.map(theWorld, "kind"))
var propertyDegrees = {
wings: "wings",
legs: "legs",
claws: "claws",
tall:" height"
}
var prevalence = function(world, kind, property){
var members = _.filter(world, {kind: kind})
return round(listMean(_.map(members, property)))
}
var prevalencePrior = function(property, world){
var p = map(function(k){return prevalence(world, k, property)}, allKinds)
return makeHistogram(p)
}
var propertyPrior = function(property){
var p = _.map(theWorld, property)
return makeHistogram(p)
}
var statePrior = function(probs){ return categorical(probs, stateBins) }
var thresholdPrior = function() { return uniformDraw(thresholdBins) }
var utterancePrior = function(property) {
var utterances = property == "height" ?
["tall", "null"] :
["generic", "null"]
return uniformDraw(utterances)
}
var meaning = function(utterance, state, threshold) {
return utterance == "generic" ? state > threshold :
utterance == "tall" ? state > threshold :
true
}
display(stateBins)
display(prevalencePrior("legs", theWorld))
display(prevalencePrior("wings", theWorld))
propertyPrior("height")
Because it could be confusing, I will be explaining what exactly prevalencePrior and propertyPrior return. Running the code, you will see four lists, each with 11 numbers. The lists returned by the prior functions are actually a list of probabilities used for a categorical draw, the 11 numbers corresponding to the 11 stateBins. You can notice that these numbers are similar to the graphs seen earlier. The highest probabilities being the means for each creature.
Now here we have the actual entirety of the code for the model. The most obvious difference is that now there is both an L1 layer and an S1 layer. The reason why these two extra layers are needed for this model compared to the old model will be explained.
///fold:
// discretized range between 0 - 1
var stateBins = [0.01,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]
var thresholdBins = [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
var alpha_1 = 5
var round = function(x){
var rounded = Math.round(10*x)/10
return rounded == 0 ? 0.01 : rounded
}
var makeHistogram = function(prevalences){
return map(function(s){
return reduce(function(x, i){
var k = x == s ? 1 : 0
return i + k
}, 0.001, prevalences)
}, stateBins)
}
var altBeta = function(g, d){
var a = g * d;
var b = (1-g) * d;
return beta(a, b)
}
var fep = function() {
return {
kind: "fep",
wings: flip(0.5),
legs: flip(0.01),
claws: flip(0.01),
height: round(altBeta(0.5, 10))
}
}
var wug = function() {
return {
kind: "wug",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.3),
height: round(altBeta(0.2, 10))
}
}
var glippet = function() {
return {
kind: "glippet",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.2),
height: round(altBeta(0.8, 10))
}
}
///
var theWorld = _.flatten([repeat(10, fep), repeat(10, wug), repeat(10, glippet)])
var allKinds = _.uniq(_.map(theWorld, "kind"))
var propertyDegrees = {
wings: "wings",
legs: "legs",
claws: "claws",
tall:" height"
}
var prevalence = function(world, kind, property){
var members = _.filter(world, {kind: kind})
return round(listMean(_.map(members, property)))
}
var prevalencePrior = function(property, world){
var p = map(function(k){return prevalence(world, k, property)}, allKinds)
return makeHistogram(p)
}
var propertyPrior = function(property){
var p = _.map(theWorld, property)
return makeHistogram(p)
}
var statePrior = function(probs){ return categorical(probs, stateBins) }
var thresholdPrior = function() { return uniformDraw(thresholdBins) }
var utterancePrior = function(property) {
var utterances = property == "height" ?
["tall", "null"] :
["generic", "null"]
return uniformDraw(utterances)
}
var meaning = function(utterance, state, threshold) {
return utterance == "generic" ? state > threshold :
utterance == "tall" ? state > threshold :
true
}
var literalListener = cache(function(utterance, threshold, stateProbs) {
Infer({method: "enumerate"}, function(){
var state = statePrior(stateProbs)
var m = meaning(utterance, state, threshold)
condition(m)
return state
})
})
var speaker1 = cache(function(state, threshold, stateProbs, property) {
Infer({method: "enumerate"}, function(){
var utterance = utterancePrior(property)
var L0 = literalListener(utterance, threshold, stateProbs)
factor(alpha_1 * L0.score(state))
return utterance
})
})
var pragmaticListener = cache(function(utterance, property, world) {
Infer({method: "enumerate"}, function(){
var stateProbs = property == "height" ?
propertyPrior(property) :
prevalencePrior(property, world)
var state = statePrior(stateProbs)
var threshold = thresholdPrior()
var S1 = speaker1(state, threshold, stateProbs, property)
observe(S1, utterance)
return state
})
})
var worldWithTallness = map(function(individual){
var tallDistribution = Infer({method: "enumerate"}, function(){
var utterance = utterancePrior("height")
factor(pragmaticListener(utterance, "height").score(individual.height))
return utterance
})
return _.extend(individual,
{tall: Math.exp(tallDistribution.score("tall"))})
}, theWorld)
var speaker2 = function(kind, predicate){
Infer({method: "enumerate"}, function(){
var property = predicate.split(' ')[1]
var degree = propertyDegrees[property]
var world = _.isNumber(theWorld[0][degree]) ?
worldWithTallness : theWorld
var prev = prevalence(world, kind, property)
var utterance = utterancePrior(property)
var L1 = pragmaticListener(utterance, property, world)
factor(2*L1.score(prev))
return utterance=="generic" ?
kind + "s " + predicate :
"don't think so"
})
}
Comparing the L0 and S0 layers of the old code and the extended code, we can see why the new layers are needed. The two models essentially have the same literal listener. They take in a random state and compare it to a random threshold. But how the S0 layer is implemented really shows the difference. In the old model, the S0 layer gets a lot of prior knowledge from the get-go, and this is due to the “prior” variable. The speaker already knows the prevalence of a trait in the world and it already knows the prevalence of a trait among a single creature. But looking at the extended model, it is still just comparing thresholds and states for a single property. Neither speaker nor listener knows what creature is being talked about yet nor the prevalence of that trait. The old model only needs to go up to S0 because it has so much prior information inputted into it. On the opposite side of the spectrum, due to the fact that this is a randomly generated world, the extended model needs more layers to reason about the world, each creature, and each property separately.
///fold:
// discretized range between 0 - 1
var stateBins = [0.01,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]
var thresholdBins = [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
var alpha_1 = 5
var round = function(x){
var rounded = Math.round(10*x)/10
return rounded == 0 ? 0.01 : rounded
}
var makeHistogram = function(prevalences){
return map(function(s){
return reduce(function(x, i){
var k = x == s ? 1 : 0
return i + k
}, 0.001, prevalences)
}, stateBins)
}
var altBeta = function(g, d){
var a = g * d;
var b = (1-g) * d;
return beta(a, b)
}
var fep = function() {
return {
kind: "fep",
wings: flip(0.5),
legs: flip(0.01),
claws: flip(0.01),
height: round(altBeta(0.5, 10))
}
}
var wug = function() {
return {
kind: "wug",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.3),
height: round(altBeta(0.2, 10))
}
}
var glippet = function() {
return {
kind: "glippet",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.2),
height: round(altBeta(0.8, 10))
}
}
///
var theWorld = _.flatten([repeat(10, fep), repeat(10, wug), repeat(10, glippet)])
var allKinds = _.uniq(_.map(theWorld, "kind"))
var propertyDegrees = {
wings: "wings",
legs: "legs",
claws: "claws",
tall:" height"
}
var prevalence = function(world, kind, property){
var members = _.filter(world, {kind: kind})
return round(listMean(_.map(members, property)))
}
var prevalencePrior = function(property, world){
var p = map(function(k){return prevalence(world, k, property)}, allKinds)
return makeHistogram(p)
}
var propertyPrior = function(property){
var p = _.map(theWorld, property)
return makeHistogram(p)
}
var statePrior = function(probs){ return categorical(probs, stateBins) }
var thresholdPrior = function() { return uniformDraw(thresholdBins) }
var utterancePrior = function(property) {
var utterances = property == "height" ?
["tall", "null"] :
["generic", "null"]
return uniformDraw(utterances)
}
var meaning = function(utterance, state, threshold) {
return utterance == "generic" ? state > threshold :
utterance == "tall" ? state > threshold :
true
}
var literalListener = cache(function(utterance, threshold, stateProbs) {
Infer({method: "enumerate"}, function(){
var state = statePrior(stateProbs)
var m = meaning(utterance, state, threshold)
condition(m)
return state
})
})
var speaker1 = cache(function(state, threshold, stateProbs, property) {
Infer({method: "enumerate"}, function(){
var utterance = utterancePrior(property)
var L0 = literalListener(utterance, threshold, stateProbs)
factor(alpha_1 * L0.score(state))
return utterance
})
})
var sample_prev = prevalencePrior("wings", theWorld)
// viz(literalListener("generic", 0.2, sample_prev))
// viz(literalListener("null", 0.2, sample_prev))
speaker1(0.8, 0.2, sample_prev, "wings")
// the old model
var listener = function(utterance, statePrior) {
Infer({model: function(){
var prevalence = sample(statePrior)
var threshold = thresholdPrior()
var m = meaning(utterance, prevalence, threshold)
condition(m)
return prevalence
}})
}
var speaker = function(prevalence, statePrior){
Infer({model: function(){
var utterance = utterancePrior();
var L = listener(utterance, statePrior);
factor( alpha * (L.score(prevalence) - cost[utterance]))
return utterance
}})
}
var observed_prevalence = 0.03
var prior = priorModel({
potential: 0.01,
prevalenceWhenPresent: 0.01,
concentrationWhenPresent: 5
})
viz.density(prior)
viz(speaker(observed_prevalence, prior))
Running the code here just shows how basic these layers are compared to the old model. The literal listener is just showing if a state is higher than the inputted threshold. Here a randomly generated state using the prevalencePrior of wings is used for example. For the generic statement, it is just the prevalence graph of wings above the inputted threshold of 0.2 while the null is just the same graph, since the null does not take into account the threshold. The speaker1 function here is not really useful in showing anything too informative as no information about the world has been implemented yet.
Now looking at the final two layers, we can now see the information needed that the old model had already known due to the “prior” variable. The L1 layer finally reasons about the prevalences of a property in the world, while the S1 layer finally reasons about a single species of creature and a property.
///fold:
// discretized range between 0 - 1
var stateBins = [0.01,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1]
var thresholdBins = [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
var alpha_1 = 5
var round = function(x){
var rounded = Math.round(10*x)/10
return rounded == 0 ? 0.01 : rounded
}
var makeHistogram = function(prevalences){
return map(function(s){
return reduce(function(x, i){
var k = x == s ? 1 : 0
return i + k
}, 0.001, prevalences)
}, stateBins)
}
var altBeta = function(g, d){
var a = g * d;
var b = (1-g) * d;
return beta(a, b)
}
var fep = function() {
return {
kind: "fep",
wings: flip(0.5),
legs: flip(0.01),
claws: flip(0.01),
height: round(altBeta(0.5, 10))
}
}
var wug = function() {
return {
kind: "wug",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.3),
height: round(altBeta(0.2, 10))
}
}
var glippet = function() {
return {
kind: "glippet",
wings: flip(0.5),
legs: flip(0.99),
claws: flip(0.2),
height: round(altBeta(0.8, 10))
}
}
///
var theWorld = _.flatten([repeat(10, fep), repeat(10, wug), repeat(10, glippet)])
var allKinds = _.uniq(_.map(theWorld, "kind"))
var propertyDegrees = {
wings: "wings",
legs: "legs",
claws: "claws",
tall:" height"
}
var prevalence = function(world, kind, property){
var members = _.filter(world, {kind: kind})
return round(listMean(_.map(members, property)))
}
var prevalencePrior = function(property, world){
var p = map(function(k){return prevalence(world, k, property)}, allKinds)
return makeHistogram(p)
}
var propertyPrior = function(property){
var p = _.map(theWorld, property)
return makeHistogram(p)
}
var statePrior = function(probs){ return categorical(probs, stateBins) }
var thresholdPrior = function() { return uniformDraw(thresholdBins) }
var utterancePrior = function(property) {
var utterances = property == "height" ?
["tall", "null"] :
["generic", "null"]
return uniformDraw(utterances)
}
var meaning = function(utterance, state, threshold) {
return utterance == "generic" ? state > threshold :
utterance == "tall" ? state > threshold :
true
}
var literalListener = cache(function(utterance, threshold, stateProbs) {
Infer({method: "enumerate"}, function(){
var state = statePrior(stateProbs)
var m = meaning(utterance, state, threshold)
condition(m)
return state
})
})
var speaker1 = cache(function(state, threshold, stateProbs, property) {
Infer({method: "enumerate"}, function(){
var utterance = utterancePrior(property)
var L0 = literalListener(utterance, threshold, stateProbs)
factor(alpha_1 * L0.score(state))
return utterance
})
})
var pragmaticListener = cache(function(utterance, property, world) {
Infer({method: "enumerate"}, function(){
var stateProbs = property == "height" ?
propertyPrior(property) :
prevalencePrior(property, world)
var state = statePrior(stateProbs)
var threshold = thresholdPrior()
var S1 = speaker1(state, threshold, stateProbs, property)
observe(S1, utterance)
return state
})
})
var worldWithTallness = map(function(individual){
var tallDistribution = Infer({method: "enumerate"}, function(){
var utterance = utterancePrior("height")
factor(pragmaticListener(utterance, "height").score(individual.height))
return utterance
})
return _.extend(individual,
{tall: Math.exp(tallDistribution.score("tall"))})
}, theWorld)
var speaker2 = function(kind, predicate){
Infer({method: "enumerate"}, function(){
var property = predicate.split(' ')[1]
var degree = propertyDegrees[property]
var world = _.isNumber(theWorld[0][degree]) ?
worldWithTallness : theWorld
var prev = prevalence(world, kind, property)
var utterance = utterancePrior(property)
var L1 = pragmaticListener(utterance, property, world)
factor(2*L1.score(prev))
return utterance=="generic" ?
kind + "s " + predicate :
"don't think so"
})
}
viz.auto(pragmaticListener("generic", "legs", theWorld))
print("Are glippets tall?")
viz.auto(speaker2("glippet", "are tall"))
print("Do glippets have wings?")
viz.auto(speaker2("glippet", "have wings"))
worldWithTallness
The pragmatic listener layer is fairly simple. Running the code will show the exact same graphs seen at the beginning of this report, prevalence charts of a property over the world. This is just where the code gets the prevalences over the world and also a randomly generated threshold to put into the literal speaker. You can also see here the split between height and the rest of the traits in the stateProbs variable.
Finally, we have the pragmatic speaker. What the pragmatic speaker does is compare the prevalence of a trait among a single kind of creature in the world versus that trait’s prevalence among the entire world. It will then choose either to say the generic statement or the null statement. The most interesting part of the pragmatic speaker is we finally get to see why the split is implemented. If the generic is used with the word “tall”, then theWorld isn’t used. Instead, a new variable called worldWithTallness is used. Running worldWithTallness shows theWorld, but now the heights are rounded to a stateBin and there is a new category “tall” with a number slightly different from the height value. This “tall” value is the probability assigned by the code that that creature will be considered tall. This worldWithTallness essentially acts like another pragmatic speaker, but instead reasons about the tallness of each individual creature rather than just the three species. Running speaker2 we can see that the code runs pretty close to intuition. Traits with means close to one end of the spectrum get obvious results, while ones with means at 0.5 get more even results from the code. This makes sense as at 0.5 probability, chances of a species being above or below a threshold are the same, so the speaker will be more undecided over choosing the generic or the null statement. An interesting result can be seen when all the creatures have a trait. Changing legs for feps to 0.99, we can see the code is undecided whether any creature has legs. This is because when every creature has legs, the prevalence of legs will always be higher than any threshold. This makes the generic statement true no matter what. And when comparing it to the null statement, which is also true no matter what, it leaves both at 50%. This is because both statements now give the exact same amount of information.
Although very different from its base model, you can see how this model was built off of it. A lot of new code was added to take into account the gradable adjective. Additionally, a lot of new code was added to account for the fact that the world being reasoned over was entirely made up. But base things like the utterances and the meanings are essentially the same. The literal listeners are the same as well. Although the extended model has two more layers, these layers essentially act the same as the “prior” variable of the old model. Both models are still comparing the prevalence of a trait in the world versus the prevalence of a trait in a single kind of creature, and seeing if the difference is enough to warrant the generic statement.