機械学習周りのプログラミング中心。 イベント情報
ポケモンバトルAI本電子書籍通販中

iPhone 15 Proの有線LAN接続検証

世界コンピュータ将棋選手権では、対局に使うコンピュータを会場の有線LAN(Ethernet)に接続する必要があります。

普通のパソコンであれば難なくできる作業ですが、スマートフォンを有線LANに接続するのはあまり一般的な作業ではありません。 私が対局に利用予定のiPhone 15 Proが有線LANに接続できるかどうか検証しました。

iPhoneシリーズはiPhone 15以降、有線コネクタがLightningからUSB-Cに変わりました。 Lightningコネクタを持つiPad (第9世代)についてはこちら。 select766.hatenablog.com

以下の組み合わせで成功しました。iPhoneの設定は特に不要で、機内モードWi-Fiオフにしても正しく通信できました。

  • iPhone 15 Pro
  • USB-C Digital AV Multiportアダプタ (MUF82ZA/A)
    • 2019年8月購入、現行品
  • ETX3-US2 (I-O DATA)
    • 2018年4月購入、生産終了
    • まだ手に入るようです

iPhone15Proを有線LANに接続

iPhoneで将棋AI大会に出るときのヒヤリハット(電竜戦)

第4回世界将棋AI電竜戦本戦(2023年12月2日~3日)に将棋AI「ねね将棋」で出場しました。ほとんどの参加者はパソコンかクラウド計算機上で将棋AIを動作させる中、ねね将棋はiPhone上で動作させるというユニークなソフトです。

ソフトの概要や電竜戦参加の記録はこちらのページから。

select766.hatenablog.com

本記事では、iPhoneで将棋AIを動作させるという特殊な仕組みに起因して大会出場時に起こった・未然に防いだオペレーションミスについて紹介し、再発防止策を検討します。

プロビジョニングプロファイルの有効期限切れ

自作のiOSアプリをiPhoneで動作させる際には「プロビジョニングプロファイル」というものが必要で、有効期限があります。アプリに埋め込まれたプロビジョニングプロファイルの有効期限が切れると、「"ねね将棋"はもう利用できません」というような表示が出てアプリを起動できなくなります。無料でアプリ開発している場合は有効期限が7日しかないため、頻繁にMacでアプリをビルドしなおしてインストールする必要があります。私はこの有効期限について勘違いしており、インストールしなおすたびに有効期限が更新されるものと想定していました。例えば1月1日に初回ビルド(とインストール)、1月5日に2回目のビルドを行った場合、1月12日まで有効ではなく、1月8日まで有効です。有効期間内にビルドしなおしても有効期限が更新されません。私はこの挙動を認識しておらず、大会数日前にビルドしなおしていたにもかかわらず、大会の朝にアプリが起動できなくなり焦りました。

この挙動と対策については以下のサイトが参考になります。

無料の開発者アカウントで iPhone にインストールしたアプリの有効期限を更新する方法 - Neo's World

Apple Developer Program に登録せず、無料の Apple ID アカウントで iOS アプリを実機にインストールした場合、プロビジョニング・プロファイルの有効期限は7日間となる。 「Automatically manage signing」を利用するとプロビジョニング・プロファイルを自動的に作成し、期限が切れてからの再インストール時は自動的に有効期限を更新してはくれる。しかし、7日間経って期限が切れるまでは、プロビジョニング・プロファイルを更新してくれない。

通信仲介用のMacがスリープし、接続切れ

将棋の通信プロトコルの都合で、iPhoneと対局サーバの間にMacを仲介させていました。Macは電源アダプタに接続しているときはスリープしないようにしていたのですが、電源への接続を忘れており一定時間でスリープしてしまい、通信切断が発生しました。接続を忘れた理由は上記のプロビジョニングプロファイルの有効期限切れが発生し、急遽MaciPhoneを優先接続する際に電源ケーブル(USB-C)を流用したためです。テスト対局時に発生したので本番への影響はありませんでしたが、スリープに入るまでの時間によっては本番で失敗していたので危険でした。

対策は対局開始時に電源接続を確認することです。

iPhoneのスリープ

iPhoneは一定時間操作しないとスリープに入り、アプリの動作が停止してしまいます。スリープさせない設定も可能ですが、普段持ち歩いている端末なのでこの設定は不便です。大会前の検証中にスリープが発生したため、事前に対策していました。

対策:アプリ内でスリープを防止するAPIを呼び出す。SwiftUIで以下の処理により対応できる。

UIApplication.shared.isIdleTimerDisabled = true

iPhoneの電源接続忘れ

対局の合間に、将棋AIを止めてLINEなどを確認し、将棋AIを起動しなおした際に電源の接続を忘れ、対局が進みました。電池残量60%程度になったところで気づいて電源を接続し、実害はありませんでした。

対策は、対局前の操作手順を壁に貼るなどして、見落としをなくすことです。また、バッテリー状態を取得するAPIを用いて、画面上に警告を表示することも考えられます。

iPhoneへの電話着信

対局中は他のアプリの通知などが発生しないよう「おやすみモード」を有効にしていましたが、電話は着信する設定になっており、実際に電話が着信しました。その結果、アプリの動作が数十秒停止しました。電話を取らずに呼び出しが終わると動作が再開したため負けにはなりませんでしたが、残り時間がわずかな場合に起こっていれば負けていました。

対策は、「おやすみモード」の中の詳細な設定で電話の着信をしないように設定することや、機内モードを有効にする(WiFiはONにできる)ことです。

以上のようなオペレーションミスがありましたので、次回は対策して臨みたいと思います。

第4回世界将棋AI電竜戦本戦の結果(2023/12/02)

NPO法人AI電竜戦プロジェクト主催 第4回世界将棋AI電竜戦本戦 に将棋AI「ねね将棋」で参加しました。電竜戦は、オンラインのコンピュータ将棋AIの大会です。 将棋AIは通常パソコンやクラウド計算機上で動作させますが、ねね将棋は携帯端末上で動作する点がユニークな点です。前回まではタブレット端末のiPad(第9世代)を用いていましたが、今回は2023年9月発売のスマートフォンであるiPhone 15 Proで動作させました。機種の変更により、局面評価速度が約5倍になりました。私の普段の利用方法であればProではないiPhone 15で十分なのですが、大会でより良い成績を出したかったのでProを買いました。

ソフトウェアの内容

ソフトウェアの内容は、ふかうら王をiOSに移植したものになります。第33回世界コンピュータ将棋選手権で行っていた合議は削除し、DL系思考部単体で動作しています。持ち時間が短めなので、定跡を搭載できるように改良を施し、やねうら王のテラショック定跡を搭載しました。

select766.hatenablog.com

従来はiPadで動作させており冷却の効果はありませんでしたが、iPhoneでは冷却しないと速度が低下するため、スマートフォン用冷却ファンを取り付けて実行しました。

select766.hatenablog.com

大会の結果

予選で6勝8敗で27位(40チーム中)になりました。C級(上位20チーム以外の部門)に進出しました。C級では7勝6敗1分け(7.6点)で8位(16チーム中)となりました。

特筆すべき対局としては、「shotgun」と1勝1敗になりました。shotgunは、2017年に行われた第5回電王トーナメントの準優勝ソフトで、当時の強さを再現する設定で参加しています。第5回電王トーナメントはねね将棋がデビューした大会でもあり、shotgun対たぬきの決勝は大変盛り上がったのを記憶しております。今回、当時ハイエンドのゲーミングPCで動いていた最高峰のソフトと同じレベルの強さがスマートフォン上で実現できることが示せてうれしく思います。2017年当時はとても弱かったDL系ソフトが発展し、スマートフォンのハードウェア面の進歩も合わさってここまで強くなりました。

ねね将棋がshotgunに負けた対局では、一時ねね将棋が優勢だったのですが、23手詰めを受けて負けました。DL系の詰みに弱い特徴がそのまま出た対局でした。

https://golan.sakura.ne.jp/denryusen/dr4_production/dist/#/dr4prd+buoy_blackbid300_dr4c-5-bottom_4_neneshogi_shotgun-600-2F+neneshogi+shotgun+20231203144534/101

【C級】第4回電竜戦本戦 5回裏☗ねね将棋-☖shotgun

この対局については千田先生にも解説いただきました。4時間29分ごろからです。101手目の▲4三歩が人間も指しそうな手ではあるものの敗着とのことでした。

【将棋AI】第4回世界将棋AI電竜戦 決勝午後【解説:千田翔太七段】 - YouTube

iPhoneで将棋AIを動作させたという理由で独創賞を頂きました。電気通信大学エンターテイメントと認知科学研究ステーション様、賞金のご提供ありがとうございました。

大会全体では、NNUE系ソフトである水匠が優勝しました。DL系のdlshogiと優勝争いが繰り広げられ、ハードもソフトも大きく異なる2系統のソフトの棋力が非常に近いというのは技術的にも興味深い結果でした。運営、スポンサー、対局者の皆様ありがとうございました。

【ゲームボーイ強化学習本】技術書典15 頒布中

技術書典15は2023年11月11日から26日まで開催される技術系同人誌の頒布イベントです。当サークルはオンライン頒布(通販)のみです。

当サークル「ヤマブキ計算所」は技術書典15に出展します。新刊「ゲームボーイ強化学習を実装してみた」を頒布します。1989年に発売されたゲーム機であるゲームボーイで動作する、強化学習アルゴリズムの実装を解説します。コンテンツとしては、ブログ記事シリーズを書籍の形に編集・図の追加等を行い、さらに新規のゲームを開発しその作り方を解説したもの(16ページ分の加筆)となっております。電子書籍(PDF)は1000円、物理本は1000円です。物理本を購入した場合も、直ちに電子書籍をダウンロード可能です。

以下のリンクから購入ページに飛べます。

techbookfest.org

新規ゲーム部分の動画はこちらです。

当サークル以外にも面白い技術書がたくさんあると思うので、ぜひ覗いてみてください。ゲームボーイに関する本もいくつかあります。

techbookfest.org

techbookfest.org

ゲームボーイ上で強化学習を行う part8

前回、ソフトが完成しました。今回は実機動作確認です。

ゲームボーイ実機で動作させる

せっかくゲームボーイソフトを開発したので、エミュレータ上だけでなく実機でも動作させてみたいです。そのためには、パソコンから書き込めるカートリッジが必要です。 本プロジェクトでは、同人サークル「CUBIC STYLE」から頒布されている「GBカートフラッシャー開発セット (USB Type-C)」を用います。コミックマーケット102で入手できました。通販もあるようです。 パッケージには、何度も書き換え可能なROMを内蔵したフラッシュカートリッジと、パソコンからROMデータを転送するための書き込み機(GB Cart Flasher)が含まれています。まずパソコンからROMデータをカートリッジに書き込み、カートリッジをゲームボーイ実機に挿入することで自作のソフトをゲームボーイ実機で実行できます。

GB Cart Flasherの外観

ROMデータを書き込んだカートリッジをゲームボーイカラーで動作させました。動作画面を示します。ゴールに到達するまでには30分ほど学習時間がかかりますが、全く不具合なく動作しました。これにて実機動作確認成功です!

ゲームボーイカラー実機の画面

同様に、ゲームボーイアドバンスでも動作させました。ゲームボーイアドバンスにはゲームボーイソフトを動作させる互換機構があります。

ゲームボーイアドバンス実機の画面

ちなみに、コースが青く表示されているように見えますが、これはモノクロ液晶を搭載したゲームボーイ向けに作られたソフトを表示する際に、カラー液晶を搭載したゲームボーイカラーゲームボーイアドバンスが自動的に彩色しています。もちろん、ゲームボーイカラーに対応したソフトを作れば色をソフト側から指定できます。

【GBC20周年企画(1)】覚えてる? ゲームボーイカラーのトリビア20連発! - ファミ通.com

おわりに

ゲームボーイソフトの開発を始めてから完成させるまでおよそ40時間かかりました。 GBDKのCコンパイラは快適で、予想外に躓く点はなくPC向けプログラムとあまり変わらない実装ができました。 強化学習では当たり前に出てくる浮動小数点数演算を回避する工夫がポイントでした。今回は強化学習が動作しさえすればよいという要件でしたので極端に難しいことはありませんでしたが、当時ゲーム開発をされていた方は性能向上のためにより技巧的なテクニックを開発されていたのではないかと思います。 今回はプレイヤーの操作に依存する要素がなくゲームとは呼び難いものができましたが、もしうまいゲームデザインが考えられれば、プレイヤーと対戦して強くなっていくようなAIも作れるのではないかと思います。

ゲームボーイ上で強化学習を行う part7

実装

Q学習とアニメーションの共存

Q学習でQテーブルを更新すると同時に、最新のQテーブルに基づいて車が運動するアニメーションを表示する実装を行います。課題は、学習と表示をどのように並行動作させるかです。もっとも単純に学習過程を表示する方法は、学習に用いているMountainCar環境が1ステップ進むたびに、それに合わせてスプライトの座標を移動することです。しかし実際には、1秒間に100ステップ以上進行するため動きが速くなりすぎますし、スプライトの座標を更新するにはVBLANK割り込みを待つ必要があるため学習アルゴリズム内で直接スプライトの座標を操作できない(1ステップごとにVBLANK割り込みを待つと、学習が著しく遅くなる)という問題があります。今回採用した解決策は、main関数で学習を進めつつ、VBLANK割り込みハンドラで学習とは独立したMountainCar環境を動作させるというものです。VBLANK割り込みはmain関数で何をしているかにかかわらず、60fpsの周期でVBLANK割り込みハンドラを実行し、それが終了するとmain関数の続きを実行する仕様となっています。main関数では、表示のことは気にせずQ学習を行い、Qテーブルを更新します。VBLANK割り込みハンドラは、独立したMountainCar環境を保持し、ハンドラが呼び出されるごとにQテーブルを参照して行動を決定し、環境を1ステップ進め、スプライトの座標を更新します。デメリットとして、ハンドラ内で1エピソードが進行する間にQテーブルがmain関数側で書き換えられてしまい一貫性がなくなることがありますが、テーブルの値の変化はわずかなので実質的に問題はありません。他の方法として、main関数側で実行したエピソードの各ステップでの座標を記録しておき、VBLANKハンドラ側で再生するという方法も考えられます。こちらは一貫性はあるものの、エピソードの長さに比例したメモリが必要になります。以上の考えに基づいて実装したコードが以下の通りです。main関数では学習を行い、100エピソードごとに評価を行います。評価結果として、エポック数(100エピソードを1エポックと数える)および1エピソードの平均ステップ数(短いほうが良い)をTrainState型のグローバル変数経由でVBLANKハンドラに渡しています。アニメーション用のエピソード実行だけでなく、評価結果の表示もVBLANKハンドラ側で担当します。main関数の実行中のどのタイミングでも割り込みが発生するため、データ構造の書き換え中に割り込みが発生して中途半端なデータが読まれないよう、__criticalを用いて一時的に割り込みを停止しています(このコードでそこまでする必要性は薄いですが)。

#define STEP_CYCLE 2

typedef struct
{
    QLearningState *q_state;
    MountainCarState state;
    uint8_t cycle;
    uint8_t updated;
    int epoch;
    uint8_t avg_steps;
} TrainState;

static TrainState *ts;

// VBL割り込み関数側では、現在のモデルを用いて独立にエピソードを実行し、車の動きを可視化する。
void train_vbl()
{
    const QLearningState *q_state = ts->q_state; // main側と共用

    if (--ts->cycle == 0)
    {
        ts->cycle = STEP_CYCLE;
        step_display(q_state, &ts->state); // MountainCar環境の状態であるts->stateはmainと独立
    }

    if (ts->updated)
    {
        ts->updated = 0;
        gotoxy(0, 16);
        printf("%d00 episodes\nAvg steps: %d", ts->epoch, ts->avg_steps);
    }
}

// main関数でスプライト等の初期化後に呼び出される
void train_main()
{
    ts = malloc(sizeof(TrainState));
    MountainCarReset(&ts->state);
    QLearningState *q_state = QLearningStateCreate();
    ts->q_state = q_state;
    ts->cycle = STEP_CYCLE;
    ts->updated = 1;
    ts->epoch = 0;
    ts->avg_steps = MAX_EPISODE_LEN;

    __critical
    {
        add_VBL(train_vbl);
    }

    // main関数側では、割り込み処理をしていない間ずっとモデルの学習を行う。
    for (int epoch = 0; epoch < 1000; epoch++)
    {
        initrand((unsigned int)epoch);
        for (int i = 0; i < 100; i++)
        {
            TrainEpisode(q_state);
        }
        // fix test case
        initrand((unsigned int)1);
        uint32_t total_steps = 0;
        for (int i = 0; i < 10; i++)
        {
            uint8_t steps;
            TestEpisode(q_state, &steps);
            total_steps += steps;
        }

        __critical
        {
            ts->epoch = epoch + 1;
            ts->avg_steps = total_steps / 10;
            ts->updated = 1;
        }
    }
}

以上の実装により、Q学習を行いながら、最新のQテーブルを用いて車を運動させてアニメーション表示させることができました。

学習しながらアニメーションを行っている画面

ボタン入力によるモード選択

ここまでのプログラムでは、計算と画面出力しか実装していませんでしたので、最後にボタン入力の方法について紹介します。

タイトル画面を実装し、Aボタンを押せば学習を行うモード、Bボタンを押せば学習済みQテーブルを用いてアニメーションのみを行うモードに分岐することにします。実装は簡単で、joypad関数で現在のボタンの状態を読み込むことができます。Aボタンが押されていれば、bit 4が1になり、Bボタンが押されていれば、bit 5が1になるという要領です。ボタンとビットの対応関係は、定数J_AJ_Bなどに格納されています。

    uint8_t mode = 0;
    while (!mode)
    {
        uint8_t button = joypad();
        if (button & J_A)
        {
            mode = 1;
        }
        else if (button & J_B)
        {
            mode = 2;
        }
        wait_vbl_done();
    }

以上の実装で、ゲームボーイ上で強化学習を行うソフトが完成しました!

タイトル画面

次回、最終回として実機で動作確認をします。

ゲームボーイ上で強化学習を行う part6

前回、画面表示に必要な機構を準備しました。今回は車のスプライトを動かします。

実装

学習済みQテーブルの実行

スプライトの動きの実装に集中するため、まずはPCで学習したQテーブルを用いて車の運動を計算し、うまく坂を上るアニメーションを実現します。 Qテーブルをソフトに埋め込むため、PCで学習した結果得られたQLearningState構造体の内容を、そのままC言語の定数配列に変換します。

const unsigned char pc_trained_params[] = {0xED, 0xEF, ...}

以下のコードで、Qテーブルに従って行動を決めて車の座標を計算し、スプライトの座標に反映します。MountainCar環境自体はx座標しか考慮しませんが、自然な表示を実現するため、地面のカーブに沿ってy座標も計算します。初期化処理以外はすべてVBLANK割り込みハンドラで処理しています。割り込みは1秒間に60回呼び出されますが、車の動きが速すぎるため、2回に1回だけ更新処理を実行します。表示に関する状態は、RunPretrainedStateという型にまとめ、グローバル変数にそのポインタを置きました。この例ではポインタではなく実体を静的に確保しても構わないのですが、後で実装する学習モードを実行する場合には不要な領域となりますので動的確保しています。しかし実際のところ、mallocの割り当て管理用のメモリ領域のほうが大きいかもしれません。ゲームボーイのRAMは8kBですが、ルーズな実装をしても意外と余裕があるという印象です。

#define STEP_CYCLE 2 // xフレームに1回だけ移動(60fpsは速すぎるので)

typedef struct
{
    MountainCarState state;
    uint8_t cycle;
} RunPretrainedState;

// 学習済みモデル実行用の状態
static RunPretrainedState *rps;

// x座標に対応するy座標を格納したテーブル
const uint8_t display_y_table[160] = {54, 55, 57, 59, 60, 62, 64, /* ... */};

void step_display(const QLearningState *q_state, MountainCarState *state)
{
    // 現在の状態をスプライトの座標に反映
    // 車の中心のx座標(画面左端=0)を計算
    uint8_t center_x = (uint8_t)((uint16_t)(state->position + 12000) / (18000 / 160));
    // move_spriteの引数xは、x=8でスプライトの左端が画面の左端になる。x=center_xのときに、スプライト中心が画面の左端に来てほしい。スプライトの幅は8。
    move_sprite(0, center_x + 4, display_y_table[center_x]);

    // 状態を1ステップ進める
    MountainCarAction action = GetBestAction(q_state, state);
    Reward reward = MountainCarStep(state, action);
    if (state->done)
    {
        MountainCarReset(state);
    }
}

// VBLANK割り込みで呼び出される関数(60fps)
void run_pretrained_vbl()
{
    const QLearningState *q_state = (const QLearningState *)(pc_trained_params);

    if (--rps->cycle == 0)
    {
        rps->cycle = STEP_CYCLE;
        step_display(q_state, &rps->state);
    }
}

// main関数で背景等の初期化が完了した後に呼ばれる
void run_pretrained_main()
{
    gotoxy(0, 16);
    printf("Running pre-trained model");
    rps = malloc(sizeof(RunPretrainedState));
    MountainCarReset(&rps->state);
    rps->cycle = STEP_CYCLE;
    __critical
    {
        add_VBL(run_pretrained_vbl);
    }

    while (1)
    {
        wait_vbl_done();
    }
}

ここまでの実装を終えると、学習済みQテーブルを用いて車が左右に移動し、坂を上るアニメーションを見ることができます。

学習済みQテーブルによる動作画面

次回は、Q学習アルゴリズムとグラフィックを連携させ、ゲームボーイ上で学習をし、その結果を画面に表示します。