Рубрики
Без рубрики

Создание продуктов данных с помощью Python: Использование машинного обучения для предоставления рекомендаций

Это третья часть нашего урока о том, как создать веб-систему обзора и рекомендаций вина с использованием технологий Python, таких как Django, Pandas, SciPy и Scikit-learn. В этой части вы узнаете, как использовать машинное обучение, чтобы рекомендовать пользователям вина на основе их предпочтений.

Автор оригинала: Jose A Dianes.

Это третья часть нашего урока о том, как создать веб-систему обзора и рекомендаций вина с использованием технологий Python, таких как Django , Pandas , SciPy и Scikit-learn .

В первом уроке мы запустили проект Django и приложение Django для нашего веб-приложения Wine recommender. Все это будет постепенным процессом, за которым может последовать проверка отдельных тегов в нашем репо GitHub . Таким образом, вы можете работать над теми отдельными задачами на данном этапе, которые вам кажутся более интересными или трудными.

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

Итак, эта третья часть покажет, как использовать машинное обучение для предоставления рекомендаций по винам для пользователей нашего веб-сайта. В первом разделе мы будем использовать Pandas для загрузки данных из CSV-файлов. Таким образом, у нас будут некоторые предварительно сгенерированные данные, которые мы будем использовать для создания наших моделей. Затем, во втором разделе, мы будем использовать наивные критерии рекомендаций, чтобы лучше сосредоточиться на построении представлений и моделей, необходимых для предоставления рекомендаций. Мы закончим учебник, используя K-means кластеризацию в качестве модели машинного обучения, которая использует сходство пользователей, чтобы обеспечить лучшие рекомендации wine.

Помните, что вы можете следовать учебнику на любом этапе разработки, разветвив репо в свою учетную запись GitHub, а затем клонировав его в свою рабочую область и проверив соответствующий тег. Разветвляя репо, вы можете изменять его по своему усмотрению и экспериментировать с ним столько, сколько вам нужно. Если в какой-то момент вам захочется немного помочь с каким-то шагом урока или сделать его своим собственным, мы можем провести сеанс 1:1 Codementor об этом.

Импорт данных из CSV с помощью Pandas

В этом разделе мы будем использовать Pandas для загрузки данных из CSV-файлов. Таким образом, у нас будут некоторые предварительно сгенерированные данные, которые мы будем использовать для создания наших моделей. Мы будем использовать до трех различных CSV-файлов и их соответствующих импортеров. Они будут иметь дело с пользователями, винами и отзывами соответственно.

Файлы данных-это просто файлы, разделенные запятыми, или CSV. Например, так выглядит файл reviews.csv :

id,username,wine_id,rating,comment
0,jadianes,0,4,Beautiful Manzanilla. Great price.
1,jadianes,1,3,Classy Rose. Not great.
2,jadianes,3,4,This can be great with time.
3,jadianes,9,2,Drinkable...
4,jadianes,10,5,A treasure of a wine
5,john,0,2,Tastes like old wine
6,john,1,4,Love it... Luxury!
7,john,3,2,Not a big fan of sweets
8,john,9,3,Could drink this more often... Love the fruit.
9,john,10,2,"too strong, sorry"
10,john,7,4,Powerful and elegant. Masculine.
11,mari,0,3,It reminds me of Sanlucar :)
12,mari,1,2,Not a big fan of bubbles
13,mari,3,5,Love sweets!
14,mari,4,5,The best white wine I ever had.
15,yasset,0,4,It is good
16,yasset,1,2,I don't like champagne
17,yasset,3,1,I don't like sweet wine
18,yasset,4,4,Very good wine.
19,yasset,6,5,So good wine
20,carlos,0,4,Se sale
21,carlos,3,4,Viva Malaga
22,carlos,7,5,Wonderful
23,teus,0,4,This is very special wine
24,teus,10,5,Wow!
25,teus,3,5,"Hey, this is great stuff!"
26,teus,5,4,This is going to be in my memory for a very long time
27,lluis,0,4,"Chalk, almonds, rain"
28,lluis,2,4,"Dry fruit, lead, iron, dry flowers."
29,lluis,5,5,"Rioja, rioja, rioja"
30,lluis,8,4,"God!"
31,pepe,10,5,"Jooe!"
32,pepe,6,4,"Vega Siclia..."
33,pepe,0,4,"Esto y unas gambitas!"
34,pepe,1,2,"No esta mal"
35,pepe,2,4,"Muy bueno"

Файл должен быть понятным, так как первая строка содержит имена полей. Каждая строка файла-это обзор вина, который мы хотим поместить в нашу базу данных. У нас есть идентификатор обзора, имя пользователя, делающего этот обзор, идентификатор wine (который пересекается с файлом wine.csv ), рейтинг и комментарий.

Для чтения файла мы используем метод Pandas dataframe read_csv . Затем мы будем использовать apply над каждой строкой в фрейме данных и создадим экземпляр Review , используя наши объекты модели review. Но нам лучше взглянуть на код.

import sys, os 
import pandas as pd
import datetime

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "winerama.settings")

import django
django.setup()

from reviews.models import Review, Wine 


def save_review_from_row(review_row):
    review = Review()
    review.id = review_row[0]
    review.user_name = review_row[1]
    review.wine = Wine.objects.get(id=review_row[2])
    review.rating = review_row[3]
    review.pub_date = datetime.datetime.now()
    review.comment = review_row[4]
    review.save()
    
    
# the main function for the script, called by the shell    
if __name__ == "__main__":
    
    # Check number of arguments (including the command name)
    if len(sys.argv) == 2:
        print "Reading from file " + str(sys.argv[1])
        reviews_df = pd.read_csv(sys.argv[1])
        print reviews_df

        # apply save_review_from_row to each review in the data frame
        reviews_df.apply(
            save_review_from_row,
            axis=1
        )

        print "There are {} reviews in DB".format(Review.objects.count())
        
    else:
        print "Please, provide Reviews file path"

Сценарий начинается с проверки длины аргумента, а затем создает фрейм данных с помощью read_csv , как описано выше. Затем мы печатаем содержимое фрейма данных и применяем функцию save_review_from_row над axis=1 (на строку). Функция определена ранее и в основном использует review.models.Review для создания нового экземпляра review из необработанных данных. Обратите внимание на то, как мы используем идентификатор wine для поиска экземпляра wine в Wine.objects.get(id=review_row[2]) . Это означает, что нам нужно загрузить вина, прежде чем загружать обзоры. Мы также должны загружать пользователей перед обзорами, но поскольку мы не ссылаемся на объекты пользователей из обзоров, а используем имена пользователей напрямую, это не обязательно.

Два других файла данных и скрипта эквивалентны, и лучше, если вы посмотрите на репо и поймете их. Поместите три скрипта в корень проекта (рядом с manage.py ) и файлы данных в папке с именем data в корневом каталоге проекта. Затем нам нужно запустить каждый сценарий следующим образом.

python load_users.py data/users.csv
python load_wines.py data/wines.csv
python load_reviews.py data/reviews.csv

Если все пойдет хорошо (вы увидите некоторые предупреждения), в последних строках каждого скрипта выводится количество записей в базе данных. Должно быть согласовано с количеством записей в файлах csv , плюс один пользователь из-за пользователя admin .

Этот этап проекта соответствует тегу stage-2.1 .

Создание представления рекомендаций с базовой моделью

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

Представление рекомендаций

Мы делали это несколько раз. Просто зайдите в views.py и добавьте следующую функцию.

@login_required
def user_recommendation_list(request):
    return render(request, 'reviews/user_recommendation_list.html', {'username': request.user.username})

Как вы видите, это представление просто отображает шаблон reviews/user_recommendation_list.html и передает имя пользователя запроса. Шаблон, который мы визуализируем, является просто расширением карты вин, как вы можете видеть.

{% extends 'reviews/wine_list.html' %}

{% block title %}

Recommendations for {{ username }}

{% endblock %}

Мы просто переопределяем название оригинального шаблона винной карты, чтобы отобразить имя пользователя. Позже мы изменим функцию просмотра, чтобы также передать список вин, чтобы шаблон мог его отобразить. Но сначала давайте добавим сопоставление URL-адресов в reviews/urls.py таким образом, это выглядит так, как выглядит в следующем файле GitHub на этапе-2.2 (у нас возникли проблемы с рендерингом этого кода, вырезанного здесь с помощью учебника прямо сейчас).

Наше новое сопоставление сопоставляет URL рекомендацию/ с представлением, которое мы только что определили в views.py .

Наконец, давайте добавим ссылку в строке меню, чтобы зарегистрированный пользователь мог перейти к его рекомендациям. Перейдите и измените элемент в templates/base.html чтобы выглядеть следующим образом (если вы не видите html-теги, отображаемые в следующем фрагменте, посмотрите файл репо GitHub на этапе 2.2 ).


введите описание изображения здесь

Мы добавили новую ссылку, которая будет отображаться при входе пользователя в систему. Если это так, пункт меню Wine suggestions позволит пользователю перейти к представлению, которое мы только что реализовали.

Этот этап проекта соответствует тегу stage-2.2 .

Возврат вин, не просмотренных пользователем

Во-первых, не очень умный механизм рекомендаций может просто возвращать вина, которые пользователь никогда раньше не просматривал. Это будет равносильно совершенно невежественному продавцу винного магазина, который, когда его просят порекомендовать вино для сегодняшнего ужина, сначала спрашивает, что мы пробовали, а затем предлагает что-то с другим названием, на самом деле не учитывая стиль вина или наш вкус.

Мы можем сделать это очень легко в нашем user_recommendation_list следующим образом.

@login_required
def user_recommendation_list(request):
    # get this user reviews
    user_reviews = Review.objects.filter(user_name=request.user.username).prefetch_related('wine')
    # from the reviews, get a set of wine IDs
    user_reviews_wine_ids = set(map(lambda x: x.wine.id, user_reviews))
    # then get a wine list excluding the previous IDs
    wine_list = Wine.objects.exclude(id__in=user_reviews_wine_ids)

    return render(
        request, 
        'reviews/user_recommendation_list.html', 
        {'username': request.user.username,'wine_list': wine_list}
    )

Это требует небольшого объяснения. Сначала мы создаем набор запросов для всех отзывов для текущего пользователя. Мы также предварительно выбираем объекты wine, чтобы избежать последовательных запросов для каждого обзора, так как нам нужно будет получить доступ к этим объектам wine (подробнее о prefetch_related ). Затем мы создаем набор всех различных винных детей, используя map , чтобы применить лямбда-выражение к каждому обзору в предыдущем результате и получить идентификатор вина. И, наконец, мы создаем новый набор запросов Wine , исключающий все предыдущие идентификаторы. Важным моментом здесь является то, как мы суффиксируем поле id с __in , чтобы реализовать функциональность SQL IN . Подробнее об этом здесь . Полученный список-это то, что мы передаем в наш шаблон рендеринга. Мощно, не правда ли?

Этот этап проекта соответствует тегу stage-2.3 .

Использование кластеризации k-средних для предоставления лучших рекомендаций

То, что мы сделали до сих пор с точки зрения рекомендаций по вину, не особенно впечатляет. Любой из нас может попытаться произвести впечатление на подругу, порекомендовав вино, которое мы недавно попробовали, а она нет. Много раз мы делаем это, на самом деле не зная предпочтений нашей подруги и, следовательно, не зная, понравится ли ей наши рекомендации.

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

Существует второй подход, который не требует никаких знаний о вине, но нужно только знать, что нравится и не нравится другим людям (а не только нашему другу/клиенту). Зная это, мы просто пытаемся найти человека с похожими предпочтениями на нашего друга. Затем мы спрашиваем у этого второго человека ее любимые вина и предлагаем их нашему первому другу, не включая те, которые наш первый друг уже пробовал. Мы просто выступаем в качестве посредника, и компьютерная система здесь может выполнить эту работу лучше, чем любой человек, поскольку она может “спросить” миллионы людей за короткое время.

Это то, что будет делать наша система, и для этого мы будем использовать кластеризацию. Почему? Простой. Вместо того, чтобы пытаться сравнивать нашего пользователя с каждым другим пользователем в системе каждый раз, когда требуются рекомендации, мы предварительно сгруппируем всех пользователей в системе по оценкам отзывов о вине. Таким образом, у нас будут группы похожих пользователей. Затем, когда пользователь запрашивает рекомендации, мы будем искать их только в кластере, в котором находится этот пользователь. Поскольку мы знаем, что все пользователи в этом кластере имеют одинаковый вкус (они одинаково оценивали одни и те же вина, хорошие или плохие), это значительно сокращает пространство поиска.

Конечно, существуют более сложные рекомендательные системы (например, коллаборативная фильтрация с использованием ALS), но эта система легко понятна с точки зрения ее значения и использует некоторые методы машинного обучения, о которых мы уже знаем.

Создание объекта модели для хранения информации о кластеризации

Для реализации ранее упомянутого подхода нам необходимо отслеживать кластер, к которому принадлежит пользователь. Таким образом, мы сможем отфильтровать представление, которое у нас уже есть (то, которое возвращает вина, не просмотренные пользователем), по идентификатору кластера и отсортировать их по баллам. Если мы предположим, что идентификатор кластера был ранее назначен нашим алгоритмом кластеризации, весь этот процесс даст нам предложения wine, которые удовлетворяют двум условиям:

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

Мы могли бы хранить это вместе с остальной информацией пользователя, но для этого нам нужно будет предоставить пользовательский объект пользователя. Кроме того, если мы сделаем это, нам потребуется восстановить все таблицы базы данных, связанные с аутентификацией (этот процесс не поддерживает миграцию), и это является серьезной реструктуризацией нашей системы. Итак, в качестве обходного пути мы создадим новый объект модели Пользовательский кластер , в котором хранятся ссылки на пользовательские объекты.

Перейти на наш reviews/models.py и добавьте следующий класс.

class Cluster(models.Model):
    name = models.CharField(max_length=100)
    users = models.ManyToManyField(User)

    def get_members(self):
        return "\n".join([u.username for u in self.users.all()])

Для кластера мы храним имя и список пользователей. Мы оставляем дверь открытой для пользователей, принадлежащих более чем к одному кластеру, используя это ManyToManyField . Мы также определяем метод для получения всех имен пользователей-членов get_members .

Поскольку мы изменили наш слой модели, нам необходимо перенести таблицы базы данных. Из корневой папки проекта выполните следующие две команды.

python manage.py makemigrations

и

python manage.py migrate

И хотя это не будет основным инструментом для управления информацией о кластере, мы собираемся добавить класс модели в наш интерфейс администратора. Отредактируйте reviews/admin.py файл выглядит следующим образом.

from django.contrib import admin

from .models import Wine, Review, Cluster

class ReviewAdmin(admin.ModelAdmin):
    model = Review
    list_display = ('wine', 'rating', 'user_name', 'comment', 'pub_date')
    list_filter = ['pub_date', 'user_name']
    search_fields = ['comment']
    

class ClusterAdmin(admin.ModelAdmin):
    model = Cluster
    list_display = ['name', 'get_members']

    
admin.site.register(Wine)
admin.site.register(Review, ReviewAdmin)
admin.site.register(Cluster, ClusterAdmin)

Мы только что импортировали и зарегистрировали класс модели Cluster и связанный с ним класс Cluster Admin , который будет лучше визуализировать информацию о кластере (имя и члены) в интерфейсе администратора. Итак, перейдите в интерфейс администратора и создайте три кластера со следующими членами из доступных нам пользователей:

  • 1: джадианес, карлос и луис
  • 2: джон, теус, актив
  • 3: пепе, мари

Если у вас возникли проблемы с этим, просто проверьте тег для этого этапа проекта, который является stage-2.4 и содержит весь предыдущий код и информацию в базе данных.

Использование информации о кластере в представлении рекомендаций

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

@login_required
def user_recommendation_list(request):
    
    # get request user reviewed wines
    user_reviews = Review.objects.filter(user_name=request.user.username).prefetch_related('wine')
    user_reviews_wine_ids = set(map(lambda x: x.wine.id, user_reviews))

    # get request user cluster name (just the first one righ now)
    user_cluster_name = \
        User.objects.get(username=request.user.username).cluster_set.first().name
    
    # get usernames for other members of the cluster
    user_cluster_other_members = \
        Cluster.objects.get(name=user_cluster_name).users \
            .exclude(username=request.user.username).all()
    other_members_usernames = set(map(lambda x: x.username, user_cluster_other_members))

    # get reviews by those users, excluding wines reviewed by the request user
    other_users_reviews = \
        Review.objects.filter(user_name__in=other_members_usernames) \
            .exclude(wine__id__in=user_reviews_wine_ids)
    other_users_reviews_wine_ids = set(map(lambda x: x.wine.id, other_users_reviews))
    
    # then get a wine list including the previous IDs, order by rating
    wine_list = sorted(
        list(Wine.objects.filter(id__in=other_users_reviews_wine_ids)), 
        key=lambda x: x.average_rating, 
        reverse=True
    )

    return render(
        request, 
        'reviews/user_recommendation_list.html', 
        {'username': request.user.username,'wine_list': wine_list}
    )

Здесь многое происходит:

  1. Сначала мы получаем список винных детей, просмотренных пользователем-запросчиком, как и раньше.
  2. Затем мы получаем имя кластера, к которому принадлежит пользователь. Мы делаем это с помощью поля User.objects.get(..).cluster_set , которое ссылается на пользовательскую сторону отношения “многие ко многим”, которое мы имеем с кластерами. Мы также исключаем пользователя, запрашивающего запрос, из этого списка. Это не является строго необходимым из-за того, что мы собираемся делать дальше, но может сократить время запроса.
  3. Затем мы используем предыдущий список имен, чтобы получить отзывы для тех пользователей в кластере, исключая те отзывы, которые относятся к винам, которые мы получили на шаге 1. В результате мы получаем список идентификаторов вин.
  4. Наконец, мы используем предыдущий список идентификаторов, чтобы получить все вина и отсортировать их по средней ставке.

Теперь мы готовы перейти на нашу страницу предложений по вину и увидеть что-то, что использует сходство пользователей. Нам просто нужно сгенерировать эти кластеры с помощью k-средних, а не вручную, и все готово!

Этот этап урока соответствует тегу stage-2.5 .

Кластеризация пользователей

Таким образом, это последний шаг для того, чтобы наша система предоставляла предложения, основанные на сходстве пользователей. Что касается пользовательского интерфейса, у нас все на месте благодаря работе предыдущих разделов. Нам просто нужно немного настроить уровень представления, чтобы решить, когда выполнять кластеризацию k-средних, а затем нам нужно написать фактический код кластеризации. Если вы хотите узнать больше о кластеризации k-means в Python, ознакомьтесь с нашим учебником о том, как это сделать с помощью R и Python .

Так что все по порядку. Когда мы вычисляем, к какому кластеру принадлежит пользователь? Давайте поставим хотя бы пару ограничений:

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

Первое ограничение может быть выполнено, если мы вызовем обновление назначений кластеров в представлении, которое обрабатывает добавление обзоров wine. Итак, давайте отредактируем файл reviews/views/py , чтобы метод add_review выглядел следующим образом.

@login_required
def add_review(request, wine_id):
    wine = get_object_or_404(Wine, pk=wine_id)
    form = ReviewForm(request.POST)
    if form.is_valid():
        rating = form.cleaned_data['rating']
        comment = form.cleaned_data['comment']
        user_name = request.user.username
        review = Review()
        review.wine = wine
        review.user_name = user_name
        review.rating = rating
        review.comment = comment
        review.pub_date = datetime.datetime.now()
        review.save()
        update_clusters()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('reviews:wine_detail', args=(wine.id,)))
    
    return render(request, 'reviews/wine_detail.html', {'wine': wine, 'form': form})
    

Если мы сравним с предыдущей версией, то увидим, что мы только что добавили вызов update_clusters() . Это новая функция, которую мы определим в отдельном reviews/suggestions.py файл, и нам нужно его импортировать. Поэтому добавьте следующий импорт в reviews/views.py .

from .suggestions import update_clusters

А затем создайте и отредактируйте файл reviews/suggestions.py итак, это выглядит следующим образом.

from .models import Review, Wine, Cluster
from django.contrib.auth.models import User
from sklearn.cluster import KMeans
from scipy.sparse import dok_matrix, csr_matrix
import numpy as np

def update_clusters():
    num_reviews = Review.objects.count()
    update_step = ((num_reviews/100)+1) * 5
    if num_reviews % update_step == 0: # using some magic numbers here, sorry...
        # Create a sparse matrix from user reviews
        all_user_names = map(lambda x: x.username, User.objects.only("username"))
        all_wine_ids = set(map(lambda x: x.wine.id, Review.objects.only("wine")))
        num_users = len(all_user_names)
        ratings_m = dok_matrix((num_users, max(all_wine_ids)+1), dtype=np.float32)
        for i in range(num_users): # each user corresponds to a row, in the order of all_user_names
            user_reviews = Review.objects.filter(user_name=all_user_names[i])
            for user_review in user_reviews:
                ratings_m[i,user_review.wine.id] = user_review.rating

        # Perform kmeans clustering
        k = int(num_users / 10) + 2
        kmeans = KMeans(n_clusters=k)
        clustering = kmeans.fit(ratings_m.tocsr())
        
        # Update clusters
        Cluster.objects.all().delete()
        new_clusters = {i: Cluster(name=i) for i in range(k)}
        for cluster in new_clusters.values(): # clusters need to be saved before referring to users
            cluster.save()
        for i,cluster_label in enumerate(clustering.labels_):
            new_clusters[cluster_label].users.add(User.objects.get(username=all_user_names[i]))

Давайте объясним процесс кластеризации. Функция update_clusters выполняет назначение кластера в три этапа и обновляется только в том случае, если общее количество просмотров в системе удовлетворяет определенному уравнению (подробнее об этом позже):

  1. Создайте разреженную матрицу, используя оценки отзывов пользователей. Эта матрица необходима для выполнения кластеризации k-средних. Для построения матрицы нам необходимо получить:
  • Получите список имен пользователей. У нас будет строка для каждого пользователя в нашей матрице.
  • Получите список уникальных идентификаторов вина. У нас будет столбец для каждого вина в нашей матрице.
  • Каждый элемент (i,j) в нашей матрице содержит рейтинг пользователя i для вина j. Имя пользователя для пользователя i будет указано по позиции этого имени в нашем списке имен пользователей.
  • Обратите внимание, что мы используем класс dok_matrix из scipy , чтобы легко построить разреженную матрицу. Прочтите документацию, если хотите узнать об этом больше. Наш код совсем не сложный и просто в своей матрице с правильными размерами, а затем присваивает оценки правильным элементам.
  1. Выполните кластеризацию k-средних. Некоторые замечания:
  • Здесь мы используем некоторые магические числа, чтобы иметь по крайней мере три кластера или более. Общее число будет зависеть от того, сколько пользователей, разделенных на 10, у нас есть в системе. Это далеко не основано на реальной кластерной структуре и должно быть улучшено в производственной системе. Мы просто предполагаем, что чем больше у нас пользователей, тем больше вероятность, что у них будут разные вкусы.
  • Обратите также внимание, что мы преобразуем наш dok_matrix в csr_matrix , который лучше подходит для вычислений, необходимых для кластеризации k-средних.
  1. Наконец, мы обновляем назначения кластеров в нашей базе данных. Для того, чтобы сделать это:
  • Сначала мы удаляем все предыдущие кластеры.
  • Затем мы создаем и сохраняем новые кластеры без назначений пользователей. Нам нужно сохранить их, если мы хотим создать экземпляр отношений “многие ко многим” с пользователями на следующем шаге.
  • Для каждого назначения метки в результатах кластеризации k-средних мы добавляем пользователя в нужный кластер. Django автоматически сохранит отношение многие ко многим .

Мы почти на месте. Нам просто нужно небольшое обновление в нашем представлении “получить предложения”. В случае, когда назначение кластера не было выполнено (например, новый пользователь зарегистрирован в системе), нам нужно уловить эту ситуацию и вызвать наш новый метод update_clusters . Перейти и отредактировать reviews/views.py таким образом, метод user_recommendation_list выглядит следующим образом:

@login_required
def user_recommendation_list(request):
    
    # get request user reviewed wines
    user_reviews = Review.objects.filter(user_name=request.user.username).prefetch_related('wine')
    user_reviews_wine_ids = set(map(lambda x: x.wine.id, user_reviews))

    # get request user cluster name (just the first one righ now)
    try:
        user_cluster_name = \
            User.objects.get(username=request.user.username).cluster_set.first().name
    except: # if no cluster has been assigned for a user, update clusters
        update_clusters()
        user_cluster_name = \
            User.objects.get(username=request.user.username).cluster_set.first().name
    
    # get usernames for other memebers of the cluster
    user_cluster_other_members = \
        Cluster.objects.get(name=user_cluster_name).users \
            .exclude(username=request.user.username).all()
    other_members_usernames = set(map(lambda x: x.username, user_cluster_other_members))

    # get reviews by those users, excluding wines reviewed by the request user
    other_users_reviews = \
        Review.objects.filter(user_name__in=other_members_usernames) \
            .exclude(wine__id__in=user_reviews_wine_ids)
    other_users_reviews_wine_ids = set(map(lambda x: x.wine.id, other_users_reviews))
    
    # then get a wine list including the previous IDs, order by rating
    wine_list = sorted(
        list(Wine.objects.filter(id__in=other_users_reviews_wine_ids)), 
        key=lambda x: x.average_rating, 
        reverse=True
    )

    return render(
        request, 
        'reviews/user_recommendation_list.html', 
        {'username': request.user.username,'wine_list': wine_list}
    )

В принципе, мы добавили предложение try-except , чтобы иметь дело с несуществующими назначениями кластера для пользователя при получении первого кластера. В этом случае мы сначала вызываем update_clusters , а затем снова извлекаем кластер.

Поэтому мы готовы попробовать. Наш подход имеет некоторые ограничения. Один из них заключается в том, что если у пользователя есть более одного отзыва на одно и то же вино, будет использоваться только один из них. Еще одно ограничение заключается в том, что все это работает лучше, если есть несколько вин, которые были просмотрены как можно большим количеством пользователей. Они будут эквивалентны очень популярным винам, о которых знает большинство людей, или очень популярным классическим фильмам, если бы мы создавали сайт фильмов. На самом деле, если вы когда-либо пользовались такими сайтами, как Netflix, вы бы заметили, что они заставляют вас пройти через серию фильмов, которые вы можете оценить еще до завершения процесса регистрации. В нашем случае мы просто загрузим больше данных, используя наши сценарии загрузки данных. Мы добавили еще несколько отзывов в наш файл data/reviews.csv . Либо вы проверяете последний тег на этапе репо -3 – и в этом случае у вас уже будут данные в базе данных – или вы получите (даже копирование и вставка будут работать) этот отдельный файл в

python load_reviews.py data/reviews.csv

Этот этап урока соответствует тегу stage-3 .

Вывод

Так что на сегодня все. В этом третьем уроке мы объяснили, как предоставлять рекомендации по вину для пользователей нашего веб-сайта. Мы сделали это, постепенно создавая необходимые модели и представления вокруг очень простого механизма рекомендаций. Затем мы использовали кластеризацию K-средних в качестве модели машинного обучения, которая использовала сходство пользователей, чтобы обеспечить лучшие рекомендации wine. Наша модель будет работать лучше с большим количеством отзывов пользователей, поэтому создайте свою и добавьте в нее друзей, чтобы увидеть, как она себя ведет. Сделав это, вы сможете преодолеть его ограничения, повысить его точность и многому научиться!

Возможно, K-means-не самая обычная модель машинного обучения при построении рекомендательных систем. Однако у него есть некоторые хорошие характеристики. Это быстрый алгоритм кластеризации, который имеет параллельные и масштабируемые реализации (например, см. Spark ). И в целом, очень легко понять, что делает K-means и как это работает. Кластер пользователей-это просто группа пользователей, близких друг к другу в зависимости от того, как они оценивают элементы. Для получения более сложных и популярных альтернатив см. Наш учебник по Spark и совместной фильтрации .

Помните, что вы можете следовать учебнику на любом этапе разработки, разветвив репо в свою учетную запись GitHub, а затем клонировав его в свою рабочую область и проверив соответствующий тег. Разветвляя репо, вы можете изменять его по своему усмотрению и экспериментировать с ним столько, сколько вам нужно. Если в какой-то момент вам захочется немного помочь с каким-то шагом урока или сделать его своим собственным, мы можем провести сеанс 1:1 Codementor об этом!