Привет, Хабр!
С момента предыдущей публикации прошел год, и наступил момент закрыть гештальт, возникший, в том числе, по результатам ваших комментариев. А именно: можно ли вообще обойтись без внешних устройств при решении задачи профилирования активности пользователей по данным энергопотребления их устройств?
И хотя лично мне это кажется контр-продуктивным — имхо, сугубо имхо, лучше вообще не связываться с системой, которая может быть скомпрометирована, и получать данные из дополнительного источника, никак с тестируемой системой не связанного. Ни гальванически ни, тем более, в рамках одной операционной системы. Умная розетка (не обязательно от Сбера) казалась вполне себе доступной по цене альтернативой амперметру. Но вопрос был задан, и спустя год (ну извините, это все-таки pet-проект, а не основная работа) на него есть ответ:
Да, это возможно. Причем точность классификации разных сценариев поведения может достигать 93%.
Шаг 1. Получение информации от встроенных в системную плату сенсоров
Для начала, надо было понять, какие доступны утилиты сбора информации о сенсорах. Не то, чтобы я считаю себя закоренелым питонистом, но на C предпочитаю кодить только когда дело доходит до embedded-устройств, поэтому фокусировался на Python. Немаловажный вопрос - а какая, собственно, информация и с каких сенсоров с помощью этих утилит может быть получена. Использовавшаяся ранее кросс-платформенная утилита psutils для этой задачи не годилась — она собирает только информацию по процессам, утилизации процессора и памяти и т. д.

Ни поиск, ни генеративный ИИ мне не помогли, пришлось погрузиться в специфику Windows Management Instrumentation (так уж сложилось, что исследуемая система работала на Windows) и разобраться в том, как подключаться к WMI-сервисам.
На мое счастье, нашелся проект LibreHardwareMonitor, который изначально задумывался для мониторинга сенсоров температуры (если есть тут те, кто помнит про оверклокинг — привет, олды!), но, к счастью для меня, также умеет добывать показания датчиков напряжения процессора.

Если верить скриншоту, утилита умеет измерять потребляемую процессором мощность (Power). Если это действительно так, то решение уже у нас в кармане!
Но чтобы получить этот показатель, как и любой другой, нужно подписаться на сообщения LibreHardwareMonitor в WMI:
import wmi
w = wmi.WMI(namespace="root\LibreHardwareMonitor")
Есть два способа обращения к сенсорам через WMI:
1. Путем выполнения WQL (микро-аналог SQL внутри WMI)-запроса, выглядящего примерно так:
sensors = w.query("SELECT * FROM Sensor")
2. С помощью специального метода .sensor():
sensors = w.Sensor()
Этот метод возвращает список объектов типа Sensor, поддерживающих некоторый набор методов и характеристик, из которых меня в рамках моего исследования интересовали только две, Sensor.Name и Sensor.Value. Для удобства обработки их можно объединить в словарь, впрочем, у каждого аналитика свой набор тараканов предпочтений. Полный код утилиты Power_monitor.py для сбора значений и записи в лог доступен в открытом репозитории по ссылке.
Чтобы минимизировать нагрузку на систему, рекомендуется скомпилировать в исполняемый файл командой:
!pyinstaller -F Power_monitor.py
Впрочем, если кто-то предпочитает держать открытой консоль интерпретатора Python или IDE — не осуждаю, хотя и замечу, что в этом случае интерпретатор может дополнительно влиять на энергопотребление системы.
Шаг 2: сбор данных
По аналогии с предыдущим исследованием, я смоделировал несколько интересующих меня сценариев: закачку данных с испытуемой системы в облако (FileUpload, сценарий, в том числе, моделирует эксфильтрацию данных), просмотр видео (поскольку активный период исследования совпал с кампанией по замедлению YouTube, на испытуемой системе запускался просмотр видео на RuTube, впрочем, полагаю, что паттерны энергопотребления у них, скорее всего, похожи) и, конечно же, «контрольный» режим «ничегонеделания» (Idle). В этот раз мне почти не пришлось писать текстов на исследуемой системе, а число классов хотелось довести до четырех, то, чтобы обеспечить сравнение с предыдущим экспериментом, был добавлен еще один класс нагрузки — прослушивание музыки (Music).

Получить данные о режиме Idle было проще всего. Просто оставить компьютер включенным на ночь с запущенным на нем утилитой по сбору данных. Все остальные сценарии потребовали чуть больше ухищрений, в результате чего датасет получился… не самым сбалансированным. Но попробуем обучить модель, которая классифицирует сценарий поведения пользователей только на основании… Кстати, а что в итоге пишется в лог-то?
Шаг 3: анализ данных
На уровне самой утилиты Process_monitor.py ограничений в количестве записываемых в лог полей нет — пишется все, что отдает LibreHardwareMonitor в WMI. Посмотрим на список названий в Sensor.Name:
Index(['Data Uploaded', ' Data Downloaded', ' Upload Speed', ' Temperature #2', ' Download Speed', ' Used Space', ' Network Utilization', ' CPU Core #1', ' Fan #4', ' CPU Core #3', ' CPU Core #2', ' CPU Package', ' Memory Used', ' CPU Cores', ' Read Rate', ' Virtual Memory Available', ' +3V Standby', ' Fan #1', ' Temperature #3', ' Write Activity', ' CPU Core #4 Distance to TjMax', ' D3D Other', ' Bus Speed', ' CPU Core #4', ' CPU Core #2 Distance to TjMax', ' CPU Total', ' Memory', ' CPU Platform', ' D3D Shared Memory Free', ' Voltage #6', ' D3D Copy', ' Temperature #1', ' Voltage #4', ' Core Max', ' Voltage #3', ' D3D Overlay', ' D3D Video Processing', ' Voltage #2', ' Write Rate', ' Virtual Memory', ' Temperature', ' Core Average', ' Fan #3', ' CPU Core Max', ' Total Activity', ' Virtual Memory Used', ' D3D Video Decode', ' GPU Power', ' D3D Shared Memory Total', ' Voltage #5', ' Read Activity', ' CPU Core', ' CPU Core #3 Distance to TjMax', ' CMOS Battery', ' Temperature #4', ' CPU Core #1 Distance to TjMax', ' Memory Available', ' CPU Memory', ' D3D Shared Memory Used', ' Voltage #7', ' Voltage #1', ' Fan #2', ' D3D 3D', ' timestamp'], dtype='object')
Внимательный читатель обратит внимание, что названия характеристик Sensor.Name не совпадают с тем, что мы видим на скриншоте LibreHardwareMonitor – так, например, нет ничего, что напоминало бы Power, так что, увы, простого решения задачи не будет.
Давайте разбираться в данных. Для начала выкинем все те характеристики, выборочное стандартное отклонение среднего для которых равно нулю (то есть, они не меняются со временем). А заодно добавим фильтр по названию и дальше в анализе будем использовать только те, в названиях которых присутствует строка Voltage.
Почему Voltage? Потому что формуле расчета мощности энергопотребления участвует напряжение, ток или сопротивление. Из всех этих величин сенсоры LibreHardwareMonitor выдают только напряжение.
Сенсоров напряжения в логах семь штук: Voltage #1... Voltage #7. Соответствуют ли они нумерации в GUI LibreHardwareMonitor? Запишем как гипотезу для проверки на следующем шаге.
Но сначала поймем, с какой частотой писался лог - в самой утилите Power_monitor.py нет никаких таймеров задержки, так что частота опроса системы ограничивалась исключительно быстродействием системы. Как оказалось, испытуемая система записывала показания в среднем раз в секунду — в два раза реже, чем мультиметр (в пред-предыдущем эксперименте) или умная розетка. Точнее, если построить график задержек времени между каждый отсчетом, он выглядел так:

Среднее время между отсчетами составляет 1,0±0,15 с, но иногда видны задержки, достигающие трех секунд (видимо, в эти моменты система была загружена чем-то еще). Конкретно этот датасет, для которого был построен график, пришлось «подчистить», удалив из него последние 9700 слишком уж «грязных» записей.
Шаг 4: обучение классификатора
Не будем изобретать велосипед и воспользуемся ансамблем LGBM-классификаторов, который обучается в рамках фреймворка LightAutoML, использованного и в предыдущем эксперименте. Разобьем датасет на фрагменты длительностью от 30 до 120 секунд и посмотрим на сходимость метрик.
Но есть нюанс — какую именно величину напряжения выбрать? Попробуем взять Voltage #6 с самым большим средним значением. Вдруг нам повезет и именно эта характеристика соответствует CPU Core?

На графике отложены значения F-меры для каждого из классов (а цветовая легенда продублирована в подписи) в зависимости от размера фрагмента. И сразу видно, что сценарий выгрузки файлов определяется крайне плохо — его F-мера флуктуирует около нуля и от числа отсчетов не зависит.
Как дата-аналитик со стажем скажу: примерно так выглядит ситуация, когда "пациент" 100% мертв
Значит ли это, что нас постигло фиаско? Нет, у нас же есть показания еще шести сенсоров. Найдем среди них тот, у которого максимально выборочное среднеквадратичное отклонение. Это Voltage #1:

А вот это уже интересно. Во-первых, с ростом числа измерений подрастает F-мера для каждого из классов, как и общая Accuracy. Во-вторых, все сценарии определяются, в целом, неплохо — и это при том, что FFT-преобразование (я не буду подробно разбирать, что дает переход от временных рядов к Фурье-гармоникам, так как это уже было в предыдущей статье) не учитывает разброс значений отсчетов времени. Но напомню как выглядят флуктуации напряжения в разных сценариях:

и соответствующие им FFT-гармоники:

Даже визуально видно, что отделить один спектр от другого проще, чем их временные ряды.
Важный нюанс: FFT-преобразование для этой задачи применяется потому, что мы изначально знаем, что это квазипериодический процесс
Максимальное значение макроусредненной F-меры для всех классов составляет 93% (совпадает с Accuracy) и достигается на 115 измерениях:
Сценарий | Точность | Полнота | F-мера | Количество примеров |
Idle | 0,98 | 1,00 | 0,99 | 113 |
FileUpload | 0,86 | 0,83 | 0,84 | 23 |
RuTube | 0,82 | 0,82 | 0,82 | 22 |
Music | 0,82 | 0,78 | 0,80 | 18 |
Макроусреднение | 0,87 | 0,86 | 0,86 | 176 |
Средневзвешенное | 0,93 | 0,93 | 0,93 | 176 |
Конечно, видно, что датасет не сбалансирован: так как получить замеры для «ничегонеделания» проще всего, их поэтому и много. Немного страдает полнота для сценария прослушивания музыки, что вполне объяснимо, потому что такие классификаторы склонны к ложноположительным срабатываниям. Но в целом, с поправкой на малое число примеров, выглядит все неплохо. И да, если говорить о средневзвешенной F-мере классификации на этом участке с поправкой на малое число классов, посмотрев на соседние отсчеты (113 и 114 с), то корректнее утверждать, что она находится в диапазоне от 90 до 93%.
Код обучения и тестирования ансамбля LGBM-классификаторов полностью аналогичен базовому примеру применения TabularAutoML для задачи многоклассовой классификации. Все, что нужно сделать, это определить соответствующую задачу классификации в константах AutoML:
task = Task('multiclass')
Но если будет много желающих посмотреть на многостраничный отчет об оптимизации градиентных бустингов — пишите в комментариях, выложу ноутбук.
Выводы.
Главный вывод: ответ на вопрос «можно ли для решения задачи классификации поведения пользователя по паттернам энергопотребления обойтись вообще без внешних устройств?» получен, и он положительный. За почти десять лет моего личного интереса к этой теме удалось пройти путь от лабораторных мультиметров с 32-разрядными АЦП до 20-разрядных АЦП, встроенных в умные розетки. А теперь - и до считывания показаний сенсора питания процессорного ядра, которое варьируется всего-то в пределах 0,2 В.
Понятно, что тут есть много направлений для дальнейшего улучшения: по-хорошему, нужно и накопить больше данных, подобрать физическую модель (чтобы использовать не одно значение напряжения с максимальным выборочным среднеквадратичным отклонение, а учесть их все) и, конечно же, пределы для улучшения модели классификации вообще отсутствуют.
Если же смотреть на эксперимент через призму непрерывной аутентификации, то стоит задуматься о разумном периоде накопления данных и подбора эвристик на их основе.
Было бы здорово воспроизвести данный эксперимент и на других системах — есть предположение, что на более мощных процессорах, нежели испытуемый N100, можно добиться и большей частоты дискретизации. Ну или хотя бы — такой же, как для умных розеток (2 Гц). Если есть желающие — весь исходный код доступен в репозитории. Повторюсь, моя личная рекомендация — скомпилировать в исполняемый файл и запускать отдельно от интерпретатора Python и/или IDE.
Было бы также здорово обойтись без дополнительной утилиты LibreHardwareMonitor и провести эксперимент на других ОС (пока что код Power_monitor.py написан под Windows). Но это если к данной теме будет интерес со стороны существенно большего числа людей, чем один отказывающийся стареть душой гик.
В общем, буду признателен за обратную связь - как тут, так и в сообществе DIY-энтузиастов.