コードとか書けないけどとりあえず機械学習がやりたい

## はじめに

ライトノベルみたいなタイトルになってしまいました。
教師あり学習の方法について知識がある方へ向けた、とりあえず機械学習を動かしてみよう、というプログラムです。
~~使いこなせる方はそもそも自身で機械学習の実装ができそうなので、需要は皆無な気がしています。~~

ここでは、MNISTとCIFAR-10を用いた画像の分類を行います。

MNISTは手書きの数字を集めたデータセットです。
MNISTの例（Wikipediaより）
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566695/ce39233e-fade-51d2-de18-fde1ce76eac1.png">

CIFAR-10は乗り物と動物を集めたデータセットです
CIFAR-10の例（https://www.cs.toronto.edu/~kriz/cifar.html より）
<img width="400" alt="スクリーンショット (889).pngf" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3566695/16b50ce1-11ea-5281-2b30-71572b6c5424.png">


## ソースコード
~~~Python
print("準備中\n")

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import torch.utils.data
import torchvision.datasets
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torchvision.utils as utils
import numpy as np

if torch.cuda.is_available():
    device = "cuda"
else:
    device = "cpu"
    
def dataset_select():
    print("データセットを選んでください")
    dataset_num = int(input("0:MNIST, 1:CIFAR10\n"))
    
    if dataset_num == 0:
        print("MNIST")
    else:
        print("CIFAR10")
    
    return dataset_num

def load_data(dataset_num):
    #MNISTの読み込み
    if dataset_num == 0:
        train_data = torchvision.datasets.MNIST(root = "\data",
                                                download = True,
                                                train = True,
                                                transform = transforms.ToTensor())
        test_data = torchvision.datasets.MNIST(root = "\data",
                                               download = True,
                                               train = False,
                                               transform = transforms.ToTensor())
    
        train_loader = torch.utils.data.DataLoader(dataset = train_data,
                                                   batch_size = 64,
                                                   shuffle = True)
        test_loader = torch.utils.data.DataLoader(dataset = test_data,
                                                  batch_size = 64,
                                                  shuffle = False)
        return train_loader, test_loader
    
    else:
        transform = transforms.Compose([transforms.Grayscale(), transforms.ToTensor()])

        train_data = torchvision.datasets.CIFAR10(root='\data', 
                                                train=True, 
                                                download=True, 
                                                transform=transform)
        test_data = torchvision.datasets.CIFAR10(root='\data',
                                               train=False, 
                                               download=True, 
                                               transform=transform)
        
        train_loader = torch.utils.data.DataLoader(train_data, 
                                                  batch_size=64, 
                                                  shuffle=True,
                                                  num_workers=1)
        test_loader = torch.utils.data.DataLoader(test_data, 
                                                 batch_size=64, 
                                                 shuffle=True,
                                                 num_workers=1)
        
        return train_loader, test_loader

    
def setting_MLP(dataset_num):
    layer_num = int(input("隠れ層は何層ですか（半角数字, 1以上）\n"))
    
    #先頭に入力数、returnの直前に出力ラベル数を追加
    if dataset_num == 0:
        layer_info = [28*28]
    else:
        layer_info = [32*32]
    
    c= 1
    while c <= layer_num:
        z = int(input(f"隠れ層{c}の頂点数を入力してください\n"))
        layer_info.append(z)
        c += 1
                
    layer_info.append(10)
    return layer_info

def calc_character_size(dataset_num, kernel_size):
    if dataset_num == 0:
        image_node = 28
    else:
        image_node = 32
    
    x = (image_node - kernel_size) + 1
    x = x // 2
    x = (x - kernel_size) + 1
    x = x // 2
    x = x * x
    
    return int(x)
    
def setting_CNN(dataset_num):
    c1 = int(input("畳み込み1層目の出力チャネル数を入力してください\n"))
    c2 = int(input("畳み込み2層目の出力チャネル数を入力してください\n"))
    
    kernel_size = int(input("カーネルサイズを入力してください（ 1 ～ 9 ）\n"))

    character_size = calc_character_size(dataset_num, kernel_size)
    
    return c1, c2, kernel_size, character_size
    
def MLP(layer_info):
    
    layer = []
    
    for i in range(0, len(layer_info) - 1):
        if i == len(layer_info) - 2 :
            layer.append(nn.Linear(layer_info[i], layer_info[i+1]))
        else:
            layer.append(nn.Linear(layer_info[i], layer_info[i+1]))
            layer.append(nn.ReLU())
            
    return layer

class CNN(nn.Module):   
    def __init__(self):
        super().__init__()
        #畳み込みとプーリング
        self.conv1 = nn.Conv2d(1, c1, kernel_size)
        self.conv2 = nn.Conv2d(c1, c2, kernel_size)

        self.pool = nn.MaxPool2d(2, 2)

        #全結合層
        self.fc1 = nn.Linear(c2 * character_size, 128)
        self.fc2 = nn.Linear(128, 64)
        self.fc3 = nn.Linear(64, 10)
        
    def forward(self, x):
        x = self.pool(F.relu(self.conv1(x)))
        x = self.pool(F.relu(self.conv2(x)))

        x = x.view(-1, c2 * character_size)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

def select_net(dataset_num):

    print("ニューラルネットワークのアーキテクチャを選んでください")
    
    #実は0以外CNNになる
    network = int(input("0: MLP, 1:CNN\n"))
    
    return network

def learning_mlp(net, dataset_num, train_loader, criterion, optimizer):
    net.to(device)
    
    if dataset_num == 0:
        z0 = 28*28
    else:
        z0 = 32*32
        
    epoch = int(input("エポック数を入力してください\n"))
    
    print("学習します")
    
    for e in range(epoch):
        total_loss = 0
        for train_x, label in train_loader:
        
            train_x = train_x.reshape(-1, z0)
            
            #GPUかCPUに送る
            train_x, label = train_x.to(device), label.to(device)
        
            optimizer.zero_grad()
            loss = criterion(net(train_x), label)
            loss.backward()
            optimizer.step()
            total_loss += loss.data
            
            #使ったやつをCPUに戻しておく
            train_x, label = train_x.to("cpu"), label.to("cpu")
    
        if(e + 1) % 1 == 0:
            print(f"epoch{e + 1}",total_loss)
            
    net.to("cpu")
            
    print()
    print("学習しました")

def calc_accuracy_mlp(net, dataset_num, train_loader, test_loader):
    print("正解率を計算します")
    
    if dataset_num == 0:
        z0 = 28*28
    else:
        z0 = 32*32
    
    with torch.no_grad():
        
        for train_x, train_label in train_loader:
            train_x = train_x.reshape(-1, z0)
            train_net = net(train_x).detach()
            train_result = torch.max(train_net, 1)[1]
            train_acu = sum(train_label.data.numpy() == train_result.numpy()) / len(train_label.data.numpy())

        for test_x, test_label in test_loader:
            test_x = test_x.reshape(-1, z0)
            test_net = net(test_x).detach()
            test_result = torch.max(test_net, 1)[1]
            test_acu = sum(test_label.data.numpy() == test_result.numpy()) / len(test_label.data.numpy())
    
    print("学習用データの正解率：", train_acu)
    print("テスト用データの正解率：", test_acu)
    
def learning_cnn(net, train_loader, criterion, optimizer):
    epoch = int(input("エポック数を入力してください\n"))
    print()
    print("学習します")
    
    net.to(device)
    
    for e in range(epoch):
        total_loss = 0
        for train_x, label in train_loader:
            
            #GPUかCPUに流す
            train_x, label = train_x.to(device), label.to(device)
            
            optimizer.zero_grad()
            loss = criterion(net(train_x), label)
            loss.backward()
            optimizer.step()
            total_loss += loss.data
            
            #使ったら戻す
            train_x, label = train_x.to("cpu"), label.to("cpu")
    
        if(e + 1) % 1 == 0:
            print(f"epoch{e + 1}",total_loss)
            
    net.to("cpu")
            
    print()
    print("学習しました")

def calc_accuracy_cnn(net, train_loader, test_loader):
    print("正解率を計算します")
            
    with torch.no_grad(): 
        correct = 0
        total = 0
    
        for data in train_loader:
            images, labels = data
            outputs = net(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            train_acu = correct / total

    with torch.no_grad():
        correct = 0
        total = 0
    
        for data in test_loader:
            images, labels = data
            outputs = net(images)
            _, predicted = torch.max(outputs.data, 1)
            total += labels.size(0)
            correct += (predicted == labels).sum().item()
            test_acu = correct / total
        
    print("学習用データの正解率：", train_acu)
    print("テスト用データの正解率：", test_acu)
    
def main():
    dataset_num = dataset_select()
    train_loader, test_loader = load_data(dataset_num)
    arch = select_net(dataset_num)
    
    if arch == 0:
        
        layer_info = setting_MLP(dataset_num)
        layer = MLP(layer_info)
        net = nn.Sequential(*layer)
        
        print("device :", device)
        print(net)
        
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.SGD(net.parameters(), lr=0.005)
        
        learning_mlp(net, dataset_num, train_loader, criterion, optimizer)
        
        calc_accuracy_mlp(net, dataset_num, train_loader, test_loader)
    
    #実は0以外CNNになる
    else:
        global c1, c2, kernel_size, character_size
        c1, c2, kernel_size, character_size = setting_CNN(dataset_num)
        net = CNN()
        
        print("device :", device)
        print(net)
        
        criterion = nn.CrossEntropyLoss()
        optimizer = optim.Adam(net.parameters(), lr=0.005)
        
        learning_cnn(net, train_loader, criterion, optimizer)
        
        calc_accuracy_cnn(net, train_loader, test_loader)
        
print("準備完了\n")

main()

~~~

## 使い方
Google Colab での動作を想定しています。
https://colab.research.google.com/

Colabのセルにコピペして実行してください。
半角数字とEnterを押すだけで動きます、おそらく。

実行すると、準備中と表示されます。ここでは学習に使うデータを読み込んでいます。

準備完了と表示されると、データセットを選択します。
次に、ニューラルネットワークのアーキテクチャを選択します。
多層パーセプトロン（MLP）を使用する場合は 0、畳み込みニューラルネットワーク（CNN）を使用する場合は 1 を入力してください。

多層パーセプトロン（MLP）を選択した場合は、隠れ層の数, 隠れ層それぞれの頂点数, エポック数を指定してください。
畳み込みニューラルネットワーク（CNN）を使用する場合は、カーネルサイズ, チャネル数, エポック数を指定してください。

入力が終わると学習が始まり、正解率を計算します。

## 仕様
### CUDA
CUDAが使用できる場合には、CUDAを使用します。

### ライブラリ
NumPyとPyTorchを使用しています。

### データセット
MNISTとCIFAR-10を使って学習ができます。
CIFAR-10の元データはカラーですが、ここではグレースケールに変換して学習します。

### ニューラルネットワークのアーキテクチャ
多層パーセプトロン（MLP）と畳み込みニューラルネットワーク（CNN）が使えます。
多層パーセプトロンは隠れ層の数, 隠れ層それぞれの頂点数, エポック数を指定できます。
畳み込みニューラルネットワークはカーネルサイズ, チャネル数, エポック数を指定できます。畳み込みとプーリングを2回行います。プーリングは2*2のMaxpoolです。

### 最適化手法
多層パーセプトロンはSGD、畳み込みニューラルネットワークはAdamを使用します。

### 損失
交差エントロピーで計算します。

## 今後のアプデ
・畳み込みニューラルネットワークの自由度の改善
・学習率, 最適化手法その他の自由度の改善
・画像の表示

上記のようなアプデを考えています。
~~縛られないで機械学習をやりたいという方は、自身でコードを書くのが賢明かもしれません。~~