Одно увлекательное знакомство с 1,5 млн фотографий квартир - Часть 1 - Проверка копцепции

Может закончиться хорошо или ничем - давайте узнаем.

Posted by yara_tchk on July 23, 2017

 

 

Оригинал статьи на английском

TLDR

 

 

Мне лично нравится начинать все мои статьи с таинственной картинки... но если вы читаете это, быть может, вы догадываетесь о том, что это такое…

 


 

0. Начало

Эта статья будет немного отличаться от остальных, так как я не могу опубликовать код и все детали.

Если говорить кратко, однажды я нашел доступ к серверу ssh в одном из моих чатов Telegram. У машины, на которой я работал, оказалось вот что:

  • Это виртуальная машина внутри контейнера VM-ware;
  • У нее графический процессор Titan X, проброшенный через хост-машину через kvm-qemu, что является своего рода передовой технологией (за ориентирами GPU для глубокого обучения сюда
  • (Также у нее есть вторая подобная GPU, не проброшенная);
  • На ней еще около 1,5 млн случайных фотографий квартир;>
  • Также я нашел эту статью

Отлично, что можно сделать с 1,5 млн фотографиями (очевидно, спаршенными с сайтов, с которыми я знаком - веб-сайты для бронирования / покупки квартир) и офигенной GPU? Конечно, натренировать нейронные сети (или намайнить криптовалют)! Но для этого понадобится разметка и какая-то конкретная цель. Цель может быть очевидной:

  • Распознавание настенных ковровых покрытий (его ворсейшейство);
  • Обнаружение архитектурного стиля;
  • Поиск похожих объектов / квартир;
  • Поиск объектов, таких как телевизоры, холодильники и т.д.;

 

Размеченного подмножества фотографий не было, поэтому я решил повозиться с ними и посмотреть, что я могу выжать за пару часов из набора данных, используя простые методы unsupervised learning и готовые предварительно обученные нейронные сети.

Прежде всего, я немного порыскал в интернете и нашел список потрясающих статей:


Топ моделей в соревнованиях imagenet за последние 5-10 лет


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

1. Предварительное исследование

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

currentDir = os.getcwd()images_path = '/mnt/dataset1/Images/'pic_path = '/mnt/dataset1/Images/Download/'pic_list = pd.read_csv(images_path+"downloadedImages.txt")pic_list.shape

Получается (1530210, 1) ~ 1,5 м снимков. Неплохо. Я не буду вдаваться в подробности моего анализа того, откуда пришли фотографии, каковы их размеры / форматы и т.д. Достаточно сказать, что это топ 10-15 вебсайтов по продаже/аренде квартир, фотографии - это, как правило, скучные фотографии русских квартир, и в основном изображения с качеством HD с посредственным освещением и файловыми форматами jpg и png.

Сделаем выборку фотографий для наших целей

pic_list[pic_list['2_split']=='folder1'].raw.values
pic_array = pic_list[pic_list['2_split']=='folder1].raw.values
pic_array = np.random.permutation(pic_array)
pic_array_shuf = pic_array[0:10000]
pic_array_shuf.size

Поскольку изображения расположены на внешнем смонтированном томе, давайте также скопируем их на наш виртуальный диск простым набором команд:

from shutil import copyfile
copyfile(orig_path, test_path+test_split)
for pic in log_progress(pic_array_shuf):
    orig_path = pic_path + pic
    copyfile(orig_path, test_path+pic.split('/')[5])
%ls -ls $test_path | wc -l

Теперь у нас есть 10 000 случайно выбранных снимков в нашей папке. Почему 10 000? Чтобы сделать все расчеты в разумные сроки. Это только исследование.

2. Наивный подход

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

Забыл упомянуть, чтобы сделать все в разумные сроки, вам нужно правильно настроить keras и GPU. Мой список зависимостей выглядит небольшим:

sudo passwd
sudo apt-get install tmux
sudo pip3 install jupyter_contrib_nbextensions
sudo pip3 install jupyter_nbextensions_configurator
sudo jupyter nbextensions_configurator enable --user
sudo pip3 install numpy 
sudo pip3 install matplotlib 
sudo pip3 install keras
sudo pip3 install tensorflow 
sudo pip3 install sklearn
sudo apt install glances
cd ~/
cd flat-nn/
jupyter notebook --no-browser --port=8888 --ip=server-ip

Но для правильной работы keras на графическом процессоре также необходимо настроить драйверы CUDA и наколдовать некоторые конфиги. Я разместил подборку здесь в моем телеграм канале. Первоначально эта конфигурация была взята из форумов fast.ai. К ключевым частям этой конфигурации относятся (сильно зависит от системы, используйте с осторожностью, при необходимости замените pip на pip3):

# install and configure theano
pip install theano
echo "[global]
device = gpu
floatX = float32
[cuda]
root = /usr/local/cuda" > ~/.theanorc
# install and configure keras
pip install keras==1.2.2
mkdir ~/.keras
echo '{
    "image_dim_ordering": "th",
    "epsilon": 1e-07,
    "floatx": "float32",
    "backend": "theano"
}' > ~/.keras/keras.json
# install cudnn libraries
wget "http://platform.ai/files/cudnn.tgz" -O "cudnn.tgz"
tar -zxf cudnn.tgz
cd cuda
sudo cp lib64/* /usr/local/cuda/lib64/
sudo cp include/* /usr/local/cuda/include/

Установив все, что нужно, начнем

# Extract features from an arbitrary intermediate layer with VGG19
from keras.applications.vgg19 import VGG19
from keras.preprocessing import image
from keras.applications.vgg19 import preprocess_input
from keras.models import Model
import numpy as np

Если все сделано верно, вы получите сообщение вроде такого:

Using gpu device 0: GeForce GTX TITAN X (CNMeM is disabled, cuDNN not available)

Посмотрим, что внутри модели VGG-19

base_model = VGG19(weights='imagenet')
base_model.summary()

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

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 224, 224, 64)      36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 112, 112, 64)      0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 112, 112, 128)     73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 112, 112, 128)     147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 56, 56, 128)       0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 56, 56, 256)       295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_conv4 (Conv2D)        (None, 56, 56, 256)       590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 28, 28, 256)       0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 28, 28, 512)       1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_conv4 (Conv2D)        (None, 28, 28, 512)       2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 14, 14, 512)       0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_conv4 (Conv2D)        (None, 14, 14, 512)       2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 7, 7, 512)         0         
_________________________________________________________________
flatten (Flatten)            (None, 25088)             0         
_________________________________________________________________
fc1 (Dense)                  (None, 4096)              102764544 
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
predictions (Dense)          (None, 1000)              4097000   
=================================================================
Total params: 143,667,240
Trainable params: 143,667,240
Non-trainable params: 0
_________________________________________________________________

 

base_model = VGG19(weights='imagenet')
model = Model(inputs=base_model.input, outputs=base_model.get_layer('fc2').output)
images_path = '/mnt/dataset1/Images/'
pic_path = '/amnt/dataset1/Images/Download/'
test_path = currentDir+'/flat_pics/'
def predict_batches(model, path, batch_size=8):
    test_batches = get_batches(
        path,
        shuffle=False,
        batch_size=batch_size,
        class_mode=None,
        imageSizeTuple = (224,224)
    )
    return\
        test_batches,\
        model.predict_generator(test_batches, test_batches.samples/batch_size)
test_batches, preds = predict_batches(model, test_path, batch_size=128)
preds.shape

 

Итак, мы получили матрицу (10000, 4096), заполненную предсказаниями VGG-19 (для этой цели можно взять модель imagenet). В идеале мы должны сделать следующее

  • Сложить две нейронные сети вместе, используя функциональное API keras;
  • Настроить все слои, кроме новых слоев нашей новой нейросети, как необучаемые;
  • Оставить тренироваться сиамскую нейросеть на день-неделю и посмотреть результаты;

... но у нас нет разметки, помните? Итак, сделаем все возможное, используем версию sklearn алгоритма affinity propagation для вычисления расстояний между 4000+ переменными от VGG-19. Это очень неэффективно, потому что оно не распараллеливается и не использует GPU. Конечно, мы могли бы изменить этот пример и рассчитать все за секунды, но можно просто полениться и подождать пару минут, если не хочется слишком много думать =). Также заполним диагональные элементы матрицы некоторым случайным значением (изображение больше всего похоже на себя конечно же, а нам это свойство будет мешать и поэтому не нужно).

from sklearn.cluster import AffinityPropagation
from sklearn import metrics
af = AffinityPropagation().fit(preds)
np.save('/mnt/virtfs/home/av/flat-nn/flat-nn/aff_matrix', af.affinity_matrix_)
aff_matrix = af.affinity_matrix_
np.fill_diagonal(aff_matrix, -9000)
most_different = aff_matrix.argmin(axis=1)
most_similar = aff_matrix.argmax(axis=1)
pic_filenames =test_batches.filenames

 

3. Результат

Посмотрим, что дает нам наш наивный подход! Не забывайте, что это было просто исследование совсем без разметки. Также обратите внимание, что я не делал никакого правильного анализа размера изображения и настройки - я просто скормил все keras как есть.

# utility for easy plot generation
def plots(ims,
          figsize=(12,6),
          rows=1,
          interp=False,
          titles=None):
    if type(ims[0]) is np.ndarray:
        ims = np.array(ims).astype(np.uint8)
        if (ims.shape[-1] != 3):
            ims = ims.transpose((0,2,3,1))
    f = plt.figure(figsize=figsize)
    for i in range(len(ims)):
        sp = f.add_subplot(rows, len(ims)//rows, i+1)
        if titles is not None:
            sp.set_title(titles[i], fontsize=18)
        plt.imshow(ims[i], interpolation=None if interp else 'none')

 

def plots_idx(idx, titles, path, filenames, figsize):
    plots([image.load_img(path + filenames[i]) for i in idx], titles=titles, figsize = figsize, rows=1)
for pic in np.arange(9):
    idx = [pic,most_different[pic],most_similar[pic]]
    titles = ['pic','most_different', 'most_similar']
    path = test_path
    filenames = pic_filenames
    
    plots_idx (idx,titles,path,filenames, figsize = (10,20))

 

Немного случайных примеров ниже

Помимо ужасного совкового унитаза, который случайно выскакивает тут и там, мы видим, что этот наивный подход:

  • Обнаруживает копии одного и того же изображения;
  • Может различать углы;
  • Может отличать наружные фотографии от фотографий в помещении;
  • Можно отличить городские пейзажи и море;
  • Может различать схемы квартир и этажей;

 

Неплохо для абсолютно несвязанного алгоритма (imagenet) без обработки данных и / или обучения на случайных изображениях! Представьте себе, каковы были бы результаты, если мы натренировали классификатор сами.

4. Копнем глубже

Используем t-sne проекцию (или простой PCA), чтобы спроектировать наши данные 4000-ой размерности в 2-мерную плоскость

%%time
from sklearn.manifold import TSNE
tsne = TSNE(random_state=17)
X_tsne = tsne.fit_transfor
plt.figure(figsize=(12,10))
plt.scatter(X_tsne[:, 0], X_tsne[:, 1],  
            edgecolor='none', alpha=0.7, s=40,
            cmap=plt.cm.get_cmap('nipy_spectral', 10))
plt.title('Flats VGG-19 last FCN layer. t-SNE projection')

Легко заметить несколько кластеров

Что самое прекрасное, так это то, что разные кластеры соответствуют разным типам изображений.

Схемы

Экстерьеры (Многоквартирные дома)

Интерьеры

Неплохо для такого исследования, не так ли?