Хакнуть Tinder
В этой статье мы поговорим о совершенно необычном применении искусственного интеллекта и покажем, как его использовать для поиска подходящей пары в Tinder.
Можно создать Auto-tinder на основе Tensorflow и Python 3, который будет автоматически лайкать профили в Tinder на основе ваших предпочтений.
Для этого нам нужно сделать последовательно вот эти шаги:
- Проанализировать веб-страницы Tinder на предмет внутренних вызовов API, воссоздать их в Postman, и анализировать их содержимое
- Создать класс-обертку для API на Python, чтобы дальше использовать Tinder API.
- Загрузить изображения пользователей, которые находятся рядом
- Создать обычный классификатор для изображений
- Разработать препроцессор, использующий Tensorflow API для распознавания объектов
- Переобучить нейронную сеть inceptionv3 на классифицированных данных
- Применить классификатор вместе с оберткой Tinder API, чтобы они свайпали за нас.
Шаг 1: Анализ API
Вначале узнаем, как приложение Tinder взаимодействует с бэкендом. Для этого переходим на сайт Tinder, открываем Chrome Devtools и изучаем сетевой протокол. У Tinder есть внутренний API, который они используют для связи между фронт-эндом и бэкэндом. Эта конечная точка 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": [] } } ] } }
Все изображения являются общедоступными и их может видеть кто угодно. Оригиналы фотографий имеют высокое разрешение и хранятся на публично доступных серверах. Если вы не выберете «show_gender_on_profile», через API все равно можно увидеть ваш пол («gender»: 1, где 1 = женщина, 0 = мужчина).
Анализируя заголовки, можно быстро найти наш личный ключ к API: X-Auth-Token. Скопировав этот токен в Postman, можно увидеть, что мы действительно можем свободно общаться с API Tinder, используя только правильный URL и токен авторизации.
Шаг 2: Обертка для API
Мы будем пользоваться библиотекой Python Requests для взаимодействия с API и создания обертки. Класс Person будет принимать ответ API и позволит взаимодействовать с ним.
Давайте начнем с написания класса. Он должен получать данные от 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.
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: Загружаем фотографии
Далее нам нужно автоматически загрузить около 2000 фотографий пользователей поблизости, которые мы будем использовать для обучения нашего ИИ. Во-первых, расширяем наш класс 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+"rn") 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)
После этого запускаем скрипт и даем ему поработать несколько часов, чтобы получить нужное количество фотографий.
Шаг 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. Размер базы данных: наша база относительно мала. Мы имеем дело с плюс-минус двумя тысячами фотографий, что считается очень маленьким количеством для данных такой сложности.
2. Разнообразие данных: фотографии выполнены с разных ракурсов - со спины, иногда на фото есть только лицо, иногда людей на фото нет.
3. Зашумленность данных: на большинстве фотографий есть не только сам человек, но и фон, который может мешать ИИ.
Мы будем решать эти проблемы двумя способами:
1. Преобразовывать фотографии из цветных в черно-белые, чтобы уменьшить объем информации в 3 раза
2. Вырезать часть фотографии, где находится человек
Первый способ очень просто сделать с использованием Pillow, в котором мы можем открыть наше изображение и перевести его в оттенки серого.
Второй - мы реализуем с помощью Tensorflow API для распознавания объектов и сети MobileNet на базе данных COCO.
Наш скрипт для распознавания людей на фото будет состоять из четырех частей:
Часть 1: Открываем предобученную сеть
В репозитории на 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
Часть 2: Загружаем фотографии
Используем Pillow для работы с изображениями. Так как Tensorflow для работы нужны numpy-массивы, поэтому напишем функцию, которая преобразует изображения в такие массивы:
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 и модель inceptionv3. Вызываем скрипт в директории вашего проекта со следующими параметрами:
python retrain.py --bottleneck_dir=tf/training_data/bottlenec ks --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/retraine d_labels.txt -- image_dir=./images/classified -- how_many_training_steps=50000 -- testing_percentage=20 -- learning_rate=0.001
В результате у нас будет переобученная модель inceptionV3 в файле «tf/training_output/retrained_graph.pb». Теперь мы должны написать класс Classifier, который открывает сессию. В ней передается граф и имеется метод «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 - высокая вероятность понравиться (меньше этого значения профили свайпаются вслево). Получился скрипт, который будет работать 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()
Теперь у вас есть инструкция, чтобы повторить эксперимент. Кто попробует, ждем ваши отзывы о работе.
🔥 Ставь реакцию, если понравилась статья
⚡️Остались вопросы? Пиши - @golden_hpa