Loren谈MATLAB的艺术

将想法转化为MATLAB

请注意

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

构建Wordle求解器

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

生成我们的词汇

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

找出最常用的字母

现在我们有了五个字母的单词列表,但是如何选择先猜哪个单词呢?我们的第一个猜测是盲目的,对最终答案没有任何线索。由于Wordle通过字母给出反馈,一个简单的方法是选择有最常用字母的单词。
让我们首先将每个单词分成字母,并查看字母的整体直方图。我们可以看到,有些字母的使用频率远远高于其他字母。
把我们的单词分成单独的字母
字母= split(分裂)"");
%这也会创建前导和尾随空白字符串,删除它们
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),字母);
%和字母分数以创建单词分数
Word_score = sum(letters_score,2);
找出高分和对应的单词。
[top_scores,top_idx] = sort(word_score,1,)“下”);
Word_scores = table(word5(top_idx),top_scores,“VariableNames”,[“单词”“分数”]);

选择一个单词并进行第一次猜测

虽然我没有 博弈理论家 显然,我们的第一步应该是使用5个不同的、受欢迎的字母,以最大限度地增加我们获得有用反馈的机会,从而缩小搜索范围。在删除重复字母的单词后,我们看到rising是第一个单词的首选,所以让我们尝试一下。
找出每个单词中有多少个唯一的字母
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个。
%我们之前的猜测
猜测=“起来”;
%编码反馈
Results = [1,1,1,0,0];
%筛选剩下的候选人
Top_words_filtered = filter_words(word_scores,guess,results)
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是下一个首选。
%我们之前的猜测
猜测= [“起来”;“比”];
%编码反馈
Results = [1,1,1,0,0;
1、2 0,0,1];
%筛选剩下的候选人,没有要求唯一的字母
Top_words_filtered = filter_words(word_scores,guess,results)
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。
%我们之前的猜测
猜测= [“起来”;“比”;“卡罗”];
%编码反馈
Results = [1,1,1,0,0;
1、2 0 0 1;
0 2 1 2 0);
%筛选剩下的候选人
Top_words_filtered = filter_words(word_scores,guess,results)
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谜题。

进行第五次猜测

现在我们有两个选择,有两次猜测。
%我们之前的猜测
猜测= [“起来”;“比”;“卡罗”;“庄园”];
%编码反馈
Results = [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,guess,results)
top_words_filtered = 2×3表
单词 分数 num_letters
1 “蒸汽” 5803 5
2 “帮忙” 5494 5
wordle5crop.png
图7:第五次猜测后的Wordle谜题。

进行第六次也是最后一次猜测

我们最后的猜测只剩一个选项了。祈祷!
%我们之前的猜测
猜测= [“起来”;“比”;“卡罗”;“庄园”;“蒸汽”];
%编码反馈
Results = [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,guess,results)
top_words_filtered = 1×3表
单词 分数 num_letters
1 “帮忙” 5494 5
wordle6crop.png
图8:第六次猜测后的Wordle谜题。成功!
所以,Wordle谜题的答案是正确的,但是六次都猜错了,所以我们猜得很接近。总的来说,这种方法的效果如何?

随机玩一个Wordle游戏

如果MATLAB知道答案是什么,我们就可以自动执行Wordle游戏的过程,看看我们的算法是否能正确猜出答案。我们将从创建另一个辅助函数开始 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,”“”“);
%也要用大写
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(numel(mystery_words));
[answer,win,played_words] = play_wordle(word_scores,mystery_words(answer_idx))
回答=“喷”
赢= 1
played_words = 1×6弦
“崛起”“巢穴”“小飞虱”“喷雾" "" ""

玩所有可能的Wordle游戏

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

需要改进的地方

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

附录

函数Word_scores_filtered = filter_words(word_scores,words_guessed,results)
因为那不是答案
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)
字母= extract(words_guessed(rlp(ii)),clp(ii));
只保留在正确位置有正确字母的单词。
Word_scores_filtered (extract(word_scores_filtering .words,clp(ii))==letter,:);
结束
结束
%筛选在其他位置也包含正确字母的单词(黄色字母)
[rl,cl] = find(results==1);
如果~ isempty (rl)
Jj = 1:数值(rl)
字母= extract(words_guessed(rl(jj)),cl(jj));
删除相同位置有字母的单词。
Word_scores_filtered (extract(word_scores_filtering .words,cl(jj))==letter,:) = [];
删除不包含字母的单词。
Word_scores_filtered (~contains(word_scores_filters .words,letter),:) = [];
结束
结束
%筛选也不包含错误字母(灰色字母)的单词
[ri,ci] = find(results==0);
如果~ isempty (ri)
Kk = 1:numel(ri)
字母= extract(words_guessed(ri(kk)),ci(kk));
删除包含不正确字母的单词。
Word_scores_filtered (contains(word_scores_filters .words,letter),:) = [];
结束
结束
结束% filter_words
函数结果= wordle_feedback(答案,猜测)
Results = 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_misses = 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,:), suspects (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,:), suspects (1:ii-1),results(1:ii-1,:));
结束
其他的第三次猜完后,不要对重复的字母设置限制。
Top_words_filtered = filter_words(top_words,guess (1:ii-1),results(1:ii-1,:));
结束
产生我们的猜测(如果我们有的话)
如果Height (top_words_filtered) == 0%如果我们的词汇表中没有合适的词
Win = 0;我们不知道这个单词,我们输了。
返回别再猜了
其他的%否则生成一个新的猜测并得到结果
猜测(1,ii) = top_words_filtered.words(1);
Results (ii,:) = wordle_feedback(word_to_guess, suspects (1,ii));
结束
评估我们是否赢了,输了,或者应该继续玩下去。
如果guess (1,ii) == word_to_guess如果我们的猜测是正确的
Win = 1;%设置胜利标志
返回别再猜了
elseifIi == max_misses . i%,如果我们已经用了所有的猜测,它们都是错误的
Win = 0;%我们失去了,循环将结束
其他的否则我们还在玩
结束
结束
结束% play_wordle
引用:吉姆·古道尔,2020。Stack Overflow,可在:https://stackoverflow.com/a/60181033获得
函数[clean_s] = removediacritics(s)
从文本中移除变音符。
此函数从字符串中删除许多常见的变音符,例如
% á -尖锐的重音
à -严肃的口音
% â -旋音重音
% ü -透析,或颤音,或变音
% ñ -波浪线
% ç -雪松
% å -环,或bolle
% ø -斜杠,或solidus,或virgule
%大写
S = regexprep(S,‘(?:| | | | |一个)的“一个”);
S = regexprep(S,“(?:Æ)”“AE”);
S = regexprep(S,“(?:ß)”“党卫军”);
S = regexprep(S,“(?:C)”“C”);
S = regexprep(S,“(?:Ð)”' D ');
S = regexprep(S,”(?:E E E | | | E)”“E”);
S = regexprep(S,”(?:我| | |我)”“我”);
S = regexprep(S,“(?:N)”“N”);
S = regexprep(S,”(?:O O O O O | | | | |Ø)”“O”);
S = regexprep(S,“(?:œ)”“OE”);
S = regexprep(S,”(?:U | | | U)”“U”);
S = regexprep(S,”(?:Y |ÿ)'“Y”);
%小写
S = regexprep(S,‘(?:| | | | |一个)的“一个”);
S = regexprep(S,“(?:æ)”“ae”);
S = regexprep(S,“(?:c)”“c”);
S = regexprep(S,“(?:ð)”' d ');
S = regexprep(S,”(?:e e e | | | e)”“e”);
S = regexprep(S,”(?:我| | |我)”“我”);
S = regexprep(S,“(?:n)”“n”);
S = regexprep(S,”(?:o o o o o | | | | |ø)”“o”);
S = regexprep(S,“(?:œ)”“oe”);
S = regexprep(S,”(?:u | | | u)”“u”);
S = regexprep(S,(: | y)的“y”);
返回清理后的字符串
Clean_s = s;
结束
|
Baidu
map