Location via proxy:   
[Report a bug]   [Manage cookies]                

Lahの部屋

落書き帳です。見たい人は見てください。

【将棋AI】KIF→SFEN 変換プログラムを作った

経緯

 今週から将棋AI作りを始めた。以前も取り組んでいた時期があったのだが、将棋盤GUIとの連携などでしんどくなってやめてしまった。

 学習データを準備するべくデータベース(Shogi DB2 - 無料の棋譜サービス 将棋DB2)を見てみると、将棋AI界で標準とされているSFEN形式でダウンロードできないではないか。仕方がないので、KIF形式からSFEN形式への変換プログラムを作成した。

 

概要

 要するに、「1 2六歩(27) (00:00/00:00:00)」→「2g2f」の変換を行う。

 実装はC#で行った。Visual Basicなどをインストールすればすぐにビルドができるので、よければ使ってみてほしい。

 ネットに公開したときに便利だろうと、最初はPowershellで作ろうと思っていた。しかし、意外と複雑な処理が求められたためC#に切り替えた。

 ソースコードは長いので最後に載せる。

 

使い方

 アプリを任意のフォルダに配置し、そのフォルダでコマンドプロンプトを開く。

 コマンドプロンプト上で "KIF2SFEN > test.txt" を実行する。

 アプリ実行中になると、こんな画面がでる。

 棋譜を貼り付ける前に "s" と一文字打って実行(Enter)する。これによって、出力テキストファイルに "startpos" という文字が出力されるので、対局開始の表示になる。

 将棋DB2Shogi DB2 - 無料の棋譜サービス 将棋DB2)からKIF形式のソースコードをコピーし、そのまま貼り付けてEnterキーを押す。

 続けて棋譜を入力したいときには、"s"コマンドを実行していったん区切ってから、次の棋譜を入力する。

 使い終わったら"quit"コマンドでアプリを終了できる。

 同じフォルダに出力されている test.txt を見てみると、棋譜がSFEN形式で出力されている。

 

コマンド一覧

  • s : "startpos"を結果に出力する。棋譜棋譜の間にはこれを実行して区切る。
  • KIF表現 : 将棋DB2の表現に則り、先頭が数字である文字列をこれと認識する。
  • quit : アプリを終了する。
  • その他 : 上記以外の文字列はすべて無視される。

 

なぜKIF形式なのか

 将棋DB2KIF形式のほかにもCSA形式KI2形式に対応している。これらの中からKIF形式を選んだ理由も記載しておく。

 KIF形式で最も処理上優れていると思ったのは、駒打ちを明示してくれるところである。6五に移動できる銀があったとしても、「6五銀打」と表現してくれる。また、移動前の駒の位置を明示してくれる点も、SFENと通ずる。

 

ソースコード


using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml.XPath;
using System.Text.RegularExpressions;

namespace KIF2SFEN
{
    internal class Program
    {
        static void Main(string[] args)
        {
            string cmd;
            // "同○○"の記法に対応するためループ外で定義
            int destX = 0; //駒移動の終点X(1~9)
            int destY = 0; //駒移動の終点Y(1~9)
            while ((cmd = Console.ReadLine()) != null)
            {
                string[] kif = cmd.Split(' '); // KIFコマンド
                // アプリを終了するコマンド
                if (cmd == "quit")
                {
                    break;
                }
                // 対局同士を区切るためのコマンド
                else if (cmd == "s")
                {
                    Console.WriteLine("startpos");
                }
                // メインとなる変換処理
                // 先頭が数字でない行は無視する
                else if (Regex.IsMatch(kif[0], @"^\d+$"))
                {
                    int originX = 0; //駒移動の起点X(1~9)
                    int originY = 0; //駒移動の起点Y(1~9)
                    char piece = ' '; // 駒種
                    Boolean isPromoted; // 成り移動の場合のみTRUE
                    string result = ""; // 結果
                    // 投了の場合
                    if (kif[1] == "投了")
                    {
                        result = "resign";
                    }
                    // 駒打ちの場合
                    else if (kif[1].Contains("打"))
                    {
                        // 先手番の場合
                        if (int.Parse(kif[0]) % 2 == 1)
                        {
                            // 打たれた駒種の判別
                            string koma = new StringInfo(kif[1]).SubstringByTextElements(2, 1);
                            switch (koma)
                            {
                                case "歩":
                                    piece = 'P'; break;
                                case "香":
                                    piece = 'L'; break;
                                case "桂":
                                    piece = 'N'; break;
                                case "銀":
                                    piece = 'S'; break;
                                case "金":
                                    piece = 'G'; break;
                                case "角":
                                    piece = 'B'; break;
                                case "飛":
                                    piece = 'R'; break;
                            }
                        }
                        // 後手番の場合
                        else
                        {
                            // 打たれた駒種の判別
                            string koma = new StringInfo(kif[1]).SubstringByTextElements(2, 1);
                            switch (koma)
                            {
                                case "歩":
                                    piece = 'p'; break;
                                case "香":
                                    piece = 'l'; break;
                                case "桂":
                                    piece = 'n'; break;
                                case "銀":
                                    piece = 's'; break;
                                case "金":
                                    piece = 'g'; break;
                                case "角":
                                    piece = 'b'; break;
                                case "飛":
                                    piece = 'r'; break;
                            }
                        }
                        // 打たれた場所の特定
                        string firstChar = new StringInfo(kif[1]).SubstringByTextElements(0, 1);
                        string secondChar = new StringInfo(kif[1]).SubstringByTextElements(1, 1);
                        // 1文字目はそのまま数字に変換する
                        switch (firstChar)
                        {
                            case "1":
                                destX = 1; break;
                            case "2":
                                destX = 2; break;
                            case "3":
                                destX = 3; break;
                            case "4":
                                destX = 4; break;
                            case "5":
                                destX = 5; break;
                            case "6":
                                destX = 6; break;
                            case "7":
                                destX = 7; break;
                            case "8":
                                destX = 8; break;
                            case "9":
                                destX = 9; break;
                        }
                        // 2文字目の漢数字部分を変換する
                        switch (secondChar)
                        {
                            case "一":
                                destY = 1; break;
                            case "二":
                                destY = 2; break;
                            case "三":
                                destY = 3; break;
                            case "四":
                                destY = 4; break;
                            case "五":
                                destY = 5; break;
                            case "六":
                                destY = 6; break;
                            case "七":
                                destY = 7; break;
                            case "八":
                                destY = 8; break;
                            case "九":
                                destY = 9; break;
                        }
                        // 結果文字列の組み立て
                        result = piece.ToString() + "*" + destX.ToString() + ((char)(destY + 'a' - 1)).ToString();
                    }
                    // 駒移動の処理
                    else
                    {
                        // "同○○"でない場合は移動先を更新する
                        if (!kif[1].Contains("同"))
                        {
                            string firstChar = new StringInfo(kif[1]).SubstringByTextElements(0, 1);
                            string secondChar = new StringInfo(kif[1]).SubstringByTextElements(1, 1);
                            // 1文字目はそのまま数字に変換する
                            switch (firstChar)
                            {
                                case "1":
                                    destX = 1; break;
                                case "2":
                                    destX = 2; break;
                                case "3":
                                    destX = 3; break;
                                case "4":
                                    destX = 4; break;
                                case "5":
                                    destX = 5; break;
                                case "6":
                                    destX = 6; break;
                                case "7":
                                    destX = 7; break;
                                case "8":
                                    destX = 8; break;
                                case "9":
                                    destX = 9; break;
                            }
                            // 2文字目の漢数字部分を変換する
                            switch (secondChar)
                            {
                                case "一":
                                    destY = 1; break;
                                case "二":
                                    destY = 2; break;
                                case "三":
                                    destY = 3; break;
                                case "四":
                                    destY = 4; break;
                                case "五":
                                    destY = 5; break;
                                case "六":
                                    destY = 6; break;
                                case "七":
                                    destY = 7; break;
                                case "八":
                                    destY = 8; break;
                                case "九":
                                    destY = 9; break;
                            }
                        }
                        // 移動元の読み取り
                        int index = kif[1].IndexOf('(') + 1;
                        originX = kif[1][index] - '0';
                        originY = kif[1][index + 1] - '0';
                        // 成り移動かどうかを取得
                        isPromoted = new StringInfo(kif[1]).SubstringByTextElements(3, 1) == "成";
                        // 結果文字列の組み立て
                        result = originX.ToString() + ((char)(originY + 'a' - 1)).ToString() + destX.ToString() + ((char)(destY + 'a' - 1)).ToString() + (isPromoted ? "+" : "");
                    }
                    // 結果文字列の出力
                    Console.WriteLine(result);
                }
            }
        }
    }
}

 

 

言語活動としての倫理的判断

 倫理学の書籍を読んでいると、どうも、「倫理」というものが特別視されすぎているという印象を受ける。倫理的判断は何ら特別な哲学的判断なのではなく、普通の言語活動の一環だと思っている。そういう視点からメタ倫理学的な考え事を書いていきたい。メタ倫理学と呼ぶことすら仰々しい。そう呼ばれているものの一部は、単に言語活動一般として分析されれば事足りるのではないか。

 

 倫理的判断ないし倫理的な発言とは、「善い」「悪い」「すべき」「それはダメでしょ」等の語群を使う発言だといえる。これらを「倫理的語彙」と呼ぼう。我々の倫理的判断の根拠や源泉をあたるためには、それら倫理的語彙の学習過程に着目する必要がある。我々は、倫理的語彙やその使用をどのように学んできたか?

 

 私たちは、倫理的語彙の意味を教えられた後にその用例を学んだ、のではないはずだ(そういう言語化ができないことは、ムーアが「自然主義的誤謬」の名で示している)。実のところ、ある出来事とそれら倫理的語彙が使用されている場面とが一挙に、言語学習過程の我々に与えられてきたはずだ。つまり、それら倫理的語彙は、それらが言及しようとしている事例とともに示されるのが常であった。

 

 そのとき、我々は単に語彙が使用されているのを見るだけであり、それが倫理的発言であるということすら明示されない。片付けをした直後に「えらいね」と発話され、別の園児を叩いた子に対し「ダメ」と発話される。そのような場面を幾度となく経験することで、私たちは「善い」「悪い」という語がそれぞれどんな場面に似つかわしいかを感じ取っていく。これが倫理的語彙、ひいては倫理の学習における実際の姿ではないだろうか。

 

 このような学習は幼少期のみに見られるわけではない。我々は幼少期のうちに倫理的語彙を「学び終わる」のではないのだ。それら語彙は日常の言語活動において絶えず用いられ、事例は常に蓄積されていく。

 

 そのような学習と同時に我々も、他の人の真似をして倫理的判断を下していく。ある種の場面や言動に対して、倫理的語彙を使うのだ。ここで考慮されるのは、類似する過去事例の存在と、そこで発揮された語彙の効力である。盗みを働こうとする人に対して「よくないよ」と発言することは、単に自然な発言だったというだけでなく、その盗みを抑止する効果も発揮してきたはずだ。私たちは、その語がその場に適合することを確信しつつ、その語彙が放つ効力にも期待して、その語を発するのである。中には「倫理的判断が難しい事柄」というものもあるが、それは類似する過去事例の不足を主張しているに過ぎない。

 

 これが、倫理的判断がどのように下されているか、の現状だ。では、この説明によってメタ倫理学の探究に終止符が打たれるのかというと、それは全く違う。世の中であまり明示されないが、メタ倫理学はふたつのことを探求していると私は考えている。①我々は現状どのように倫理的判断を下しているのか、と、②我々は理想的にはどのように倫理的判断を下すべきなのか、である。今回述べたのは①に対する回答にすぎず、そこでの自分の立場を整理したものだ。今後は②についても何か言えたらいいなと思っている。

 

【自分用】テキスト変換装置

はてなブログからローカルへの移植のためのちょっとした変換装置を作った。

トランスエイジからトランスジェンダーを考える

 最近、トランスエイジという概念が注目を集めている。トランスエイジとは、身体の年齢(戸籍上の年齢)と自認する年齢が異なる状態のことだ。トランスジェンダーが比較的世間から受け入れられつつあるのに対して、トランスエイジ概念はかなり批判を浴びているみたい。まあいろいろ言われているが、僕たち(年齢違和を感じるか否かにかかわらず)が問うべきことはひとつだ。


 僕たちはトランスエイジという概念を認めるべきなのか?


 そもそもだけど、新しい概念が提唱されているわけだ。既存観念では十分でなく、トランスエイジ概念が必要だとされた理由があるはずだ。ここで、オッカムの剃刀を応用した考え方をする。トランスエイジという概念を使用せずしてその問題を解決できるんなら、そのほうがシンプルでいいはず、ということだ。


 本来であれば、そのあたりを踏まえて、トランスエイジ概念の必要性についてみんなで議論しよう!という話になる。でもここで問題がある。トランスエイジ概念の必要性は、年齢についての違和を感じる人間にしか経験できない、という壁だ。民族や宗教の問題においてもそうだけど、万人が経験できないということは対話において分断を生んでしまう。そして、対話ができないことをいいことに、雑多な概念が乱立してしまうこともある。もどかしいね。


 現時点でトランスエイジについてこれ以上の深掘りは難しそう。  むしろこれを機に、すでに世間から認識されているトランスジェンダーについてもう一度考えてみたい。


 トランスジェンダーとは、狭義には、身体性と性自認が異なる状態のこと。ここで重要なのは性自認という概念だな。ある見方をすれば、性自認とは、自分の望む扱われ方を性別の名で呼んだもののことだといえる。つまりそれは、性別によって扱われ方が変わるという前提に基づいている。ということは、世間が性別に関係なく人間を扱うようになってしまったら、性自認概念は根底から揺さぶられる。不思議な構造だなあ。


 さて、性自認についてもオッカムの剃刀風に考えてみる。  性自認という概念を持ち出すことなく、世間での扱われ方を変えてもらうことができるなら、そのほうがシンプルでいいよねって話。しかし、思うに、今の世間は性別によって人間の扱い方を変えることをやめようとはしない。少なくとも、すぐに世間が変わることはなさそう。そういうことを踏まえると、既存の「性別」という概念の力を借りることによって自らの扱われ方を変えることができる「性自認」概念は、かなり理にかなっていて有用なアイテムだなあと感心する。(何様?)


 ここで、「身体の性が本来の性別だ!」とか「性自認というのが真の性別なのだ!」などというのはどちらもナンセンスだ。なぜなら、ここで問題になっているのは「性という言葉をどのように用いることが良い社会に繋がるのか?」ということだからだ。


 新しい概念が生み出されようとしているとき、そこには必ずプラグマティックな問いが生じる。生じなくてはならない。

モンテカルロ法で円周率を求める

モンテカルロ法で円周率を求めるやつを実装した。 プロットする点の数を入力してボタンを押すと、点がランダムに生成される。どれくらいの割合で円の内部に含まれたかを計算することにより、円周率を求めることができる。プロット数が大きいほど当然精度は上がるが、処理はクライアントサイドなので好きに決めてほしい。

プロット数:

計算結果:

ChatGPTコーディングのTIPS

 ChatGPTはすこぶる優秀なプログラマーだ。私は、これまでに色々な開発を彼にお願いしてきた。今回は、彼に開発を依頼するときの作法をまとめようと思う。備忘録のようなものなので、体系的にはならなかった。

 

GPT-4に英語で命令せよ

 まずは基本的なところ。2023年5月現在のGPTの最新バージョンであるGPT-4を使用しておきたい。その上で、他の言語よりも英語で命令するのが望ましい。当然ながら、英語の文章はGPTの学習に際して最も多く投入されている。正確なレスポンスが欲しいのであれば英語で命令しない選択肢はない。

 英語が苦手な人は、それこそGPTに翻訳させるのがいいと思う。彼は言語処理のプロなので、文脈もある程度汲み取って柔軟に翻訳してくれる。

 

単体開発をさせるべし

 これはChatGPTだからというよりも、システム開発の基本である。

 例えば、「Webサイトから抽出してきた文字列をSNSに投稿する」というシステムを作成するとする。

 このとき、このシステム全体をGPTに作らせてはいけない。うまく動作しなかった際に、原因の切り分けがしにくくなるからだ。

 以下の1、2というユニットに分けて、それぞれをGPTに作成させるのがよい。

  1. Webサイトから文字列を抽出してコンソールに出力する機能
  2. 与えられた文字列をSNSに投稿する機能

 それぞれ上手く動作したことを確認したら、自分で結合して完成だ。

 

箇条書き形式で命令せよ

 相手は人工知能なのだから、人間に伝えるような自然な文章として体裁を整える必要はないのだ。そのため、箇条書き形式で命令をしたほうが、手間もかからないし、要件を伝達しやすいと思う。

 "Please generate a script. The requirements are below."などのように伝え、要件を羅列していくのが最も書きやすかった。

 

情報の不足を考慮せよ

 この命令ができるところがChatGPTの真価であると言ってよい(言い過ぎ)。命令文の最後に「情報に不足があれば質問してください」という旨を記載する。"If you need more information, please ask me."みたいな感じでいい。

 僕らは人間なので、文脈や前提条件などを要件に盛り込み忘れるときがある。そうした場合、普段ならGPTは勝手に文脈を想定してとりあえずの回答をくれるのだ。でも、この文言を追加すれば、回答に必要なピースを向こうから求めてきてくれるのだ。

 すべての命令文の末尾には、とりあえずこれをつけておくのが無難だ。

 

出力が途切れたときは

 GPTに文章を生成させていると、ときどき文章の途中で回答が途切れることがある。その場合は、"Go on""Continue" などと命令すれば、途切れたところから出力を続けてくれる。日本語で命令したときには「つづき」などと言えばよい。

 

まとめ

 AIにプログラミングを任せられるようになったのは非常に嬉しい。もっと性能が向上すれば、個人が気軽に大規模システムを生成できる時代が来るかもしれない。

Cesium JS をはてなブログに埋め込んでみる

↓下に地球儀を表示したい↓。いろいろ試しているが、上手くできない。