<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
 <channel>
  <title>nth_element&#39;s blog</title>
  <link>https://blog.nth.ink/</link>
  <description>nth_element&#39;s blog</description>
  <item>
   <title>一次C++内存错误的解决思路</title>
   <link>https://blog.nth.ink/posts/cpp-memory-bug</link>
   <guid>https://blog.nth.ink/posts/cpp-memory-bug</guid>
   <pubDate>Thu, 21 Mar 2024 15:51:00 +0000</pubDate>
   <description><![CDATA[<p>前情提要：昨天srt同学给我看了一段代码，出现了一个很奇怪的问题，就是cout出现在程序main函数的最前面，却没有输出：</p>




<p class="syn_hl_click"><span class="ycl">#include&lt;bits/stdc++.h&gt;
</span><br/>using namespace std<span class="operator">;</span>
<br/>const int MOD<span class="operator">=</span>2009<span class="operator">;</span>
<br/>const int N<span class="operator">=</span>105<span class="operator">;</span>
<br/>struct Matrix
<br/><span class="operator">{</span>
<br/>    int a<span class="operator">[</span>N<span class="operator">]</span><span class="operator">[</span>N<span class="operator">]</span><span class="operator">;</span>
<br/>    int n<span class="operator">,</span>m<span class="operator">;</span>
<br/>    Matrix<span class="operator">(</span>int n<span class="operator">=</span>0<span class="operator">,</span>int m<span class="operator">=</span>0<span class="operator">)</span><span class="operator">:</span>n<span class="operator">(</span>n<span class="operator">)</span><span class="operator">,</span>m<span class="operator">(</span>m<span class="operator">)</span><span class="operator">{</span>memset<span class="operator">(</span>a<span class="operator">,</span>0<span class="operator">,</span>sizeof<span class="operator">(</span>a<span class="operator">)</span><span class="operator">)</span><span class="operator">;</span><span class="operator">}</span>
<br/>    void init<span class="operator">(</span>int _n<span class="operator">=</span>0<span class="operator">,</span>int _m<span class="operator">=</span>0<span class="operator">)</span><span class="operator">{</span>n<span class="operator">=</span>_n<span class="operator">;</span>m<span class="operator">=</span>_m<span class="operator">;</span>memset<span class="operator">(</span>a<span class="operator">,</span>0<span class="operator">,</span>sizeof<span class="operator">(</span>a<span class="operator">)</span><span class="operator">)</span><span class="operator">;</span><span class="operator">}</span>
<br/>    
<br/>    Matrix operator<span class="operator">*</span><span class="operator">(</span>Matrix b<span class="operator">)</span>
<br/>    <span class="operator">{</span>
<br/>        <span class="zs">/*
<br/>        省略
<br/>        */</span>
<br/>        return ans<span class="operator">;</span>
<br/>    <span class="operator">}</span>
<br/>    
<br/><span class="operator">}</span><span class="operator">;</span>
<br/>
<br/>Matrix qpow<span class="operator">(</span>Matrix a<span class="operator">,</span>int n<span class="operator">)</span>
<br/><span class="operator">{</span>
<br/>    <span class="zs">/*
<br/>    省略
<br/>    */</span>
<br/><span class="operator">}</span>
<br/>int n<span class="operator">,</span>t<span class="operator">,</span>cnt<span class="operator">;</span>
<br/>int main<span class="operator">(</span><span class="operator">)</span>
<br/><span class="operator">{</span>
<br/>    cout <span class="operator">&lt;</span><span class="operator">&lt;</span> <span class="zfc">&#34;hello world\n&#34;</span> <span class="operator">&lt;</span><span class="operator">&lt;</span> endl<span class="operator">;</span>
<br/>    cin<span class="operator">&gt;</span><span class="operator">&gt;</span>n<span class="operator">&gt;</span><span class="operator">&gt;</span>t<span class="operator">;</span> 
<br/>    Matrix A<span class="operator">,</span>B<span class="operator">;</span>
<br/>    <span class="zs">/*
<br/>    省略
<br/>    */</span>
<br/>    return 0<span class="operator">;</span>
<br/><span class="operator">}</span></p>



<p>经过研究与和hyq同学的讨论，我最终找到了这个问题的根源。</p>



<p>打开gdb，在CPU Dump中，指出了问题所在的位置：</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/cpp-memory-bug/1711007118-image-19e5abca.png"><img loading="lazy" decoding="async" loading="lazy" width="1099" height="825" src="https://blog.nth.ink/media/posts/cpp-memory-bug/1711007118-image-19e5abca.png" alt="" class="wp-image-5584" style="aspect-ratio:1.332121212121212;width:529px;height:auto"/></a></figure>



<p>是“__chkstk_ms()”中的问题。不过似乎这不能说明什么问题，我们就继续查找思路。</p>



<p>首先，我们打开任务管理器，发现过程是RAM占用不断增加，直至报Segmentation Fault，被迫中止。因此确定了问题在于内存爆了导致的段错误。</p>



<p>我昨晚和hyq讨论了很久是否是在循环过程中因为大量创建的临时变量未被析构而导致占用了大量内存，但是事实证明并非如此。其实就是开了1005*1005的int数组就大到足以报错了。如这段我简化过后的程序：</p>



<p class="syn_hl_click"><span class="ycl">#include &lt;bits/stdc++.h&gt;
</span><br/>using namespace std<span class="operator">;</span>
<br/>
<br/>int main<span class="operator">(</span><span class="operator">)</span> <span class="operator">{</span>
<br/>    system<span class="operator">(</span><span class="zfc">&#34;pause&#34;</span><span class="operator">)</span><span class="operator">;</span>
<br/>    int x<span class="operator">[</span>1000<span class="operator">]</span><span class="operator">[</span>1000<span class="operator">]</span><span class="operator">;</span>
<br/>    memset<span class="operator">(</span>x<span class="operator">,</span>0<span class="operator">,</span>sizeof<span class="operator">(</span>x<span class="operator">)</span><span class="operator">)</span><span class="operator">;</span>
<br/>    return 0<span class="operator">;</span>
<br/><span class="operator">}</span></p>



<p>在电脑上会重现与昨日一样的错误。因此这个“哪里出了问题”这个问题已经被解决了。</p>



<p>下一个问题，为什么昨天即使把 Matrix A,B; 这个定义语句放到了cout的后面，cout也无法输出。</p>



<p>我通过Compiler Explorer（https://godbolt.org/，超级好用）查看了编译我写的测试程序得到的汇编代码，摘录如下：</p>



<p class="syn_hl_click">main<span class="operator">:</span>
<br/>        sub     rsp<span class="operator">,</span> 4000008
<br/>        mov     edx<span class="operator">,</span> 4000000
<br/>        mov     esi<span class="operator">,</span> 0
<br/>        mov     rdi<span class="operator">,</span> rsp
<br/>        call    memset
<br/>        mov     eax<span class="operator">,</span> 0
<br/>        add     rsp<span class="operator">,</span> 4000008
<br/>        ret</p>



<p>可以看到，程序的入口点处（即“main”的后一行）就出现了对rsp寄存器值减去4000008的操作，而4000000就是1000*1000*4(sizeof int)，所以这些变量会占用的栈空间在程序开头即被预留，注意rsp即为栈顶指针，减去后它小于了栈基指针的值，便出现了段错误。问题至此就解释清楚了。</p>



<p>注意在我为了复现而编写的代码中保留了 memset(x,0,sizeof(x)) 一句，因为只有通过memset初始化了后C++才会预留这一部分空间。对于下列代码</p>



<p class="syn_hl_click">int main<span class="operator">(</span><span class="operator">)</span> <span class="operator">{</span>
<br/>    int x<span class="operator">[</span>1000<span class="operator">]</span><span class="operator">[</span>1000<span class="operator">]</span><span class="operator">;</span>
<br/>    return 0<span class="operator">;</span>
<br/><span class="operator">}</span></p>



<p>编译得到的汇编是</p>



<p class="syn_hl_click">main<span class="operator">:</span>
<br/>        mov     eax<span class="operator">,</span> 0
<br/>        ret</p>



<p>并没有预留空间的操作。</p>]]></description>
  </item>
  <item>
   <title>HYWiki危机：互信体系延伸的界限，理想与现实的妥协</title>
   <link>https://blog.nth.ink/posts/hywiki-crisis</link>
   <guid>https://blog.nth.ink/posts/hywiki-crisis</guid>
   <pubDate>Wed, 08 Feb 2023 13:09:00 +0000</pubDate>
   <description><![CDATA[<p>HYWiki今日卷入建站一个月以来最大的争议之中。争议的详细过程暂且略过。</p>



<p>本文认为，此次危机反映出了Hywiki在扩大过程中面临的，必须正视的问题与挑战。</p>



<p>第一个问题，在于“华育维基是否在纵容和鼓励‘校园欺凌’”。</p>



<p>HYwiki的建立者，23564，也是本站的站长。本文认为，在他建站的初衷中，绝对不会有一丝这样的念头，即他的网站将是用于欺凌的工具，更不用说鼓励或者纵容这样的行为。</p>



<p>然而，事实证明，Hywiki已经造成了这样的印象，给很多同学带来了困扰。对于这一问题，本文认为：争议的双方从自身的立场出发，都没有错。这一争议真正反映的问题，是以“校群体系”为基础的互信体系在过度延伸中出现的崩溃。</p>



<p>Hywiki一开始便是依托校群体系（主要是QQ体系）发展，主要用户与校群人物高度重合，且记录事件也包含大量校群史事件。此时，wiki甚至可以被称为校群史wiki。在这一过程中，几乎没有什么争议，少量的争议都可以通过快速的沟通解决。</p>



<p>按理说，校群也是一个矛盾迭生的地方，为什么没有产生影响恶劣的事件呢？本文认为，这反映了校群实际上是有基础的互信的，并不是完全的黑暗森林，而这一点常常被人忽略。校群矛盾有斗争性，但也有同一性，即群友都是认可校群的价值，华育的价值，认同华育校友和校群群友的身份。这就使得校群的利益各方类似于同一民主国家内部的各个政党，虽然互相斗争，但是出发点都是爱国的，都是试图促进国家发展进步的，因而事实上存在广泛的共同利益。</p>



<p>在校群，这一互信体系甚至延伸到如22873ebm这样的前群友。所谓“倒猫战争”，实际上也轻易可以被形容为为“校园欺凌”事件。如果有人说ebm的反击力量巨大，且其本身行为也有欺凌性，那实际上此次事件中的矛盾双方其实也大致实力相当，并不是单方面的欺凌。当然这并不是本文讨论的重点。重点是，22873这个曾经被描述为“激进，难以沟通”的，并曾在华育语这样琐碎的项目中为了几个“创建不当”的“单词”（Erpaimaus表示“傲慢”）而掀起编辑战的人物，看到了对自己的略显偏颇的描述，没有提请删除，也没有攻击群友，而是积极参与到了wiki的编撰过程中，主要使用补充的方式，非常客观的纠正了有关自己的叙述，重新获得了同侪们的尊重。这就体现了873表面上与群友曾有巨大矛盾，但事实上仍然服从于校群的互信秩序之下。</p>



<p>互信秩序的另一个体现就在于校群人之间的任意互写。十分讽刺的是，这次矛盾因为建设性的“书写”而起，但实际上有不少校群和维基最热忱的用户，其个人介绍空空如也（如20851，21361）。甚至有人几乎苦苦哀求，希望其他人能够编辑自己的页面。显然，这些人心中是有一个隐性前提的：他们愿意接受任何这样的编辑，即便是出自平时与他“有矛盾”的人之手。这也是体现校群的矛盾其实是在基础互信之上的，实际上存在基础互信。</p>



<p>有了这样的观念，就不难理解此次矛盾的一个关键点何在了。本文重申，以23564为代表的维基管理层都是高度的理想主义者。实际上，他们想当然的认为全华育都有共同体意识，华育学生的同窗情谊跨越小团体，跨越性别，班级和年级，跨越毕业生与在校生，甚至这一纽带还要超过校群中的，矛盾迭生之下的所谓“基础互信”。因此，他们在激进的推广hywiki，扩张其用户面的时候，也是基于这样片面的信念去做，在没有为可能的此类事件做出预案和提前准备的方面体现了惊人的短视。</p>



<p>在这样的基础上，也不难理解23564等人对于提请删除的过激反应。删除的动机，在他们理解中，很容易与对他们假想的那种互信的一种颠覆和破坏相混淆。实际上，希望删除的人并不是不认同华育，也不是不认同维基，更不是对维基管理层有意见，只是客观存在的，超越校群互信体系的矛盾延伸到此领域而已。这就像上述民主国家内部各政党的一种体系，突然要延伸以包含不同主权国家，乃至互相敌对的主权国家，所必然产生的一种错乱和崩溃。这样再来看2月6日的公开信，其比较过激的语言则也是这一效应的体现。</p>



<p>第二个问题，则是维基原教旨主义在华育现实下所要做出妥协的问题。这一话题与上一相关，但也不完全一致。</p>



<p>以20851为首的维基原教旨主义者怀有这样一种信念：维基，写人物本就不需要和当事人确认。你做了的事情，就不是隐私。任何文本，只要是客观事实，就可以写上维基。任何内容，只要对还原世界的全貌有利，就不应当被删除。维基文字的权益属于编辑者，不属于其他任何人。</p>



<p>可以发现，这也是一种高度的理想主义。它的背景其实是以整个互联网为场域的正统wikipedia。但是，在只以华育中学为场域的hywiki，这样一种信念也势必做出妥协。这样一种信念能够建立，其实是因为在全社会，全人类的视角下，个体哪怕再有权有势，也终究是渺小的，是可以屈从于一个近乎全知的观察者之下的。然而在华育视角下，每一个同学都很重要，即便大部分都认同，仅有一人反对某种内容，也很难说维护这一内容有很大正当性。而且，这样的行为也会被理解成所谓“校园欺凌”。</p>



<p>华育维基因而和正统维基就产生了重大的不同：华育维基其实终究有一个隐性使命，即维护华育学生的团结，而正统维基则没有维护全人类团结的义务。而华育维基为了实现这一隐性使命，在执行上也需要妥协而偏离正统维基的路线。</p>



<p>这两个问题，一个是由小推大（以小的校群体系推断全华育的情况）产生的错乱，一个是以大推小（以全人类的情况推断小小华育的情况）产生的错乱。希望本文的观点能够给HYWiki的管理者一点参考，把我们华育学生共同的遗产建设的越来越好。</p>]]></description>
  </item>
  <item>
   <title>科忒尼察的MCTS：皇后棋、JNP、AlphaZero</title>
   <link>https://blog.nth.ink/posts/mcts-of-kotenyca</link>
   <guid>https://blog.nth.ink/posts/mcts-of-kotenyca</guid>
   <pubDate>Fri, 20 Jan 2023 20:42:00 +0000</pubDate>
   <description><![CDATA[<p>注：科忒尼察（Kotenyca）为虚构地名，是“佘山国”的一座经济发达的城市。在这里引用的灵感来自于乌克兰战争的新闻：乌西城市利沃夫的科技企业为前线捐助3D打印机，并为用神经网络分析军事情报提供指导。“利沃夫”（波兰语叫Lwow，Eu4里就这么标了）这个古老的名字听起来那么遥远、神秘，以至于有些童话色彩——然而那里也有科技企业，也有程序员，也在利用着深度学习技术。这就产生了一种奇妙的陌生感和反差感。</p>


<div class="wp-block-post-author"><div class="wp-block-post-author__content"><p class="wp-block-post-author__name">作者：旅菇佘侨159</p></div></div>


<p>最近，HYwiki方兴未艾。我作为wiki的管理员之一，也对该地流连忘返。在同学们各自叙述的各种璀璨的班级文化中，我注意到了228人喜欢玩的小游戏：<a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9oeXdpa2kueHl6L3dpa2kvJUU3JTlBJTg3JUU1JTkwJThFJUU2JUEzJThC" target="_blank">皇后棋</a>。这款游戏抽象而简洁，但富有趣味性，局面变化多样，似乎是相当可玩的。于是在1月13日，我决定使用Python来编写此游戏，而我更终极的目标，则是为其开发一款AI，这样能够让我们以前所未有的深度来探索这个游戏。</p>



<p>皇后棋的核心在于国际象棋里的“皇后”这个棋子，它能够在方形晶格中沿横、竖、以及对角线方向移动。皇后棋分为单后棋和多后棋，其中多后棋中最常见的形式是双后棋。</p>



<p>单后棋使用一个8*8的棋盘，两名玩家依次移动皇后。移动时，皇后经过的格子，以及最后停留的格子便会被“挖去”，以后就不能停留在上面或者经过。最后，哪名玩家不能够合法移动皇后，那么这方就赢了。多后棋的规则类似，不过几个人分别操纵自己的后，获胜的是最后一个还能合法移动皇后的人。而且，多后棋中不能移动到能被其他后攻击的位置。</p>



<p>我先编写的是单后棋，因为它相对简单。我采用了Numpy库作为重要的辅助，因此不是纯粹的原生Python。我喜欢利用jupyter notebook来边实验边编写，减少最后debug时的难度。</p>



<p>首先必然需要一个矩阵来表示棋盘，然后一个坐标来表示皇后的位置。其实皇后棋的状态最基本的也就这两样东西。</p>



<p>皇后棋编写的难点一个在于判定可以移动到的位置，一个在于在移动后进行挖去。对于第一个问题，其实有很多写法，比如在得到输入之后可以首先考虑该点是不是在皇后的八个正方向上并没有出图，从皇后当前坐标开始，模拟皇后的移动，看看中途是否会遇到被挖去的格子。不过，我最后的目的是要编写一个ai程序，而根据我对于深度学习算法的经验，这样的一个即时且高度迭代式的解决方案似乎很不方便（这种直觉的来源后面会解释）。</p>



<p>也是根据这样的经验，我最终采取了“Masking”法，即对于每一步都计算一个和棋盘同形的矩阵，包含了每一个点的可走与否的信息。而计算的方法则与上述的过程差不多。这里我最初的方案是将八个方向的“单位向量”（就是位移-1，0或者1）先储存在一个数组里，然后对于每一个方向循环移动，直至出图。我不得不写了八个循环，然后一开始要么会出图，要么会在离边界一格的地方停下，而且每次改都得改八处，实在是太烦！因此很快我就采用了一种更优化的方法。</p>



<pre class="wp-block-code"><code class="prettyprint">#最初的写法
def action_masking(board,coords):
    n_board = board
    mask = np.zeros(board.shape)
    x = coords[0]
    y = coords[1]
    
    state = 0
    n_x = x
    n_y = y
    while n_x&lt;7 and state!=-1:
        n_x+=1
        state = n_board[n_x,n_y]
        if state != -1:
            mask[n_x,n_y]=np.inf
        
    state = 0
    n_x = x
    n_y = y 
    while n_y&lt;7 and state!=-1:
        n_y+=1
        state = n_board[n_x,n_y]
        if state != -1:
            mask[n_x,n_y]=np.inf
      

…………节约篇幅
    
     
    state = 0
    n_x = x 
    n_y = y 
    while n_x&lt;7 and n_y&gt;0 and state!=-1:
        n_x+=1
        n_y-=1
        state = n_board[n_x,n_y]
        if state != -1:
            mask[n_x,n_y]=np.inf
      
    state = 0
    n_x = x 
    n_y = y 
    while n_x&gt;0 and n_x&lt;7 and n_y&gt;0 and n_y&lt;7 and state!=-1:
        n_x-=1
        n_y+=1
        state = n_board[n_x,n_y]
        if state != -1:
            mask[n_x,n_y]=np.inf
        
    return mask</code></pre>



<p>优化的写法是将边界判定语句先包装成一个函数cond，然后把迭代更新的部分也统一包装，这个函数作用很像探索，姑且叫explore。</p>



<pre class="wp-block-code"><code class="prettyprint">def explore(board, x, y, d_x, d_y, mask):
    state = 0
    n_x = x
    n_y = y
    xlb = n_x &gt; 0
    xub = n_x &lt; (board.shape[0] - 1)
    ylb = n_y &gt; 0
    yub = n_y &lt; (board.shape[1] - 1)

    while cond(xlb, xub, ylb, yub, d_x, d_y) and state != -1:
        n_x += d_x
        n_y += d_y
        state = board[n_x, n_y]
        if state != -1:
            mask[n_x, n_y] = 1
        xlb = n_x &gt; 0
        xub = n_x &lt; (board.shape[0] - 1)
        ylb = n_y &gt; 0
        yub = n_y &lt; (board.shape[1] - 1)

    return mask


def cond(a, b, c, d, d_x, d_y):
    return (d_x &gt; -1 or a) and (d_x &lt; 1 or b) and (d_y &gt; -1 or c) and (d_y &lt; 1 or d)


def action_masking2(board, coords):
    n_board = board
    mask = np.zeros(board.shape)
    x = coords[0]
    y = coords[1]

    movements = [[1, 0], [0, 1], [-1, 0], [0, -1], [1, 1], [-1, -1], [1, -1], [-1, 1]]
    for i in movements:
        mask = explore(n_board, x, y, i[0], i[1], mask)

    return mask</code></pre>



<p>好多了！下一个问题就是更改棋盘的问题。同样，我也采取了一种“计算矩阵”的思路，试图把每一次挖去的部分表示成一个增量（或者减量）矩阵，姑且称之为trace。这个问题其实比判定行动可行与否还要棘手一点。因为，我假设程序获得的唯一信息就是起始坐标和终止坐标两样。</p>



<p>我最后采取的，依赖numpy线性代数功能的方案多少有点离经叛道：对于矩阵上的每一个点，我计算它到起始点和终止点的向量，并用行列式判定共线性，同时用两个向量和总向量的点积来判定方向正确。现在想来可能这个方法不如运用和masking一致的迭代式方法。这里我一开始遇到了一些奇怪的问题，比如挖出来的方向是反的，或者有两个点莫名其妙漏挖了等等。不过最后还是解决了。这个函数我称之为“ripping”，因为很有一种皇后划过并撕破棋盘的感觉。</p>



<p>顺带一提，我在程序中对“皇后棋”的非正式英语名称是“Flight of Queen”(皇后的逃亡），缩写FoQ，因为单后棋的那种混乱很有一种逃杀的感觉。</p>



<p>有了这两个方法，皇后棋写出来便很容易了。编了一个界面玩了两局，我便开始考虑AI的问题。这便是我之前并不熟悉的强化学习（Reinforced Learning）问题。实际上我一开始考虑的是一种类似Policy Gradient的方法，直接对策略求导来训练某种神经网络。不过这个想法太模糊了，最后我不得不放弃。</p>



<p>然后我就想到了在围棋，象棋等上大放异彩的AlphaGo-AlphaZero方法。实际上这个想法还是挺令人畏惧的，因为虽然这两个成果已经有几年了，在深度学习领域算是老腊肉（划掉），但是毕竟还是引起社会广泛关注的精尖技术，我能够操作这样的算法吗？于是我的想法其实是退而求其次，考虑AlphaGo核心的MCTS（蒙特卡洛树搜索）算法。皇后棋相当简单，所以没准直接用经典MCTS就能搞出一个很强的AI。于是我就直接在浏览器开始搜mcts。有趣的是，MCTS似乎已经和AlphaZero深度绑定了，我搜出来的使用Jax实现的MCTS（叫做<a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9naXRodWIuY29tL2RlZXBtaW5kL21jdHg=" target="_blank">mctx</a>），直接就是DeepMind搞出来为AlphaZero服务的模块。mctx自己给的案例写的十分复杂，我大概花了几个小时都没看懂，一度有点想放弃MCTS。后来我觉得pypi上可能还能淘到一些好东西，所以便又上pypi找MCTS包，果然找到了一款“只要写一个class就能在任意完全信息博弈领域进行搜索的”<a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9naXRodWIuY29tL2tzdHJ1ZW1wZi9NQ1RT" target="_blank">MCTS库</a>，python类的多态继承大显神威！</p>



<p>这个MCTS要求写这几个成员函数：</p>



<pre class="wp-block-preformatted">get_possible_actions()
get_current_player()
take_action()
is_terminal()
get_reward()
</pre>



<p>其中第一个告诉这个经典智能体它能够采取的行动，用列表表示，第二个是一个getter，输出当前轮次，第三个则是最关键的走子。第四个判断棋局是否终结。最后一个是终结时输出一个奖励，用于搜索的叶子节点。单后棋写出来是这样：</p>



<pre class="wp-block-code"><code class="prettyprint">class FlightOfQueen(BaseState):
    def __init__(self):
        self.board = np.zeros((8, 8))
        self.currentPlayer = 1
        self.coords = (0, 0)
        self.rounds = 0

    def get_current_player(self):
        return self.currentPlayer

    def masking(self):
        return action_masking2(self.board, self.coords)

    def get_possible_actions(self):
        possibleActions = []
        mask = self.masking()
        #print(mask)
        for i in range(self.board.shape[0]):
            for j in range(self.board.shape[1]):
                if mask[i][j] == 1:
                    possibleActions.append(Action(player=self.currentPlayer, coords=(i, j)))
        if len(possibleActions) == 0:
            possibleActions.append(Action(player=self.currentPlayer,coords=self.coords[self.currentPlayer]))
        return possibleActions

    def take_action(self, action):
        newState = deepcopy(self)
        newState.board = ripping(self.board, self.coords, action.coords)
        newState.currentPlayer = self.currentPlayer * -1
        newState.coords = action.coords
        newState.board[action.coords[0], action.coords[1]] = 2
        self.rounds += 1
        return newState

    def is_terminal(self):
        mask = self.masking()
        return mask.sum() == 0

    def get_reward(self):
        return self.currentPlayer * 20/(self.rounds+1)</code></pre>



<p>reward写法是为了鼓励智能体速战速决。</p>



<p>似乎MCTS算法在单后棋上效果还不错，经常能够击败我这个新手玩家。</p>



<p>我与22885讨论了一下这个成果。他告诉我单后棋实在太抽象，他们一般下双后棋。</p>



<p>于是我便考虑如何改造成双后棋。其实原则上并不难，只是变成维持两个坐标，然后把actionmasking变成一个自己的actionmask减去对方actionmask然后判定的操作就差不多。当然get_reward也要重写成和单后棋多少相反的方法，这个我纠结了好久。花了一点时间，带AI的双后棋也编出来了。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674143847-image-6661fac3.png"><img loading="lazy" decoding="async" loading="lazy" width="352" height="410" src="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674143847-image-6661fac3.png" alt="" class="wp-image-5202"/></a><figcaption class="wp-element-caption">与MCTS下双后棋</figcaption></figure></div>


<p>实际上这个MCTS算法也没有我说的那么轻松。一开始他经常会弹出一个错误，叫“Should Never Reach Here”，是他内部定义的一个catch error。然而显然这个提示对于debug毫无帮助。实际上最后会发现，step里面各种令人瞠目结舌的错误都有可能导致这个问题，而他却没有更好的提示。还有一个常见错误是“No Possible moves in non_terminal state”,这个倒是好理解，就是游戏没结束，一方却没有可行动作了。和这些错误缠斗了我好一会。</p>



<p>然后我又回到了MCTX库那里。他给了一个外部链接，指向配套的<a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9naXRodWIuY29tL05UVDEyMy9hMC1qYXg=" target="_blank">AlphaZero项目</a>。我之前一直在研究mctx自己给的井字棋样例，然而这么个简单的小游戏也让我很头疼。没想到，AlphaZero项目里给的井字棋案例令人豁然开朗，让我觉得：我也能训练AlphaZero！因为和那个经典MCTS包一样，这个库也是只要定义一个特殊的类，便可以使用它作为一个enviroment来强化学习我们的智能体！</p>



<p>于是我便首先开始研究如何用jax重构皇后棋代码，使这个版本的AlphaZero可以采用。各位可能对jax不熟悉，不过你们可能都知道PyTorch或者TensorFlow，Keras这样的深度学习框架（或者更粗暴地，“自动微分框架”）。Jax就是谷歌准备的下一代自动微分框架，有很大的潜力（所以DeepMind就带头采用）。Jax的魅力有很多，改天可以专门写文章介绍。比如，它包含Jax.numpy（jnp）,也就是numpy能干的活他都能干。更重要的是，jnp可以在GPU、TPU上以高度并行化的方式运行，而numpy本身是针对CPU优化的。在特定条件下，jnp可以比np快上百倍。不仅如此，jnp的函数是可以直接求导的！没错，只要该函数完全由可微分算子复合而成，那么一个简单的grad算符就可以立刻告诉你这个函数的输出相对于其每一个输入的偏微分。同时，jax还鼓励一种函数式编程的范式，有时甚至有点像Haskell。Vmap算符更是强大，可以完全规避烦人的矩阵维度问题，轻松考虑batch dimension，拯救不擅长思考高位空间人类。听说jax还准备搞一个xmap，允许你直接给每一个轴命名，更是让此类问题成为过去。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674144339-image-ff6ddce5.png"><img loading="lazy" decoding="async" loading="lazy" width="250" height="145" src="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674144339-image-ff6ddce5.png" alt="" class="wp-image-5203"/></a><figcaption class="wp-element-caption">Jax！</figcaption></figure></div>


<p>总之，我开始用jnp改写双后棋。最后结果类似这样：（这个代码其实已经是我后期反复修改过的，一开始还不是这样）</p>



<pre class="wp-block-code"><code class="prettyprint">class Ripper(pax.Module):
    
    num_cols: int = 9
    num_rows: int = 8
    
    def __init__(self):
        super().__init__()
        self.all_coords = jnp.stack([jnp.repeat(jnp.arange(self.num_rows),repeats=self.num_cols),jnp.tile(jnp.arange(self.num_cols),reps=self.num_rows)],axis=0)

    def __call__(self, board: jnp.array, coords: jnp.array, new_coords: jnp.array):
        translation = new_coords - coords
        coords_tile = jnp.tile(coords,reps=(self.num_cols*self.num_rows,1)).transpose()
        new_coords_tile = jnp.tile(new_coords,reps=(self.num_cols*self.num_rows,1)).transpose()
        temp_translation = self.all_coords - coords_tile
        temp_translation2 = new_coords_tile - self.all_coords
        #print(temp_translation.shape, temp_translation2.shape)

        def vacuuming(trans, temp_trans,temp_trans2):
            det = jnp.linalg.det(jnp.stack([trans, temp_trans]))
            dire = jnp.logical_and(jnp.dot(temp_trans, trans) &gt;= 0, jnp.dot(temp_trans2,trans) &gt;= 0)
            return jnp.logical_and(dire, jnp.abs(det)&lt;=0.01).astype(jnp.int8)
        
        trace = vmap(vacuuming, in_axes=(None,1,1))(translation, temp_translation,temp_translation2)
        trace = trace.reshape((self.num_rows,self.num_cols))

        return board - trace
    
class ActionMasker(pax.Module):
    num_cols: int = 9
    num_rows: int = 8
    def __init__(self):
        super().__init__()
        self.movements = jnp.array([[1, 0], [0, 1], [-1, 0], [0, -1], [1, 1], [-1, -1], [1, -1], [-1, 1]], dtype=jnp.int8)

    def explore(self,board, x, y, d_x, d_y, mask):
        def cond(val):
            state, x, y, d_x, d_y, conds, board, mask = val
            del x, y, board, mask
            d_conds = jnp.array([d_x &gt; -1 , d_x &lt; 1, d_y &gt; -1, d_y &lt; 1], dtype=jnp.bool_)
            conds = jnp.logical_or(d_conds,conds)
            conds = jnp.logical_and(conds[:2],conds[2:])
            conds = jnp.logical_and(conds[0],conds[1])
            return jnp.logical_and(conds, state!=-1)

        def probe(val):
            state, x, y, d_x, d_y, conds, board, mask = val
            x += d_x
            y += d_y
            state = board[x, y]
            mask = jnp.where(state != -1,mask.at[x,y].set(1),mask)
            conds = jnp.array([x &gt; 0, x &lt; (board.shape[0] - 1), y &gt; 0, y &lt; (board.shape[1] - 1)], dtype=jnp.bool_)
            val = state, x, y, d_x, d_y, conds, board, mask
            return val

        conds = jnp.array([x &gt; 0, x &lt; (board.shape[0] - 1), y &gt; 0, y &lt; (board.shape[1] - 1)], dtype=jnp.bool_)
        container = 0, x, y, d_x, d_y, conds, board, mask
        state, x, y, d_x, d_y, conds, board, mask = while_loop(cond,probe,container)

        return mask

    def __call__(self, board, coords):
        n_board = jnp.reshape(board,(self.num_rows,self.num_cols))
        mask = jnp.zeros_like(n_board)
        x = coords[0]
        y = coords[1]

        def oct_explore(n_board, x, y, movement):
            return self.explore(n_board, x, y, movement[0], movement[1], mask)

        mask = vmap(oct_explore,in_axes=(None,None,None,0))(n_board,x,y,self.movements).sum(axis=0)

        return mask
    
    
class FlightOfQueenGame(Enviroment):
    &#34;&#34;&#34;FOQ game environment&#34;&#34;&#34;

    board: chex.Array
    who_play: chex.Array
    terminated: chex.Array
    recent_boards: chex.Array  # a list of recent positions
    winner: chex.Array
    num_cols: int = 9
    num_rows: int = 8
    num_recent_positions: int = 4

    def __init__(self, num_cols: int = 9, num_rows: int = 8):
        super().__init__()
        self.masker = ActionMasker()
        self.ripper = Ripper()
        self.board = jnp.zeros((num_rows, num_cols), dtype=jnp.int32)
        self.recent_boards = jnp.stack([self.board] * self.num_recent_positions)
        self.who_play = jnp.array(0, dtype=jnp.int32)
        self.terminated = jnp.array(0, dtype=jnp.bool_)
        self.winner = jnp.array(0, dtype=jnp.int32)
        self.count = jnp.array(0, dtype=jnp.int32)
        self.coords = jnp.array([[0,0],[7,8]],dtype=jnp.int32)
        self.all_coords = jnp.stack([jnp.repeat(jnp.arange(self.num_rows),repeats=self.num_cols),jnp.tile(jnp.arange(self.num_cols),reps=self.num_rows)],axis=0)
        self.reset()

    def num_actions(self):
        return self.num_cols * self.num_rows
    
    def duo_mask(self, whom):
        mask = self.masker(self.board, self.coords[whom])
        mask = mask - self.masker(self.board, self.coords[(whom + 1) % 2])
        return  mask.flatten()

    def invalid_actions(self) -&gt; chex.Array:
        return self.duo_mask(self.who_play) &lt;= 0

    def reset(self):
        self.board = jnp.zeros((self.num_rows, self.num_cols), dtype=jnp.int32)
        self.recent_boards = jnp.stack([self.board] * self.num_recent_positions)
        self.who_play = jnp.array(0, dtype=jnp.int32)
        self.terminated = jnp.array(0, dtype=jnp.bool_)
        self.winner = jnp.array(0, dtype=jnp.int32)
        self.count = jnp.array(0, dtype=jnp.int32)

    @pax.pure
    def step(self, action: chex.Array) -&gt; Tuple[&#34;FlightOfQueenGame&#34;, chex.Array]:
        &#34;&#34;&#34;One step of the game.
        An invalid move will terminate the game with reward -1.
        &#34;&#34;&#34;
        invalid_move = self.duo_mask(self.who_play)[action] &lt;= 0
        new_coords = self.all_coords[:,action]
        #self.board = self.board.at[self.coords[self.who_play, 0]*9+self.coords[self.who_play, 1]].set(-1)
        board_ = self.ripper(self.board, self.coords[self.who_play],new_coords)
        board_ = jnp.clip(board_,-1,2)
        self.coords = self.coords.at[self.who_play].set(new_coords)
        self.board = select_tree(self.terminated, self.board, board_)
        self.recent_boards = jnp.concatenate((self.recent_boards[1:], self.board[None]))
        
        self.winner = jnp.where((self.duo_mask(self.who_play)&lt;=0).sum() == 0,-1,0)
        reward_ = jnp.where(self.who_play == 0,self.winner,-self.winner)
        # increase column counter
        self.who_play = 1 - self.who_play
        self.count = self.count + 1
        self.terminated = jnp.logical_or(self.terminated, reward_ != 0)
        self.terminated = jnp.logical_or(
            self.terminated, self.count &gt;= self.num_cols * self.num_rows
        )
        self.terminated = jnp.logical_or(self.terminated, invalid_move)
        reward_ = jnp.where(invalid_move, -1.0, reward_)
        return self, reward_

    def render(self) -&gt; None:
        &#34;&#34;&#34;Render the game on screen.&#34;&#34;&#34;
        #board = jnp.reshape(self.board,(self.num_rows,self.num_cols))
        board = self.board
        board = board.at[self.coords[self.who_play, 0], self.coords[self.who_play, 1]].set(1)
        board = board.at[self.coords[1-self.who_play, 0], self.coords[1-self.who_play, 1]].set(2)
        print(self.observation())
        for row in range(self.num_rows):
            for col in range(self.num_cols):
                if board[row, col].item() == 1:
                    print(&#34;X&#34;, end=&#34; &#34;)
                elif board[row, col].item() == -1:
                    print(&#34;O&#34;, end=&#34; &#34;)
                elif board[row, col].item() == 2:
                    print(&#34;A&#34;, end=&#34; &#34;)
                else:
                    print(&#34;.&#34;, end=&#34; &#34;)
            print()
        print()
        
    def neutral_obs(self, whom) -&gt; chex.Array:
        board = jnp.zeros_like(self.board)
        board = board.at[self.coords[whom,0],self.coords[whom,1]].set(1)
        board = board.at[self.coords[1-whom,0],self.coords[1-whom,1]].set(-1)[None]
        coord_visual = self.duo_mask(whom).reshape((self.num_rows,self.num_cols))[None]
        return jnp.concatenate([self.recent_boards,board,coord_visual])

    def observation(self) -&gt; chex.Array:
        #return jnp.reshape(board, (self.num_rows, self.num_cols))
        return self.neutral_obs(0)

    def canonical_observation(self) -&gt; chex.Array:
        return self.neutral_obs(self.who_play)

    def is_terminated(self):
        return self.terminated

    def max_num_steps(self) -&gt; int:
        return self.num_cols * self.num_rows

    def symmetries(self, state, action_weights):
        action = action_weights.reshape((self.num_rows, self.num_cols))
        #out.append((state, action_weights))
        for i in [0,2]:
            rotated_state = np.rot90(state, k=i, axes=(0, 1))
            rotated_action = np.rot90(action, k=i, axes=(0, 1))
            out.append((rotated_state, rotated_action.reshape((-1,))))

            flipped_state = np.fliplr(rotated_state)
            flipped_action = np.fliplr(rotated_action)
            out.append((flipped_state, flipped_action.reshape((-1,))))

        return out</code></pre>



<p>这就是已经完全按照a0要求编写的环境了。可以看到我把ripper和masker都变成了pax.module类，方便在foq类内部使用。</p>



<p>在jnp重写中，值得注意的一点是我尽量试图让操作并行化，而少用循环结构。比如，ripper部分我就构造了一个向量，直接72维代表每一个格点的坐标，然后用vmap与重叠72遍的起点/终点坐标相配计算。甚至explore部分的八向探索我也试图用单一lax进程优化。不过在jupyter环境下，测算下来这样改反而要花560ms执行一步，而原先只要430ms，反而慢了。不过我猜想这是因为一直在GPU和CPU之间搬运信息导致的减慢，如果jit加速之后一直在GPU上运行，应当是节省时间的。</p>



<p>虽然相关资料很多，这里还是很粗略很不专业的科普一下AlphaZero的原理。</p>



<p>AlphaZero的本质类似一种“受神经网络指导的MCTS搜索”。也就是它结合了两种算法，一种经典的，一种神经的。这个神经网络是双头的，首先会有一个骨干网络进行通用的局面分析，然后就分叉到两个头，一个叫策略头（Policy Head），输出一个类别型概率分布，其大小与动作空间的大小一样大。比如围棋有361个格点，外加轮空一种，共有362种操作，策略头输出的就是362维的向量，代表采取每一种动作的估算先验概率logit，后面会mask掉不可能的并softmax转为正式分布；另一个叫估值头（Value Head），只输出一个值，这个值代表当前局面下，可能的终局奖励：赢即为1，输为-1，平为0。围棋AI给的“胜率”参数其实就是估值头的输出。可以认为策略头是战术层面的，进行微观的，局部的判断，而估值头则是战略层面的，进行宏观的，大局的判断。</p>



<p>搜索树就受这双头蛇式的网络指导，不断进行模拟，模拟的早期尽量按照先验概率大的方法走，晚期按照估值较大的方法走（大概）。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674216411-image-cc690fa3.png"><img loading="lazy" decoding="async" loading="lazy" width="356" height="212" src="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674216411-image-cc690fa3.png" alt="" class="wp-image-5219"/></a><figcaption class="wp-element-caption">双头蛇</figcaption></figure></div>


<p>在训练的一开始，神经网络的策略是完全随机的，也就是搜索树会随机落子。称之为“树”，是因为它将每一个局面（State）视为节点，而每一个操作将使其转移到下一个节点。游戏结束的局面称之为叶子节点（Leaf）。对于每一步，搜索树会模拟一定数量的局面，可能为32个或者64个，然后返回一个更优化的策略分布（更能导向胜利）。这一过程称之为“策略提升”。</p>



<p>智能体将每一局游戏下完，就会记录胜负，以及抽取一个局面记住当时自己估算的策略分布和搜索树返回的提升分布，并最终用这些东西（每一iteration可能下102400盘）进行一波有监督学习，目标是让自己的神经网络从任意给定局面直接看出最后胜负（估值头拟合胜负Reward），并让自己预估的策略更接近斟酌后的提升策略（策略头拟合策略），两者分别采用均方误差损失和交叉熵损失。然后用训练过后的神经网络指导自我对弈，周而复始。</p>



<p>对于皇后棋，我一开始认为它比较简单，因此就选用了一个比较简易的参数化方式：直接用一个两三层的MLP拟合，总共五万多参数量。网络的输入是一个76维向量，前72维代表每一个格子挖去与否的状态，后四维则粗暴地直接输入了两个后的坐标。</p>



<p>这里让我十分头疼的其实是对称性问题上。众所周知，很多数据其实都具有良好的对称性，比如围棋，其实把棋盘转一转，翻一翻，动作也就转一转、翻一翻就是了，是高度对称的，然而，比如卷积神经网络的卷积核，如果不加约束，只能自己慢慢摸索出“对称性”的概念，这会十分浪费（体现了深度学习的非智能性）。而DeepMind给的解决方案也只是“数据增强”层面上的，也就是在训练的时候把棋盘动作转一转，翻一翻一块输进去，我承认我不是很满意。理论上是完全可以在网络结构上加一些约束，来保证更好的equivariance的。我听说有一些数学家在做invariance和equivariance的深入研究，比如把一些群论的观点引入深度学习，这我是十分支持的，肯定能节约训练时间。而让我头疼的点在于我把棋盘展平成为一维向量的做法：这意味着我不得不把最后四维坐标先截下来，然后手算一个旋转和翻转，再和那72个能够正常旋转和翻转的数据“会师”，显得很不优雅。不过也只好这样做了。</p>



<p>这样就可以训练了。模仿他的样例对我自己的类输了一行指令，训练便开始了。</p>



<p>这里也并不一帆风顺。第一次我试的时候，发现每一次evaluation（新老智能体对弈），256番棋都是正好128胜128负。一看，竟然是坐标改变的算法没有写好，让两个人都没法移动，只能“发波”挖去格子困死对方。这太傻了。原来是jnp的小麻烦之一：数组完全没法直接修改（jnp arrays are immutable），必须用一个奇怪的“.at[x].set(a)”方法，返回一个更改了的复制品（所有的函数返回的都是复制品，原输入默认只读），所以如果忘写一个=赋值就会出问题。</p>



<p>改好后再试，后又发现出现大量的平局。按理说唯一出现平局的可能性是“行动超过72轮”（我设定的），然而棋盘上格子也就72个，即便两玩家一直乌龟爬，72轮也总能结束游戏。最后，又是一个离奇的错误：我设置障碍物是严格判定“-1”这个值，然而每一次后落在的位置会因为前后各减一次而变成“-2”，这就导致这个值永远的不成为障碍物，而且畅通无阻！所以我用一个jnp.clip语句修正了，让值最小就是-1。</p>



<p>然后我就在19日上午训练了大概一个半小时，只进行了23个iteration，来看看效果如何。训练时policy loss倒是一直在下降，从一开始2.56到最后只有0.9几了，说明拟合提升后的策略有进步。然而value loss居高不下甚至多有反复，徘徊在0.46附近，可谓“当局者迷”级别。现在想来是因为这两个值的数量级有一点区别，所以一开始很可能是策略损失主导了训练，值损失是第二位的（毕竟这还是个多目标优化问题！）。和这个ai下了一下，感觉傻傻的，很容易就击败了，可见是十分欠训练的。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674216149-b3_y3w2j3_epkak5olme-42f58c40.png"><img loading="lazy" decoding="async" loading="lazy" width="432" height="440" src="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674216149-b3_y3w2j3_epkak5olme-42f58c40.png" alt="" class="wp-image-5217"/></a><figcaption class="wp-element-caption">训练的过程</figcaption></figure></div>


<p>不过我并不很打算继续训练这个试水的agent，而是用更强的网络打造一个好一点的。我原先想直接用ResNet，可是发现那个云平台CuDNN版本太低，没法调用tf后台执行卷积，又不怎么支持升级这种半硬件内容。所以我不得不想办法自己搞一个土ResNet，一个“没有卷积的卷积神经网络”出来，目前已经有了一定的进展。大致思路是按照长、宽维度展平用MLP做成“条状卷积”，然后进行残差连接，模仿ResNet的残差块。不过对于Batch Norm我还有一些实践上的困惑，可能需要实验一下来解答。</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674216186-ljsbmwxchxd-2d4u6y3h-f2734c8e.png"><img loading="lazy" decoding="async" loading="lazy" width="258" height="155" src="https://blog.nth.ink/media/posts/mcts-of-kotenyca/1674216186-ljsbmwxchxd-2d4u6y3h-f2734c8e.png" alt="" class="wp-image-5218"/></a><figcaption class="wp-element-caption">CPU：一核有难，二十四核围观</figcaption></figure></div>


<p>总之这就是目前为止的成果了。下一步有几个方向，一方面是继续训练，一方面是扩展到其他的棋类，比如248宣棋。</p>



<p>欢迎大家来和我交流！</p>]]></description>
  </item>
  <item>
   <title>A Bayesian Analysis of High School Acceptance Rates in Shanghai</title>
   <link>https://blog.nth.ink/posts/bayesian-high-school-acceptance</link>
   <guid>https://blog.nth.ink/posts/bayesian-high-school-acceptance</guid>
   <pubDate>Mon, 22 Aug 2022 23:15:00 +0000</pubDate>
   <description><![CDATA[<h2 class="wp-block-heading">Introduction</h2>



<p>It is a commonplace practice to estimate, evaluate, and compare the educational qualities of middle schools by using an enrollment based metric. To elaborate, if one intends to measure the quality of a school, he or she will calculate the proportion of students there that are accepted into prestegious high schools. A school is defined as superior if it can ensure a greater ratio of Quality High School Acceptance. For example, at Huayu Middle School in Xuhui District, Shanghai, more than one third of the students are ultimately accepted into Shanghai High School , the best of the renowned &#34;Four High Schools&#34; in Shanghai, each year. In addition, another 50 students or so will find their way into the other three good schools, or into Qibao High School, another educational powerhouse. Altogether, this data enshrines Huayu as practically the best middle school in all of Shanghai.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/dscn1884-b1398666.jpg"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/dscn1884-b1398666.jpg" alt="" class="wp-image-3654" width="424" height="318"/></a><figcaption>Arbitary Students of Huayu Middle School</figcaption></figure></div>


<p>Despite being effective and straightfoward, this acceptance rate metric has its shortcomings. It is a Frequentist measurement that assumes the likelihood as an approximation of the prior probability. Plainly speaking, a Huayu student may regard the historical acceptance frequency (likelihood) as the acceptance probability (prior) for himself/herself, which is far from reality. Hence, this model is ,to a certain extent, deceptive, for it conceals and disguises our ignorance on the topic. Note that if &#34;Big data&#34; is present in this context, then the Frequentist assumption will stand well. If so, one can then happily use the likelihood as the approximate prior probability. Unfortunately, with the amount of data present ,uncertainty is not, and cannot be completely eliminated from the scope. Therefore, I would like to present to you ,in this blog post, a Bayesian approach to the issue of evaluating middle schools.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661161519-image-94c42df1.png"><img loading="lazy" decoding="async" loading="lazy" width="516" height="75" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661161519-image-94c42df1.png" alt="" class="wp-image-4869"/></a><figcaption>Baye&#39;s Theorem</figcaption></figure></div>


<h2 class="wp-block-heading">An Introduction to Bayesian Inference</h2>



<blockquote class="wp-block-quote"><p>Bayesian methods are one of many in a modern data scientist&#39;s toolkit. They can be used to solve problems in prediction, classiﬁcation, spam detection, ranking, inference, and many other tasks.</p><cite>—— Bayesian Methods for Hackers</cite></blockquote>



<p>The philosophy of Bayesian Inference is very simple. It involves updating your beliefs after considering new evidence, which is natural to us humans. If one keeps making mistakes on a certain kind of problem, then he will start to lose confidence in his ability to solve this particular sort of problem. Conversely, if one seldom fails to do some kind of problem, his confidence will grow. Though, he will never be absolutely sure in himself. Bayesian Inference works identically: We update our beliefs about an outcome, but rarely can we be absolutely sure unless we rule out all other alternatives.</p>



<p>From the Bayesian Perspective, probability is interpreted as measure of <strong>believability</strong>. That is, how confident we are in an event occuring. If we assign an event with the probability 0, then we are asserting our belief that this event will never happen. Assigning 1, on the other hand, asserts something that is bound to happen. Assigning a value in between will mean that the event would have some chance of happening, but we are not totally confident.</p>



<p>The belief we hold on a certain event before seeing any evidence is called a <strong>prior</strong>. It reflects our intuitive thought on an issue, and it is potentially very wrong. For example, if you are presented with a dice, then you might assume that it is unweighted, having a uniform distribution of probability across its six sides. Then, the evidence from experimentation and observation that help us update our beliefs is called the <strong>likelihood</strong>. E.g. you roll the dice six times and find that you get &#34;6&#34; three times, &#34;4&#34; twice, and &#34;2&#34; once. Finally, you update your prior according to the likelihood, and obtain a more accurate belief about the event, namely, the <strong>posterior</strong>. The method of update can vary. A typical method might be <strong>MCMC</strong> (Monte Carlo Markov Chain), which applies a method dubbed rejection sampling to intelligently explore the posterior landscape, returning an amount of samples cumulatively called a <strong>trace</strong>. A more state-of-the-art sampler would be the No-U-Turn-Sampler, or <strong>NUTS</strong>. NUTS belongs to the <strong><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly96aHVhbmxhbi56aGlodS5jb20vcC82NzY5MTU4MQ==" target="_blank">HMC</a></strong> (Hamiltonian Monte Carlo) family of samplers, which utilizes the <strong>gradient</strong> of the posterior landscape to assist the sampling process. This would sound quite familiar to Neural Network enthusiasts, who uses Tensorflow or Pytorch in their work very often. To me, though, the intuition behind HMC is more straightforward: the posterior is really regarded as a “landscape”, complete with notions of <strong>gravity</strong>! The sampler can then be seen as an infinitesimal ball traversing it, governed by kinetic and potential energy…… The details of these go beyond this article: those of you interested can look it up yourself.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661173586-image-f84a0473.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661173586-image-f84a0473.png" alt="" class="wp-image-4878" width="551" height="228"/></a><figcaption>An illustration of HMC</figcaption></figure></div>


<p>In the dice example here, however, a very neat mathematical trick can be used, which allows us to directly express the posterior distribution in its exact form! This trick is called using a <strong>Conjugate Prior</strong>. The conjugate prior of the <strong><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly93d3cucHltYy5pby9wcm9qZWN0cy9kb2NzL2VuL3N0YWJsZS9hcGkvZGlzdHJpYnV0aW9ucy9nZW5lcmF0ZWQvcHltYy5CaW5vbWlhbC5odG1s" target="_blank">Binomial Distribution</a></strong> is called the <strong><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly93d3cucHltYy5pby9wcm9qZWN0cy9kb2NzL2VuL3N0YWJsZS9hcGkvZGlzdHJpYnV0aW9ucy9nZW5lcmF0ZWQvcHltYy5CZXRhQmlub21pYWwuaHRtbA==" target="_blank">Beta Distribution</a></strong>, while the conjugate prior of the <strong><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly93d3cucHltYy5pby9wcm9qZWN0cy9kb2NzL2VuL3N0YWJsZS9hcGkvZGlzdHJpYnV0aW9ucy9nZW5lcmF0ZWQvcHltYy5NdWx0aW5vbWlhbC5odG1s" target="_blank">Multinomial Distribution</a></strong> is called the <strong><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly93d3cucHltYy5pby9wcm9qZWN0cy9kb2NzL2VuL3N0YWJsZS9hcGkvZGlzdHJpYnV0aW9ucy9nZW5lcmF0ZWQvcHltYy5EaXJpY2hsZXRNdWx0aW5vbWlhbC5odG1s" target="_blank">Dirichlet Distribution</a></strong>. How do we use the conjugate prior to derive the posterior? The dice conforms to a multinomial distribution with 6 outcomes. Hence, it has a Dirichlet conjugate prior with 6 parameters. The uniform prior of probabilities(p = {0.167 * 6}) we assigned to the dice can be put down as <em>p ~ D(θ)</em>，where θ = {1,1,1,1,1,1}. How do we add the likelihood into it? Simply <strong>add it with the parameters</strong>! Now we have <em>p ~ D(θ&#39;)</em> where θ&#39; = θ + Δθ, Δθ={3, 0, 2, 0, 1, 0}. θ&#39; = {4, 1, 3, 1, 2, 1}. This method will be used in the discussion of admission rates below.</p>



<h2 class="wp-block-heading">Inference of the True Acceptance Rate</h2>



<p>We shall commence the modelling of High School Enrollment from a Bayesian perspective. The true acceptance rate for each of the Four High Schools is the unknown variable -&gt;p, with five possible outcomes: SHS, EFZ, FF, JF, and None. We choose the Dirichlet Multinomial D as the prior distribution of -&gt;p.</p>



<p>There is no official, centralized and recognized source for the  enrollment data of each school. Thus, we adopt the data from <a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly96aHVhbmxhbi56aGlodS5jb20vcC8xNjAyMDE4OTA=" target="_blank">https://zhuanlan.zhihu.com/p/160201890</a>.</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661175906-image-9e7b7d86.png"><img loading="lazy" decoding="async" loading="lazy" width="1080" height="523" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661175906-image-9e7b7d86.png" alt="" class="wp-image-4881"/></a></figure>



<p>Note that I was only able to obtain the data via Preemptive Enrollment. Technically the enrollment via admission test should be included as well, but that&#39;s a nebulous administrative allocation even harder to obtain.</p>



<p>We assume the Uniform Prior for each school. For each student, a five-sided &#34;dice&#34; is cast. Here we try to determine how the dice is weighted for each school.</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176605-image-ebe750f2.png"><img loading="lazy" decoding="async" loading="lazy" width="818" height="330" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176605-image-ebe750f2.png" alt="" class="wp-image-4884"/></a></figure>


<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176397-7ebf23f200de3f694d97da41a8dc0b0-65cab75d.jpg"><img loading="lazy" decoding="async" loading="lazy" width="719" height="446" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176397-7ebf23f200de3f694d97da41a8dc0b0-65cab75d.jpg" alt="" class="wp-image-4882"/></a></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176436-118fa838dcd352edb4607203077d344-4f36c02b.jpg"><img loading="lazy" decoding="async" loading="lazy" width="813" height="442" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176436-118fa838dcd352edb4607203077d344-4f36c02b.jpg" alt="" class="wp-image-4883"/></a></figure></div>


<p>These are the shapes of the posterior distributions. The Dirichlet is sometimes called the &#34;Distribution of Distributions&#34;, because it actually contains probability vectors that can dictate the shape of a whole family of distributions. Here the distribution for None is not displayed, as it generally takes much greater values than actual enrollment. Note that the five probabilities should sum up to 1 in each individual sample. </p>



<p>From the posterior densities, we can infer that Huayu students have a 30% to 35% chance of entering SHS through Preemptive, but only around 5% for JFZ and EFZ, and around 0% for FF. For a Lansheng student, the worst-case-scenario still offers a more than a 30% probability to enter FF, while the best-case-scenario approximates <strong>50%</strong>! (Are we talking about die or coins?) Notice that the distributions for Lansheng are spread out more than those from Huayu. That is, they have a larger variance. This is a result of Lansheng&#39;s smaller size, which puts more doubt on the posterior. In other words, with only 190 samples, Lansheng&#39;s data eliminates less uncertainty from the prior. This is one of the beauties of Bayesian Inference, where uncertainty is retained, and a more robust comparision can be done between entities of varying size.</p>



<p>Evidently the SHS is inclined towards Huayu, while FF is inclined to the Lansheng Fudan, a well know phenomena.</p>



<p>Here are some other schools.</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176645-bc26c724194a9a9624c6a12a83a60b6-a0782b59.jpg"><img loading="lazy" decoding="async" loading="lazy" width="867" height="488" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176645-bc26c724194a9a9624c6a12a83a60b6-a0782b59.jpg" alt="" class="wp-image-4885"/></a></figure>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176656-cf0d3f0d089cbc235b2211dafab21ff-d8dcd233.jpg"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176656-cf0d3f0d089cbc235b2211dafab21ff-d8dcd233.jpg" alt="" class="wp-image-4886" width="840" height="487"/></a></figure>



<p>Note the difference in scale for the axes.</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176665-89fc7056ee4f162719ed5dc2185412a-b3fda3cf.jpg"><img loading="lazy" decoding="async" loading="lazy" width="871" height="495" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661176665-89fc7056ee4f162719ed5dc2185412a-b3fda3cf.jpg" alt="" class="wp-image-4887"/></a></figure>



<h2 class="wp-block-heading">Applying a Loss (Win) Function and CI</h2>



<blockquote class="wp-block-quote"><p>&#34;Staticians are a sour bunch. They measure the outcomes in terms of <em>Losses</em>. When one wons, he or she says that he or she <em>Lost Less</em>&#34;</p><cite>Bayesian Inference for Hackers</cite></blockquote>



<p>Now we have estimated the distribution of enrollment probabilities, we should summarize them in a robust way. It is said that SHS and EFZ are the better two of the Four Schools, because more of their students are further admitted into Tsinghua or Peking University, while FF and JF students primarily go to Fudan and Jiaotong University. Actually this layer of admission can also be modelled with a Dirichlet-Multinomial. But I did not wish to create something resembing a <em><strong>Latent Dirichlet Allocation</strong></em> just yet. So I simply assigned the Win score 1 to anyone going to SHS or EFZ, and 0.6 to anyone going to FF or JF.</p>



<p>Aside from this &#34;Win Function&#34;, I decided to mark the CI (Confidence Interval) as well. If a scenario of admission is so bad that only 5% percent of the simulated scenarios are worse, then we call it the 5% lower bound. Conversly, a super win situation that is better than 95% of the simulated situations is deemed the 95% upper bound. We mark the two limits with lines.</p>



<p>Here&#39;s the revised code:</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179442-image-713af50e.png"><img loading="lazy" decoding="async" loading="lazy" width="820" height="442" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179442-image-713af50e.png" alt="" class="wp-image-4891"/></a></figure>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179376-image-ec8c1bdf.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179376-image-ec8c1bdf.png" alt="" class="wp-image-4890" width="599" height="373"/></a></figure></div>


<p>In the legend, P. and O. stand for Pessimistic and Optimistic.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179483-image-5dd04f24.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179483-image-5dd04f24.png" alt="" class="wp-image-4892" width="666" height="371"/></a></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179515-image-64caaa02.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179515-image-64caaa02.png" alt="" class="wp-image-4893" width="699" height="362"/></a></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179628-image-21ebf55b.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179628-image-21ebf55b.png" alt="" class="wp-image-4894" width="597" height="334"/></a></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179659-image-9882e311.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179659-image-9882e311.png" alt="" class="wp-image-4895" width="530" height="330"/></a></figure></div>


<p>We also have another Evaluation of Huayu with 2022 Data.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179803-image-d2f6259c.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661179803-image-d2f6259c.png" alt="" class="wp-image-4896" width="638" height="372"/></a></figure></div>


<p>Here are some numeric summaries :</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661180373-image-ab9ac913.png"><img loading="lazy" decoding="async" loading="lazy" width="601" height="306" src="https://blog.nth.ink/media/posts/bayesian-high-school-acceptance/1661180373-image-ab9ac913.png" alt="" class="wp-image-4897"/></a></figure>



<p>It appears that in the year 2020, Lansheng beat Huayu in terms of Preemptive Admission. However, we have to consider a large amount of Huayu Students still manage to enter SHS through the Standardized Test, the data of which I was not able to obtain. Furthermore, if the coefficent of FF and JF is lowered to 0.4 from 0.6, the win distribution of Lansheng does not significantly excel that of Huayu.</p>



<h2 class="wp-block-heading">Summary</h2>



<p>In this blog post, I experimented with infering the posterior admission rate of various middle schools in Shanghai using Bayesian Statistics. I intend to propose an alternative to the Frequentist admission-based-metric for evaluating Chinese Schools. In this model, we can quantify the uncertainty of the values, instead of having just one mean value. Comparing Schools in this perspective is simply calculating the probability that one excels another in the designated norm, namely the &#34;Win function&#34;. We can also compare according to the Worst-Case-Scenarios, where the enrollment of the school is at the theoretical worst.</p>



<h2 class="wp-block-heading">Anglo-Chinese Cross Reference of Technical Terms</h2>



<p>Bayesian Inference: 贝叶斯推断</p>



<p>Prior, Likelihood, Posterior: 先验、似然、后验</p>



<p>Conjugate Prior: 共轭先验</p>



<p>Binomial Distribution: 二项分布</p>



<p>Multinomial Distribution: 多项分布</p>



<p>Preemptive Enrollment/Admission: 自招录取、预录（个人翻译，字面：先发制人地录取）</p>



<p>Latent Dirichlet Allocation (LDA): 隐狄利克雷配分，一种无监督的文档主题模型，其中使用了双层的狄利克雷-多项分布共轭，因而这里提及。</p>



<p>Loss Function: 损失函数</p>



<p>Win Function: 赢函数（赢麻了！）</p>



<p>Confidence Interval: 置信区间</p>]]></description>
  </item>
  <item>
   <title>H2KG-华育知识图谱项目报告（1）</title>
   <link>https://blog.nth.ink/posts/h2kg-project-report-1</link>
   <guid>https://blog.nth.ink/posts/h2kg-project-report-1</guid>
   <pubDate>Sun, 10 Jul 2022 14:31:00 +0000</pubDate>
   <description><![CDATA[<p><strong>H2KG-华育知识图谱项目报告 第一期</strong></p>



<p>本文中所有数据截止2022年7月9日晚上10点。</p>



<p>H2KG项目地址: <a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9oeXdpa2kueHl6" target="_blank">https://hywiki.xyz</a>。</p>



<p>现在是2022年7月9日深夜（写好已经是7月10日凌晨了）。想起刚过去的7月5号，我十分兴奋。这一天，经过一天多讨论的华育知识图谱项目正式开放内测。在此之前，它只是一个腾讯文档，当然初期的网站也好不到哪去，没有写任何css样式，且只有添加数据的功能，连查看都是稍后几小时才有了的。但显然我与21159同学并未达成一致，在朋友圈、QQ天气等渠道进行了多方宣传。可惜的是，当时我的代码还没有经过严格的测试，由于把判断用户输入是否为空放在了获取用户输入之前，大量同学尝试使用却发现我给出了一个“Illegal input”的报错。这让我懊悔不已，并在第二天起床后迅速修改了该问题，当然这也许已经错失了一大部分有创造性的数据。哎，这只能怪罪php这种语言的弱类型等特性了，当然倒过来讲这也增加了程序编写的便捷性。在修改该问题的过程中又引入了一个小错误，但很快被修复，这个程序从此开始正常运转。</p>



<p>7月6日，该项目已经在一定范围内有所传播，数据量也开始快速上涨，这时，反对的声音出现了——最先提出的是21103，之后也有许多同学和校友对部分条目表示异议。迫于压力以及避免项目第二日即出乱子，我紧急下线了“现有数据”功能，留下一句话：“瞎表白的人太多了，先下线此功能。”同时，也加紧研制“举报”功能。在群友的讨论下，我决定将“举报”定为：先立即将该条目在列表中去除，并增设一个“后台管理”功能，其账号密码仅有“H2KG——华育知识图谱计划权限组”群聊的用户可以获得，可以决定对用户的举报是“删除”，还是“驳回”，同学们的意见也逐渐减少，大家也接受了这个项目并参与其中。事实证明“举报”功能在本项目中还是起到了十分积极的作用，虽然也出现了有人对前60条数据进行了无差别举报的闹剧，但后来也不再上演。</p>



<p>早期数据充斥着大量的“A喜欢B”，这完全在我意料之中的，这很好反应了同学们对于传绯闻的孜孜不倦，但也有些背离了本项目的初衷——构建华育的知识体系，转而变成了“实名表白墙”（231直呼内行）。随着此类数据已经被添加得差不多以及举报功能的上线，这种现象有所缓解，新添加的数据也逐步走上正轨。</p>



<p>2022年7月8日，安倍晋三遇刺。在各个社交平台激烈讨论这一重大事件的同时，依然有大量同学在为本项目的数据添砖加瓦并策划新功能，侧面体现了该项目已经具有了一定影响力，这也让我十分兴奋。有趣的是，数据库中也出现了一系列与该事件有关的内容，最典型的包括“24885”“内射”“安倍晋三”等内容，当然一些十分离谱的（包括这条）一出现即被举报或删除。有趣的是，正确的“山上徹也”“刺杀”“安倍晋三”直到9日下午才出现，即便该条消息在8日即广为流传。</p>



<p>这天这个项目也有了许多新的进展。首先是“可视化”功能的上线，把这个项目推上了一个新的高度，图片的各个角落在许多同学的朋友圈中频繁出现，大家都热衷于在图中找到自己所在的位置以及与周边用户的关系。很多原本对枯燥的图表提不起兴趣的同学也开始关注起了这个项目，也很好推广了本项目。图表采用ECharts实现，应当说这属于现在比较好的一个网页图表解决方案了，相比HighCharts可以说并不落后。当然，迫于学习成本过高以及时间有限，我并没有采用更自由、更复杂的D3.js，这虽然使后续开发遇到了一些可自定义程度低的问题，但其实可以接受，真要解决也并不困难。知识图谱其实就是一种力导向图，而这在绝大多数图表库中都有实现，所以我的选项其实很丰富。当然现在碰到的一个问题是性能问题，现在的数据已经越来越丰富，有着约500的节点与约1400的边数，这对于电脑渲染是一个巨大的挑战，在一台配有m3-7y30的低配置上网本中，可以明显发觉缩放、移动等操作极为卡顿，之后非常严重。在更高一些的配置（i5-8250u）中，该问题仍然存在但有所缓解。当然，使用M1 iPad配上Safari浏览器，这样的问题不再出现，但并不能得知在面对之后更为庞大的数据时是否有能力处理，因此可视化图表的性能问题目前亟待解决，我也在研究各类方案，但进展十分缓慢。也许可以随机隐藏一些边？也许可以给予用户更多的筛选选项？或者继续寻找可以驾驭更大型数据集的解决方案？我也正在思考如何给这个问题一个合理的解决方案。现在几千条的数据，仅作简单的数据库处理对于服务器其实毫无压力，只要不添加搜索功能，我认为处理上万条数据应该也不会对服务器有很大困难，实在不行就筹钱买云数据库！</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/h2kg-project-report-1/1657434675-9d52df84-c0be-461d-914d-fcf6d97e6def-e1e91eae.png"><img loading="lazy" decoding="async" loading="lazy" width="3840" height="2159" src="https://blog.nth.ink/media/posts/h2kg-project-report-1/1657434675-9d52df84-c0be-461d-914d-fcf6d97e6def-e1e91eae.png" alt="" class="wp-image-4658"/></a></figure>



<p>在欣赏完可视化图表美观又高级的页面后，回去看首页，真的是没眼看了。对于美的不懈追求使得我迫不及待地想去美化这个页面。然而自己从头写css显然我是懒得弄的，于是我就思考用哪个UI库比较合适。思来想去，我还是使用了比较方便使用又简洁优雅的Semantic-UI，再加上我对一些元素的小改，页面呈现的效果立刻上了一个档次。对了，看这个UI库的名字就知道它是十分有特点的——它是语义化的，用这个真的是一种特别的体验，编辑class就像是在堆砌形容词。我们之前部署的SYZOJ用的也是这个库，所以一些同学对其界面可能感到熟悉。但是好归好，这个库有一个问题就在于它运行性能其实不太好，21159同学反应页面加载时间要增加两至三秒我不清楚是否就由于这个原因，当然，比起庞大的可视化图表那就是小巫见大巫了。</p>



<p>7月9日晚上，我进行了一个熬夜，这段时间又发生了许多事情。比如说22888李雪同学突然情绪崩溃，利用管理员的职务之便将QQ天气、华育语项目的群聊人员全部清除。虽然之后用群恢复功能还是把出去的群员拉回来了，但当时还是因为这件事烦了挺久。为了防止这位同学进行恶意举报，“举报”功能暂时下线，取而代之的一句话是“防火防盗防李雪”，当然第二天见他情绪有所好转便恢复了举报。前两天21159同学一直催促我实现pagerank算法，我便在熬夜期间改了改他发来的Python代码实现了pagerank并放进了“节点列表”中，这个计算会消耗大量服务器资源，甚至会卡住，虽然其实一般来说只要3秒左右即可算完，但每次新添加数据就算显然不现实，于是我用crontab定时每小时进行一次计算，解决了这个问题。但似乎同学们并没有对这个结果感到兴趣，因为最终pr值最高的都是一些抽象的、概括性的词，如“课代表”，而同学们的学号和班级则大多在末尾且并列很多，这并不符合我们的初衷。究其原因，是因为同学们的学号等节点，以流出，即指向别的事物居多。比如说“23564”“担任”“数学课代表”，而“数学课代表”又“属于”“课代表”，与我类似情况的同学很多，因此“课代表”节点的被指向就会很多，其pr值也随之飙升。这样“事物”们的pr值很高也就很好理解了，当然pagerank算法最早使用于搜索引擎对网页进行排名，在我们这里出现这种情况也完全可以理解。显然我们对这个结果并不满意，于是我们想是否可以直接忽略箭头指向，直接将关系都改成双向关系，我写完文章就去试试看。</p>



<p>以上就是关于本项目并不丰富的历史，更多的内容，只能等待尊敬的21159贾知白同学更新他的报告时再补充了。对了，他说到了1000条时就要出分析的，我在此进行一个催更。</p>



<p>关于源代码，我使用的是PHP，毕竟用它写网站是十分便捷的。为了防止当时LandChat出现的一堆漏洞（它们甚至很容易在无意间被触发），我在一开始就做了MySQL预处理查询和html实体字符转义，以防止可能的SQL注入和XSS漏洞。Pagerank计算使用的是Python，我最终还是没有让Python直接操作MySQL，而是直接让PHP调用它并返回结果。肯定有人问为什么没用最爱的C++，显然用C++写网页用到cgicc库，而这是我懒得搞的，且它本身也并不适合。稍后我会上传源码至本文的附件，各位有需要的可以自行下载阅读。</p>



<p>说说关于本项目的展望吧。搜索功能我觉得可以有，但应该只能是精确搜索，模糊搜索在数据太大的情况下对MySQL的压力太大了；在用户输入数据达到更大的数量以后我觉得就要对“已收集数据”页面分页了。还有创建一个自动替换清单，规范化词语的使用，方便分析。一些有趣的功能，比如通过最短路径算法实现“速通”。我们应该也会提升可视化图的观感，气泡大小根据节点的pr值或者出入度决定。性能优化，尤其是可视化页面的，我们一直在想着做，但实在能力有限，可能做不好。实在不行我觉得甚至可以考虑在服务器端进行渲染？当然，pjax也是要做的，这样切换页面就能很流畅啦！</p>



<p>最后，我决定把现在的H2KG<s>公厕</s>公测版上升到RC版（即候选版本），等收集到2000条数据且基本不再发现Bug时，可以正式改为稳定版并大规模推广。本项目还有远期规划，会新增人物档案功能，避免大量不必要的“A担任B”的边出现，淹没一些真正有趣、有价值的数据。更长远的也许会集成21159的Huayu2Vec项目的部分功能，比如常见的“最相关人物分析”，方便电脑上没有Python环境的同学使用，等等。甚至可以不止H2V，与一些别的企划进行联动，真正打造得符合我们的域名——“hywiki.xyz”。我们也热切欢迎各位好的想法。还有一个需要顾虑的点是老师，他们可能会对我们直呼姓名、传播绯闻、在可视化图中被归类为“事物”等事情表示反对，那即使我能顶住压力也会是一个很大的阻碍。在这里我只能恳求各位守口如瓶，先别跟老师分享了。</p>



<p>啊，全文三千多字，感谢你能看到这里。我稍后会增加一些图片以提升阅读体验。对了，本项目还没有logo，需要设计一个。各位辛苦了！</p>]]></description>
  </item>
  <item>
   <title>机械键盘轴体简单科普</title>
   <link>https://blog.nth.ink/posts/mechanical-keyboard-switches</link>
   <guid>https://blog.nth.ink/posts/mechanical-keyboard-switches</guid>
   <pubDate>Mon, 20 Jun 2022 14:13:00 +0000</pubDate>
   <description><![CDATA[<p>最近买的新键盘到了，激动至极，便顺便来<s>死了很久的</s>E站写一篇文章。</p>



<p>话不多说，直入正题。</p>



<p>说明一点，本文所提到的轴体都是最基础的，按照老大哥Cherry的产品进行分类，但随着数年前Cherry MX轴的专利到期，国产轴如雨后春笋般冒出，再加上Cherry的模具老化，当前国产轴已经是全方位吊打Cherry轴的存在了，当然Cherry鼻祖的地位还是无法撼动的。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708360-image-6226c8ec.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708360-image-6226c8ec.png" alt="" class="wp-image-4309" width="480" height="705"/></a><figcaption>结构图</figcaption></figure>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708436-image-fcfa3c8a.png"><img loading="lazy" decoding="async" loading="lazy" width="448" height="497" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708436-image-fcfa3c8a.png" alt="" class="wp-image-4310"/></a></figure>



<h2 class="wp-block-heading">红轴</h2>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708179-image-4204c697.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708179-image-4204c697.png" alt="" class="wp-image-4301" width="150" height="150"/></a></figure>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655700761-image-34de3dd5.png"><img loading="lazy" decoding="async" loading="lazy" width="195" height="150" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655700761-image-34de3dd5.png" alt="" class="wp-image-4271"/></a></figure>



<p>可以说是最中庸的存在了。线性轴，直上直下；45g触发，中等；2mm触发，4mm触底，都是最平常的状态。</p>



<p>因为没什么特点，所以类似的太多了。基本的有TTC金红，凯华Box红，佳达隆G黄Pro、G红Pro。</p>



<h2 class="wp-block-heading">黑轴</h2>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708217-image-68fe8dc6.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708217-image-68fe8dc6.png" alt="" class="wp-image-4303" width="150" height="150"/></a></figure>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655700914-image-1ee701b1.png"><img loading="lazy" decoding="async" loading="lazy" width="195" height="150" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655700914-image-1ee701b1.png" alt="" class="wp-image-4272"/></a></figure>



<p>线性轴，习武之人专用，60g触发，90g触底，偏重，给指力足的人用。其它还是跟上面一样，2mm触发，4mm触底。</p>



<p>太重了，国内类似的不太好找。</p>



<h2 class="wp-block-heading">茶轴</h2>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708227-image-d2c98c40.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708227-image-d2c98c40.png" alt="" class="wp-image-4305" width="150" height="150"/></a></figure>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701009-image-eb8596c6.png"><img loading="lazy" decoding="async" loading="lazy" width="195" height="150" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701009-image-eb8596c6.png" alt="" class="wp-image-4273"/></a></figure>



<p>无声段落轴，段落感自然，属于Cherry少有的还很难被超越的轴，55g，2mm，4mm。容易上手，有各种轴体的特色但都只有一点点，适合第一次使用。</p>



<p>类似的有TTC金茶，BOX茶V1（V2不是）。</p>



<h2 class="wp-block-heading">青轴</h2>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708239-image-f08662c4.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708239-image-f08662c4.png" alt="" class="wp-image-4306" width="150" height="150"/></a></figure>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701229-image-2d34b72c.png"><img loading="lazy" decoding="async" loading="lazy" width="195" height="150" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701229-image-2d34b72c.png" alt="" class="wp-image-4274"/></a></figure>



<p>有声段落轴，非常有特色，最有机械键盘的感觉，那种咔哒咔哒的感觉，清脆，但声音很大<s>，容易被室友暴打</s>。</p>



<p>类似的有凯华Box白，TTC兄弟轴。</p>



<h2 class="wp-block-heading">银轴</h2>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708201-image-f1af1e71.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708201-image-f1af1e71.png" alt="" class="wp-image-4302" width="150" height="150"/></a></figure>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701413-image-a0b7318f.png"><img loading="lazy" decoding="async" loading="lazy" width="195" height="150" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701413-image-a0b7318f.png" alt="" class="wp-image-4275"/></a></figure>



<p>线性轴，和上面的茶轴一样也是Cherry比较优秀的轴体之一，但近几年被ttc快银超越了。这也是特色十分明显的轴，一个字：快。极快的触发（1.2mm），极短的键程（3.4mm），也有较强的回弹，适合大部分游戏，需要一定习惯时间，容易误触。</p>



<p>类似的是TTC快银，佳达隆G银Pro。</p>



<h2 class="wp-block-heading">静音红/静音黑轴</h2>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708166-image-6a0e3cc7.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708166-image-6a0e3cc7.png" alt="" class="wp-image-4300" width="150" height="150"/></a></figure>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701757-image-a97dfbb5.png"><img loading="lazy" decoding="async" loading="lazy" width="195" height="150" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655701757-image-a97dfbb5.png" alt="" class="wp-image-4276"/></a></figure>



<p>静音轴，声音确实轻，会牺牲一定手感（偏肉），略贵。</p>



<p>类似的有TTC静音红、Box静音红</p>



<p></p>



<p>还有一些Cherry的特殊轴体：</p>



<p>绿轴：有声段落轴，很重，80g</p>



<p>还有白轴、奶轴等，不介绍了</p>



<h2 class="wp-block-heading">优秀的国产轴体</h2>



<p>除了上面那些Cherry的轴体为我们提供一个大概的分类，国内厂商也做出了大量有着各种特色的优秀轴体。主要的是三家。</p>



<p>当然，国产厂家的优势是更多的选择、更好的顺滑度等等等等，当然缺点就是寿命差了点。</p>



<h3 class="wp-block-heading">TTC</h3>



<p>以高端轴体为主，大多数产品都是真的很棒，也是真的不便宜。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655702848-image-39f949b4.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655702848-image-39f949b4.png" alt="" class="wp-image-4277" width="150" height="150"/></a></figure>



<p>金粉轴，超级有名，轻压力的线性轴，不便宜，当然现在v1降到2.3左右还是很有性价比的。</p>



<p>因为加长弹簧，所以即使十分轻柔（37g），依然有着十分有力的回弹。需要注意的是，它的声音不小。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655702877-image-48b8763f.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655702877-image-48b8763f.png" alt="" class="wp-image-4278" width="254" height="211"/></a></figure>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655704940-image-467ac072.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655704940-image-467ac072.png" alt="" class="wp-image-4280" width="150" height="150"/></a></figure>



<p>快银轴，基本上全方位比C银强了，45g的力也修复了C银过于容易误触的问题，宣传是适合游戏，当然打字办公习惯后也是很爽的。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705084-image-a688eb90.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705084-image-a688eb90.png" alt="" class="wp-image-4283" width="205" height="173"/></a></figure>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655704973-image-172d6842.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655704973-image-172d6842.png" alt="" class="wp-image-4281" width="150" height="150"/></a></figure>



<p>月白轴，拿出来讲是因为它又是新的一类：提前大段落轴（类hp，类圣熊猫），就是刚开始按就会有一个大的段落。争议很大，有人喜欢，有人讨厌，有点像薄膜键盘，据说适合做大键，不太能全键位使用。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705019-image-685b1302.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705019-image-685b1302.png" alt="" class="wp-image-4282" width="237" height="205"/></a></figure>



<p>剩下的太多了，不详细介绍。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655707868-image-e50b0e71.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655707868-image-e50b0e71.png" alt="" class="wp-image-4290" width="150" height="150"/></a></figure>



<p>ACE轴：因为读音而被俗称为屁股轴，可以看成重压力更强会谈版本的金粉，52g，适合做大键（太轻回弹不够强的轴不适合做大键，尤其是空格）。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655707889-image-517e1166.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655707889-image-517e1166.png" alt="" class="wp-image-4291" width="150" height="150"/></a></figure>



<p>Wild轴：有42g/55g两个版本，也可以看作是金粉升级版，但被炒得贵了。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708113-image-a65d5aa1.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708113-image-a65d5aa1.png" alt="" class="wp-image-4298" width="150" height="150"/></a></figure>



<p>烈艳红轴：45g，同样是类金粉，更Q弹。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708007-image-672464d4.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708007-image-672464d4.png" alt="" class="wp-image-4294" width="150" height="150"/></a></figure>



<p>虎轴：虎年限定版，有收藏价值，快银升级版，贵。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708060-image-58bce5a2.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655708060-image-58bce5a2.png" alt="" class="wp-image-4297" width="150" height="150"/></a></figure>



<p>爱心轴：乐高结构，十分有趣的。</p>



<p>还有很多很多，但以压力的轻微改变与换色为主，就不细说了。</p>



<p>静音的有冰静轴（静音金粉），白静轴（静音月白），茶静轴（带防尘壁的静音茶），静音红等。</p>



<h3 class="wp-block-heading">佳达隆（Gateron）</h3>



<p>最主要的是GPro系列里，特点就是性价比超级高。厚润滑以后确实很不错，顺滑，但轴芯没那么稳。</p>



<figure class="wp-block-image size-full is-resized"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705287-image-f7f48c0d.png"><img loading="lazy" decoding="async" loading="lazy" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705287-image-f7f48c0d.png" alt="" class="wp-image-4284" width="810" height="2044"/></a></figure>



<p>官方给了图，就不用我动手了，哈哈。</p>



<p>它还有cap轴、CJ轴、ink轴等高端产品线，在此不多介绍。</p>



<h3 class="wp-block-heading">凯华（Kailh）</h3>



<p>有趣的是，这家和TTC两家公司都是在惠州的，不知道为啥。特色是box轴，是自己创新的结构，将触点放在专门的腔内，防水防尘，提升耐用性，轴芯稳定性也是不错的。</p>



<p>不想写了，摆烂了</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705611-image-0fe55aed.png"><img loading="lazy" decoding="async" loading="lazy" width="1972" height="1521" src="https://blog.nth.ink/media/posts/mechanical-keyboard-switches/1655705611-image-0fe55aed.png" alt="" class="wp-image-4286"/></a></figure>



<h3 class="wp-block-heading">其它</h3>



<p>当然，上面三家还有大量的产品未被提及，可以上各家的官网或者淘宝店看。</p>



<p>其它的还有jwk（精微科）、ktt（康腾特）、高特、环诺等，再别的就真的啥都不是了。</p>



<p>有些键盘品牌或各种知名up或商家等也有大量优秀的定制轴体（当然是找上面提到的几家代工的），因此其实这一块也是十分丰富的。</p>



<h2 class="wp-block-heading">总结</h2>



<p>如果买第一把，且不嫌吵，可以买类青轴来体验一下这种感觉。</p>



<p>如果想长期使用，就可以买各种类红轴还有各种国产线性轴。</p>



<p>如果实在选择困难，就买什么手感都带一点的茶轴吧。</p>



<p>除非真的有静音需求，否则不建议买静音轴，手感妥协太多了。</p>



<p>讲了那么多，其实没有必要那么追求最高端的轴体，体验最明显的无非是有没有段落（就是按下去有一段会顶着手指），以及压力克数。颜色就更不用关注了，除非你是透壳，反正根本看不到。</p>



<p>现在有热插拔的键盘很多，可以直接把轴拔下来换，非常方便，没有的就得自己焊。当然切忌粗莽操作。轴也可以混合使用，当然新手不推荐。还有三脚轴和五脚轴，三脚轴兼容性更好，当然只要pcb上开了五脚轴的孔就都可以用。</p>



<h2 class="wp-block-heading">其它非机械轴（想到就说了）</h2>



<h3 class="wp-block-heading">静电容</h3>



<p>国产的NIZ，超贵的RealForce和HHKB等</p>



<p>据说是高级薄膜手感</p>



<h3 class="wp-block-heading">光轴</h3>



<p>结构比较有趣，大概是和光的折射反射有关。比较有名的是ROG的RX光红/光蓝。</p>



<p></p>



<p>超级长文，写了近2h，我用过的也就一小部分，剩余来自他人分享或官网资料。说实话，在今天新的这把之前也是有一段时间没用过机械键盘啦。</p>



<p></p>



<p></p>]]></description>
  </item>
  <item>
   <title>如何使用相机拍摄到月亮</title>
   <link>https://blog.nth.ink/posts/photographing-the-moon</link>
   <guid>https://blog.nth.ink/posts/photographing-the-moon</guid>
   <pubDate>Fri, 17 Jun 2022 09:41:00 +0000</pubDate>
   <description><![CDATA[<p>各位同学们大家好，实在不知道写什么了，就先水一篇吧。最近看到不少同学在拍月亮，我也来凑凑热闹。</p>



<p>首先在镜头的选择方面，焦段最最少200mm了，我105mm的镜头拍出来的月亮只有一个点。当然，400mm、600mm等是最好的。</p>



<p>其次是曝光，这我觉得有点小坑。拍月亮一定要使用手动模式而不是自动。自动测光一般是平均测光，你拍摄时旁边大片的黑夜与中间有一个很亮的月亮平均下来还是暗的，因此会认为你在拍很暗的事物，于是将ISO拉的很高，快门速度很慢，光圈开满，结果却发现天空噪点爆炸，亮部，也就是月亮，细节全部丢失，看上去就像一个发光的球。我后来使用的参数是ISO640，1/500s，f/8.0，得到了下方的图像，可以看出除了不清晰（没办法，焦段不够）以外没什么问题，背景的黑夜也很干净。如果实在不会自行调整，也请将自动测光模式更换成点测光，对准月亮。我曾经就是自动测光，然后发现还没有手机拍得好，细节全无，想来想去才想到是曝光的问题。</p>



<p>还有一点是白平衡，这个需要自己调整，因为黑夜中没有很好的参照，自动白平衡一般不能很好工作，容易把金色的月亮拍成白色。</p>



<p>最后是三脚架，这个倒是不必要，因为根据前条其实1/500s足矣，手持完全没问题。当然也可以打开反光镜预升来防止振动（微单不需要，因为没有反光镜）。当然，如果拍摄的不只是月亮而是星空，那三脚架和一个大光圈的镜头还是很必要的。</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/photographing-the-moon/1655429511-img_1471-938c9e46.jpg"><img loading="lazy" decoding="async" loading="lazy" width="720" height="480" src="https://blog.nth.ink/media/posts/photographing-the-moon/1655429511-img_1471-938c9e46.jpg" alt="" class="wp-image-4265"/></a><figcaption>564前天拍到的。经过大幅度的裁切。</figcaption></figure>]]></description>
  </item>
  <item>
   <title>HYRing项目代码解读</title>
   <link>https://blog.nth.ink/posts/hyring-code-walkthrough</link>
   <guid>https://blog.nth.ink/posts/hyring-code-walkthrough</guid>
   <pubDate>Mon, 13 Jun 2022 14:59:00 +0000</pubDate>
   <description><![CDATA[<p>各位同学们大家好，今天我来和大家讲讲HYRing。</p>



<p>Github地址：<a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9naXRodWIuY29tL2VyaWMtbnRoL0hZUmluZw==" target="_blank">eric-nth/HYRing: Huayu Ringtone Software 2022 (github.com)</a></p>



<h2 class="wp-block-heading">README.md</h2>



<p>Huayu Ringtone Software 2022</p>



<h3 class="wp-block-heading"><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9naXRodWIuY29tL2VyaWMtbnRoL0hZUmluZyMlRTclOUIlQUUlRTclOUElODQ=" target="_blank"></a>目的</h3>



<p>使用 Visual Studio Community 2022 编写，目的是解决2022年上海市民办华育中学网课期间电脑铃声的问题。</p>



<p>若下载缓慢，请访问：<a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9ibG9nLm50aC5pbmsvb3MtYW5kLXNvZnR3YXJlL2FwcC1pbnN0YWxsYXRpb24vUDM5ODEuaHRtbA==" target="_blank">HYRing铃声系统2022发布 - Ericnth的小站</a></p>



<h3 class="wp-block-heading"><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9naXRodWIuY29tL2VyaWMtbnRoL0hZUmluZyMlRTclODklODglRTYlOUQlODMlRTUlQTMlQjAlRTYlOTglOEU=" target="_blank"></a>版权声明</h3>



<p>本软件遵循GPLv3协议开源。x64/Release/rings文件夹下的*.mp3音乐文件除外，归版权方所有，此处仅供测试，如有侵权请联系删除。</p>



<h3 class="wp-block-heading"><a href="https://blog-wp.nth.ink/l/?dest=aHR0cHM6Ly9naXRodWIuY29tL2VyaWMtbnRoL0hZUmluZyMlRTUlOEElOUYlRTglODMlQkQlRTclODklQjklRTYlODAlQTc=" target="_blank"></a>功能特性</h3>



<p>可根据配置文件所指定的时间表与铃声进行打铃，使用winmm库，支持mp3格式的音频。</p>



<p>支持通过窗口置顶、半透明化来实现上课期间的使用。</p>



<p>使用传统Win32编写，因此应该有较高的兼容性。</p>



<p>不足之处是只能指定星期几来打铃，无法解决放假、调休的情况。</p>



<h2 class="wp-block-heading">工作区合影</h2>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101371-image-836e7987.png"><img loading="lazy" decoding="async" loading="lazy" width="1920" height="1020" src="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101371-image-836e7987.png" alt="" class="wp-image-4177"/></a></figure>



<p>依赖：kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies)</p>



<h2 class="wp-block-heading">资源文件（Resource.rc）</h2>



<p>图标：（由 23786 制作）</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101545-256-a05d62ef.png"><img loading="lazy" decoding="async" loading="lazy" width="256" height="256" src="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101545-256-a05d62ef.png" alt="" class="wp-image-4178"/></a></figure>



<p>项目属性：（会体现在生成的可执行文件的属性里）</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101639-image-8b28113d.png"><img loading="lazy" decoding="async" loading="lazy" width="1249" height="618" src="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101639-image-8b28113d.png" alt="" class="wp-image-4181"/></a></figure>



<p>字符串：（写在这里是为了规避可恶的char和WCHAR的问题，同时避免因为字符编码而乱码）</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101689-image-5f90d218.png"><img loading="lazy" decoding="async" loading="lazy" width="846" height="148" src="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101689-image-5f90d218.png" alt="" class="wp-image-4182"/></a></figure>



<p>菜单：</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101762-image-b4aecd60.png"><img loading="lazy" decoding="async" loading="lazy" width="276" height="155" src="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101762-image-b4aecd60.png" alt="" class="wp-image-4183"/></a></figure>



<p>帮助对话框：</p>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101811-image-e2743154.png"><img loading="lazy" decoding="async" loading="lazy" width="436" height="278" src="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101811-image-e2743154.png" alt="" class="wp-image-4184"/></a></figure>



<h2 class="wp-block-heading">HYRing.h（头文件与宏定义）</h2>



<figure class="wp-block-image size-full"><a href="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101881-image-42575150.png"><img loading="lazy" decoding="async" loading="lazy" width="1251" height="646" src="https://blog.nth.ink/media/posts/hyring-code-walkthrough/1655101881-image-42575150.png" alt="" class="wp-image-4185"/></a></figure>



<h2 class="wp-block-heading">HYRing.cpp（程序主体）</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="cpp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">// HYRing.cpp : 定义应用程序的入口点。
//

#include &#34;framework.h&#34;
#include &#34;HYRing.h&#34;

#define MAX_LOADSTRING 100

// 全局变量:
HINSTANCE hInst;                                // 当前实例
WCHAR szTitle[MAX_LOADSTRING];                  // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING];            // 主窗口类名
WCHAR szWelcomeText[MAX_LOADSTRING];            // 欢迎使用
WCHAR szMainText[MAX_LOADSTRING];               // 主字符串
HFONT winFont;                                  // 窗口字符字体
BOOL  winTopped;                                // 窗口置顶选择
BOOL  winTransparent;                           // 窗口半透明选择
HANDLE hMonitorThread;
HANDLE hWinTopThread;
WCHAR szReadBlock[2048] = { 0 } ;
HWND hMainText;
WORD ringLoopEnd = 0;
BOOL stopRingFlag = 0;
OPENFILENAME ofn;
WCHAR szLoadCfgFileName[MAX_PATH];

// 此代码模块中包含的函数的前向声明:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK    About(HWND, UINT, WPARAM, LPARAM);
VOID CDECL          TimeMonitorProc(LPVOID);
VOID CDECL          WinTopperProc(LPVOID);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // TODO: 在此处放置代码。

    // 初始化全局字符串
    LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
    LoadStringW(hInstance, IDS_WELCOME, szWelcomeText, MAX_LOADSTRING);
    LoadStringW(hInstance, IDS_MAINTEXT, szMainText, MAX_LOADSTRING);
    LoadStringW(hInstance, IDC_HYRING, szWindowClass, MAX_LOADSTRING);
    MyRegisterClass(hInstance);

    // 执行应用程序初始化:
    if (!InitInstance (hInstance, nCmdShow))
    {
        return FALSE;
    }

    HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_HYRING));

    MSG msg;


    // 主消息循环:
    while (GetMessage(&amp;msg, nullptr, 0, 0))
    {
        if (!TranslateAccelerator(msg.hwnd, hAccelTable, &amp;msg))
        {
            TranslateMessage(&amp;msg);
            DispatchMessage(&amp;msg);
        }
    }

    return (int) msg.wParam;
}



//
//  函数: MyRegisterClass()
//
//  目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_HYRING));
    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_HYRING);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&amp;wcex);
}

//
//   函数: InitInstance(HINSTANCE, int)
//
//   目标: 保存实例句柄并创建主窗口
//
//   注释:
//
//        在此函数中，我们在全局变量中保存实例句柄并
//        创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; // 将实例句柄存储在全局变量中

   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}</pre>



<p>前面这段在Win32开发中可以说是套话了。基本没有什么值得讲的地方。开头为引用头文件，进行全局变量定义与函数声明。UNREFERENCED_PARAMETER宏用来避免触发参数未使用的警告。WinMain是程序入口点，传入参数，读入前面设置的字符串，设置窗口参数，初始化窗口实例，读入快捷键，开始主消息循环（获取并转译窗口发来的消息后交给WndProc处理）。</p>



<h3 class="wp-block-heading">WndProc</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="cpp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LONG winStyle;
    switch (message)
    {
    case WM_CREATE:
        //创建控件
        CreateWindow(_T(&#34;STATIC&#34;), szWelcomeText, WS_CHILD | WS_VISIBLE, 10, 10, 350, 20, hWnd, (HMENU)IDM_WELCOME, GetModuleHandle(NULL), NULL);
        hMainText = CreateWindow(_T(&#34;STATIC&#34;), szMainText, WS_CHILD | WS_VISIBLE, 10, 35, 350, 40, hWnd, (HMENU)IDM_MAINTEXT, GetModuleHandle(NULL), NULL);
        //设置字体
        //winFont = CreateFont(20, 0, 0, 0, 0, FALSE, FALSE, 0, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T(&#34;Microsoft YaHei UI&#34;));
        winFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
        SendDlgItemMessage(hWnd, IDM_WELCOME, WM_SETFONT, (WPARAM)winFont, MAKELPARAM(TRUE, 0));
        SendDlgItemMessage(hWnd, IDM_MAINTEXT, WM_SETFONT, (WPARAM)winFont, MAKELPARAM(TRUE, 0));
        //启动线程
        hMonitorThread = (HANDLE)_beginthread(TimeMonitorProc, 0, NULL);
        //hWinTopThread = (HANDLE)_beginthread(WinTopperProc, 0, (LPVOID)hWnd);
        winTopped = 1;
        winTransparent = 1;
        //窗口及风格设置
        SetWindowPos((HWND)hWnd, HWND_TOPMOST, 0, 0, 400, 180, 0);
        winStyle = GetWindowLong(hWnd, GWL_STYLE);
        winStyle &amp;= ~(WS_MAXIMIZEBOX) &amp; ~WS_THICKFRAME;
        SetWindowLong(hWnd, GWL_STYLE, winStyle);
        winStyle = GetWindowLong(hWnd, GWL_EXSTYLE);
        winStyle ^= WS_EX_LAYERED;
        SetWindowLong(hWnd, GWL_EXSTYLE, winStyle);
        SetLayeredWindowAttributes(hWnd, 0, 200, LWA_ALPHA);
        break;
    case WM_COMMAND:
        {
            int wmId = LOWORD(wParam);
            // 分析菜单选择:
            switch (wmId)
            {
            case IDM_ABOUT:
                DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
                break;
            case IDM_TOP:
                CheckMenuItem(GetSubMenu(GetMenu(hWnd), 0), 0, MF_BYPOSITION | (winTopped ? MF_UNCHECKED : MF_CHECKED));
                winTopped = !winTopped;
                if (winTopped) {
                    SetWindowPos((HWND)hWnd, HWND_TOPMOST, 0, 0, 400, 180, 0);
                }
                else {
                    SetWindowPos((HWND)hWnd, HWND_NOTOPMOST, 0, 0, 400, 180, 0);
                }
                break;
            case IDM_LOADCFG: 
                memset(&amp;ofn, 0, sizeof(OPENFILENAME));
                memset(szLoadCfgFileName, 0, sizeof(WCHAR) * MAX_PATH);
                ofn.lStructSize = sizeof(OPENFILENAME);
                ofn.lpstrFilter = L&#34;HYRing Config (.cfg)\0*.cfg&#34;;
                ofn.lpstrFile = szLoadCfgFileName;
                ofn.nMaxFile = MAX_PATH;
                ofn.Flags = OFN_FILEMUSTEXIST;
                if (GetOpenFileName(&amp;ofn)) {
                    //MessageBox(NULL, szLoadCfgFileName, L&#34;即将打开配置文件...&#34;, NULL);
                    ringLoopEnd = 1;
                }
                else {
                    break;
                }
                Sleep(1000);
                ringLoopEnd = 2;
                Sleep(500);
                hMonitorThread = (HANDLE)_beginthread(TimeMonitorProc, 0, NULL);
                break;
            case IDM_STOPRING:
                stopRingFlag = 1;
                break;
            case IDM_TOGGLE_TRANSPARENCY:
                CheckMenuItem(GetSubMenu(GetMenu(hWnd), 1), 2, MF_BYPOSITION | (winTransparent ? MF_UNCHECKED : MF_CHECKED));
                winTransparent = !winTransparent;
                if (winTransparent) {
                    SetLayeredWindowAttributes(hWnd, 0, 200, LWA_ALPHA);
                }
                else {
                    SetLayeredWindowAttributes(hWnd, 0, 255, LWA_ALPHA);
                }
                break;
            case IDM_EXIT:
                DestroyWindow(hWnd);
                break;
            default:
                return DefWindowProc(hWnd, message, wParam, lParam);
            }
        }
        break;
    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hWnd, &amp;ps);
            // TODO: 在此处添加使用 hdc 的任何绘图代码...
            EndPaint(hWnd, &amp;ps);
        }
        break;
    case WM_CLOSE:
        if (MessageBox(hWnd, _T(&#34;确认要退出HYRing？&#34;), _T(&#34;HYRing - Close&#34;), MB_OKCANCEL) == IDOK)
            SendMessage(hWnd, WM_DESTROY, 0, 0);
        break;
    case WM_DESTROY:
        //CloseHandle(hMonitorThread);
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}</pre>



<p>大名鼎鼎，有或者说是臭名昭著的WndProc，这是Win32程序设计中一个十分不优雅，又是非常主要的函数，它负责接受窗口发来的消息，通过一个switch，来根据消息编号进行用户指定的操作，如果用户没有指定（也就是到了default），就会调用DefWindowProc来进行系统默认的操作。我认为这比起Qt中信号-槽的机制与其它图形界面开发中常见的事件机制要傻上不少。况且，在WndProc中进行的操作不能是阻塞的，否则会阻止下一个消息处理的开始，导致应用未响应。wParam和lParam是为事件传入的参数。</p>



<p>首先是WM_CREATE，代表窗口被创建，我在此处进行了绘制应用界面的操作（因为我不知道如何在主窗口中使用可视化的控件编辑器），通过CreateWindow创建控件，通过SendDlgItemMessage发送字体设置，设置窗口置顶与半透明度，以及启动本程序中最重要的线程TimeMonitorProc（这就是我写的打铃程序，为了避免播放时阻塞程序运行，我把它放进了一个单独的线程里运行，虽然之后这也带来了许多麻烦）。</p>



<p>WM_COMMAND则一般用来处理菜单栏点击的操作。</p>



<p>WM_CLOSE与WM_DESTROY用于处理窗口关闭时的情况。</p>



<h3 class="wp-block-heading">TimeMonitorProc</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="cpp" data-enlighter-theme="" data-enlighter-highlight="" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-title="" data-enlighter-group="">VOID CDECL TimeMonitorProc(LPVOID lpParam) {
    time_t rawtime;
    struct tm timeinfo;
    time(&amp;rawtime);
    localtime_s(&amp;timeinfo, &amp;rawtime);

    WCHAR szCfgFileName[260] = {0};

    if (ringLoopEnd == 2) {
        wsprintf(szCfgFileName, L&#34;%s&#34;, szLoadCfgFileName);
    }
    else {
        wsprintf(szCfgFileName, L&#34;./rings/%d.cfg&#34;, timeinfo.tm_wday);
    }
    
    WCHAR szMp3PlayCommand[330] = { 0 };
    FILE* pCfgFile = NULL;
    _wfopen_s(&amp;pCfgFile, szCfgFileName, L&#34;r&#34;);
    if (pCfgFile == 0) {
        MessageBox(NULL, L&#34;File Open Error&#34;, L&#34;HYRing&#34;, 0);
        PostQuitMessage(0);
        _endthread();
    }
    int iHrs[50] = {0}, iMin[50] = {0}, iPtr = 0, iSFlag/*是否指定显示内容*/;
    WCHAR szRingName[260][50] = {0};
    int iRingCount = 0;
    WCHAR szMainTextCopy[300] = {0};

    for (int i = 0; i &lt; 50; i++) {
        fwscanf_s(pCfgFile, L&#34;%d:%d %s\n&#34;, &amp;(iHrs[i]), &amp;(iMin[i]), szRingName[i], 260);
        if (iHrs[i] == -1 || iMin[i] == -1) {
            iRingCount = i;
            break;
        }
    }

    while (1) {
        if (ringLoopEnd == 1) {
            _fcloseall();
            _endthread();
            return;
        }
        if (stopRingFlag) {
            stopRingFlag = 0;
            wsprintf(szMp3PlayCommand, L&#34;stop ring%d&#34;, iPtr - 1);
            mciSendString(szMp3PlayCommand, NULL, 0, NULL);
            MessageBox(NULL, L&#34;铃声已停止！&#34;, L&#34;HYRing&#34;, 0);
        }
        
        iSFlag = 0;
        time(&amp;rawtime);
        localtime_s(&amp;timeinfo, &amp;rawtime);
        for (; iPtr &lt; iRingCount; iPtr++) {
            if (iHrs[iPtr] &gt; timeinfo.tm_hour) {
                break;
            }
            if (iHrs[iPtr] == timeinfo.tm_hour) {
                if (iMin[iPtr] &gt;= timeinfo.tm_min) {
                    break;
                }
            }
            if (iPtr &gt;= iRingCount - 1) {
                iSFlag = 1;
                wsprintf(szMainTextCopy, L&#34;今日无更多闹铃&#34;);
                break;
            }
        }
        if (iPtr &gt; iRingCount - 1) {
            iSFlag = 1;
            wsprintf(szMainTextCopy, L&#34;今日无更多闹铃&#34;);
        }
        if (iHrs[iPtr] == timeinfo.tm_hour &amp;&amp; iMin[iPtr] == timeinfo.tm_min) {
            //Ring!
            iSFlag = 1;
            wsprintf(szMainTextCopy, L&#34;正在闹铃: %s&#34;, szRingName[iPtr]);
            //PlaySound(szRingName[iPtr], NULL, SND_FILENAME | SND_ASYNC);
            wsprintf(szMp3PlayCommand, L&#34;open \&#34;./rings/%s.mp3\&#34; alias ring%d&#34;, szRingName[iPtr], iPtr);
            mciSendString(szMp3PlayCommand, NULL, 0, NULL);
            wsprintf(szMp3PlayCommand, L&#34;play ring%d&#34;, iPtr);
            mciSendString(szMp3PlayCommand, NULL, 0, NULL);
            iPtr++;
        }
        if (!iSFlag) {
            wsprintf(szMainTextCopy, szMainText, timeinfo.tm_wday, iRingCount, iHrs[iPtr], iMin[iPtr], iPtr + 1, szRingName[iPtr]);
        }
        SetWindowText(hMainText, szMainTextCopy);
        Sleep(500);
    }
    _fcloseall();
    _endthread();
}</pre>



<p>2-5行：获取时间。</p>



<p>7-18行：创建配置文件输入流。</p>



<p>24-35行：根据格式读入当日打铃时间与铃声文件名。</p>



<p>43-48行：当点击UI中“停止铃声”，停止正在播放的铃声。</p>



<p>54-71行：通过对比得到当前即将需要打的铃声。</p>



<p>75-76行：声音播放代码。76行是winmm方式（支持mp3），75行预留了windows的原生方式（仅支持wav，但wav是不压缩的导致文件非常大）。</p>



<p>83-86行：将当前进度同步到UI的对话框上。</p>



<h2 class="wp-block-heading">后记</h2>



<p>主要的代码就这些，能看到这里真是辛苦大家了！</p>



<p>Win32开发真的是槽点多多，但又好处多多。槽点是真的很复杂，真的很麻烦，真的很难debug，很难找到资料，不能跨系统；好处是文件小（本次项目的exe文件仅60KB），运行无压力（占用系统资源极少，本次项目仅使用2MB不到的RAM，远不到1%的CPU），兼容性不错（本软件暂时还没有碰到过此类问题，除了祖传的高DPI兼容性），没有做不到的。以往一直坚持Win32的我可能之后也要转向Qt了。</p>



<p>之后再见，先去填WWDC的坑了。</p>]]></description>
  </item>
 </channel>
</rss>