黄金点第四次博客

1  上周回顾:

  在已有图形界面的基础上,我们通过前端后端的综合运用,改变了以前游戏数据的 “即用即存” 的形式,而是将玩家的历史选择、历史得分、历史黄金点都放入了本地数据库中,需要时直接调用,结合前端  html,js 等方法渲染可视化分析结果(折线图、箱型图、柱形图等)。但我们也发现了程序运行时还存在一点 bug ,在后续开发过程中我们会逐渐完善。

 

2  本周内容介绍

  本周针对以下几个方面进行优化、开发:

  • 将每次用户的输入改为 2 个数,用户的最终参与黄金点判定时的选择为两个输入数据的平均数;
  • 针对 bug 的优化
  • 继续针对需求建立预测模块(采用强化学习 Q-Learning 模型)并逐步实现

 

3  本周工作成果展示

 

  3.1 AI 预测部分

      考虑到程序运算执行的时间,在这里我们只设置 7 种状态,分别为第 4 局 ~ 第10局。由于在之前的博客中已经写到,如果都是 “聪明” 的玩家,那么第 10 局之后的黄金点将会在 0.5 ~ 3 之间波动,此时预测的效果将不显著,因此想要赢得最后累计最多的分数,在第 1 ~ 10 局将会是最好的机会。

      ① 设置一个一维数组全局变量 action, 每轮更新:

 

static double[] action = new double[]{
        前一轮的黄金点,
        前两轮黄金点的均值,
        前三轮黄金点的均值,
        前十轮中每隔 1 轮进行采样后的黄金点的均值,
        前十轮中每隔 2 轮进行采样后的黄金点的均值,
        前十轮中最大的三个的GN的均值,
        前十轮中最小的三个的GN的均值,
};    
action[0] = gold_point[i - 1];
action[1] = (gold_point[i - 1] + gold_point[i - 2]) / 2;
action[2] = (gold_point[i - 1] + gold_point[i - 2] + gold_point[i - 3]) / 3;
// 第四个动作:当前前10轮每隔1轮取黄金点均值
int temp = 0, temp_count = 0;
for(int j = i; j >= 0; j -= 2) {
    temp += a[j], temp_count++;
    if(temp_count > 10) break;
}
action[3] = temp / temp_count;
// 第五个动作:当前前10轮每隔2轮取黄金点均值
temp = 0, temp_count = 0;
for(int j = i; j >= 0; j -= 3) {
    temp += a[j], temp_count++;
    if(temp_count > 10) break;
}
action[4] = temp / temp_count; 
// 用一个临时数组temp_gold_point[]把当前前10局的黄金点存入
quickSort(temp_gold_point, 0, n - 1);
action[5] = (temp_gold_point[n - 1] + temp_gold_point[n - 2] + temp_gold_point[n - 3]) / 3;
action[6] = (temp_gold_point[0] + temp_gold_point[1] + temp_gold_point[2]) / 3;
// 其中的快速排序算法
public static void quickSort(double a[], int low, int high){
    if(low >= high)      return;
    double temp = a[low + high >> 1], l = low - 1, r = high + 1;
    while(l < r){
        do i++; while(a[l] < temp);
        do r--; while(a[r] > temp);
        if(l < r)    swap(a[l], a[r]);
    }
    quickSort(a, low, r), quickSort(a, r + 1, high);
}

 

      ②初始化 Q 表

double[][] Q = new double[][] {
{0, 0, 0, 0, 0, 0, 0},
{-1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1},
{-1, -1, -1, -1, -1, -1, -1}
};

 

     这里解释一下我们所理解的 state 状态:我们认为,每一个状态就是结合当前所在轮的所知的所有信息基础上的状态,假定从第 4 - 10 轮开始,共 7 轮,之后每七轮重新定义一次状态。当前的 “预测” 功能可以基于当前状态,在每一个状态都可以最期望的分数。比如从第4轮开始预测,那第一行的 7 个动作都为 0,代表此时七个动作都可以选择;第 2 ~ 7 行此时为 -1,代表当前还没有到后面几轮,所以 Q 表值为 -1,此时不能选择。后面会根据当前的轮数重新设置 Q 表。

 

  ③设置奖励

 

int reward = abs(action[i] - (gold_point[i - 1] + gold_point[i - 2]) / 2); // 奖励 = 每个动作的选择数所减去上两轮黄金点的平均值的绝对值

 

  ④完整模块代码

package homework;
import java.util.Arrays;

import static com.sun.tools.javac.jvm.ByteCodes.swap;
import static java.lang.Math.*;

public class QLearning {
    /*  每一局游戏都要更新,全局变量
    double[] possible_choice = new double[]{
        前一轮的黄金点,
        前两轮黄金点的均值,
        前三轮黄金点的均值,
        前十轮中每隔 1 轮进行采样后的黄金点的均值,
        前十轮中每隔 2 轮进行采样后的黄金点的均值,
        前十轮中最大的三个的GN的均值,
        前十轮中最小的三个的GN的均值,
    };
    */

    public static void main(String[] args) {
        double[][] Q = new double[][] {
                {0, 0, 0, 0, 0, 0, 0},
                {-1, -1, -1, -1, -1, -1, -1},
                {-1, -1, -1, -1, -1, -1, -1},
                {-1, -1, -1, -1, -1, -1, -1},
                {-1, -1, -1, -1, -1, -1, -1},
                {-1, -1, -1, -1, -1, -1, -1},
                {-1, -1, -1, -1, -1, -1, -1}
        };

        double epsilon = 0.8;
        double alpha = 0.2;
        double gamma = 0.8;

        int N = 10;
        int MAX_EPISODES = 400; // 一般都通过设置最大迭代次数来控制训练轮数

        action[0] = gold_point[i - 1];
        action[1] = (gold_point[i - 1] + gold_point[i - 2]) / 2;
        action[2] = (gold_point[i - 1] + gold_point[i - 2] + gold_point[i - 3]) / 3;
        // 第四个动作:当前前10轮每隔1轮取黄金点均值
        int temp = 0, temp_count = 0;
        for(int j = i; j >= 0; j -= 2) {
            temp += a[j], temp_count++;
            if(temp_count > 10) break;
        }
        action[3] = temp / temp_count;
        // 第五个动作:当前前10轮每隔2轮取黄金点均值
        temp = 0, temp_count = 0;
        for(int j = i; j >= 0; j -= 3) {
            temp += a[j], temp_count++;
            if(temp_count > 10) break;
        }
        action[4] = temp / temp_count;
        // 用一个临时数组temp_gold_point[]把当前前10局的黄金点存入
        quickSort(temp_gold_point, 0, n - 1);
        action[5] = (temp_gold_point[n - 1] + temp_gold_point[n - 2] + temp_gold_point[n - 3]) / 3;
        action[6] = (temp_gold_point[0] + temp_gold_point[1] + temp_gold_point[2]) / 3;



        for(int episode = 0; episode < MAX_EPISODES; ++episode) {
            System.out.println("第"+episode+"轮训练...");
            int index = 0;
            while(index <= 9) { // 到达目标状态,结束循环,进行下一轮训练
                int next;
                if(Math.random() < epsilon) next = max(Q[index]); // 通过 Q 表选择动作
                else next = randomNext(Q[index]); // 随机选择可行动作
                /*if(index == 0)  next = Q[index][0];
                if(index == 1){
                    if(Math.random() < epsilon) next = max(Q[index][0], Q[index][1]);
                    else    next = randomNext(Q[index]);
                }*/
                // 每一轮更新所有action对应的值


                int reward = abs(action[i] - pre_round_gp); // 奖励 = 每个动作所减去
                Q[index][next] = (1-alpha)*Q[index][next] + alpha*(reward+gamma*maxNextQ(Q[next]));
                index = next; // 更新状态
            }
        }
        System.out.println(Arrays.deepToString(Q));
    }

    private static int randomNext(double[] is) { // 蓄水池抽样,等概率选择流式数据
        int next = 0, n = 1;
        for(int i = 0; i < is.length; ++i) {
            if(is[i] >= 0 && Math.random() < 1.0/n++) next = i;
        }
        return next;
    }

    private static int max(double[] is) {
        int max = 0;
        for(int i = 1; i < is.length; ++i) {
            if(is[i] > is[max]) max = i;
        }
        return max;
    }

    private static double maxNextQ(double[] is) {
        double max = is[0];
        for(int i = 1; i < is.length; ++i) {
            if(is[i] > max) max = is[i];
        }
        return max;
    }

    // 快速排序
    public static void quickSort(double a[], int low, int high){
        if(low >= high) return;
        double temp = a[(low + high) >> 1], l = low - 1, r = high + 1;
        while(l < r){
            do l++; while(a[l] < temp);
            do r--; while(a[r] > temp);
            if(l < r)   swap(a[l], a[r]);
        }
        quickSort(a, low, r), quickSort(a, r + 1, high);
    }
}

 

  ⑤选择操作

  在训练出较为稳定的代码后,我们选择当前所在行的七个值中最大的两个数作为输入。看一个例子(这里我们还没有加入到整个游戏中,还在改进模型中:)

  假设有一个最短路问题:

 

  那么,奖励 reward 和 Q 表可以设置成:

  

 

 

  训练过程中:

  

 

  那么,最后的路径选择就是 A -> B(4.56) -> D(3.20),问题解决。我们设想的黄金点思路也是这样。不过我们是 java 语言开发,而且经过不少的资料查阅,用神经网络训练出的 Q-Learning(DNQ)效果并不会显著强于普通 Q-Learning 方法,所以这里为了简便便暂时采用该方法。

 

 

   3.2 增加成立两个输入

 

 

 

 

 

 

 

 

 

 

 

  3.3 语音播报

  我们在展示每轮成绩以及最后的总成绩时,加入了语音播报功能。语音播报用到了activeX库,先将每轮成绩的展示以字符串的形式存入.txt文件,再由我们设定的方法进行读取并且朗读,可以自由设定播报音量以及速度。

  首先是java中对每一轮成绩展示以字符串形式并存入文本文件的方法:

  

// 写入文件的方法(每次得到了整体的字符串就写一个文件)
public static boolean to_write_scores(int turnNum, String str, boolean append){
    // turnNum是所展示语音播报的轮数(如第2轮)
    // str拿到的存储句子, 如 String str = "张三的成绩是:5,\n李四的成绩是:10,\n王五的成绩是8。"
    // 不同人的成绩间一定要用逗号结尾,因为语音播报句号之间是不会停顿的。
    // append为true代表可追加写入,否则不能追加。
    boolean falg = false;
    // 写入文件举例
    String file_path = "D:\\软工课设\\";
    file_path = file_path + turnNum + ".txt";  // 结果为: D:\\软工课设\\1.txt
    
    File file = new File(file_path);
    try{
        if(!file.exists())    file.createNewFile();  // 不存在则创建该文件
        FileWriter fw = new FileWriter(file_path, append);  // append表示可以追加
        fw.write(str);
        fw.close();
    }catch(IOException e){
        e.printStackTrace();
    }
    flag = true;
    return flag;
}

 

  然后是语音播报的代码:

  

// 每次在输入数字后弹出本轮成绩页面的时候,就用这个try, file_path是存文本文件的路径。例如第一局:D:\\1.txt, 第二局:D:\\2.txt。
try{
    voice(file_path);
}catch(IOException e){

}
    // 音响方法
    public static void voice(String file_path) throws IOException {
        // 拿到音响
        ActiveXComponent sap = new ActiveXComponent("sapi.SpVoice");
        // 找到本地要朗读的文件
        try {
            File srcFile = new File(file_path);
            // 获取文本文档的内容
            FileReader file = new FileReader(srcFile);

            // 从缓存区拿到数据
            BufferedReader bf = new BufferedReader(file);

            // 拿到缓冲区的数据
            String content = "", result = ""; //= bf.readLine();
            while((content = bf.readLine()) != null){
                result += content;
            }
            // 测试一下 有没有拿到数据
            System.out.println(result);

            // 调节语速 音量大小
            sap.setProperty("Volume", new Variant(100));
            sap.setProperty("Rate", new Variant(1));

            Dispatch xj = sap.getObject();
            // 执行朗读 没有读完就继续读
            while (result != null) {
                Dispatch.call(xj, "Speak", new Variant(result));
            }

            xj.safeRelease();
            bf.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            sap.safeRelease();
        }

    }

 

  经过实际测试后,程序能够很好地将每轮成绩存入指定文件夹,并且调用该方法进行语音播报。

 

 

4  总结

   下一步我们将继续优化预测模型,并且开始准备收尾工作:测试并修改 bug,美化软件界面,使可视化结果更加清晰直观,并根据黄金点历史生成游戏的文字总结。

posted @ 2020-11-30 10:17  软工课设第29组  阅读(82)  评论(0)    收藏  举报