読者です 読者をやめる 読者になる 読者になる

daruma3940の日記

理解や文章に間違い等あればどんなことでもご指摘お願いします

機械学習部を読んでいこうじぇ3

前回
機械学習部を読んでいこうじぇ2 - daruma3940の日記
の続き



f:id:daruma3940:20160520223745p:plain
ゆおおおお~~~
f:id:daruma3940:20160521003616p:plain
どうしたの??

yaneuraou.yaneu.com


f:id:daruma3940:20160520223745p:plain
この記事読んだら
この前言ってたAperyの学習部のわからないことのうち3つが理解できたのじぇ~~

f:id:daruma3940:20160520223530p:plain
わからないことの内容は


lowerDimension()の次元下げでは具体的に何が行われているのか

incParam()の目的は何なのか

incParam(dT)をしてるのになんでわざわざincParam(sum_dT)をもう一回するのか

dTの符号の変え方はどのような目的で符号を変えているのか

parse1で探索して評価値を出しているのに

なんでわざわざparse2でも評価関数を読んで評価値を計算するのか

predictionとは何なのか

だったよ!

f:id:daruma3940:20160520223745p:plain
このうちincParam()関連と評価値計算についてがわかったのじぇ!!


わかったことをまとめたlearn parse1とlearn parse2はこんな感じになるのじぇ!!!



	void learnParse1Body(Position& pos, std::mt19937& mt) {
		/*uniform_int_distributionは、指定された範囲の値が等確率で発生するよう離散分布するクラスである*/
		std::uniform_int_distribution<Ply> dist(minDepth_, maxDepth_);//探索深さをばらけさせる?

		pos.searcher()->tt.clear();
		for (size_t i = lockingIndexIncrement<true>(); i < gameNumForIteration_; i = lockingIndexIncrement<true>()) {
			StateStackPtr setUpStates = StateStackPtr(new std::stack<StateInfo>());
			pos.set(DefaultStartPositionSFEN, pos.searcher()->threads.mainThread());
			auto& gameMoves = bookMovesDatum_[i];


			//教師データについて
			for (auto& bmd : gameMoves) {
				if (bmd.useLearning) {
					//alpha betaの設定
					pos.searcher()->alpha = -ScoreMaxEvaluate;
					pos.searcher()->beta  =  ScoreMaxEvaluate;
					go(pos, dist(mt), bmd.move);//指定された差し手(教師手)を用いて探索開始
					const Score recordScore = pos.searcher()->rootMoves[0].score_;//教師手の評価値を記録
					++moveCount_;
					bmd.otherPVExist = false;
					bmd.pvBuffer.clear();//pvを0にする??

					if (abs(recordScore) < ScoreMaxEvaluate) {
						int recordIsNth = 0; // 正解の手が何番目に良い手か。0から数える。
						auto& recordPv = pos.searcher()->rootMoves[0].pv_;//教師手
						bmd.pvBuffer.insert(std::end(bmd.pvBuffer), std::begin(recordPv), std::end(recordPv));//vector2つの連結
						const auto recordPVSize = bmd.pvBuffer.size();

						//全合法手に対して
						for (MoveList<LegalAll> ml(pos); !ml.end(); ++ml) {
							if (ml.move() != bmd.move) {//教師手以外の差し手を調べる
								pos.searcher()->alpha = recordScore - FVWindow;//FVWindow(256)の外にあるような評価値となる指し手は考えない方がよい
								pos.searcher()->beta  = recordScore + FVWindow;
								go(pos, dist(mt), ml.move());
								const Score score = pos.searcher()->rootMoves[0].score_;//探索した指し手の評価値

								//alpha beta内に収まる値が帰ってくれば
								if (pos.searcher()->alpha < score && score < pos.searcher()->beta) {
									auto& pv = pos.searcher()->rootMoves[0].pv_;
									bmd.pvBuffer.insert(std::end(bmd.pvBuffer), std::begin(pv), std::end(pv));//pvBufferの連結 (指し手と評価値を記録)
								}
								if (recordScore < score)//教師手の価値を超えれば超えた差し手の数を記録?
									++recordIsNth;
							}
						}


						//教師手に近い値(棋譜の手の評価値±256以内)の指し手があったか?(これは全合法手に対してのループで一つでもpvBufferに要素が入れられていればtureになる、。)
						bmd.otherPVExist = (recordPVSize != bmd.pvBuffer.size());

						//predictionとは一体何のために用意されている?
						for (int i = recordIsNth; i < PredSize; ++i)
							++predictions_[i];
					}
				}
				setUpStates->push(StateInfo());
				pos.doMove(bmd.move, setUpStates->top());//教師手で差し手を進める
			}
		}
	}//end of phase1 body

	/*===============================
	教師手を用いて探索をする。その時に帰ってきた評価値を記録する。
    それ以外の指し手を用いて探索する。その時に帰ってきた評価値を記録する。
	==============================*/
	void learnParse1(Position& pos) {
		Time t = Time::currentTime();
		// 棋譜をシャッフルすることで、先頭 gameNum_ 個の学習に使うデータをランダムに選ぶ。
		std::shuffle(std::begin(bookMovesDatum_), std::end(bookMovesDatum_), mt_);
		std::cout << "shuffle elapsed: " << t.elapsed() / 1000 << "[sec]" << std::endl;
		index_ = 0;
		moveCount_.store(0);//現在の合法手の数
		for (auto& pred : predictions_)
			pred.store(0);
		std::vector<std::thread> threads(positions_.size());
		for (size_t i = 0; i < positions_.size(); ++i)
			threads[i] = std::thread([this, i] { learnParse1Body(positions_[i], mts_[i]); });//thread並列処理
		learnParse1Body(pos, mt_);

		//スレッド終了処理
		for (auto& thread : threads)
			thread.join();

		std::cout << "\nGames = " << bookMovesDatum_.size()
				  << "\nTotal Moves = " << moveCount_
				  << "\nPrediction = ";

		//predictionとは一体何なのか何の確率をここで表示しているのか
		for (auto& pred : predictions_)
			std::cout << static_cast<double>(pred.load()*100) / moveCount_.load() << ", ";
		std::cout << std::endl;
		std::cout << "parse1 elapsed: " << t.elapsed() / 1000 << "[sec]" << std::endl;

	}//end of learn phase1


	static constexpr double FVPenalty() { return (0.2/static_cast<double>(FVScale)); }//罰金項? 0.2/32


	//特徴ベクトルの更新!!!!!!!
	//@  v		特徴ベクトル
	//@  dv   目的関数の微分(罰金項はまだ計算されていない)
	template <bool UsePenalty, typename T>
	void updateFV(std::array<T, 2>& v, const std::array<std::atomic<float>, 2>& dvRef) {


		std::array<float, 2> dv = {dvRef[0].load(), dvRef[1].load()};//黒番から見たものと白番から見たもの

	
		const int step = count1s(mt64_() & updateMask_);//更新1ステップの分の更新値の大きさは乱数にmaskをかけて得られる?
		for (int i = 0; i < 2; ++i) {
			if (UsePenalty) {
				
				//罰金項の分だけdvを減少させる これによって次世代のパラメーターの値は値は全世代の値から大きく離れてしまうことはなくなる。
				if      (0 < v[i]) dv[i] -= static_cast<float>(FVPenalty());//vが正の値の場合はマイナス方向に傾きを減らす
				else if (v[i] < 0) dv[i] += static_cast<float>(FVPenalty());//vが負の値の場合はプラス方向に傾きを増やす。
			}

			// T が enum だと 0 になることがある。
			// enum のときは、std::numeric_limits<std::underlying_type<T>::type>::max() などを使う。
			static_assert(std::numeric_limits<T>::max() != 0, "");
			static_assert(std::numeric_limits<T>::min() != 0, "");

			//dvの符号に合わせて更新1step分の大きさに対応したを特徴ベクトルに足したり引いたりする。
			if      (0.0 <= dv[i] && v[i] <= std::numeric_limits<T>::max() - step) v[i] += step;
			else if (dv[i] <= 0.0 && std::numeric_limits<T>::min() + step <= v[i]) v[i] -= step;//出現しなかった特徴因子はゼロに近づけられる(sparseに近づく)
		}
	}

	//特徴ベクトルの更新&書き出し
	template <bool UsePenalty>
	void updateEval(const std::string& dirName) {

		//特徴ベクトルの更新
		for (size_t i = 0; i < eval_.kpps_end_index(); ++i)
			//第一引数:特徴ベクトル	第二引数:目的関数の微分(without 罰金項)
			updateFV<UsePenalty>(*eval_.oneArrayKPP(i), *parse2EvalBase_.oneArrayKPP(i));
		for (size_t i = 0; i < eval_.kkps_end_index(); ++i)
			updateFV<UsePenalty>(*eval_.oneArrayKKP(i), *parse2EvalBase_.oneArrayKKP(i));
		for (size_t i = 0; i < eval_.kks_end_index(); ++i)
			updateFV<UsePenalty>(*eval_.oneArrayKK(i), *parse2EvalBase_.oneArrayKK(i));



		// 学習しないパラメータがある時は、一旦 write() で学習しているパラメータだけ書きこんで、再度読み込む事で、
		// updateFV()で学習しないパラメータに入ったノイズを無くす。(??よくわからん)



		eval_.write(dirName);//更新した特徴ベクトルの値をここでファイルに書き出す。
		eval_.init(dirName, false);//書きだした値をもう一度読み込む
		g_evalTable.clear();//TTの初期化
	}

	//シグモイド関数
	double sigmoid(const double x) const {
		const double a = 7.0/static_cast<double>(FVWindow);//ゲイン
		const double clipx = std::max(static_cast<double>(-FVWindow), std::min(static_cast<double>(FVWindow), x));//-FVwindowとFVwindowの間にxをclipするようにしている
		return 1.0 / (1.0 + exp(-a * clipx));
	}
	//シグモイド関数の一回微分
	double dsigmoid(const double x) const {
		if (x <= -FVWindow || FVWindow <= x) { return 0.0; }//window外の値が入ってきてしまったら0を返す。
#if 1
		// 符号だけが大切なので、定数掛ける必要は無い。
		const double a = 7.0/static_cast<double>(FVWindow);
		return a * sigmoid(x) * (1 - sigmoid(x));
#else
		// 定数掛けない方を使う。
		return sigmoid(x) * (1 - sigmoid(x));
#endif
	}

	//step用マスクのアップデート
	void setUpdateMask(const int step) {
		const int stepMax = stepNum_;
		const int max = count1s(updateMaxMask_);
		const int min = count1s(updateMinMask_);
		updateMask_ = max - (((max - min)*step+(stepMax>>1))/stepMax);
	}

	void learnParse2Body(Position& pos, Parse2Data& parse2Data) {
		parse2Data.clear();//phase2データの初期化
		SearchStack ss[2];

		for (size_t i = lockingIndexIncrement<false>(); i < gameNumForIteration_; i = lockingIndexIncrement<false>()) {
			StateStackPtr setUpStates = StateStackPtr(new std::stack<StateInfo>());

			pos.set(DefaultStartPositionSFEN, pos.searcher()->threads.mainThread());//局面の初期化

			auto& gameMoves = bookMovesDatum_[i];
			for (auto& bmd : gameMoves) {
				PRINT_PV(pos.print());
				if (bmd.useLearning && bmd.otherPVExist) {//教師手の価値と近い手が存在し、教師手は学習に使えるものである
					const Color rootColor = pos.turn();
					int recordPVIndex = 0;
					PRINT_PV(std::cout << "recordpv: ");

					//pvBufferの頭に格納されているのは正解の手である。 教師手に対して
					for (; !bmd.pvBuffer[recordPVIndex].isNone(); ++recordPVIndex) {
						PRINT_PV(std::cout << bmd.pvBuffer[recordPVIndex].toCSA());
						setUpStates->push(StateInfo());
						pos.doMove(bmd.pvBuffer[recordPVIndex], setUpStates->top());//phase1で調べた差し手でpvBufferがなくなるまで局面を進めていく?
					}
					// evaluate() の差分計算を無効化する。
					ss[0].staticEvalRaw.p[0][0] = ss[1].staticEvalRaw.p[0][0] = ScoreNotEvaluated;//まだ評価されたことはないフラグ つまり差分計算はなされない

					const Score recordScore = (rootColor == pos.turn() ? evaluate(pos, ss+1) : -evaluate(pos, ss+1));//正解の手の評価値の計算(phase1で探索させて評価値を記録しているはずでは?)
					PRINT_PV(std::cout << ", score: " << recordScore << std::endl);

					for (int jj = recordPVIndex - 1; 0 <= jj; --jj) {//評価値を計算し終われば局面を戻す
						pos.undoMove(bmd.pvBuffer[jj]);
					}

					//sum_DTは目的関数の微分の足し上げ
					std::array<double, 2> sum_dT = {{0.0, 0.0}};

					//教師手以外の差し手に対して
					for (int otherPVIndex = recordPVIndex + 1; otherPVIndex < static_cast<int>(bmd.pvBuffer.size()); ++otherPVIndex) {
						PRINT_PV(std::cout << "otherpv : ");
						for (; !bmd.pvBuffer[otherPVIndex].isNone(); ++otherPVIndex) {
							PRINT_PV(std::cout << bmd.pvBuffer[otherPVIndex].toCSA());
							setUpStates->push(StateInfo());
							pos.doMove(bmd.pvBuffer[otherPVIndex], setUpStates->top());//指し手で局面をすすめる
						}
						ss[0].staticEvalRaw.p[0][0] = ss[1].staticEvalRaw.p[0][0] = ScoreNotEvaluated;//差分計算はなされない
						const Score score = (rootColor == pos.turn() ? evaluate(pos, ss+1) : -evaluate(pos, ss+1));//正解の手以外で進んだ局面で評価値計算
						/*================================================================================================
						↑phase1で探索させて評価値を記録しているはずでは?なぜわざわざもう一回評価値を計算させる?
						phase1でみた評価値は探索させた末端の局面の評価値であるから
						教師手に近い指し手を指した時の評価値とは違う!!!!!
						ほしいのは教師手に近い値を持つ指し手の末端局面の評価値ではなく
						その手で一手進めただけの局面の評価値!!!!!!!!!!!
						=============================================================================================*/


						const auto diff = score - recordScore;//教師手の評価値との差をとる
						const double dsig = dsigmoid(diff);//シグモイド関数の微分

						//dTは手番を考慮しているということ??(diff 手番?)
						//TODO 最終的にどうなるように符号を変えてるんだ??
						std::array<double, 2> dT = {{(rootColor == Black ? dsig : -dsig), dsig}};
						PRINT_PV(std::cout << ", score: " << score << ", dT: " << dT[0] << std::endl);
						sum_dT += dT;//目的関数の微分の足し上げ?(全局面とこの局面の合法手に対しての足し上げ?)
						dT[0] = -dT[0];
						dT[1] = (pos.turn() == rootColor ? -dT[1] : dT[1]);//incParamで減算させるために-dTにする。
						parse2Data.params.incParam(pos, dT);//教師データ以外の指し手に出て来た特徴には小さくなってほしいのでdTだけ減算する。



						//局面を戻す
						for (int jj = otherPVIndex - 1; !bmd.pvBuffer[jj].isNone(); --jj) {
							pos.undoMove(bmd.pvBuffer[jj]);
						}
					}

					//ここは何をしている?
					for (int jj = 0; jj < recordPVIndex; ++jj) {
						setUpStates->push(StateInfo());
						pos.doMove(bmd.pvBuffer[jj], setUpStates->top());//どんな指し手で局面を進めている?
					}
					sum_dT[1] = (pos.turn() == rootColor ? sum_dT[1] : -sum_dT[1]);//incparamで加算させるために+sumdTにする。

					/*
					なぜ教師データにはsum_dT加算するかというと
					教師データのに出ていて
					教師データではない指し手にも出ている特徴因子には
					加点も原点もしたくないから!!!!
					*/
					parse2Data.params.incParam(pos, sum_dT);//教師データの指し手で進めた時に出てきた特徴は大きくなって欲しいのでsum_DTだけ加点する
					


					for (int jj = recordPVIndex - 1; 0 <= jj; --jj) {
						pos.undoMove(bmd.pvBuffer[jj]);
					}
				}
				setUpStates->push(StateInfo());
				pos.doMove(bmd.move, setUpStates->top());//教師手で指し手をすすめる?
			}
		}
	}

	/*==================================================================
	phase2
	目的関数の微分の値の正負によってパラメーターを更新
	OK===include parameterとはなに?
	lowerDimension()とはなに?
	OK===incParam()とはなに?
	=====================================================================*/
	void learnParse2(Position& pos) {
		Time t;
		for (int step = 1; step <= stepNum_; ++step) {
			t.restart();//step2にかかる時間の計測
			std::cout << "step " << step << "/" << stepNum_ << " " << std::flush;
			index_ = 0;
			std::vector<std::thread> threads(positions_.size());

			//処理の並列化
			for (size_t i = 0; i < positions_.size(); ++i)
				threads[i] = std::thread([this, i] { learnParse2Body(positions_[i], parse2Datum_[i]); });
			learnParse2Body(pos, parse2Data_);
			for (auto& thread : threads)
				thread.join();


			for (auto& parse2 : parse2Datum_) {
				parse2Data_.params += parse2.params;//parse2Data.params.incParamされたデータの足し上げ?(スレッドに関しての足し上げ)
			}

			parse2EvalBase_.clear();

			//ここが一番何をしているのかわからない また頑張って理解する。
			lowerDimension(parse2EvalBase_, parse2Data_.params);//KPP次元下げ!!!!!!!!!!!

			//特徴ベクトルはstep分だけ更新される
			setUpdateMask(step);

			std::cout << "update eval ... " << std::flush;
			//======================================================
			//ここで計算結果を用いて評価値ベクトルを更新しファイルに書き出している!
			//=======================================================
			if (usePenalty_) updateEval<true >(pos.searcher()->options["Eval_Dir"]);
			else             updateEval<false>(pos.searcher()->options["Eval_Dir"]);
			std::cout << "done" << std::endl;
			std::cout << "parse2 1 step elapsed: " << t.elapsed() / 1000 << "[sec]" << std::endl;
			print();
		}
	}//end of learnphase2


f:id:daruma3940:20160520223745p:plain
これで今のところわからないことは

lowerDimension()の次元下げでは具体的に何が行われているのか

incParam()の目的は何なのか

incParam(dT)をしてるのになんでわざわざincParam(sum_dT)をもう一回するのか

dTの符号の変え方はどのような目的で符号を変えているのか

parse1で探索して評価値を出しているのに

なんでわざわざparse2でも評価関数を読んで評価値を計算するのか

predictionとは何なのか

f:id:daruma3940:20160520223745p:plain
かなりの前進なのじぇ!!!


daruma3940.hatenablog.com