Problem To Artificial Intelligence ~AIを作るAI~

# はじめに
本記事において生成するAIはLinear Regression Modelを想定します．
複雑なNeural NetworkやTree Decision Modelを想像してページを開いた方はごめんなさい．
(Neural Architecture Searchの記事でもないです)

# AIをつくるAIとは？

通常のAI(機械学習)は問題($X, y \in P$)が与えられたときに，$y=f(X)$となるような関数$f$を見つけ出します．
このAIの入力は$X$であり，出力は$y$です．

![problemToAI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219048/8e237919-f124-ddfb-c752-ff52b3dd99ca.jpeg)

一方で，AIをつくるAIとは問題が入力されたときに，AIを出力するAIのことを指します．

![problemToAI (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219048/a996490e-e9bd-b60b-4e94-52b4735eaf36.jpeg)

ちょっとややこしいので，AIをつくるAIをMeta AI，作られるAIを単にAIと呼びます．

(Meta AIが過学習しなければ)このとき出力されるAIは問題$P$を解くことができるのです．

# 実験設定
さて，AIを出力するといっても，不定形のものを学習することはできません．

そこで本記事では，出力されるAIは**Linear Regressionの重み**とします．

また，入力として与える問題$P$は，**データ数および特徴量数が固定の回帰問題**とします．

この実験設定でうまくいかないのであれば，どんなAIもつくることは困難でしょう．
![problemToAI (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219048/716458c6-344c-87f9-8e97-bf4012f8e38d.jpeg)

## 問題の生成
さて，Meta AIを学習させるためには大量の問題が必要となります．現実的に(データ数と特徴量数が同じ)問題を大量に集めることは不可能なので，今回は問題を生成することにします．

いま，作られるAIは回帰問題を解くAIなので，ランダムに回帰問題を作ります．
このとき，問題の特徴量$X$は，ガウス分布から生成します．このとき平均値および標準偏差は[0.0,1.0]の任意の数にします．
(一様分布にしたらMeta AIの学習があんまりうまくいかなかった)

```python
weight = np.random.random(features_num)
X = []
for _ in range(features_num):
	x = np.random.normal(loc=random.random(), scale=random.random(),size=instance_num)
	X.append(x)
X = np.array(X).T
#X = np.random.random((instance_num,features_num))
y = np.sum(X*weight, axis=1)
```

また，この問題をMeta AIに与えるときのMeta特徴量は，~~めんどうなので~~$X$をflatにして$y$とconcatした物にします．

```python
def makeProblemFeatures(X,y):
	features = np.concatenate([X.flatten(),y])
	return features
```
前述のとおり，Meta AIはLinear Regressionの重みとします．
この問題を解いた時の重みは明らかに```weight```です．
これを好きな学習データ数分生成します．
今回は，暇だったので1000000問題つくりました．また，各問題のデータ数は100，特徴量数は3です．
過学習すると，正則化とか表現力とかパラメータ調整とか，面倒な話が出てくるので，問題数でMeta AIの過学習を抑えます．

```python
import numpy as np
import pandas as pd
import pickle
import random
import sys

def makeProblemFeatures(X,y):
	features = np.concatenate([X.flatten(),y])
	return features

features_num = 3
instance_num = 100
train_num = 1000000
test_num = 10000

dataset = {}

problem_F = []
problem_target = []
inputs = []
target = []
for i in range(train_num):
	weight = np.random.random(features_num)
	X = []
	for _ in range(features_num):
		x = np.random.normal(loc=random.random(), scale=random.random(),size=instance_num)
		X.append(x)
	X = np.array(X).T
	y = np.sum(X*weight, axis=1)
	problem_F.append(makeProblemFeatures(X,y))
	problem_target.append(weight)
	inputs.append(X)
	target.append(y)
	sys.stdout.write("\r making training dataset ({} / {})".format(i+1,train_num))
print("end")
problem_F = np.array(problem_F)
problem_target = np.array(problem_target)
inputs = np.array(inputs)
target = np.array(target)

dataset["train"] = {}
dataset["train"]["problem_F"] = problem_F
dataset["train"]["problem_target"] = problem_target
dataset["train"]["inputs"] = inputs
dataset["train"]["target"] = target


problem_F = []
problem_target = []
inputs = []
target = []
for i in range(test_num):
	weight = np.random.random(features_num)
	X = []
	for _ in range(features_num):
		x = np.random.normal(loc=random.random(), scale=random.random(),size=instance_num)
		X.append(x)
	X = np.array(X).T
	y = np.sum(X*weight, axis=1)
	problem_F.append(makeProblemFeatures(X,y))
	problem_target.append(weight)
	inputs.append(X)
	target.append(y)
	sys.stdout.write("\r making test dataset ({} / {})".format(i+1,test_num))
print("end")

problem_F = np.array(problem_F)
problem_target = np.array(problem_target)
inputs = np.array(inputs)
target = np.array(target)

dataset["test"] = {}
dataset["test"]["problem_F"] = problem_F
dataset["test"]["problem_target"] = problem_target
dataset["test"]["inputs"] = inputs
dataset["test"]["target"] = target
f = open("dataset","wb")
pickle.dump(dataset,f)
f.close()
```
testデータも同様に生成しました．testデータは10000問題です．
また，各問題の$X,y$(```input```, ```target```)を保存していますが以後使わないです．

## Meta AIのモデル
AIをつくるAIのモデルですが，全結合のNeural Networkとします．
入力は問題のmeta 特徴量として，400次元($100 \times 3 + 100$)のVectorが入ります．
出力は3次元Vectorです．
中間層は8層にしました．10層full connection layerにロマンを感じたので．
ほんとは，$X$と$y$を別入力で入れて，何層か挟んでからconcatしたほうがいい気がします．
モデルは，pytorchで書きました．初めて書いたけど思ってたより書きやすいですね．

```python
import torch
import torch.nn as nn
import torch.nn.functional as functional

from torch.autograd import Variable 
import torch.optim as optim 
import torch.utils.data 
from torchvision import datasets, models, transforms
import numpy as np
import sys
import pickle
from sklearn.preprocessing import StandardScaler 
from sklearn.preprocessing import MinMaxScaler

def multi_output_loss(out, weight):
	loss = torch.sum(torch.abs(out - weight))/(weight.shape[0]*weight.shape[1])
	return loss


class Network(nn.Module):
	def __init__(self, feature_num):
		super(Network, self).__init__()
		self.net = nn.Sequential(
			nn.Linear(feature_num, 1000),
			nn.ReLU(),
			nn.Linear(1000, 1000),
			nn.ReLU(),
			nn.Linear(1000, 1000),
			nn.ReLU(),
			nn.Linear(1000, 1000),
			nn.ReLU(),
			nn.Linear(1000, 1000),
			nn.ReLU(),
			nn.Linear(1000, 1000),
			nn.ReLU(),
			nn.Linear(1000, 1000),
			nn.ReLU(),
			nn.Linear(1000, 1000),
			nn.ReLU(),
			nn.Linear(1000, 100),
			nn.ReLU(),
			nn.Linear(100, 3)
		)

	def forward(self, x):
		x = self.net(x)
		return x

	def fit(self, problem_F, weight):
		device = torch.device("cuda")
		#device = torch.device("cpu")
		train = torch.utils.data.TensorDataset(
			torch.from_numpy(problem_F), torch.from_numpy(weight))
		train_loader = torch.utils.data.DataLoader(train, batch_size=1000, shuffle=True)

		optimizer = optim.Adam(self.parameters(), lr=0.001)
		model  = self.to(device)
		max_epoch = 100
		for epoch in range(max_epoch):
			training_loss = 0
			for i, data in enumerate(train_loader):
				_F, _weight = data
				_F = Variable(_F.to(device))
				_weight = Variable(_weight.to(device))

				optimizer.zero_grad()
				out = model(_F)
				loss = multi_output_loss(out, _weight)
				training_loss += loss.item()
				loss.backward()
				optimizer.step()
			training_loss =training_loss / len(train_loader)
			print("training ({} / {}), train Loss : {}".format(epoch+1,max_epoch, training_loss))
		print(" end")
	def predict(self, F):
		model  = self.to("cpu")
		F = Variable(torch.from_numpy(F))
		with torch.no_grad():
			w = model(F).detach().numpy()
		return w

if __name__ == "__main__":
	f = open("dataset","rb")
	dataset = pickle.load(f)
	f.close()

	problem_F = dataset["train"]["problem_F"]
	weight = dataset["train"]["problem_target"]
	problem_F = problem_F.astype(dtype=np.float32)
	weight = weight.astype(dtype=np.float32)
	print("train data num {}, train data feature num {}".format(problem_F.shape[0], problem_F.shape[1]))

	model = Network(problem_F.shape[1])
	model.fit(problem_F, weight)
	print("---------train--------")
	pred_w = model.predict(problem_F)
	for i in range(10):#len(pred_w)):
		print("true weight {}, pred weight {}".format(weight[i], pred_w[i]))

	train_error = np.sum(np.abs(pred_w - weight))/(weight.shape[0] * weight.shape[1])

	problem_F = dataset["test"]["problem_F"]
	weight = dataset["test"]["problem_target"]
	problem_F = problem_F.astype(dtype=np.float32)
	weight = weight.astype(dtype=np.float32)
	print("---------test--------")
	pred_w = model.predict(problem_F)
	for i in range(10):#len(pred_w)):
		print("true weight {}, pred weight {}".format(weight[i], pred_w[i]))

	test_error = np.sum(np.abs(pred_w - weight))/(weight.shape[0] * weight.shape[1])

	print("train error {}, test error {}".format(train_error, test_error))

	torch.save(model.state_dict(), "model")
```

## Parameters
Meta AIの学習パラメータですが

- Batch Size : 1000
- optimizer : Adam
- Learning rate : 0.001
- max epoch : 100

にしました．
誤差関数ですが，平均絶対値誤差(MAE)を使いました．MSEでもそんなに変わらないです．

# 実験結果
結果からいうと不完全なAIの生成には成功しました．
testのMAE誤差が0.048程度なので，あんまり精度がよくないです．
ちょっと層数を増やしすぎたかもしれません．過学習はデータ数をもっと増やして，データ数でごり押せば何とかなりそう．

```
train error 0.03311155989583333, test error 0.047954313151041664
```
テストデータにおいて，生成されたLinear Regressionの重みは次通りです(10個だけ表示)．
true weightが真のLinear Regressionの重み，
pred weightがMeta AIが予想したLinear Regressionの重みです．

```
---------test--------
true weight [0.2661951  0.00673665 0.6146182 ], pred weight [0.27162865 0.07822143 0.20651376]
true weight [0.6896427  0.00971592 0.55464643], pred weight [0.72070605 0.04845329 0.5430689 ]
true weight [0.17735223 0.1530034  0.21936643], pred weight [0.20701459 0.14589433 0.23499718]
true weight [0.9953587  0.30713415 0.09861181], pred weight [0.9829714  0.31441572 0.06271259]
true weight [0.1797012  0.34231284 0.12426861], pred weight [0.26220655 0.36576685 0.06143581]
true weight [0.09689087 0.9697109  0.697348  ], pred weight [0.18424456 0.95420986 0.32859597]
true weight [0.1987553  0.41723123 0.37762934], pred weight [0.16770707 0.482017   0.3129202 ]
true weight [0.7517576  0.05367153 0.34765583], pred weight [0.73589396 0.13084184 0.32682523]
true weight [0.40666658 0.30940092 0.5539815 ], pred weight [0.41471976 0.29739022 0.58724344]
true weight [0.6700682  0.20830701 0.5595491 ], pred weight [0.66049206 0.3199482  0.5781056 ]
```
なんとなく相対的に(3つの重みのスケール感)はあっている気がしますが，絶対値で見るとあんまりあっていません．
特に，少数点3桁になると予想しにくいようです．おそらく，値が小さくなると，その特徴量xのデータを使うことが予測のはずれを生むのでしょう．
精度向上は今後の課題ですね．

#おわりに
本記事ではAIをつくるAIをやってみました．
もっとも簡単と想定されるLinear Regressionでも完全には作ることができなそうです．
また，本記事における実験は，**すべての問題の特徴量数・データ数が同じである**という設定にしていますが，**現実的にそんなことはありません**．問題そのものの特徴量を抽出する方法を考えなければいけません．さらに，**Linear Regressionで解ける問題**のみを扱っていること，**問題にノイズが含まれていないこと**も非現実的です．もっと言うと，実問題で学習する場合には，全ての問題を解く必要があります．今回のような100万個の問題を用意することは，不可能だと考えられます．
しかし，今回の実験によってAIをつくるAIが可能であることも示唆されています．もしも画像ベースなどの問題でも同じことが可能なのかを検証してみたいです．(とりあえず次は非線形問題かな)