XServerの無料VPSを利用してFreecivのサーバを構築する
この記事の目的
無料で2コア2GBのサーバを借りることができます。これならFreecivのような軽量なゲームサーバを動かすのに十分ですし、実際にやってみましょう。詳しい仕様は無料VPSのランディングページにあります。
Freecivのサーバをセットアップする
XServerにログインする
クレジットカードが必要なので手元に用意します。
無料VPSのランディングページに飛び、今すぐ無料で始めるをクリックします。
XServerのアカウントがない場合はすぐにスタート!新規申し込みをクリックしてアカウント作成に入ります。アカウントがある場合はログインをクリックしてログインします。
サーバマシンを用意する
- サーバ名は何でもよいです。例えば
fun-with-freecivとします。 - プランは
2GB、メモリ 2GB、CPU 2core、NVMe SSD 30GBを選びます。 - イメージタイプは
Ubuntuの最新リリースを選びます。 - rootパスワードに安全なパスワードを入力します。このrootパスワードは後で使うSSH秘密鍵のパスワードにもなります。
- SSH Keyは
キーを作成するにします。登録方法は自動生成とし、好きな名前(例:freeciv)を入力します。この名前は後でダウンロードするSSH秘密鍵のファイル名に使われます。 - 利用規約に同意するチェックボックスにチェックマークを入れ、
お申込内容を確認するをクリックします。 - SSH秘密鍵のファイルをダウンロードし、ドキュメントフォルダなどの紛失しにくいフォルダに保存します。
申し込むをクリックします。契約管理トップへをクリックします。これでマシンが作成されました。
サブドメインの取得
トップページにあるXServerのサブドメインにドメイン追加というリンクがあるのでクリックしましょう。
XServerではyour-subdomain.xvps.jpという形式で無料のドメインが取得できます。
例えばfun-with-freeciv.xvps.jpというドメインが欲しければ、追加するドメイン名にfun-with-freecivと入力します。
入力できたら、確認画面に進むをクリックします。内容を確認したら、XServerのサブドメインを追加するをクリックします。
ファイアウォールの設定を済ませてサーバマシンにログインする
今はインターネットから一切のアクセスを受け付けない状態です。SSHとFreecivの通信を許可しましょう。
- トップページに作ったサーバマシンが表示されているはずです。そこの
VPS管理をクリックします。 - 左にある
パケットフィルター設定をクリックします。 パケットフィルター設定を追加するをクリックします。- フィルターに
SSHが選ばれているはずです。そのまま追加するをクリックします。 - もう一度
パケットフィルター設定を追加するをクリックします。 - フィルターを
手動で設定、プロトコルをTCP、ポート番号を単一ポートの5556、許可する送信元IPアドレスをすべて許可、メモをFreecivとします。 追加するをクリックします。
接続元を制限したいときは、管理者、プレイヤーが利用しているISPに割り当てられたIPアドレスの範囲を調べ、範囲ごとにパケットフィルターの規則を追加します。
SSHでログインする
RLoginなどのSSHクライアントを用意してください。最近のWindowsにはOpenSSHが付属しているのでそれでも代用できます。
RLoginを使う場合
ファイル(F)→サーバに接続(N)をクリックします。新規(N)をクリックします。- ホスト名(サーバーIPアドレス)に取得したサブドメイン(例:
fun-with-freeciv.xvps.jp)、ログインユーザー名にroot、パスワードにrootのパスワードを入力します。 SSH認証鍵(K)をクリックします。- ダウンロードしたSSH秘密鍵を指定します。
OKをクリックします。このホストの公開鍵を信頼して接続しますか?と聞かれた場合は接続するをクリックします。信頼するホスト鍵のリストを更新しますか?と聞かれた場合ははいをクリックします。
これでサーバマシンにログインできました。
OpenSSHを使う場合(Windows)
初めに、SSH秘密鍵ファイルのアクセス権限を設定し、自分しかアクセスできないようにします。これをしておかないとログインエラーになる場合があります。
WindowsキーとXキーを同時に押し、ターミナル(管理者)(A)をクリックします。
ドキュメントフォルダにfreeciv.pemという名前で保存している場合は次のようにコマンドを入力します。
$file = "freeciv.pem" Join-Path $env:USERPROFILE "Documents" | Set-Location icacls $file /inheritance:r takeown /F $file icacls $file /grant "${env:USERNAME}:F"
このようなときは次のコマンドを入力します。
ssh -i Documents/freeciv.pem root@fun-with-freeciv.xvps.jp
ホスト鍵を信頼するか聞かれた場合はyesと入力します。
これでサーバマシンにログインできました。
Freecivサーバを動かす準備をする
この記事では、Freecivをソースコードからインストールする方法を取ります。FreecivはOSのパッケージ管理ツールでもインストールできますが、最新バージョンが収録されていなかったり、任意のバージョンを選びにくいです。
それともう一つ。Freecivのサーバプログラムはフォアグラウンドとして動作するため、SSH接続を切ってしまうとサーバプログラムが終了してしまいます。サーバマシンにログインしていなくてもサーバプログラムを動かし続けるにはいくつか方法がありますが、今回はGNU Screenというソフトウェアのアタッチ/デタッチ機能を利用して簡易的に実現してみようと思います。
まずシステムを最新の状態にします。お好みでホスト名も設定しましょう。
# ホスト名を標準のものから変更します。 # your-subdomainの部分を置き換えてください。 hostnamectl hostname your-subdomain.xvps.jp # システムにインストールされているパッケージをすべて更新して再起動します。 apt update apt full-upgrade reboot
SSH接続が切れるはずです。1分程度待ってから接続しなおします。
# GNU Screen、コンパイラ、依存ライブラリをインストールします。 apt install screen gcc make libtool libcurl4-gnutls-dev libicu-dev libsqlite3-dev liblzma-dev libzstd-dev libreadline-dev # Freecivサーバを動かすユーザfreecivを作り、パスワードを設定します。 useradd -m -s /bin/bash freeciv passwd freeciv # GNU Screenを設定します。 tee ~freeciv/.screenrc >/dev/null <<EOF startup_message off escape ^Zz shell /bin/bash defscrollback 100000 EOF chown freeciv:freeciv ~freeciv/.screenrc
rootユーザとしての作業はここまでです。freecivユーザで作業しましょう。
# GNU Screen付きでfreecivユーザになるためのエイリアスsufcを用意します。 echo "alias sufc='su -lc \"screen -dR\" freeciv'" >> ~root/.bash_aliases source ~root/.bash_aliases sufc
Freecivのソースコードをコンパイルしてインストールします。
# バージョン3.2.0をインストールしてみます。 TXZ=freeciv-3.2.0.tar.xz curl -sO https://files.freeciv.org/stable/$TXZ # ファイルのハッシュ値が正しいか確認してから展開しましょう。 curl -s https://files.freeciv.org/stable/SHA256SUM | grep $TXZ | sha256sum -c && tar xf $TXZ # ソースコードがあるディレクトリに移動します。 cd ${TXZ%.tar.xz} # ソースコードからサーバだけコンパイルしてホームディレクトリにインストールします。 # 順番に構成を説明します。 # --prefix=$HOME/.local インストール先は~freeciv/.localです。~freeciv/.profileにある設定によって~freeciv/.local/binに自動的にパスが通ります。 # --enable-fcdb=sqlite3 ユーザ認証機能用データベースとしてSQLite3を使います。 # --enable-fcmp=cli Modpackインストーラのコマンドライン版を利用できるようにします。 # --enable-client=no クライアントはコンパイルしません。 # --enable-shared --enable-aimodules=yes AIモジュールを利用できるようにします。 # --with-readline サーバプログラムのコンソールでコマンドの編集ができるようにします。 # --with-missinglist ライブラリの不足などが原因で無効にされた機能を確認できます。 ./configure \ --prefix=$HOME/.local \ --enable-fcdb=sqlite3 \ --enable-fcmp=cli \ --enable-client=no \ --enable-shared --enable-aimodules=yes \ --with-readline \ --with-missinglist make -j4 install source ~freeciv/.profile
umiteru2004さんが公開されている多島戦用のルールセットを採用してみましょう。
cd curl -sL https://github.com/umiteru2004/freeciv/archive/refs/heads/develop.tar.gz | tar zx mkdir -p ~/.freeciv/3.2 # Freecivのバージョンに合わせる cp -r freeciv-develop/rulesets/civ23umi* ~/.freeciv/3.2 rm -r freeciv-develop # Freecivサーバの初期化スクリプトを用意します。 # 参考: https://github.com/umiteru2004/freeciv/blob/develop/rulesets/sns-isls.serv tee ~freeciv/init.serv >/dev/null <<EOF cheating rulesetdir civ23umi set aifill 8 set generator ISLAND set mapsize XYSIZE set xsize 32 set ysize 32 set startpos VARIABLE set nationset all cmdlevel admin first metamessage Welcome to our Freeciv server! connectmsg Welcome to our Freeciv server! set timeout 60 set endturn 256 EOF
これで準備が整いました。さっそくサーバプログラムを起動してみましょう。
# Freecivサーバを起動します。XServerで取得したサブドメインを-iオプションで指定できます。 # 下記のほかに、ユーザ認証やスコアログの保存に関するオプションがあります。 # -r init 初期化スクリプトを読み込ませる。拡張子はつけない。 # -q 300 誰もいなくなって300秒後に試合を終了する。 # -s saves ./savesにセーブデータを保存する。 # -m メタサーバにこのサーバを登録する。 # -i $(hostname -f) サーバの名前 freeciv-server -r init -q 300 -s saves -m -i $(hostname -f)
手元のマシンでFreecivのクライアントを起動してみると、メタサーバに自分のサーバが並んでいることを確認できるはずです。
SSHクライアントに戻りCtrl+zに続けてdキーを押してみてください。画面が変わるはずです。ここまでくればexitコマンドなどでSSH接続を切っても大丈夫です。この状態でもサーバプログラムは動いたままであり、メタサーバにも自分のサーバが残っているはずです。
メンテナンス
サーバプログラムに戻るときはSSHクライアントでサーバマシンにつなぎ、設定しておいたエイリアスを呼び出します。
# メンテナンスするときはアタッチすることで、再びFreecivサーバの画面に戻れます。
sufc
初期化スクリプトを編集したいときはnanoやmg(Emacs風のテキストエディタ)といったテキストエディタを使うと便利です。インストールするには、新しくSSH接続を開き、rootユーザとして次のコマンドを入力します。
apt update && apt install nano mg
mgを使うには次のように操作しましょう。
# freecivユーザに切り替えます。 su -l freeciv # mgでinit.servを開きます。 # `-f bsmap-mode`はバックスペースキーを使えるようにするオプションです。 mg -f bsmap-mode init.serv
保存するにはCtrl+xに続いてCtrl+s、終了するにはCtrl+xに続いてCtrl+cと入力します。
TODO
- セットアップスクリプトを用意して
scpで転送すれば、すぐにFreecivを動かせるようになるかも? - GNU Screenよりシンプルなdtachを使うのもよい考えです。
ゼロから作るDeep LearningをCommon Lispで書き直す(ステップ19)
ステップ19 変数を使いやすく
このステップでは次の機能を実装する。
- 変数に名前を付けられるようにする。
- 変数から配列の形状を知ることができるアクセサを実装する。
- プリティプリンタを実装する。
変数に名前を付ける
<variable>クラスにnameスロットを足し、コンストラクタ関数から指定できるようにする。
(defclass <variable> () ((data :initarg :data :accessor @data) (name :initarg :name :initform nil :accessor @name) ; 名前を記録する (gradient :initform nil :accessor @gradient) (creator :initform nil :accessor @creator) (generation :initform 0 :accessor @generation))) (defun <variable> (data &optional name) ; (<variable> #(3 4) "var A") (make-instance '<variable> :data data :name name))
アクセサ関数を増やす
NumPyのそれを参考にarray-operationsの関数を使って実装する。
ndarrayに対するlenって最初の次元の数であってるんだろうか?
(defmethod shape ((var <variable>)) "list of array dimensions" (aops:dims (@data var))) (defmethod ndim ((var <variable>)) "number of array dimenstions" (aops:rank (@data var))) (defmethod size ((var <variable>)) "number of elements in the array" (aops:size (@data var))) (defmethod dtype ((var <variable>)) "data type of array's elements" (aops:element-type (@data var))) (defmethod len ((var <variable>)) "number of array's first dimention" (aops:dim (@data var) 0))
プリティプリンタを実装する
デバッグのときに変数の中身が容易に見えると便利なのでprint-object総称関数にメソッドを実装する。 CLHSの書式文字列の説明と下記の記事を参考にした。
(defmethod print-object ((var <variable>) stream) (print-unreadable-object (var stream :type t :identity nil) (format stream "~:@_~<data: ~W ~_name: ~W ~_gradient: ~W ~_creator: ~W ~_generation: ~W~:>" (list (@data var) (@name var) (@gradient var) (@creator var) (@generation var))))) (<variable> #(3 2) "var A") ; => #<<VARIABLE> data: #(3 2) name: "var A" gradient: NIL creator: NIL generation: 0> (defmethod print-object ((func <function>) stream) (print-unreadable-object (func stream :type t :identity nil) (format stream "~<generation: ~W~:>" (list (@generation func)))))
そのほか
array-operationsのマニュアルを読んでいたら、vectorize-reduceというマクロがあることに気がついたので、勾配確認のときに書いたall-close関数を書き直して試してみた。最初のバージョンのall-close関数であるall-close-efvより、vectorize-reduceを使ったall-close-vrのほうが3割から4割短い時間で計算できることが分かった。マクロ展開してみると、all-close-efvではアキュムレータとなる配列を割り当てていたが、all-close-vrではそれがなかったため、おそらくそれが要因だろうと思う。
(defun all-close-efv (x y) (every (lambda (x) (<= x 1d-08)) (aops:flatten (aops:vectorize (x y) (/ (abs (- x y)) (abs x)))))) (defun all-close-vr (x y) (>= 1d-08 (aops:vectorize-reduce #'max (x y) (/ (abs (- x y)) (abs x))))) (let ((x (aops:generate* 'double-float (lambda () (1+ (random 1.0d0))) '(5000 5000))) (y (aops:rand '(5000 5000) 'double-float))) ;; warm-up (all-close-efv x y) (all-close-vr x y) ;; benchmark (time (all-close-efv x y)) (time (all-close-vr x y))) ;; Evaluation took: ;; 1.400 seconds of real time ;; 1.381522 seconds of total run time (1.283582 user, 0.097940 system) ;; [ Real times consist of 0.500 seconds GC time, and 0.900 seconds non-GC time. ] ;; [ Run times consist of 0.499 seconds GC time, and 0.883 seconds non-GC time. ] ;; 98.71% CPU ;; 5,287,418,680 processor cycles ;; 2,599,993,824 bytes consed ;; Evaluation took: ;; 0.939 seconds of real time ;; 0.944718 seconds of total run time (0.894239 user, 0.050479 system) ;; [ Real times consist of 0.090 seconds GC time, and 0.849 seconds non-GC time. ] ;; [ Run times consist of 0.087 seconds GC time, and 0.858 seconds non-GC time. ] ;; 100.64% CPU ;; 3,585,169,280 processor cycles ;; 2,400,026,576 bytes consed
ほかにはzeros-like関数やones-like関数が特殊化された配列(:element-typeがtでない)に対応できていなかったので、array-operationsを使って書き直した。
<function>クラスに対するforwardメソッドやbackwardメソッドは消しておく。単にエラーメッセージを出力するデフォルトメソッドを用意するより、メソッドが存在しないときにデバッガに落ちる方がCommon Lispらしいやりかただろう。
ゼロから作るDeep LearningをCommon Lispで書き直す(ステップ18)
ソースコード
ステップ18 メモリ使用量を減らすモード
計算の途中で得た微分は不要な場合が多いため、用が済んだら消去するように改良する。 また、逆伝播を無効にするモードも実装する。
モード切替を実装するにあたり、backward総称関数にキーワード引数を追加する代わりにスペシャル変数を定義することにした。
というのも、オリジナルのコードでretain_gradは一カ所しか使っていないし、enable_backpropとコンテキストマネージャの件はCommon Lispが持つスペシャル変数の典型的な用例ではないかと思ったからである。その特性から言って、総称関数はインターフェースとして安定しているべきなんだろう。
<variable>クラスのbackwardメソッドでは、uiop:if-letを使った計算をwith-accessorsを使って置き換え、読みやすさを改善してみた。loopをdolistで置き換え、インデントも直した。
ゼロから作るDeep LearningをCommon Lispで書き直す(ステップ17)
ソースコード
ステップ17 メモリ管理と循環参照
弱参照を使うことで、循環参照を解消し、メモリ使用量を減らす。 循環参照があると、参照カウント方式のメモリ管理では解放が難しいという。ガベージコレクタの方式によっては循環参照になっているオブジェクトでも解放できるが、コストが高いらしい。マニュアルによればSBCLは世代別GCを持っているとのことである。
Common Lispでは弱参照がANSI Common Lispの範囲にないため、処理系ごとにAPIがばらばらに定義されている。そこで、統一的に扱うためのラッパーとしてtrivial-garbageというライブラリがあるので使うことにした。
Pythonのweakref.ref関数のように弱参照を作る関数はtg:make-weak-pointerである。同様に弱参照からオブジェクトを得るにはtg:weak-pointer-value関数を呼べばよい。
Common Lisp標準のtimeマクロとSBCLのsb-sprofというプロファイラを使ってメモリ使用量を測ってみたところ、ステップ16とステップ17ではメモリ使用量にあまり差は出なかった。何がおかしいのだろうか?
ゼロから作るDeep LearningをCommon Lispで書き直す(ステップ14~16)
ソースコード
ステップ14 同じ変数を繰り返し使う
微分を累算して、同じ変数を使っても結果がおかしくならないようにする。
オリジナルのコードにある次のコード片をCommon Lispにどう翻訳するか少し悩んだ。そのままだと(@gradient x)という式があちこちに出てくるし、aops:vectorizeマクロで書けないのである。
if x.grad is None: x.grad = gx else: x.grad = x.grad + gx
解決策として、UIOPというライブラリで定義されたuiop:if-letというマクロを使うことにした。uiop:if-letは判定に使う値に名前を付けて、フォームの中で使えるようにしてくれるマクロである。
ここで、UIOPはよくある処理に対するユーティリティを多数提供するライブラリである。Common Lisp処理系には、CのMakeにあたるASDFというビルドツールが付属しており、UIOPはASDFの部品として同梱されているので、準標準ライブラリとして使える。
(setf (@gradient x) (uiop:if-let ((agx (@gradient x))) (aops:vectorize (agx gx) (+ agx gx)) gx))
見直してみて気が付いたが、fletを使ってローカル関数を定義する、with-accessorマクロを使うことでも解決できそう。
微分をリセットするためのclear-gradientメソッドも用意した。
(defmethod clear-gradient ((var <variable>)) (setf (@gradient var) nil))
ステップ15 複雑な計算グラフ(理論編)
説明だけなので省略。
ステップ16 複雑な計算グラフ(実装編)
関数や変数に世代を覚えさせることで、逆伝播を正しく計算できるようにする。
そのためにgenerationというスロットを用意し、世代順にソートを行う。
Common Lispのsort関数は実践 Common Lispの著者がいうところのリサイクルな関数なので、必ず返り値を受け取ってsetfする。
実践 Common Lispは、載せられている事例が古びてきているものの、Common Lispを学ぶための最初の一冊として素晴らしいと思う。原書は無料公開されているので、読める人はぜひ。
ゼロから作るDeep LearningをCommon Lispで書き直す(ステップ11~13)
ソースコード
ステップ11 可変長の引数(順伝播編)
可変長引数を取り扱うため、callやforwardのシグネチャを変更し、関数の入力も出力もリストとなるよう変更する。
総称関数の特性上影響が大きく、コンパイルエラーを参考に直していくことになる。
ステップ12 可変長の引数(改善編)
Common Lispの関数は、仮引数リストの中で&restを使うことで可変長引数を受け取れるようになる。
forwardは総称関数として実装しているので、すべてのクラスで同じシグネチャにしなければならない。
オリジナルを見ると、仮引数リストが異なる同名のメソッドが定義されており、これでも問題ないところに違いを感じた。
apply関数を使って、リストをアンパッキングし、可変長の引数を渡すようにする。
ステップ13 可変長の引数(逆伝播編)
<variable>クラスのbackwardメソッドを直し、可変長引数に対応できるようにする。
また、<square>クラスも可変長引数に対応させる。だんだんややこしいコードになってきた。
ゼロから作るDeep LearningをCommon Lispで書き直す(ステップ9~10)
ソースコード
ステップ9 関数をより便利に
まず、呼び出し側が毎回gradientに全要素が1の配列を設定しなければならないのを直す。
ユーティリティ関数として次のようなものを定義する。
(defun full-like (array fill-value) (let ((dims (array-dimensions array))) (make-array dims :initial-element fill-value))) (defun ones-like (array) (full-like array 1))
あとは次の行を書いておけば、gradientが未設定の時、初期値を設定できる。
(unless (@gradient var) (setf (@gradient var) (ones-like (@data var))))
defclassの:initformオプションを使ってもよかったかもしれない。
次に、<variable>クラスに好き勝手な値を設定できないようにする。これにはcheck-typeを使うことで実行時に型を検査できる。インスタンスを作るときの挙動を変更するにはinitialize-instance総称関数をオーバーライドすればよい。
(defmethod initialize-instance :after ((var <variable>) &key) (check-type (@data var) array))
渡された値が数値なら配列に包み、配列であればそのまま返す関数も用意する。
(defun ensure-array (x) (if (numberp x) (vector x) x))
ステップ10 テストを行う
Common Lispではテストフレームワークがいくつか知られており、特にFiveAMが最も代表的なものとされている。今回はroveを試すことにした。
数値微分とバックプロパゲーションの結果を比較し、計算が正しいか検証するため勾配確認という方法を使う。オリジナルではNumPyのallclose関数を使うことでほぼ等しいか判定できていたがCommon Lispにはそういうものはない。そこで次のような関数を定義することにした。
(defun all-close (x y) (every (lambda (x) (<= x 1d-08)) (aops:flatten (aops:vectorize (x y) (/ (abs (- x y)) (abs x))))))
配列の各要素について差を求め、1次元配列につぶし、差が定数以下であればほぼ等しいと判定する。効率は良くないかもしれないが、今のところはこれで十分だろう。この辺りはmsyksphinzさんの記事を参考にさせていただいた。
