В моей предыдущей истории я рассмотрел вопрос о том, сколько слоев нужно нейронной сети для простых задач классификации. Я изучил линейную классификацию наборов данных AND и OR, а также более сложные XOR и данные двух лун.

Используя Keras, я показал, что нам нужен один слой для классификации линейных данных, в первую очередь потому, что одной гиперплоскости достаточно, чтобы различать разные метки, а для более сложных данных нам нужно как минимум два слоя. Действительно, согласно теореме об универсальной аппроксимации (Цыбенко, 1989: Приближение суперпозициями сигмоидальной функции), двухслойной нейронной сети достаточно для представления любой функции, но три (или более) слоя позволяют нам более гибко построение логических комбинаций.

В этом посте я хотел бы сосредоточиться на количестве нейронов в слое. Вопрос, который я задаю, заключается в том, сколько нейронов необходимо для задачи классификации? Чтобы лучше понять проблему, я строю иерархию наборов данных с различными измерениями и фокусируюсь на минимальном количестве нейронов на слой, необходимых для различения меток.

Я начну с создания двух трехмерных сфер, одна содержит другую, и цель состоит в том, чтобы различать их. После изучения трехмерной задачи я спроецирую трехмерные данные на двумерную плоскость и одномерную линию и изучу взаимосвязь между количеством нейронов в слое и размерностью данных.

# load Keras, numpy, matplotlib and sklearn
from keras import models
from keras import layers
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from sklearn.utils import check_random_state
from sklearn.decomposition import PCA
generator = check_random_state(42)
np.random.seed(42)
# define a few functions to generate the data and for ploting
def make_two_3d_circles(n_samples=10, r_out=1, r_in=0.7, noise=0.05):
    
    fig = plt.figure(figsize=(20,10))
    ax  = fig.add_subplot(111, projection='3d')
# generate data
    u_out = np.random.uniform(0, 2 * np.pi, n_samples) 
    v_out = np.random.uniform(0, 2 * np.pi, n_samples)
    u_in  = np.random.uniform(0, 2 * np.pi, n_samples) 
    v_in  = np.random.uniform(0, 2 * np.pi, n_samples)
    
    x_out = r_out * np.outer(np.cos(u_out), np.sin(v_out)) + generator.normal(scale=noise, size=(n_samples,n_samples))
    y_out = r_out * np.outer(np.sin(u_out), np.sin(v_out)) + generator.normal(scale=noise, size=(n_samples,n_samples))
    z_out = r_out * np.outer(np.ones(np.size(u_out)), np.cos(v_out)) + generator.normal(scale=noise, size=(n_samples,n_samples))
    x_in  = r_in  * np.outer(np.cos(u_in), np.sin(v_in)) + generator.normal(scale=noise, size=(n_samples,n_samples))
    y_in  = r_in  * np.outer(np.sin(u_in), np.sin(v_in)) + generator.normal(scale=noise, size=(n_samples,n_samples))
    z_in  = r_in  * np.outer(np.ones(np.size(u_in)), np.cos(v_in)) + generator.normal(scale=noise, size=(n_samples,n_samples))
# Plot the surface
    ax.scatter(x_in,  y_in,  z_in,  alpha=0.4 , color='b', s=200)
    ax.scatter(x_out, y_out, z_out, alpha=0.4 , color='r' , s=200)
for t in ax.xaxis.get_major_ticks(): t.label.set_fontsize(20)
    for t in ax.yaxis.get_major_ticks(): t.label.set_fontsize(20)
    for t in ax.zaxis.get_major_ticks(): t.label.set_fontsize(20)
ax.set_xlabel('\n\ndistance from the origin', fontsize=20)
    ax.set_ylabel('\n\ndistance from the origin', fontsize=20)
    ax.set_zlabel('\n\ndistance from the origin', fontsize=20)
    plt.show()
X = np.vstack((np.append(x_out, x_in),
                   np.append(y_out, y_in),
                   np.append(z_out, z_in))).T
    y = np.hstack([np.zeros(int(len(X)/2), dtype=np.intp),
                   np.ones (int(len(X)/2), dtype=np.intp)])
    return(X,y)
def plot_data_2d(X, y, title_str):
    
    amin, bmin = X.min(axis=0) - 0.1
    amax, bmax = X.max(axis=0) + 0.1
    hticks = np.linspace(amin, amax, 101)
    vticks = np.linspace(bmin, bmax, 101)
    
    plt.figure(figsize=(20,10))
    plt.scatter(X[y==0,0], X[y==0,1],c='r', s=200, alpha=0.4)
    plt.scatter(X[y==1,0], X[y==1,1],c='b', s=200, alpha=0.4)
    plt.title(title_str,fontsize=20)
    plt.xticks(fontsize=20)
    plt.yticks(fontsize=20)
    plt.xlim(amin, amax)
    plt.ylim(bmin, bmax)
    plt.grid('on')
def plot_data_1d(X, y, title_str):
plt.plot(X[y==1],'ob', markersize=12, alpha=0.4)
    plt.plot(X[y==0],'or', markersize=12, alpha=0.4)
    plt.title(title_str,fontsize=20)
    plt.xticks(fontsize=20)
    plt.yticks(fontsize=20)
    plt.grid('on')
def plot_data_1d_2nd_look(X, y, title_str):
    
    plt.plot(X[y==1],np.ones(X[y==1].shape),'ob', markersize=12, alpha=0.1)
    plt.plot(X[y==0],np.zeros(X[y==0].shape),'or', markersize=12, alpha=0.1)
    plt.title(title_str,fontsize=20)
    plt.xticks(fontsize=20)
    plt.yticks(fontsize=20)
    plt.grid('on')
def plot_loss_acc(loss_values, acc_values, titles, legends):
    
    plt.subplot(121)
    epochs = range(1, len(loss_values) + 1)
    plt.plot(epochs, loss_values, 'o', label=titles[0])
    plt.title(titles[0],fontsize=20)
    plt.xlabel('Epoch',fontsize=20)
    plt.ylabel('Loss',fontsize=20)
    plt.legend(legends,fontsize=20)
    plt.xticks(fontsize=20)
    plt.yticks(fontsize=20)
plt.subplot(122)
    epochs = range(1, len(acc_values) + 1)
    plt.plot(epochs, acc_values, 'o', label=titles[1])
    plt.title(titles[1],fontsize=20)
    plt.xlabel('Epoch',fontsize=20)
    plt.ylabel('Accuracy',fontsize=20)
    plt.legend(legends,fontsize=20)
    plt.xticks(fontsize=20)
    plt.yticks(fontsize=20)

Трехмерные данные

Вот рисунок, показывающий данные. Внутренняя сфера (синие точки) помечена как ноль, а внешняя сфера (красные точки) помечена как единица. Цель состоит в том, чтобы провести различие между ними, используя модель нейронной сети с минимальным количеством нейронов и слоев.

X_3d, y_3d = make_two_3d_circles(n_samples=15, r_out=1, r_in=0.5, noise=0.05)

print(X_3d.shape)
print(y_3d.shape)
(450, 3)
(450,)

Как я объяснял ранее и как видно, одна гиперплоскость не может разделить две сферы, и мы должны использовать нелинейную модель. Минимальное количество слоев — два, но сколько нейронов нам нужно в каждом?

В отличной онлайн-книге Майкла Нильсена в интерактивном режиме исследуется, что могут вычислять нейроны (глава 4), а важный итог заключается в том, что один нейрон может обучиться ступенчатой ​​функции, а пара нейронов может представить функцию выпуклости.

Функции ступенька и выпуклость необходимы для классификации, поскольку они могут различать область или регион. Таким образом, пара нейронов может изолировать связанные одномерные данные, такие как данные XOR в моем последнем посте, в основном путем изучения двух лучших прямых (или гиперплоскостей), необходимых для различения данных. Для двумерных данных требуется не менее четырех гиперплоскостей, а для трехмерных данных требуется не менее шести гиперплоскостей. Позвольте мне объяснить, почему это так.

Давайте начнем с размышлений о трехмерной задаче. Наши данные имеют два шара, один внутри другого, и мы пытаемся изолировать один от другого. Представьте, что вы берете воображаемую коробку из-под обуви и кладете в нее внутренний мячик. Это отличный способ отделить внутренний шар от внешнего, не так ли?

Сколько сторон (или гиперплоскостей) у обувной коробки?

Шесть!

Итак, для задачи трехмерной классификации необходимо по крайней мере шесть гиперплоскостей для классификации или изоляции связанных выпуклых данных, таких как трехмерный шар или сфера. Другими словами, функция рельефа в каждом из трех направлений позволяет нам представить замкнутую область в трехмерной области.

В заключение, это простое интуитивное обсуждение привело нас к выводу, что минимальная сеть, которая может классифицировать внутренний шар, представляет собой двухслойную сеть с шестью и одним нейроном.

#training constants
epochs_num=500
batch_size_num=32
verbose_num=0
model = models.Sequential()
model.add(layers.Dense(2, activation='relu', input_shape=(3,)))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history_2_1 = model.fit(X_3d, y_3d, epochs=epochs_num, 
                        batch_size=batch_size_num, verbose=verbose_num).history
model = models.Sequential()
model.add(layers.Dense(4, activation='relu', input_shape=(3,)))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history_4_1 = model.fit(X_3d, y_3d, epochs=epochs_num, 
                        batch_size=batch_size_num, verbose=verbose_num).history
model = models.Sequential()
model.add(layers.Dense(6, activation='relu', input_shape=(3,)))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history_6_1 = model.fit(X_3d, y_3d, epochs=epochs_num, 
                        batch_size=batch_size_num, verbose=verbose_num).history

Как и ожидалось, мы видим, что двухслойная нейронная сеть с шестью и одним нейроном (обозначается 6_1) учится правильно классифицировать данные примерно после 400 итераций, в то время как четыре-один (обозначается 4_1) и два-один (обозначается 2_1) сети не обладают достаточной нелинейной гибкостью, чтобы изолировать внутренний шар.

plt.figure(figsize=(20,10))
plot_loss_acc(history_2_1['loss'], history_2_1['acc'],
              ['Training loss','Training accuracy'],['2_1','4_1','6_1'])
plot_loss_acc(history_4_1['loss'], history_4_1['acc'],
              ['Training loss','Training accuracy'],['2_1','4_1','6_1'])
plot_loss_acc(history_6_1['loss'], history_6_1['acc'],
              ['Training loss','Training accuracy'],['2_1','4_1','6_1'])
plt.show()

Двумерные данные

Далее я хочу снова использовать Keras и исследовать двумерную проблему. Вместо того, чтобы разрезать предыдущие трехмерные шары, я спроецирую трехмерные шары на двумерную плоскость, используя технику размерного сокращения PCA, по существу проецируя данные на два направления, содержащие наиболее значительную изменчивость данных (радиус и угол). ).

X_2d = PCA(n_components=2).fit_transform(X_3d)
y_2d = np.hstack([np.zeros(int(len(X_2d)/2), dtype=np.intp),
                  np.ones (int(len(X_2d)/2), dtype=np.intp)])
plot_data_2d(X_2d, y_2d,'2-d projection via PCA')
plt.ylabel('distance from the origin',fontsize=20)
plt.xlabel('distance from the origin',fontsize=20)
plt.show()

Видно, что внутренний 2-мерный шар содержит в основном синие точки, а внешний 2-мерный шар полностью состоит из красных точек — это следствие проекции PCA.
Сколько слоев и нейронов нужно, чтобы различить две метки? Логика предыдущего раздела верна, и это должно привести вас к выводу, что двухслойная сеть с четырьмя и одним нейроном — это минимум, необходимый для классификации данных. Давайте посмотрим, так ли это.

model = models.Sequential()
model.add(layers.Dense(2, activation='relu', input_shape=(2,)))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history_2_1 = model.fit(X_2d, y_2d, epochs=epochs_num, 
                        batch_size=batch_size_num, verbose=verbose_num).history
model = models.Sequential()
model.add(layers.Dense(4, activation='relu', input_shape=(2,)))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history_4_1 = model.fit(X_2d, y_2d, epochs=epochs_num, 
                        batch_size=batch_size_num, verbose=verbose_num).history

Как и ожидалось, мы видим, что двухслойная нейронная сеть с четырьмя и одним нейроном (обозначается 4_1) учится правильно классифицировать данные примерно после 400 итераций, в то время как двухслойная сеть (обозначается 2_1) не обладает достаточной нелинейной гибкостью. чтобы изолировать внутренний 2-й шар.

plt.figure(figsize=(20,10))
plot_loss_acc(history_2_1['loss'], history_2_1['acc'],
              ['Training loss','Training accuracy'],['2_1','4_1'])
plot_loss_acc(history_4_1['loss'], history_4_1['acc'],
              ['Training loss','Training accuracy'],['2_1','4_1'])
plt.show()

Одномерные данные

Наконец, я спроецирую трехмерный шар на одномерную линию и попытаюсь изолировать метки. Я повторю свою предыдущую методологию, проецируя трехмерные данные с использованием метода PCA.

На следующем рисунке я показываю, как выглядят одномерные данные двумя разными способами — суть в том, что данные получаются менее чистыми и менее разделимыми. Таким образом, мы не ожидаем, что сеть будет классифицировать данные со 100% точностью, но все же ожидаем, что функция потерь будет монотонно уменьшаться с количеством итераций.

X_1d = PCA(n_components=1).fit_transform(X_3d)
y_1d = np.hstack([np.zeros(int(len(X_1d)/2), dtype=np.intp),
                  np.ones (int(len(X_1d)/2), dtype=np.intp)])
plt.figure(figsize=(20,10))
plt.subplot(121)
plot_data_1d(X_1d, y_1d,'1-d projection via PCA')
plt.ylabel('distance from the origin',fontsize=20)
plt.xlabel('point number',fontsize=20)
plt.subplot(122)
plot_data_1d_2nd_look(X_1d, y_1d,'1-d projection via PCA')
plt.ylabel('label',fontsize=20)
plt.xlabel('distance from the origin',fontsize=20)
plt.show()

Сколько слоев и нейронов необходимо, чтобы различить два одномерных шара? Предыдущая логика остается в силе, и это должно привести вас к выводу, что двухслойная сеть с двумя и одним нейроном (обозначается 2_1) — это минимум, необходимый для классификации данных.

epochs_num=800
model = models.Sequential()
model.add(layers.Dense(2, activation='relu', input_shape=(1,)))
model.add(layers.Dense(1, activation='sigmoid'))
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history_2_1 = model.fit(X_1d, y_1d, epochs=epochs_num, 
                        batch_size=batch_size_num, verbose=verbose_num).history

Как объяснялось и как ожидалось, чем дольше мы обучаем данные, тем ниже становится функция потерь и тем выше становится точность прогнозирования.

plt.figure(figsize=(20,10))
plot_loss_acc(history_2_1['loss'], history_2_1['acc'],
              ['Training loss','Training accuracy'],['2_1','4_1','6_1'])
plt.show()

Вывод

В завершение поста я изучил иерархию классификации двух шаров, один внутри другого, в различных измерениях. Я показал, что для этой выпуклой задачи двухслойной сети достаточно, чтобы правильно классифицировать данные, и что минимальное количество нейронов равно 2N и 1, где N обозначает размер шаров.

np.random.seed(42)