[科普] 制作一个成语接龙AI
最近闲的蛋疼,搞了个成语接龙 AI,现在让我来手把手教你搭建一个成语接龙 AI。
我搞这个的目的大概是:
- 跟小冰玩
装逼虐菜
说实在的,这个AI很菜。
注:这里的成语接龙定义:
- 成语的首字要与上个成语的末字相同,并非拼音相同!例如
稀奇古怪-怪声怪气
就是一个合法的例子 - 当有一方接不上时,另一方胜利
- 由玩家指定开头成语
成语库
若要想制作一个好的 AI,那么第一步一定是拥有一个完整的成语库。
寻找成语库
目前使用的是在上的一个成语库:戳我
首先点击右边的Download
,等它加载完,然后一波Ctrl+A Ctrl+C
,复制到文本文档,起名为data.txt
。
加载完是指浏览器标签页图标不再是加载圈圈,顺便检查一下文本文档最后面是否是"做张做智", "abbreviation": "zzzz"}]
这几个字。如果是,恭喜你可以进入下一步了。
筛选
你会发现复制过来有很多定义啊,来源啊这些。而我们只需要一样东西——成语
手筛非常麻烦(如果您很闲),肯定是让程序帮你筛啦!
仔细观察可以发现:"word":
后面都是成语。
所以利用find
函数截取。
具体实现cut.cpp
:
#include<bits/stdc++.h>
using namespace std;
string s;
int main(){
ifstream I("data.txt");
ofstream O("data-fixed.txt");
while(getline(I,s)){//整行读入
if(s.find("\"word\": \"")==-1)continue;//如果没找到就结束开始下一次整行读入
else{//找到
int i=0;//初始化i
while(s.find("\"word\": \"",i)!=-1){//从i开始一直找"word": "
int v=s.find("\"",s.find("\"word\": \"",i)+10);//获取汉字右边的双引号
//if(v-s.find("\"word\": \"",i)-9==8)
//若只需要四字成语请将上面一行的注释去掉即可
O<<s.substr(s.find("\"word\": \"",i)+9,v-s.find("\"word\": \"",i)-9)<<" ";//截取汉字
i=v;//记得i要往前走,否则会导致死循环
}
}
}
return 0;
}
最后输出的应该在data-fixed.txt
里面。
现在我们就有一个成语库了!包含非四字成语共个成语
接龙
这个很简单,顺便使他会学习,当然这里没有用到什么神经网络之类的,只是用到了每个成语的权值,权值越大代表越难接上,每次接的时候就会选择权值最大的成语去接龙。
目前由四种情况(人类水平电脑水平):
- 电脑能接上人类的词:人类的词权值。
- 电脑不能接上人类的词:人类的词权值,因为当输入时如果人类输入的词在成语库找不到那么电脑不会判定这是一个成语,需要重新输入,所以人类的词必须都在成语库里存在,而电脑接词也是查找成语库,那么如果电脑接不上这个词,就说明这个词很强,所以权值,到以后如果电脑说这个词人类也会接不出来。
- 人类能接上电脑的词:电脑的词权值
- 人类接不上电脑的词(程序中输入
awsl
):电脑的词权值,为什么不,因为人类水平电脑水平,玩家接不上不代表电脑接不上。
权值表在learn.txt
中,在第一次玩之前必须运行rebuild.exe
来创建权值表。若需要清空权值表也可以运行rebuild.exe
。
代码rebuild.cpp
:
#include<bits/stdc++.h>
using namespace std;
int main(){
ofstream O("learn.txt");
for(int i=0;i<30000;i++)O<<"0 ";//清除或重建
}
代码成语接龙.cpp
:
#include<bits/stdc++.h>
using namespace std;
string s,now,last;
int da;
struct S{
string chinese;//成语
long long difficult;//权值
}Data[31000];
void read(){//读入成语和权值
string w;
ifstream I("data-fixed.txt");
ifstream I2("learn.txt");
while(I>>w){
Data[da].chinese=w;
I2>>Data[da].difficult;
da++;
}
I.close();
I2.close();
}
void save(){//保存权值
ofstream O("learn.txt");
for(int i=0;i<31000;i++)O<<Data[i].difficult<<" ";
}
int main(){
long long maxx=-pow(2,63),maxn=-1,si,ok;
/*maxx最大权重 maxn最大权重的成语的下标 si玩家输入的成语在成语库的下标 ok成语是否在成语库里*/
read();
cout<<"请输入开头成语\n";
while(1){
ok=0;
cout<<"你:";
cin>>s;
for(int i=0;i<31000;i++){//查找成语是否在成语库里
if(s==Data[i].chinese){//如果有
si=i;//存下标
ok=1;
}
}
cout<<"AI:";
if(s=="awsl"){//玩家接不上
cout<<"很遗憾,你失败了!";
Data[maxn].difficult++;
save();//学习
while(1);
}
if(!ok){//找不到这个成语
cout<<"输入错误!\n";
continue;
}
Data[maxn].difficult--;save();//合法,接上了电脑的成语,所以学习
maxx=-pow(2,63),maxn=-1;//初始化,准备找权值最大的成语
for(int i=0;i<31000;i++){//在成语库找
if(Data[i].chinese.substr(0,2)==s.substr(6,2)){//前提是要能接上
if(Data[i].difficult>maxx){//取最大值
maxx=Data[i].difficult;
maxn=i;
}
}
}
if(maxn==-1){//没有找到,玩家胜利
cout<<"恭喜你,你胜利了!\n";
Data[si].difficult+=100;
save();//学习
while(1);
}
Data[si].difficult--;save();//电脑能接上,学习
cout<<Data[maxn].chinese<<endl;//输出电脑的词语
}
}
经过多局电脑玩家对决后,会变得越来越强
自学成才
但是,玩家和电脑对决,电脑学习到的只是不多,而且效率慢,所以我们需要电脑自己学习(即自己和自己对决)
当时通过自我博弈,仅仅博弈了三天就超过了之前训练许久的版本,博弈天超过曾打败柯洁的,还发现了新的围棋策略,为围棋这项古老游戏带来了新的见解。
说了这么多牛逼的东西,说实在我这个AI进行自我对决还只是老办法赋权值而已
主要思路还是赋权值
- 如果接的上,那么权值
- 如果接不上,权值
训练方法:
- 输入要训练的组数
- 每组以一个随机成语开头,然后不断接下去,直到接不下去为止
- 学习(赋权值)这轮的结果
现在真的怀疑我这到底是不是人工智能了,我认为这只是一个分辨出哪些词容易接哪些词难接的机器
代码:
#include<bits/stdc++.h>
#define ll long long//鸡肋的宏定义
using namespace std;
int N;
struct S{
string chinese;
long long difficult;
}Data[31000];
int da;
int main(){
mt19937 gen(time(0));//随机数种子
uniform_int_distribution<>dis(0,30894);//范围是从0到30894
/*下面开始读取成语库及权值表*/
string w;
ifstream I("data-fixed.txt");
ifstream I2("learn.txt");
while(I>>w){
Data[da].chinese=w;//读入成语
I2>>Data[da].difficult;//权值
da++;
}
I.close();
I2.close();
/*以上是读取数据*/
cout<<"请输入练习次数(请勿在练习中中途关闭):";//如果在中途关闭就是前功尽弃
cin>>N;
for(ll i=1;i<=N;i++){//进行N次练习
int noi=dis(gen);//生成一个随机数
S now=Data[noi];//上一个词
//若要显示每局对弈过程,注释掉下面这行
//cout<<"\n/*第"<<i<<"轮*/\n"<<now.chinese<<endl;
while(1){//直到接不了
int maxx=-1,maxn=0;//权值最大及下标
S maxS;//权值最大的成语(这个很鸡肋)
for(int i=0;i<31000;i++){//寻找能接的成语
if(now.chinese.substr(now.chinese.size()-2,2)==Data[i].chinese.substr(0,2)){
if(maxx<Data[i].difficult){//找权值最大的
maxx=Data[i].difficult;
maxn=i;
maxS=Data[i];
}
}
}
if(maxx==-1){//居然接不上
Data[noi].difficult++;
break;
}
//若要显示每局对弈过程,注释掉下面这行
//cout<<Data[maxn].chinese<<endl;
Data[noi].difficult--;//接的上
noi=maxn;//现在接的成语变成上一个成语
now=maxS;//同上
}
}
ofstream O("learn.txt");
for(int i=0;i<31000;i++)O<<Data[i].difficult<<" ";//更新权值表
O.close();
//因为权值表在练习完后才更新,所以如果中途关闭就会前功尽弃
cout<<"\n\n\n\n练习完成";
}
当时,我只用这个训练了分钟,基本可以步内结束接龙(除非第一个成语就接不上)
但是,他还是有可能会被玩家打败(不要跟我说你开两个互打)因为它只会选择最难的成语去接龙,而不会考虑到用这个成语接龙后被反杀的概率。这也是他的一个致命的缺点。
慰问
话说要开学了,你们作业写完没啊我还没动过