Хакнуть Tinder: Как научить ИИ свайпать за вас

Auto-tinder, созданный на основе Tensorflow и Python 3, может изучить ваши предпочтения в противоположном поле, а затем лайкать за вас профили в Tinder, которые могли бы вам понравиться. 

В этой статье я планирую рассказать о шагах, которые потребовались мне, чтобы создать Auto-tinder, а именно:

  • Анализ веб-страницы Tinder на предмет внутренних вызовов API, реконструкция этих вызовов в Postman, и анализ их содержимого
  • Создание класса-обертки для API на Python, чтобы дальше использовать Tinder API для лайков\дизлайков\мэтчей
  • Загрузка изображений пользователей, которые находятся неподалеку
  • Создание простого классификатора для пометки изображений
  • Разработка препроцессора, использующего Tensorflow API для распознавания объектов, чтобы определять расположение человека на фото
  • Переобучение сверточной нейронной сети inceptionv3 на классифицированных данных
  • И, наконец, использование классификатора вместе с оберткой Tinder API, чтобы они свайпали за нас

Шаг 0: Мотивация и дисклеймер

Auto Tinder — это проект, который был создан исключительно в образовательных целях, ну и забавы ради. Он не должен быть использован во вред кому-либо или для того, чтобы заспамить платформу. Кроме того, не используйте скрипты Auto-Tinder под вашим профилем, поскольку они, безусловно, нарушают условия использования Tinder.

Я написал эту программу в основном по двум причинам:

1. Потому что могу и потому что это было весело. 🙂

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

3. Ну и третья, абсолютно вымышленная причина: я ленивый человек, так почему бы не потратить 15 часов, чтобы написать Auto-tinder, плюс 5 часов, чтобы классифицировать изображения, чтобы в итоге сэкономить несколько часов, которые мне пришлось бы потратить на то, чтобы сидеть в Tinder самому. Звучит как разумный план!

Step 1: Анализ API

Первый шаг — выяснить, как приложение Tinder взаимодействует с бэкендом. Поскольку у Tinder есть веб-версия, это достаточно просто: идем на tinder.com, открываем Chrome Devtools и смотрим на сетевой протокол.

Содержимое, показанное на картинке выше, было получено по запросу https://api.gotinder.com/v2/recs/corehttps://api.gotinder.com/v2/recs/core, который был сделан при загрузке страницы tinder.com. Понятно, что у Tinder есть какой-то внутренний API, который они используют для связи между фронт-эндом и бэкэндом.

После анализа содержимого /recs/core становится ясно, что эта конечная точка API возвращает список профилей пользователей, которые находятся поблизости.

Данные включают (среди многих других полей) следующую информацию:

{
    "meta": {
        "status": 200
    },
    "data": {
        "results": [
            {
                "type": "user",
                "user": {
                 "_id": "4adfwe547s8df64df",
                    "bio": "19y.",
                    "birth_date": "1997-17-06T18:21:44.654Z",
                    "name": "Anna",
                    "photos": [
                        {
                            "id": "879sdfert-lskdföj-8asdf879-987sdflkj",
                            "crop_info": {
                                "user": {
                                    "width_pct": 1,
                                    "x_offset_pct": 0,
                                    "height_pct": 0.8,
                                    "y_offset_pct": 0.08975463
                                },
                                "algo": {
                                    "width_pct": 0.45674357,
                                    "x_offset_pct": 0.984341657,
                                    "height_pct": 0.234165403,
                                    "y_offset_pct": 0.78902343
                                },
                                "processed_by_bullseye": true,
                                "user_customized": false
                            },
                            "url": "https://images-ssl.gotinder.com/4adfwe547s8df64df/original_879sdfert-lskdföj-8asdf879-987sdflkj.jpeg",
                            "processedFiles": [
                                {
                                    "url": "https://images-ssl.gotinder.com/4adfwe547s8df64df/640x800_879sdfert-lskdföj-8asdf879-987sdflkj.jpg",
                                    "height": 800,
                                    "width": 640
                                },
                                {
                                    "url": "https://images-ssl.gotinder.com/4adfwe547s8df64df/320x400_879sdfert-lskdföj-8asdf879-987sdflkj.jpg",
                                    "height": 400,
                                    "width": 320
                                },
                                {
                                    "url": "https://images-ssl.gotinder.com/4adfwe547s8df64df/172x216_879sdfert-lskdföj-8asdf879-987sdflkj.jpg",
                                    "height": 216,
                                    "width": 172
                                },
                                {
                                    "url": "https://images-ssl.gotinder.com/4adfwe547s8df64df/84x106_879sdfert-lskdföj-8asdf879-987sdflkj.jpg",
                                    "height": 106,
                                    "width": 84
                                }
                            ],
                            "last_update_time": "2019-10-03T16:18:30.532Z",
                            "fileName": "879sdfert-lskdföj-8asdf879-987sdflkj.webp",
                            "extension": "jpg,webp",
                            "webp_qf": [
                                75
                            ]
                        }
                    ],
                    "gender": 1,
                    "jobs": [],
                    "schools": [],
                    "show_gender_on_profile": false
                },
                "facebook": {
                    "common_connections": [],
                    "connection_count": 0,
                    "common_interests": []
                },
                "spotify": {
                    "spotify_connected": false
                },
                "distance_mi": 1,
                "content_hash": "slkadjfiuwejsdfuzkejhrsdbfskdzufiuerwer",
                "s_number": 9876540657341,
                "teaser": {
                    "string": ""
                },
                "teasers": [],
                "snap": {
                    "snaps": []
                }
            }
        ]
    }
}

Несколько интересных моментов (обратите внимание, что я изменил все данные, чтобы не нарушать конфиденциальность этого пользователя):

  • Все изображения являются общедоступными. Если вы скопируете URL-адрес изображения и откроете его в другом окне, оно все равно мгновенно загрузится, а значит все изображения пользователей Tinder находятся в публичном доступе, и их может видеть кто угодно.
  • Оригиналы фотографий, доступные через API, имеют очень высокое разрешение. Если вы загрузите фотографию в Tinder, то они уменьшат ее для использования в приложении, но оригинал остается храниться на публично доступных серверах.
  • Даже если вы не выберете «show_gender_on_profile», через API все равно можно увидеть ваш пол («gender»: 1, где 1 = женщина, 0 = мужчина).
  • Если вы последовательно отправляете несколько запросов в API Tinder, вы всегда получаете разные результаты (например, разные профили). Поэтому мы можем просто несколько раз вызывать эту конечную точку, чтобы получить кучу изображений, которые мы позже сможем использовать для обучения нашей нейронной сети.

Анализируя заголовки, можно быстро найти наш личный ключ к API: X-Auth-Token.

Скопировав этот токен в Postman, можно увидеть, что мы действительно можем свободно общаться с API Tinder, используя только правильный URL и токен авторизации.

Немного пошарясь по веб-приложению Tinder, я быстро нашел все нужные нам конечные точки:

Step 2: Создание обертки для API

Итак, давайте перейдем к коду. Для удобства мы будем использовать библиотеку Python Requests для взаимодействия с API и написания обертки для API.

Мы напишем класс Person, который будет принимать определяющий пользователя ответ API и в целом позволит производить некоторые взаимодействовать с API Tinder.

Давайте начнем с написания класса. Он должен получать данные от API и сохранять все нужные данные во внутренних переменных. Кроме того, он должен иметь некоторые базовые функции, такие как “like” или “dislike»”, которые делают запрос к API, чтобы мы могли лайкать интересный нам профиль, используя команду “some_person.like ()”.

import datetime
from geopy.geocoders import Nominatim

TINDER_URL = "https://api.gotinder.com"
geolocator = Nominatim(user_agent="auto-tinder")
PROF_FILE = "./images/unclassified/profiles.txt"

class Person(object):

    def __init__(self, data, api):
        self._api = api

        self.id = data["_id"]
        self.name = data.get("name", "Unknown")

        self.bio = data.get("bio", "")
        self.distance = data.get("distance_mi", 0) / 1.60934

        self.birth_date = datetime.datetime.strptime(data["birth_date"], '%Y-%m-%dT%H:%M:%S.%fZ') if data.get(
            "birth_date", False) else None
        self.gender = ["Male", "Female", "Unknown"][data.get("gender", 2)]

        self.images = list(map(lambda photo: photo["url"], data.get("photos", [])))

        self.jobs = list(
            map(lambda job: {"title": job.get("title", {}).get("name"), "company": job.get("company", {}).get("name")}, data.get("jobs", [])))
        self.schools = list(map(lambda school: school["name"], data.get("schools", [])))

        if data.get("pos", False):
            self.location = geolocator.reverse(f'{data["pos"]["lat"]}, {data["pos"]["lon"]}')


    def __repr__(self):
        return f"{self.id}  -  {self.name} ({self.birth_date.strftime('%d.%m.%Y')})"


    def like(self):
        return self._api.like(self.id)

    def dislike(self):
        return self._api.dislike(self.id)
На самом деле наша API-обертка - это не более, чем причудливый способ вызова Tinder API с использованием нашего класса:
import requests

TINDER_URL = "https://api.gotinder.com"

class tinderAPI():

    def __init__(self, token):
        self._token = token

    def profile(self):
        data = requests.get(TINDER_URL + "/v2/profile?include=account%2Cuser", headers={"X-Auth-Token": self._token}).json()
        return Profile(data["data"], self)

    def matches(self, limit=10):
        data = requests.get(TINDER_URL + f"/v2/matches?count={limit}", headers={"X-Auth-Token": self._token}).json()
        return list(map(lambda match: Person(match["person"], self), data["data"]["matches"]))

    def like(self, user_id):
        data = requests.get(TINDER_URL + f"/like/{user_id}", headers={"X-Auth-Token": self._token}).json()
        return {
            "is_match": data["match"],
            "liked_remaining": data["likes_remaining"]
        }

    def dislike(self, user_id):
        requests.get(TINDER_URL + f"/pass/{user_id}", headers={"X-Auth-Token": self._token}).json()
        return True

    def nearby_persons(self):
        data = requests.get(TINDER_URL + "/v2/recs/core", headers={"X-Auth-Token": self._token}).json()
        return list(map(lambda user: Person(user["user"], self), data["data"]["results"]))

Теперь мы можем использовать API, чтобы находить пользователей поблизости, смотреть их профили и даже лайкать их. Для этого замените YOUR-API-TOKEN на X-Auth-Token, который вы нашли, используя Chrome Devtools.

if __name__ == "__main__":
    token = "YOUR-API-TOKEN"
    api = tinderAPI(token)

    while True:
        persons = api.nearby_persons()
        for person in persons:
            print(person)
            # person.like()

Шаг 3: Загрузка фотографий пользователей

Далее мы хотим автоматически загрузить какое-то количество фотографий пользователей поблизости, которые мы будем использовать для обучения нашего ИИ. Под «каким-то количеством» я имею в виду 1500-2500 фотографий.

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

# At the top of auto_tinder.py
PROF_FILE = "./images/unclassified/profiles.txt"

# inside the Person-class
    def download_images(self, folder=".", sleep_max_for=0):
        with open(PROF_FILE, "r") as f:
            lines = f.readlines()
            if self.id in lines:
                return
        with open(PROF_FILE, "a") as f:
            f.write(self.id+"\r\n")
        index = -1
        for image_url in self.images:
            index += 1
            req = requests.get(image_url, stream=True)
            if req.status_code == 200:
                with open(f"{folder}/{self.id}_{self.name}_{index}.jpeg", "wb") as f:
                    f.write(req.content)
            sleep(random()*sleep_max_for)

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

Мы запишем ID всех пользователей в файл с именем “profiles.txt”. Если сначала мы будем проверять есть ли конкретный человек в этом файле, то мы сможем пропускать профили, которые нам уже попадались. Этим мы гарантируем, что нам не придется классифицировать одних и тех же людей несколько раз (позже вы поймете почему это может быть проблемой).

Теперь мы можем просто итерировать по профилям людей, находящихся неподалеку, и загружать их изображения в папку “unclassified”.

if __name__ == "__main__":
    token = "YOUR-API-TOKEN"
    api = tinderAPI(token)

    while True:
        persons = api.nearby_persons()
        for person in persons:
            person.download_images(folder="./images/unclassified", sleep_max_for=random()*3)
            sleep(random()*10)
        sleep(random()*10)

Теперь мы можем просто запустить этот скрипт и дать ему поработать несколько часов, чтобы получить нужное количество фотографий. Если вы пользователь Tinder PRO, то обновляйте свое местоположение время от времени, чтобы получать новые профили.

Шаг 4: Классификация фотографий

Теперь, когда у нас есть куча изображений, давайте сделаем очень простой и “страшненький” классификатор.

Он просто будет проходить циклом по всем фотографиям в нашей папке “unclassified” и открывать их в GUI. Щелкнув правой кнопкой мыши по человеку, мы будем помечать его как непонравившегося, а щелчок левой кнопкой мыши будет значить, что человек нам понравился.

Позже этот выбор будет отображаться в имени файла: 4tz3kjldfj3482.jpg будет переименован в 1_4tz3kjldfj3482.jpg, если мы лайкнем фотографию или 0_4tz3kjldfj3482.jpg в противном случае.

Давайте используем tkinter, чтобы быстро создать этот GUI:

from os import listdir, rename
from os.path import isfile, join
import tkinter as tk
from PIL import ImageTk, Image

IMAGE_FOLDER = "./images/unclassified"

images = [f for f in listdir(IMAGE_FOLDER) if isfile(join(IMAGE_FOLDER, f))]
unclassified_images = filter(lambda image: not (image.startswith("0_") or image.startswith("1_")), images)
current = None

def next_img():
    global current, unclassified_images
    try:
        current = next(unclassified_images)
    except StopIteration:
        root.quit()
    print(current)
    pil_img = Image.open(IMAGE_FOLDER+"/"+current)
    width, height = pil_img.size
    max_height = 1000
    if height > max_height:
        resize_factor = max_height / height
        pil_img = pil_img.resize((int(width*resize_factor), int(height*resize_factor)), resample=Image.LANCZOS)
    img_tk = ImageTk.PhotoImage(pil_img)
    img_label.img = img_tk
    img_label.config(image=img_label.img)

def positive(arg):
    global current
    rename(IMAGE_FOLDER+"/"+current, IMAGE_FOLDER+"/1_"+current)
    next_img()

def negative(arg):
    global current
    rename(IMAGE_FOLDER + "/" + current, IMAGE_FOLDER + "/0_" + current)
    next_img()


if __name__ == "__main__":

    root = tk.Tk()

    img_label = tk.Label(root)
    img_label.pack()
    img_label.bind("<Button-1>", positive)
    img_label.bind("<Button-3>", negative)

    btn = tk.Button(root, text='Next image', command=next_img)

    next_img() # load first image

    root.mainloop()

Мы загружаем все неклассифицированные фотографии в список «unclassified_images», открываем tkinter, передаем ему первое изображение вызвав next_img () и меняем размер изображения, чтобы оно поместилось на экране. Затем мы задаем два щелчка, левой и правой кнопкой мыши, и вызываем функции “positive” и “negative”, которые переименовывают изображения в зависимости от того какой кнопкой мыши был сделан щелчок, а потом показывают следующую фотографию.

Шаг 5: Создание препроцессора, чтобы обрезать фотографию вокруг человека

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

1. Размер базы данных: наша база относительно мала. Мы имеем дело с плюс-минус двумя тысячами фотографий, что считается очень маленьким количеством для данных такой сложности (RGB изображения с высоким разрешением).

2. Разнообразие данных: некоторые фотографии сделаны со спины, иногда на фото есть только лицо, иногда людей на фото вообще нет.

3. Зашумленность данных: на большинстве фотографий есть не только сам человек, но и фон, который может мешать ИИ.

Мы будем бороться с этими проблемами двумя способами:

1. Преобразовывая фотографии из цветных в черно-белые, чтобы уменьшить объем информации, который должен обработать ИИ, в 3 раза (от RGB до G)

2. Вырезая только ту часть фотографии, где находится человек, и ничего больше

Первое очень просто сделать с использованием Pillow, в котором мы можем открыть наше изображение и перевести его в оттенки серого.

Второй способ мы реализуем с помощью Tensorflow API для распознавания объектов и сверточной сети MobileNet, предварительно обученной на базе данных COCO.

Наш скрипт для распознавания людей на фото будет состоять из четырех частей:

Часть 1: Открыть предобученную сеть в виде графа Tensorflow

В моем репозитории на Github вы найдете файл .pb с предобученной сетью MobileNet.

Давайте откроем его как граф Tensorflow:

import tensorflow as tf

def open_graph():
    detection_graph = tf.Graph()
    with detection_graph.as_default():
        od_graph_def = tf.GraphDef()
        with tf.gfile.GFile('ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb', 'rb') as fid:
            serialized_graph = fid.read()
            od_graph_def.ParseFromString(serialized_graph)
            tf.import_graph_def(od_graph_def, name='')
    return detection_graph

Part 2: Загрузка фотографий в виде массивов numpy

Для работы с изображениями мы используем Pillow. Так как Tensorflow для работы нужны numpy-массивы, давайте напишем небольшую функцию, которая преобразует изображения, обработанные в Pillow, в такие массивы:

import numpy as np

def load_image_into_numpy_array(image):
    (im_width, im_height) = image.size
    return np.array(image.getdata()).reshape(
        (im_height, im_width, 3)).astype(np.uint8)

Часть 3: Вызов API для распознавания

Следующая функция берет изображение и граф Tensorflow, запускает процесс распознавания, а потом возвращает всю информацию о распознанных классах (типах объектов), ограничивающие рамки для них и оценки (уверенность в том, что объект был распознан верно).

import numpy as np
from object_detection.utils import ops as utils_ops
import tensorflow as tf

def run_inference_for_single_image(image, sess):
    ops = tf.get_default_graph().get_operations()
    all_tensor_names = {output.name for op in ops for output in op.outputs}
    tensor_dict = {}
    for key in [
        'num_detections', 'detection_boxes', 'detection_scores',
        'detection_classes', 'detection_masks'
    ]:
        tensor_name = key + ':0'
        if tensor_name in all_tensor_names:
            tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(
                tensor_name)
    if 'detection_masks' in tensor_dict:
        # The following processing is only for single image
        detection_boxes = tf.squeeze(tensor_dict['detection_boxes'], [0])
        detection_masks = tf.squeeze(tensor_dict['detection_masks'], [0])
        # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size.
        real_num_detection = tf.cast(tensor_dict['num_detections'][0], tf.int32)
        detection_boxes = tf.slice(detection_boxes, [0, 0], [real_num_detection, -1])
        detection_masks = tf.slice(detection_masks, [0, 0, 0], [real_num_detection, -1, -1])
        detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks(
            detection_masks, detection_boxes, image.shape[1], image.shape[2])
        detection_masks_reframed = tf.cast(
            tf.greater(detection_masks_reframed, 0.5), tf.uint8)
        # Follow the convention by adding back the batch dimension
        tensor_dict['detection_masks'] = tf.expand_dims(
            detection_masks_reframed, 0)
    image_tensor = tf.get_default_graph().get_tensor_by_name('image_tensor:0')

    # Run inference
    output_dict = sess.run(tensor_dict,
                           feed_dict={image_tensor: image})

    # all outputs are float32 numpy arrays, so convert types as appropriate
    output_dict['num_detections'] = int(output_dict['num_detections'][0])
    output_dict['detection_classes'] = output_dict[
        'detection_classes'][0].astype(np.int64)
    output_dict['detection_boxes'] = output_dict['detection_boxes'][0]
    output_dict['detection_scores'] = output_dict['detection_scores'][0]
    if 'detection_masks' in output_dict:
        output_dict['detection_masks'] = output_dict['detection_masks'][0]
    return output_dict

Часть 4: Собираем все вместе и находим человека на фото

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

import numpy as np
from PIL import Image

PERSON_CLASS = 1
SCORE_THRESHOLD = 0.5

def get_person(image_path, sess):
    img = Image.open(image_path)
    image_np = load_image_into_numpy_array(img)
    image_np_expanded = np.expand_dims(image_np, axis=0)
    output_dict = run_inference_for_single_image(image_np_expanded, sess)

    persons_coordinates = []
    for i in range(len(output_dict["detection_boxes"])):
        score = output_dict["detection_scores"][i]
        classtype = output_dict["detection_classes"][i]
        if score > SCORE_THRESHOLD and classtype == PERSON_CLASS:
            persons_coordinates.append(output_dict["detection_boxes"][i])

    w, h = img.size
    for person_coordinate in persons_coordinates:
        cropped_img = img.crop((
            int(w * person_coordinate[1]),
            int(h * person_coordinate[0]),
            int(w * person_coordinate[3]),
            int(h * person_coordinate[2]),
        ))
        return cropped_img
    return None

Часть 5: Размещение фото по папкам в зависимости от классификации

В качестве последнего шага мы напишем скрипт, который будет перебирать все фото в папке “unclassified”, проверять, есть ли у них закодированная в имени метка, а потом копировать изображение в соответствующую папку с применением предварительной обработки, которую мы написали ранее:

import os
import person_detector
import tensorflow as tf

IMAGE_FOLDER = "./images/unclassified"
POS_FOLDER = "./images/classified/positive"
NEG_FOLDER = "./images/classified/negative"


if __name__ == "__main__":
    detection_graph = person_detector.open_graph()

    images = [f for f in os.listdir(IMAGE_FOLDER) if os.path.isfile(os.path.join(IMAGE_FOLDER, f))]
    positive_images = filter(lambda image: (image.startswith("1_")), images)
    negative_images = filter(lambda image: (image.startswith("0_")), images)

    with detection_graph.as_default():
        with tf.Session() as sess:

            for pos in positive_images:
                old_filename = IMAGE_FOLDER + "/" + pos
                new_filename = POS_FOLDER + "/" + pos[:-5] + ".jpg"
                if not os.path.isfile(new_filename):
                    img = person_detector.get_person(old_filename, sess)
                    if not img:
                        continue
                    img = img.convert('L')
                    img.save(new_filename, "jpeg")

            for neg in negative_images:
                old_filename = IMAGE_FOLDER + "/" + neg
                new_filename = NEG_FOLDER + "/" + neg[:-5] + ".jpg"
                if not os.path.isfile(new_filename):
                    img = person_detector.get_person(old_filename, sess)
                    if not img:
                        continue
                    img = img.convert('L')
                    img.save(new_filename, "jpeg")


Когда мы запускаем этот скрипт, все помеченные изображения обрабатываются и перемещаются в соответствующие подпапки в папке “classified”.

Шаг 6: Переобучение inceptionv3 и написание классификатора

Для переобучения мы будем просто использовать скрипт Tensorflow retrain.py и модель inceptionv3.

Вызовите скрипт в корневой директории вашего проекта со следующими параметрами:

python retrain.py --bottleneck_dir=tf/training_data/bottlenecks --model_dir=tf/training_data/inception --summaries_dir=tf/training_data/summaries/basic --output_graph=tf/training_output/retrained_graph.pb --output_labels=tf/training_output/retrained_labels.txt --image_dir=./images/classified --how_many_training_steps=50000 --testing_percentage=20 --learning_rate=0.001

Обучение занимает около 15 минут на GTX 1080Ti. Финальная точность составила около 80% для моего набора данных, но она сильно зависит от качества ваших входных данных и меток.

Результатом этого процесса будет переобученная модель inceptionV3 в файле «tf/training_output/retrained_graph.pb». Теперь мы должны написать класс Classifier, который будет использовать новые веса в графе Tensorflow для прогнозирования верной классификации.

Давайте напишем Classifier-Class, который открывает сессию, в которую передается граф, и имеет метод «classify», который получает фото и возвращает словарь с оценками вероятности того, что фото должно быть помечено “positive” или “negative”.

Класс принимает на вход путь к графу и путь к файлу с метками — оба находятся в нашей папке «tf/training_output/». Мы создаем вспомогательные функции для преобразования файла с фото в тензор, который мы сможем подать в наш граф, вспомогательную функцию для загрузки графа и меток, а также важную маленькую функцию, которая закроет граф после того, как мы закончим работу с ним.

import numpy as np
import tensorflow as tf

class Classifier():
    def __init__(self, graph, labels):

        self._graph = self.load_graph(graph)
        self._labels = self.load_labels(labels)

        self._input_operation = self._graph.get_operation_by_name("import/Placeholder")
        self._output_operation = self._graph.get_operation_by_name("import/final_result")

        self._session = tf.Session(graph=self._graph)

    def classify(self, file_name):
        t = self.read_tensor_from_image_file(file_name)

        # Open up a new tensorflow session and run it on the input
        results = self._session.run(self._output_operation.outputs[0], {self._input_operation.outputs[0]: t})
        results = np.squeeze(results)

        # Sort the output predictions by prediction accuracy
        top_k = results.argsort()[-5:][::-1]

        result = {}
        for i in top_k:
            result[self._labels[i]] = results[i]

        # Return sorted result tuples
        return result

    def close(self):
        self._session.close()


    @staticmethod
    def load_graph(model_file):
        graph = tf.Graph()
        graph_def = tf.GraphDef()
        with open(model_file, "rb") as f:
            graph_def.ParseFromString(f.read())
        with graph.as_default():
            tf.import_graph_def(graph_def)
        return graph

    @staticmethod
    def load_labels(label_file):
        label = []
        proto_as_ascii_lines = tf.gfile.GFile(label_file).readlines()
        for l in proto_as_ascii_lines:
            label.append(l.rstrip())
        return label

    @staticmethod
    def read_tensor_from_image_file(file_name,
                                    input_height=299,
                                    input_width=299,
                                    input_mean=0,
                                    input_std=255):
        input_name = "file_reader"
        file_reader = tf.read_file(file_name, input_name)
        image_reader = tf.image.decode_jpeg(
            file_reader, channels=3, name="jpeg_reader")
        float_caster = tf.cast(image_reader, tf.float32)
        dims_expander = tf.expand_dims(float_caster, 0)
        resized = tf.image.resize_bilinear(dims_expander, [input_height, input_width])
        normalized = tf.divide(tf.subtract(resized, [input_mean]), [input_std])
        sess = tf.Session()
        result = sess.run(normalized)
        return result

Шаг 7: Применение скриптов в деле

Теперь, когда у нас есть классификатор, давайте расширим класс Person с помощью функции «predict_likeliness», которая использует классификатор, чтобы проверить следует ли лайкать этого пользователя или нет.

# In the Person class

    def predict_likeliness(self, classifier, sess):
        ratings = []
        for image in self.images:
            req = requests.get(image, stream=True)
            tmp_filename = f"./images/tmp/run.jpg"
            if req.status_code == 200:
                with open(tmp_filename, "wb") as f:
                    f.write(req.content)
            img = person_detector.get_person(tmp_filename, sess)
            if img:
                img = img.convert('L')
                img.save(tmp_filename, "jpeg")
                certainty = classifier.classify(tmp_filename)
                pos = certainty["positive"]
                ratings.append(pos)
        ratings.sort(reverse=True)
        ratings = ratings[:5]
        if len(ratings) == 0:
            return 0.001
        return ratings[0]*0.6 + sum(ratings[1:])/len(ratings[1:])*0.4

Теперь давайте соберем все кусочки головоломки вместе.

Во-первых, давайте инициализируем Tinder API с помощью нашего токена. Затем мы создадим сессию для нашего классифицирующего графа Tensorflow с использованием переобученной модели и меток. Затем мы получим данные пользователей поблизости и увидим какова вероятность того, что тот или иной пользователь мне понравится.

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

Профили всех людей, у которых оценка вероятности понравиться мне равна 0.8 или больше, свайпаются вправо, а все остальные — влево. 

Я сделал скрипт, который дает Auto-Tinder работать в течение 2 часов после его запуска.

from likeliness_classifier import Classifier
import person_detector
import tensorflow as tf
from time import time

if __name__ == "__main__":
    token = "YOUR-API-TOKEN"
    api = tinderAPI(token)

    detection_graph = person_detector.open_graph()
    with detection_graph.as_default():
        with tf.Session() as sess:

            classifier = Classifier(graph="./tf/training_output/retrained_graph.pb",
                                    labels="./tf/training_output/retrained_labels.txt")

            end_time = time() + 60*60*2
            while time() < end_time:
                try:
                    persons = api.nearby_persons()
                    pos_schools = ["Universität Zürich", "University of Zurich", "UZH"]

                    for person in persons:
                        score = person.predict_likeliness(classifier, sess)

                        for school in pos_schools:
                            if school in person.schools:
                                print()
                                score *= 1.2

                        print("-------------------------")
                        print("ID: ", person.id)
                        print("Name: ", person.name)
                        print("Schools: ", person.schools)
                        print("Images: ", person.images)
                        print(score)

                        if score > 0.8:
                            res = person.like()
                            print("LIKE")
                        else:
                            res = person.dislike()
                            print("DISLIKE")
                except Exception:
                    pass

    classifier.close()

Вот и все! Теперь мы можем позволить Auto-tinder работать так долго, как нам хочется, и больше не напрягать руку свайпами.Если у вас есть вопросы или вы нашли ошибки, не стесняйтесь вносить правки на Github.

Оригинал: [Hacking Tinder] Train an AI to Auto-Swipe for You 🖖

Перевод: Ухарова Елена

Поделиться: