AI でルービックキューブを揃う(その2)Unity で目視ユニットテストを兼ねて可視化する
Django Backend
まず Django のプロジェクトを作る.
django-admin startproject DeepCubeServer
Django に必要な設定を行い,tests という app を作る.
python3 manage.py startapp tests
Django のフォルダーにおいて,前回作ったクラスを DeepCube というフォルダーに移動する./DeepCube/__init__.py を忘れずに作る.
前回のクラスを継承して,新しく CubeUnity クラスを作成し,Json で渡せる形の辞書を作りたい.Unity で利用するので,最も理想の形は,各 Cubelet の各面の色の説明辞書を渡すことだと思われる.これは前回ちゃんと定義したクラスから簡単に作れる.(コメントを除くわずか 12 行)
class CubeUnity(Cube): """ Cube class use for unity. """ def get_description(self) -> dict[str, dict[int, int]]: """ Target format 20 x 3 dictionary. :return: { SLOT: { FACE_CHAR: COLOR_CHAR, ... }, ... } e.g. { 'UFR': { 'U': 'W', 'F': 'R', 'R': 'G' }, ... } """ # Center description = {face: {face: COLORS_[code]} for face, code in FACES.items()} for i in range(20): slot = CUBELETS_[i] colors = [COLORS_[FACES[j]] for j in CUBELETS_[self.states[i][0]]] phase = self.states[i][1] divisor = 3 if i < 8 else 2 faces = [slot[(j + phase) % divisor] for j in range(divisor)] print(slot, faces, colors) description[slot] = {j: k for j, k in zip(faces, colors)} return description
最後に,/tests/views.py で Unity で呼び出すための API を作る.
def test_cube_class(request): """ A restful API to test if the `move` method of `Cube` class work correctly. GET parameters: actions string e.g. "UFuf" return: Json Dictionary. See `CubeUnity`. """ actions = request.GET.get('actions', '') cube = CubeUnity() try: for a in actions: cube.move(a) except ValueError: return JsonResponse({}, status=403) return JsonResponse(cube.get_description(), status=200)
Unity
Unity では,Script のみでキューブを作成する.空の GameObject を作り,GameMaster と名付ける.C# の Script TS1GM.cs を作成し,GM にくっつける.
まずは必要な定数と変数を定義する.
// Faces
private readonly char[] faces = { 'U', 'D', 'F', 'B', 'L', 'R' };
// Colors
private readonly Dictionary<char, Color> colorDict = new()
{
{'K', new Color(0f, 0f, 0f)},
{'B', new Color(0f, 70f / 255f, 173f / 255f)},
{'G', new Color(0f, 155f / 255f, 72f / 255f)},
{'O', new Color(255f / 255f, 88f / 255f, 0f)},
{'R', new Color(183f / 255f, 18f / 255f, 52f / 255f)},
{'W', new Color(1f, 1f, 1f)},
{'Y', new Color(255f / 255f, 213f / 255f, 0f)}
};
// Position and Scale of stickers.
private readonly Dictionary<char, Vector3> stickerPositionDict = new()
{
{'U', new Vector3(0f, 0.49f, 0f)},
{'D', new Vector3(0f, -0.49f, 0f)},
{'F', new Vector3( 0.49f, 0f, 0f)},
{'B', new Vector3(-0.49f, 0f, 0f)},
{'L', new Vector3(0f, 0f, -0.49f)},
{'R', new Vector3(0f, 0f, 0.49f)},
};
private readonly Dictionary<char, Vector3> stickerScaleDict = new()
{
{'U', new Vector3(0.85f, 0.04f, 0.85f)},
{'D', new Vector3(0.85f, 0.04f, 0.85f)},
{'F', new Vector3(0.04f, 0.85f, 0.85f)},
{'B', new Vector3(0.04f, 0.85f, 0.85f)},
{'L', new Vector3(0.85f, 0.85f, 0.04f)},
{'R', new Vector3(0.85f, 0.85f, 0.04f)},
};
// Position of cubelets.
private readonly Dictionary<char, Vector3> cubeletPositionDict = new()
{
{'U', new Vector3(0f, 1f, 0f)},
{'D', new Vector3(0f, -1f, 0f)},
{'F', new Vector3( 1f, 0f, 0f)},
{'B', new Vector3(-1f, 0f, 0f)},
{'L', new Vector3(0f, 0f, -1f)},
{'R', new Vector3(0f, 0f, 1f)},
};
// URL
private const string URL = "http://127.0.0.1:8000/tests/cube_class/";
// Cube Object
private GameObject cube;
private string currentActions = "";
次に,小さい Cubelet 及びその上の Sticker を作るための method を作る.
private void CreateCubelet(string cubeletName, Vector3 position, Dictionary<char, char> face2color, GameObject parentObj)
{
// Initialize.
GameObject cubelet = GameObject.CreatePrimitive(PrimitiveType.Cube);
cubelet.name = cubeletName;
cubelet.GetComponent<MeshRenderer>().material.color = colorDict['K'];
// Set up stickers.
foreach (char face in faces)
{
GameObject sticker = GameObject.CreatePrimitive(PrimitiveType.Cube);
sticker.name = face.ToString();
// Set position, scale.
sticker.transform.position = stickerPositionDict[face];
sticker.transform.localScale = stickerScaleDict[face];
sticker.GetComponent<MeshRenderer>().material.color = face2color.TryGetValue(face, out char color) ? colorDict[color] : colorDict['K'];
// Grouping.
sticker.transform.SetParent(cubelet.transform);
}
cubelet.transform.position = position;
cubelet.transform.SetParent(parentObj.transform);
}
- cubeletName: URF みたいな名前,実質な用途はないが,もし Debug が必要になったら使えるかもしれない
- position: cubelet が cube に関する相対位置.
- face2color: 先 Django で定義した辞書.サーバーから Json の形で渡し,
_UpdateCubeで C# の辞書にしてここに渡す. - parentObj: どの Cube の cubelet かを指定する.
次に,サーバーに description を請求して,その中身を用いて先定義した method を呼び出す.
private IEnumerator _UpdateCube(string actions)
{
currentActions += actions;
// Request Json response.
string uri = string.IsNullOrEmpty(currentActions) ? URL : $"{URL}?actions={currentActions}";
print(uri);
using var webRequest = UnityWebRequest.Get(uri);
yield return webRequest.SendWebRequest();
string[] pages = uri.Split('/');
int page = pages.Length - 1;
switch (webRequest.result)
{
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError:
Debug.LogError($"{pages[page]}: Error: {webRequest.error}");
break;
case UnityWebRequest.Result.Success:
// Convert Json to Dictionary.
var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<char, char>>>(webRequest.downloadHandler.text);
// Reinitialize cube.
Destroy(cube);
cube = new GameObject("Cube");
// Set up cubelets.
foreach (var (slot, face2color) in jsonDict)
{
Vector3 position = slot.Aggregate(Vector3.zero, (_position, face) => _position + cubeletPositionDict[face]);
CreateCubelet(slot, position, face2color, cube);
}
break;
case UnityWebRequest.Result.InProgress:
default:
throw new ArgumentOutOfRangeException();
}
}
private void UpdateCube(string action = "")
{
StartCoroutine(_UpdateCube(action));
}
最後に,Cube を初期化して,キーボードのキーと action をくっつければ完成.
private void Awake()
{
// Handle input.
InputActionMap cubeActionMap = new InputActionMap();
foreach (var face in faces)
{
string faceUpper = face.ToString();
string faceLower = faceUpper.ToLower();
// Upper case
InputAction faceAction = cubeActionMap.AddAction(faceUpper, type: InputActionType.Button);
faceAction.AddCompositeBinding("OneModifier")
.With("Binding", $"<Keyboard>/{faceLower}")
.With("Modifier", "<Keyboard>/shift");
faceAction.performed += context => { UpdateCube(faceUpper); };
// Lower case
cubeActionMap.AddAction($"{faceUpper}'", binding: $"<Keyboard>/{faceLower}", type: InputActionType.Button).performed += context => { UpdateCube(faceLower); };
}
cubeActionMap.AddAction("esc", binding: "<Keyboard>/escape", type: InputActionType.Button).performed += context => { ResetCube(); };
cubeActionMap.Enable();
}
void Start()
{
// Initialize cube.
cube = new GameObject("Cube");
ResetCube();
}
private void ResetCube()
{
currentActions = "";
UpdateCube();
}
実際に試してみれば,各面が想定通りに動ける.


Script 全体:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;
using UnityEngine.InputSystem;
public class TS1GM : MonoBehaviour
{
// Faces
private readonly char[] faces = { 'U', 'D', 'F', 'B', 'L', 'R' };
// Colors
private readonly Dictionary<char, Color> colorDict = new()
{
{'K', new Color(0f, 0f, 0f)},
{'B', new Color(0f, 70f / 255f, 173f / 255f)},
{'G', new Color(0f, 155f / 255f, 72f / 255f)},
{'O', new Color(255f / 255f, 88f / 255f, 0f)},
{'R', new Color(183f / 255f, 18f / 255f, 52f / 255f)},
{'W', new Color(1f, 1f, 1f)},
{'Y', new Color(255f / 255f, 213f / 255f, 0f)}
};
// Position and Scale of stickers.
private readonly Dictionary<char, Vector3> stickerPositionDict = new()
{
{'U', new Vector3(0f, 0.49f, 0f)},
{'D', new Vector3(0f, -0.49f, 0f)},
{'F', new Vector3( 0.49f, 0f, 0f)},
{'B', new Vector3(-0.49f, 0f, 0f)},
{'L', new Vector3(0f, 0f, -0.49f)},
{'R', new Vector3(0f, 0f, 0.49f)},
};
private readonly Dictionary<char, Vector3> stickerScaleDict = new()
{
{'U', new Vector3(0.85f, 0.04f, 0.85f)},
{'D', new Vector3(0.85f, 0.04f, 0.85f)},
{'F', new Vector3(0.04f, 0.85f, 0.85f)},
{'B', new Vector3(0.04f, 0.85f, 0.85f)},
{'L', new Vector3(0.85f, 0.85f, 0.04f)},
{'R', new Vector3(0.85f, 0.85f, 0.04f)},
};
// Position of cubelets.
private readonly Dictionary<char, Vector3> cubeletPositionDict = new()
{
{'U', new Vector3(0f, 1f, 0f)},
{'D', new Vector3(0f, -1f, 0f)},
{'F', new Vector3( 1f, 0f, 0f)},
{'B', new Vector3(-1f, 0f, 0f)},
{'L', new Vector3(0f, 0f, -1f)},
{'R', new Vector3(0f, 0f, 1f)},
};
// URL
private const string URL = "http://127.0.0.1:8000/tests/cube_class/";
// Cube Object
private GameObject cube;
private string currentActions = "";
private void Awake()
{
// Handle input.
InputActionMap cubeActionMap = new InputActionMap();
foreach (var face in faces)
{
string faceUpper = face.ToString();
string faceLower = faceUpper.ToLower();
// Upper case
InputAction faceAction = cubeActionMap.AddAction(faceUpper, type: InputActionType.Button);
faceAction.AddCompositeBinding("OneModifier")
.With("Binding", $"<Keyboard>/{faceLower}")
.With("Modifier", "<Keyboard>/shift");
faceAction.performed += context => { UpdateCube(faceUpper); };
// Lower case
cubeActionMap.AddAction($"{faceUpper}'", binding: $"<Keyboard>/{faceLower}", type: InputActionType.Button).performed += context => { UpdateCube(faceLower); };
}
cubeActionMap.AddAction("esc", binding: "<Keyboard>/escape", type: InputActionType.Button).performed += context => { ResetCube(); };
cubeActionMap.Enable();
}
void Start()
{
// Initialize cube.
cube = new GameObject("Cube");
ResetCube();
}
private void CreateCubelet(string cubeletName, Vector3 position, Dictionary<char, char> face2color, GameObject parentObj)
{
// Initialize.
GameObject cubelet = GameObject.CreatePrimitive(PrimitiveType.Cube);
cubelet.name = cubeletName;
cubelet.GetComponent<MeshRenderer>().material.color = colorDict['K'];
// Set up stickers.
foreach (char face in faces)
{
GameObject sticker = GameObject.CreatePrimitive(PrimitiveType.Cube);
sticker.name = face.ToString();
// Set position, scale.
sticker.transform.position = stickerPositionDict[face];
sticker.transform.localScale = stickerScaleDict[face];
sticker.GetComponent<MeshRenderer>().material.color = face2color.TryGetValue(face, out char color) ? colorDict[color] : colorDict['K'];
// Grouping.
sticker.transform.SetParent(cubelet.transform);
}
cubelet.transform.position = position;
cubelet.transform.SetParent(parentObj.transform);
}
private IEnumerator _UpdateCube(string actions)
{
currentActions += actions;
// Request Json response.
string uri = string.IsNullOrEmpty(currentActions) ? URL : $"{URL}?actions={currentActions}";
print(uri);
using var webRequest = UnityWebRequest.Get(uri);
yield return webRequest.SendWebRequest();
string[] pages = uri.Split('/');
int page = pages.Length - 1;
switch (webRequest.result)
{
case UnityWebRequest.Result.ConnectionError:
case UnityWebRequest.Result.DataProcessingError:
case UnityWebRequest.Result.ProtocolError:
Debug.LogError($"{pages[page]}: Error: {webRequest.error}");
break;
case UnityWebRequest.Result.Success:
// Convert Json to Dictionary.
var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, Dictionary<char, char>>>(webRequest.downloadHandler.text);
// Reinitialize cube.
Destroy(cube);
cube = new GameObject("Cube");
// Set up cubelets.
foreach (var (slot, face2color) in jsonDict)
{
Vector3 position = slot.Aggregate(Vector3.zero, (_position, face) => _position + cubeletPositionDict[face]);
CreateCubelet(slot, position, face2color, cube);
}
break;
case UnityWebRequest.Result.InProgress:
default:
throw new ArgumentOutOfRangeException();
}
}
private void UpdateCube(string action = "")
{
StartCoroutine(_UpdateCube(action));
}
private void ResetCube()
{
currentActions = "";
UpdateCube();
}
}
AI でルービックキューブを揃う(その1)Cube クラスを実装する
導入
機械学習の準備として,まずルービックキューブを定式化する.そのために,Python で Cube クラスを作る.Cube では,次のプロパティとメソッドを想定する.
- ルービックキューブの状態を表すデータを保存する変数 states
- ルービックキューブの状態を初期化するための関数 reset
- 動きを受理して状態の遷移を定義する関数 move
- AI に使いやすい形の one-hot code を出力するための関数 one_hot
ルービックキューブのエンジニアリング
定数
Cube は Front, Back, Left, Right, Up, Down の6面を持っている.それぞれの面をその頭文字で表す.
FACES = {
'U': 0, # Up
'D': 1, # Down
'F': 2, # Front
'B': 3, # Back
'L': 4, # Left
'R': 5, # Right
}
FACES_ = {v: k for k, v in FACES.items()}
動きに関しては,6 面それぞれ時計回りと反時計回りで動けるので,計12パターンがある.面の大文字で時計回り,小文字で反時計回りで表すと,次のようになる.
ACTIONS = {
"U", "u'",
"D", "d'",
"F", "f",
"B", "b",
"L", "l",
"R", "r'",
}
色に関しては,世界標準配色を採用したうえ,初期状態の面と同じコードを振る.
COLORS = {
'Y': 0, # Yellow
'W': 1, # White
'R': 2, # Red
'O': 3, # Orange
'B': 4, # Blue
'G': 5, # Green
}
COLORS_ = {v: k for k, v in COLORS.items()}
Cube は 26 ピースの小さい Cube がある.それらを cubelet(s) と呼ぶ.そのうち,六面に中央 cubelet 各1つ,コーナーにある cubelets 8 個,エージにある cubelets 12 個.中央 cubelets は動けないため,考察の対象外にする.コーナー(エージー)の cubelets は,初期状態でどの3つ(2つ)の面にあるのかによって,それらの面の3(2)文字で表す.例えば,UFR は 上,前,右にある cubelet を表す.
CUBELETS = {
# コーナー
'UFR': 0,
'URB': 1,
'UBL': 2,
'ULF': 3,
'DFL': 4,
'DLB': 5,
'DBR': 6,
'DRF': 7,
# エッジ
'UF': 8,
'UR': 9,
'UB': 10,
'UL': 11,
'FR': 12,
'FL': 13,
'BR': 14,
'BL': 15,
'DF': 16,
'DR': 17,
'DB': 18,
'DL': 19,
}
CUBELETS_ = {v: k for k, v in CUBELETS.items()}
最後に,便利のためあとで使う INDICES_24 を定義しておく.
INDICES_24 = [i for i in range(24)]
Cube クラス
状態と One-Hot Code
まず,cubelet と slot(枠)という2つの概念を分けて考えなければならない.UFR という slot といえば,上前右の位置にある cubelet を収めるための仮想の枠の意味で使う.UFR という cubelet とえば,揃えた状態で UFR slot にある cubelet (すなわち黄色,赤,緑の三色の cubelet) のことを指す.
状態を特定するために,2通りの方法がある.
1. ある cubelet に対し,どの slot にあるのかを記録する
2. ある slot に対し,どの cubelet を収めているのかを記録する
後で述べる move の定義の便利上,ここで 2 の方式で定義する.中央 cubelets を除いたものの位置と向きを特定すれば良い.
コーナー cubelets は8個x3つの向きで,エージ cubelets は12個x2つの向きである.よって,どの cubelet に対しても,24種類の状態しかない.よって,AIへ渡す One-Hot Code のサイズは 20x24 で良い.一方,計算の都合上,状態の定義は 20x2 のテンサーにする.axis 0 の $i (0 \le i < 20)$ 次元は,前述 CUBELETS という辞書で定義した対応する枠を指す.axis 1 の第 0 次元はどの cubelet かを指し,コーナーの場合は 0 ~ 7,エージの場合は 0 ~ 11 の番号が指定できる.axis 1 の第 1 次元はどの向きを向いているかを指す.コーナーの場合,揃えた状態で上もしくは下を向く sticker (cubelet の面)が上もしくは下を向くとき,値を 0 とする.この状態から観察者からして逆時計回りで 120 度,240 度回した場合,それぞれ 1 と 2 で表す.エージの場合,六面を U = D > F = B > L = R という優先順位を付け,揃えた状態で高優先順位 sticker がまた高優先順位である場合は 0,そうでない場合は 1 とする.
実装すると,次のようになる.
class Cube: """ Rubik's Cube クラス :arg states: 20 x 2 ndarray, (location, phase). Cubelet slot states. For example, `self.states[CUBELETS['UFR']]` stand for the up-front-right corner cubelet slot, and the yellow-red-green corner cubelet should be in it for a solved cube. See also `CUBELETS`. For index (i, j), 0 <= i < 20, 0 <= j < 3. If i 0 <= i < 8, it refers to a corner cubelet slot. For a solved cube, the default value of (i, 0) should be i, and (i, 1) should be 0. (i, 0) can be any value between 0 and 7, indicating which corner cubelet in the slot i. (i, 1) can be 0, 1, or 2, corresponding to 0, 120, or 240 degree the cubelet rotated counterclockwise, where the 0 degree indicate the white or yellow stick of the cubelet facing up or down. Let m, n be (states[i, 0], states[i, 1]), then the one-hot value can be expressed by m + n * 8. If 8 <= i < 20, it refers to an edge cubelet slot. For a solved cube, the default value of (i, 0) should be i - 8, and (i, 1) should be 0. (i, 0) can be any value between 0 and 11, indicating which edge cubelet in the slot i. (i, 1) can be 0 or 1, corresponding to 0 or 180 degree. Let m, n be (states[i, 0], states[i, 1]), then the one-hot value can be expressed by m + n * 12. """ def __init__(self): self.states = np.empty((20, 2), dtype=np.unit8) self.reset() def reset(self): self.states[:, 0] = np.arange(20, dtype=np.uint8) def one_hot(self) -> np.ndarray: states = np.empty(20, dtype=np.uint8) states[:8] = self.states[:8, 0] + self.states[:8, 1] * 8 states[8:] = self.states[8:, 0] + self.states[8:, 1] * 12 one_hot = np.zeros((20, 24), dtype=np.uint8) one_hot[INDICES_24, states] = 1 return one_hot
移動
次に,移動による状態遷移を定義する.
def move(self, action): match action: case "F": # Corner ufr_0, ufr_1 = self.states[CUBELETS['UFR']] ulf_0, ulf_1 = self.states[CUBELETS['ULF']] dfl_0, dfl_1 = self.states[CUBELETS['DFL']] drf_0, drf_1 = self.states[CUBELETS['DRF']] self.states[CUBELETS['UFR']] = ulf_0, (ulf_1 + 2) % 3 self.states[CUBELETS['ULF']] = dfl_0, (dfl_1 + 1) % 3 self.states[CUBELETS['DFL']] = drf_0, (drf_1 + 2) % 3 self.states[CUBELETS['DRF']] = ufr_0, (ufr_1 + 1) % 3 # Edge a, b, c, d = CUBELETS['UF'], CUBELETS['FL'], CUBELETS['DF'], CUBELETS['FR'] _states = self.states[[b, c, d, a]] _states[:, 1] = (_states[:, 1] + 1) % 2 self.states[[a, b, c, d]] = _states case "f": # Corner ufr_0, ufr_1 = self.states[CUBELETS['UFR']] ulf_0, ulf_1 = self.states[CUBELETS['ULF']] dfl_0, dfl_1 = self.states[CUBELETS['DFL']] drf_0, drf_1 = self.states[CUBELETS['DRF']] self.states[CUBELETS['UFR']] = drf_0, (drf_1 + 2) % 3 self.states[CUBELETS['ULF']] = ufr_0, (ufr_1 + 1) % 3 self.states[CUBELETS['DFL']] = ulf_0, (ulf_1 + 2) % 3 self.states[CUBELETS['DRF']] = dfl_0, (dfl_1 + 1) % 3 # Edge a, b, c, d = CUBELETS['UF'], CUBELETS['FL'], CUBELETS['DF'], CUBELETS['FR'] _states = self.states[[d, a, b, c]] _states[:, 1] = (_states[:, 1] + 1) % 2 self.states[[a, b, c, d]] = _states case "B": # Corner urb_0, urb_1 = self.states[CUBELETS['URB']] dbr_0, dbr_1 = self.states[CUBELETS['DBR']] dlb_0, dlb_1 = self.states[CUBELETS['DLB']] ubl_0, ubl_1 = self.states[CUBELETS['UBL']] self.states[CUBELETS['URB']] = dbr_0, (dbr_1 + 1) % 3 self.states[CUBELETS['DBR']] = dlb_0, (dlb_1 + 2) % 3 self.states[CUBELETS['DLB']] = ubl_0, (ubl_1 + 1) % 3 self.states[CUBELETS['UBL']] = urb_0, (urb_1 + 2) % 3 # Edge a, b, c, d = CUBELETS['UB'], CUBELETS['BR'], CUBELETS['DB'], CUBELETS['BL'] _states = self.states[[b, c, d, a]] _states[:, 1] = (_states[:, 1] + 1) % 2 self.states[[a, b, c, d]] = _states case "b": # Corner urb_0, urb_1 = self.states[CUBELETS['URB']] dbr_0, dbr_1 = self.states[CUBELETS['DBR']] dlb_0, dlb_1 = self.states[CUBELETS['DLB']] ubl_0, ubl_1 = self.states[CUBELETS['UBL']] self.states[CUBELETS['URB']] = ubl_0, (ubl_1 + 1) % 3 self.states[CUBELETS['DBR']] = urb_0, (urb_1 + 2) % 3 self.states[CUBELETS['DLB']] = dbr_0, (dbr_1 + 1) % 3 self.states[CUBELETS['UBL']] = dlb_0, (dlb_1 + 2) % 3 # Edge a, b, c, d = CUBELETS['UB'], CUBELETS['BR'], CUBELETS['DB'], CUBELETS['BL'] _states = self.states[[d, a, b, c]] _states[:, 1] = (_states[:, 1] + 1) % 2 self.states[[a, b, c, d]] = _states case "L": # Corner ulf_0, ulf_1 = self.states[CUBELETS['ULF']] ubl_0, ubl_1 = self.states[CUBELETS['UBL']] dlb_0, dlb_1 = self.states[CUBELETS['DLB']] dfl_0, dfl_1 = self.states[CUBELETS['DFL']] self.states[CUBELETS['ULF']] = ubl_0, (ubl_1 + 2) % 3 self.states[CUBELETS['UBL']] = dlb_0, (dlb_1 + 1) % 3 self.states[CUBELETS['DLB']] = dfl_0, (dfl_1 + 2) % 3 self.states[CUBELETS['DFL']] = ulf_0, (ulf_1 + 1) % 3 # Edge a, b, c, d = CUBELETS['UL'], CUBELETS['BL'], CUBELETS['DL'], CUBELETS['FL'] self.states[[a, b, c, d]] = self.states[[b, c, d, a]] case "l": # Corner ulf_0, ulf_1 = self.states[CUBELETS['ULF']] ubl_0, ubl_1 = self.states[CUBELETS['UBL']] dlb_0, dlb_1 = self.states[CUBELETS['DLB']] dfl_0, dfl_1 = self.states[CUBELETS['DFL']] self.states[CUBELETS['ULF']] = dfl_0, (dfl_1 + 2) % 3 self.states[CUBELETS['UBL']] = ulf_0, (ulf_1 + 1) % 3 self.states[CUBELETS['DLB']] = ubl_0, (ubl_1 + 2) % 3 self.states[CUBELETS['DFL']] = dlb_0, (dlb_1 + 1) % 3 # Edge a, b, c, d = CUBELETS['UL'], CUBELETS['BL'], CUBELETS['DL'], CUBELETS['FL'] self.states[[a, b, c, d]] = self.states[[d, a, b, c]] case "R": # Corner urb_0, urb_1 = self.states[CUBELETS['URB']] ufr_0, ufr_1 = self.states[CUBELETS['UFR']] drf_0, drf_1 = self.states[CUBELETS['DRF']] dbr_0, dbr_1 = self.states[CUBELETS['DBR']] self.states[CUBELETS['URB']] = ufr_0, (ufr_1 + 2) % 3 self.states[CUBELETS['UFR']] = drf_0, (drf_1 + 1) % 3 self.states[CUBELETS['DRF']] = dbr_0, (dbr_1 + 2) % 3 self.states[CUBELETS['DBR']] = urb_0, (urb_1 + 1) % 3 # Edge a, b, c, d = CUBELETS['UR'], CUBELETS['FR'], CUBELETS['DR'], CUBELETS['BR'] self.states[[a, b, c, d]] = self.states[[b, c, d, a]] case "r": # Corner urb_0, urb_1 = self.states[CUBELETS['URB']] ufr_0, ufr_1 = self.states[CUBELETS['UFR']] drf_0, drf_1 = self.states[CUBELETS['DRF']] dbr_0, dbr_1 = self.states[CUBELETS['DBR']] self.states[CUBELETS['URB']] = dbr_0, (dbr_1 + 2) % 3 self.states[CUBELETS['UFR']] = urb_0, (urb_1 + 1) % 3 self.states[CUBELETS['DRF']] = ufr_0, (ufr_1 + 2) % 3 self.states[CUBELETS['DBR']] = drf_0, (drf_1 + 1) % 3 # Edge a, b, c, d = CUBELETS['UR'], CUBELETS['FR'], CUBELETS['DR'], CUBELETS['BR'] self.states[[a, b, c, d]] = self.states[[d, a, b, c]] case "U": # Corner a, b, c, d = CUBELETS['URB'], CUBELETS['UBL'], CUBELETS['ULF'], CUBELETS['UFR'] self.states[[a, b, c, d]] = self.states[[b, c, d, a]] # Edge a, b, c, d = CUBELETS['UB'], CUBELETS['UL'], CUBELETS['UF'], CUBELETS['UR'] self.states[[a, b, c, d]] = self.states[[b, c, d, a]] case "u": # Corner a, b, c, d = CUBELETS['URB'], CUBELETS['UBL'], CUBELETS['ULF'], CUBELETS['UFR'] self.states[[a, b, c, d]] = self.states[[d, a, b, c]] # Edge a, b, c, d = CUBELETS['UB'], CUBELETS['UL'], CUBELETS['UF'], CUBELETS['UR'] self.states[[a, b, c, d]] = self.states[[d, a, b, c]] case "D": # Corner a, b, c, d = CUBELETS['DRF'], CUBELETS['DFL'], CUBELETS['DLB'], CUBELETS['DBR'] self.states[[a, b, c, d]] = self.states[[b, c, d, a]] # Edge a, b, c, d = CUBELETS['DF'], CUBELETS['DL'], CUBELETS['DB'], CUBELETS['DR'] self.states[[a, b, c, d]] = self.states[[b, c, d, a]] case "d": # Corner a, b, c, d = CUBELETS['DRF'], CUBELETS['DFL'], CUBELETS['DLB'], CUBELETS['DBR'] self.states[[a, b, c, d]] = self.states[[d, a, b, c]] # Edge a, b, c, d = CUBELETS['DF'], CUBELETS['DL'], CUBELETS['DB'], CUBELETS['DR'] self.states[[a, b, c, d]] = self.states[[d, a, b, c]] case _: raise ValueError("ACTION UNKNOWN")
次の文で,Unity でルービックキューブを実装し,ここで定義している Cube クラスの正当性を確認するために目視でユニットテストを行う.
AI でルービックキューブを揃う(導入)
概要
ルービックキューブの揃え方として,LBL や CFOP からはじめ,多種多様の方法が知られている.これらの方法と群論の知識を前提にアルゴリズムを作ってルービックキューブを解くことは簡単であるが,一方,ルービックキューブの知識を使わずにエンジニアリングの知識のみで解くのは極めて難しく,未だに完全に解決されていない.
3階ルービックキューブの状態空間は4325京2003兆超え非常に大きいため,単純に強化学習の Q-Learning を適用しても収束することはほぼ不可能である.2018 年の論文「人類知識なしでルービックキューブを揃う」(以下,論文と略す.文末参照)で始めて 15 ステップ以上スクランブルしたルービックキューブを9割以上の確率で解いた.
目標
- 論文で導入した DeepCube を実装する.
- Unity で DeepCube を可視化する.
このブログに関わるテーマ
このブログに関わらないテーマ
このブログで想定したプログラミング基礎
AI 部分:Python, NumPy, Tensorflow
可視化部分:Unity, C#
なお,ある部分の基礎知識がなくても,その他の部分への理解は差し支えない.
目次
- Cube クラスを実装する](https://www.itd-blog.jp/entry/2023/11/30/125909)
- Unity でユニットテストを兼ねて可視化する
- ADI を実装する
- (順次更新中......)
参考文献
Stephen McAleer, Forest Agostinelli, Alexander Shmakov, and Pierre Baldi, Solving the Rubik's Cube Without Human Knowledge, arXiv:1805.07470, 2018
【AI アイデア】自動レジ(飲食店向)
バックグラウンド・モチーフ
自動レジAIは、飲食関連の店を中心とした商品の自動検出およびレジ打ちを行うAIサービスのことである。 パンやうどんのトッピング等セルフサービスで商品を取り、商品にバーコードが付いていないため、店員さんにより商品を認知してレジを打つ。 実際、このような店では、期間限定の商品も多く、新人の店員さんが正しく商品を覚えるために三ヶ月以上の研修期間が必要だと言われている。 人手不足の影響も踏まえて、経験者の店員一人が離職しただけでも、店の運営に重大な影響を与えかねない。 そこで、もしバーコードの代わりに商品の外観だけで商品の品目がわかるようなプログラムがあれば、上述の問題も回避できるだろう。 この発想から作られたのは自動レンジAIである。
技術ポイント:オブジェクト検知AIとの違い
オブジェクト検知AIは多分最も広く認識されたビジュアルAIである。 一枚の写真を与えれば、写真内の人や動物、車、テレビ、ペットボトル等の身近いオブジェクトを区別し、さらにボンディングボックスト呼ばれる四角形でその位置をマークすることができる。 残念ながら、このような汎用型AIは今回の目的に達成できない。 一つの主な理由は、粒度が足りないことである。オブジェクト検知AIはパンとその他のオブジェクトと区別が付くものの、どの種類のパンであるか区別が付かない。 再度機械学習を行うことで、パン同士の区別を付けるようにすることもできるが、しかし、期間限定の商品も考慮すれば、その都度機械学習を行うことはコストと収益が釣り合わない。 粒度の細かい認識と期間限定があっても再び機械学習不要という条件をクリアするためには、Embedding - Clustering アプローチのAIが必要である。 このようなAIは、オブジェクトそのものを認識することではなく、与えられたオブジェクトに対し、機械の言語(Embedding)でそのオブジェクトの外見を説明する。 オブジェクトが似ているほど言葉も近く、異なるオブジェクトに対し異なった言葉で説明できる(Clustering)ように機械学習を行う。
【第17回】簡易体積算出アルゴリズム 入出力に関する検討3
今回の概要
- 精度が低下した理由について
- 精度向上のための考察
作業内容
- 精度が落ちる理由について
いままではデータをシャッフルして学習していましたが、今回の場合はシャッフルしないほうが汎化性能を高めるためには重要であると確信しました。
image argumentationを行ってもCNNで共通の特徴が抽出される可能性が高いため、これをシャッフルした場合には学習データと検証データに似たデータが有るために汎化性能を測る上では良くありません。
そこで、シャッフルせずにVGG19に3層の全結合層で試した結果、loss = 5.122568597793579となり、検証データの精度も全く上がりませんでした。
また、iPhoneで撮影した写真でテストしてみるとすべて0.045となり、全く当てにならない値になってしまいました。
現時点ですと、以前までの手法のほうが良い結果が出たのではないかと考えられます。
そこで、今までの検討で変更した部分をテストデータで試行してみます。
まずは、検討の最初に行った出力変数を体積にするモデルを試してみました。
結果は、この時点から精度が悪化していることが分かりました。
相対誤差の範囲が30~120%と修正前のものより分散と最悪値が大きくなっているため、これらの変更は良いものとは言えません。
しかし、今までのデータも今回のデータも同じ家電に対しては似た値を示しています。
結果は以下に示します。
結果
results20200606.txt loss: 0.183223158121109 , acc: 0.0 [[0.40894938] [0.40193376] [0.341351 ] [0.13559507] [0.18933596] [0.33866742] [0.33038357]]
精度向上のための考察
これらの結果から、VGG19での学習では似ている家電の分別は可能なものの、回帰値として体積を算出する場合には大きな誤差を伴うということがわかってきました。
つまり、VGG19では家電の種類の分別は可能であることは確認できましたが、回帰値として体積などの値は抽出が難しいことが言えます。
そこで、これに続く案としてとしては、機能を大幅に制限する案となりますが、家電の種類毎に分類したあとに、その家電に対するモデルを設けて体積を判別させる方法があります。
まずはSSDなどで家電の画像を抽出して、切り取った画像から統計モデルで学習し、体積を回帰値として算出するものになります。
もう1つの案は、深度推定アルゴリズムとSSDを併用した体積推定アルゴリズムです。
深度推定アルゴリズムにより空間的な解釈を見出し、SSDで標的家電を抽出して、それらのデータを全結合やSVMなどの統計モデルで体積値と対応付ける手法です。
どちらも複雑なアルゴリズムであるため、前もっての見極めが必要と考えられます。
次回は、そのための考察を行います。
【第16回】簡易体積算出アルゴリズム 入出力に関する検討2(結果)
今回の概要
- 入力に画像サイズの情報を付与した結果について
- ニューラルネットの評価
- アンサンブル学習の準備
入力に画像サイズの情報の付与した結果について
探索項目と構築ネットワークが大きく時間がかかりましたので、途中までの結果を示します。
結果は以下の通りで、前回と同等の結果となりました。
以前と比較すると、one-hot vecにすることによりaccuracyは飛躍的に良くなり、交差検証でも検証データの精度がほぼ全て"1"になるほどです。
ここに入力情報を加えることで0.002から0.004に落ちましたが、これは入力の多様性による汎化性能においては大きな差がないと考えられますので、今はこの両手法を保持しておきたいと思います。
Current best loss is 0.004090719259034131 with parameters: {'hidden_neurons1': 3600.0, 'hidden_neurons2': 1300.0, 'drop': 0.29899967194322563, 'lr': 0.0005058194828119688, 'activation': 'sigmoid'}.
その上で、このモデルについて評価を行いました。
評価に使用したソースコードを以下に示します(VGG19_evaluation3.py)。
python:VGG19_evaluation3.py
import keras
import numpy as np
import matplotlib.pyplot as plt
import os
import re
import csv
import tensorflow as tf
from sklearn.model_selection import KFold
from keras.utils import np_utils
from keras.models import Sequential, Model
from keras.layers import Input, Conv2D, MaxPooling2D, Dense, Dropout, Activation, Flatten
from keras.preprocessing.image import array_to_img, img_to_array, load_img
from keras.applications import ResNet50, VGG19
from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras import optimizers
from sklearn.model_selection import train_test_split
from keras.backend import tensorflow_backend
config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))
session = tf.Session(config=config)
tensorflow_backend.set_session(session)
hidden_neurons1 = 3600
hidden_neurons2 = 1300
out_neurons = 200
batch_size = 8
nb_epochs = 1000
drop_num = 0.29899967194322563
lr = 0.0005058194828119688
X = []
X_add = []
Y_pre = []
Y = np.zeros((1000,200))
with open('X_data.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('X_add_data.csv') as f:
for row in csv.reader(f):
X_add.append(row)
with open('Y_data.csv') as f:
for row in csv.reader(f):
Y_pre.append(row)
X = np.array(X)
X_add = np.array(X_add)
Y_pre = np.array(Y_pre)
for i,num in enumerate(Y_pre):
j = float(num[0]) / 0.005 - 1
Y[i,int(j)]=1
X = np.concatenate([X, X_add], 1)
print(X.shape)
print(Y.shape)
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=42, shuffle=True)
print(x_train.shape, x_test.shape, y_train.shape, y_test.shape)
es = EarlyStopping(monitor='val_loss', min_delta=0, patience=30, verbose=1, mode='auto')
modelCheckpoint = ModelCheckpoint(filepath = 'Checkpoint.h5',
monitor='val_loss',
verbose=1,
save_best_only=True,
save_weights_only=False,
mode='min',
period=1)
model = Sequential()
model.add(Dense(hidden_neurons1, input_shape= (25090, )))
model.add(Activation("sigmoid"))
model.add(Dropout(drop_num))
model.add(Dense(hidden_neurons2))
model.add(Activation("sigmoid"))
model.add(Dropout(drop_num))
model.add(Dense(out_neurons))
model.add(Activation("softmax"))
model.compile(loss='categorical_crossentropy', metrics = ['accuracy'], optimizer=optimizers.Adam(lr=lr))
#model.summary()
result = model.fit(x_train,
y_train,
batch_size=batch_size,
epochs=nb_epochs,
verbose=1,
validation_data=(x_test, y_test),
shuffle=True,
callbacks=[modelCheckpoint,es])
model.load_weights('Checkpoint.h5')
scores = model.evaluate(X, Y, verbose=0)
print("%s: %.2f%%" % (model.metrics_names[1], scores[1]*100))
result.history.keys() # ヒストリデータのラベルを見てみる]
ep =len(result.history['acc'])
plt.figure()
plt.plot(range(1, ep+1), result.history['acc'], label="training")
plt.plot(range(1, ep+1), result.history['val_acc'], label="validation")
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.savefig('acc_test.png')
plt.figure()
plt.plot(range(1, ep+1), result.history['loss'], label="training")
plt.plot(range(1, ep+1), result.history['val_loss'], label="validation")
plt.xlabel('Epochs')
plt.ylabel('loss')
plt.legend()
plt.savefig('loss_test.png')
#可視化
model.summary()
# モデル評価
print("Load test dataset")
X = []
X_add = []
Y_pre = []
Y = np.zeros((1000,200))
with open('X_data_test.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('X_add_data_test.csv') as f:
for row in csv.reader(f):
X_add.append(row)
with open('Y_data_test.csv') as f:
for row in csv.reader(f):
Y_pre.append(row)
X = np.array(X)
X_add = np.array(X_add)
Y_pre = np.array(Y_pre)
for i,num in enumerate(Y_pre):
j = float(num[0]) / 0.005 - 1
Y[i,int(j)]=1
X = np.concatenate([X, X_add], 1)
print(X.shape)
print(Y.shape)
model.load_weights('Checkpoint.h5')
loss, accuracy = model.evaluate(X, Y, verbose=0)
print('loss: '+str(loss)+' , acc: '+str(accuracy))
pred = model.predict(X_test, batch_size=1, verbose=0)
print(pred)
print("end of script")
学習の結果は、学習データ、検証データ共に"1"と非常に高い精度で推定することができました。
しかし、テストデータは精度が"0"となっており、全く推定できていませんでした。
もう一度ソースコードと考え方を見直して、精度が落ちる原因を探りたいと思います。
まずは、これまでシャッフルしていたデータをシャッフルせずに最適化問題を解いてみます。
アンサンブル学習の準備
前回作成したプログラムを修正して学習、検証しました。
まずはrandom forestについて修正したソースコードを以下に示します(RF_train3.py)。
python:RF_train3.py
import numpy as np
import matplotlib.pyplot as plt
import os
import re
import csv
import gc
from sklearn.model_selection import KFold
import optuna
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
from pandas.plotting import scatter_matrix
X = []
X_add = []
Y_pre = []
Y = np.zeros((1000,200))
fold_num = 5
with open('X_data.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('X_add_data.csv') as f:
for row in csv.reader(f):
X_add.append(row)
with open('Y_data.csv') as f:
for row in csv.reader(f):
Y_pre.append(row)
X = np.array(X)
X_add = np.array(X_add)
Y_pre = np.array(Y_pre)
for i,num in enumerate(Y_pre):
j = float(num[0]) / 0.005 - 1
Y[i,int(j)]=1
X = np.concatenate([X, X_add], 1)
print(X.shape)
print(Y.shape)
kf = KFold(n_splits=fold_num, shuffle=True)
def objective(trial):
#最適化するパラメータの設定
#中間層1のユニット数
max_depth = int(trial.suggest_discrete_uniform("max_depth", 5, 500, 10))
n_estimators = int(trial.suggest_discrete_uniform("n_estimators", 5, 500, 10))
max_leaf_nodes = int(trial.suggest_discrete_uniform("max_leaf_nodes", 4, 64, 4))
min_samples_split = trial.suggest_int("min_samples_split", 8, 16)
criterion = trial.suggest_categorical("criterion", ["gini", "entropy"])
acc_all = []
for train, test in kf.split(X):
random_forest = RandomForestClassifier(verbose=True,
min_samples_split = min_samples_split,
max_leaf_nodes = max_leaf_nodes,
criterion = criterion,
max_depth = max_depth,
n_estimators = n_estimators,
random_state = 42)
random_forest.fit(X[train], Y[train])
# 予測値算出
y_pred = random_forest.predict(X[test])
#モデルを作成する段階でのモデルの識別精度
trainaccuracy_random_forest = random_forest.score(X[train], Y[train])
print('TrainAccuracy: {}'.format(trainaccuracy_random_forest))
#作成したモデルに学習に使用していない評価用のデータセットを入力し精度を確認
accuracy_random_forest = accuracy_score(Y[test], y_pred)
print('Accuracy: {}'.format(accuracy_random_forest))
acc_all.append(accuracy_random_forest)
del random_forest, y_pred, trainaccuracy_random_forest, accuracy_random_forest
print(acc_all)
return 1.0 - np.mean(acc_all)
study = optuna.create_study()
study.optimize(objective, n_trials=50)
print("End!!")
print("All trials")
print(study.trials)
print("Best Parameters")
print(study.best_params)
print("Best Value")
print(study.best_value)
print("end of script")
trial10までを試しに実行すると、シャッフルしていても精度が0.7と比較的低い結果となりました。
ニューラルネットと決定木では得意とする入力・出力形式が異なるため、これを考慮しなければ良い精度は得られないと考えられます。
もともと、この入力はニューラルネットの中間層を抜き取ったものですので、そのことも影響していると考えられます。
次回の予定
次回は、精度が低下の理由を調査します。
【第15回】自然言語処理の基礎調査
Word2Vec
自然言語処理では単語を扱いやすくするため、単語をベクトルで表現することが多いです。
最も代表的な手法として、ニューラルネットワークを用いた手法であるWords2Vecが知られています。
Word2VecにはCBoWとskip-gramの2種類のモデルがあります。
どちらも入力層、隠れ層、出力層で構成される3層の全結合のニューラルネットワークです。
その2種類のモデルの学習のためには、文章を単語毎に区切り、重複のないように整理した「ボキャブラリ」と呼ばれる形式に変換したものを利用します。
以下にボキャブラリへの変換の例を示します。
- 変換前
I want to eat apple. I like apple.
- 変換後
{apple eat I like to want .}
順序が関係なくなっていることがわかります。
このボキャブラリを単語毎にワンホットで表すことで、モデルの入力とすることができます。
CBoW
CBoWは、入力を複数の単語のワンホットベクトルとします。
この入力単語の個数を窓サイズといいます。
出力は、ボキャブラリ内の単語が、入力された複数の単語の間に入る確率です。
学習のためには、元の文章からある単語を抜き取り、その単語周辺の単語を入力として、抜き取った単語の確率が1となるように学習すれば良いです。
そのように学習させたモデルの隠れ層は入力した単語の特徴ベクトルとみなせるため、この隠れ層のベクトルを用いれば、単語をベクトルで表すことができます。
skip-gram
skip-gramは、CBoWの出力と入力を逆にしたモデルです。
一つの単語から、周辺に出てくる単語の確率を出力として学習させます。
skip-gramはCBoWよりも少ない学習データでも精度が出るとされています。
実際にWords2Vecはマーケティングに利用されています。
単語をベクトル化し、ベクトルのコサイン類似度を計算することによって顧客が購入した商品と類似する商品をサジェストしたり、
これまで顧客の購入した商品のベクトルを足し合わせることによって顧客の趣向情報をベクトル化したりできます。
Doc2Vec
Doc2Vecは単語よりも大きな集合をベクトル化するモデルです。
Doc2Vecは文書要約に用いられてきました。
文書を要約する上で重要な仮定として、複数の文章ベクトルによって作られた領域はその中に入るベクトルを内包するという点があります。
以下にリクルート式 自然言語処理技術の適応事例紹介から引用したイメージ図を示します。

まとめ
単語や文をベクトル化することで、自然言語に対して数学的アプローチができるようになること、ニューラルネットワークの入力として用いることができることが分かりました。
この「言語のベクトル化」を、次に行うことに役立てたいと思います。
参考文献
- https://qiita.com/g-k/items/69afa87c73654af49d36
- https://qiita.com/Hironsan/items/11b388575a058dc8a46a
- https://deepage.net/machine_learning/2017/01/08/doc2vec.html
- https://www.slideshare.net/recruitcojp/ss-56150629
【第15回】簡易体積算出アルゴリズム 入出力に関する検討2
今回の概要
- 出力値を体積に変更した結果について
- 回帰値→one-hot vecに変更した結果について
- 入力に画像サイズの情報の付与(プログラム)
- アンサンブル学習の準備
出力値を体積に変更
まず、今回は高負荷なハイパーパラメータを設定しているため、メモリ不足による停止を防ぐためにソースコードの最適化を行いました。
ソースコードをVGG19_train1.pyに示します。
結果として、前回までより分散が少なく、平均loss = 0.004690358456224204と前回の検討より1桁低い損失を示しました。
出力値を体積にしたことにより、大きく精度が向上したと考えられます。
python:VGG19_train1.py
import keras
import numpy as np
import matplotlib.pyplot as plt
import os
import re
import csv
import gc
from sklearn.model_selection import KFold
import keras.backend as K
import optuna
from keras.utils import np_utils
from keras.models import Sequential, Model
from keras.layers import Input, Conv2D, MaxPooling2D, Dense, Dropout, Activation, Flatten
from keras.preprocessing.image import array_to_img, img_to_array, load_img
from keras.applications import ResNet50, VGG19
from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras import optimizers
from sklearn.model_selection import train_test_split
from keras.backend import clear_session
import tensorflow as tf
batch_size = 8
nb_epochs = 1000
fold_num = 5
X = []
Y = []
with open('X_data.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('Y_data.csv') as f:
for row in csv.reader(f):
Y.append(row)
X = np.array(X)
Y = np.array(Y)
print(X.shape)
print(Y.shape)
kf = KFold(n_splits=fold_num, shuffle=True)
def objective(trial):
# gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
# sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
# K.set_session(sess)
#最適化するパラメータの設定
#中間層1のユニット数
hidden_neurons1 = int(trial.suggest_discrete_uniform("hidden_neurons1", 100, 5000, 100))
hidden_neurons2 = int(trial.suggest_discrete_uniform("hidden_neurons2", 100, 5000, 100))
# drop = trial.suggest_uniform("drop", 0.1, 0.5)
out_neurons = 1
#optimizer
lr = trial.suggest_loguniform("lr", 1e-1, 1e-4)
activation = trial.suggest_categorical("activation", ["linear", "sigmoid", "relu"])
loss_all = []
for train, test in kf.split(X):
es = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1, mode='auto')
model = Sequential()
model.add(Dense(hidden_neurons1, input_shape= (25088, )))
model.add(Activation(activation))
model.add(Dropout(0.5))
model.add(Dense(hidden_neurons2))
model.add(Activation(activation))
model.add(Dropout(0.5))
model.add(Dense(out_neurons))
model.add(Activation("linear"))
model.compile(loss="mean_squared_error", metrics = ['accuracy'], optimizer=optimizers.Adam(lr=lr))
history = model.fit(X[train],
Y[train],
batch_size=batch_size,
epochs=nb_epochs,
verbose=1,
validation_data=(X[test], Y[test]),
shuffle=True,
callbacks=[es])
#検証用データに対する正答率が最大となるハイパーパラメータを求める
loss = history.history["val_loss"]
loss.sort()
loss_all.append(loss[0])
print(" ")
print("Loss: "+str(loss[0]))
print(" ")
clear_session()
del model, history
gc.collect()
print(loss_all)
print("%.2f%% (+/- %.2f%%)" % (np.mean(loss_all), np.std(loss_all)))
return np.mean(loss_all)
study = optuna.create_study()
study.optimize(objective, n_trials=50)
print("End!!")
print("All trials")
print(study.trials)
print("Best Parameters")
print(study.best_params)
print("Best Value")
print(study.best_value)
print("end of script")
回帰値→one-hot vecに変更
次に、出力にone-hot vecを導入して出力層を200とし、カテゴリカルに判別しました。
ソースコードをVGG19_train2.pyに示します。
その結果はloss = 0.0020341352004761572と上記の半分の損失のため、向上していることが分かります。
この変化により、精度が向上することが考えられます。
python:VGG19_train2.py
import keras
import numpy as np
import matplotlib.pyplot as plt
import os
import re
import csv
import gc
from sklearn.model_selection import KFold
import keras.backend as K
import optuna
from keras.utils import np_utils
from keras.models import Sequential, Model
from keras.layers import Input, Conv2D, MaxPooling2D, Dense, Dropout, Activation, Flatten
from keras.preprocessing.image import array_to_img, img_to_array, load_img
from keras.applications import ResNet50, VGG19
from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras import optimizers
from sklearn.model_selection import train_test_split
from keras.backend import clear_session
import tensorflow as tf
batch_size = 8
nb_epochs = 1000
fold_num = 5
X = []
Y_pre = []
Y = np.zeros((1000,200))
with open('X_data.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('Y_data.csv') as f:
for row in csv.reader(f):
Y_pre.append(row)
X = np.array(X)
Y_pre = np.array(Y_pre)
for i,num in enumerate(Y_pre):
j = float(num[0]) / 0.005 - 1
Y[i,int(j)]=1
print(X.shape)
print(Y.shape)
kf = KFold(n_splits=fold_num, shuffle=True)
def objective(trial):
# gpu_options = tf.GPUOptions(per_process_gpu_memory_fraction=0.8)
# sess = tf.Session(config=tf.ConfigProto(gpu_options=gpu_options))
# K.set_session(sess)
#最適化するパラメータの設定
#中間層1のユニット数
hidden_neurons1 = int(trial.suggest_discrete_uniform("hidden_neurons1", 100, 5000, 100))
hidden_neurons2 = int(trial.suggest_discrete_uniform("hidden_neurons2", 100, 5000, 100))
# drop = trial.suggest_uniform("drop", 0.1, 0.5)
out_neurons = 200
#optimizer
lr = trial.suggest_loguniform("lr", 1e-1, 1e-4)
activation = trial.suggest_categorical("activation", ["linear", "sigmoid", "relu"])
loss_all = []
for train, test in kf.split(X):
es = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1, mode='auto')
model = Sequential()
model.add(Dense(hidden_neurons1, input_shape= (25088, )))
model.add(Activation(activation))
model.add(Dropout(0.5))
model.add(Dense(hidden_neurons2))
model.add(Activation(activation))
model.add(Dropout(0.5))
model.add(Dense(out_neurons))
model.add(Activation("softmax"))
model.compile(loss='categorical_crossentropy', metrics = ['accuracy'], optimizer=optimizers.Adam(lr=lr))
history = model.fit(X[train],
Y[train],
batch_size=batch_size,
epochs=nb_epochs,
verbose=1,
validation_data=(X[test], Y[test]),
shuffle=True,
callbacks=[es])
#検証用データに対する正答率が最大となるハイパーパラメータを求める
loss = history.history["val_loss"]
loss.sort()
loss_all.append(loss[0])
print(" ")
print("Loss: "+str(loss[0]))
print(" ")
clear_session()
del model, history
gc.collect()
print(loss_all)
print("%.2f%% (+/- %.2f%%)" % (np.mean(loss_all), np.std(loss_all)))
return np.mean(loss_all)
study = optuna.create_study()
study.optimize(objective, n_trials=50)
print("End!!")
print("All trials")
print(study.trials)
print("Best Parameters")
print(study.best_params)
print("Best Value")
print(study.best_value)
print("end of script")
入力に画像サイズの情報の付与
次に、入力を改良します。
入力(VGG19の出力)に画像のサイズを付与します。
具体的には、224/画像のheight or weightを入力値として2つ付与して、学習させます。
まずは、その情報を作成しました。
ソースコードをexp_trans.pyに示します。
また,学習プログラムをVGG19_train3.pyに示します。
学習及び評価結果は、次回の記事に記載します。
python:exp_trans.py
import numpy as np
import os
import re
import csv
import cv2
def list_pictures(directory, ext='jpg|jpeg|bmp|png|ppm'):
return [os.path.join(root, f)
for root, _, files in os.walk(directory) for f in files
if re.match(r'([\w]+\.(?:' + ext + '))', f.lower())]
X = []
picture_hw = np.zeros((1000, 2))
path = './test_dataset/'
base_hw = 224
count = 0
path0 = './dataset/'
for picture in list_pictures(path):
picture_name=float(picture[picture.find(path)+len(path):picture.find('_0_')])
im = cv2.imread(path0+str(int(picture_name))+".jpg")
h, w, c = im.shape
picture_hw[count, 0] = base_hw/h
picture_hw[count, 1] = base_hw/w
count = count + 1
print(len(picture_hw))
X_post = []
with open('X_add_data.csv', 'w', newline="") as f:
writer = csv.writer(f)
writer.writerows(picture_hw)
with open('X_add_data.csv') as f:
for row in csv.reader(f):
X_post.append(row)
X_post = np.array(X_post)
print(X_post.shape)
python:VGG19_train3.py
import keras
import numpy as np
import matplotlib.pyplot as plt
import os
import re
import csv
import gc
from sklearn.model_selection import KFold
import keras.backend as K
import optuna
from keras.utils import np_utils
from keras.models import Sequential, Model
from keras.layers import Input, Conv2D, MaxPooling2D, Dense, Dropout, Activation, Flatten
from keras.preprocessing.image import array_to_img, img_to_array, load_img
from keras.applications import ResNet50, VGG19
from keras.callbacks import ModelCheckpoint, EarlyStopping
from keras import optimizers
from sklearn.model_selection import train_test_split
from keras.backend import clear_session
import tensorflow as tf
batch_size = 8
nb_epochs = 1000
fold_num = 5
X = []
X_add = []
Y_pre = []
Y = np.zeros((1000,200))
with open('X_data.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('X_add_data.csv') as f:
for row in csv.reader(f):
X_add.append(row)
with open('Y_data.csv') as f:
for row in csv.reader(f):
Y_pre.append(row)
X = np.array(X)
X_add = np.array(X_add)
Y_pre = np.array(Y_pre)
for i,num in enumerate(Y_pre):
j = float(num[0]) / 0.005 - 1
Y[i,int(j)]=1
X = np.concatenate([X, X_add], 1)
print(X.shape)
print(Y.shape)
kf = KFold(n_splits=fold_num, shuffle=True)
def objective(trial):
#最適化するパラメータの設定
#中間層1のユニット数
hidden_neurons1 = int(trial.suggest_discrete_uniform("hidden_neurons1", 100, 5000, 100))
hidden_neurons2 = int(trial.suggest_discrete_uniform("hidden_neurons2", 100, 5000, 100))
drop = trial.suggest_uniform("drop", 0.1, 0.5)
out_neurons = 200
#optimizer
lr = trial.suggest_loguniform("lr", 1e-4, 1e-1)
activation = trial.suggest_categorical("activation", ["linear", "sigmoid", "relu"])
loss_all = []
for train, test in kf.split(X):
es = EarlyStopping(monitor='val_loss', min_delta=0, patience=10, verbose=1, mode='auto')
model = Sequential()
model.add(Dense(hidden_neurons1, input_shape= (25090, )))
model.add(Activation(activation))
model.add(Dropout(drop))
model.add(Dense(hidden_neurons2))
model.add(Activation(activation))
model.add(Dropout(drop))
model.add(Dense(out_neurons))
model.add(Activation("softmax"))
model.compile(loss='categorical_crossentropy', metrics = ['accuracy'], optimizer=optimizers.Adam(lr=lr))
history = model.fit(X[train],
Y[train],
batch_size=batch_size,
epochs=nb_epochs,
verbose=1,
validation_data=(X[test], Y[test]),
shuffle=True,
callbacks=[es])
#検証用データに対する正答率が最大となるハイパーパラメータを求める
loss = history.history["val_loss"]
loss.sort()
loss_all.append(loss[0])
print(" ")
print("Loss: "+str(loss[0]))
print(" ")
clear_session()
del model, history
gc.collect()
print(loss_all)
print("%.2f%% (+/- %.2f%%)" % (np.mean(loss_all), np.std(loss_all)))
return np.mean(loss_all)
study = optuna.create_study()
study.optimize(objective, n_trials=50)
print("End!!")
print("All trials")
print(study.trials)
print("Best Parameters")
print(study.best_params)
print("Best Value")
print(study.best_value)
print("end of script")
アンサンブル学習の準備
アンサンブル学習の問題点としては、様々な機械学習アルゴリズムを動かさなくてはならないために、処理が重くなることが挙げられます。
そのため、アンサンブル学習を実装するよりも全結合層の検討等を行い、よりNNに対する検討を行うと同時に、それぞれの機械学習の精度を確認してアンサンブル学習を行う予定です。
機械学習アルゴリズムのラインナップはランダムフォレストとXGBTです。
これらはNNとは異なるアルゴリズムで、非線形的な表現も可能です。
これらを単体で試行するため、学習・検証プログラムをRF_train3.py、XGBT_train3.pyにそれぞれ示します。
python:RF_train3.py
import numpy as np
import matplotlib.pyplot as plt
import os
import re
import csv
import gc
from sklearn.model_selection import KFold
import optuna
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import confusion_matrix
from pandas.plotting import scatter_matrix
X = []
X_add = []
Y_pre = []
Y = np.zeros((1000,200))
with open('X_data.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('X_add_data.csv') as f:
for row in csv.reader(f):
X_add.append(row)
with open('Y_data.csv') as f:
for row in csv.reader(f):
Y_pre.append(row)
X = np.array(X)
X_add = np.array(X_add)
Y_pre = np.array(Y_pre)
for i,num in enumerate(Y_pre):
j = float(num[0]) / 0.005 - 1
Y[i,int(j)]=1
X = np.concatenate([X, X_add], 1)
print(X.shape)
print(Y.shape)
kf = KFold(n_splits=fold_num, shuffle=True)
def objective(trial):
#最適化するパラメータの設定
#中間層1のユニット数
max_depth = int(trial.suggest_discrete_uniform("max_depth", 5, 500, 10))
n_estimators = int(trial.suggest_discrete_uniform("n_estimators", 5, 500, 10))
max_leaf_nodes = int(trial.suggest_discrete_uniform("max_leaf_nodes", 4, 64, 4))
min_samples_split = trial.suggest_int("min_samples_split", 8, 16)
criterion = trial.suggest_categorical("criterion", ["gini", "entropy"])
acc_all = []
for train, test in kf.split(X):
random_forest = RandomForestClassifier(verbose=True,
min_samples_split = min_samples_split,
max_leaf_nodes = max_leaf_nodes,
criterion = criterion,
max_depth=max_depth,
n_estimators=n_estimators,
random_state=42)
random_forest.fit(X[train], Y[train])
# 予測値算出
y_pred = random_forest.predict(X[test])
#モデルを作成する段階でのモデルの識別精度
trainaccuracy_random_forest = random_forest.score(X[train], Y[train])
print('TrainAccuracy: {}'.format(trainaccuracy_random_forest))
#作成したモデルに学習に使用していない評価用のデータセットを入力し精度を確認
accuracy_random_forest = accuracy_score(Y[test], y_pred)
print('Accuracy: {}'.format(accuracy_random_forest))
acc_all.append(accuracy_random_forest)
clear_session()
del random_forest, y_pred, trainaccuracy_random_forest, accuracy_random_forest
gc.collect()
print(acc_all)
return 1.0 - np.mean(loss_all)
study = optuna.create_study()
study.optimize(objective, n_trials=50)
print("End!!")
print("All trials")
print(study.trials)
print("Best Parameters")
print(study.best_params)
print("Best Value")
print(study.best_value)
print("end of script")
import numpy as np
import matplotlib.pyplot as plt
import xgboost as xgb
import os
import re
import csv
import gc
import optuna
from sklearn.model_selection import train_test_split, kFold
from sklearn.metrics import accuracy_score, precision_score
from sklearn.ensemble import RandomForestClassifier
X = []
X_add = []
Y_pre = []
Y = np.zeros((1000,200))
with open('X_data.csv') as f:
for row in csv.reader(f):
X.append(row)
with open('X_add_data.csv') as f:
for row in csv.reader(f):
X_add.append(row)
with open('Y_data.csv') as f:
for row in csv.reader(f):
Y_pre.append(row)
X = np.array(X)
X_add = np.array(X_add)
Y_pre = np.array(Y_pre)
for i,num in enumerate(Y_pre):
j = float(num[0]) / 0.005 - 1
Y[i,int(j)]=1
X = np.concatenate([X, X_add], 1)
print(X.shape)
print(Y.shape)
kf = KFold(n_splits=fold_num, shuffle=True)
def objective(trial):
#最適化するパラメータの設定
#中間層1のユニット数
max_depth = trial.suggest_int('max_depth',1,30)
learning_rate = trial.suggest_uniform('learning_rate',0.0,1)
round_num = trial.suggest_int('round_num',1,30)
acc_all = []
for train, test in kf.split(X):
train = xgb.DMatrix(X[train],label=Y[train])
param = { 'max_depth':max_depth,'learning_rate':learning_rate,'objective':'reg:logistic' }
bst = xgb.train(param,dtrain,num_round)
ans_train = predict(bst,X[train]).round().astype(int)
print('train score :',accuracy_score(ans_train.round(),Y[train]))
ans_test = predict(bst,X[test]).round().astype(int)
print('test score :',accuracy_score(ans_test.round(),Y[test]))
acc_all.append(accuracy_score(ans_test.round(),Y[test]))
clear_session()
del train, param, bst, ans_test, ans_train
gc.collect()
print(acc_all)
return 1 - np.mean(loss_all)
study = optuna.create_study()
study.optimize(objective, n_trials=50)
print("End!!")
print("All trials")
print(study.trials)
print("Best Parameters")
print(study.best_params)
print("Best Value")
print(study.best_value)
print("end of script")
次回の予定
- 次回は、アンサンブル学習を行います。
