ニューラルネットワークを使えるようにする。

Luaで、引いてはNScripter人工知能の一種であるニューラルネットワークを使えるようにする。
http://www.forums.evilmana.com/psp-lua-codebase/lua-neural-networks/をコピペ。

NeuralNetwork.lua

ACTIVATION_RESPONSE = 1

NeuralNetwork = {
	transfer = function( x) return 1 / (1 + math.exp(-x / ACTIVATION_RESPONSE)) end --This is the Transfer function (in this case a sigmoid)
}

function NeuralNetwork.create( _numInputs, _numOutputs, _numHiddenLayers, _neuronsPerLayer, _learningRate)
	_numInputs = _numInputs or 1
	_numOutputs = _numOutputs or 1
	_numHiddenLayers = _numHiddenLayers or math.ceil(_numInputs/2)
	_neuronsPerLayer = _neuronsPerLayer or math.ceil(_numInputs*.66666+_numOutputs)
	_learningRate = _learningRate or .5
	--order goes network[layer][neuron][wieght]
	local network = setmetatable({
		learningRate = _learningRate
	},{ __index = NeuralNetwork});
	network[1] = {}   --Input Layer
	for i = 1,_numInputs do
		network[1][i] = {}
	end
	for i = 2,_numHiddenLayers+2 do --plus 2 represents the output layer (also need to skip input layer)
		network[i] = {}
		local neuronsInLayer = _neuronsPerLayer
		if i == _numHiddenLayers+2 then
			neuronsInLayer = _numOutputs
		end
		for j = 1,neuronsInLayer do
			network[i][j] = {bias = math.random()*2-1}
			local numNeuronInputs = table.getn(network[i-1])
			for k = 1,numNeuronInputs do
				network[i][j][k] = math.random()*2-1  --return random number between -1 and 1
			end
		end
	end
	return network
end
	
function NeuralNetwork:forewardPropagate(...)
	if table.getn(arg) ~= table.getn(self[1]) and type(arg[1]) ~= "table" then
		error("Neural Network received "..table.getn(arg).." input[s] (expected "..table.getn(self[1]).." input[s])",2)
	elseif type(arg[1]) == "table" and table.getn(arg[1]) ~= table.getn(self[1]) then
		error("Neural Network received "..table.getn(arg[1]).." input[s] (expected "..table.getn(self[1]).." input[s])",2)
	end
	local outputs = {}
	for i = 1,table.getn(self) do
		for j = 1,table.getn(self[i]) do
			if i == 1 then
				if type(arg[1]) == "table" then
					self[i][j].result = arg[1][j]
				else
					self[i][j].result = arg[j]
				end
			else
				self[i][j].result = self[i][j].bias
				for k = 1,table.getn(self[i][j]) do
					self[i][j].result = self[i][j].result + (self[i][j][k]*self[i-1][k].result)
				end
				self[i][j].result = NeuralNetwork.transfer(self[i][j].result)
				if i == table.getn(self) then
					table.insert(outputs,self[i][j].result)
				end
			end
		end

	end
	return outputs
end

function NeuralNetwork:backwardPropagate(inputs,desiredOutputs)
	if table.getn(inputs) ~= table.getn(self[1]) then
		error("Neural Network received "..table.getn(inputs).." input[s] (expected "..table.getn(self[1]).." input[s])",2)
	elseif table.getn(desiredOutputs) ~= table.getn(self[table.getn(self)]) then
		error("Neural Network received "..table.getn(desiredOutputs).." desired output[s] (expected "..table.getn(self[table.getn(self)]).." desired output[s])",2)
	end
	self:forewardPropagate(inputs) --update the internal inputs and outputs
	for i = table.getn(self),2,-1 do --iterate backwards (nothing to calculate for input layer)
		local tempResults = {}
		for j = 1,table.getn(self[i]) do
			if i == table.getn(self) then --special calculations for output layer
				self[i][j].delta = (desiredOutputs[j] - self[i][j].result) * self[i][j].result * (1 - self[i][j].result)
			else
				local weightDelta = 0
				for k = 1,table.getn(self[i+1]) do
					weightDelta = weightDelta + self[i+1][k][j]*self[i+1][k].delta
				end
				self[i][j].delta = self[i][j].result * (1 - self[i][j].result) * weightDelta
			end
		end
	end
	for i = 2,table.getn(self) do
		for j = 1,table.getn(self[i]) do
			self[i][j].bias = self[i][j].delta * self.learningRate
			for k = 1,table.getn(self[i][j]) do
				self[i][j][k] = self[i][j][k] + self[i][j].delta * self.learningRate * self[i-1][k].result
			end
		end
	end
end

function NeuralNetwork:save()
	--[[
	File specs:
		|INFO| - should be FF BP NN
		|I| - number of inputs
		|O| - number of outputs
		|HL| - number of hidden layers
		|NHL| - number of neurons per hidden layer
		|LR| - learning rate
		|BW| - bias and weight values
	]]--
	local data = "|INFO|FF BP NN|I|"..tostring(table.getn(self[1])).."|O|"..tostring(table.getn(self[table.getn(self)])).."|HL|"..tostring(table.getn(self)-2).."|NHL|"..tostring(table.getn(self[2])).."|LR|"..tostring(self.learningRate).."|BW|"
	for i = 2,table.getn(self) do -- nothing to save for input layer
		for j = 1,table.getn(self[i]) do
			local neuronData = tostring(self[i][j].bias).."{"
			for k = 1,table.getn(self[i][j]) do
				neuronData = neuronData..tostring(self[i][j][k])
				neuronData = neuronData..","
			end
			data = data..neuronData.."}"
		end
	end
	data = data.."|END|"
	return data		
end
function NeuralNetwork.load( data)
	local dataPos = string.find(data,"|")+1
	local currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
	local dataPos = string.find(data,"|",dataPos)+1
	local _inputs, _outputs, _hiddenLayers, _neuronsPerLayer, _learningRate
	local biasWeights = {}
	local errorExit = false
	while currentChunk ~= "END" and not errorExit do
		if currentChuck == "INFO" then
			currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
			dataPos = string.find(data,"|",dataPos)+1
			if currentChunk ~= "FF BP NN" then
				errorExit = true
			end
		elseif currentChunk == "I" then
			currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
			dataPos = string.find(data,"|",dataPos)+1
			_inputs = tonumber(currentChunk)
		elseif currentChunk == "O" then
			currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
			dataPos = string.find(data,"|",dataPos)+1
			_outputs = tonumber(currentChunk)
		elseif currentChunk == "HL" then
			currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
			dataPos = string.find(data,"|",dataPos)+1
			_hiddenLayers = tonumber(currentChunk)
		elseif currentChunk == "NHL" then
			currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
			dataPos = string.find(data,"|",dataPos)+1
			_neuronsPerLayer = tonumber(currentChunk)
		elseif currentChunk == "LR" then
			currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
			dataPos = string.find(data,"|",dataPos)+1
			_learningRate = tonumber(currentChunk)
		elseif currentChunk == "BW" then
			currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
			dataPos = string.find(data,"|",dataPos)+1
			local subPos = 1 
			local subChunk
			for i = 1,_hiddenLayers+1 do
				biasWeights[i] = {}
				local neuronsInLayer = _neuronsPerLayer
				if i == _hiddenLayers+1 then
					neuronsInLayer = _outputs
				end
				for j = 1,neuronsInLayer do
					biasWeights[i][j] = {}
					biasWeights[i][j].bias = tonumber(string.sub(currentChunk,subPos,string.find(currentChunk,"{",subPos)-1))
					subPos = string.find(currentChunk,"{",subPos)+1
					subChunk = string.sub( currentChunk, subPos, string.find(currentChunk,",",subPos)-1)
					local maxPos = string.find(currentChunk,"}",subPos)
					while subPos < maxPos do
						table.insert(biasWeights[i][j],tonumber(subChunk))
						subPos = string.find(currentChunk,",",subPos)+1
						if string.find(currentChunk,",",subPos) ~= nil then
							subChunk = string.sub( currentChunk, subPos, string.find(currentChunk,",",subPos)-1)
						end
					end
					subPos = maxPos+1
				end
			end			
		end
		currentChunk = string.sub( data, dataPos, string.find(data,"|",dataPos)-1)
		dataPos = string.find(data,"|",dataPos)+1
	end
	if errorExit then
		error("Failed to load Neural Network:"..currentChunk,2)
	end
	local network = setmetatable({
		learningRate = _learningRate
	},{ __index = NeuralNetwork});
	network[1] = {}   --Input Layer
	for i = 1,_inputs do
		network[1][i] = {}
	end
	for i = 2,_hiddenLayers+2 do --plus 2 represents the output layer (also need to skip input layer)
		network[i] = {}
		local neuronsInLayer = _neuronsPerLayer
		if i == _hiddenLayers+2 then
			neuronsInLayer = _outputs
		end
		for j = 1,neuronsInLayer do
			network[i][j] = {bias = biasWeights[i-1][j].bias}
			local numNeuronInputs = table.getn(network[i-1])
			for k = 1,numNeuronInputs do
				network[i][j][k] = biasWeights[i-1][j][k]
			end
		end
	end
	return network
end

使い方

NL_dofile("NeuralNetwork.lua") -- まずは読み込む。
-- ネットワークを作る。サンプルとして、xorを実現する人工知能を作る。
input = 2 -- 入力用ニューロンの数。
output = 1 -- 出力用ニューロンの数。
-- xor は二つの入力から一つの出力を発生させる。なので、2と1になる。
hiddenLayers = 1 -- 隠れレイヤーの数
neuronsPerLayer = 4 -- 隠れレイヤー一つあたりのニューロンの数。
-- 隠れレイヤーは1以上の整数ならなんでもいいが普通は1でよい。増やしても極端に精度がよくなる訳でもない。レイヤーあたりの数は、適当。
learningRate = 0.5 -- 学習精度。0.5くらいで始めて。増やすと大雑把に、減らすと細かくなる。減らすほど学習に時間がかかることも忘れずに。
network = NeuralNetwork.create(input,output,hiddenLayers,neuronsPerLayer,learningRate)
-- networkに作成した人工知能が入る。以降はこれに対して操作をする。

-- 計算をさせる。引数は、入力用ニューロンに最初に与える値。
result = network:forewardPropagate(in1,in2) -- 結果はテーブルの形を取る。result[1]のようにしてアクセスする。

-- 学習をさせる。引数は、二つのテーブル。最初のテーブルは入力用ニューロンに与える値、第二引数は教師信号
network:backwardPropagate({0,0},{0})
-- ただし、一度の学習ではちょっとしか学習しないので、10,000回はぶん回すのが基本。
for i = 1, 10000 do network:backwardPropagate({0,0},{0}) end
-- これならいい加減覚えるだろう。きっと多分。

-- 人工知能を保存する。
str = network:save()
-- str にダンプした人工知能の文字列が入る。これをNScripterの文字列変数に保存してもよい。

-- 人工知能を再生する。
new_network = NeuralNetwork.load(str)
-- ちなみに実際やってみると、同じ人工知能でも保存・再生で微妙に差異が出る模様。コピーすると劣化するとか、生物っぽいね!

これはどういうもので、何ができるか。

ニューラルネットワークは、一種の人工知能である。
入力には、0〜1までの数値*1を入力用ニューロンの数と同じだけ与えると、内部で謎の計算をして、出力用ニューロンの数だけやはり0〜1の範囲の数値を返してくる。
作られたばかりの初期値では内部がランダムに設定されているので、出力される数値もデタラメである。

result = network:fowardPropagate(i1,i2)

そこで、教師信号を与えて学習をさせる。つまり、ある入力の時は、教師信号と同じ数値(実際には近似値)を返すように繰り返し躾けるのである。

for i=1,10000 do network:backwardPropagate({i1,i2},{o1}) end

こうして学習したニューラルネットワークは、その特定の入力を与えれば特定の出力を返すし、逆にそれ以外の入力を与えても「それっぽい」出力を返すようになる。
この「それっぽい」が肝心なところで、どんな難問にも回答不能に陥ることなく、なんらかの回答を返す堅牢性がニューラルネットワークの特徴である。(その回答が正しいかどうかはまた別の問題)

*1:実はこの範囲から逸脱してもよい。