罗兰谈MATLAB的艺术

将想法转化为MATLAB

请注意

罗兰谈MATLAB的艺术已存档,不会更新。

构建Wordle求解器

今天的客座博主是Adam Filion, MathWorks的高级数据科学家。Adam在MathWorks从事过许多数据科学领域的工作,包括帮助客户理解和实施数据科学技术,管理和确定我们的开发工作的优先级,构建 Coursera类 以及领导内部数据科学项目。
我妻子最近给我介绍了一款令人上瘾的益智游戏 Wordle .在游戏中,你要做一系列的猜测来找出当天的秘密答案。答案总是一个五个字母的英语单词,你有六次机会猜出正确答案。每次猜测后,游戏会告诉你距离答案有多近。
图1:Wordle如何提供反馈的示例。
我一直是一个数字迷,所以在每天被Wordle卡住后,我决定用MATLAB看看我是否能做出更好的猜测。在这篇文章中,我将介绍一种为Wordle游戏生成建议的简单方法,在不知道Wordle的官方单词列表的情况下,可以在94%的时间内在六次猜测中得到正确答案。这个谜题的例子来自2022年1月12日。
wordle0crop.png
图2:空白字谜。还剩六次猜测!

生成词汇

如果我们要玩Wordle游戏,我们需要一个由五个字母组成的英语单词表。游戏的粉丝们已经抓取了Wordle的源代码,并分享了2315个 神秘的文字 和12972年 可推测的单词 (由于 FiveThirtyEight !)稍后我们将回到神秘单词来检查我们的准确性,但在求解器中使用它感觉有点像作弊,所以让我们假装不知道Wordle使用的列表。没有一个完整的英语单词列表,所以让我们为寻找英语单词列表的程序员选择一个通用的来源,一个unix系统在/usr/share/dict/words下提供的列表如果你用的是Windows系统,你可以在诸如 github .我们可以很容易地读取这样的文本文件,直接从网络上进行文本处理使用 readline .这个列表包括首字母缩写词和专有名词,我们可以通过忽略以大写字母开头的条目来删除它们。虽然它没有包含完整的英语语言,但它给了我们4581个五个字母的单词列表。我们可能会错过Wordle神秘列表中的一些单词,但它仍然足够接近,可以提供有用的建议。
%将单词列表读入字符串数组
R =阅读线(“https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words”);
使用附录中的自定义函数替换变音符号
Rs = removediacritics(r);
只保留以小写字母开头的条目
rs = rs(startsWith(rs,characterListPattern(“一个”“z”)));
去掉带有撇号的条目,比如缩写
Rs = Rs(~包含(Rs,“”));
Wordle使用全部大写字母
Rs =上(Rs);
%得到唯一的五个字母单词的列表
Word5 = unique(rs(strlength(rs)==5))
经常= 4581×1的字符串
“算盘”
“吓”
“在船尾”
“自卑”
使局促不安”
“减弱”
“abb”
“艾比”
“方丈”
“横”

找出最常用的字母

现在我们有了一个由五个字母组成的单词列表,但是如何先猜出哪个单词呢?我们的第一个猜测是盲目的,没有最终答案的线索。由于Wordle以字母的形式给出反馈,一个简单的方法就是选择最常用字母的单词。
让我们先把每个单词分成字母,然后看看字母的整体直方图。我们可以看到,有些字母的使用频率比其他字母高得多。
把单词分成单独的字母
字母= split(word5,"");
%这也会创建前导和尾空字符串,删除它们
Letters = Letters (:,2:end-1);
%查看字母使用次数
H =直方图(分类(字母(:)));
ylabel (五个字母单词的使用次数
让我们把它放在一个表中,用于创建单词分数。
lt = table(h.Categories',h.Values',“VariableNames”, (“字母”“分数”])
lt = 26日×2表
分数
1 “一个” 1841
2 “B” 556
3. “C” 790
4 ' D ' 989
5 “E” 2449
6 “F” 445
7 ‘G’ 543
8 “H” 646
9 “我” 1258
10 “J” 69
11 “K” 477
12 “L” 1301
13 “米” 639
14 “N” 1019

为每个单词创建一个分数

我们现在可以根据单词所使用字母的受欢迎程度来创建一个单词评分。首先将每个字母替换为其单独的分数,然后将字母分数相加以创建单词分数。
%为每个字母,将其替换为相应的字母分数
Letters_score = arrayfun(@(x) lt.score(lt.letters==x),letters);
%将字母分数相加以创建单词分数
Word_score = sum(letters_score,2);
找到得分最高的单词和对应的单词
[top_score,top_idx] = sort(word_score,1,“下”);
Word_scores = table(word5(top_idx),top_scores,“VariableNames”, (“单词”“分数”]);

选一个词,第一次猜

而我不是 博弈理论家 很明显,我们的第一步应该是使用五个不同的流行字母,以最大限度地提高我们获得有用反馈的机会,从而缩小搜索范围。在删除重复字母的单词后,我们看到rose是第一个单词的首选,所以让我们尝试一下。
找出每个单词中有多少个唯一的字母
word_scores。num_letters= arrayfun(@(x) numel(unique(char(x))),word_scores.words);
只保留没有重复字母的单词
Top_words_norep = word_scores(word_scores.num_letters==5,:);
头(top_words_norep)
ans = 8×3表
单词 分数 num_letters
1 “起来” 9792 5
2 “伯爵” 9628 5
3. “激光” 9628 5
4 “实数” 9628 5
5 “沉香” 9609 5
6 “ASTER” 9589 5
7 “利率” 9589 5
8 “盯着” 9589 5

考虑Wordle的反馈

wordle1crop.png
图3:第一次猜测后的Wordle谜题。
提交第一个猜测后,我们可以看到A、R和O这三个字母出现在最终答案中,但位置不同。字母S和E根本不在这个单词里。这种反馈消除了大量可能的单词。
现在我们有了这个反馈,我们该如何整合它呢?这是一个相当简单的问题,表示收到的反馈,然后循环这些结果,并删除不再是可能的解决方案的单词。我们这样做 filter_words 属性中的Helper函数 附录 .通过它,我们传递单词表和它们的分数,我们到目前为止猜到的单词,以及这些猜测的编码结果。结果被编码为一个矩阵,每个猜想有一行,每个字母有一列。如果字母是错误的,它被编码为0,如果字母在答案中但不在那个位置,它被编码为1,如果它在正确的位置,它被编码为2。

再猜一下

我们开了个好头!将此信息传递给 filter_words ,我们已经将候选词从4581个缩小到35个。
%我们之前的猜测
猜测=“起来”;
%编码反馈
结果= [1,1,1,0,0];
%筛选剩下的候选人
Top_words_filtered = filter_words(word_scores,猜测,结果)
top_words_filtered = 35×3表
单词 分数 num_letters
1 “塔罗牌” 7314 4
2 “比” 7310 5
3. “广播” 7037 5
4 “卡罗” 6881 5
5 “珊瑚” 6881 5
6 “极地” 6845 5
7 “氡” 6798 5
8 “摩尔” 6730 5
9 “道德” 6730 5
10 “皇家” 6726 5
11 “劳动” 6647 5
12 “缓慢的” 6634 5
13 “庄园” 6448 5
14 “罗马” 6448 5
我们可以看到下一个单词的最高分是TAROT,但在这一点上,我们可能还是使用有五个唯一字母的单词更好,所以让我们试试RATIO。
wordle2crop.png
图4:第二次猜测后的Wordle谜题。

第三次猜

现在“A”在正确的位置,我们已经删除了两个更流行的字母。加上这些信息后,只剩下10个候选人,CAROL是下一个首选。
%我们之前的猜测
猜测= [“起来”;“比”];
%编码反馈
结果= [1,1,1,0,0;
1、2 0,0,1];
%筛选到剩余的候选,不要求唯一的字母
Top_words_filtered = filter_words(word_scores,猜测,结果)
top_words_filtered = 10×3表
单词 分数 num_letters
1 “卡罗” 6881 5
2 “劳动” 6647 5
3. “庄园” 6448 5
4 “男爵” 6365 5
5 “英勇” 6350 5
6 “反弹” 6219 5
7 “市长” 6064 5
8 “蒸汽” 5803 5
9 “主要的” 5498 5
10 “帮忙” 5494 5
wordle3crop.png
图5:进行第三次猜测后的Wordle谜题。

第四次猜

现在我们在正确的位置上有了两个字母,通过消去,我们知道R一定在最后。加上这些信息,我们看到只剩下5个选项,其中3个以M开头,所以我们选择MANOR。
%我们之前的猜测
猜测= [“起来”;“比”;“卡罗”];
%编码反馈
结果= [1,1,1,0,0;
1、2 0 0 1;
0 2 1 2 0);
%筛选剩下的候选人
Top_words_filtered = filter_words(word_scores,猜测,结果)
top_words_filtered = 5×3表
单词 分数 num_letters
1 “庄园” 6448 5
2 “市长” 6064 5
3. “蒸汽” 5803 5
4 “主要的” 5498 5
5 “帮忙” 5494 5
wordle4crop.png
图6:进行第四次猜测后的Wordle谜题。

第五次猜

现在我们有两个选择可以猜两次。
%我们之前的猜测
猜测= [“起来”;“比”;“卡罗”;“庄园”];
%编码反馈
结果= [1,1,1,0,0;
1、2 0 0 1;
0 2 1 2 0;
0 2 0 2 2);
%筛选剩下的候选人
Top_words_filtered = filter_words(word_scores,猜测,结果)
top_words_filtered = 2×3表
单词 分数 num_letters
1 “蒸汽” 5803 5
2 “帮忙” 5494 5
wordle5crop.png
图7:第五次猜测后的Wordle谜题。

第六次也是最后一次猜测

还有一个选项是我们最后的猜测。祈祷!
%我们之前的猜测
猜测= [“起来”;“比”;“卡罗”;“庄园”;“蒸汽”];
%编码反馈
结果= [1,1,1,0,0;
1、2 0 0 1;
0 2 1 2 0;
0 2 0 2 2;
1、2 0 2 2);
%筛选剩下的候选人
Top_words_filtered = filter_words(word_scores,猜测,结果)
top_words_filtered = 1×3表
单词 分数 num_letters
1 “帮忙” 5494 5
wordle6crop.png
图8:第六次猜测后的Wordle谜题。成功!
这个Wordle字谜的答案是正确的,但我们一共猜了六次,所以结果很接近。总的来说,这种方法的效果如何?

玩Wordle的随机游戏

如果MATLAB知道答案是什么,我们就可以自动化玩Wordle游戏的过程,看看我们的算法是否能正确地猜出来。我们将从创建另一个helper函数开始 wordle_feedback 附录 在正确答案的基础上对每个猜测的反馈进行编码。
现在我们可以自动使用我们的 play_wordle helper函数。它接受由五个字母组成的单词表和它们的分数,以及一个作为答案的单词。它将返回我们试图猜测的答案,无论我们是否在游戏中获胜,以及在整个过程中所做的猜测。当我们玩游戏时,我们会要求我们的前三次猜测不能使用重复的字母(假设这样的单词仍然是可能的),但从第四次猜测开始,字母可以重复。
因为我们知道哪里有一个列表 神秘的文字 ,我们可以把它从谷歌表直接读入MATLAB。
mystery_id =“1-M0RIVVZqbeh0mZacdAsJyBrLuEmhKUhNaVAI-7pr2Y”;%取自上面链接的表单的URL
Mystery_url = sprintf(“https://docs.google.com/spreadsheets/d/%s/gviz/tq?tqx=out: csv”, mystery_id);
Mystery_words = readlines(mystery_url);
%有一组额外的双引号,所以让我们把它们去掉
Mystery_words = erase(”“”“);
%我们也用大写
Mystery_words = upper(Mystery_words);
我们的算法只能从我们给它的词汇中猜测单词。我们的词汇中大约有4%的神秘词汇是缺失的,所以即使我们使用我们知道的词汇完美地玩游戏,我们能期待的最佳胜率也只有96%。
Num_missing = sum(~ismember(mystery_words,word_scores.words))
Num_missing = 94
Perc_missing = num_missing / nummel (mystery_words) * 100
Perc_missing = 4.0605
现在我们有了谜题列表,我们可以玩一个游戏,随机猜一个答案。
Answer_idx = randi(nummel (mystery_words));
[answer,win,played_words] = play_wordle(word_scores,mystery_words(answer_idx))
回答=“喷”
赢= 1
played_words = 1×6弦
"兴起""巢穴"" sprat ""喷雾" "" ""

玩所有可能的游戏Wordle

我们可以通过循环运行来测试整个2315个神秘单词的算法。我们可以看到,这个简单的方法在六次猜测中得到正确答案的概率是94%,这非常接近96%的最大值!如果我们真的赢了,我们通常猜四次就赢了。
Num_games = numel(mystery_words);
Wins = nan(num_games,1);
猜测=字符串(num_games,6);
答案=字符串(num_games,1);
Ii = 1:num_games%为我们词汇中的每个单词
玩字谜游戏,字谜就是我们要猜的答案。
[答案(ii),赢(ii),猜测(ii,:)] = play_wordle(word_scores,mystery_words(ii));
结束
流(“这个策略的结果是赢~%0.1f%%的时间。”总和(赢得)/元素个数(赢得)* 100)
这一策略的结果是94.2%的胜利。
num_attempts = sum(attempts (wins==1,:)~=""2);
直方图(num_guesses“归一化”“概率”
包含(赢取Wordle时猜对的次数
ylabel (“胜利的分数”
这是我们没有得到正确答案的游戏过程。
Missed_answers = answers(wins==0);
[answer,win,played_words] = play_wordle(word_scores,missed_answers(1))
回答=“”,
Win = 0
played_words = 1×6弦
“崛起”“外星人" "" "" "" ""
错过答案似乎有两种模式。
  1. 如上所述,大约4%的答案不在我们的词汇中,比如RAMEN和ZESTY。你可以知道什么时候会发生这种情况,因为我们没有使用所有的猜测就输掉了游戏,因为我们用完了所有允许的单词。
  2. 一些答案结合了一个常见的字母模式和一个很少使用的字母,我们没有足够的猜测来缩小范围。例如,当答案是FIXER时,我们的词汇中有39个单词是在第二个位置使用“I”,在最后使用“ER”。在所有这些字母中,FIXER的得分最低,因为F和X都在最不常用的七个字母中。我们的六种猜测分别是rose, l, DINER, RIPER, HIKER, FIBER,我们在得到FIXER之前就已经猜完了。

需要改善的地方

还有什么其他的事情可以让我们的胜率达到100%?以下是一些建议:
  • 我们确定了上述答案缺失的两种主要模式。显然,第一种模式可以通过将Wordle的神秘词汇添加到我们的词汇表中来解决。
  • 第二种模式的解决方案就不那么清晰了。我们目前的单词评分方法的一个缺点是分数是静态的,所以如果像FIXER这样的单词以较低的分数开始,它将永远不会改变。我们可以通过从分数计算中删除不符合条件的单词和/或解决的字母位置来更新我们的分数,从而获得更多正确的猜测。
  • 我们还可以尝试通过寻找常见的模式来改进我们的评分方法字格.最常见的n-gram是用来寻找常见的单词组合,但它也可以用来寻找常见的字母组合。我们可以提取顶部字母n-grams并将其纳入我们的得分中,因为猜测一个带有常见n字母的单词会让我们对许多相似的单词得到反馈。
  • 我们已经要求我们的前三个猜测使用不重复的字母,这是我通过反复试验选择的策略,可能不是最优的。我们还可以在前几次猜测中使用不重叠的单词,即使我们已经猜对了一些字母。这要求我们在前两次猜测中总是使用10个不同的字母,即使我们必须做出我们知道不可能正确的猜测。我试过普遍使用这种方法,实际上它会略微降低总体胜率,但可能有更聪明的方法在情境中使用它。
你还有其他更好的策略吗?让我们知道 在评论中

附录

函数Word_scores_filtered = filter_words(word_scores,words_guessed,results)
% remove words_guess,因为这些不是答案
Word_scores_filtered = word_scores;
Word_scores_filtered (matches(Word_scores_filtered .words,words_guessed),:) = [];
%过滤到在正确位置有正确字母的单词(绿色字母)
[rlp,clp] = find(results==2);
如果~ isempty (rlp)
Ii = 1:数字(rlp)
字母=提取(words_guess (rlp(ii)),clp(ii));
只保留在正确位置有正确字母的单词。
Word_scores_filtered = Word_scores_filtered (extract(Word_scores_filtered .words,clp(ii))==letter,:);
结束
结束
%过滤到在其他位置也包含正确字母的单词(黄色字母)
[rl,cl] = find(results==1);
如果~ isempty (rl)
Jj = 1:数字(rl)
字母= extract(words_guess (rl(jj)),cl(jj));
删除相同位置含有字母的单词
Word_scores_filtered (extract(Word_scores_filtered .words,cl(jj))==letter,:) = [];
删除不包含字母的单词
Word_scores_filtered (~contains(Word_scores_filtered .words,letter),:) = [];
结束
结束
%筛选到不包含不正确字母(灰色字母)的单词
[ri,ci] = find(results==0);
如果~ isempty (ri)
Kk = 1:数值(ri)
Letter = extract(words_guess (ri(kk)),ci(kk));
删除包含不正确字母的单词
Word_scores_filtered (contains(Word_scores_filtered .words,letter),:) = [];
结束
结束
结束% filter_words
函数结果= wordle_feedback(答案,猜测)
结果= nan(1,5);
Ii = 1:5%为我们猜的每个字母
字母=提取(猜测,ii);%提取该字母
如果提取(答案,ii) ==字母
如果答案中的字母在相同的位置
结果(ii) = 2;
elseif包含(答案,信)
如果答案中那个字母在另一个位置
结果(ii) = 1;
其他的
%如果答案不包含该字母
结果(ii) = 0;
结束
结束
结束% wordle_feedback
函数[word_to_guess,win,guess] = play_wordle(word_scores, word_to_guess)
Top_words = sortrows(word_scores,2,“下”);确保分数被排序
猜测=字符串(1,6);
结果= nan(6,5);
max_attempts = 6;
Ii = 1: max_attempts%
%使用逐步不同的策略过滤我们的总词汇表以候选猜测
如果Ii == 1对于我们的第一个猜测,过滤到有五个唯一字母的单词,并获得最高分
Top_words_filtered = top_words(top_words.num_letters==5,:);
elseifIi <= 3%如果我们在进行第二次或第三次猜测
过滤掉不符合条件的单词,如果可能的话,要求5个唯一的字母。
Min_uniq = 5;
Top_words_filtered = filter_words(top_words(top_words.num_letters==min_uniq,:), (1:ii-1),results(1:ii-1,:));
%如果过滤到五个唯一的字母会删除所有单词,则允许更多重复的字母
Height (top_words_filtered) == 0 && min_uniq > min(word_scores.num_letters)
Min_uniq = Min_uniq - 1;
Top_words_filtered = filter_words(top_words(top_words.num_letters==min_uniq,:), (1:ii-1),results(1:ii-1,:));
结束
其他的在第三次猜测后,不限制重复字母
Top_words_filtered = filter_words(top_words,猜测(1:ii-1),结果(1:ii-1,:));
结束
%生成我们的猜测(如果我们有的话)
如果Height (top_words_filtered) == 0%如果我们的词汇表中没有合格的单词
Win = 0;我们不知道这个词,我们输了。
返回别再猜了
其他的%否则生成一个新的猜测并得到结果
猜测(1,ii) = top_words_filtered.words(1);
Results (ii,:) = wordle_feedback(word_to_guess, (1,ii));
结束
评估我们是否赢了,输了,或者应该继续玩下去。
如果猜测(1,ii) == word_to_guess%如果我们的猜测正确的话
Win = 1;%设置胜利标志
返回别再猜了
elseifIi == max_attempts%如果我们已经用了所有的猜测,而且都是错的
Win = 0;%我们输了,循环将会结束
其他的否则我们还在玩
结束
结束
结束% play_wordle
引用:吉姆·古道尔,2020年。Stack Overflow,可从:https://stackoverflow.com/a/60181033获得
函数[clean_s] = removediacritics(s)
从文本中删除变音符号。
此函数从字符串中删除许多常见的变音符号,例如
á -重音
à -严肃的口音
â -回旋重音
% ü -变音,或trema或变音
ñ -波浪号
ç -雪松
å -环,或bolle
% ø -斜线,或实线,或贞线
%大写
S = regexp (S,‘(?:| | | | |一个)的“一个”);
S = regexp (S,“(?:Æ)”“AE”);
S = regexp (S,“(?:ß)”“党卫军”);
S = regexp (S,“(?:C)”“C”);
S = regexp (S,“(?:Ð)”' D ');
S = regexp (S,”(?:E E E | | | E)”“E”);
S = regexp (S,”(?:我| | |我)”“我”);
S = regexp (S,“(?:N)”“N”);
S = regexp (S,”(?:O O O O O | | | | |Ø)”“O”);
S = regexp (S,“(?:œ)”“OE”);
S = regexp (S,”(?:U | | | U)”“U”);
S = regexp (S,”(?:Y |ÿ)'“Y”);
%小写
S = regexp (S,‘(?:| | | | |一个)的“一个”);
S = regexp (S,“(?:æ)”“ae”);
S = regexp (S,“(?:c)”“c”);
S = regexp (S,“(?:ð)”' d ');
S = regexp (S,”(?:e e e | | | e)”“e”);
S = regexp (S,”(?:我| | |我)”“我”);
S = regexp (S,“(?:n)”“n”);
S = regexp (S,”(?:o o o o o | | | | |ø)”“o”);
S = regexp (S,“(?:œ)”“oe”);
S = regexp (S,”(?:u | | | u)”“u”);
S = regexp (S,(: | y)的“y”);
返回已清理的字符串
Clean_s = s;
结束

コメント

コメントを残すには,ここをクリックしてMathWorksアカウントにサインインするか新しいMathWorksアカウントを作成します。

Baidu
map