Як швидко створити MVP ML-моделі для AutoRia, що дозволяє продавати авто швидше та дорожче
Мене звуть Максим Бочок. Я Senior Data Scientist в компанії ITRex group. Сьогодні я покажу як швидко розробити прототип моделі машинного навчання для сервісу продажу авто.
Версія моделі, описана в цій статті (v0.1), дозволяє прогнозувати ціну машини на основі лицевої фотографії, року випуску, моделі та марки машини. Наступні версії:
- Дозволяють прогнозувати час продажу.
- Використовують інші предиктори, включаючи всі фотографії оголошення з attention фактором вибору фотографій (для багатьох людей важливим є не тільки зовнішній вигляд машини, модель, марка та опис, а ще і як виглядає салон).
- Включають кращу deep learning модель та кращий training setup.
- Рекомендують, що можна змінити в оголошенні, щоб авто продалось швидше та дорожче.
Саме цю версію моделі можна використовувати в деплої при виставленні оголошення та показувати продавцю прогнозовану вартість машини після кожного апдейту оголошення. Це дозволяє йому продати машину за більшу ціну. Таку модель можна розробити для будь якого маркетплейсу.
MVP моделі розроблено для власного дослідження, дані не поширюються і не використовуються в комерційних цілях, персональні ідентифікатори не збираються.
Крок 1. Збір даних
Збір даних був зроблений локально на MacBook Pro
Завантаження необхідних бібліотек для збору даних:
import requests from lxml import etree import urllib from bs4 import BeautifulSoup from tqdm import tqdm import pandas as pd
Збір посилань:
def get_all_links(): links = [] for page_id in tqdm(range(2, 5000)): url = f"https://auto.ria.com/uk/legkovie/?page={page_id}" page = requests.get(url) soup = BeautifulSoup(page.content, "html.parser") dom = etree.HTML(str(soup)) for item_id in range(1,11): try: elems = dom.xpath(f'//*[@id="searchResults"]/section[{item_id}]/div[4]/div[2]/div[1]/div/a') l = elems[0].attrib['href'] links.append(l) except: pass return links links = get_all_links()
Збір даних за посиланнями:
def get_car_info(url): res = {} page = requests.get(url) soup = BeautifulSoup(page.content, "html.parser") dom = etree.HTML(str(soup)) picture_elem = dom.xpath('//*[@id="photosBlock"]/div[1]/div[1]/div[1]/picture/img') picture_link = picture_elem[0].attrib['src'] picture_name = picture_link.split('/')[-1] urllib.request.urlretrieve(picture_link, f'data/photos/{picture_name}') #wget.download(picture_link, f'data/photos/{picture_link}') res['picture_name'] = picture_name price_elem = dom.xpath('//*[@id="showLeftBarView"]/section[1]/div[1]/strong/text()')[0] price = ''.join([s for s in price_elem.split() if s.isdigit()]) res['price'] = int(price) head_elem = dom.xpath('//*[@id="heading-cars"]/div/h1/text()')[0] head_elem = head_elem.split(' ') year = int(head_elem[-1]) brand = head_elem[0] model = head_elem[1] res['year'] = int(head_elem[-1]) res['brand'] = head_elem[0] res['model'] = head_elem[1] return res data = [] for i in tqdm(range(len(links))): try: res = get_car_info(links[i]) except: continue data.append(res) if i % 1000 == 0: df = pd.DataFrame(data=data, columns=data[0].keys()) df.to_csv('data/data.csv', index=False) df = pd.DataFrame(data=data, columns=data[0].keys()) df.to_csv('data/data.csv', index=False)
Крок 2. Створення та навчання моделі
Завантаження необхідних бібліотек для навчання моделі:
import torch from PIL import Image import torchvision.transforms as transforms import pandas as pd from sklearn.preprocessing import MinMaxScaler from tqdm import tqdm import random from operator import itemgetter import matplotlib.pyplot as plt import numpy as np import zipfile
Для навчання моделі використаний звичайний Google Colab з підтримкою GPU. Оскільки збір даних відбувався локально, потрібно перенести та розпакувати архів з даними:
!ls -al drive/MyDrive/data.zip !nvidia-smi -L with zipfile.ZipFile('drive/MyDrive/data.zip', 'r') as zip_ref: zip_ref.extractall('data')
Далі йде базова реалізація методів для обробки даних. Наступні версії моделі включають покращену трансформація зображень, покращену обробку табличних даних та створений ітератор завантаження даних:
def get_image_to_tensor(path): image = Image.open(path) transform = transforms.Compose([ transforms.PILToTensor(), transforms.Resize((224, 224)), transforms.ConvertImageDtype(torch.float), ]) return transform(image) def load_filtering_normalization(): df = pd.read_csv('data/data/data.csv') df = df[(df['price'] < 100_000) & (df['year'] > 2000)] scaler = MinMaxScaler() df[['year', 'price']] = scaler.fit_transform(df[['year', 'price']]) return df[:15000] def create_dataloader(): df = load_filtering_normalization() onehot_brand = pd.get_dummies(df['brand']) onehot_model = pd.get_dummies(df['model']) onehot_df = pd.concat([onehot_brand, onehot_model], axis=1) list_numeric = [torch.tensor(i, dtype=torch.float) for i in df['year'].values.reshape(-1,1).tolist()] list_onehot = [torch.tensor(i, dtype=torch.float) for i in onehot_df.values.tolist()] list_target = [torch.tensor(i, dtype=torch.float) for i in df.price.tolist()] list_resnet = [get_image_to_tensor(f"data/data/photos/{i}") for i in tqdm(df.picture_name.tolist())] return {'numeric': list_numeric, 'onehot': list_onehot, 'resnet': list_resnet, 'target': list_target} dataloader = create_dataloader()
Створення дуже простої моделі прогнозу ціни. Покращення — Layer Normalization, dropouts, покращена побудова архітектури моделі і т. п.
class Model(torch.nn.Module): def __init__(self, onehot_dim, out_for_each_branch=100): super().__init__() self.resnet_model = torch.hub.load('pytorch/vision:v0.10.0', 'resnet50', pretrained=True) resnet_blocks = list(self.resnet_model.children()) self.resnet_last_fc = torch.nn.Linear(resnet_blocks[-1].out_features, out_for_each_branch) numeric_fc1 = torch.nn.Linear(1, 50) numeric_fc2 = torch.nn.Linear(50, 500) numeric_fc3 = torch.nn.Linear(500, out_for_each_branch) numeric_lst = [numeric_fc1, numeric_fc2, numeric_fc3] self.numeric_branch = torch.nn.Sequential(*numeric_lst) onehot_fc1 = torch.nn.Linear(onehot_dim, 1000) onehot_fc2 = torch.nn.Linear(1000, 300) onehot_fc3 = torch.nn.Linear(300, out_for_each_branch) onehot_lst = [onehot_fc1, onehot_fc2, onehot_fc3] self.onehot_branch = torch.nn.Sequential(*onehot_lst) self.fc_last1 = torch.nn.Linear(300, 20) self.fc_last2 = torch.nn.Linear(20, 1) def forward(self, sample): resnet_out = self.resnet_model(sample['resnet']) resnet_out = self.resnet_last_fc(resnet_out) numeric_out = self.numeric_branch(sample['numeric']) onehot_out = self.onehot_branch(sample['onehot']) cat = torch.cat((resnet_out, numeric_out, onehot_out), dim=-1) out = self.fc_last1(cat) out = self.fc_last2(out) return out batch_size = 32 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = Model(onehot_dim=dataloader['onehot'][0].shape[0]).to(device)
Навчання моделі
Навчання моделі виконується протягом ~ 15 000 ітерацій. Базові покращення — додаткові складові функції втрат (loss function), train-val-test split, Optimizer Scheduler, Cross validation, логування, автоматичний вибір кращої моделі і т. п.
loss_fn = torch.nn.MSELoss() opt = torch.optim.AdamW(model.parameters(), lr=0.001, betas=(0.9, 0.999)) losses = [] for it in range(100_000): opt.zero_grad() indeces = random.sample(range(len(dataloader['onehot'])), batch_size) sample_numeric = torch.stack(itemgetter(*indeces)(dataloader['numeric'])).to(device) sample_onehot = torch.stack(itemgetter(*indeces)(dataloader['onehot'])).to(device) try: sample_resnet = torch.stack(itemgetter(*indeces)(dataloader['resnet'])).to(device) except: continue sample_target = torch.stack(itemgetter(*indeces)(dataloader['target'])).to(device) sample = {'numeric': sample_numeric, 'onehot': sample_onehot, 'resnet': sample_resnet} output = model(sample) loss = loss_fn(output, sample_target.unsqueeze(-1)) loss.backward() opt.step() losses.append(loss.item()) if it % 100 == 0: print(it, np.mean(losses[-1000:])) print(output, sample_target.unsqueeze(-1)) if it % 1000 == 0: torch.save(model.state_dict(), 'drive/MyDrive/autotia_model')
Висновки
У цій статті описана ідея створення сервісу для маркетплейсів та її дуже базова реалізація на прикладі аutoria. Приємного Діп Льорнінгу!
24 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів