我正在构建一个音乐推荐引擎,该引擎使用曲目的歌词来确定歌曲在情感上彼此之间的紧密关系。我已经使用tfidf算法(这里没有显示)为每首歌曲生成一个分数,我想在django模型字段tfidf中存储每个轨道的tfidf分数。但是我希望将每个tfidf分数标准化为0-1。
我遇到的困难在于,只要有人在管理界面输入tfidf值,就会知道如何自动规范化这些tfidf分数。因此,想象一下,您已进入管理界面,并希望将歌曲“In Da Club”添加到数据库中。您输入歌曲的名称及其tfidf分数如下:
我想要做的是确保只要点击“保存”按钮,它就会自动使用规范化值填充空的normalized_tfidf列。我正在使用一个简单的算法来规范化tfidf值。在我进入之前,让我告诉你这个表的样子,以便你更清楚地了解算法的作用。因此,在将“In Da Club”添加到数据库中(并且数据已经规范化)之后,表格列应如下所示:
Song x和song y只是我在数据库中播放的虚拟歌曲,用于设置算法的上限和下限。您看到的.50077值就是我想要自动生成的值。
算法说要找到歌曲x中特征tfidf的归一化值(nv),找出歌曲的tfidf得分与表格中最小的tfidf得分之间的差异,并将其除以最大值和最小值之间的差值表中的tfidf得分。这是数学上的。
nv(在俱乐部 tfidf )=在俱乐部 tfidf - tfidf min / tfidf max - TFIDF 分钟
这是计算:
nv(在俱乐部)= .25048 - .00010 / .50000 - .00010 = .50077
所以我正在尝试将其编码到我的模型中。问题是django似乎没有允许我按照SQL语句的方式在表中选择最小和最大tfidf值的方法。我对django很新,并不完全了解它的能力。如果我的这个表的模型看起来像我下面的那样,那么最好的方法是重写它,以便一旦你输入管理员,tfidf会自动规范化吗?
答案 0 :(得分:1)
保存模型时有两种方法可以触发某些操作:覆盖save
方法,或编写post_save
侦听器。我将展示覆盖方法,因为它有点简单,并且非常适合这个用例。
要获得最大/最小值,您可以使用Django的queryset aggregation functions:
from django.db.models import Max, Min
class Party(models.Model):
...
def save(self, *args, **kwargs):
max = Party.objects.all().aggregate(Max('tfidf'))['tfidf__max']
min = Party.objects.all().aggregate(Min('tfidf'))['tfidf__min']
self.normalized_tfidf = (self.tfidf - min) / (max - min)
super(Party, self).save(*args, **kwargs)
覆盖save
之类的默认模型方法非常简单,但如果您有兴趣,还可以获得更多信息here。
请注意,如果您在任何时候执行bulk updates到Party.tfidf
,则不会调用保存处理程序(或者发送post_save信号),因此您必须处理手动的所有行 - 这意味着大量的数据库写入,并且几乎使批量更新毫无意义。
答案 1 :(得分:0)
为了防止出现陈旧数据等问题,正如@klaws 在上面的评论中提到的,在添加新歌曲时计算归一化值可能并不理想。
相反,您可以使用查询让数据库在需要时计算标准化值。
您需要从 django 的 expressions 和 aggregates 中导入一些内容:
from django.db.models import Window, F, Min, Max
这是一个简单的例子,适用于 OP 的问题,假设不需要分组:
def query_normalized_tfidf(party_queryset):
w_min = Window(expression=Min('tfidf'))
w_max = Window(expression=Max('tfidf'))
return party_queryset.annotate(
normalized_tfidf=(F('tfidf') - w_min) / (w_max - w_min))
Window
类允许我们继续注释单个对象,如解释,例如here 和 Django 的 docs。
我们也可以将其添加到自定义 model manager 中,而不是使用单独的查询函数。
如果您需要针对某些组计算归一化值(例如,如果歌曲有 genre
),则可以对上述内容进行扩展和概括,如下所示:
def query_normalized_values(queryset, value_lookup, group_lookups=None):
"""
a generalized version that normalizes data with respect to the
extreme values within each group
"""
partitions = None
if group_lookups:
partitions = [F(group_lookup) for group_lookup in group_lookups]
w_min = Window(expression=Min(value_lookup), partition_by=partitions)
w_max = Window(expression=Max(value_lookup), partition_by=partitions)
return queryset.annotate(
normalized=(F(value_lookup) - w_min) / (w_max - w_min))
这可以如下使用,假设会有一个 Party.genre
字段:
annotated_parties = query_normalized_values(
queryset=Party.objects.all(), value_lookup='tfidf',
group_lookups=['genre'])
这将使 tfidf
值相对于每个 tfidf
内的极端 genre
值标准化。
注意:在除以零的特殊情况下(当 w_min
等于 w_max
时),结果“归一化值”将为 None
。