Глубокое обучение переживает бум. Теперь, когда доступен высокоуровневый API нейронных сетей, такой как Keras, легко запускать глубокие нейронные сети и решать сложные задачи классификации. Тем не менее, время от времени полезно возвращаться к простым задачам, которые мы можем полностью понять, и это цель данной публикации.
В этом посте я хотел бы спросить, какое минимальное количество нейронов и слоев необходимо для классификации простых разделяемых функций. Хотя это не обязательно новая проблема, я исследую несколько интересных аспектов этой проблемы с помощью Keras.
Если вам интересно, и в целом, ответ таков: однослойная сеть может представлять полуплоскости (то есть логику И или ИЛИ), двухслойная сеть может классифицировать точки внутри любого количества произвольных линий, а трехслойная сеть может классифицировать точки внутри любого количества произвольных линий. сеть может классифицировать любую произвольную форму по произвольным размерам. Следовательно, двухслойная модель может классифицировать любой выпуклый набор, а трехслойная модель может классифицировать любое количество непересекающихся выпуклых или вогнутых форм.
Но теперь позвольте мне подробнее остановиться на нескольких конкретных, но простых примерах. Начнем с загрузки Keras, numpy и matplotlib.
from keras import models from keras import layers import numpy as np import matplotlib.pyplot as plt np.random.seed(2018)
Классификация - это описание данных. На практике данные представляют собой своего рода измерения, и у нас нет возможности узнать, как эти данные были сгенерированы. Представьте на мгновение, что данные, которые у нас есть, показаны ниже синим цветом. Определенная полиномиальная функция сгенерировала данные (см. Код Python ниже), но скажем, что мы этого не знаем. Наша цель - оценить данные наилучшим образом с помощью некоторой функции. Представьте, что единственный доступный нам инструмент - это прямая линия (см. Ниже черным цветом), и мы можем оптимизировать только наклон и пересечение. В этом случае мы не можем правильно подобрать или описать данные с минимальной ошибкой (то есть с нулевой среднеквадратичной ошибкой). Линейная функция просто не может отображать колебания сгенерированных данных.
x=np.linspace(0,1,100) y = 1 + x - x**2 + 10*x**3 - 10*x**4 fit = np.polyfit(x,y,1) fit_fn = np.poly1d(fit) # fit_fn is now a function which takes in x and returns an estimate for y plt.figure(figsize=(20,10)) plt.plot(x,y,'ob',linewidth=2) plt.plot(x, fit_fn(x),'k',linewidth=2) plt.title('Polynomial and linear fit',fontsize=20) plt.xticks(fontsize=20) plt.yticks(fontsize=20) plt.grid('on') plt.show()
Этот пост посвящен описанию данных или «переобучению». Но здесь переобучение - это хорошо, и я задаю вопрос, какое минимальное количество функций необходимо функции для представления изменчивости данных. Как только мы узнаем, какая функция нам нужна, мы можем добавить ограничения в процесс оптимизации и убедиться, что мы не представляем шум или переоснащение в плохом смысле. Другими словами, тонкий процесс компромисса смещения и дисперсии не может начаться, если у нас нет нужного инструмента (то есть мы не знаем, какую функцию использовать), и именно об этом этот пост.
Затем я спрошу, сколько нейронов и сколько слоев нужно нейронной сети для классификации простых наборов данных. Я начну с простых наборов данных, где мы хотим классифицировать данные И и ИЛИ, и продолжу исследовать наступивший сложный XOR и, наконец, проблемы классификации двух лун, используя Keras.
Вот некоторые константы, которые я буду использовать позже для создания наборов данных.
# constants npts = 100 # points per blob tot_npts = 4*npts # total number of points s = 0.005 # ~standard deviation sigma = np.array([[s, 0], [0, s]]) #cov matrix
Проблема И
Проблема AND проста. Как вы можете видеть ниже, данные сгруппированы по четырем областям: [0,0], [0,1], [1,0] и [1,1]. Когда мы применяем логическую функцию И к каждой паре, следует, что [0,0] = 0, [0,1] = 0, [1,0] = 0, но [1,1] = 1. Мы помечаем пару данных как один (синий), когда обе точки равны единице. В противном случае мы помечаем данные как ноль (красный).
# Generate Data
data1 = np.random.multivariate_normal( [0,0], sigma, npts)
data2 = np.random.multivariate_normal( [0,1], sigma, npts)
data3 = np.random.multivariate_normal( [1,0], sigma, npts)
data4 = np.random.multivariate_normal( [1,1], sigma, npts)
and_data = np.concatenate((data1, data2, data3, data4)) # data
and_labels = np.concatenate((np.ones((3*npts)),np.zeros((npts)))) # labels
print(and_data.shape)
print(and_labels.shape)
plt.figure(figsize=(20,10))
plt.scatter(and_data[:,0][and_labels==0], and_data[:,1][and_labels==0],c='b')
plt.scatter(and_data[:,0][and_labels==1], and_data[:,1][and_labels==1],c='r')
plt.plot()
plt.title('AND problem',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.grid('on')
plt.show()
(400, 2)
(400,)
Разделить данные AND просто. Прямая линия может разделить данные на синие и красные.
Это было просто.
Линейная линия представлена как Wx + b (где W - наклон, а b - смещение), а в мире нейронных сетей - как np.dot (W, x) + b. Таким образом, одного слоя с одним нейроном (то есть одной линейной линии) будет достаточно для разделения данных AND.
Ниже вы можете увидеть мою реализацию Keras нейронной сети с одним слоем, одним нейроном и сигмовидной активацией. В качестве функции потерь я выбрал binary_crossentropy с оптимизатором Adam. Перебирая данные с batch_size, равным 16, модель сходится к правильному решению, измеренному по точности, примерно после 100 итераций по всем данным.
model = models.Sequential()
model.add(layers.Dense(1, activation='sigmoid', input_shape=(2,)))
model.summary()
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(and_data,
and_labels,
epochs=200,
batch_size=16,
verbose=0)
history_dict = history.history
history_dict.keys()
plt.figure(figsize=(20,10))
plt.subplot(121)
loss_values = history_dict['loss']
#val_loss_values = history_dict['val_loss']
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, loss_values, 'bo', label='Training loss')
#plt.plot(epochs, val_loss_values, 'b', label='Validation loss')
plt.title('Training loss',fontsize=20)
plt.xlabel('Epochs',fontsize=20)
plt.ylabel('Loss',fontsize=20)
plt.legend(fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.subplot(122)
acc_values = history_dict['acc']
#val_acc_values = history_dict['val_acc']
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, acc_values, 'bo', label='Training acc')
#plt.plot(epochs, val_acc_values, 'b', label='Validation acc')
plt.title('Training accuracy',fontsize=20)
plt.xlabel('Epochs',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.legend(fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
Проблема с операцией "ИЛИ"
Задача ИЛИ тоже проста. Опять же, данные сгруппированы по четырем областям: [0,0], [0,1], [1,0] и [1,1]. Как и раньше, к каждой паре применяется логическая функция ИЛИ. Отсюда следует, что [0,0] = 0, но [0,1] = 1, [1,0] = 1 и [1,1] = 1. Мы помечаем пару данных как ноль (красный), только когда обе точки равны нулю. В противном случае мы помечаем данные как один (синий).
# Generate Data data1 = np.random.multivariate_normal( [0,0], sigma, npts) data2 = np.random.multivariate_normal( [0,1], sigma, npts) data3 = np.random.multivariate_normal( [1,0], sigma, npts) data4 = np.random.multivariate_normal( [1,1], sigma, npts) or_data = np.concatenate((data1, data2, data3, data4)) or_labels = np.concatenate((np.ones((npts)),np.zeros((3*npts)))) plt.figure(figsize=(20,10)) plt.scatter(or_data[:,0][or_labels==0], or_data[:,1][or_labels==0],c='b') plt.scatter(or_data[:,0][or_labels==1], or_data[:,1][or_labels==1],c='r') plt.title('OR problem',fontsize=20) plt.xticks(fontsize=20) plt.yticks(fontsize=20) plt.grid('on') plt.show()
Разделить эти данные также просто. Что касается данных AND, прямой линии будет достаточно, и, как и раньше, нейронная сеть с одним слоем и одним нейроном является минимальной моделью, которая нам нужна для правильного разделения или классификации данных. Используя ту же архитектуру, что и для задачи AND, вы можете видеть, что модель сходится к правильному решению примерно после 300 итераций. В качестве примечания позвольте мне просто упомянуть, что количество итераций не важно для этого поста, поскольку мы просто ищем модель, которая может дать 100% точность.
model = models.Sequential()
model.add(layers.Dense(1, activation='sigmoid', input_shape=(2,)))
model.summary()
model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy'])
history = model.fit(or_data,
or_labels,
epochs=400,
batch_size=16,
verbose=0)
history_dict = history.history
history_dict.keys()
plt.figure(figsize=(20,10))
plt.subplot(121)
loss_values = history_dict['loss']
#val_loss_values = history_dict['val_loss']
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, loss_values, 'bo', label='Training loss')
#plt.plot(epochs, val_loss_values, 'b', label='Validation loss')
plt.title('Training loss',fontsize=20)
plt.xlabel('Epochs',fontsize=20)
plt.ylabel('Loss',fontsize=20)
plt.legend(fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.subplot(122)
acc_values = history_dict['acc']
#val_acc_values = history_dict['val_acc']
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, acc_values, 'bo', label='Training acc')
#plt.plot(epochs, val_acc_values, 'b', label='Validation acc')
plt.title('Training accuracy',fontsize=20)
plt.xlabel('Epochs',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.legend(fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()
________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_2 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
Проблема XOR
Проблема XOR немного сложнее. Опять же, точки данных сгруппированы вокруг четырех областей, и мы применяем логическую функцию XOR к каждой паре. Для логики XOR результатом является [0,0] = 0 и [1,1] = 0, но [0,1] = 1 и [1,0] = 1.
# Generate Data data1 = np.random.multivariate_normal( [0,0], sigma, npts) data2 = np.random.multivariate_normal( [0,1], sigma, npts) data3 = np.random.multivariate_normal( [1,0], sigma, npts) data4 = np.random.multivariate_normal( [1,1], sigma, npts) xor_data = np.concatenate((data1, data4, data2, data3)) xor_labels = np.concatenate((np.ones((2*npts)),np.zeros((2*npts)))) plt.figure(figsize=(20,10)) plt.scatter(xor_data[:,0][xor_labels==0], xor_data[:,1][xor_labels==0],c='b') plt.scatter(xor_data[:,0][xor_labels==1], xor_data[:,1][xor_labels==1],c='r') plt.title('XOR problem',fontsize=20) plt.xticks(fontsize=20) plt.yticks(fontsize=20) plt.grid('on') plt.show()
Проблема в том, что ни одна прямая линия не может правильно разделить данные. Однако, если в качестве первого шага мы изолируем [0,0] и [1,1] отдельно, используя две линейные линии, то в качестве второго шага мы можем применить функцию И к обоим разделениям, и перекрывающаяся область даст нам правильную классификацию. . Таким образом, необходимо двухэтапное решение: первый применяет две линейные линии, а второй объединяет два разделения с помощью логики И. Другими словами, минимальная сеть - это двухслойная нейронная сеть, где первая должна иметь два нейрона (т. Е. Две линейные линии), а вторая - только один (т. Е. С применением логики И и до того, как мы показали, что для этого требуется только один нейрон. ).
model = models.Sequential() model.add(layers.Dense(1, activation='sigmoid', input_shape=(2,))) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) history = model.fit(xor_data, xor_labels, epochs=400, batch_size=32, verbose=0) history_dict_10 = history.history model = models.Sequential() model.add(layers.Dense(1, activation='relu', input_shape=(2,))) model.add(layers.Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) history = model.fit(xor_data, xor_labels, epochs=400, batch_size=32, verbose=0) history_dict_11 = history.history 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 = model.fit(xor_data, xor_labels, epochs=400, batch_size=32, verbose=0) history_dict_21 = history.history history = model.fit(xor_data, xor_labels, epochs=400, batch_size=32, verbose=0) history_dict_41 = history.history
Чтобы проверить минимальный набор из двух слоев с двумя и одним нейронами (обозначен как 2_1), я также запустил еще две реализации Keras с меньшим количеством слоев и нейронов. Действительно, вы можете видеть, что модель 2_1 (два слоя с двумя и одним нейроном) сходится к правильному решению, в то время как модели 1_0 (один слой с одним нейроном) и 1_1 (два слоя с одним и одним нейроном) довольно хорошо аппроксимируют данные. но никогда не сходятся до 100% точности. Итак, хотя это не формальное доказательство, охватывающее все аспекты минимального набора, необходимого для классификации, оно должно дать вам достаточно практического понимания того, почему минимальный набор требует только двух слоев с двумя и одним нейроном.
plt.figure(figsize=(20,10)) plt.subplot(121) loss_values = history_dict_10['loss'] epochs = range(1, len(loss_values) + 1) plt.plot(epochs, history_dict_10['loss'], 'o', label='Training loss') plt.plot(epochs, history_dict_11['loss'], 'o', label='Training loss') plt.plot(epochs, history_dict_21['loss'], 'o', label='Training loss') plt.title('Training loss',fontsize=20) plt.xlabel('Epochs',fontsize=20) plt.ylabel('Loss',fontsize=20) plt.legend(['1_0','1_1','2_1','4_1'],fontsize=20) plt.xticks(fontsize=20) plt.yticks(fontsize=20) plt.subplot(122) acc_values = history_dict_10['loss'] epochs = range(1, len(loss_values) + 1) plt.plot(epochs, history_dict_10['acc'], 'o', label='Training loss') plt.plot(epochs, history_dict_11['acc'], 'o', label='Training loss') plt.plot(epochs, history_dict_21['acc'], 'o', label='Training loss') plt.title('Training accuracy',fontsize=20) plt.xlabel('Epochs',fontsize=20) plt.ylabel('Accuracy',fontsize=20) plt.legend(['1_0','1_1','2_1','4_1'],fontsize=20) plt.xticks(fontsize=20) plt.yticks(fontsize=20) plt.show()
Проблема двух лун
Проблема двух лун хорошо известна. Точки данных с изображением луны обращены друг к другу, но немного смещены по горизонтали. Найдите минутку и подумайте - какое минимальное количество нейронов и слоев необходимо, чтобы разделить луны?
import sklearn.datasets as sk
tm_data, tm_labels = sk.make_moons(n_samples=400, shuffle=True, noise=0.05, random_state=0)
print(tm_data.shape)
print(tm_labels.shape)
(400, 2)
(400,)
plt.figure(figsize=(20,10))
plt.scatter(tm_data[:,0][tm_labels==0],tm_data[:,1][tm_labels==0],c='b')
plt.scatter(tm_data[:,0][tm_labels==1],tm_data[:,1][tm_labels==1],c='r')
plt.title('Two-moon problem',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.grid('on')
plt.show()
Здесь я рекомендую вам взять лист бумаги и попытаться отделить красную луну от синей луны, используя только прямые линии. Какое минимальное количество прямых линий необходимо?
Подождите несколько минут, и вскоре вы обнаружите, что необходимы четыре линии: две линии на одном конце красной (или синей) луны и две на другом конце красной (или синей) луны. Итак, первый шаг - разместить четыре линии. Второй шаг - применить логику И к каждой паре ребер (т.е. взять области перекрытия) - это формирует два треугольника, каждый из которых покрывает примерно половину луны. Третий шаг - применить логику ИЛИ к двум треугольникам (т.е. взять объединение обоих треугольников), чтобы полностью отделить красную луну от синей. В целом, минимальная сеть использует три уровня: первый слой использует четыре нейрона, второй слой - два, а третий - один.
e_num=1000 bs_num=32 model = models.Sequential() model.add(layers.Dense(1, activation='sigmoid', input_shape=(2,))) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) history = model.fit(tm_data, tm_labels, epochs=e_num, batch_size=bs_num, verbose=0) history_dict = history.history 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 = model.fit(tm_data, tm_labels, epochs=e_num, batch_size=bs_num, verbose=0) history_dict_21 = history.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 = model.fit(tm_data, tm_labels, epochs=e_num, batch_size=bs_num, verbose=0) history_dict_41 = history.history model = models.Sequential() model.add(layers.Dense(2, activation='relu', input_shape=(2,))) model.add(layers.Dense(2, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) history = model.fit(tm_data, tm_labels, epochs=e_num, batch_size=bs_num, verbose=0) history_dict_221 = history.history model = models.Sequential() model.add(layers.Dense(3, activation='relu', input_shape=(2,))) model.add(layers.Dense(2, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) history = model.fit(tm_data, tm_labels, epochs=e_num, batch_size=bs_num, verbose=0) history_dict_321 = history.history model = models.Sequential() model.add(layers.Dense(4, activation='relu', input_shape=(2,))) model.add(layers.Dense(2, activation='relu')) model.add(layers.Dense(1, activation='sigmoid')) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) history = model.fit(tm_data, tm_labels, epochs=e_num, batch_size=bs_num, verbose=0) history_dict_421 = history.history
Как и раньше, чтобы проверить минимальный набор из трех слоев с четырьмя, двумя и одним нейронами (обозначен как 4_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1), я также запустил еще несколько реализаций Keras с меньшим количеством слоев и нейронов. Легко видеть, что только модель 4_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1 достигает 100% точности, в то время как сети 2_1, 4_1, 2_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1, 3_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1 хорошо аппроксимируют данные, но никогда не сходятся до 100% точности.
plt.figure(figsize=(20,10))
plt.subplot(121)
loss_values = history_dict_21['loss']
epochs = range(1, len(loss_values) + 1)
plt.plot(epochs, history_dict_21['loss'], 'o', label='Training loss')
plt.plot(epochs, history_dict_41['loss'], 'o', label='Training loss')
plt.plot(epochs, history_dict_221['loss'], 'o', label='Training loss')
plt.plot(epochs, history_dict_321['loss'], 'o', label='Training loss')
plt.plot(epochs, history_dict_421['loss'], 'o', label='Training loss')
plt.title('Training loss',fontsize=20)
plt.xlabel('Epochs',fontsize=20)
plt.ylabel('Loss',fontsize=20)
plt.legend(['2_1','4_1','2_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1','3_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1','4_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1'],fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.subplot(122)
acc_values = history_dict_21['loss']
epochs = range(1, len(acc_values) + 1)
plt.plot(epochs, history_dict_21['acc'], 'o', label='Training loss')
plt.plot(epochs, history_dict_41['acc'], 'o', label='Training loss')
plt.plot(epochs, history_dict_221['acc'], 'o', label='Training loss')
plt.plot(epochs, history_dict_321['acc'], 'o', label='Training loss')
plt.plot(epochs, history_dict_421['acc'], 'o', label='Training loss')
plt.title('Training accuracy',fontsize=20)
plt.xlabel('Epochs',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.legend(['2_1','4_1','2_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1','3_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1','4_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense_1 (Dense) (None, 1) 3
=================================================================
Total params: 3
Trainable params: 3
Non-trainable params: 0
_________________________________________________________________
1'],fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()
Подводя итог, можно сказать, что классификация с использованием нейронных сетей позволяет очень сложными способами составлять линейные функции и классифицировать невыпуклые данные. Хотя запускать модели и правильно классифицировать данные легко, мы хотим убедиться, что понимаем и хорошо интуитивно понимаем, что модель делает за сценой.
Здесь я выбрал несколько простых примеров и исследовал минимальное количество нейронов и слоев, необходимых для правильной классификации. Я показал, что однослойная система может представлять полуплоскости (то есть логику И или ИЛИ), что двухслойная система может классифицировать точки внутри любого количества произвольных линий (то есть логика XOR) и что 3- Система слоев может классифицировать невыпуклые формы (например, проблема двух лун).