焼きなまし法のコツ Ver. 1.3

焼きなまし法そのものの解説は少しだけにして、焼きなまし法の使い方のコツをメインに書こうと思います。

焼きなまし法の概要

最適化(=最大値か最小値を求める)の手法の1つです。以下は擬似コードです。

あらかじめ、計算する時間を決める。
以下のスコープ内のコードを時間経過するまで繰り返す。
{
    適当に、次の状態(近傍)を選ぶ。
    (1) 次の状態のスコアが、今の状態のスコアより良くなった場合は、常に、状態を次の状態にする(遷移)。
    (2) 次の状態のスコアが、今の状態のスコアより悪くなった場合でも、ある確率で、状態を次の状態にする。
        確率は以下のように決める。
       (2.A) 序盤(経過時間が0に近いとき)ほど高確率で、終盤ほど低確率。
       (2.B) スコアが悪くなった量が大きければ大きいほど、低確率。
}
最後に、今までで一番スコアのよかった状態を選ぶ。

次の状態が悪くてもそれを選ぶ可能性がある(2)の部分が、焼きなまし法の最大の特徴です。

「状態」「スコア」というのがピンとこないかもしれませんが、

  • もし、巡回セールスマン問題であれば、
    • 状態 →ルート
    • 次の状態(近傍)→元のルートをちょっとだけかえてみたルート
    • スコア →総距離
  • もし、y=f(x)の最大値を求める問題であれば、
    • 状態 →x
    • 次の状態(近傍)→ x' = x + a (aは小さめの値)
    • スコア →y

といったかんじです。

サンプルソースコード

AtCoderのコンテストIntroduction to Heuristics Contest - AtCoderで、スケジューリング問題が出題されました。 この問題は焼きなまし法が効果的です。C++でのサンプルを2つ用意しました。変数名は問題に合わせています。

サンプル1

単純な焼きなまし法。1165人中400位相当の解法です。焼きなまし法の本質は、// 焼きなまし法(ここから)の部分です。
AtCoderの提出コード


ソースコードを開く

#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>
#include <cassert>
#include <climits>
#include <cmath>

using namespace std;
using namespace chrono;

// 0以上UINT_MAX(0xffffffff)以下の整数をとる乱数 xorshift https://ja.wikipedia.org/wiki/Xorshift
static uint32_t randXor()
{
    static uint32_t x = 123456789;
    static uint32_t y = 362436069;
    static uint32_t z = 521288629;
    static uint32_t w = 88675123;
    uint32_t t;

    t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
}

// 0以上1未満の小数をとる乱数
static double rand01()
{
    return (randXor() + 0.5) * (1.0 / UINT_MAX);
}

const static int NUM_TYPES = 26; // コンテストの種類数

// 全体のスコア(今回の問題のスコア)
static int getFullScore(const int D, const vector <int>& C, const vector <int>& T, const vector<vector<int>>& S)
{
    int score = 0;

    vector <int> last(NUM_TYPES, 0);

    for (int d = 0; d < D; d++)
    {
        const int j = T[d];
        last[j] = d + 1;
        for (int t = 0; t < NUM_TYPES; t++)
        {
            score -= (d + 1 - last[t]) * C[t];
        }
        score += S[d][j];
    }

    return score;
}

int main()
{
    //  入力
    int D;
    cin >> D;
    vector<int> C(NUM_TYPES);
    for (int &c : C)
    {
        cin >> c;
    }

    vector<vector<int>> S(D, vector<int>(NUM_TYPES));
    for (auto &s : S)
        for (auto &x : s)
        {
            cin >> x;
        }

    // 初期解
    vector<int> T(D);   // T[d] d日目に開くコンテスト
    for (int d = 0; d < D; d++)
    {
        T[d] = d % NUM_TYPES;
    }

    // 焼きなまし法(ここから)
    auto startClock = system_clock::now();
    int score = getFullScore(D, C, T, S);   // 現在のスコア
    int bestScore = INT_MIN;                // 最高スコア
    vector <int> bestT;                     // 最高スコアを出したときのT

    const static double START_TEMP = 1500; // 開始時の温度
    const static double END_TEMP = 100; // 終了時の温度
    const static double END_TIME = 1.8; // 終了時間(秒)

    double time = 0.0;   // 経過時間(秒)
    do
    {
        const double progressRatio = time / END_TIME;   // 進捗。開始時が0.0、終了時が1.0
        const double temp = START_TEMP + (END_TEMP - START_TEMP) * progressRatio;

        // 近傍 : d日目のコンテストをtに変えてみる。d,tはランダムに選ぶ。
        const int d = randXor() % D;
        const int t = randXor() % NUM_TYPES;
        const int backupT = T[d];
        int deltaScore = 0; // スコアの差分 = 変更後のスコア - 変更前のスコア
        deltaScore -= getFullScore(D, C, T, S);
        T[d] = t;   // 変更
        deltaScore += getFullScore(D, C, T, S);
        const double probability = exp(deltaScore / temp); // 焼きなまし法の遷移確率

        if (probability > rand01())
        {
            // 変更を受け入れる。スコアを更新
            score += deltaScore;
            if (score > bestScore)
            {
                bestScore = score;
                bestT = T;
            }
        }
        else
        {
            // 変更を受け入れないので、元に戻す
            T[d] = backupT;
        }

        time = (duration_cast<microseconds>(system_clock::now() - startClock).count() * 1e-6);
    } while (time < END_TIME);
    // 焼きなまし法(ここまで)


    // 出力
    for (int t : bestT)
    {
        cout << t + 1 << endl;
    }
    cerr << "bestScore: " << max(0, bestScore + 1000000) << endl;

    return 0;
}


サンプル2

やや複雑ですが、かなり改善した焼きなまし法。1位相当の解法です。
AtCoderの提出コード


ソースコードを開く

#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>
#include <cassert>
#include <climits>
#include <cmath>

using namespace std;
using namespace chrono;

// 0以上UINT_MAX(0xffffffff)以下の整数をとる乱数 xorshift https://ja.wikipedia.org/wiki/Xorshift
static uint32_t randXor()
{
    static uint32_t x = 123456789;
    static uint32_t y = 362436069;
    static uint32_t z = 521288629;
    static uint32_t w = 88675123;
    uint32_t t;

    t = x ^ (x << 11);
    x = y; y = z; z = w;
    return w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
}

// 0以上1未満の小数をとる乱数
static double rand01()
{
    return (randXor() + 0.5) * (1.0 / UINT_MAX);
}

const static int NUM_TYPES = 26; // コンテストの種類数

// 全体のスコア(今回の問題のスコア)
static int getFullScore(const int D, const vector <int>& C, const vector <int>& T, const vector<vector<int>>& S)
{
    int score = 0;

    vector <int> last(NUM_TYPES, 0);

    for (int d = 0; d < D; d++)
    {
        const int j = T[d];
        last[j] = d + 1;
        for (int t = 0; t < NUM_TYPES; t++)
        {
            score -= (d + 1 - last[t]) * C[t];
        }
        score += S[d][j];
    }

    return score;
}

// d日目のコンテストT[d]を開いたときのスコア増分
static int getContestScore(const int d, const int D, const vector <int>& C, const vector <int>& T, const vector<vector<int>>& S)
{
    const int t = T[d];
    int score = S[d][t];

    int prev = d - 1;   // d日目以前に開かれる同じコンテストの日
    while (prev >= 0 && T[prev] != t)
    {
        prev--;
    }

    int next = d + 1;   // d日目以降に開かれる同じコンテストの日
    while (next < D && T[next] != t)
    {
        next++;
    }

    // sum(x) = x*(x+1)/2 つまり1,2...,xまでの等差数列の和とすると
    //	score -= sum(next - prev -1) * (-C[t]);                       // ... (1)
    //	score += sum(day  - prev -1) + sum(next - day  -1) * (-C[t]); // ... (2)

    // p---------n コンテストをおく前
    //  123456789 // ... (1)

    // p---d-----n コンテストをおいた後
    //  123 12345 // ... (2) 

    score += (next - d) * (d - prev) * C[t];    // (1)と(2)を計算すると、簡単になる。

    return score;
}

int main()
{
    auto startClock = system_clock::now();

    //  入力
    int D;
    cin >> D;
    vector<int> C(NUM_TYPES);
    for (int &c : C)
    {
        cin >> c;
    }

    vector<vector<int>> S(D, vector<int>(NUM_TYPES));
    for (auto &s : S)
        for (auto &x : s)
        {
            cin >> x;
        }

    vector<int> T(D);   // T[d] d日目に開くコンテスト
    for (int d = 0; d < D; d++)
    {
        T[d] = d % NUM_TYPES;
    }

    int score = getFullScore(D, C, T, S);   // 現在のスコア
    int bestScore = INT_MIN;                // 最高スコア
    vector <int> bestT;                     // 最高スコアを出したときのT

    const static double START_TEMP = 1500; // 開始時の温度
    const static double END_TEMP   =  100; // 終了時の温度
    const static double END_TIME   =  1.8; // 終了時間(秒)

    double temp = START_TEMP;   // 現在の温度

    long long steps;    // 試行回数
    for (steps = 0; ; steps++)
    {
        if (steps % 10000 == 0)
        {
            const double time = duration_cast<microseconds>(system_clock::now() - startClock).count() * 1e-6;   // 経過時間(秒)
            if (time >= END_TIME)
            {
                break;
            }

            const double progressRatio = time / END_TIME;   // 進捗。開始時が0.0、終了時が1.0
            temp = START_TEMP + (END_TEMP - START_TEMP) * progressRatio;
        }

        if (rand01() > 0.5)
        {
            // 近傍1 : d日目のコンテストをtに変えてみる
            const int d = randXor() % D;
            const int t = randXor() % NUM_TYPES;
            const int backupT = T[d];
            int deltaScore = 0;

            // 変更前のd日目のコンテストのスコアを引き、変更して、変更後のコンテストのスコアtを足す。
            deltaScore -= getContestScore(d, D, C, T, S);
            T[d] = t;
            deltaScore += getContestScore(d, D, C, T, S);
            const double probability = exp(deltaScore / temp); // 焼きなまし法の遷移確率

            if (probability > rand01())
            {
                // 変更を受け入れる。スコアを更新
                score += deltaScore;
                if (score > bestScore)
                {
                    bestScore = score;
                    bestT = T;
                }
            }
            else
            {
                // 変更を受け入れないので、元に戻す
                T[d] = backupT;
            }
        }
        else
        {
            // 近傍2 : d0日目のコンテストとd1日目コンテストを交換してみる
            const int diff = randXor() % 10 + 1;    // d0とd1は10日差まで
            const int d0 = randXor() % (D - diff);
            const int d1 = d0 + diff;

            int deltaScore = 0;

            // 変更前のd0日目とd1日目のコンテストのスコアを引き、変更後のd0日目とd1日目のコンテストのスコアを足す
            deltaScore -= getContestScore(d0, D, C, T, S);
            deltaScore -= getContestScore(d1, D, C, T, S);
            swap(T[d0], T[d1]);
            deltaScore += getContestScore(d0, D, C, T, S);
            deltaScore += getContestScore(d1, D, C, T, S);

            const double probability = exp(deltaScore / temp); // 焼きなまし法の遷移確率
            if (probability > rand01())
            {
                // 変更を受け入れる。スコアを更新
                score += deltaScore;
                if (score > bestScore)
                {
                    bestScore = score;
                    bestT = T;
                }
            }
            else
            {
                // 変更を受け入れないので、元に戻す
                swap(T[d0], T[d1]);
            }
        }
    }

    // 出力
    for (int t : bestT)
    {
        cout << t + 1 << endl;
    }
    cerr << "steps: " << steps << endl;
    cerr << "bestScore: " << max(0, bestScore + 1000000) << endl;

    return 0;
}

焼きなまし法のコツ

が多いほど、重要だと思っています。

1. 高速化

試行回数が多いほど、スコアが伸びる可能性があります。基本的にはボトルネックを調べて、そこを高速化しましょう。だいたい以下のところがボトルネックになるでしょう。

★★★★ プロファイルして、計算時間がかかっているボトルネックを探す

2021/03/06 追加しました。
焼きなまし法に限らず、マラソンマッチに限らず、プログラミング一般であらゆる高速化の前にやるべきことです。プロファイルの仕方はOSやツールにより違いますが、例えばVisual Studioなら「デバッグ」→「パフォーマンス プロファイラ」で、一発でできます。思わぬところがボトルネックになってることも多いので、絶対しましょう。
f:id:shindannin:20210306115133p:plain

★★★★ 高速化がどれだけ効果的か検討する。

重要です。ためしに10秒だけやってる計算を、あえて100秒とかに伸ばしてみて、結果がどうなるか見てみましょう。

  • 時間をかければかけるほどスコアが伸びる
    • 高速化しよう&スコアが素早く伸びるように次の状態を決めよう。山登り法に変えるのも良い。
  • 時間をかけてもスコアが伸びない & スコアが高い
    • 最適解がでてる可能性が高い。計算を打ち切って、余った時間を他に使う
  • 時間をかけてもスコアが伸びない & スコアが低い
    • 次の状態の決め方、スコア関数の見直しを再検討する。焼きなまし法を使わない法がよいときもあるのも忘れずに

こういった実験は自分のPCで行うわけなので、並列化などを行い計算リソースも存分に使ってよいです。

★★★★ 変更がある変数だけを保存して、戻す

先ほどのコードだと、

        const int backupT = T[d];

        // (略)    

        {
            // 変更を受け入れないので、元に戻す
            T[d] = backupT;
        }

の部分です。変更した、T[d]の部分だけをバックアップしておけば、元に戻せます。T全体を保存する必要はありません。

★★ 逆の手で、戻す

逆の手をやれば、前の状態に高速に戻せるのであれば、そうしましょう。例えば、上下左右に駒をうごかせるというパズル問題であれば、左に動かした後であれば、右に動かせば、戻せます。また、2つをswapするといった場合は、もう1回swapすれば戻せます。

★★★★ スコア関数の高速化

スコア関数の求め方は問題によって大きく異なるので、計算時間も異なってきます。ボトルネックになってたら高速化しましょう。
また、スコア関数の正確さを犠牲にしてでも、高速化したほうが良い場合もあります。1つのコツとして覚えて置きましょう。

★★★★ スコア差分計算時の打ち切り

2021/03/06 追加しました。
スコア差分計算の途中で、差分がすでに大きくマイナスになっていて、もうその後計算し続けていても絶対に遷移の受理確率がほぼ0っぽいのであれば、差分計算を打ち切って高速化することができます。焼きなまし法は遷移が成功するケースより遷移が失敗するケースが多く、失敗するケースをさっさと打ち切ることができれば高速化につながることが多いです。
また問題によっては、スコア差分の上界・下界が分かるケースもあり、これらが短時間で求められるのであれば、それもスコア差分計算の打ち切りに使用可能です。例えば、Google Hash Code 2021の練習問題では、10000ビットのORを取り、(ビットが1の数)の2乗がスコアになる問題がありました。10000ビットのORはそれなりに時間がかかりますが、

(ほんとは10000ビットある)

111001010100117個)
001100010100014個)  
↓ OR
111101010100119個。つまり81点)  

と実際にORをとらずとも、

111001010100117個)
001100010100014個)  
↓ OR
???????????????    (1が最小で7個、最大で11個、つまり49点~121点のあいだ)  

というのが分かれば、打ち切り時の計算の参考になります。

★★★ 状態を変えずにスコア差分を計算する

2021/03/06 追加しました。
状態の更新をする部分がボトルネックとなっている場合は、
サンプル2の以下のような書き方よりも、

状態を変える
差分スコアを求める
遷移確率probabilityを求める
if (probability > rand01()) // 遷移条件を満たすか
{
  // 変更を受け入れる。スコアを更新
}
else
{
  // 変更を受け入れないので、状態を元に戻す
}

以下のような形のほうが良いです。なぜなら状態遷移しないときに状態更新の計算を省けるからです。また、スコア差分計算時の打ち切りを行う際も、打ち切った際に状態を戻す必要がないため、この形のほうが恩恵が大きいです。

状態を変えずに差分スコアを求める
遷移確率probabilityを求める
if (probability > rand01()) // 遷移条件を満たすか
{
  // 変更を受け入れる。スコアを更新。状態も更新
}

ただ、短時間で書けるほうは大抵サンプル2の形なので、まずはサンプル2の形で書いて、その後この形に持っていくのが良いと思います。

★★ C言語のrand()を使わない。

C言語のrand()は遅いので、高速なxor128を使いましょう。

unsigned int randxor()
{
    static unsigned int x=123456789,y=362436069,z=521288629,w=88675123;
    unsigned int t;
    t=(x^(x<<11));x=y;y=z;z=w; return( w=(w^(w>>19))^(t^(t>>8)) );
}
時間計測の関数を呼びすぎない。

サンプル2のように、10000回に1回だけ時間計測をするという方法があります(時間計測の関数はsystem_clockを使えば近年は十分速いですが)。時間計測以外の値でも、毎回計算する必要がないものがあればここで計算してしまいましょう。

    long long steps;    // 試行回数
    for (steps = 0; ; steps++)
    {
        if (steps % 10000 == 0)
        {
            const double time = duration_cast<microseconds>(system_clock::now() - startClock).count() * 1e-6;   // 経過時間(秒)
            if (time >= END_TIME)
            {
                break;
            }

ただ、TopCoderだとこの回数を増やしすぎて、制限時間オーバーしてしまうという可能性もあります。そういった点も考えて、適切な回数おきに時間をはかるようにしましょう。残り時間が少なくなったら、ループ回数を減らすといった安全策をとる方法もあります。

2. 次の状態(状態近傍)をどう決めるか?

ここは鍵になる部分ですが、コツとしてまとめるのは難しいところですね…。

★★★★ スコアが改善しやすくなるように、次の状態を選ぶ。


これは、問題ごとによく考えて対処しなければいけないので、あまり「コツ」というかんじではないのですが…。

[問題]
タスク数aの仕事Aと、タスク数bの仕事Bがある。
作業員はx人いて、それぞれ、1時間あたりにこなせる仕事Aのタスク数と、仕事Bのタスク数は決まっている。
ただし、仕事Aの仕事Bのどちらかしか行うことができない。
仕事Aと仕事Bの両方が終わるのに時間が最小になるように、作業員を仕事Aか仕事Bのどちらかに割り当てよ。

焼きなまし法で解くなら、まず最初にてきとーに作業員をA,Bに割り振り、ここからスコア=作業時間として、改善させていきます。こういった問題を解くときに、次の状態として以下の2つを考えてみましょう。

  1. 移動:ある作業員をA→B または B→A に移動させる。
  2. 交換:Aの作業員の誰かと、Bの作業員の誰かを交換する。

移動2回は交換になることもあるので、とりあえず移動をさせておけば、交換もたまにしてくれるし良さそうに見えますが、そうとは限りません。この場合は、交換を主に行い、たまに移動させるぐらいがちょうど良いと思います。移動では、人数が偏り、片方の仕事の作業時間が増えやすくなります。2回移動すれば、交換になって、作業時間が減る可能性がありますが、

  • 移動:良スコア → 悪いスコア → とても良いスコア
  • 交換:良スコア    → とても良いスコア

と考えると、移動だけだと、スコアが悪い状態を経由しないとダメなので、運がよくないと、とても良いスコアまで辿りつけません。その点、交換ならば、人員が偏った悪いスコアになりやすい状態をスキップできるので、スムーズにとても良いスコアまで改善できます。

もちろん、正しい人数割り当てもしなければいけないので、移動も少しは試したほうがいいですが、こういった点も考えて次の状態を決めると、スコアが改善しやすくなります。

★★★★ 近傍の種類を増やす

2021/03/06 追加しました。

近傍の種類は多くあればあるほど良いです。上記のサンプル2のコードには、近傍が2種類用意されています。

            // 近傍1 : d日目のコンテストをtに変えてみる
            const int d = randXor() % D;
            const int t = randXor() % NUM_TYPES;
            // 近傍2 : d0日目のコンテストとd1日目コンテストを交換してみる
            const int diff = randXor() % 10 + 1;    // d0とd1は10日差まで
            const int d0 = randXor() % (D - diff);
            const int d1 = d0 + diff;

サンプル2では、近傍1も近傍2も50%ずつの確率で選んでいますが、ここをどの近傍をどれぐらいの確率で試すかを調整すれば、性能を改善できるチャンスがあります。また、統計をとるのも大事です。遷移が成功した回数・失敗した回数のデータを取っておけば、どの近傍が有効なのかが分かり、問題を理解するヒントにもなります。さらに、「近傍1で遷移が成功すると、その直後で近傍2で遷移が成功しやすい」などの事実が分かれば、あらたに近傍3「近傍1のあとに近傍2」という新たな近傍を用意することもできるでしょう。

★★ Greedyに、次の状態を選ぶ。

先ほどの問題であれば、「仕事Aと仕事Bそれぞれから、常に作業時間が一番多くかかっている人を1人ずつ選び、交換」とGreedyに、次の状態を選ぶことができます。こういう選び方をすると、ベストスコアは序盤はどんどん伸びていきますが、局所最適に陥りやすいです。また、ありがちなのが、Greedyに選ぶアルゴリズムが、計算時間のボトルネックになってしまうパターンです。複雑なアルゴリズムより、ランダムでとにかくいろいろ試すのが焼きなまし法の強みです。Greedyにやるにせよ、多少ランダムを入れたり、計算時間はかからない単純な選び方をしたりする工夫が必要です。

★★ 序盤は大きく変化、終盤は小さく変化させたほうがいい。

単純にy=f(x)のyを最大化する問題なら、

  • 序盤(t=0に近いとき) 次のx'は x'=x+rand()%1001 - 500
  • 終盤(t=Tに近いとき) 次のx'は x'=x+rand()% 11 - 5

とか、
作業員割り当ての問題なら、

  • 序盤(t=0に近いとき) 交換を100回やる
  • 終盤(t=Tに近いとき) 交換を 1回やる

といったかんじです。スコアが局所最適でハマっている場合は有効です。ただ、焼きなまし法自体が、局所最適にはまりにくいアルゴリズムなので、あまり効果がない場合も多いです。

★★★★ 状態が大きく変化するわりには、スコアが変わらない近傍は良い。

2021/03/06 追加しました。

  • 状態変化が大きいけど、スコアが大きい変化しかしない(悪化する確率が圧倒的に高い)
  • 状態変化が小さいけど、スコアが小さい変化しかしない近傍

となりがちなのですが、問題によっては状態が大きく変化するわりには、スコアがあまり変化がしないケースがあります。こういう近傍があると、遷移が小さい近傍だけは辿りつけない状態にも短時間で行くことができ、焼きなましの性能がアップします。見逃さないようにしましょう。

★★★ 初期状態からすべての状態に行けるようにする。

2021/03/06 追加しました。
選んだ初期状態によっては、どんなに運がよくても用意した近傍ではすべての状態に辿りつけない場合があります。特に解に制約条件があるような問題(例:パズル問題など)で、このようなことが起こりやすいです。すべての状態にいけるような近傍を用意しましょう。

  • 無理にランダムに大きく状態が変化するような近傍を入れてしまうと、それ自体が遷移の成功確率を大きく減らすことにもなり、全体の性能を落とす可能性があるので、簡単ではありません。
  • 多点スタート(後述)にすれば、ある程度緩和されますが、多点スタートでは1つの解を求めるのにかけられる計算時間が減ってしまい、あまりよくありません。
  • スコア関数にペナルティ(後述)を入れる方法もあります。
★★ 良い初期状態からスタートする

これは問題に大きく依存する項目です。初期状態はてきとーでも良い場合と、すこし時間をかけて計算して良い状態から始めたほうが良い場合があります。特に、初期状態が悪いと状態を改善する近傍の選択肢が少なすぎる、逆に言えば、状態を改善すればするほどさらに改善できる可能性が増えるタイプの問題は、初期状態に少し時間を書けて求めて、それから焼きなましたほうが良い結果がでる場合があります。

3. スコア関数

スコア関数(scoring function)とも、評価関数(evaluation function)とも呼びます。

★★★★★ 良い状態を正しく評価できるような、スコア関数を新たに用意する

問題のスコア関数をそのまま使うと、焼きなまし法がうまく行かないことがあります。良い状態を正しく評価できるような、新たなスコア関数を用意しましょう。

[問題]
 (大きく略)…  i番目の人の満足値をs(i)とする。「満足度の最小値 min s(i) 」を、最大化しなさい。

この問題は「満足度の最小値 min s(i) 」を最大化する問題なので、素直に考えると、スコア=「満足度の最小値 min s(i) 」としたくなりそうですが、これは良くありません。仮にそうした場合、

  状態A s(0)=  10, s(1)=10, s(2)=  10 のとき、スコアはmin{s(0),s(1),s(2)} = 10
  状態B s(0)=1000, s(1)=10, s(2)=1000 のとき、スコアはmin{s(0),s(1),s(2)} = 10

と状態Aと状態Bのスコアは同じになってしまいます。これはスコアは同じといえ、状態Bのほうが明らかに良い状態です。なぜなら、s(1)の点が仮に300点に増えたとしたら

  状態A' s(0)=  10, s(1)=300, s(2)=  10 のとき、スコアはmin{s(0),s(1),s(2)} =  10
  状態B' s(0)=1000, s(1)=300, s(2)=1000 のとき、スコアはmin{s(0),s(1),s(2)} = 300

と状態Bのほうは一気にスコアが増えます。しかし元のスコア関数のままだと状態Aと状態Bは同じスコアで、状態A→状態Bはスコア改善になりません。つまり焼きなまし法を行っても、状態A→状態Bには運が良くないと移動しません。
この問題であれば、例えば、
スコア=1番小さいs(i) + 0.0001 * 2番目に小さいs(i)
とすれば

  状態A s(0)=  10, s(1)=10, s(2)=  10 のとき、スコアは 10 + 0.0001 *   10  = 10.001
  状態B s(0)=1000, s(1)=10, s(2)=1000 のとき、スコアは 10 + 0.0001 * 1000  = 10.1

と状態Bの方が好スコアになり、状態A→状態Bに移動しやすくなります。
ここは問題によって大きく有効かどうか大きく異なってきますが、必ず考慮に入れたほうがいいでしょう。結果に大差がつきやすいポイントです。

★★★スコアが改善しやすくなるようなスコア関数を決める。

上記の言い換えともいえるのですが、わりと焼きなまし法のスコア関数をつくるときに、問題のスコア関数をそのまま使ったり、違う状態を正しく評価しなかったりすると、同じスコアになりがちで、青い線のような飛び飛びの平坦な関数になることが多いです。赤いように、山登りしやすいスコア関数を作れないか考えましょう。必ずしも、問題のスコア関数の最大値と、焼きなまし法で使うスコア関数の最大値が一致する必要はありません。最大値をとりうるような状態(argmax)を一致させることが重要です。
f:id:shindannin:20150526014955p:plain

★★★スコア増分が0のときを、無くす。

2021/03/06 追加しました。
スコア増分が0のときは、焼きなまし法をやっても状態が行ったりきたりブラウン運動するだけで、解がなかなか改善しません。また、遷移するときの処理のほうが遷移しないときより重くなる場合、さらにひどいことになります。必ずスコア増分の履歴をチェックし、スコア増分が0になっているときはスコアの差をつけましょう。

★★★ 制約条件がある問題では、スコア関数にペナルティをつける。

2021/03/06 追加しました。
制約条件がある問題で、「制約条件を満たさない状態への遷移を許さない」とすると、ほとんど状態遷移されないままになってしまうことがあります。こういうときの対策としては、制約を満たしていない解も許して、ペナルティをつけて、スコア関数から減点します。

  • 制約条件を満たしている度合いがひどいほど、ペナルティは大きく減点します。
  • 計算の序盤ではペナルティは小さく、徐々にペナルティを大きくして、最後には絶対に制約条件を満たすようにします。
  • 最後にでてきた最適解が制約条件を満たしているか確認しましょう(ただ、そうならないようなペナルティのつけ方を見つけるのが理想です。)

4. 遷移確率と温度

たいていの焼きなまし法の解説書には、expを使った方法が載っています。
焼きなまし法 - Wikipedia

遷移確率 P(e, e' , T) は e' < e の時に 1 を返すよう定義されている(すなわち下りの移動は必ず実行される)。
そうでない場合の確率はexp((e−e' )/T) と定義 

注:wikipediaの引用なので、本文の設定と違います。
    ここでのTは温度(=時間とともに減っていく値)です。
    最小化問題なのでe−e'となってます(最大化問題ならe'-eです)。
時刻tに置ける温度Tを調整する

最初の例の以下のコードの部分です。

     const bool FORCE_NEXT = R*(T-t)>T*(rand()%R);

上の例だと、

  • t=0のとき、常にtrue
  • t=Tのとき、常にfalse

になるような線形関数にしています。だいたいの場合は、これで問題ありませんが、バリエーションはいろいろ考えられます。実際には、ベストスコアの更新状況をみて、更新があまりに序盤と終盤に固まっているときは、2乗してみるとか、ロジスティック曲線をつかうとか、その程度で良いと思います。

★★★★exp(e-e')/Tを使う。(e-e':スコアの悪化量)

wikipediaでは以下のように書いていますが、exp(e-e')/Tの形にしたほうが良いでしょう。(理由は、次回の改定時に書きます)

焼きなまし法 - Wikipedia

関数 P は e' −e の値が増大する際には確率を減らす値を返すように設定される。
つまり、ちょっとしたエネルギー上昇の向こうに極小がある可能性の方が、
どんどん上昇している場合よりも高いという考え方である。しかし、この条件は
必ずしも必須ではなく、上記の必須条件が満たされていればよい。

ただ、このexpの式、分子はスコア差なので分かりますが、分母の温度をどう決めれば良いかは分かりづらいです。目安としては、たまには許容しても良いスコアの悪化量を温度とすると良いです。 最大の山を越えられるようにと考えると、考えられるスコアの最大値-最小値を初期温度を設定したほうが良いのですが、実際には温度調整して最終スコアがどうなるか実験してみてください。

double startTemp   =  100;
double endTemp     =   10;
double temp        = startTemp + (endTemp - startTemp) * t / T;
double probability = exp((nextScore - currentScore) / temp);  // 最大化なので、分子はnextScore - currentScoreで良い。
bool FORCE_NEXT    = probability > (double)(mXor.random() % R) / R;

時刻t=0(最初)でtemp=startTemp、時刻t=T(最後)でtemp=endTempとなります。

例えばスコアが100悪化したとき、nextScore - currentScore = -100なので、startTempを100にしてみます。endTempはとりあえず10にしてみましょう。
時間t=0であれば、exp(-100/100) = exp(- 1) = 0.36787944117と、3回に1回ぐらいは遷移します。
時間t=Tであれば、exp(-100/ 10) = exp(-10) = 0.00004539992と、まず遷移はしません。
このような感じでやると、良いと思います。

5. アレンジ

★★★★ 山登り

最初の例をFORCE_NEXT=0にすれば、山登り法になります。そもそも関数が凸であれば、山登り法も焼きなまし法も結果は一緒で、しかもrand()を使わない分、山登り法のほうが高速で、スコア改善のチャンスも多くなります。簡単に試せるので、試しましょう。

多点スタート

焼きなまし法をつかっても、局所解にはまりやすい場合は、初期値を毎回かえた、多点スタートにする方法もあります。山登りと同時に使う方法もあります。ただ、多点スタートで結果がよくなるケースは、そもそもスコア関数や近傍が良くないので、そこを見直したほうがよいかもしれません。

★★★ 1変数ごとに焼きなまし

焼きなまし法で、たとえば、y = f(x1,x2,x3,x4,x5)と、状態変数が多いとき、次の状態のパターンが多くなり、結果的にあまり多くの状態を試せず、良い解が求められないときがあります。このような場合は、x2=x3=x4=x5を固定して、x1を焼きなまし、x1=x3=x4=x5を固定して、x2を焼きなまし、x1=x2=x4=x5を固定して、x3を焼きなましというふうに1変数ずつ焼きなまししていく方法があります。xの値がそれぞれ独立性が高い(?)場合は、この方法は有効です。

6. デバッグ

スコア差分の検算

2021/03/06 追加しました。
例えばサンプル2ではスコアの差分を計算していますが、いきなり書くとバグらせる可能性もあります。以下のような手順をとれば、バグがないか確認することができます。

  1. サンプル1のgetFullScoreのように、全体のスコアの関数を用意して、それが正しいか確認します。通常のコンテストなら提出することで確認できます。
  2. 差分スコアrealDeltaScore = getFullScore(遷移後) - getFullScore(遷移前)として計算します。
  3. サンプル2のようにdeltaScoreを直接求める方法ができたら、realDeltaScoreと一致するか確認します。
デバッグの際には、決定的にする

2021/03/06 追加しました。
状態やスコアが、まれにおかしくなるといったバグがあった場合、通常の焼きなまし法は非決定的なのでバグが再現ができない場合があります。そういったときは、デバッグ時には決定的にしましょう。

  • 温度を求めるときにsystem clockを使わずに、最大ループ回数を固定して進行割合で温度を求めるようにしましょう。
    const static long long MAX_STEPS   =  10000000; // 終了までの回数
    for (long long steps = 0; steps < MAX_STEPS ; steps++) // 試行回数
    {
            const double progressRatio = static_cast<double>(steps) / MAX_STEPS;   // 進捗。開始時が0.0、終了時が1.0
            temp = START_TEMP + (END_TEMP - START_TEMP) * progressRatio;
  • 乱数はシードさえ固定すれば決定的になるので問題ありません。
  • もしマルチスレッドを使っている場合は、グローバル変数とstatic変数に気をつけましょう。とくに乱数でstaticを使っている場合は乱数をクラス化するか、シングルスレッドで実行しましょう。

7. まだ書いていない内容

  • 並列化
    • 排他制御つきの焼きなまし
    • ロックフリーの焼きなまし
  • タブーサーチ + 焼きなまし
  • レプリカ交換法

リンク集

基礎編

AtCoder wataさんによる公式解説
https://img.atcoder.jp/intro-heuristics/editorial.pdf

Tsukammoさんの記事
qiita.com

gacinさんの記事
gasin.hatenadiary.jp

応用編

ats5515さんは、巨大近傍法(状態遷移の回数は少なめになるけど、計算時間をかけてより良い次の状態を探す)が特に強いです。近傍の計算時間に。焼きなまし法の近傍を探すのに2段目の焼きなまし、制約条件をほぼ満たす準validな状態、温度遷移の工夫など、他の人があまり工夫しないもところでもいろいろバリエーションがあります。
ats5515.hatenablog.com

hakomoさんの記事は、まさに詳細という内容です。探索空間の話は、この記事が一番詳しいと思います。事例へのリンクも多いです。
github.com

2021年2月の行動目標

今月も予定が読めないので、先月達成できなかった目標は簡単にしました。

競プロ(アルゴリズム)
  • 高速フーリエ変換・数論変換の理解
  • ACLの遅延セグ木で10問解く
  • 全方位木DPをライブラリ化
競プロ(マラソン)
  • Hash Codeの練習を5回する
将棋
  • 実戦詰み筋事典を1周する(186問)
スポーツ
  • 月10回リングフィットアドベンチャーをする。

2021年1月の振り返り

仕事はだいぶおさまったのですが、プライベートがいろいろ入ったために、将棋の目標以外は達成できず。ちょっと2月以降も忙しくなるか余裕がでるか読めないのですが、おそらく1月と同じぐらいだと思うので、将棋以外はもう少し簡単な目標を立てることにします。

競プロ(アルゴリズム)
  • ABC178以降の解いてなかったやつを終わらせる
    • ❌8回分残っています…
競プロ(マラソン)
  • 記事を2つ書く
    • ❌1つも書けませんでした。
将棋
  • 5手詰めハンドブック1を1周する(200問)
    • ⭕無事完了しました。
スポーツ
  • 月15回リングフィットアドベンチャーをする。
    • ❌2回しかしていない。

2021年1月の行動目標

とにかく簡単な目標からスタートすることにしました。将棋とスポーツは朝に30分くらいやって、競プロを休日にやるかんじで。

競プロ(アルゴリズム)
  • ABC178以降の解いてなかったやつを終わらせる
競プロ(マラソン)
  • 記事を2つ書く
将棋
  • 5手詰めハンドブック1を1周する(200問)
スポーツ
  • 月15回リングフィットアドベンチャーをする。

2021年の目標

定義(成果目標と行動目標)

成果目標🏆 ... 結果を出しさえすれば良い目標。ただ、自分ががんばっても、まわりの環境により達成できない場合もあるかも(例:将棋で二段になる)
行動目標🏅 ... その行動さえやれば達成できる目標。完全に自分との戦い。(例:詰将棋10問を毎日解く)

2020年の反省

  • 2020年は成果目標も行動目標も高すぎた。2021年はまず達成できる目標を立てる。高い目標を立てていいのは、もっと現実的な目標を達成できる人間になってから!
  • 2020年は目標の種類数も多すぎた。切り替え上手な人はいいけど、意思の弱い自分は、脱線につながるのでも増えるのでことも多かったので、自分には今は無理。
  • 毎日趣味のための時間を確保すればいいと思うのだけど、今の仕事だと多忙になると難しい(無理ではないが)。とはいえ、それを目指すべきではある
  • 将棋については詰将棋が毎日定着しそうだったのだけど、自主的に負荷を増やしてみたら、その後急に忙しくなり、忙しさがある程度やんだあとも、元にもどらなくなった…。 もうちょっと目標をマメに見直していれば、途中で立て直せたかもしれない

2021年目標に立てるにあたってのテーマ

  • 行動目標の達成に集中する
  • 毎月行動目標を立てて、月末に振り返る
    • 2021年は、先が読めなく優先度が高い予定が入りそう。引っ越しは確定。仕事でも大きな動きがありそう。なんで、急に時間が確保できなくなったりすることが多いにありうる
    • 大幅に予定が狂った後に、成果目標が達成しようと帳尻あわせようと、無理やりなスケジュールを立てて、よく破綻してる。そろそろ学習して、これは避けたい。
    • まずは簡単な目標を立てて、達成できたら少しずつ難度が高い行動目標を立てていくことにします。忙しくなったら、難度を下げる。
  • そういうわけで、2021年1月の行動目標は別記事で立てました。毎月書いていきます。

shindannin.hatenadiary.com

2021年成果目標(おまけ)

年末に振り返りますが、行動目標さえ達成できれば2021年はよいことにします。成果目標はおまけ。

競プロ(アルゴリズム)
  • 2021/6/30までに、AtCoderで青1800
  • 2021/12/31までに、AtCoderで黄2000
競プロ(マラソン)
  • オンサイト決勝に進出する
  • AtCoderヒューリスティックスで、赤色相当に入る(レーティングシステムが決まり次第立て直します)
将棋
  • 2021/6/30までに、将棋ウォーズ初段50%
  • 2021/12/31までに、将棋ウォーズ二段
スポーツ
  • 週に3回以上は運動して、習慣化する。

2020年の目標が達成できたか?

結果のまとめ

予定の2割程度しか目標を達成できていないので、ダメでした。2021年は現実的な目標と、急に忙しくなった場合の対策を考えましょう(対策がなければ、極力減らす)



原文
shindannin.hatenadiary.com

以下ふりかえり。


目標原文:黒色
結果と感想:→青色

⭐⭐⭐ 2020年の目標を達成している。
⭐⭐ 2020年の目標を達成できそう。
⭐ 2020年の目標は到達できてないが、進展はある。
💀 進展していない、もしくは後退している。

目標の前に

2020年は、定時で帰る(大前提)

2019年は、仕事時間が長すぎました。長時間労働が必ずしも悪いとはいえないですし(結果を出すのがなにより重要)、別にダラダラ働いていたわけではないですが、2020年はいったん短時間労働制約を自分に課して(基本定時に帰る)で、どう効率よく仕事すればいいかを考えるきっかけにしたいと思います。これで仕事の結果がダメになったら、それは自分の責任。
→物量が読めない専門外の仕事が多く、かつ会社全体も忙しく、結局2019年よりも忙しい厳しい1年でした。9月~11月はtwitter封印しなければならない厳しい展開で、休日出勤・深夜労働もする展開に…。2021年もいろいろ変化があると思うので忙しいとは思いますが、どこらへんで折り合いをつけるかが難しいですね…。。

2020年の目標

競プロ(短時間マッチ系)💼💼🌞🌞

AtCoder 黄色
→💀1684⇒1645
AtCoder Problems 250000 pts (ABCのC以上の問題で)
→💀100100⇒135300

2019年はレートを大きく下げてしまいました。最後に目標を達成して、単純に練習量の不足なだけという結論に達したので、2020年はがんばりたいです。3ヵ月おきで目標を区切り達成を目指す予定。
(参考)2019年下半期の結果
→一番優先順位が下がってしまい、コンテストでるだけで終わってしまいました…

競プロ(マラソンマッチ系)🌞

マラソンマッチ(Hash Code・日本橋ハーフマラソンなど)で1回はオンサイトの決勝に行く
→⭐⭐⭐Hash Codeで世界大会の決勝にでる
マラソンマッチに10回参加
→⭐⭐9回

ただし、もし2020年中に、AtCoderマラソンマッチにレーティングのようなものがついたら、目標自体を追加変更するかもしれません。
→マラソンマッチ系については、一部の結果は素晴らしかったのですが(Hash Code 33位、Introduction to Heuristics Contest 1位)、Hash Codeは自分自身は不調で、実力アップのための対策や、復習も滞りました。また、時間を確保できないまま参戦しひどい結果が出てしまったのも多かったです…。

将棋 💼

将棋ウォーズ二段
→⭐将棋ウォーズ初段(達成率8.7%)
勉強記録を1年間つけ続ける
→⭐6月まではマメにつけていたが、その後滞る。ただ最低限の情報だけは記録できた。


→「将棋ウォーズの対局とその復習に偏りすぎで、忙しくなると、詰将棋・次の1手などの日課をサボってしまう」という状況から色々崩れていった気がします。対局が楽しすぎるのは、諸刃の剣で、とても忙しい時期でも将棋ウォーズ対局は現実逃避でやっていて、結果継続できたので1年で510時間25分もかけていました。このうちの100時間でも詰将棋・次の1手につぎ込んでいれば良かったのに…。とあるアマチュア向けの上達本に「将棋の実際の対局は、将棋に費やす時間の1割以下にすること」と書いてありましたが、対局7割復習2割勉強1割ぐらいになっていました…。

以下の目標は、他の目標が全然できていないので、今年の目標からカットしたもの。未達。大反省。


カットした目標

共同研究(追加) 💼🌞
以前からアイディアがあるのですが、次の記事で具体的な内容について書きますので(今週中の予定)、ぜひご協力いただければ幸いです。なお内容については変更があります。
f:id:shindannin:20200818095955p:plain
f:id:shindannin:20200818100100p:plain


ゲームをする🌞
🏅ゲームを250時間以上する
→⭐⭐⭐307時間

多くはないですが、これぐらいで。
→将棋ウォーズを入れてしまいましたが(237時間プレイした)、その他はリングフィット・壺おじ・自社のゲームぐらいしかやっていない気が。これはダメ…
→本当は他のゲームで250時間以上する予定だったのですが、目標変更するのもあれなので、今年の目標からカット!

ゲーム技術系 💼
🏆Unityの試験(エキスパート ゲームプレイプログラマ)に受かる
→💀何もしていない
🏅ゲーム開発の議論ネタになるような記事を2か月に1回書く。
→💀何もしていない

2019年は、過去のゲーム開発で得た経験・知識を徐々に忘れてきているというのを実感しました。ボケを防ぐためにも、ゲーム開発のネタの記事を今年から書いていこうと思います。記事の質は微妙になると思いますが、議論の土台になるように書いていきたいです。
また、2019年は仕事内容が変わってUnityを使うようになりました。「ゲームエンジンを使用したことがある人だったら、商用のUnityも使えるだろう」と思っていたのですが、まぁ使えてはいますが…

  • Unity経験が多い人ほど、Unityで良いゲームを作れる(あたりまえ)
  • 他のエンジンをできるだけ多く詳しく知っておくことが、将来、自前でフルスクラッチでゲームエンジンを作るときにも役立つ。

という気持ちになったので、Unityの試験を受けてみようかなと思います。
https://certification.unity.com/ja/products/expert-gameplay-programmer
→他の目標が全然できていないので、今年の目標からカット!


言語 💼

🏅中国語の勉強して、簡単な日常会話ができるよう
→💀何もしていない

まだ個人的な理由で、広東語かマンダリンのどっちを勉強するかなど、細かいことを決めていませんが、決まり次第更新します。それにより成果目標を追加予定です。
→他の目標が全然できていないので、今年の目標からカット!

ゲーム企画コンテストPERACONにおける審査員の問題

注意点

  • 「CEDEC2020のPERACONが参加人数が多すぎて、提出物の質が低くなった」という問題については、書いてません。あくまで審査員の質についてだけを書いています。*1
  • 記事を書くにあたって、審査員は匿名で書きたかったのですが、
    • 審査員全員の名前が公開されていて、中途半端に匿名にしても無意味なこと。
    • 根本的な問題では、反省はしてなさそう。
    • 審査員にも責任があるべきといっていること。

f:id:shindannin:20200906222916p:plain
なので、審査員の実名を出しました。ただ、悪い部分だけをピックアップすることはないように、できるだけ多くのデータを出し、客観的に判断するように心がけました。

はじめに

CEDECというゲーム業界のカンファレンスで、PERACONというゲーム企画コンテストがあります。今年からはCESAというゲーム業界最大の団体の人材育成部会で引き取って、毎年人材を育成するという目的で行っていくことになったようですが、これから挙げる3人の審査員が、

  • ハラスメントの問題(モラルハラスメント・パワーハラスメント)。ゲームクリエイターという以前に、人として失格。
  • 審査の内容のレベルがとても低い

という問題がありました。CESAの人材育成目的と真逆で、むしろ人材が離れていくような内容で、日本のゲーム業界の将来に悪影響を及ぼすと思うので、問題点をまとめました。

(審査員のお一方については、所属先会社の謝罪のコメントがありましたので、この箇所は削除しました)

河上京子氏の問題

CEDEC2020の審査をみたところ、1人「わからない」を連発している方がいました。確かに、今年のPERACONで内容が分からないものは去年より多かったですが、他の審査員が評価していて河上氏だけが「分からない」といったものは多いです。例えば、2020年の上位10位までの企画のうち、河上氏が評価したのは6つありますが、3つ(3位4位7位)が分からなかったようです。「ものすごく分かりやすい企画だけを重視するプロデューサー」という意味で多様性という存在価値があるのかもしれませんが、何が分からないかについて言語化できない方は、仮にゲームクリエイターとして優れていたとしても審査員として不適格だと思います。

2020年に全般についても調べてみたところ、289件について平均7.47文字と、「分からない」「で?」「は?」など、情報がないコメントばかりでした。(2019年については、47件平均13.57文字と、若干マシですが。)
f:id:shindannin:20200906193622p:plain

どういう方なんだろうと思いtwitterを見てみたのですが、この「業界無理だし、田舎帰れ」発言はひどいです。そもそも、何様なんだと思いましたが。しかも、審査委員長自らも煽っていて救いようがありません。
f:id:shindannin:20200906193318p:plain

この方も、ハラスメント体質で、審査員としての能力が低く、不適格だと思います。もしかしたら、遠藤氏の煽りと作品の多さで、正常な判断力を失ったのかもしれませんが、ハラスメントの部分については、同情の余地はありません。

遠藤雅伸氏の問題

PERACON審査委員長の遠藤雅伸氏が最も害になっています。遠藤雅伸氏については、上記2人とは違い、審査でなぜダメかについて理由は述べています。ただ、他が救いようがありません。

まず、上記審査委員長にもかかわらず、自らハラスメントの風潮を作ろうとしたり正当化しようしたりしています。全く気にしていないようなので、恐らくこの文面をみても何が問題なのかは分からないでしょう。
peracon.cesa.or.jp
CEDECのセッション(一般の方は見れない)のほうでも、わざわざプロの最低順位の作品を、会社名をあげ応募者の名前を呼び捨てで、侮辱しています。これもありえないです。教え子だから許してとtwitterで言っていますが、あのセッションをみた人はビリだと晒しあげで侮辱されるんだろうなぁと思うだけです。

最悪なのが、応募者に対して「ゲーム作り諦めてくれ」とか「業界から消えろ」と罵倒することです。最初に言ったように「CESAというゲーム業界最大の団体の人材育成部会で引き取って、毎年人材を育成するという目的で行っていく」ことになったイベントでです。
例えば、PERACON2020で、遠藤氏が応募者に「諦め」と言ったもの6件あります。*2

  • 188位(審査員得点:1点)ジャンル、プレイ人数、プラットフォームを書けと教わったと分かるなんちゃって提案。「湿った雰囲気」というだけで、後はできの悪い既存ゲームの劣化版。こんなシート書いてるようじゃゲーム作り諦めてくれ。
  • 369位(審査員得点:0点)作成中の試行錯誤に疲れてそれ自体を形にしたなんちゃって提案。面白い作品が作れないのなら、作品を出すのもゲームクリエイターへの道も諦めてくれ!
  • 89位(審査員得点:3点)扉を閉めてゾンビから逃げるという、相当に擦られたゴミ。この段階でこんなもの出してくるセンスが、企画能力の無さを露呈している。ゲーム制作は無理だから諦めてほしい。
  • 215位(審査員得点:1点)ドアを閉めることをしめるとした作者の日常。まだ反抗期なのかな?母親を攻撃の対象としている当たりに子供らしさを感じる。企画は新規性も独自性もなく、審査に値しないレベルで、ゲーム企画は諦めた方がよい。
  • 330位(審査員得点:0点)メーターを占めるという謎な状態をしめるとこじつけたやっつけ仕事。小学生の妄想を書いてるだけで、ゲームに必要な情報は説明されていない。審査に値しないレベルで、才能ないからゲーム企画は諦めれば?
  • 142位(審査員得点:2点)タイヤの場合はネジじゃなくてホイールナットなのも調べないダメ学生。ネジを締めるから発想して、タイヤ交換を思いついたのだろうが、そんな作業を切り出して何が面白いのかを考える力がない。ゲーム企画は無理だから諦めろ!

今年の応募数は、583位まで有効なので、平凡ではありますが、絶望的に順位が低いというわけではありません。例えば、89位の作品には、現役のゲームクリエイターの3名の方から「いいね!」がついています。それがだいぶ前に現場からから離れてしまった1人の勝手な判断で、諦めろいうのは、本当にひどいです。

他の方からもご指摘があったのですが、それについても悪びれずに、このような返信をしています。
f:id:shindannin:20200906220414p:plain
f:id:shindannin:20200906223405p:plain

このような人物が一番上に立っているというだけで、本当におかしいです。

ありそうな質問や言い訳(順次追加していきます)

PERACONに参加していないのに、口出しするな。なぜ、そんなに気にするのか?

  • ゲーム業界全体に害があります。
    • ゲーム業界の将来を担うかもしれない人材を諦めさせる行動により、直接的に人材が入ってこなくなる可能性があります。
    • 単純にモラルレベルの低いの業界だと思われ、他の業界に人が移ってしまう可能性があります。
  • 誹謗中傷しながら「ハラスメント型の指導」を、業界最大の団体が是としているように見えてもしょうがないので。
  • ハラスメント一般のことですが、はびこると排除が難しいので
    • ハラスメントする側は正義で「常勝」になりやすい。
      • された人が成功したら、「俺がわざと悪役になって奮起させたんだ」となって、正義。
      • された人が失敗したら、「ほら俺のいった通りになった」となって、正義。
    • といったかんじで、ハラスメントする人がいったん高い立場になると、そのまま残れる可能性が高い。なかなか排除されない毒なのです。
    • また、「クリエイターなんで、こういうハラスメントは許されるんだ」と理解した学生が、将来クリエイターになったときに、その傾向をひきついで増えていく可能性はあります。
  • おそらく自分以外の方は意見が出しづらいです。
    • 参加者の方は盛り上がってるので、こういう意見が非常に出しづらい。
    • 日本のゲーム業界の人も、意見が出しづらいと思います。というか、ふぁぼるのさえ避ける人が多いと思います。業界最大団体がバックにあり、また遠藤氏の知名度は高いので。そもそも言いやすかったら、こんな立場についているはずがありません。
    • CESAのお問い合わせページはこちらです。CESA:お問い合わせ

辛口・憎まれ役がいたほうがいい。そのほうが奮起して結局レベルアップになる。

  • 厳しい批評をするなと言っているわけではありません。高いハードルを求めることはできます。問題点を1つ1つ、なぜ高クォリティーに届いていないのか説明していけばいいのです。それは別に罵倒しなくてもできます。
  • 大多数のクリエイターの方は、そんなことをしなくても、良いゲームを作れています。
    • ハラスメント型のクリエイターと、非ハラスメント型のクリエイターが、現在どちらが良いゲームを作れているか比較してみたいです。CESAの研究題材としてどうですか?

業界のプロがクソ忙しい中600近い素人応募に目を通すのだから、暴言が入るのもしょうがない。

  • 全部の作品にコメントを書く義務はありませんでした。暴言書くぐらいなら書かないこともできたはずです。
  • これで多くの審査員のコメントが2020年から悪化しているのであれば、その主張は通るのですが、そんなことはありません。
    • 審査員は2019年は47名、2020年は36名います
      • 2019年に「クソ」とコメントしたのは、遠藤雅伸氏(複数回)を含む2名のみ
      • 2020年に「クソ」とコメントしたのは、遠藤雅伸氏(複数回)を含む3名のみ
  • 他の審査員の方々もコメントも見ましたが、特に問題があったようには思えませんでした。上記の3人だけが2019年も2020年もひどかったです。

単に普段から口が悪いだけで、悪意はそんなにないのでは?

  • 本人の悪意はなくても、ハラスメントにはなるので…。
  • 気になるのが、今回の審査は第3者に対しての審査です。また、学生ではなくプロも参加しています。決して上下関係にないのですが、それでこのありさまです。これがもし上司-部下、先生-生徒の関係だったとしてら、これよりひどいハラスメントをしている可能性はあると思います。

CESAに自浄作用があるのか?

  • たぶん問題があるのには、中の人は気づいていたかと思いますが…。CEDECのセッションをみて不安になりました。
  • 外からコメントするのあれば、CESAのお問い合わせページはこちらです。

CESA:お問い合わせ

(追加)CEDECといえども所詮、一部の内輪グループ、良くも悪くも影響力は少なく、特に大ごとではない。

  • 初期のCEDECでは、開催に貢献した人達の内輪のゲームカンファンスっぽい風潮はありましたが、各社協賛するようになってから随分時間が経ちました。特に事情を知らない外に対しての影響力は大きいと思います。
    • インターネットで調べたところ、今回上げられた遠藤氏は、大学や専門学校での教育にも力を入れていて、CEDECの経歴も載せていたりします。そういった影響力あるゲームクリエイターに、CEDEC委員という立場は、さらなる影響力を加えている可能性があります。
  • ちなみに、CEDECの外で、内輪で同じような企画コンテストをやるのは、問題ありません。例えば、昔あった「マネーの虎」みたいなかんじでやればいいのではないでしょうか?そこで、ハラスメント的指導のほうが優れたゲームデザインができるということを示してほしいです。自分はハラスメントそのものの時点でアウトと考えていますが、指導者と被指導者がどっちも納得であれば、それを阻むことはしません。

(追加)多様性があったほうがいいので、いろんな審査員がいて良い。

  • まず、PERACONの趣旨から、15秒で分かるものということで、「ゲームデザイナーが、上司のディレクターやプロデューサーに新規ゲームを提案する状況」を想定しているのかなと思いました。もし定義が間違ってたらごめんなさい。
  • そう考えると、「何をもって良いとするか」が各ディレクターによって異なるので、多様性を持たせたほうが良いというのは筋が通ります。反論はありません。
  • ただ、ハラスメントをするディレクターや、企画をあまり読んでないディレクターといった、多様性は必要あるでしょうか?避けたほうがいいのでは?
  • もし、PERACONの趣旨が「プレイヤーに受けるゲーム企画」だったら、例えば説明書を全然読まないプレイヤーもいるので、そういう多様性も意味があるとは思いますが、それだったらそもそも一般投票でよい気がします(組織票対策の必要はありますが)。

(追加)ゲーム業界はハラスメント体質があるのか、こわい・行きたくない。

  • 多くの会社は、大丈夫です。
  • ゲーム業界は非常に狭く、悪名はすぐに広まります。なんで中にいれば、ハラスメントをする人・会社の噂というのはある程度分かるので、それを避けることは可能です。私見ですが、ハラスメントする人同士が固まる傾向があるので(今回の上記の2人のツイッターのやりとりを見れば分かると思いますが)、そういうところはできるだけ避けましょう。
  • ただ、新卒のときは職場をそれほど選べず、そういうハラスメント指導をするディレクター・プロデューサーが運悪く上司になってしまうかもしれません。そのときは、度胸がある人なら、
    1. 本人に直接問題を指摘する→
    2. まぁ大抵直らないので、その上司に指摘する→
    3. だめなら上司の上司に相談する→
    4. それでもダメなら転職することにしましょう。
  • 世の中、たくさんいいゲームクリエイターがいるのに、そういう人に振り回させるのは人生のムダです!
  • また、度胸がない人は最初から(4)でいいです。ガマンして心や体を壊すのに比べたら、ずっとマシです。

(追加)遠藤氏の発言は、口は悪いけど、その裏にはちゃんと愛が含まれてる。厳しく接してくれる方が懐いてくる学生は多いと思う。

  • 暴言や暴力に愛があるというのは、ハラスメントする側の言い訳の常套手段です。
  • 実際、遠藤氏が他のイベントでも、学生を罵倒したという、タレコミはいただきました。(主催者側は、遠藤氏のそういうハラスメントを期待してはいませんでした。)

改善案

  • まず、上記の3名は審査員から外すのが一番良いと思います。他に審査員の方はみんな良いですし、この3名がいなくてもプロデューサーの多様性は確保できるでしょう。
  • 簗瀬氏の記事 PERACON 2020 審査を終えて|Yanace|noteは、本当に分かりやすく中身も濃く、参加者への配慮がみられます。簗瀬氏を審査委員長にするのが一番良いと思いますが、忙しさなど諸事情はあると思うので、残りの審査員の方で今後のことは相談して決めたら良いと思います。
  • 今後、問題のある審査員が入ってくるもゼロではないので、ガイドラインを用意しておく必要があるとは思います。

参考資料

PERACON2020の統計情報(上記で述べた通り、審査するしないは自由だったようなので、コメント数が少ないこと自体は問題ありません)
f:id:shindannin:20200906233119p:plain

PERACON2019の統計情報
f:id:shindannin:20200906233449p:plain

*1:作品の質の話もすると議論が分散してしまうので、それを避けました。なお、自分自身は、審査員コメントだけでなく作品自体もかなりチェックしていますが、今回の問題は作品の質とは独立した問題なので、記事内では作品の質については必要のない限り触れていません。

*2:なお似たような意味のコメントは他にもありますし、2019年のコメントにもあります。