前回の記事「PyTorch Concise Tutorial Part 1」に引き続き、多層パーセプトロン、畳み込みニューラルネットワーク、LSTMNetについて学習していきます。 1. 多層パーセプトロン多層パーセプトロンは、ネットワークに 1 つ以上の隠れ層を追加することで線形モデルの制限を克服します。これはシンプルなニューラル ネットワークであり、ディープラーニングの重要な基盤です。具体的な図は次のとおりです。 import numpy as np import torch from torch.autograd import Variable from torch import optim from data_util import load_mnist def build_model(input_dim, output_dim): return torch.nn.Sequential( torch.nn.Linear(input_dim, 512, bias=False), torch.nn.ReLU(), torch.nn.Dropout(0.2), torch.nn.Linear(512, 512, bias=False), torch.nn.ReLU(), torch.nn.Dropout(0.2), torch.nn.Linear(512, output_dim, bias=False), ) def train(model, loss, optimizer, x_val, y_val): model.train() optimizer.zero_grad() fx = model.forward(x_val) output = loss.forward(fx, y_val) output.backward() optimizer.step() return output.item() def predict(model, x_val): model.eval() output = model.forward(x_val) return output.data.numpy().argmax(axis=1) def main(): torch.manual_seed(42) trX, teX, trY, teY = load_mnist(notallow=False) trX = torch.from_numpy(trX).float() teX = torch.from_numpy(teX).float() trY = torch.tensor(trY) n_examples, n_features = trX.size() n_classes = 10 model = build_model(n_features, n_classes) loss = torch.nn.CrossEntropyLoss(reductinotallow='mean') optimizer = optim.Adam(model.parameters()) batch_size = 100 for i in range(100): cost = 0. num_batches = n_examples // batch_size for k in range(num_batches): start, end = k * batch_size, (k + 1) * batch_size cost += train(model, loss, optimizer, trX[start:end], trY[start:end]) predY = predict(model, teX) print("Epoch %d, cost = %f, acc = %.2f%%" % (i + 1, cost / num_batches, 100. * np.mean(predY == teY))) if __name__ == "__main__": main() (1) 上記のコードは単層ニューラルネットワークのコードと似ています。違いは、build_model が 3 つの線形層と 2 つの ReLU 活性化関数を持つニューラルネットワークモデルを構築することです。 - 入力特徴の数 input_dim と出力特徴の数 512 で、最初の線形レイヤーをモデルに追加します。
- 次に、ReLU アクティベーション関数とドロップアウト レイヤーを追加して、モデルの非線形機能を強化し、過剰適合を防ぎます。
- 512 個の入力フィーチャと 512 個の出力フィーチャを持つ 2 番目の線形レイヤーをモデルに追加します。
- 次に、ReLU アクティベーション関数とドロップアウト レイヤーを追加します。
- 入力特徴数が 512、出力特徴数が output_dim (モデルの出力カテゴリの数) である 3 番目の線形レイヤーをモデルに追加します。
(2)ReLU活性化関数とは何ですか? ReLU (Rectified Linear Unit) 活性化関数は、ディープラーニングやニューラル ネットワークでよく使用される活性化関数です。ReLU 関数の数式は f(x) = max(0, x) です。ここで、x は入力値です。 ReLU 関数の特徴は、入力値が 0 以下の場合は出力が 0 になり、入力値が 0 より大きい場合は出力が入力値と等しくなることです。簡単に言えば、ReLU 関数は負の部分を 0 に抑制し、正の部分は変更しません。ニューラル ネットワークにおける ReLU 活性化関数の役割は、ニューラル ネットワークが複雑な非線形関係に適合できるように非線形要素を導入することです。同時に、ReLU 関数は、他の活性化関数 (Sigmoid や Tanh など) と比較して、計算速度が速く、収束速度が速いという利点があります。 (3)ドロップアウト層とは何ですか?ドロップアウト レイヤーは、ニューラル ネットワークで過剰適合を防ぐために使用される手法です。トレーニングプロセス中、ドロップアウト層は、一部のニューロンの出力をランダムに 0 に設定し、つまりこれらのニューロンを「破棄」します。その目的は、ニューロン間の相互依存性を減らし、ネットワークの一般化能力を向上させることです。 (4) print("Epoch %d, cost = %f, acc = %.2f%%" % (i + 1, cost / num_batches, 100. * np.mean(predY == teY))) 最後に、現在のトレーニングラウンド、損失値、accを出力します。上記のコード出力は次のようになります。 ... Epoch 91, cost = 0.011129, acc = 98.45% Epoch 92, cost = 0.007644, acc = 98.58% Epoch 93, cost = 0.011872, acc = 98.61% Epoch 94, cost = 0.010658, acc = 98.58% Epoch 95, cost = 0.007274, acc = 98.54% Epoch 96, cost = 0.008183, acc = 98.43% Epoch 97, cost = 0.009999, acc = 98.33% Epoch 98, cost = 0.011613, acc = 98.36% Epoch 99, cost = 0.007391, acc = 98.51% Epoch 100, cost = 0.011122, acc = 98.59% 同じデータ分類の場合、単層ニューラル ネットワークよりも精度が高いことがわかります (98.59% > 97.68%)。 2. 畳み込みニューラルネットワーク畳み込みニューラル ネットワーク (CNN) は、ディープラーニング アルゴリズムです。行列を入力すると、CNN は重要な部分と重要でない部分を区別できます (重みを割り当てます)。他の分類タスクと比較して、CNN はそれほど高度なデータ前処理を必要としません。十分にトレーニングされていれば、次の図に示すように、行列内の特徴を学習できます。 import numpy as np import torch from torch.autograd import Variable from torch import optim from data_util import load_mnist class ConvNet(torch.nn.Module): def __init__(self, output_dim): super(ConvNet, self).__init__() self.conv = torch.nn.Sequential() self.conv.add_module("conv_1", torch.nn.Conv2d(1, 10, kernel_size=5)) self.conv.add_module("maxpool_1", torch.nn.MaxPool2d(kernel_size=2)) self.conv.add_module("relu_1", torch.nn.ReLU()) self.conv.add_module("conv_2", torch.nn.Conv2d(10, 20, kernel_size=5)) self.conv.add_module("dropout_2", torch.nn.Dropout()) self.conv.add_module("maxpool_2", torch.nn.MaxPool2d(kernel_size=2)) self.conv.add_module("relu_2", torch.nn.ReLU()) self.fc = torch.nn.Sequential() self.fc.add_module("fc1", torch.nn.Linear(320, 50)) self.fc.add_module("relu_3", torch.nn.ReLU()) self.fc.add_module("dropout_3", torch.nn.Dropout()) self.fc.add_module("fc2", torch.nn.Linear(50, output_dim)) def forward(self, x): x = self.conv.forward(x) x = x.view(-1, 320) return self.fc.forward(x) def train(model, loss, optimizer, x_val, y_val): model.train() optimizer.zero_grad() fx = model.forward(x_val) output = loss.forward(fx, y_val) output.backward() optimizer.step() return output.item() def predict(model, x_val): model.eval() output = model.forward(x_val) return output.data.numpy().argmax(axis=1) def main(): torch.manual_seed(42) trX, teX, trY, teY = load_mnist(notallow=False) trX = trX.reshape(-1, 1, 28, 28) teX = teX.reshape(-1, 1, 28, 28) trX = torch.from_numpy(trX).float() teX = torch.from_numpy(teX).float() trY = torch.tensor(trY) n_examples = len(trX) n_classes = 10 model = ConvNet(output_dim=n_classes) loss = torch.nn.CrossEntropyLoss(reductinotallow='mean') optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) batch_size = 100 for i in range(100): cost = 0. num_batches = n_examples // batch_size for k in range(num_batches): start, end = k * batch_size, (k + 1) * batch_size cost += train(model, loss, optimizer, trX[start:end], trY[start:end]) predY = predict(model, teX) print("Epoch %d, cost = %f, acc = %.2f%%" % (i + 1, cost / num_batches, 100. * np.mean(predY == teY))) if __name__ == "__main__": main() (1) 上記のコードは、torch.nn.Module クラスを継承し、畳み込みニューラルネットワークを表す ConvNet というクラスを定義します。__init__ メソッドでは、畳み込み層と完全結合層を表す 2 つのサブモジュール conv と fc が定義されています。 conv サブモジュールでは、2 つの畳み込み層 (torch.nn.Conv2d)、2 つの最大プーリング層 (torch.nn.MaxPool2d)、2 つの ReLU 活性化関数 (torch.nn.ReLU)、およびドロップアウト層 (torch.nn.Dropout) を定義します。 fc サブモジュールでは、2 つの線形レイヤー (torch.nn.Linear)、ReLU 活性化関数、および Dropout レイヤーが定義されています。 (2)プーリング層を定義する目的は何ですか?プーリング層は CNN の重要なコンポーネントです。プーリング層の主な目的は次のとおりです。 - 次元削減: プーリング層は、入力特徴マップのローカル領域をダウンサンプリングすることで、特徴マップのサイズを縮小します。これにより、後続のレイヤーのパラメータ数が減り、計算の複雑さが軽減され、トレーニング プロセスが高速化されます。
- 変換不変性: プーリング層は、入力画像に対するネットワークの変換不変性を向上させることができます。画像内の特徴がわずかに移動した場合でも、プーリング層の出力には同様の特徴表現が残ります。これにより、モデルの一般化能力が向上し、異なる場所やスケールで同じ特徴を認識できるようになります。
- 過剰適合を防ぐ: 特徴マップのサイズを縮小することで、プーリング層はモデルのパラメータ数を減らし、過剰適合のリスクを軽減できます。
- 強化された特徴表現: プーリング操作により、ローカル領域内の特徴を集約できるため、より重要な特徴情報が強化され、強調表示されます。一般的なプーリング操作には、最大プーリングと平均プーリングがあり、それぞれローカルエリア内の最大値または平均値を出力として取得します。
(3) print("Epoch %d, cost = %f, acc = %.2f%%" % (i + 1, cost / num_batches, 100. * np.mean(predY == teY))) 最後に、現在のトレーニングラウンド、損失値、accを出力します。上記のコード出力は次のようになります。 ... Epoch 91, cost = 0.047302, acc = 99.22% Epoch 92, cost = 0.049026, acc = 99.22% Epoch 93, cost = 0.048953, acc = 99.13% Epoch 94, cost = 0.045235, acc = 99.12% Epoch 95, cost = 0.045136, acc = 99.14% Epoch 96, cost = 0.048240, acc = 99.02% Epoch 97, cost = 0.049063, acc = 99.21% Epoch 98, cost = 0.045373, acc = 99.23% Epoch 99, cost = 0.046127, acc = 99.12% Epoch 100, cost = 0.046864, acc = 99.10% 同じデータ分類の場合、精度は多層パーセプトロンよりも高いことがわかります (99.10% > 98.59%)。 3. LSTMネットLSTMNet は、Long Short-Term Memory (LSTM) ネットワークを使用して構築されたニューラル ネットワークです。その中心となるアイデアは、長期依存情報をある程度まで保持できる「メモリ ユニット」と呼ばれる構造を導入することです。LSTM の各ユニットには、入力ゲート、忘却ゲート、および出力ゲートが含まれます。これらのゲートの機能は、メモリ ユニット内の情報の流れを制御して、ネットワークが有用な情報をいつ保存、更新、または出力するかを学習できるようにすることです。 import numpy as np import torch from torch import optim, nn from data_util import load_mnist class LSTMNet(torch.nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super(LSTMNet, self).__init__() self.hidden_dim = hidden_dim self.lstm = nn.LSTM(input_dim, hidden_dim) self.linear = nn.Linear(hidden_dim, output_dim, bias=False) def forward(self, x): batch_size = x.size()[1] h0 = torch.zeros([1, batch_size, self.hidden_dim]) c0 = torch.zeros([1, batch_size, self.hidden_dim]) fx, _ = self.lstm.forward(x, (h0, c0)) return self.linear.forward(fx[-1]) def train(model, loss, optimizer, x_val, y_val): model.train() optimizer.zero_grad() fx = model.forward(x_val) output = loss.forward(fx, y_val) output.backward() optimizer.step() return output.item() def predict(model, x_val): model.eval() output = model.forward(x_val) return output.data.numpy().argmax(axis=1) def main(): torch.manual_seed(42) trX, teX, trY, teY = load_mnist(notallow=False) train_size = len(trY) n_classes = 10 seq_length = 28 input_dim = 28 hidden_dim = 128 batch_size = 100 epochs = 100 trX = trX.reshape(-1, seq_length, input_dim) teX = teX.reshape(-1, seq_length, input_dim) trX = np.swapaxes(trX, 0, 1) teX = np.swapaxes(teX, 0, 1) trX = torch.from_numpy(trX).float() teX = torch.from_numpy(teX).float() trY = torch.tensor(trY) model = LSTMNet(input_dim, hidden_dim, n_classes) loss = torch.nn.CrossEntropyLoss(reductinotallow='mean') optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) for i in range(epochs): cost = 0. num_batches = train_size // batch_size for k in range(num_batches): start, end = k * batch_size, (k + 1) * batch_size cost += train(model, loss, optimizer, trX[:, start:end, :], trY[start:end]) predY = predict(model, teX) print("Epoch %d, cost = %f, acc = %.2f%%" % (i + 1, cost / num_batches, 100. * np.mean(predY == teY))) if __name__ == "__main__": main() (1)上記コードの一般的な部分、特にLSTMNetクラスについては説明しません。 - self.lstm = nn.LSTM(input_dim, hidden_dim) は、入力次元 input_dim と隠しレイヤー次元 hidden_dim を持つ LSTM レイヤーを作成します。
- self.linear = nn.Linear(hidden_dim, output_dim,bias=False) は、入力次元が hidden_dim、出力次元が output_dim の線形レイヤー (完全に接続されたレイヤー) を作成し、バイアスは設定しません。
- h0 = torch.zeros([1, batch_size, self.hidden_dim]) は、LSTM レイヤーの隠し状態 h0 を初期化します。これは、[1, batch_size, hidden_dim] の形状を持つ完全なゼロテンソルです。
- c0 = torch.zeros([1, batch_size, self.hidden_dim]) は、LSTM レイヤーのセル状態 c0 を初期化します。これは、[1, batch_size, hidden_dim] の形状を持つ完全なゼロテンソルです。
- fx, _ = self.lstm.forward(x, (h0, c0)) は、入力データ x、初期の隠れ状態 h0、およびセル状態 c0 を LSTM レイヤーに渡して、LSTM レイヤーの出力 fx を取得します。
- return self.linear.forward(fx[-1]) は、LSTM レイヤーの出力を線形レイヤーに渡して計算し、最終出力を取得します。ここでfx[-1]はLSTMレイヤーによって出力された最後のタイムステップのデータを取得することを意味します。
(2) print("Epoch %d, cost = %f, acc = %.2f%%" % (i + 1, cost / num_batches, 100. * np.mean(predY == teY))) 最後に、現在のトレーニングラウンド、損失値、accを出力します。上記のコード出力は次のようになります。 Epoch 91, cost = 0.000468, acc = 98.57% Epoch 92, cost = 0.000452, acc = 98.57% Epoch 93, cost = 0.000437, acc = 98.58% Epoch 94, cost = 0.000422, acc = 98.57% Epoch 95, cost = 0.000409, acc = 98.58% Epoch 96, cost = 0.000396, acc = 98.58% Epoch 97, cost = 0.000384, acc = 98.57% Epoch 98, cost = 0.000372, acc = 98.56% Epoch 99, cost = 0.000360, acc = 98.55% Epoch 100, cost = 0.000349, acc = 98.55% 4. 補助コード2 つの記事の from data_util import load_mnist の data_util.py コードは次のとおりです。 import gzip import os import urllib.request as request from os import path import numpy as np DATASET_DIR = 'datasets/' MNIST_FILES = ["train-images-idx3-ubyte.gz", "train-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz", "t10k-labels-idx1-ubyte.gz"] def download_file(url, local_path): dir_path = path.dirname(local_path) if not path.exists(dir_path): print("Creating the directory '%s' ..." % dir_path) os.makedirs(dir_path) print("Downloading from '%s' ..." % url) request.urlretrieve(url, local_path) def download_mnist(local_path): url_root = "http://yann.lecun.com/exdb/mnist/" for f_name in MNIST_FILES: f_path = os.path.join(local_path, f_name) if not path.exists(f_path): download_file(url_root + f_name, f_path) def one_hot(x, n): if type(x) == list: x = np.array(x) x = x.flatten() o_h = np.zeros((len(x), n)) o_h[np.arange(len(x)), x] = 1 return o_h def load_mnist(ntrain=60000, ntest=10000, notallow=True): data_dir = os.path.join(DATASET_DIR, 'mnist/') if not path.exists(data_dir): download_mnist(data_dir) else: # check all files checks = [path.exists(os.path.join(data_dir, f)) for f in MNIST_FILES] if not np.all(checks): download_mnist(data_dir) with gzip.open(os.path.join(data_dir, 'train-images-idx3-ubyte.gz')) as fd: buf = fd.read() loaded = np.frombuffer(buf, dtype=np.uint8) trX = loaded[16:].reshape((60000, 28 * 28)).astype(float) with gzip.open(os.path.join(data_dir, 'train-labels-idx1-ubyte.gz')) as fd: buf = fd.read() loaded = np.frombuffer(buf, dtype=np.uint8) trY = loaded[8:].reshape((60000)) with gzip.open(os.path.join(data_dir, 't10k-images-idx3-ubyte.gz')) as fd: buf = fd.read() loaded = np.frombuffer(buf, dtype=np.uint8) teX = loaded[16:].reshape((10000, 28 * 28)).astype(float) with gzip.open(os.path.join(data_dir, 't10k-labels-idx1-ubyte.gz')) as fd: buf = fd.read() loaded = np.frombuffer(buf, dtype=np.uint8) teY = loaded[8:].reshape((10000)) trX /= 255. teX /= 255. trX = trX[:ntrain] trY = trY[:ntrain] teX = teX[:ntest] teY = teY[:ntest] if onehot: trY = one_hot(trY, 10) teY = one_hot(teY, 10) else: trY = np.asarray(trY) teY = np.asarray(teY) return trX, teX, trY, teY
|