意味AIを作る – 複数ワード検索システム

ぼちぼち改良を続けています

今回はグーグル検索のように”スペース区切りの複数ワード“で知識を検索できるようにしてみました

処理の流れとしては

  1. HTTP通信で受け取った文字列をスペースで分割し、複数のワードを取り出す
  2. 各ワードを知識IDに変換
  3. それぞれの知識から最も近い点(重心)を求める
  4. 該当する知識をスコア順に並べて表示する

と言った感じです

HTTP通信で受け取った文字列を分割する

検索例ですが、webページ経由で「毒 魚介類」と検索をすると次のようなデータが送られてきます

文字列がURLエンコードされており、%と16進数で表現されます。半角スペースは+記号に変換されます。

これを人が読める文字列に戻すため、次のデコード関数に通します。

int URLDecode(char* dst, char* src, int maxLen)
{
	int i;
	char c;
	char str2[16];
	char* p = src;
	i = 0;
	while (1) {
		if (i >= maxLen)
			break;

		if (*p == '&' || *p == '\0' || *p == '\r')
			break;

		if (*p == '%') {
			str2[0] = p[1];
			str2[1] = p[2];
			str2[2] = '\0';
			c = atoi16_2(str2);
			p += 3;
		}
		else {
			if (*p == '+') {
				c = ' ';
			}
			else {
				c = *p;
			}
			p++;
		}

		dst[i] = c;
		i++;
	}
	dst[i] = '\0';
	return 0;
}

inline int atoi16_2(LPCTSTR pcszStr)
{
	const char* p;
	int ret, nMul;

	if (*pcszStr == '\0')
		return 0;
	ret = 0;
	nMul = 1;	// べき乗
	for (p = &pcszStr[lstrlen(pcszStr) - 1]; p >= pcszStr; p--)
	{
		if (*p >= '0' && *p <= '9') {
			ret += (*p - '0') * nMul;
		}
		else if (*p >= 'A' && *p <= 'F') {
			ret += (*p - 'A' + 10) * nMul;
		}
		else if (*p >= 'a' && *p <= 'f') {
			ret += (*p - 'a' + 10) * nMul;
		}
		else if (*p == 'x') {
			return ret;
		}
		else {
			return -1;
		}
		nMul *= 16;
	}
	return ret;
}

これでエンコード文字列がUTF-8形式の「毒 魚介類」という文字列に変換されました

その文字列をShift-JISに変換するために次の関数に通します

inline void UTF8ToShiftJis(char* bufShiftJis, char* bufUTF8, int bufLen)
{
	wchar_t bufUnicode[MAX_PATH];

	// まずUniocdeに変換する
	// サイズを計算する
	int iLenUnicode = MultiByteToWideChar(CP_UTF8, 0, bufUTF8, strlen(bufUTF8) + 1, NULL, 0);
	if (iLenUnicode <= sizeof(bufUnicode) / sizeof(bufUnicode[0]))
	{
		MultiByteToWideChar(CP_UTF8, 0, bufUTF8, strlen(bufUTF8) + 1, bufUnicode, MAX_PATH);
		// 次に、UniocdeからShiftJisに変換する
		// サイズを計算する
		int iLenUtf8 = WideCharToMultiByte(CP_ACP, 0, bufUnicode, iLenUnicode, NULL, 0,
			NULL, NULL);
		if (iLenUtf8 <= bufLen)
		{
			WideCharToMultiByte(CP_ACP, 0,
				bufUnicode, iLenUnicode,
				bufShiftJis, bufLen,
				NULL, NULL);
		}
	}
}

Win32APIを使ってます

次にShiftJISに変換した文字列をスペース区切りで取り出します

Pythonなら.splitで一瞬なのですが、C++だと処理を自作しないといけません

inline void SplitString(const char * pStr, std::vector<string>&result, std::vector<std::string>& delimiters, int nMax) {
	const char* p = pStr;
	result.clear();
	string str;
	char c_word[4] = "";

	while (*p) {
		// 1文字を取得
		if ((*p & 0x80) != 0) {
			c_word[0] = p[0];
			c_word[1] = p[1];
			c_word[2] = 0;
			p++;
		}
		else {
			c_word[0] = p[0];
			c_word[1] = 0;
		}

		// 区切り文字かどうかをチェックして分岐
		bool bFind = false;
		for (auto& delimiter : delimiters) {
			if (delimiter == c_word) {
				bFind = true;
				break;
			}
		}
		if (bFind) {
			result.push_back(str);
			str.clear();
			if (result.size() >= nMax)
				break;
		}
		else {
			str += c_word;
		}
		p++;
	}

	if (str != "") {
		result.push_back(str);
	}
}

元の文字列と区切り文字の配列を渡すことで、結果をvector<string>配列で取り出すことができます

使い方は次のような感じ

vector<string> str_list, delimitors;
delimitors.push_back(" "); // 半角スペース
delimitors.push_back(" "); // 全角スペース
SplitString(strSrc, str_list, delimitors, 8);

各ワードを知識IDに変換

これは、名称と知識IDを照合するだけなので割愛

それぞれの知識から最も近い点(重心)を求める

ここが肝心の処理

それぞれの知識からの重心を求めるにはどうしたら良いのか考えました

知識の重心とは”すべての知識から等距離にある知識“です

思いついたのが、それぞれの知識を階層的に下って行き距離に応じて重みづけをして、その重み同士を掛け合わせてスコアにするという方法

詳細な流れとしては、知識ネットワークを再帰的に下って行き、1階層ごとに重みを減点します。重みは探索開始時に10ポイントあり、10階層辿ると探索を終了します。それぞれのワードごとに重みのマップを作り、各マップのポイントを掛け合わせて一番高い数値になった知識が重心になります。

これは四角形の縦横の長さが等しいときに面積が一番大きくなる法則を応用しています(法則名知らない)。すべての地点から等距離にある場所が一番ポイントが高くなります。

// 複数のIDから情報を検索
void CMOManager::MultiIdsSearch(vector<DWORD>& ids, multimap<DWORD, DWORD>& result) {
	DWORD dimension = ids.size();
	result.clear();

	// 渡された各IDごとに探索し距離に応じた点数を付ける
	for (int i = 0; i < dimension; i++) {
		WeightedHierarchyTraversal(ids[i], i);
	}

	DWORD score;
	for (auto& obj : m_data) {
		score = 1;
		for (int i = 0; i < dimension; i++) {
			score *= obj.dwRoutes[i];
			if (score == 0)
				break;
		}
		if (score > 1) {
			result.insert(make_pair(score, obj.id));
		}
	}
}

// weighted Hierarchy Traversal
void CMOManager::WeightedHierarchyTraversal(DWORD id, int dimension) {
	for (auto& obj : m_data) {
		obj.dwRoutes[dimension] = 0;
	}

	WeightedHierarchyTraversalNext(id, dimension, 10, false);
}

void CMOManager::WeightedHierarchyTraversalNext(DWORD id, int dimension, int nest, bool bReferenceFlag) {
	if (nest == 0) return;
	CMeanObject& obj = m_data[id];
	if (obj.dwRoutes[dimension] > nest) {
		return;	// 既に通過済みなら切り上げる
	}
	else {
		obj.dwRoutes[dimension] = nest;
	}
	for (auto& link : obj.links) {
		WeightedHierarchyTraversalNext(link.id, dimension, nest - 1, true);
	}

	if (!bReferenceFlag) {
		for (auto& link : obj.rlinks) {
			WeightedHierarchyTraversalNext(link.id, dimension, nest - 1, false);
		}
	}
}

辿った経路を記録するためのメンバー変数”dwRoutes[8]”をスコア計算に再利用しています。

結果の出力にはmultimapを使いました。multimapならソート済みになりスコアが被ってもOKです。

また、参照・被参照方向に情報を展開・探索するのですが、情報を展開するルールがあることに気付きました。

例ですが、”とうがらし”ではなく”とうがらしの実”に赤いを結び付けていたため、”とうがらし”と”りんご”が赤いで交わりませんでした。参照方向に下り続けるだけでも、被参照方向に上り続けるだけでも知識は交わりません。一度、上ってから下る必要があります。そして、一度参照方向に下ったら、被参照方向には上がれないようにするのもコツです。

りんご→赤い→血。一度参照に下ってから被参照に戻る。これはNGです。これを許してしまうと、全く関係ないものが近くで繋がってしまいます。

りんご→あの山のりんご→酸っぱい。被参照に上ってから参照に下る。これはOKです。

上手く言葉で言い表せませんが、曲線ではなく直線同士を交差させるイメージでしょうか

該当する知識をスコア順で並べて表示

先ほどの処理で検索結果(スコアと知識IDのソート済み配列)が手に入ったので、あとはHTMLに出力するだけです。

multimapはrbegin, rendを使うことで降順にデータを取り出せます。

毒からも魚介類からも一階層下ったところにある魚貝毒が81(9×9)ポイントで最上位になりました。

シガテラ、ふぐ毒、貝毒。何となく連想できそうなものが出てきてますね。

これが、人が何となくやってる物事を連想する仕組みじゃないでしょうか。

検索ワードは3つでも4つでも上手く動作しました

おわりに

私は10代の頃に仕組みを解明すればするほど力が手に入るゲーム改造という世界と出会いました。

記号の羅列を眺めながら、物事を内へ内へと分解していく作業は、退屈でつまらないもののように感じられますが、分解して得られたパーツは、新しいものを生み出すための道具になります。

コンピュータの世界では1つの原理を知ることで100の新しい法則を生み出すことが出来ます。哲学ととても相性が良いのです。

お金を稼ぐためのライブラリを使った応用応用のプログラミングもありますが、原理を追求して全く新しいものを生み出す発明的なプログラミングもあります。

最近のAIは、人間らしく振る舞うことに振り切っていて、人間の思考の原理を再現することを諦めてしまっているように感じられます。それは人の思考の仕組みが”未知“だからです。

私は”どんな知識も何と繋がっているかで表現できる“という発見をしてから、15年近く、心や思考の仕組みについて哲学しています。あの日の発見は、私に100どころか10000の理解を与えてくれました。だから私はこのリバースエンジニアリングの虜なのです。

物事の仕組みを追求して、それを形にする。こんな楽しいプログラミング(生き方)は他に無いと思っています。

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です