Недавно, листая новостную ленту, наткнулся на кликбейтный пост «как нейросеть рисует поговорки»

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

Так как генеративные модели сейчас очень просты в использовании, я решил создать такую ​​игру за пару вечеров.

Идея:

1. Берем список фильмов

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

3. Дайте викторину с 4 вариантами

4. Игра до 10 угаданных картинок подряд

Чтобы было интереснее играть, 3 неправильных варианта тоже должны в какой-то степени соответствовать картинке, а не быть совсем случайными (идея используется для повышения интереса к настольной игре Имаджинариум/Диксит)

Первая модель генерирует изображение, используя имя фильма.

Вторая модель ищет 4 названия фильмов, которые лучше всего описывают сгенерированное изображение. Решил воспользоваться списком названий 1000 лучших фильмов от IMDB.

0. Создание папок в проекте — место для хранения сгенерированных изображений и веб-страницы для игры (см. GitHub)

1. Чтение 1000 лучших фильмов IMDB

import pandas as pd
movies=pd.read_csv('imdb (1000 movies) in june 2022.csv')
names=movies['movie name\r\n'][:]

2. Генерация изображений с использованием предварительно обученной модели Huggingface. Несмотря на то, что код выглядит простым, генерация имеет некоторую сложность под капотом.

from diffusers import StableDiffusionPipeline, StableDiffusionInpaintPipeline
pipe = StableDiffusionPipeline.from_pretrained("stabilityai/stable-diffusion-2-1", use_auth_token=False).to("cuda")
answers=[0]*1000
images=[0]*1000
for i in range(1000):
    if '{}.png'.format(i) not in os.listdir('1000movies/'):
        selected=np.random.choice(names)
        image= pipe([names[i]])['images'][0]
        answers[i]=names[i]#selected
        image.save('1000movies/{}.png'.format(i))
    print(i)

3. Для создания жестких и вводящих в заблуждение вариантов используйте модель CLIP.

from transformers import CLIPProcessor, CLIPModel
import os
import tqdm
import torch
import numpy as np
model = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")
imagenames=natsorted(os.listdir('1000movies/'))
images=[Image.open('1000movies/'+imgname) for imgname in imagenames]
moviesfeat=[]
img_embed=[]
for name in tqdm.tqdm(names):
    moviesfeat.append(model.get_text_features(**processor.tokenizer([name],return_tensors='pt')))
moviesfeat=torch.cat(moviesfeat,0)
for i in tqdm.tqdm(range (len(images))):
    batch = processor(text=None, images=images[i:i+1], return_tensors='pt',padding=True)['pixel_values']
    img_embed.append(model.get_image_features(pixel_values=batch))
img_embed=torch.cat(img_embed,0)
imgnorm=img_embed/img_embed.norm(dim=1).unsqueeze(1)
textnorm=(moviesfeat/moviesfeat.norm(dim=1).unsqueeze(1)).T
sim=imgnorm@textnorm
np.save('moivieSimilarity.npy',sim.detach().numpy())

Вот простое объяснение приведенного выше кода:
Модель CLIP является неотъемлемой частью большинства визуально-языковых моделей. Его основная задача — взять изображение и текст и преобразовать их оба в пространство для встраивания. Любой текст и любое изображение можно представить в виде вектора в 512-мерном пространстве. Модель CLIP последовательно выполняет такое преобразование, поэтому, если текст точно описывает изображение, векторы изображения и текста совпадают. Итак, для каждого сгенерированного изображения и каждой метки фильма мы получили 512-мерный вектор.
Сходство между 1000 названий фильмов и 1000 сгенерированных меток можно измерить как косинусное сходство — это просто значение, равное 1, если векторы указывают в одном направлении и -1 для противоположных направлений. Формула представляет собой просто скалярное произведение нормализованных векторов. Если мы измерим метрики по каждой сгенерированной паре картинке-названию фильма, то сможем получить матрицу сходства, за которой интересно наблюдать (вот верхний левый угол такой матрицы, соответствующий топ-100 лучших фильмов по версии IMDb):

import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10,10))
ax.matshow(sim[:100,:100], cmap=plt.cm.Blues)

Важным замечанием является то, что главная диагональ сильно выделяется, поэтому сгенерированное изображение действительно соответствует названию фильма. Кроме того, темные точки за пределами главной диагонали соответствуют названиям фильмов, которые можно интерпретировать как описание изображения, поэтому они являются кандидатами на роль поддельных вариантов.
Пользовательский интерфейс и логику игры можно нарисовать с помощью python (см. файл diffgame_inference.py на GitHub — это говорит само за себя)
Проблема такой игры в том, что ее невозможно запустить его для неопытного пользователя или для запуска на мобильном устройстве. Такая проблема типична для любой разработки на Python, и в некоторых случаях необходимо переписать множество прототипов Python для лучшей доступности.
Поскольку у меня есть все необходимые данные (изображения, названия фильмов и матрица сходства), а логика вывода довольно проста, можно запустить ее с помощью JS и создать веб-приложение. Более того, такому приложению даже не нужен бэкенд из-за простой логики, и все можно сделать на стороне клиента.
Итак, сначала все данные должны быть сериализованы в виде JSON-объектов. Матрица списка списков

import json
class NumpyArrayEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.ndarray):
            return obj.tolist()
        return JSONEncoder.default(self, obj)
with open('diffgamehtml/js/similaritytable.json','w') as f:
    json.dump(numpyData,f,cls=NumpyArrayEncoder)

И имена файлов в виде списка:

movies['movie name\r\n'].to_json('diffgamehtml/js/movieslist.json')

Поскольку у меня почти нет опыта работы с JS-фреймворками, реализация может выглядеть довольно уродливо, так как использует только ванильный js, который я почти забыл. Также весь проект написан на vanilla html/css/js и помещен в папку diffgamehtml. Вся логика занимает менее 100 строк кода и находится в файле gamelogics.js. Нет необходимости объяснять это. После тестов мне пришла в голову идея настройки сложности, поэтому на разных сложностях используется разный размер угла матрицы (от топ50 до топ 1000 фильмов)
Затем развернуть на моем сервере как статическую html веб-страницу https:/ /aidle.org/diffusionguesser/