<jupyter_start><jupyter_text>Simulating Language, Lab 5, Rational Speech Act model This lab introduces the Rational Speech Act model, which is a way of doing Bayesian modelling of pragmatic inference and rational behaviour during communication. The RSA model is potentially very general, but here we are going to use it to model pragmatic word use, and see how pragmatic speakers and listeners can go beyond literal word use to facilitate communication. Our model of word meaning builds on the model we used in Lab 2; now we are going to move beyond single words to (very small!) lexicons, and model inference during communication rather than learning.  Representing meanings, words, and lexicons As in Lab 2, we are going to assume that the meaning of a word is the set of things that that word can be used to refer to, and we are going to model "things that words refer to" (referents) as numbers. So a word meaning is just a set of numbers, representing the set of things that the word can be used to refer to. As in Lab 2, we will represent word meanings as sets, e.g. like this:```pythona_word_meaning = {0,1,2}```We also need a representation of words, which we will just represent as strings (in python, you can represent these using single- or double-quoted strings of characters, e.g.:```pythona_word = 'word'another_word = "floccinaucinihilipilification"```Now we have our representation of meanings and words, we can model a lexical entry, which is a pairing of a meaning and its associated word. We are going to represent these as *tuples* in python, which for our purposes work a bit like lists but use round brackets instead of square brackets. So this is how we would represent a lexical entry for a word which refers to referents 0, 1 and 2:```pythona_lexical_entry = ({0,1,2},'dax')```You can access the elements in a tuple by index in the same way you would pull items out from a list, e.g. `a_lexical_entry[0]` will give you the meaning from `a_lexical_entry`, `a_lexical_entry[1]` will pull out the word.Check you can access the meaning and word from `a_lexical_entry` in this way, then create your own lexical entry for a word, *fep*, which can be used to refer to referents 6, 7, 8, and 9. Now we have a way of representing meanings, words, and single lexical entries, we can represent a lexicon, which is just going to be a list of lexical entries. For instance, a lexicon with three very specific words which refer to single referents, would be represented like this:```pythonlab_tutor_lexicon = [({0},'tamar'),({1},'henry'),({2},'andres')]``` Create `lab_tutor_lexicon` by copying that line of code into a cell. How would you access the first lexical entry from this lexicon? How would you access the meaning of the 2nd entry? How would you access the word from the 3rd entry? How would you count how many lexical entries the lexicon contains?  The codeNow we've introduced the notation we are going to be using to represent lexicons we can get on and build the model. As usual we start by loading the libraries we need and various libraries for manipulating log probabilities.<jupyter_code>from scipy.special import logsumexp
from math import log, log1p, exp

%matplotlib inline
import matplotlib.pyplot as plt
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('svg', 'pdf')<jupyter_output><empty_output><jupyter_text>Functions for dealing with log probabilitiesAs per Lab 4, we are going to deal with log-probabilities rather than raw probabilities, so we need a couple of functions for dealing with those. I am also providing a little utility function that will convert log probabilities to normal probabiltities, which you might find helpful when it comes to looking at the model results.<jupyter_code>def log_subtract(x,y):
    return x + log1p(-exp(y - x))

def normalize_logprobs(logprobs):
    logtotal = logsumexp(logprobs) #calculates the summed log probabilities
    normedlogs = []
    for logp in logprobs:
        normedlogs.append(logp - logtotal) #normalise - subtracting in the log domain
                                           #is equivalent to dividing in the normal domain
    return normedlogs

#A little utility function that converts a list of log probabilities to normal probabilities 
#Note there are other ways this could be written, e.g. using list comprehensions
def logprobs_to_probs(logprobs):
    probs = []
    for logp in logprobs:
        probs.append(exp(logp))
    return probs<jupyter_output><empty_output><jupyter_text>Possible words and possible referentsWe'll start out with a very simple two-word lexicon, where one of the words has a specific meaning and the other has a more general meaning that subsumes the meaning of the first word - that's `small_lexicon` below. In order to make the code work smoothly we also have to specify what words our model has to consider, and what possible referents there are that words can refer to. These are just lists. **Important point to note for when you are creating your own lexicons**: if your lexicon refers to a word that isn't in `possible_words` or referent that isn't in `possible_referents`, or there are possible words or referents that aren't covered in your lexicon, your code will break!<jupyter_code>small_lexicon = [({0}, 'word1'), ({0, 1, 2}, 'word2')]
possible_words = ['word1','word2']
possible_referents = [0,1,2]<jupyter_output><empty_output><jupyter_text>A literal speaker and a literal listener We'll start by writing functions for our literal speaker (*s0* in Frank & Goodman's terms) and a literal listener (*l0*). The literal speaker and literal listener just use words as dictated by their lexicon, without doing any fancy pragmatics. For our model of a literal speaker, we are going to provide a target referent (the object the literal speaker wants to refer to) and the speaker's lexicon, and the literal speaker will return a (log) probability distribution over possible words. The values in this probability distribution give the (log) probability of the literal speaker producing each of those words in order to label the target referent. The literal speaker behaves as you would expect - they are equally likely to use any of the words that include the target referent in their meaning. We also include a small probability of the literal speaker making a mistake and using the wrong word to refer to an object - this is specified by `error_probability`, it ensures we never deal with zero probabilities (which don't play nice in the log domain) but you can think of it as the probability of lexical access errors by the speaker. It can be very small if you like.<jupyter_code>error_probability = 0.001 #you can make this as small as you like, as long as it's not 0

def s0(target_referent,lexicon):
    word_probs = []
    for candidate_word in possible_words: #consider each possible word
        for meaning,word in lexicon: #work through the lexicon
            if word==candidate_word: #if the word for this lexical entry is the one we want
                if target_referent in meaning: #can this word be used to refer to this referent?
                    word_probs.append(log(1-error_probability)) #if yes, it is likely to be used                
                else:
                    word_probs.append(log(error_probability)) #otherwise it is unlikely to be used 
    return normalize_logprobs(word_probs) #normalise these so they are true log probabilities<jupyter_output><empty_output><jupyter_text>Check that the literal speaker behaves as expected using `small_lexicon`. How likely are they to produce word1 to convey target referent 0? How likely are they to use word2? You can find this out by calling `s0(0,small_lexicon)` or, if you'd rather see probabilities than log probabilities, `logprobs_to_probs(s0(0,lexicon))`. If you want to see the words printed out alongside their probabilities of being produced you can do something like:```pythons0_probabilities = logprobs_to_probs(s0(0,small_lexicon))for i in range(len(possible_words)):    print(possible_words[i],s0_probabilities[i])``` Check the literal speaker's behaviour for referents 0, 1 and 2, and make sure you understand why the literal speaker behaves the way they do. We can do the same for a literal listener, who receives a word and, based on their lexicon, returns a (log) probability distribution over referents, indicating how likely they think it is that the word they heard refers to each possible referent. The literal hearer just assumes that all referents included in the word's meaning are equally likely, and any others are very unlikely (but can occur with some small probability given by `error_probability`).<jupyter_code>def l0(received_word,lexicon):
    referent_probs = []
    for candidate_referent in possible_referents: #consider each possible referent in turn
        for meaning,word in lexicon: #consider each lexical entry
            if word==received_word: #if this lexical entry matches the received word
                if candidate_referent in meaning: #if the candidate referent is included in the word's meaning 
                    referent_probs.append(log(1-error_probability)) #then it's likely to be this referent they arer talking about
                else:
                    referent_probs.append(log(error_probability)) #otherwise it's quite unlikely
    return normalize_logprobs(referent_probs) #normalise at the end<jupyter_output><empty_output><jupyter_text>How likely does a literal listener who uses `small_lexicon` think each referent is after hearing word1? Again, you can see this probability distribution by running something like the following:```pythonl0_probabilities = logprobs_to_probs(l0('word1',small_lexicon))for i in range(len(possible_referents)):    print('referent',possible_referents[i],l0_probabilities[i])```Also check out the literal listener estimates given word2, and make sure you understand them.  A pragmatic speaker and a pragmatic listener Now we can move on to define our pragmatic speaker (*s1*), who reasons about a literal listener when selecting their word to convey an intended referent, and a pragmatic listener, (*l1*), who reasons about a pragmatic speaker when interpreting words. Because the l1 model builds on the s1 model, we will build the s1 model first.Just like the literal speaker, our s1 speaker has a target referent they'd like to convey, and a lexicon that they are going to use. But unlike s0, who just looks up the target referent in their lexicon in a fairly dumb way, s1 is going to be pragmatic: they will reason about how a literal listener might interpret the word they produce, and weight their production in favour of words that are more likely to be interpreted as conveying their intended referent. The crucial line in the s1 definition below is `l0_prob_of_target_referent = l0(candidate_word,lexicon)[target_referent]`: if I use this particular `candidate_word`, what's the probability that the literal listener will correctly conclude I was talking about `target_referent`? For the pragmatic speaker, this *probability of correct interpretation by a literal listener* is what determines the probability of producing a particular word - the pragmatic speaker is more likely to use words that have a better chance of being interpreted correctly by the literal listener.<jupyter_code>def s1(target_referent,lexicon):
    word_probs = []
    for candidate_word in possible_words: #consider each candidate word
        l0_prob_of_target_referent = l0(candidate_word,lexicon)[target_referent] #how likely is the literal listener  
                                                            #to choose target_referent if they hear this word?
        word_probs.append(l0_prob_of_target_referent)#note that down
    return normalize_logprobs(word_probs) #and normalise at the end<jupyter_output><empty_output><jupyter_text>Use `s1(0,small_lexicon)` to figure out how likely the pragmatic speaker is to produce each word in order to convey referent 0 to a literal listener; e.g. you could do:```pythons1_probabilities = logprobs_to_probs(s1(0,small_lexicon))for i in range(len(possible_words)):    print(possible_words[i],s1_probabilities[i])```Are these the same or different than the literal speaker probabilities for conveying the same target referent? Can you figure out why the pragmatic speaker behaves like this? It might help to look back at the literal listener's behaviour. Similarly, what does the pragmatic speaker do to convey referents 1 and 2? Why? Now we have the pragmatic speaker we can define our pragmatic listener, l1, who reasons about the behaviour of a pragmatic speaker to calculate a probability distribution over referents given a received word. The key line in the definition of our l1 listener is `s1_prob_of_word_given_meaning = s1(candidate_referent,lexicon)[word_index]`: how likely is a pragmatic speaker to use this word to convey some `candidate_referent`? That determines the pragmatic listener's interpretation of the word - they reason about how a pragmatic speaker would be likely to behave, and interpret the word they received accordingly.<jupyter_code>def l1(received_word,lexicon):
    word_index = possible_words.index(received_word) #.index tells me the index of received_word in possible_words
    referent_probs = []
    for candidate_referent in possible_referents: #consider each possible referent
        s1_prob_of_word_given_meaning = s1(candidate_referent,lexicon)[word_index] #how likely is the pragmatic speaker
                                                                #to use this word to convey this candidate referent?
        referent_probs.append(s1_prob_of_word_given_meaning) #note that down
    return normalize_logprobs(referent_probs) #and normalise at the end<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.05210971991025548, 'fraction_numerical': 0.00730983570963306, 'mean_word_length': 4.547169811320755, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 7, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '477876', 'license': 'MIT', 'n_tokens_mistral': 3682, 'n_tokens_neox': 3476, 'n_words': 1853, 'path': 'lab5.ipynb', 'repo_name': 'ponrawee/simlang2022', 'size': '13817'}