Исследование набора данных для обучения LVLM — SeeClick (Web Data)

В данной статье будет рассмотрен набор данных для обучения LVLM (Large Visual Language Model), который использовался авторами статьи «SeeClick: Harnessing GUI Grounding for Advanced Visual GUI Agents» при обучении модели SeeClick, которая показывает достаточно неплохие результаты, по сравнению с аналогичными решениями. Рассмотрен будет только набор данных, который использовался при обучении SeeClick для определения элементов на веб-страницах, полученный авторами этой модели с помощью Common Crawl (открытый репозиторий набора данных о веб-страницах).

Данная статья может быть полезна специалистам, которые начинают разрабатывать свою LVLM работающую со скриншотами пользовательского экрана (image-ориентированные), а не с содержимым HTML веб-приложения (text-ориентированные). В статье будет рассмотрена базовая работа с частью предоставляемого SeeClick набора данных (10000 размеченных изображений общим объёмом в 4.8 Гб).

93b473f7b9a8028cffcb4d015848a763.png

Содержание

  1. Введение

  2. Загрузка набора данных

  3. Описание содержимого разметки

  4. Визуализация изображений из набора данных

  5. Заключение

  6. Ссылки

Введение

В настоящее время я активно занимаюсь разработкой image-ориентированной LVLM, которая по инструкциям от пользователя и скриншотам экрана будет выполнять определённые задачи в веб-браузере пользователя (в основном — конкретном веб-приложении).

Для обучения LVLM нужны большие объёмы данных и бенчмарки, по которым можно судить о том, как хорошо модель справляется с решением конкретной задачи. Чтобы собрать такие объёмы данных нужно проделать очень много сложной и рутинной работы, которая никак не связана с непосредственным обучением модели или разработкой её архитектуры, поэтому я решил найти готовый набор данных для обучения в открытом доступе, поскольку уже существуют качественные LVLM, решающие задачи автоматического управления веб-браузером.

В ходе изучения уже существующих работ, я наткнулся на проект SeeClick, в котором LVLM обучалась на большом объёме данных, который был собран командой данного проекта для обучения своей модели. Данный набор данных и исходный код проекта были выложены авторами SeeClick в открытый доступ.

Модель SeeClick обучалась на разных специализированных наборах данных — отдельно для мобильных устройств, десктоп-приложений и веб-приложений, с использованием в качестве базовой LVLM Qwen-VL. Свою же модель я планирую обучать только на одном наборе данных — для веб-приложений (Web Data), в силу специфических особенностей конкретно своей задачи, с использованием в качестве базовой модели Qwen2.5-Coder-3B-instruct. Именно это послужило основной мотивацией в написании данной статьи — разбор особенностей открытого SeeClick датасета для обучения своей LVLM для поиска элементов в веб-браузере (конкретно — веб-страницах) и последующего выполнения задач по инструкциям от пользователя.

Загрузка набора данных

Исследование набора данных будет происходить в среде Google Colab.

Для загрузки части набора данных (10000 изображений веб-страниц взятых с Common Crawl) можно использовать следующую команду (ссылка может отличаться):

!wget -O screenshot.zip https://box.nju.edu.cn/f/813897fc4edc440a9e12/?dl=1
Рисунок 1 - Результат загрузки набора данных в Google Colab
Рисунок 1 — Результат загрузки набора данных в Google Colab

Загружается такой набор данных достаточно долго. У меня этот процесс занял 11 минут 38 секунд на стандартной машине от Google Colab.

Чтобы с этим набором данных работать, нужно распаковать загруженный архив с помощью следующей команды:

!unzip screenshot.zip
Рисунок 2 - Распаковка архива (10000 изображений)
Рисунок 2 — Распаковка архива (10000 изображений)

Теперь нужно загрузить разметку всех этих данных. К сожалению, авторы SeeClick не выделили отдельно разметку под эту небольшую часть набора данных, а предоставили только разметку всего набора данных в целом (этот JSON-файл размечает все 127.9 Гб набора данных), а потому он весьма увесистый (примерно 433 Мб). Это стоит учитывать, поскольку оперативная память машины ограничена и после фильтрации не нужных элементов этого загруженного JSON’a стоит воспользоваться оператором del и gc.collect (), чтобы освободить занятую оперативную память.

Загружаем разметку всего набора данных:

!wget -O seeclick_web.json https://box.nju.edu.cn/f/3b0f6ccb8bed476c8e39/?dl=1

Открыть данный JSON-файл с помощью какого-либо приложения может быть затруднительно. Даже Visual Studio Code с этим справляется плохо (просто медленно его загружает), а вот стандартный «блокнот» демонстрирует его содержимое практически мгновенно.

Рисунок 4 - Содержимое JSON-файла разметки
Рисунок 4 — Содержимое JSON-файла разметки

Перед загрузкой JSON-разметки и её фильтрацией следует написать функцию для получения списка всех файлов из директории с загруженным набором данных:

# Получение всех файлов в директории
def getFilesByPath(dir):
  return [name for name in os.listdir(dir) if os.path.isfile(os.path.join(dir, name))]

Данная функция требуется для фильтрации загруженной JSON-разметки.

Как можно увидеть из рисунка 4 (схема JSON-разметки) у каждого поля элемента массива есть ссылка на изображение, которая является уникальным значением (img_filename). Эта ссылка по сути является именем файла изображения, к которому идёт разметка расположенных в нём элементов (для этого есть поле elements).

Теперь загрузим JSON-схему разметки (этот процесс может занять определённое время)

import json

schemeJSON = []
with open('/content/seeclick_web.json') as json_file:
  schemeJSON = json.load(json_file)

Теперь определим простую функцию фильтрации всей разметки (для всего набора данных), чтобы остались только те, которые размечают выборку из 10000 изображений:

# Фильтрация элементов словаря (JSON-схемы разметки)
def filteredEntries(source_scheme, dir):
  # Все файлы по определённому пути (тут dir - путь до загруженного набора данных)
  all_files = getFilesByPath(dir)
  new_scheme = []

  # Процесс отсеивания не встреченных в загруженном наборе данных элементов словаря
  for i in range(0, len(source_scheme)):
    item = source_scheme[i]
    if item["img_filename"] in all_files:
      new_scheme.append(item)

  return new_scheme

# Отфильтрованная схема
subsetScheme = filteredEntries(schemeJSON, SOURCE_DIR_IMG)

# Удаляем все элементы загруженной ранее схемы
schemeJSON.clear()
# Удаляем ссылку на элементы в schemeJSON и инициируем сборку мусора
del schemeJSON
# Будут удалены те элементы, на которых больше ничего не ссылается
gc.collect()

Теперь в переменной subsetScheme будет находится та часть всей размеченной схемы, которая описывает конкретно загруженный набор данных из 10000 изображений. При этом предполагается, что благодаря удалению всех элементов из переменной schemeJSON (который имеет тип данных list), применению к нему оператора del и вызова сборщика мусора произойдёт удаление большей части занимаемой этой переменной данных (что было бы логично, т.к. ссылки на объект schemeJSON уже нет, а сам список был предварительно очищен), однако никаких изменений не происходит. Занятой памяти даже стало немного больше, возможно это связано с особенностями среды Google Colab, или же с особенностями модели памяти в Python.

Для оценки занимаемой приложением памяти я использую следующий код:

import psutil
process = psutil.Process()

# Занимаемая приложением RAM в Mb
print(process.memory_info().rss / 1024 / 1024)

Останавливаться на особенностях освобождении памяти в Python мы не будем и продолжим идти дальше, однако такое поведение достаточно интересно и с чем именно оно связано — непонятно. В любом случае память занимаемая schemeJSON не была освобождена, ни с помощью clear (), ни с помощью del и даже gc.collect () не помог.

Описание содержимого разметки

После того, как была загружена JSON-схема разметки следует с ней более подробно познакомится.

Прежде всего, каждый элемент схемы представляет собой разметку элементов определённой веб-страницы, имя файла изображения которой размещён в атрибуте img_filename. Помимо данного атрибута есть ещё атрибут url, который содержит фактическую ссылку на веб-страницу, скриншот которой был сделан для данного набора данных. Скриншоты и фактическая веб-страница могут отличаться, поскольку веб-сайт может изменить свой дизайн. Это может быть разрешено путём периодической актуализацией скринов веб-сайта, однако это довольно трудозатратная задача.

В схеме есть также атрибут elements, который содержит в себе массив с описанием положения элементов на веб-странице определённым образом. Схема элементов разметки приведена на рисунке 5.

Рисунок 5 - Схема элементов набора данных
Рисунок 5 — Схема элементов набора данных

Элементы в массиве elements имеют атрибут instruction, bbox и data_type. В атрибуте instruction хранится достаточно прямолинейное описание элемента, которое часто совпадает с фактическим текстом размещённым на самом элементе в веб-странице.

В атрибуте bbox хранятся нормализованные координаты четырёхугольника, в который заключён текущий элемент. Под координатами четырёхугольника здесь понимаются следующие данные: (left, top, right, down). По данным координатам можно также определить точку, которую LVLM может определить для решения задачи.

Вообще, в официальном репозитории SeeClick сказано, что данная модель способна определять как координаты четырёхугольника определённого элемента на веб-странице, так и координаты точки этого элемента, однако рекомендуется использовать координаты точки.

The prediction output represents the point of (x, y) or the bounding box of (left, top, right, down), each value is a [0, 1] decimal number indicating the ratio of the corresponding position to the width or height of the image. We recommend using point for prediction because SeeClick is mainly trained for predicting click points on GUIs.

В атрибуте data_type определён тип элемента веб-страницы. Авторы SeeClick выделили всего два типа элементов на странице — «text» (текстовые элементы) и «hover» («кликабельные» элементы в целом). Технически все текстовые элементы кликабельны и обладают возможностью кастомизации псевдо класса : hover, однако здесь эти элементы прямо не связаны с псевдо классом : hover (это важно отметить).

К hover элементу относится button, input, textarea, select и различные их визуальные комбинации, которые были выделены при формировании датасета для обучения. По крайней мере в результате визуализации изображений такое соответствие было корректно.

Визуализация изображений из набора данных

Теперь следует визуализировать несколько изображений из данного набора и выделить на них размеченные элементы, чтобы показать какие именно данные о расположении элементов на веб-странице получает LVLM в процессе обучения на этих данных (по крайней мере, так задумывали авторы SeeClick собирая данный набор данных, судя по их работе).

Для начала следует написать небольшую функцию для денормализации данных в атрибуте bbox, поскольку в наборе данных представлены именно нормализованные данные.

Каким образом это сделать, учитывая что авторы в явном виде нигде не предоставили формулу обратного преобразования? Конечно в исходном коде SeeClick, а если конкретно — в файле screenspot_test.py:

# ...
image = Image.open(img_path)
instruction = item["instruction"]
bbox = item["bbox"]
bbox = [bbox[0], bbox[1], bbox[0] + bbox[2], bbox[1] + bbox[3]]
img_size = image.size
bbox = [bbox[0] / img_size[0], bbox[1] / img_size[1], bbox[2] / img_size[0], bbox[3] / img_size[1]]
# ...

В данном коде отчётливо видно, что изменённое значение координат четырёхугольника (x, y, width, height) делятся на widthImage (ширину изображения) и heightImage (высоту изображения):

(x / widthImage, y / heightImage, width / widthImage, height / heightImage)

Таким образом для денормализации данных bbox можно воспользоваться следующей функцией:

# Денормализация данных (x, y, width, height)
def convertNormalizeBboxType1(bbox, width, height):
  new_bbox = [bbox[0], bbox[1], bbox[2] - bbox[0], bbox[3] - bbox[1]]
  new_bbox = [new_bbox[0] * width, new_bbox[1] * height, new_bbox[2] * width, new_bbox[3] * height]

  return new_bbox

Такая функция денормализации актуальна только тогда, когда мы описываем координаты четырёхугольника в форме (x, y, width, height), однако для формы (left, top, right, down) следует использовать другую функцию:

# Денормализация данных (left, top, right, down)
def convertNormalizeBboxType2(bbox, width, height):
  new_bbox = [bbox[0] * width, bbox[1] * height, bbox[2] * width, bbox[3] * height]

  return new_bbox

Теперь определим функции, с помощью которых будем отображать размеченные на каждом изображении четырёхугольники. Для этого потребуется реализовать всего две функции — одну для сборки необходимых данных из схемы JSON, а другую для непосредственной визуализации изображения с прямоугольниками через matplotlib:

# Визуализация изображения с четырёхугольниками
def show_image_with_bboxes(image, bboxes=None):
    img_height = image.shape[0]
    img_width = image.shape[1]

    dpi = 40

    # Размер изображения
    figsize = img_width / float(dpi), img_height / float(dpi)

    # Настройка matplotlib для визуализации
    fig, ax = plt.subplots(1, figsize=figsize)
    ax.imshow(image)

    if isinstance(bboxes, list):
      for i in range(0, len(bboxes)):
        bbox = bboxes[i]
        if (not bbox) or (len(bbox) >= 5 and not isinstance(bbox[4], str)):
          continue

        # Определение координат прямоугольника (x, y, width, height)
        x = int(bbox[0])
        y = int(bbox[1])
        width = int(bbox[2])
        height = int(bbox[3])

        # Определение цвета четырёхугольника
        color = 'black'
        if len(bbox) >= 5:
          color = 'black' if bbox[4] == "text" else 'blue'

        # Добавление в визуализацию matplotlib изображения четырёхугольника
        rect = patches.Rectangle((x, y), width, height, linewidth=5, edgecolor=color, facecolor='none')
        ax.add_patch(rect)

    # Не рисовать оси
    plt.axis('off')

    # Визуализация изображения
    plt.show()

# Сборка данных + визуализация данных
def show_image_rect(data):
  # Чтение изображения
  img = cv2.imread(SOURCE_DIR_IMG + data["img_filename"])

  # Получение ширины и высоты изображения
  img_height = img.shape[0]
  img_width = img.shape[1]

  bboxes = []
  elements = data["elements"]

  print("Elements: ", len(elements))

  # Сборка данных о каждом элементе
  for i in range(0, len(elements)):
    temp_bbox = data["elements"][i]["bbox"]

    # Денормализация данных bbox
    temp_bbox = convertNormalizeBboxType1(temp_bbox, img_width, img_height)
    # + добавляем тип элемента, заключённого в bbox
    temp_bbox.append(data["elements"][i]["data_type"])

    bboxes.append(temp_bbox)

  # Визуализация изображения со списком bbox'ов
  show_image_with_bboxes(img, bboxes)

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

Рисунок 6 - Визуализация 6-го изображения
Рисунок 6 — Визуализация 6-го изображения
Рисунок 7 - Визуализация 46-го изображения
Рисунок 7 — Визуализация 46-го изображения
Рисунок 8 - Визуализация 47-го изображения
Рисунок 8 — Визуализация 47-го изображения

Как можно отметить из рисунков 6, 7 и 8 не все элементы на странице корректно соотносятся по типам hover и text. Большинство, казалось бы, hover элементов относятся к text. Это хорошо видно на следующем изображении:

Рисунок 9 - Визуализация 2-го изображения
Рисунок 9 — Визуализация 2-го изображения

Здесь вообще все hover элементы соотнесены к типу text, да и далеко не все элементы размечены. Возможно это связано с тем, чтобы эти изображения затем переиспользовать в качестве тестовых, с целью обнаружения неразмеченных элементов. Учитывая, что таких изображений очень много такой подход имеет смысл.

Вообще, на атрибут data_type можно было бы никакого и внимания не обращать, поскольку почти все элементы (для работы с формами) на веб-странице кликабельны и имеют псевдо класс : hover (если не задано обратное вручную). Тем более, что после визуализации становится не понятно что под типом «hover» понимается (разве что точно к ним относятся image, input и button). Однако, если у LVLM будет задача вписать данные в какую-то форму и отправить её кликнув по кнопке то понимание различных типов HTML-элементов в веб-браузере необходимо.

Ограниченность двумя типами (text и hover) данного набора данных является его существенным недостатком, поскольку элементы на странице бывают совершенно разные и взаимодействие с ними осуществляется по разным сценариям. Например, нельзя работать с select точно также, как с input. Потому что в input базовая операция всегда ввод текста, а у select это прежде всего выбор из всех доступных элементов (коих может быть много, да ещё и со скроллом). А ещё есть и гибриды (select + input). Поэтому этот недостаток очевиден.

Визуализируем точки расположенных элементов на веб-странице, по тому же принципу соотнесения цветов с типами элементов из набора данных:

# Визуализация изображения с точками
def show_image_with_points(image, coords=None):
    img_height = image.shape[0]
    img_width = image.shape[1]

    dpi = 40

    # Размер изображения
    figsize = img_width / float(dpi), img_height / float(dpi)

    # Настройка matplotlib для визуализации
    fig, ax = plt.subplots(1, figsize=figsize)
    ax.imshow(image)

    if isinstance(coords, list):
      for i in range(0, len(coords)):
        coord = coords[i]
        if (not coord) or (len(coord) >= 3 and not isinstance(coord[2], str)):
          continue

        # Определение координат элемента (x, y)
        x = int(coord[0])
        y = int(coord[1])

        # Определение цвета четырёхугольника
        color = 'black'
        if len(coord) >= 5:
          color = 'black' if coords[3] == "text" else 'blue'

        # Добавление в визуализацию matplotlib изображения точки
        point = patches.Circle((x, y), radius=5, edgecolor=color, facecolor=color)

        ax.add_patch(point)

    # Не рисовать оси
    plt.axis('off')

    # Визуализация изображения
    plt.show()

# Преобразование bbox в точку
def bbox_2_point(bbox):
  point = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
  return point

# Сборка данных + визуализация данных
def show_image_point(data):
  # Чтение изображения
  img = cv2.imread(SOURCE_DIR_IMG + data["img_filename"])

  # Получение ширины и высоты изображения
  img_height = img.shape[0]
  img_width = img.shape[1]

  points = []
  elements = data["elements"]

  print("Elements: ", len(elements))

  # Сборка данных о каждом элементе
  for i in range(0, len(elements)):
    temp_bbox = data["elements"][i]["bbox"]

    # Денормализация данных bbox
    temp_bbox = convertNormalizeBboxType2(temp_bbox, img_width, img_height)
    # Преобразование bbox в точку
    point = bbox_2_point(temp_bbox)
    # + добавляем тип элемента, заключённого в bbox
    point.append(data["elements"][i]["data_type"])

    points.append(point)

  print(points)
  # Визуализация изображения с точками элементов
  show_image_with_points(img, points)
Рисунок 10 - Визуализация 1-го изображения
Рисунок 10 — Визуализация 1-го изображения
Рисунок 11 - Визуализация 19-го изображения
Рисунок 11 — Визуализация 19-го изображения
Рисунок 12 - Визуализация 49-го изображения
Рисунок 12 — Визуализация 49-го изображения

Из рисунков 10–12 можно отметить, что по сути точки находятся по центру четырёхугольников выделенных элементов. Т.е. LVLM должна будет предсказать координаты, которые в этот четырёхугольник хотя бы входят, а если уж по центру предскажет — то будет идеально.

Выделение LVLM конкретных координат четырёхугольника элемента (left, top, right, down) важно, поскольку при делегировании управления мышью сторонней программе (например, использующей пакет pyautogui), важно знать куда этой мыши нужно переместиться (по каким координатам) и куда нужно кликнуть.

В большинстве UI интерфейсов для взаимодействия с элементами веб-приложения достаточно кликнуть в четырёхугольник вложенного в него текстового элемента или кликнуть в button, input, select, которые так или иначе заключены в какой-то контейнер (div), позволяющий пользователю кликнуть в этот элемент. Поэтому важно научить LVLM определять как точные координаты четырёхугольника, так и точные координаты элемента на базе уже предсказанных координат четырёхугольника элемента, чтобы сделать попадание клика мыши по элементу более вероятным.

Заключение

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

В рамках изучения данного набора данных было представлено общее представление о его разметке и особенностях денормализации значений атрибута bbox, который описывает положение элемента на веб-странице в виде четырёхугольника в формате (left, top, right, down).

У данного набора данных выявлено два существенных недостатка, требующих внимания, если на данном наборе будет происходить обучение LVLM модели:

  1. Не все элементы на веб-странице размечены. В наборе данных есть страницы с очень малым числом размеченных элементов (где-то даже всего 1 элемент размечен, а остальные проигнорированы), что может повлиять на обучение LVLM распознаванию на странице элемента и установлению его точных координат относительно экрана пользователя;

  2. В наборе данных представлено только 2 типа определяемых элементов на странице — text и hover, причём не до конца понятно что под hover понимается (авторы в своей статье также не указывают на это). По визуализации изображений с разметкой понятно, что в hover входят button, image, input и textarea, однако есть и ситуации, когда select выделен как text, хотя подразумевается что он будет работать как input или хотя бы button.

Ссылки

  1. Официальный репозиторий SeeClick;

  2. Статья «SeeClick: Harnessing GUI Grounding for Advanced Visual GUI Agents»;

  3. Ссылка на исходный код.

© Habrahabr.ru