技术拆解:个人写的音乐推荐系统Embeat如何打败音乐巨头网易云?
GD-Studio
编辑于 2026年06月26日 13:30

开源仓库

  • gdstudio-org/Embeat

网页链接​

项目初衷

  • 被Spotify的歌曲推荐机制震撼到,能给出非常精准的推荐,即便推荐的冷门歌曲,也非常对胃

  • 我的GD音乐台没有推荐机制,只能播放用户搜索结果或用户自定义歌单,缺乏交互性

  • 一直都想尝试自己从零开始构建模型相关代码,这种实践比微调模型能学到更多东西

  • 项目开始时间:2026-01-22

Spotify的推荐机制

https://medium.com/biased-algorithms/contrastive-learning-for-sequential-recommendation-f4744d75128a

https://www.music-tomorrow.com/blog/how-spotify-recommendation-system-works-complete-guide

协同过滤与矩阵分解

  • 参考文章:https://medium.com/@rohitlokwani17/evolution-of-collaborative-filtering-algorithms-with-implicit-feedback-for-large-scale-recommended-e7b2e2d18d99

  • 如果用户 A 和用户 B 在过去喜欢相似的歌曲,那么用户 A 很可能也会喜欢用户 B 喜欢的其他歌曲

  • 早期的推荐系统常使用基于近邻的方法,但在亿级规模下,计算用户间相似度的成本过高。Spotify 转向了基于模型的协同过滤,特别是矩阵分解技术

  • 使用两个因子矩阵代表用户听歌习惯与歌曲风格,如使用两个 40 * 256 维的向量,其中256是embedding维度,40代表了一些隐含信息,如歌曲energy、happiness等(实际上没有具体的语义标签,是模型自己学习出来的)。然后计算用户矩阵与歌曲矩阵的乘积得到推荐度得分

  • 实际举例:用户喜欢听摇滚,他的energy为0.8。一首摇滚曲的energy为0.9,而摇篮曲的energy为0.1,用户矩阵与歌曲矩阵相乘,摇滚曲的得分更高,因此系统倾向于推荐那首摇滚曲

  • 带条件的矩阵分解:引入两个偏置项,用户偏置Bu与歌曲偏置Bi。对于Bu,某些用户天生听歌更多,偏置项确保模型不会仅仅因为某人听得多就认为他对所有歌曲都有高偏好;对于Bi,热门歌曲被播放的概率天然更高。模型需要剥离这种“全局流行度”,以挖掘用户独特的个性化品味

  • 为了在数亿用户和歌曲上训练该模型,Spotify采用了交替梯度下降,利用Hadoop和Spark集群进行大规模并行计算

  • 局限性:小众歌曲难以突围、缺乏上下文联系(无法很好说明为什么两首歌曲相关)

NLP自然语言处理

  • 参考文章:https://beatstorapon.com/blog/ultimate-guide-to-spotify-music-algorithm/

  • Spotify的爬虫全天候扫描互联网上的公开文本资源,构建庞大的音乐语料库。主要来源包括:音乐博客与评论网站、新闻与媒体报道、社交媒体讨论、论坛帖子、用户播放列表标题

  • 引申:Spotify没有评论区,理论上网易云也可以爬取自己用户发表的评论,来理解歌曲的社交属性

  • Spotify当时做这部分研究还比较早,用的是word2vec,通过文本数据清洗提取关键词,得到歌曲与关键词的匹配关系。举例:如果Daft Punk经常与French House、Robot、Funky同时出现,这些词就构成了该艺人的语义描述向量

  • 这些向量是时变的。如果一位流行歌手突然发布了一张实验性的爵士专辑,互联网上的讨论词汇会发生变化,NLP模型会捕捉到这种风格转移,并迅速调整该艺人在向量空间中的位置,从而将其推荐给爵士乐迷,而不仅仅是其原本的流行乐迷

  • 这里贴一个网站,前Spotify研究员制作的歌手与风格的匹配关系:https://everynoise.com/

原始音频分析

  • 参考文章:https://onlyoneaman.medium.com/unleashing-the-power-of-audio-features-with-the-spotify-api-c544fda1af40

  • Spotify的杀手锏,同时也是Embeat项目最重要的技术支柱

  • Spotify为开发者提供了一个用于获取音频特征的API,但在25年2月就关闭了。API输入Spotify的track_id,输出以下内容:

代码块
markdown
自动换行
复制代码
Key (调性):如C、D、E调。
Mode (调式):大调、小调。
Tempo (速度):如120BPM。
Time Signature (节拍):如四三拍、四四拍。
Loudness (响度):基于 EBU R.128 标准的响度值。
Danceability (可舞性):基于节拍稳定性、强度。
Energy (能量):强度、响度、动态范围。
Valence (情绪效价):音乐的积极/消极程度(快乐 vs 悲伤)。
Instrumentalness (器乐性):人声占比。
Acousticness (原声性):是否使用电子合成器。
Liveness (现场感):是否为现场录音。
Speechiness (现场感):是否为现场录音。
复制成功

核心数据

数据来源

  • 去年12月底,从小飞机群了解到某黑客组织公开了Spotify的全站元数据,后续据说还会公开所有歌曲的160kbps ogg数据。其中元数据120GB,未公开的歌曲数据300TB

  • 黑客公开的元数据包含这几大类:歌曲基本信息、歌手基本信息、专辑基本信息、歌曲音频分析

  • 元数据内容:(两份主要文件)

代码块
SQL
自动换行
复制代码
CREATE TABLE `artists` (
  `rowid` integer PRIMARY KEY NOT NULL,
  /* The original Spotify base62 ID. */
  `id` text NOT NULL,
  /* When the item was fetched (unixepoch ms). */
  `fetched_at` integer NOT NULL,
  /* "The name of the artist."*/
  `name` text NOT NULL,
  /* followers.total - "The total number of followers." */
  `followers_total` integer NOT NULL,
  /* "The popularity of the artist. The value will be between 0 and 100, with 100 being the most popular. The artist's popularity is calculated from the popularity of all the artist's tracks." */
  `popularity` integer NOT NULL
  );
/* "A list of the genres the artist is associated with. If not yet classified, the array is empty." */
CREATE TABLE `artist_genres` (
  `artist_rowid` integer NOT NULL,
  `genre` text NOT NULL,
  FOREIGN KEY (`artist_rowid`) REFERENCES `artists`(`rowid`)
  );
/* Images of the artist in various sizes, widest first. */
CREATE TABLE `artist_images` (
  `artist_rowid` integer NOT NULL,
  `width` integer NOT NULL,
  `height` integer NOT NULL,
  `url` text NOT NULL,
  FOREIGN KEY (`artist_rowid`) REFERENCES `artists`(`rowid`)
  );
/*

* Information about artist-albums relationships from /artists/{id}/albums, album.artists[ ] and album.tracks[ ].artists[ ].

* The relationships "album", "single", "compilation" are left out because they can be reconstructed from `album.type`.
*/
CREATE TABLE "artist_albums" (
  `artist_rowid` integer NOT NULL,
  `album_rowid` integer NOT NULL,
  /* True if this link was retrieved from /artists/{id}/albums with an "album_group" response of "appears_on". */
  `is_appears_on` integer NOT NULL,
  /* True if this link is based on the actual artists of each track in the album. Only exists if the link is not explicit (above). */
  `is_implicit_appears_on` integer NOT NULL,

  /* If neither is_appears_on or is_implicit_appears_on, the index of album.data.artists[ ] this was retrieved from. */

  `index_in_album` integer,
  FOREIGN KEY (`artist_rowid`) REFERENCES `artists`(`rowid`),
  FOREIGN KEY (`album_rowid`) REFERENCES `albums`(`rowid`)
  );
/* combinations of available markets in a separate table to save space */
CREATE TABLE `available_markets` (
  `rowid` integer PRIMARY KEY NOT NULL,
  /* comma separated ISO 3166-1 alpha-2 country codes. */
  `available_markets` text NOT NULL
  );
/* /albums/{id} - "Get Spotify catalog information for a single album." */
CREATE TABLE `albums` (
  `rowid` integer PRIMARY KEY NOT NULL,
  /* The original Spotify base62 ID. */
  `id` text NOT NULL,
  /* When the item was fetched (unixepoch ms). */
  `fetched_at` integer NOT NULL,
  /* "The name of the album. In case of an album takedown, the value may be an empty string." */
  `name` text NOT NULL,
  /* 'The type of the album. Allowed values: "album", "single", "compilation"' */
  `album_type` text NOT NULL,
  /* available markets as an index into the available_markets table to save space. - "The markets in which the album is available: ISO 3166-1 alpha-2 country codes. NOTE: an album is considered available in a market when at least 1 of its tracks is available in that market." */
  `available_markets_rowid` integer NOT NULL,
  /* external_id.upc - Universal Product Code */
  `external_id_upc` text,
  /* external_id.amgid - undocumented - AMG MUSIC GROUP Internal ID */
  "external_id_amgid" text,
  /* "The copyright" */
  `copyright_c` text,
  /* "The sound recording (performance) copyright." */
  `copyright_p` text,
  /* "The label associated with the album." */
  `label` text NOT NULL,
  /* "The popularity of the album. The value will be between 0 and 100, with 100 being the most popular." */
  `popularity` integer NOT NULL,
  /* "The date the album was first released." */
  `release_date` text NOT NULL,
  /* 'The precision with which release_date value is known. Allowed values: "year", "month", "day"' */
  `release_date_precision` text NOT NULL,
  /* tracks.total */
  `total_tracks` integer NOT NULL,
  FOREIGN KEY (`available_markets_rowid`) REFERENCES `available_markets`(`rowid`)
  );


/* album.images[ ] - "The cover art for the album in various sizes, widest first." */

CREATE TABLE "album_images" (
  `album_rowid` integer NOT NULL,
  `width` integer NOT NULL,
  `height` integer NOT NULL,
  `url` text NOT NULL,
  FOREIGN KEY (`album_rowid`) REFERENCES `albums`(`rowid`)
  );
/* /tracks/{id} */
CREATE TABLE `tracks` (
  `rowid` integer PRIMARY KEY NOT NULL,
  /* The original Spotify base62 ID. */
  `id` text NOT NULL,
  /* When the item was fetched (unixepoch ms). */
  `fetched_at` integer NOT NULL,
  /* "The name of the track." */
  `name` text NOT NULL,
  /* "A link to a 30 second preview (MP3 format) of the track. Can be null" */
  `preview_url` text,
  `album_rowid` integer NOT NULL,
  /* "The number of the track. If an album has several discs, the track number is the number on the specified disc." */
  `track_number` integer NOT NULL,
  /* http://en.wikipedia.org/wiki/International\_Standard\_Recording\_Code */
  `external_id_isrc` text,
  `external_id_ean` text,
  `external_id_upc` text,
  /* "The popularity of the track. The value will be between 0 and 100, with 100 being the most popular.
  The popularity of a track is a value between 0 and 100, with 100 being the most popular. The popularity is calculated by algorithm and is based, in the most part, on the total number of plays the track has had and how recent those plays are.
  Generally speaking, songs that are being played a lot now will have a higher popularity than songs that were played a lot in the past. Duplicate tracks (e.g. the same track from a single and an album) are rated independently. Artist and album popularity is derived mathematically from track popularity. Note: the popularity value may lag actual popularity by a few days: the value is not updated in real time." */
  `popularity` integer NOT NULL,
  /* A reference into the available_markets table. - "A list of the countries in which the track can be played, identified by their ISO 3166-1 alpha-2 code." */
  `available_markets_rowid` integer NOT NULL,
  /* "The disc number (usually 1 unless the album consists of more than one disc)." */
  `disc_number` integer NOT NULL,
  /* "The track length in milliseconds." */
  `duration_ms` integer NOT NULL,
  /* "Whether or not the track has explicit lyrics ( true = yes it does; false = no it does not OR unknown)." */
  `explicit` integer NOT NULL,
  FOREIGN KEY (`available_markets_rowid`) REFERENCES `available_markets`(`rowid`)
  );
/* "The artists who performed the track." */
CREATE TABLE `track_artists` (
  `track_rowid` integer NOT NULL,
  `artist_rowid` integer NOT NULL,
  FOREIGN KEY (`track_rowid`) REFERENCES `tracks`(`rowid`),
  FOREIGN KEY (`artist_rowid`) REFERENCES `artists`(`rowid`)
  );

CREATE INDEX `artist_genres_artist_id` ON `artist_genres` (`artist_rowid`);
CREATE INDEX `artist_genres_genre` ON `artist_genres` (`genre`);
CREATE INDEX `artist_images_artist_id` ON `artist_images` (`artist_rowid`);
CREATE UNIQUE INDEX `artists_id_unique` ON `artists` (`id`);
CREATE INDEX `artists_name` ON `artists` (`name`);
CREATE INDEX `artists_popularity` ON `artists` (`popularity`);
CREATE INDEX `artists_followers` ON `artists` (`followers_total`);
CREATE INDEX `artist_album_artist_id` ON "artist_albums" (`artist_rowid`);
CREATE INDEX `artist_album_album_id` ON "artist_albums" (`album_rowid`);
CREATE UNIQUE INDEX `albums_id_unique` ON `albums` (`id`);
CREATE INDEX `album_name` ON `albums` (`name`);
CREATE INDEX `album_popularity` ON `albums` (`popularity`);
CREATE UNIQUE INDEX `available_markets_available_markets_unique` ON `available_markets` (`available_markets`);
CREATE INDEX `track_artists_artist_id` ON `track_artists` (`artist_rowid`);
CREATE INDEX `track_artists_track_id` ON `track_artists` (`track_rowid`);
CREATE UNIQUE INDEX `tracks_id_unique` ON `tracks` (`id`);
CREATE INDEX `tracks_popularity` ON `tracks` (`popularity`);
CREATE INDEX `tracks_album` ON `tracks` (`album_rowid`);
CREATE INDEX `album_images_album_id` ON `album_images` (`album_rowid`);
CREATE INDEX tracks_isrc on tracks(external_id_isrc);
复制成功
代码块
SQL
自动换行
复制代码
/* /audio-features/{id} "Get audio feature information for a single track identified by its unique Spotify ID." */
CREATE TABLE `track_audio_features` (
  `rowid` integer PRIMARY KEY NOT NULL,
  `track_id` text NOT NULL,
  `fetched_at` integer NOT NULL,
  /* true if the API returned null for the whole request */
  `null_response` integer NOT NULL,
  /* "The duration of the track in milliseconds." */
  `duration_ms` integer,
  /* 'An estimated time signature. The time signature (meter) is a notational convention to specify how many beats are in each bar (or measure). The time signature ranges from 3 to 7 indicating time signatures of "3/4", to "7/4".' */
  `time_signature` integer,
  /* "The overall estimated tempo of a track in beats per minute (BPM). In musical terminology, tempo is the speed or pace of a given piece and derives directly from the average beat duration." */
  `tempo` integer,
  /* "The key the track is in. Integers map to pitches using standard Pitch Class notation. E.g. 0 = C, 1 = C♯/D♭, 2 = D, and so on. If no key was detected, the value is -1." */
  `key` integer,
  /* "Mode indicates the modality (major or minor) of a track, the type of scale from which its melodic content is derived. Major is represented by 1 and minor is 0." */
  `mode` integer,
  /* "Danceability describes how suitable a track is for dancing based on a combination of musical elements including tempo, rhythm stability, beat strength, and overall regularity. A value of 0.0 is least danceable and 1.0 is most danceable." */
  `danceability` real,
  /* "Energy is a measure from 0.0 to 1.0 and represents a perceptual measure of intensity and activity. Typically, energetic tracks feel fast, loud, and noisy. For example, death metal has high energy, while a Bach prelude scores low on the scale. Perceptual features contributing to this attribute include dynamic range, perceived loudness, timbre, onset rate, and general entropy." */
  `energy` real,
  /* "The overall loudness of a track in decibels (dB). Loudness values are averaged across the entire track and are useful for comparing relative loudness of tracks. Loudness is the quality of a sound that is the primary psychological correlate of physical strength (amplitude). Values typically range between -60 and 0 db." */
  `loudness` real,
  /* "Speechiness detects the presence of spoken words in a track. The more exclusively speech-like the recording (e.g. talk show, audio book, poetry), the closer to 1.0 the attribute value. Values above 0.66 describe tracks that are probably made entirely of spoken words. Values between 0.33 and 0.66 describe tracks that may contain both music and speech, either in sections or layered, including such cases as rap music. Values below 0.33 most likely represent music and other non-speech-like tracks." */
  `speechiness` real,
  /* "A confidence measure from 0.0 to 1.0 of whether the track is acoustic. 1.0 represents high confidence the track is acoustic." */
  `acousticness` real,
  /* "Predicts whether a track contains no vocals. "Ooh" and "aah" sounds are treated as instrumental in this context. Rap or spoken word tracks are clearly "vocal". The closer the instrumentalness value is to 1.0, the greater likelihood the track contains no vocal content. Values above 0.5 are intended to represent instrumental tracks, but confidence is higher as the value approaches 1.0." */
  `instrumentalness` real,
  /* "Detects the presence of an audience in the recording. Higher liveness values represent an increased probability that the track was performed live. A value above 0.8 provides strong likelihood that the track is live." */
  `liveness` real,
  /* "A measure from 0.0 to 1.0 describing the musical positiveness conveyed by a track. Tracks with high valence sound more positive (e.g. happy, cheerful, euphoric), while tracks with low valence sound more negative (e.g. sad, depressed, angry)." */
  `valence` real
  );
CREATE UNIQUE INDEX `track_audio_features_track_id_unique` ON `track_audio_features` (`track_id`);

复制成功
  • 数据公开一星期后,该黑客组织官网已被DMCA下架,相关资源也已被删除

数据清洗

  • 原始数据被保存成sqlite3文件,主数据库文件中共有256M条记录,体积约为120GB

  • TIP:超大体积的sqlite使用polars操作,不要用duckdb,已踩坑

  • 由于2025年以后AI歌曲泛滥,大量未被播放过的AI歌曲占总数的一半以上。通过筛选popularity>=1的数据,完成大部分的数据瘦身:256M -> 45M

  • 接下来需要合并数据表,思路是:获取瘦身后45M数据的track_id,从其它表中拉取相同track_id的数据,保留需要的字段,其它数据删除,然后合并到一个总表。同样的,跨sqlite文件操作,瘦身音频特征表

  • 最后只保留剩两个表:一个是结合track_id、artist_id、album_id等字段的基本数据表,另一个是结合track_id、key、mode、tempo等字段的音频分析表

  • 数据按popularity排列,热度越高item越靠前

  • 将artist_id(str)额外编码成artist_idx(int)

  • 将artist_genres取第一个genre,额外编码成artist_genre_idx(int),该字段在后续的训练中非常有用

  • 读取这两个sqlite表,合并字段,转成HuggingFace Dataset。最终保留字段:

代码块
markdown
自动换行
复制代码
Track info:
- track_id (string): Spotify base62 track id
- track_name (string)
- isrc (string): International Standard Recording Code (ISRC). May be empty / missing.
- popularity (float): track popularity normalized to 0..1, rounded to 2 decimals.
  Original Spotify scale is 0..100 (we apply popularity / 100.0).
- explicit (int): 0 = non-explicit, 1 = explicit.
  Derived from tracks.explicit (any non-zero value is treated as explicit).

Artist info (primary artist):
- artist_idx (int): popularity-based index (see “Indexes”)
- artist_id (string): updated (redirect-applied) Spotify base62 artist id
- artist_name (string): may be empty for unknown artists
- artist_popularity (float): normalized to 0..1, rounded to 2 decimals
- artist_genres (string): comma-separated genres (delimiter is ", "), may be empty.
  After artist_id redirects: if the NEW artist_id has genres, we use those; otherwise we fall back to the old artist’s genres.
- artist_genre_idx (int): frequency-based index (see “Indexes”)

Album info:
- album_id (string)
- album_name (string)
- release_year (int): extracted from album.release_date (first 4 chars); 0 if missing/unknown/invalid.

Track audio features:
- duration (int): seconds, computed as floor(duration_ms / 1000).
- time_signature (int): estimated time signature. Typically 3..7.
- tempo (int): estimated tempo in BPM, rounded to nearest integer.
- key (int): estimated key as pitch class.
  Mapping: 0=C, 1=C♯/D♭, 2=D, ..., 11=B. -1 means “unknown / not detected”.
- mode (int): modality. 1=major, 0=minor.
- danceability (float): 0.0..1.0 (rounded to 4 decimals)
- energy (float): 0.0..1.0 (rounded to 4 decimals)
- loudness (float): overall loudness in dB (typically -60..0, rounded to 4 decimals)
- speechiness (float): 0.0..1.0 (rounded to 4 decimals)
- acousticness (float): 0.0..1.0 (rounded to 4 decimals)
- instrumentalness (float): 0.0..1.0 (rounded to 4 decimals)
- liveness (float): 0.0..1.0 (rounded to 4 decimals)
- valence (float): 0.0..1.0 (rounded to 4 decimals)
复制成功

数据分析

  • 热度分布(log指数)

  • 流派分布(共741类)

  • 调式分布

  • 二维热力分布

Embeat v0.1:Triplet Loss

任务分析

  • Embeat系统主要任务:用户选择一首歌,得到这首歌的相关推荐,主要是风格类似

  • 在不引入模型,纯靠数据库实现的话,这个任务大概可以拆解成:用户选择一首歌,输入track_id,从数据库中获取到这首歌的key、mode、valance等音频high-level特征数据,然后通过SQL命令WHERE、SELECT等,筛选比如mode相同、key接近等条件,然后将筛选后的数据整理,返回给前端。但这引入了一个很严重的问题,面对45M的数据,SQL数据库使用B-Tree索引,检索压力会很大,保守估计一次请求要至少510秒的查询时间。而且这种查询还会误伤,条件过于苛刻的话甚至会将比如tempo离阈值只差1BPM,其它条件都完美符合,然后拒之门外的情况

  • 这种情况下,最好的方法是引入向量数据库,将key、mode这些数值特征通过某个embedding模型转成对应向量,向量间的距离就是歌与歌之间风格的接近程度。使用这套流程,一方面能大大提升检索速度,响应基本是毫秒级的,另一方面向量是球形检索,检索条件是浮动不僵硬的。而且可以结合上述方案,比如先筛选具有相同genre的items,然后在这些items中做余弦相似度检索,可玩性更高

  • 因此,这个模型的输入是各种音频特征文本,包含离散值与连续值,输出是一个embedding。抽象来说,这个模型除了能根据这些特征区分歌曲风格,同时也是个相似度打分器

输入输出

  • 离散变量:

代码块
markdown
自动换行
复制代码
key:调性,比如C调、A调,数值为0~12
mode:调式,小调、大调,数值为0、1
tempo_idx:由BPM值转成4个桶,BPM<80为慢速1,80~120为中速2,120~150为快速3,150以上为非常快4
time_signature:节拍,如四三拍、四四拍,数值为3~7
复制成功
  • 连续变量:(数值为0.01.0)

代码块
markdown
自动换行
复制代码
danceability (可舞性):基于节拍稳定性、强度
energy (能量):强度、响度、动态范围
valence (情绪效价):音乐的积极/消极程度(快乐 vs 悲伤)
instrumentalness (器乐性):人声占比
acousticness (原声性):是否使用电子合成器
liveness (现场感):是否为现场录音
speechiness (现场感):是否为现场录音
复制成功
  • 输出:参考Spotify之前用41维向量,加上这些数值之间相互联系,64维向量足以

Dataset

  • 从HF Datasets中抽数据,并做一些标准化转换,预留0=的情况

  • key、mode、tempo_idx:向后偏移1位

  • time_signature:不变,因为不存在0拍子的情况,0拍子即为

  • 所有dense变量:转为-1.01.0分布,其中0.0=。之后可以通过概率dropout这个dense tower来增强模型的鲁棒性

Sampler(Triplet Loss版)

  • 推荐算法的核心部分,决定了模型是否能注意到歌曲之间的联系

  • 在一个batch中,随机选取一个Anchor作为中心,然后根据一些正约束规则选取Positive,根据负约束规则选取Negative,最终使distance(A, P) + margin >= distance(A, N)

  • 正约束规则:

- 来自相同的album或相同的genre、相同的mode、相同的tempo_idx、接近的key

- 接近的key:五度圈距离不超过1,或半音圈距离不超过1

  • 负约束基础规则:

  1. 不同 track(idx 不同)

  2. 若双方 `artist_idx>0` 则 artist 不同(避免同艺人泄漏)

  3. 若双方 `isrc` 非空,则 isrc 不同(避免同曲不同版本/重复)

  4. 若双方 `album_id` 非空,则 album 不同

  • 负约束分级规则:

  1. key 强约束(所有负样本都必须满足):

    - 五度圈距离 `>= neg_key_fifths_dist_min`

    - 且半音圈距离 `>= neg_key_chromatic_dist_min`

  2. hard negative:

    - `mode` 相同、`tempo_idx` 相同

    - `valence_diff` 或 `energy_diff` 足够大(阈值由 `neg_hard_*_diff_min` 控制)

  3. semi-hard negative(同 mode):

- 若 `tempo_idx` 差异 >= `neg_semi_tempo_idx_diff_min` 则更偏“半难”(key 已满足强约束)

- easy negative:不同 genre 或不同 mode(key 已满足强约束)

  • 最终过滤

- 在满足约束的候选中,仍然用 dense 的 L2 距离选“最接近的”(harder negative)

Dataloader

  • 从Dataset中取一条条标准化后的数据,经过Sampler筛选,打包,转成一个个tensor batch

  • Dataloader shape:

返回{anchor, positive, negative},其中anchor[B][] = SpotifyTracksDataset[idx]

Model

代码块
Python
自动换行
复制代码
class EmbeatMLP(nn.Module):
    def __init__(self, config: EmbeatMLPConfig):
        super().__init__()
        self.config = config
        # Data embeddings: 13 + 3 + 6 + 5 = 27 -> 8 + 4 + 4 + 4 = 20
        self.key_embedding = nn.Embedding(int(config.key_vocab_size), int(config.key_emb_dim))
        self.mode_embedding = nn.Embedding(int(config.mode_vocab_size), int(config.mode_emb_dim))
        self.time_signature_embedding = nn.Embedding(
            int(config.time_signature_vocab_size), int(config.time_signature_emb_dim)
        )
        self.tempo_embedding = nn.Embedding(int(config.tempo_vocab_size), int(config.tempo_emb_dim))
        # Discrete tower: 20 -> 64 -> 64
        discrete_input_dim = int(self.config.key_emb_dim + self.config.mode_emb_dim + self.config.time_signature_emb_dim + self.config.tempo_emb_dim)
        self.discrete_tower = nn.Sequential(
            nn.Linear(int(discrete_input_dim), int(config.discrete_output_dim)),
            nn.BatchNorm1d(int(config.discrete_output_dim)),
            nn.PReLU(),
            nn.Linear(int(config.discrete_output_dim), int(config.discrete_output_dim)),
            nn.BatchNorm1d(int(config.discrete_output_dim)),
            nn.PReLU(),
        )
        # Acoustic tower: 7 -> 64 -> 64
        self.acoustic_tower = nn.Sequential(
            nn.Linear(int(config.acoustic_input_dim), int(config.acoustic_output_dim)),
            nn.BatchNorm1d(int(config.acoustic_output_dim)),
            nn.PReLU(),
            nn.Linear(int(config.acoustic_output_dim), int(config.acoustic_output_dim)),
            nn.BatchNorm1d(int(config.acoustic_output_dim)),
            nn.PReLU(),
        )
        # fusion_dim: 64 + 64 = 128
        fusion_dim = int(self.config.acoustic_output_dim + self.config.discrete_output_dim)
        hidden_0, hidden_1 = config.backbone_hidden_dims
        # Backbone: 128 -> 256 -> 128 -> 64
        self.backbone = nn.Sequential(
            nn.Linear(int(fusion_dim), int(hidden_0)),
            nn.BatchNorm1d(int(hidden_0)),
            nn.PReLU(),
            nn.Dropout(p=float(config.backbone_dropout_p)),
            nn.Linear(int(hidden_0), int(hidden_1)),
            nn.BatchNorm1d(int(hidden_1)),
            nn.PReLU(),
            nn.Dropout(p=float(config.backbone_dropout_p)),
            nn.Linear(int(hidden_1), int(config.embedding_dim)),
        )
复制成功
  • key:13 -> 8

  • mode:3 -> 4

  • time_signature:6 -> 4

  • tempo_idx:5 -> 4

  • discrete:8 + 4 + 4 + 4 =20 -> 64

  • dense:7 -> 64

  • discrete + dense = 128 -> 256 -> 128 -> 64

Loss

  • Triplet margin:relu(d(ap) - d(an) + margin)

  • popularity加权:popularity经过0.5指数变换后,分别加到batch中每个样本的loss上,使高权重关注度更高

  • 统计输出:loss、grad_norm、ap_dist、an_dist、active_triplet、weight_mean

效果演示

  • 动漫歌曲:

  • 小众歌曲:

  • 周杰伦vs动漫歌曲:

Embeat v0.2:Masked InfoNCE

v0.1存在的问题

  • 在Triplet loss眼中,对于每个anchor,只有一个既定的positive与一个nagative。但实际情况并非如此,一首种子歌曲可以有多个positive,同时也能有多个negative

  • Triplet loss这种非黑即白的逻辑会导致模型很快成为一个规则过滤器,一旦符合positive的规则,给高分,否则均为低分,几乎没有中间地带。而实际经验告诉我们,这样的模型泛化性不强,不能很好地反映歌曲高维声学特征间的内部规律

  • Triplet loss的margin参数很难调整,调高了所有样本非黑即白,缺乏泛化能力。调低了模型偷懒,分不清长得像的负样本。因此Triplet loss经常会配合大量负样本挖掘规则(难负样本挖掘),让负样本与正样本尽可能接近,强制模型在空间上区分正负样本

  • 为了让Triplet loss具备一定泛化能力,我们经常会开更高的dropout,让模型在前向传播中丢弃一部分参数信息,比如我设置的0.2其实已经很高了。但这并不能解决根本问题,因为当前推荐场景就不适合做一个positive与一个negative的挖掘,必须找到一种”一对多“的解决方案

InfoNCE与变体

  • InfoNCE很好地解决了这个问题。在标准的InfoNCE模型中,对于一个anchor,它有一个既定的positive,而batch中除了anchor与positive外的其它样本全是negative。这样在一个batch中(假定batch_size=4096),会产生4096*4096的分数矩阵,每一行记录着一个anchor的最终分数。假如anchor_index=0,positive_index=2,第一行的分数可能是:[1.0, 0.1, 0.2, 0.9, 0.1...]

  • 计算loss也很简单,排除掉anchor自身的1.0,把positive分数作为分子,所有negative分数相加作为分母去计算。具体计算公式如下:

  • 这里可能出现一个问题,在一个batch中,样本不是anchor、positive,难道就一定是negative吗?由于negative的数量非常多,指数处理后相加,自身分数会被淡化,降低假负样本对最终分数的影响

  • 进阶思考:对于一个anchor,我们可能有多个positive。而且除anchor外的样本直觉上应该分为三类:正样本、负样本、非正非负样本。因此SupCon出现了。我们通过正样本筛选规则从batch中取出多个正样本,又通过负样本筛选规则取出多个负样本,剩下的样本就是非正非负样本

  • 将正样本分数相加后取平均(除以正样本数量);又通过mask的方式,将非正非负样本从公式的分母部分取出,即抛弃掉这部分数据,它既不是分子也不是分母。这样就实现了我们最终想要的效果:多个正样本、多个负样本、非正非负样本不要影响计算

Sampler

  • 目标:在一个batch中,挖掘出正样本与负样本

  • 正样本规则:

  1. 必须遵守:相同的mode,相同的time_sinature,相近的tempo,相近的key,另外instrumentalness、speechiness不能相差太大

  2. 相近的key:五度圈距离±2,或半音圈距离±1,或关系大小调

  3. 声学特征相似度:anchor与positive会有接近的valence(T0)、energy(T0)、danceability(T1)、acousticness(T1)、liveness(T2)。计算anchor与positive的连续声学特征L2距离,决定其是否能被判定为positive。L2计算思路:distance = sqrt(weight_vector * (anchor_vector - candidate_vector)  2)

  4. 额外奖励:如果anchor与positive有相同的genre、album、artist,能得到一定的分数奖励,取负值加到上述distance,即拉近candidate与anchor的距离

  5. 公式原理上可以取多个正样本,实际工程中最后只取一个分数最高的正样本

  • 负样本规则:

  1. 在batch中,默认情况下,所有样本不是anchor、positive,就是negative

  2. 除非样本来自相同的专辑,或拥有相同的ISRC,这部分mask掉,不参与分母计算

代码块
Python
自动换行
复制代码
class PairSamplerConfig:
    # Positive bucket A rate: sample from same album, fallback to bucket B if miss
    pos_album_ratio: float = 0.99
    # Positive bucket B rate: sample by near acoustic similarity, fallback to bucket A if miss
    pos_genre_acoustic_ratio: float = 0.01
    # Positive dense threshold: valence difference of anchor and positive should not be too big
    pos_valence_diff_max: float = 0.20
    # Positive dense threshold: energy difference of anchor and positive should not be too big
    pos_energy_diff_max: float = 0.20
    # Positive dense threshold: danceability difference of anchor and positive should not be too big
    pos_danceability_diff_max: float = 0.30
    # Positive dense threshold: acousticness difference of anchor and positive should not be too big
    pos_acousticness_diff_max: float = 0.30
    # Positive dense threshold: liveness difference of anchor and positive should not be too big
    pos_liveness_diff_max: float = 0.20
    # Positive dense threshold: speechiness difference of anchor and positive should not be too big
    pos_speechiness_diff_max: float = 0.05
    # Positive dense threshold: L2 distance of anchor and positive should not be too far
    pos_dense_weighted_l2_max: float = 0.30
    # Positive dense weight: valence weight in L2 distance calculation
    pos_dense_weight_valence: float = 1.0
    # Positive dense weight: energy weight in L2 distance calculation
    pos_dense_weight_energy: float = 1.0
    # Positive dense weight: danceability weight in L2 distance calculation
    pos_dense_weight_danceability: float = 1.0
    # Positive dense weight: acousticness weight in L2 distance calculation
    pos_dense_weight_acousticness: float = 0.5
    # Positive dense weight: liveness weight in L2 distance calculation
    pos_dense_weight_liveness: float = 0.5
    # Positive dense weight: speechiness weight in L2 distance calculation
    pos_dense_weight_speechiness: float = 1.0
    # Discrete hard constraints: mode of anchor and positive should be the same (relative major/minor will be excluded)
    require_mode_match_for_positive: bool = True
    # Discrete hard constraints: key of anchor and positive should be close (circle-of-fifths distance, chromatic-circle distance or relative major/minor)
    require_key_close_for_positive: bool = True
    # Discrete hard constraints: tempo of anchor and positive should be close
    require_tempo_close_for_positive: bool = True
    # Discrete hard constraints: time signature of anchor and positive should be the same
    require_time_signature_match_for_positive: bool = True
    # Discrete hard constraints: artist genre of anchor and positive should be the same
    require_genre_match_for_positive: bool = False
    # Tempo threshold: abs(anchor_tempo_idx - positive_tempo_idx) <= pos_tempo_idx_diff_max
    pos_tempo_idx_diff_max: int = 1
    # Circle-of-fifths distance threshold
    pos_key_fifths_dist_max: int = 2
    # chromatic-circle distance threshold
    pos_key_chromatic_dist_max: int = 1
    # Positive filter: skip positive if positive_genre is "Unknown"
    pos_require_known_genre: bool = True
    # Anchor filter: skip anchor if anchor_genre is "Unknown" (set to True if require_genre_match_for_positive == True)
    anchor_require_known_genre: bool = True
    # Negative loss mask: anchor and negative should not be from the same album
    neg_exclude_same_album: bool = True
    # Negative loss mask: anchor and negative should not have the same ISRC
    neg_exclude_same_isrc: bool = True
    # Positive sampling bias: higher value means prefer higher popularity sample, applies to positive candidate score
    pos_popularity_power: float = 0.5
    # Positive sampling bias: minimum popularity weight
    pos_popularity_min_weight: float = 0.10
    # Positive sampling bias: maximum popularity weight
    pos_popularity_max_weight: float = 1.00
    # Positive candidates ranking bias: anchor and positive are from a same album
    score_bonus_same_album: float = 0.08
    # Positive candidates ranking bias: anchor and positive are from a same artist
    score_bonus_same_artist: float = 0.04
    # Positive candidates ranking bias: anchor and positive have a same genre
    score_bonus_same_genre: float = 0.02
    # Positive candidates ranking bias: anchor and positive have a same mode
    score_bonus_mode_match: float = 0.00
    # Positive candidates ranking bias: anchor and positive have a close key
    score_bonus_key_close: float = 0.00
    # Positive candidates ranking bias: anchor and positive have a close tempo_idx
    score_bonus_tempo_close: float = 0.00
    # Positive candidates ranking bias: anchor and positive have a same time signature
    score_bonus_time_signature_match: float = 0.00
    # Global filter: speechiness_raw >= threshold will be excluded, to avoid talk-only audio
    speechiness_max_exclusive_raw: float = 0.20
    # Cache items per genre
    cache_per_genre: int = 4096
    # Cache items per album
    cache_per_album: int = 128
    # Limit number of album ids in cache (avoid memory leak)
    max_album_cache_keys: int = 10000
    # Try time in one of positive buckets (higher value consumes more time but lower anchor_drop_frac)
    max_positive_tries: int = 128
    # Global random seed
    random_seed: int = 616
复制成功

Model

  • discrete + dense = 128 -> 256 -> 256 -> 64(Backbone倒数第二层维度从128改成256)

Trainer

  • batch_size=4096,dataset_len=45059660 -> 10000000

  • 训练跑200 steps即可,约十分之一个epoch

  • 学习率调度器使用恒定学习率,lr=1e-4,weight_decay=1e-3

  • 梯度裁剪=1.0,训练稳定后基本不会触发

  • 关闭所有dropout

  • InfoNCE温度值tau设为0.05,重要

  • 根据正负样本相似度的评估结果,最后取step=70的checkpoint,并不是loss越低越好

  • 因为模型参数量很少,加上任务简单,还没跑完一个epoch就已经基本拟合,属于正常现象

代码块
Python
自动换行
复制代码
step: 10 loss: 7.013882 lr: 0.0001 grad_norm: 4.9073 anchor_drop_frac: 0.269 pos_per_anchor: 96.736 neg_same_genre_frac: 0.001 batch_masked_frac: 0.069 step_per_sec: 0.06
step: 20 loss: 6.1141 lr: 0.0001 grad_norm: 3.9595 anchor_drop_frac: 0.456 pos_per_anchor: 34.923 neg_same_genre_frac: 0.001 batch_masked_frac: 0.103 step_per_sec: 0.02
step: 30 loss: 5.631929 lr: 0.0001 grad_norm: 1.7332 anchor_drop_frac: 0.442 pos_per_anchor: 34.743 neg_same_genre_frac: 0.001 batch_masked_frac: 0.107 step_per_sec: 0.04
step: 40 loss: 5.375176 lr: 0.0001 grad_norm: 1.1491 anchor_drop_frac: 0.419 pos_per_anchor: 34.851 neg_same_genre_frac: 0.001 batch_masked_frac: 0.111 step_per_sec: 0.03
step: 50 loss: 5.246718 lr: 0.0001 grad_norm: 0.8457 anchor_drop_frac: 0.428 pos_per_anchor: 34.026 neg_same_genre_frac: 0.001 batch_masked_frac: 0.108 step_per_sec: 0.03
step: 60 loss: 5.140089 lr: 0.0001 grad_norm: 0.6737 anchor_drop_frac: 0.426 pos_per_anchor: 33.726 neg_same_genre_frac: 0.001 batch_masked_frac: 0.113 step_per_sec: 0.03
step: 70 loss: 5.063671 lr: 0.0001 grad_norm: 0.6964 anchor_drop_frac: 0.428 pos_per_anchor: 33.639 neg_same_genre_frac: 0.001 batch_masked_frac: 0.104 step_per_sec: 0.03
step: 80 loss: 5.017426 lr: 0.0001 grad_norm: 0.6436 anchor_drop_frac: 0.416 pos_per_anchor: 34.18 neg_same_genre_frac: 0.001 batch_masked_frac: 0.116 step_per_sec: 0.03
step: 90 loss: 4.955722 lr: 0.0001 grad_norm: 0.6324 anchor_drop_frac: 0.404 pos_per_anchor: 33.867 neg_same_genre_frac: 0.001 batch_masked_frac: 0.111 step_per_sec: 0.03
step: 100 loss: 5.569067 lr: 0.0001 grad_norm: 0.4346 anchor_drop_frac: 0.17 pos_per_anchor: 96.831 neg_same_genre_frac: 0.001 batch_masked_frac: 0.075 step_per_sec: 0.09
step: 200 loss: 4.747747 lr: 0.0001 grad_norm: 0.5333 anchor_drop_frac: 0.393 pos_per_anchor: 32.798 neg_same_genre_frac: 0.001 batch_masked_frac: 0.106 step_per_sec: 0.03
step: 300 loss: 4.672141 lr: 0.0001 grad_norm: 0.4249 anchor_drop_frac: 0.397 pos_per_anchor: 32.192 neg_same_genre_frac: 0.001 batch_masked_frac: 0.113 step_per_sec: 0.02
step: 400 loss: 4.655772 lr: 0.0001 grad_norm: 0.3894 anchor_drop_frac: 0.399 pos_per_anchor: 32.641 neg_same_genre_frac: 0.001 batch_masked_frac: 0.113 step_per_sec: 0.02
step: 500 loss: 4.658092 lr: 0.0001 grad_norm: 0.4877 anchor_drop_frac: 0.396 pos_per_anchor: 32.981 neg_same_genre_frac: 0.001 batch_masked_frac: 0.113 step_per_sec: 0.02
复制成功

效果演示

  • 《Eleni Karaindrou - Karaindrou: Waltz By The River》 vs 《Eleni Karaindrou - Karaindrou: Dance Theme》:相似度0.96

  • 《周杰伦 - 晴天》 vs 《周杰伦 - 夜曲》:相似度0.73

  • 《彩菜 - Last regrets》 vs 《Lia - 鳥の詩》:相似度0.67

  • 《周杰伦 - 晴天》 vs 《Lia - 鳥の詩》:相似度0.64

  • 《Oskar Schuster - Gizeh》 vs 《Gorillaz - Feel Good Inc.》:相似度0.36

分析

  • 通过将Triplet loss替换成InfoNCE变体,结果稳定且正确很多

  • 解决了Triplet loss难以挖掘难负样本的问题

  • 解决了Triplet loss容易退化成规则分类器的问题

  • InfoNCE只需关注正样本的挖掘,Sampler规则更好写

  • InfoNCE的模型也具备一定泛化能力,能正确处理介于positive与negative中间态的样本

Embeat v0.3:Qdrant向量数据库

目标

  • 最终目标是输入种子曲目,输出与种子歌曲风格相似的歌单,因此需要对接数据库

  • 目前我们已经拥有能将high-level连续声学特征转换成embedding的模型EmbeatMLP。可以考虑使用向量数据库,将45M的歌曲经过模型全部计算embedding,然后保存到数据库中

  • 常见的向量数据库有Milvus、Pinecone、Weaviate、Qdrant、Chroma‌。在数据库软件的选型上,考虑到要Python兼容、轻量、极速,最终选择Qdrant这款产品

  • 数据来源是之前清洗的HuggingFace Dataset,输出是Qdrant向量数据库Collection

数据库字段

  • 数据库是最后部署上线的部分,因此要严格控制文件体积跟内存占用。我们不需要所有的HF Dataset字段都保存到数据库中,只选择最重要的部分:

-    track_id (str):用于检索定位种子曲目ID,以及返回内容也需要

-    track_name (str):返回内容需要

-    popularity (float):用于Re-rank排序

-    artist_name (str):返回内容需要

-    artist_idx (int):“相同艺人”检索需要(参考v0.5多路召回章节)

-    artist_genres (str):“相似风格”检索需要(参考v0.5多路召回章节)

-    artist_genre_idx (int):“相似风格”检索需要(参考v0.5多路召回章节)

-    related_artist_idxs (List[int]):“相关艺人”检索需要(参考v0.5多路召回章节)

-    album_name (str):返回内容需要

-    isrc (str):过滤多地区发行版本需要

创建记录

  • 首先需要为每条记录创建一个独一无二的索引,在Qdrant中称为point_id。point_id只接受两种数据类型,int或UUID格式的str。这里我选择使用track_id应用UUID5,转成point_id。这样query时只要输入track_id,就能毫秒级定位到数据行,等于为数据行生成一个极速的基于track_id的索引

  • 对HF Dataset数据做切块,每512条数据一个块,将dataset_slice (List[dict])转成rows (Dict[list])

  • 过滤掉track_id为空或popularity < 0.01的行

  • 从数据dict(energy=0.5, valence=0.4, etc)创建torch features

  • 调用EmbeatMLP模型,batch推理,将audio features转成embedding,然后量化成uint8(节省75%的向量存储空间)并转成list(Qdrant只认list,不认torch或numpy数据)

  • 集齐512条最终数据,upsert写进数据库

创建Payload索引

  • 创建索引是为了更快的query速度,为一些可能传入作为query条件的字段创建索引,检索速度能达到百倍提升

  • 创建索引唯一的缺点是占硬盘存储空间与部分内存空间,如果服务器资源吃紧可能会受影响,但利远大于弊

  • track_id已通过point_id创建索引,这里不需要重复创建

  • 我为artist_genre_idx (int)、artist_idx (int)、popularity (float)创建了必要索引

  • 另外可自选为track_name、artist_name创建TextIndex索引,引入TokenizerType.WORD分词器,支持模糊检索。这部分索引永远保存在disk上,不占内存

创建向量HNSW索引

  • 检索过程中需在向量空间计算余弦相似度匹配声学相似items,提前建立好向量的HNSW索引可以加快这部分检索速度

  • HNSW有两个比较关键的参数,一个是HnswConfigDiff.m,代表每个节点有多少条边。边数越多图连接越稠密,召回准确率越高,同时内存与硬盘的消耗也越多。边数越少,省内存、建图快,但图可能断路,召回下降。默认值是16,但因为我的模型维度64较小,因此我将这个值改为8,可以节省一半的HNSW初始化内存(实测约3GB)

  • 另一个是HnswConfigDiff.ef_construct,代表建图时的搜索宽度,即为每个新插入节点寻找邻居时维护的候选列表大小,数值越大图精度越高。经验值是100200,因为选用比较小的m值,这里我将这个值改为200,比默认值更大

  • 在建数据库时,我将optimizers_config.indexing_threshold的值设为0,意味着不建立HNSW索引,这样我能得到一份体积最小且最干净的数据库。先打包这份数据库

  • 完成备份后,通过update_collection方法更新indexing_threshold的值为1,意味着只要遇到1个新item,马上为其建图,这样能为我的数据库充分建图,保证每个item都有HNSW索引。因为数据库后续大概率不会再新增item,所以indexing_threshold可以设为1。如果后续仍有可能更新,最好改成默认值20000,但修改此值会再次触发优化器重新整理segments,CPU内存资源上升

实测效果

  • 种子曲目:《周杰伦 - 晴天》

  • 种子曲目:《梶浦由記 - Decretum》

分析

  • 通过部署Qdrant数据库,Embeat的应用雏形终于成型,通过输入query_track_id,得到相似结果

  • 但这个推荐结果可以说是非常糟糕,推荐歌曲与种子歌曲间确实有接近的节拍、速度、甚至调式等等,它们的声学数据在数据库中,但听感却完全不一样。比如和声、配器、风格,可能都完全不同

  • 好消息是,种子歌曲是人声曲,推荐歌曲返回人声曲;种子歌曲是纯音乐,推荐歌曲返回纯音乐。这说明instrumentalness参数起作用了

Embeat v0.4:引入歌手流派

目标

  • 只有声学特征相似并不能找到正确的推荐歌曲,因为key、mode、tempo等参数相同,得到的也可能是听感接近但风格截然不同的两首歌曲

  • 这里我想到了Spotify早期做歌曲推荐时,大量使用NLP自然语言处理。从全网挖掘文本,比如通过音乐博客、乐评网站、维基百科等,建立artist与keyword的联系,即共现矩阵。如果人们在讨论两个音乐人时使用了高度重合的词汇,他们在特征空间中的距离就会被拉近,而这个词汇,大概率就是一种流派。这是一种还没处理的“伪流派”

  • 之后就是量化这个流派。Spotify拥有energy、danceability、acousticness等上述提及的参数,通过计算这些艺人的作品的声学特征在空间中的分布,筛选掉一些噪声艺人。之后就能为艺人在共现矩阵与声学相似这两个集合中取交集。这个交集的质心,就是一个微流派

  • 现在我有45M歌曲的声学embedding,但却没有流派信息。全网挖掘文本这个工作量非常大,由我一人完成也不太现实,因此考虑曲线救国,用网络上的数据填补这个缺口

Every Noise at Once

  • 这个网站是音乐信息检索和推荐系统领域非常经典的一个可视化项目。它的创作者Glenn McDonald曾就职于Spotify,专业处理Data Alchemist

  • 这个网站将艺人精准映射到几千个极端细分的微流派,每个艺人可能有多个微流派,且每个艺人在流派中包含权重大小信息,如下图所示

  • 同时,这个网站还有6000+种流派的二维地图与一维列表

一阶段爬虫

  • 从1D-Map中获取genre_list(pop, rap, rock...),保存成txt

  • 从genre_list中取genre_str,拼接URL,再从具体的genre页面中抓取artist_id、weight、map_x、map_y等信息,保存成json

  • 可惜everynoise.com后台返回的不是json,而是静态网页,因此要使用bs4做前端爬虫处理

  • 最后一共爬取6144个微流派

  • 将genre_str通过index转成genre_idx,其中,idx的大小决定了流派的流行度(与1D-Map相同)。将genre_idx=0分配给,用于处理无流派的歌手

  • 重新生成HF Dataset,将genre_idx写进数据集中,同时重新生成Qdrant数据库

二阶段爬虫

  • EveryNoise网站上收录了绝大多数的艺人,但有些艺人因为过于小众,并未被EveryNoise收录。因此我借助“相似艺人”这个API,补充了这部分小众艺人的genre_idx(相似艺人部分参考v0.5)

  • 筛选了45M数据集中所有artist_genre_idx=0的艺人,一共有229.9万人。最后使用这种方法为92.7万小众艺人找到流派

同艺人多流派处理

  • 一个艺人可能存在多个流派,我将这些流派使用artist_genres (str)这个字段保存了流派名。而genre_idx (int)只能存一个值,这样设计的初衷是,给这个艺人找到属于ta的最准确流派

  • 起初的想法是,艺人在哪个流派的权重最高,那ta就属于哪个流派。这样上线后的系统发现一个严重问题,当这个流派实在太小众,而艺人的影响力又很大时,ta的权重就会变得特别高

  • 比如歌手齐豫,她的微流派是“华语经典”、“华语流行”、“佛教音乐”。按常理讲,齐豫应该被划分为“华语经典”,可“华语经典”高手荟萃,而“佛教音乐”相比之下小众很多,所以齐豫在“佛教音乐”的权重就会变高。如果将齐豫归为这个类别,以后遇到齐豫的种子曲目,会推荐大量的佛教音乐

  • 我爬取了EveryNoise网站的2D-Map,与每个流派的子页面,我能拿到这些数据:流派的x, y坐标(x代表energy,y代表accoustic,已归一化)、流派的权重、艺人的权重

  • 我在这里引入一个引力模型,具体是这样工作的。首先计算多个流派的质心(Cx, Cy):

-    分子部分:总拉力,求和所有 x坐标 * 流派权重

-    分母部分:总质量,求和所有 流派权重

-    质心:求得最后的 (Cx, Cy)

  • 接下来计算艺人在每个流派中的得分(得分公式就是万有引力公式变体):

-    混合质量 = 艺人在流派中的权重 * 流派本身的权重

-    D = 每个流派坐标到质心的距离,使用欧式距离公式计算

-    S = 距离缩放常数,即“舒适圈”半径,D超过S时就判定为“远离”了这个质心。S很大时对“异类”流派包容度更高

-    a = 惩罚系数。当D超过S时,惩罚有多大。这里我将a设为2,参考万有引力公式

-    分母加上系数1,是为了让Score不会因分母小于1接近0时暴增

  • 这套模型的关键在于S值的确定。这里我用HDBSCAN,将空间中所有的二维平面点(流派)聚类,然后计算簇与簇之间的距离,取平均除以2,作为最终的S值。在HDBSCAN中有个关键参数叫min_cluster_size,意味着形成一个簇至少要多少个点。我将min_cluster_size从2慢慢调整到10,得到以下结果:

代码块
Python
自动换行
复制代码
Current min_cluster_size: 2
Cluster number: 961
Best scale value: 0.0077

Current min_cluster_size: 3
Cluster number: 516
Best scale value: 0.0106

Current min_cluster_size: 4
Cluster number: 314
Best scale value: 0.0138

Current min_cluster_size: 5
Cluster number: 224
Best scale value: 0.0155

Current min_cluster_size: 6
Cluster number: 3
Best scale value: 0.2554

Current min_cluster_size: 7
Cluster number: 3
Best scale value: 0.2573

Current min_cluster_size: 8
Cluster number: 2
Best scale value: 0.2592

Current min_cluster_size: 9
Cluster number: 2
Best scale value: 0.2620

Current min_cluster_size: 10
Cluster number: 2
Best scale value: 0.2626
复制成功

-    可以发现,min_cluster_size=5到min_cluster_size=6发生一个突变,簇数量从224爆减到3。EveryNoise有6000多个微流派。把6000个微流派聚合成224个大簇,符合我们对流派大类的理解数量,所以5这个值维系音乐流派区别的最小簇数量

-    这意味着EveryNoise的流派排布极其致密,稍微走错地图的1.5%,就可能从“流行圈”转到“摇滚圈”或“民谣圈”。所以最终确定S=0.015,而且这个值毋庸置疑

  • 通过上述计算,我为每个艺人计算了一个属于ta最合适的流派,并记录成artist_genre_idx

  • 顺带一提,通过这套模型计算齐豫的风格,最终将她分类为“华语经典”

Qdrant端的处理

  • 将genre_idx作为candidates的筛选条件,先筛选相同genre_idx的candidates,然后应用query_vector,按相似度从高到低排序,得到基于流派与声学模型过滤的推荐结果

  • 这里的genre_idx一般情况下会使用artist_genre_idx,即艺人的最终流派。但这里我引入另一套处理多流派的机制,来获取track_genre_idx。为什么要这样设计?如果用户输入齐豫的佛教音乐,而系统返回齐豫或张学友的经典音乐,这多少会有些割裂

  • track_genre_idx具体是这样设计的:

  1. 首先将artist_genres通过6144个genre的字典翻译成artist_genre_idxs (list)

  2. 然后artist_genre_idxs作为Qdrant的搜索条件,找到符合这些流派且声学特征相近的所有歌曲candidates

  3. get_track_genre_idx函数有两个输入:candidates (list), fallback_idx (int)。其中candidates按声学特征从高到低排序,fallback_idx等于artist_genre_idx或0

  4. 计算candidates中每个流派的得分,流派分数 = 求和所有 (index  0.5)

  5. 通过得分对流派进行排序,当第一流派分数 > 第二流派分数 * 2.0时,第一流派胜出,作为track_genre_idx。否则track_genre_idx=fallback_idx

  6. track_genre_idx的设计思路是,当声学相近的歌曲中大量出现某个流派,且该流派排名靠前时,这个流派就是track_genre_idx,而不是简单使用artist_genre_idx

  7. track_genre_idx会作为筛选条件,筛掉candidates中流派与track_genre_idx不同的结果。这样当用户输入齐豫的佛教音乐,经过get_track_genre_idx函数能得到佛教音乐的genre_idx,通过这个genre_idx过滤掉非佛教音乐的candidates结果,得到的结果纯度会高很多

效果展示

  • 种子曲目:《周杰伦 - 晴天》

  • 种子曲目:《梶浦由記 - Decretum》

  • 种子曲目:《Oskar Schuster - Gizeh》

分析

  • v0.4推荐结果比v0.3好非常多,看到周杰伦能推荐G.E.M,看到梶浦由记能推荐泽野弘之,看到Oskar Schuster能推荐Ludovico Einaudi,而且返回的前几首歌曲风格与种子曲目非常接近,调式调性、节奏韵律都很像,结合了声学相似+风格相似的优点

Embeat v0.5:多路召回

目标

  • 通过阅读Spotify与网易云的技术报告可知,目前主流的音乐厂商在做歌曲推荐系统时都会用到多路召回。它指的是推荐的歌曲不应该从单一路径获取,而是多角度的,通过多种途径获取,汇总后通过一定规则过滤、重排、输出

  • 目前的Embeat只有“流派声学相似”这一路输出,因此需要为其设计更多输出路径

第二路输出:同流派高热度

  • 思路:由于声学特征的过滤,大量同流派但能量、配器不同的歌曲可能被过滤,这就导致用户只会听到一种情绪的音乐,同质化比较严重。引入这一路,将同流派中热度高的歌曲推荐给听众,由于热度高,听众很可能曾经听过这部分音乐,能给予其更多亲切感

  • 方法:通过artist_genres (str) 转 artist_genre_idxs (List[int]),从多个流派中筛选歌曲,最后按热度从高到低排序输出

  • 可能的问题:假如种子曲目是《齐豫 - 橄榄树》,但假如这一路推荐了齐豫唱的佛经音乐,会带来割裂感。好在佛经音乐的热度普遍不高,通过热度排序,排名靠前的全是华语经典、华语流行

  • 优质例子:种子曲目《Gareth Coker - Light of Nibel》,这是一首高能量的带人声咏唱的管弦乐作品。经过这一路推荐后,获得了《Berlinist - Gris, Pt. 2》这首低能量,但同为咏唱风格的游戏音乐。我在体验Spotify过程中,也得到了相同的返回结果

第三路输出:同歌手高热度

  • 思路:假如歌手实在太小众,第一、二路输出都可能获取不到同歌手的音乐作品,而同歌手的音乐无论风格与种子曲目相差有多大,听到同歌手总能带给用户熟悉感,所以需要引入这一路输出

  • 方法:根据artist_idx (int)搜索同歌手作品,不加其它任何筛选条件,最后按流行度从高到低排序输出

  • 重排:如果在输出的candidates中发现同专辑作品,插入到队头,拥有更高的推荐权重

  • 控制数量:最终返回的同歌手曲目数量不能太多也不能太少,这里我引入same_artist_ratio_range这个参数,我将在后续的重排部分展开介绍

  • 优质例子:种子曲目《潘安邦 - 爸爸的草鞋》(低能量小调音乐),推荐歌曲《潘安邦 - 外婆的澎湖湾》(低能量大调音乐),同歌手但风格可能迥然不同,带来熟悉感与破圈感

第四路输出:相似歌手(当前版本新增)

  • 思路:歌手级的协同过滤。假设歌手A可能与歌手B风格迥然不同,但他们在文化圈中有交集,他们的音乐也因此有了共性。完成这一路输出,整套推荐系统会有质的飞跃,才能做到真正懂用户

  • 准备工作:将45M数据集中的artist_id与artist_idx批量导出,通过某API多线程爬取Spotify上的related_artist接口,得到artist_id (str) -> artist_related_ids (List[str])这样的映射关系。最后重新映射回related_artist_idxs (List[int]),作为一个新字段保存进Qdrant数据库

  • 进度:共324万艺人,为105万艺人找到相似歌手,构建起庞大的歌手关系网络

  • 方法:将artist_related_ids与query_vector传进Qdrant,找到满足artist_related_ids的艺人作品,最后按与种子曲目声学向量距离排序输出

  • 优质例子:种子曲目《Lia - 鳥の詩》(来自Key社动漫《AIR》),推荐曲目《riya - 小さなてのひら》(来自Key社动漫《CLANNAD》)。这种圈内人才懂的推荐,如今Embeat也能实现

第五路输出:歌单协同过滤(v0.6新增)

  • [ 参考本文章节v0.6 ]

重排输出

  • 对于top_k=20的结果,会从四路输出中得到80个candidates结果

  • 首先四路输出在进入重排阶段前,之前会经过筛选处理,比如筛除掉相同track_name、带remix的曲目、低于最低热度的曲目(目前值为0.01)等操作,确保每一路的candidates都尽可能接近20,不浪费槽位

  • 进入重排阶段后,会对这四路结果做ISRC过滤,排除掉那些相同音乐但因发行地区不同的原因出现的重复曲目,这些曲目无法通过track_name简单过滤,比如《周杰伦 - 青花瓷》《Jay Chou - Qing Hua Ci》

  • 接下来每个candidate进行打分,具体运作流程是:

  1. 来自第一路声学相似输出,给1.71.0分

  2. 来自第二路同流派高热度输出,给1.00.8分

  3. 来自第三路同歌手高热度输出,给1.91.0分

  4. 来自第四路相似歌手输出,给1.81.0分

  5. 来自第五路歌单协同过滤输出,给2.0~1.0分

  6. 浮动分是根据candidate所在那路输出的index给的分数,因为四路输出都做过排名处理,排名靠前的价值更高

  7. 一个candidate可能命中多路输出,分数可累加,这种candidate很容易在最终阶段胜出

  • 由于第二路输出给的分数偏低,很可能在最终20个输出中拿不到席位。所以加了个保底机制,10%的歌曲必须来自第二路输出,插到队尾中,给推荐列表一定的破圈感

效果展示

  • 种子曲目:《周杰伦 - 晴天》

代码块
Python
自动换行
复制代码
Query track_id: 5pIcwtJYNJx93l420oR2Vm
Query track info: 晴天 - Jay Chou
======= Top 20 items =======
index   track_id                track_name      artist_name     album_name      sources
1       6USP6g71C4b6wkRyR5sabC  奇蹟 Miracle    G.E.M.  The Best of G.E.M. 2008 - 2012  ['similar', 'related_artist']
2       2tqF9MPNdYdJU70U0ULO23  告白氣球        Jay Chou        周杰倫的床邊故事        ['popular', 'same_artist']
3       5INxVTneIiVxUprpJFyEBl  在你耳邊說      Eric Chou       戀愛筆記        ['similar', 'related_artist']
4       4BlnR7y7sDouqBggjzuZxR  說了再見        Jay Chou        跨時代  ['same_artist']
5       1SMC4asXLbJMhckDg1PKAq  Six Degrees     Jay Chou        Six Degrees     ['same_artist']
6       0L0YcC8trjXcGi7gUUTTJY  年少有为        Ronghao Li      2019中国好声音巅峰之夜  ['related_artist']
7       3p4UTiSIIpP4LFn0KEyEOj  十面埋伏        Eason Chan      Live For Today  ['related_artist']
8       21aqdGfLlYC3nRI86JQ8oz  阿怪    Eason Chan      黑·白·灰        ['related_artist']
9       4xTN75476Zux2ZkQc6Vyyp  談戀愛  Della   談戀愛  ['similar']
10      6CBQGet200AWXYTKlt7LzJ  突然好想你      Mayday  五月天 | 步步 | 自選作品輯 the Best of 1999-2013        ['related_artist']
11      7oKRQKokAtk3MKB78K6LBD  Thrilled        Wanting         Everything In The World (Celebration Version)   ['similar']
12      6MeN0fn6XodP7N3VZx59EU  粉筆和塗鴉      Pets Tseng      粉筆和塗鴉      ['similar']
13      6grLvolCX0Y6JbkhLEDBdB  夢的可能        Eason Chan      準備中  ['related_artist']
14      0AuGbsggLNbfx0pfYLW8Ve  奇蹟    G.E.M.  The Best of G.E.M. 2008 - 2012 (Deluxe Version)         ['similar']
15      1CFob3XXGftpT5hZGusvWV  我曾用心爱着你 - Live   潘美辰  潘美辰演唱会现场录音特辑 (Live)         ['similar']
16      6Aq2oV748LSfZdp5FieYZD  極光    Bii     電視劇《解憂公主》      ['similar']
17      3pyfEMTvunanVkcTFME2IK  小時候的我們    Eric Chou       小時候的我們    ['related_artist']
18      28WXeygFMCoYWq6QpZ882f  沙林    ATK     沙林    ['similar']
19      7sNdTYUKm8Lom5hjrn1765  瞬间    成方圆  九州方圆——电视歌会      ['similar']
20      2gug6MRv4xQFYi9LA3PJCS  你,好不好? - TVBS連續劇【遺憾拼圖】片尾曲     Eric Chou       愛,教會我們的事        ['popular']
复制成功
  • 种子曲目:《梶浦由記 - Sis puella magica!》

代码块
Python
自动换行
复制代码
Query track_id: 0HDh5QaLfCwi2xyvkei917
Query track info: Sis puella magica! - 梶浦 由記
======= Top 20 items =======
index   track_id                track_name      artist_name     album_name      sources
1       15qBxGjAD4kWONlicpYpuR  Puella in somnio        梶浦 由記       「魔法少女まどか☆マギカ」 Music Collection      ['same_artist']
2       6AICX6AYZYfM4lU24Fs08m  Decretum        梶浦 由記       「魔法少女まどか☆マギカ」 Music Collection      ['same_artist']
3       72SGuXbyrObLqljoNEscPq  Dragon  KOKIA   KOKIA 25th Anniversary Best -The Lighthouse- vol.1      ['related_artist']
4       4SBbneN8BP416V3gZgrxPy  vanity ((LIVE))         FictionJunction         FictionJunction 2008-2010 The BEST of Yuki Kajiura LIVE         ['related_artist']
5       6RidxKPzPY91OK4CX9mpfG  gunland         梶浦 由記       ソードアート・オンライン ミュージックコレクション       ['same_artist']
6       4dEtOeOPiMLS1JFmXNNfU7  ドラゴン        KOKIA   The Animal Kingdom vol.4        ['related_artist']
7       0gJLT6VVo8sfGT2VfP4zLP  Conturbatio     梶浦 由記       「魔法少女まどか☆マギカ」 Music Collection      ['same_artist']
8       4nqshrmkaHP6gxg5fuHKRQ  Forbidden Love  Cécile Corbel   Kari-gurashi    ['similar']
9       0QKqLi8BiJb1LWjiwCWWWt  なつのおもいで  Hideyuki Fukasawa       月姫 -A piece of blue glass moon- Original Soundtrack   ['related_artist']
10      401z2mPimsxygC60yte7mO  Bran (2016 Remastered)  Cécile Corbel   Harpe celtique et chants du monde (2016 Remastered)     ['similar']
11      69YY8LxMWOqcFPoqxVZBRz  Distance        FictionJunction         Distance / eternal blue        ['related_artist']
12      4VSzSJJBJdpFW7Pqpdp3LT  Flag    Wakana  そのさきへ      ['related_artist']
13      480g4Wdq5XeLqURUaQIBzI  Angel of Doom   Shiro SAGISU    Evangelion: 1.0 You Are (Not) Alone (Original Soundtrack Album)         ['similar']
14      1GahVE98RPgqBymlM1RDV6  serenato        Kalafina        Seventh Heaven  ['related_artist']
15      0QBH1oiMzl9lvQl5EKP2og  銀鷗的迴旋      HOYO-MiX        原神-幽暮襯映之月 (遊戲《原神》原聲音樂)        ['similar']
16      4JrASFUGwmeneZe47HpIR3  九月    Kalafina        After Eden      ['related_artist']
17      3GFvv1FS9oPx7yC9nsO5QV  Shir al etz (2016 Remastered)   Cécile Corbel   Harpe celtique et chants du monde (2016 Remastered)     ['similar']
18      2b9xJJDHQN1NTk1XkVT0vo  メインテーマ/眠り      Hideyuki Fukasawa       魔法使いの夜 オリジナルサウンドトラック         ['related_artist']
19      2ND8SkQ3Rav1x4zpmTwA3E  going home      Shiro SAGISU    TV Animation BLEACH Original Soundtrack 1       ['popular']
20      4dHOnPucB5VBYq3gjRtYy9  You See Big Girl / T:T  Sawano Hiroyuki         TV Anime "Attack on Titan Season 2" (Original Soundtrack)       ['popular']
复制成功
  • 种子曲目:《Oskar Schuster - Gizeh》

代码块
Python
自动换行
复制代码
Query track info: Gizeh - Oskar Schuster
======= Top 20 items =======
index   track_id                track_name      artist_name     album_name      sources
1       7EYle14QET9bWMMxfI2yeS  Für Ofelia      Oskar Schuster  Für Ofelia      ['same_artist']
2       74x7IsqYo9IwXF5K1Yu180  Vervlogen       Oskar Schuster  Singur  ['same_artist']
3       0d7dpG6tUWelr6FiWwF9uy  Maribel         Oskar Schuster  Maribel         ['same_artist']
4       66Q6qHI1mPeGiUj85F9ZGu  Historiette No. 5       Fabrizio Paterlini      Historiette No. 5       ['related_artist']
5       7bDSSxeXWsoBAea0cBGRHe  Keys for Kids   Gian Marco La Serra     Piano Whispers 01       ['related_artist']
6       4YYBYxpyLzniQw3ftqUf7W  Fantaisie       Oskar Schuster  Elixía  ['same_artist']
7       5qVwIAzhqECrb40fuRY3g0  Riddle  Matt Stewart-Evans      Solo II         ['related_artist']
8       5Ey5IykoI059V2ibfrVThZ  24 Preludes and Fugues, Op. 87: Fugue No. 2 in A Minor "Allegretto"     Dmitri Shostakovich     Shostakovich: 24 Preludes and Fugues    ['similar']
9       4QSqk7blTTNO4DuctGu8N9  Matilda         Oskar Schuster  Xorkia (Collected Singles)      ['same_artist']
10      7bWHTgGjDqGN0eoKMh0Fs1  Week #10 - Live         Fabrizio Paterlini      Live in Bratislava      ['related_artist']
11      3ml88Fc0tgun50FyxKQVBi  Adams: John's Book Of Alleged Dances: Stubble Crotchet  Kronos Quartet  25 Years        ['similar']
12      01HoInikQgofDHG7eYfa4o  Leicester       Lambert         Alone II        ['similar']
13      7EWYdhDukbN6clqu9yfL05  Week No. 9      Fabrizio Paterlini      Autumn Stories  ['related_artist']
14      0kSUqCeU3D7eeq9Z65fYpu  Marlen In Love  Lambert         Alles in bester Ordnung (Original Motion Picture Soundtrack)    ['similar']
15      4yyqznbhQpzvuAY14rpA9X  24 Preludes & Fugues, Op. 87: No. 2b, Fugue in A Minor  Dmitri Shostakovich     "Pianissimo - Classical Masterpieces"   ['similar']
16      56Z1kxbqvraUNNZjk2A5oI  24 Preludes and Fugues, Op. 87: Fugue No. 2 in A Minor: Allegretto      Dmitri Shostakovich     Shostakovich: 24 Preludes and Fugues, Op. 87    ['similar']
17      0syXiT3P1NI6dKdxDVCtPK  Fractions       Gabriel Parker  KEEN: Complete Concentration Vol. 1     ['related_artist']
18      0XaUDFrmop9zsrfKqXeHBp  Song for the Village    Federico Albanese       Blackbirds and the Sun of October       ['related_artist']
19      0VLascXUZ2Ze2u5kUD5J1f  I Giorni        Ludovico Einaudi        Islands - Essential Einaudi     ['popular']
20      1ZC21hJb2GASRkaoZvUcCS  Beving: Ala     Joep Beving     Trilogy         ['popular']
复制成功

分析

  • v0.5推荐结果相当惊艳,简直是断代升级!这推荐质量我已几乎挑不出毛病。与大厂推荐系统的PK对比可以看后续章节

Embeat v0.6:引入歌单级协同过滤

目标

  • 爬虫并清洗Spotify Playlist数据。目前已采集到6000万份Spotify Playlist数据等待清洗

  • 根据之前采集到的related_artist网络,建立playlist-artist-track之间的联系,抽出在playlist中共现度最高的track,创建related_track网络,新增第五路输出结果

  • 同时要避免出现网易云高度依赖“UGC用户自建歌单”的协同过滤,导致标签被大众用户的随意组合所污染这种情况

清洗数据

  • 目标是将这些歌单数据清洗成txt数据,一行代表一个歌单,用空格分隔歌曲ID

  • 符合条件的歌单:5 <= 歌曲总数 <= 100;收藏数 > 0;歌单名非空

  • 符合条件的歌曲:非本地文件;非播客数据;track_id需要在上述45M Qdrant数据库中有值

  • 丢弃筛选后歌曲总数<5的歌单

  • 丢弃来自同一张专辑的歌单(重要,有些用户会直接将专辑转成歌单保存)

  • 最后获得188万份歌单数据(txt),以下是txt数据的分析结果:

代码块
Python
自动换行
复制代码
Playlist count: 1885651
Playlist min track count: 5
Playlist max track count: 100
Playlist median track count: 48

All track count: 9360252
Track appears in >= 5 playlists count: 2034982
Track appears in >= 10 playlists count: 1093603
Track appears in >= 20 playlists count: 578127
Track appears in >= 50 playlists count: 231827
复制成功

方案A:加权共现统计

  • 计算每一首歌曲与其它歌曲在188万份歌单中的共现次数,然后排序,保存Top10的结果

  • 当然要设置一个共现次数阈值,比如歌曲A与歌曲B出现在>=10份歌单中,才记录这对关系,否则抛弃

  • 共现统计可能导致热门歌曲出现概率偏高,比如歌曲A与歌曲B同为热门歌曲,那么它们会在推荐中大量出现,而冷门歌曲C与热门歌曲A虽然相似,但共现次数少,可能不会被推荐。因此可以使用Jaccard相似度或PMI点互信息的方式,压制热门歌曲假相似,放大小众圈子真相似

  • 最终产物是一份JSON数据(假设共现阈值设为10),key为109万条歌曲ID(str),value为它们的Top10共现歌曲ID(list)

方案B:Track2Vec(最终使用)

  • Track2Vec其实就是用Word2Vec的思路,只是单词变成了track_id,句子变成playlist。因为歌单里的单曲是有上下文含义的,表示它们的关联性,所以可以用这种方式映射track与track的向量空间关系

  • Track2Vec相比加权共现统计来说,它能更好地捕捉歌曲与歌曲之间的更广泛的关系。比如歌曲A与歌曲B在歌单A中,歌曲A与歌曲C在歌单B中,Track2Vec能在向量空间找到歌曲B与歌曲C的关系,而加权共现统计并不能。或者说加权共现统计是Track2Vec的一个零阶近似

  • 训练使用Gensim包的Word2Vec实现,调参参考论文《Word2vec applied to Recommendation: Hyperparameters Matter》(https://arxiv.org/abs/1804.04212),具体参数如下:

代码块
Python
自动换行
复制代码
LineSentence = LineSentence(training_data)   # 使用Gensim自带的LineSentence加载txt训练数据
vector_size = 64   # 参考EmbeatMLP的向量维度为64,因为需要在CPU机器上推理,维度不宜过大。实测过64与128推理差别不大,为保障推理速度选64
min_count = 10   # 核心参数,影响vocab长度。参考上述txt训练数据的分析结果,109万的vocab属于非常合理的区间
sg = 1   # 使用Skip-Gram,用中心词预测上下文,适合稀疏数据分布,且符合推理场景(由seed_track推related_tracks)
window = 100   # 训练时每个中心歌曲能看到前后各n首歌曲作为上下文,100代表将歌单视为无序bag,歌单内所有歌曲均参与训练
negative = 20   # 负采样数量,每个正样本对应20个随机负样本,数值越大训练速度越慢。数值参考Word2Vec论文推荐取值范围
sample = 1e-3   # 高频词降采样阈值,数值越小对热门歌曲的抑制越明显。因为我有其它路召回,因此反而不应该加热门抑制
epochs = 60   # 训练轮数
ns_exponent = -0.5   # 论文重要发现,负样本采样规则。0=均匀采样,>0采样高频样本,<0采样长尾样本
复制成功
  • 训练日志:(gensim_loss在多线程下有bug,建议参考mean_delta值,两个epoch的参数相减取平均)

代码块
Python
自动换行
复制代码
Start training...
2026-05-28 23:41:34,407 INFO Epoch 0/60 | gensim_loss=10697942.0000 | mean_delta=4.1419
2026-05-28 23:44:51,709 INFO Epoch 1/60 | gensim_loss=5617480.0000 | mean_delta=1.0037
2026-05-28 23:48:09,558 INFO Epoch 2/60 | gensim_loss=4657060.0000 | mean_delta=0.7313
2026-05-28 23:51:23,022 INFO Epoch 3/60 | gensim_loss=5056134.0000 | mean_delta=0.6386
2026-05-28 23:54:40,028 INFO Epoch 4/60 | gensim_loss=4859238.0000 | mean_delta=0.5931
2026-05-28 23:57:57,542 INFO Epoch 5/60 | gensim_loss=4265310.0000 | mean_delta=0.5658
2026-05-29 00:01:15,150 INFO Epoch 6/60 | gensim_loss=3753824.0000 | mean_delta=0.5469
2026-05-29 00:04:33,429 INFO Epoch 7/60 | gensim_loss=3436768.0000 | mean_delta=0.5320
2026-05-29 00:07:50,205 INFO Epoch 8/60 | gensim_loss=3683332.0000 | mean_delta=0.5196
2026-05-29 00:11:04,196 INFO Epoch 9/60 | gensim_loss=3505080.0000 | mean_delta=0.5084
2026-05-29 00:14:22,693 INFO Epoch 10/60 | gensim_loss=3895364.0000 | mean_delta=0.4984
2026-05-29 00:17:38,231 INFO Epoch 11/60 | gensim_loss=3717476.0000 | mean_delta=0.4887
2026-05-29 00:20:51,954 INFO Epoch 12/60 | gensim_loss=3599576.0000 | mean_delta=0.4796
2026-05-29 00:24:08,539 INFO Epoch 13/60 | gensim_loss=3526380.0000 | mean_delta=0.4705
2026-05-29 00:27:24,021 INFO Epoch 14/60 | gensim_loss=3292308.0000 | mean_delta=0.4614
2026-05-29 00:30:37,936 INFO Epoch 15/60 | gensim_loss=1675064.0000 | mean_delta=0.4525
2026-05-29 00:33:52,654 INFO Epoch 16/60 | gensim_loss=1668216.0000 | mean_delta=0.4438
2026-05-29 00:37:07,566 INFO Epoch 17/60 | gensim_loss=1726856.0000 | mean_delta=0.4349
2026-05-29 00:40:22,895 INFO Epoch 18/60 | gensim_loss=1666640.0000 | mean_delta=0.4261
2026-05-29 00:43:43,298 INFO Epoch 19/60 | gensim_loss=1691360.0000 | mean_delta=0.4176
2026-05-29 00:47:00,953 INFO Epoch 20/60 | gensim_loss=1714128.0000 | mean_delta=0.4087
2026-05-29 00:50:15,335 INFO Epoch 21/60 | gensim_loss=1690992.0000 | mean_delta=0.3997
2026-05-29 00:53:32,367 INFO Epoch 22/60 | gensim_loss=1686600.0000 | mean_delta=0.3909
2026-05-29 00:56:46,125 INFO Epoch 23/60 | gensim_loss=1694440.0000 | mean_delta=0.3820
2026-05-29 01:00:00,575 INFO Epoch 24/60 | gensim_loss=1683208.0000 | mean_delta=0.3731
2026-05-29 01:03:12,995 INFO Epoch 25/60 | gensim_loss=1588208.0000 | mean_delta=0.3641
2026-05-29 01:06:27,510 INFO Epoch 26/60 | gensim_loss=1766392.0000 | mean_delta=0.3552
2026-05-29 01:09:40,711 INFO Epoch 27/60 | gensim_loss=1607168.0000 | mean_delta=0.3461
2026-05-29 01:12:55,208 INFO Epoch 28/60 | gensim_loss=1757848.0000 | mean_delta=0.3372
2026-05-29 01:16:12,403 INFO Epoch 29/60 | gensim_loss=1759744.0000 | mean_delta=0.3280
2026-05-29 01:19:27,043 INFO Epoch 30/60 | gensim_loss=1715968.0000 | mean_delta=0.3190
2026-05-29 01:22:42,381 INFO Epoch 31/60 | gensim_loss=1754432.0000 | mean_delta=0.3098
2026-05-29 01:25:58,849 INFO Epoch 32/60 | gensim_loss=1605992.0000 | mean_delta=0.3006
2026-05-29 01:29:14,724 INFO Epoch 33/60 | gensim_loss=1680360.0000 | mean_delta=0.2915
2026-05-29 01:32:28,661 INFO Epoch 34/60 | gensim_loss=1715792.0000 | mean_delta=0.2820
2026-05-29 01:35:43,445 INFO Epoch 35/60 | gensim_loss=1824312.0000 | mean_delta=0.2726
2026-05-29 01:38:57,053 INFO Epoch 36/60 | gensim_loss=1802056.0000 | mean_delta=0.2632
2026-05-29 01:42:15,332 INFO Epoch 37/60 | gensim_loss=1828976.0000 | mean_delta=0.2539
2026-05-29 01:45:30,897 INFO Epoch 38/60 | gensim_loss=1768280.0000 | mean_delta=0.2444
2026-05-29 01:48:48,761 INFO Epoch 39/60 | gensim_loss=1706584.0000 | mean_delta=0.2348
2026-05-29 01:52:08,388 INFO Epoch 40/60 | gensim_loss=1848656.0000 | mean_delta=0.2252
2026-05-29 01:55:23,611 INFO Epoch 41/60 | gensim_loss=1694264.0000 | mean_delta=0.2156
2026-05-29 01:58:41,017 INFO Epoch 42/60 | gensim_loss=1755352.0000 | mean_delta=0.2058
2026-05-29 02:02:00,063 INFO Epoch 43/60 | gensim_loss=1725520.0000 | mean_delta=0.1959
2026-05-29 02:05:17,193 INFO Epoch 44/60 | gensim_loss=1824152.0000 | mean_delta=0.1860
2026-05-29 02:08:35,027 INFO Epoch 45/60 | gensim_loss=1801016.0000 | mean_delta=0.1760
2026-05-29 02:11:54,045 INFO Epoch 46/60 | gensim_loss=1757320.0000 | mean_delta=0.1659
2026-05-29 02:15:12,126 INFO Epoch 47/60 | gensim_loss=1777720.0000 | mean_delta=0.1557
2026-05-29 02:18:28,780 INFO Epoch 48/60 | gensim_loss=1861072.0000 | mean_delta=0.1454
2026-05-29 02:21:47,227 INFO Epoch 49/60 | gensim_loss=1845992.0000 | mean_delta=0.1349
2026-05-29 02:25:09,127 INFO Epoch 50/60 | gensim_loss=1883192.0000 | mean_delta=0.1243
2026-05-29 02:28:29,265 INFO Epoch 51/60 | gensim_loss=1769672.0000 | mean_delta=0.1136
2026-05-29 02:31:53,848 INFO Epoch 52/60 | gensim_loss=1845152.0000 | mean_delta=0.1026
2026-05-29 02:35:12,050 INFO Epoch 53/60 | gensim_loss=485760.0000 | mean_delta=0.0915
2026-05-29 02:38:34,214 INFO Epoch 54/60 | gensim_loss=0.0000 | mean_delta=0.0801
2026-05-29 02:41:57,032 INFO Epoch 55/60 | gensim_loss=0.0000 | mean_delta=0.0684
2026-05-29 02:45:17,398 INFO Epoch 56/60 | gensim_loss=0.0000 | mean_delta=0.0564
2026-05-29 02:48:36,465 INFO Epoch 57/60 | gensim_loss=0.0000 | mean_delta=0.0442
2026-05-29 02:51:56,200 INFO Epoch 58/60 | gensim_loss=0.0000 | mean_delta=0.0317
2026-05-29 02:55:16,690 INFO Epoch 59/60 | gensim_loss=0.0000 | mean_delta=0.0199
DONE! Vocab length: 1093603
复制成功
  • 最终产物是track2vec.wv(词表)与track2vec.wv.vectors.npy(向量),109万的词表,两个文件加起来不到300MB,推理时完全可以全量加载进内存

Track2Vec推理例子

代码块
Python
自动换行
复制代码
Sis puella magica! - 梶浦 由記 related tracks:
  4S40MDl8IuzSHsmsctWaKI  score=0.9334
  0BEnKTrkKp5saTb2iFxnT1  score=0.9325
  0gJLT6VVo8sfGT2VfP4zLP  score=0.9310
  7pzGD0fepRx0gwZKUrHdBH  score=0.9285
  1maYnq1a9WoiNUPaTcs9uR  score=0.9277
  69dDnfotBtbj2hVHWpeKhO  score=0.9269
  6AICX6AYZYfM4lU24Fs08m  score=0.9258
  4sPKrqfM6lBaCX0COmOgzQ  score=0.9247
  3QnYPtjMm8YxCxgEI6pz0R  score=0.9230
  42fHftcN2NYb0g8x4nRzhL  score=0.9210
Query used time: 0.127ms

Tori no Uta - VISUAL ARTS / Key related tracks:
  3skl1XsMWVYXPOwq3BHRDG  score=0.9642
  5WuuZ9w47sBHBEU14KdLv0  score=0.9530
  4VTxO7SMtu0AcGqazV15AL  score=0.9519
  0vnQh69kXw7PqpyWsOgFMz  score=0.9511
  0CDma8JyHAPKM4qpJlonfA  score=0.9510
  3tXmPXQBCkAlo0FICWetwz  score=0.9505
  5wUWbDQRB3KR1JKT9oEMbX  score=0.9504
  2yyeFsdpzTIH8E9PBtNxHw  score=0.9477
  1jd2jYSUu3reC3rR6EAwYB  score=0.9452
  5SXk7a9m3bvvDOQl75K4cF  score=0.9441
Query used time: 0.018ms

Uptown Funk (feat. Bruno Mars) - Bruno Mars related tracks:
  6b8Be6ljOzmkOmFslEb23P  score=0.9380
  6JV2JOEocMgcZxYSZelKcc  score=0.9290
  57n3qOwXcoRMyGFjeqC1Rh  score=0.9155
  60nZcImufyMA1MKQY3dcCH  score=0.9035
  55h7vJchibLdUkxdlX3fK7  score=0.9001
  2iuZJX9X9P0GKaE93xcPjk  score=0.8931
  4kbj5MwxO1bq9wjT5g9HaA  score=0.8849
  2Foc5Q5nqNiosCNqttzHof  score=0.8824
  4rmFRTmHa2bWUmMLIRVEXQ  score=0.8808
  7pYfyrMNPn3wtoCyqcTVoI  score=0.8785
Query used time: 0.016ms
复制成功

效果展示(related_track加入多路召回)

  • 种子曲目:《梶浦由記 - Sis puella magica!》

代码块
Python
自动换行
复制代码
Query track_id: 0HDh5QaLfCwi2xyvkei917
Query track info: Sis puella magica! - 梶浦 由記
Query artist genres: ['anime score', 'japanese vgm']
-> Find query record used time: 2ms
-> Similar recall used time: 31ms
-> Popular recall used time: 13ms
-> Same artist recall used time: 3ms
-> Related artist recall used time: 3ms
-> Related track recall used time: 180ms
-> Re-ranking used time: 4ms
Result artist genres: ['anime score', 'mecha', 'japanese vgm', 'anime']
======= Top 20 items =======
index   track_id                track_name      artist_name     album_name      sources         score
1       0gJLT6VVo8sfGT2VfP4zLP  Conturbatio     梶浦 由記       「魔法少女まどか☆マギカ」 Music Collection      ['same_artist', 'related_track']        1.0
2       2nOcNupR8YMJFAB1ew1a7L  Desiderium      梶浦 由記       「魔法少女まどか☆マギカ」 Music Collection      ['same_artist', 'related_track']        1.0
3       75dXCLRNjxt97ZxytZwEZn  Noi!    梶浦 由記       「魔法少女まどか☆マギカ」 Ultimate Best         ['same_artist', 'related_track']        1.0
4       3n4MLZyy29Cqpa2SNxGSWv  輝く空の静寂には        Kalafina        After Eden      ['related_artist']      0.9
5       6I1hV9NMDrwhbymhdA2P8X  ARIA    Kalafina        Seventh Heaven  ['related_artist']      0.88
6       2CbcuWo8KUs3psdpV5OJ0i  優しい夜明け    See-Saw         Dream Field     ['related_artist']      0.86
7       4nqshrmkaHP6gxg5fuHKRQ  Forbidden Love  Cécile Corbel   Kari-gurashi    ['similar']     0.85
8       0Zya2iWNaZk3SngvH8sQFB  花守の丘        FictionJunction         Everlasting Songs       ['related_artist']      0.84
9       2Yj0xm6WNvEx4X1oGPdlGl  Brilliant       Yuki Hayashi    TVアニメ『ボールルームへようこそ』オリジナルサウンドトラック    ['similar']     0.83
10      3TZfOYS67mtB7scdml7CBj  Gloria  Kalafina        Lacrimosa       ['related_artist']      0.82
11      0cvVx7TnD8wSpcM49TyjsB  Arrietty's Song(Arrietty's Song)        Cécile Corbel   借りぐらしのアリエッティ サウンドトラック       ['similar']     0.81
12      16BZEyW9bSbvNIX7xfPshD  Forrest Song    Kevin Penkin    ”Spice and Wolf:MERCHANT MEETS THE WISE WOLF”Original Soundtrack Vol.1  ['similar']     0.79
13      4GwRl3fIx1UsCr3x1CYRxl  EM21 no choir   Shiro SAGISU    Shiro SAGISU outtakes from Evangelion   ['similar']     0.77
14      2UekJVxWxYfEyUbgCSCwsi  Les Passagers Du Vent   Cécile Corbel   La Fiancée      ['similar']     0.75
15      3JTJGb9OwunRDXSQlWYJ0p  新日本紀行      Taro Hakase     Dal Segno Story of My Life      ['similar']     0.73
16      480g4Wdq5XeLqURUaQIBzI  Angel of Doom   Shiro SAGISU    Evangelion: 1.0 You Are (Not) Alone (Original Soundtrack Album)         ['similar']     0.71
17      3QnYPtjMm8YxCxgEI6pz0R  黒点    藤澤慶昌        TVアニメ『宝石の国』オリジナルサウンドトラック  ['related_track']       0.67
18      60Tgoqdfj5pyYUCSaNChC0  Marginalia #143         Masakatsu Takagi        Marginalia VII  ['similar']     0.65
19      6AFkv6rIVRusZNifR74Q7t  Shingeki Gt 20130218 Kyojin     Sawano Hiroyuki         TV Anime "Attack on Titan Season 2" (Original Soundtrack)       ['popular']     0.5
20      6TT1rkkXEDaITj1xSOovbY  Zoltraak        Evan Call       Frieren: Beyond Journey's End (Original Soundtrack)     ['popular']     0.49

Query used time: 0.239s
复制成功
  • 种子曲目:《Bruno Mars - Uptown Funk》

代码块
Python
自动换行
复制代码
Query track_id: 32OlwWuMpZ6b0aN2RZOeMS
Query track info: Uptown Funk (feat. Bruno Mars) - Bruno Mars
Query artist genres: ['dance pop', 'pop']
-> Find query record used time: 22ms
-> Similar recall used time: 17ms
-> Popular recall used time: 8ms
-> Same artist recall used time: 3ms
-> Related artist recall used time: 3ms
-> Related track recall used time: 206ms
-> Re-ranking used time: 1ms
Result artist genres: ['pop', 'uk pop', 'dance pop', 'modern rock', 'filter house']
======= Top 20 items =======
index   track_id                track_name      artist_name     album_name      sources         score
1       6b8Be6ljOzmkOmFslEb23P  24K Magic       Bruno Mars      24K Magic       ['popular', 'same_artist', 'related_track']     1.0
2       0KKkJNfGyhkQ5aFogxQAPU  That's What I Like      Bruno Mars      24K Magic       ['popular', 'same_artist', 'related_track']     1.0
3       3w3y8KPTfNeOKPiqUTakBh  Locked out of Heaven    Bruno Mars      Unorthodox Jukebox      ['popular', 'related_track']    1.0
4       3cHyrEgdyYRjgJKSOiOtcS  Timber  Pitbull         Global Warming: Meltdown (Deluxe Version)       ['popular', 'related_track']    1.0
5       7uT1gmdO1Zm4wvSJxrp8kv  Give A Little More      Maroon 5        Hands All Over (Revised International Standard version)         ['related_artist']      0.9
6       6JV2JOEocMgcZxYSZelKcc  CAN'T STOP THE FEELING! (from DreamWorks Animation's "TROLLS")  Justin Timberlake       CAN'T STOP THE FEELING! (from DreamWorks Animation's "TROLLS")  ['related_track']       0.9
7       60nZcImufyMA1MKQY3dcCH  Happy - From "Despicable Me 2"  Pharrell Williams       G I R L         ['related_track']       0.88
8       2iuZJX9X9P0GKaE93xcPjk  Sugar   Maroon 5        V       ['related_track']       0.86
9       4Xf7EL8G6FRYwvJADl0nf4  Bass Down Low   DEV     Bass Down Low   ['similar']     0.85
10      7pYfyrMNPn3wtoCyqcTVoI  Moves Like Jagger - Studio Recording From "The Voice" Performance       Maroon 5        Hands All Over (Revised International Standard version)         ['related_track']       0.84
11      76hfruVvmfQbw0eYn1nmeC  Cake By The Ocean       DNCE    DNCE    ['related_track']       0.82
12      4kbj5MwxO1bq9wjT5g9HaA  Shut Up and Dance       WALK THE MOON   TALKING IS HARD         ['related_track']       0.8
13      5KwNEhNRmcARfqdodCQQX8  Borrowed Time   Scissor Sisters         Scissor Sisters (Deluxe)        ['similar']     0.78
14      2Foc5Q5nqNiosCNqttzHof  Get Lucky (Radio Edit) [feat. Pharrell Williams and Nile Rodgers]       Daft Punk       Get Lucky (Radio Edit) [feat. Pharrell Williams and Nile Rodgers]       ['related_track']       0.78
15      5WBT8hjs92lQvzynqB2UfF  Like I Love You         Justin Timberlake       Like I Love You         ['related_artist']      0.76
16      5jE48hhRu8E6zBDPRSkEq7  All About That Bass     Meghan Trainor  Title (Deluxe)  ['related_track']       0.76
17      0cqRj7pUJDkTCEsJkx8snD  Shake It Off    Taylor Swift    1989 (Deluxe)   ['related_track']       0.74
18      4D8tHXcLIM2Rpp7fXwx6jS  HEADHIGH        Doja Cat        Scarlet 2 CLAUDE        ['similar']     0.71
19      40EqrQIeP09f2Z1Xnghki5  Love Me Down    Jason Derulo    Everything Is 4         ['related_artist']      0.7
20      7qiZfU4dY1lWllzX7mPBI3  Shape of You    Ed Sheeran      ÷ (Deluxe)      ['related_track']       0.7

Query used time: 0.264s
复制成功

分析

  • 《Sis puella magica!》关联到同动漫的其它歌曲《Desiderium》《Noi!》这个并不意外,因为很多用户就是按这样的规律整理歌单

  • 《Uptown Funk》这种律动感很强的音乐能匹配到《CAN'T STOP THE FEELING!》与《Happy》等歌曲说明related_track这路推荐非常成功,说明多个用户都捕捉到“强律动”这个特征,并以此规律创建的歌单

Embeat v1.0:正式上线

数据库优化

  • 体积优化:4500万条数据,未创建索引20.1GB,创建索引后约37.4GB,处于完全可接受的范围

  • 内存优化:由于track_name (str)与 artist_name (str)的索引放到硬盘上,数据库启动后,内存占用无HNSW约9GB,有HNSW约17GB,可以接受

  • 检索速度优化:多路回召,从query_track_id (str)进,到final_result (list)出,速度控制在30~100ms区间,非常不错

API封装

  • 使用Python FastAPI对接口进行封装,输入track_id或track_name + artist_name,以及可选参数top_k,输出推荐列表

  • 这里需要做一层翻译,将query的track_name + artist_name转成track_id。由于之前Qdrant数据库以及做过分词处理,可以进行模糊搜索,但大部分时候这种模糊搜索仍不够智能,如果我想听《周杰伦 - 太阳之子》,而数据库记录的是《Jay Chou - Son of the Sun》,这是一定会匹配失败的

  • 我的处理方案是,调用Spotify的开发者API,通过`track:"太阳之子" artist:"周杰伦"`作为query,利用Spotify强大的别名搜索功能,联网找到query对应的track_id

  • 另外还设置了一套回退方案,由于Spotify严格限制API的调用频率,当请求数过多出现429时,从Tunebat爬虫获取track_id。实测过Tunebat背后调用的也是Spotify的开发者API

接入系统

  • 将Embeat接入我的免费音乐网站GD音乐台(https://music.gdstudio.xyz)

  • 新增“心动模式”:播放器底下可选择播放模式,顺序播放、单曲循环、随机播放,以及心动模式。用户点击一首歌曲开始播放后,之后听到的歌曲都以这首种子曲目为中心进行推荐播放。若用户重新点击另一首歌曲,会生成新的推荐歌单

  • V1.0先以最小功能进行发行,V2.0将会有大量好玩的新功能

对比竞品

Embeat vs 网易云

  • Embeat的推荐质量究竟如何,直接进入实战对比。这里我选用LLM-as-a-Judge方法,调用主流大模型(GPT-5.5、Gemini Flash 3.5、Claude Sonnet 4.6)当裁判,做AB盲测,以下是我的核心提示词设计:

代码块
Python
自动换行
复制代码
你是一位专业的音乐推荐质量评审员。你将对两套音乐推荐系统的输出结果进行盲评。

评测流程:
1. 你负责出题:每轮选取一首种子单曲,要求覆盖不同流派和不同热度(从冷门到热门都要有)
2. 我会根据你选的种子歌曲,获取A、B两组推荐结果(各5首歌)返回给你
3. 你需要从以下维度评价哪组推荐更好(按重要性排序,权重递减):
   - 风格相似度(权重40%):推荐结果与种子单曲的风格/氛围是否接近
   - 多样性(权重35%):推荐结果内部是否有合理的多样性而非大量重复艺人/专辑
   - 发现性(权重25%):是否能推荐出不那么显而易见但确实合适的歌曲
4. 每轮给出你的判断和简要理由

重要说明:
- 每一轮的A、B标签都是随机分配的,A和B在不同轮次中可能代表不同的系统。你只需要对当前轮次做出独立判断,不要试图跨轮次关联A或B的身份。
- 如果某组返回的歌曲数量少于5首,这是系统能力的体现,可以作为扣分项考虑。
- 如果两组差距很小难以区分,请判定为平局(tie),不要强行选出胜者。

请严格基于音乐知识和客观事实进行评价。你的回复必须只包含要求的JSON格式,不要输出任何思考过程。
复制成功
  • Claude Sonnet 4.6:8胜2负

代码块
Python
自动换行
复制代码
# Embeat vs Netease 音乐推荐系统盲评报告

- **评估日期**: 2026-05-27 19:48
- **评估模型**: anthropic/claude-sonnet-4.6
- **评测轮数**: 10

---

## 总体统计

| Embeat 胜 | Netease 胜 | 平局 |
|:---------:|:----------:|:----:|
| 8 | 2 | 0 |

---

## 各轮评测详情

### 第1轮(胜出: Embeat)

- **种子歌曲**: Bohemian Rhapsody - Queen
- **流派**: 经典摇滚
- **热度**: 大众热门

#### Embeat 推荐结果(本轮标记为B组):

1. I Want To Break Free - ['Queen']
2. We Shall Go To Town - 2021 Remaster - ['David Bowie']
3. Tower Of Babel - ['Elton John']
4. Ten Cent Pistol - ['The Black Keys']
5. I Won't Hold You Back - ['TOTO']

#### Netease 推荐结果(本轮标记为A组):

1. The Professor & La Fille Danse (Live) - ['Damien Rice']
2. Antichrist Superstar - ['Marilyn Manson']
3. (You Make Me Feel Like) A Natural Woman - ['Aretha Franklin']
4. We Are the Champions - ['Queen']
5. Shotgun Rider - ['Tim McGraw']

#### 评委评语:

B组在风格相似度上明显优于A组。Bohemian Rhapsody是Queen标志性的艺术摇滚/硬摇滚作品,B组推荐的David Bowie(华丽摇滚)、Elton John(钢琴摇滚/艺术流行)、TOTO(经典摇滚)、The Black Keys(蓝调摇滚)都与种子歌曲处于相近的风格圈子内,同一艺术家Queen的I Want To Break Free也是合理的推荐。A组则问题明显:Damien Rice是忧郁民谣、Aretha Franklin是R&B灵魂乐、Tim McGraw是乡村音乐,与Bohemian Rhapsody的风格相去甚远,Marilyn Manson的金属工业风格也偏离较多,仅We Are the Champions(同为Queen作品)勉强相关。多样性方面,B组涵盖华丽摇滚、钢琴摇滚、蓝调摇滚等子流派,内部有合理变化但整体仍围绕经典摇滚核心;A组虽然多样但多样得毫无章法,风格跨度过大。发现性方面,B组中Tower Of Babel和Ten Cent Pistol是相对不那么显而易见的推荐,有一定惊喜感。综合来看,B组明显胜出。

---

### 第2轮(胜出: Embeat)

- **种子歌曲**: 晴天 - 周杰伦
- **流派**: 华语流行
- **热度**: 大众热门

#### Embeat 推荐结果(本轮标记为B组):

1. 安靜 - ['Jay Chou']
2. 突然好想你 - ['Mayday']
3. 生命線 - 電視劇《院長爸爸》片頭曲 - ['Bii']
4. 十面埋伏 - ['Eason Chan']
5. 天天晴朗(蘇打綠版) - ['sodagreen']

#### Netease 推荐结果(本轮标记为A组):

1. In Love - ['艺曈']
2. 褪色的梦 - Let It Be - ['帷幕VMusic', '洛天依Official']
3. Soft - ['罗灵Rollinggirl']
4. 沧海一声笑 (Live) - ['那英', '周杰伦', '刘欢', '陈奕迅']
5. 流浪猫 - ['爱男孩']

#### 评委评语:

B组在风格相似度上明显优于A组。晴天是周杰伦标志性的华语流行/带中国风元素的情歌,B组推荐了周杰伦本人的安靜(同艺人推荐合理,同为抒情风格)、五月天的突然好想你(台湾主流流行摇滚,受众高度重合)、苏打绿的天天晴朗(台湾小清新流行,气质与晴天接近)、陈奕迅的十面埋伏(华语流行主流佳作)、Bii的生命線(台湾偶像流行,风格接近)。整体来看,B组紧紧围绕台湾华语流行这一核心,风格契合度高。A组则问题较大:帷幕VMusic和洛天依是虚拟歌手/翻唱内容,爱男孩是相对陌生的小众艺人,罗灵Rollinggirl的风格也较为小众,整体推荐质量参差不齐,与晴天的风格吻合度不高。多样性方面,B组推荐了台湾不同时代、不同风格方向的华语流行代表,多样性合理;A组虽然有不同艺人但质量和相关性欠佳。发现性方面两组差距不大,但B组的陈奕迅、苏打绿是更具品质感的非显而易见推荐。综合来看,B组全面胜出。

---

### 第3轮(胜出: Embeat)

- **种子歌曲**: Dynamite - BTS
- **流派**: K-Pop
- **热度**: 大众热门

#### Embeat 推荐结果(本轮标记为A组):

1. Permission to Dance - ['BTS']
2. Seven (feat. Latto) (Clean Ver.) - ['Jung Kook']
3. Side To Side - Phantoms Remix - ['Ariana Grande']
4. ICON - ['TWICE']
5. Daydream (백일몽) - ['j-hope']

#### Netease 推荐结果(本轮标记为B组):

1. 별이 되어... (Someday...) - ['帝国的孩子们']
2. 이별아...니가 이겼어 - ['李准基']
3. 여기 있을게 - ['朴炯植']
4. 눈이 멀었다 - ['MC 몽']
5. Life Goes On (Live) - ['BTS (防弹少年团)']

#### 评委评语:

A组在风格相似度上明显优于B组。Dynamite是BTS面向全球市场的英文迪斯科流行单曲,充满活力与律动感。A组推荐了Permission to Dance(BTS同系列英文单曲,风格高度一致)、Jung Kook的Seven(BTS成员个人作品,现代流行风格相符)、TWICE的ICON(K-Pop女团流行,风格接近)、j-hope的Daydream(BTS成员嘻哈流行,有合理延伸)、Ariana Grande的Side To Side Remix(流行舞曲,与Dynamite氛围相近)。整体而言,A组紧紧围绕现代K-Pop/流行舞曲这一核心,具有合理的多样性。B组则问题严重:推荐的帝国的孩子们、李准基、朴炯植、MC 몽都是偏传统韩国流行或R&B风格,与Dynamite那种充满能量的迪斯科流行气质相去甚远,Life Goes On虽是BTS作品但风格是慢板抒情,整组推荐与种子歌曲的风格偏离较大。多样性方面,A组内部有合理变化(BTS本人、BTS成员solo、K-Pop女团、欧美流行),B组则偏向韩语抒情流行,且艺人知名度参差。发现性方面A组略有优势。综合来看,A组全面胜出。

---

### 第4轮(胜出: Embeat)

- **种子歌曲**: Strobe - deadmau5
- **流派**: 电子/Progressive House
- **热度**: 电子乐爱好者中知名,大众相对冷门

#### Embeat 推荐结果(本轮标记为B组):

1. The Veldt - ['deadmau5']
2. Innocence - ['NERO']
3. In the Dark - ['The Toxic Avenger']
4. Angel On My Shoulder - ['Kaskade']
5. Relocation - ['Feed Me']

#### Netease 推荐结果(本轮标记为A组):

1. Re_Jaded - ['deadmau5']
2. Pale Hill - ['Buckethead']
3. Blue Fear - ['Armin van Buuren']
4. Pets - ['deadmau5']
5. HR 8938 Cephei - ['deadmau5']

#### 评委评语:

两组在风格相似度上都有可取之处,但B组整体表现更均衡。Strobe是deadmau5的标志性Progressive House作品,以绵长渐进的合成器氛围和情感张力著称。B组推荐了The Veldt(deadmau5自身作品,风格高度一致)、NERO的Innocence(电子舞曲,氛围接近)、Kaskade的Angel On My Shoulder(Progressive House/电子流行,与Strobe气质相符)、Feed Me的Relocation(电子舞曲)、The Toxic Avenger的In the Dark(电子摇滚/暗潮电子,有一定延伸但仍在合理范围)。B组在艺人多样性上做得更好,涵盖了Strobe所在风格圈子内不同代表艺人。A组问题在于多样性严重不足:5首中有3首是deadmau5本人作品(Re_Jaded、Pets、HR 8938 Cephei),过度依赖同一艺人,这属于懒惰推荐;Buckethead是吉他/前卫摇滚艺人,与Progressive House风格偏离明显;仅Armin van Buuren的Blue Fear(Trance风格)是合理的跨艺人推荐。多样性维度A组明显失分。发现性方面,B组推荐了Feed Me和The Toxic Avenger等相对小众但确实相关的电子音乐人,惊喜感更强。综合来看,B组胜出。

---

### 第5轮(胜出: Embeat)

- **种子歌曲**: Lemon - Kenshi Yonezu
- **流派**: 日本流行/J-Pop
- **热度**: 大众热门

#### Embeat 推荐结果(本轮标记为A组):

1. 灰色と青 ( + 菅田将暉 ) - ['Kenshi Yonezu']
2. ビンテージ - ['OFFICIAL HIGE DANDISM']
3. 白日 - ['King Gnu']
4. 何故、旅をするのだろう - ['KOBUKURO']
5. まちがいさがし - ['SUDA MASAKI']

#### Netease 推荐结果(本轮标记为B组):

1. LOSER - ['米津玄師']
2. 打上花火 - ['Daoko', '米津玄師']
3. 群青 - ['YOASOBI']
4. ブルーバード - ['いきものがかり']
5. 願い~あの頃のキミへ~ - ['當山みれい']

#### 评委评语:

两组都有合理推荐,但A组整体表现略优。Lemon是米津玄師风格成熟期的代表作,以细腻的情感表达、精致的编曲和略带忧郁的J-Pop气质著称。A组推荐了灰色と青(米津玄師本人与菅田将暉合作,风格高度一致)、OFFICIAL HIGE DANDISMの ビンテージ(现代J-Pop精品,编曲风格与Lemon接近)、King Gnuの白日(都市感强、情感浓郁的J-Pop,与Lemon气质相符)、KOBUKUROの何故、旅をするのだろう(抒情J-Pop,稍显传统但仍在范围内)、菅田将暉のまちがいさがし(温柔抒情,与Lemon的情感色调接近)。A组涵盖了米津玄師本人作品及当代日本流行音乐多个代表,整体风格契合度高且有合理多样性。B组推荐的LOSER和打上花火都是米津玄師作品(其中打上花火是与DAOKO的合作),群青是YOASOBI的代表作(风格接近),ブルーバードはいきものがかり的经典J-Pop。但願い~あの頃のキミへ~(當山みれい)是偏传统流行/歌谣曲风格,与Lemon的现代感有一定距离。B组在多样性上稍显不足,米津玄師自身作品占了两首。总体来看,A组推荐的艺人更多样,且都属于当代高质量J-Pop范畴,风格契合度和多样性综合评分略高于B组。

---

### 第6轮(胜出: Embeat)

- **种子歌曲**: Clair de Lune - Claude Debussy
- **流派**: 古典/印象派钢琴
- **热度**: 古典乐中知名,大众有一定认知

#### Embeat 推荐结果(本轮标记为A组):

1. Rêverie, L. 68: Rêverie - ['Claude Debussy']
2. Nocturne in C Minor, Op. 48 No. 1 - Live - ['Frédéric Chopin']
3. Symphony No. 2 in E Minor, Op. 27: III. Adagio - ['London Symphony Orchestra']
4. Bach: Prelude in B minor, BWV 855a (Arr. by Alexander Siloti) - ['Johann Sebastian Bach']
5. Un sospiro - ['Franz Liszt']

#### Netease 推荐结果(本轮标记为B组):

1. Sonata for 2 pianos in D, K.448:3. Allegro molto - ['Ingrid Haebler', 'Ludwig Hoffmann']
2. Klavierstücke, Op. 118: II. Intermezzo: Andante teneramente - ['Johannes Brahms', 'Marc Pantillon']
3. Suite Bergamasque: III. Clair De Lune - ['Walter Gieseking', 'Claude Debussy']
4. 亚麻色头发的少女 - ['Claude Debussy']
5. Deux danses pour harpe et orchestre à cordes, L. 103: I. Danse sacrée - ['Anaïs Gaudemard', 'Claude Debussy', "Orchestre de l'Opéra de Rouen Normandie", 'Leo Hussain']

#### 评委评语:

两组推荐都以古典钢琴为核心,但A组整体表现更优。Clair de Lune是德彪西印象派风格的代表作,以朦胧、诗意的和声和流动的琴声著称,适合与同类印象派或浪漫主义钢琴作品搭配。A组推荐了德彪西本人的Rêverie(印象派气质一致)、肖邦的Nocturne(夜曲,钢琴抒情风格与Clair de Lune高度契合)、李斯特的Un sospiro(浪漫主义钢琴,情感色调相近)、巴赫的Prelude(键盘音乐,稍显古早但仍在古典钢琴范畴)、拉赫玛尼诺夫的Symphony No.2慢乐章(管弦乐作品,与种子歌曲风格有一定距离但氛围相符)。A组在艺人多样性上涵盖了德彪西同时代及前后的多位重要作曲家,均属浪漫主义/印象派核心圈。B组问题较明显:Suite Bergamasque III实质上就是Clair de Lune本身(换了演奏者Walter Gieseking版本),这属于推荐同一作品,不应计入有效推荐;亚麻色头发的少女是德彪西另一钢琴前奏曲,风格一致但与Rêverie同属德彪西自身,多样性不足;莫扎特的双钢琴奏鸣曲风格偏古典时期,与印象派气质有差距;勃拉姆斯的间奏曲较为晦涩;竖琴协奏曲虽是德彪西作品但以竖琴为主,脱离了钢琴这一核心乐器。综合来看,A组在风格相似度和多样性上均优于B组。

---

### 第7轮(胜出: Netease)

- **种子歌曲**: 阿楚姑娘 - 花粥
- **流派**: 华语民谣
- **热度**: 国内民谣圈知名,大众相对小众

#### Embeat 推荐结果(本轮标记为B组):

1. You Are My Only One (電視劇“我和兩個他”主題曲) - ['TIA RAY']
2. 赌 - 网剧《鹦鹉》主题曲 - ['Kelly Yu']
3. Life Is Like a Drama (Take Five) 戏说戏 - ['Jasmine Chen & Steve Sweeting']
4. 浮光劫 (電視劇《書卷一夢》漣漪曲) - ['黃齡']
5. 淡寫 (電視劇《春閨夢裡人》片尾曲) - ['劉惜君']

#### Netease 推荐结果(本轮标记为A组):

1. 归去来兮 - ['花粥']
2. 我本将心向明月 - ['王朝1982', '朱旭BooBoo']
3. 燕无歇 - ['蒋雪儿Snow.J']
4. 青丝 - ['邓寓君(等什么君)']
5. 九万字 - ['黄诗扶']

#### 评委评语:

A组在风格相似度上远优于B组。阿楚姑娘是花粥风格的代表作,以叙事性歌词、木吉他伴奏、清新自然的民谣气质为核心,属于当代中国独立民谣/folk风格。A组推荐了花粥本人的归去来兮(同艺人,风格高度一致)、邓寓君(等什么君)的青丝(口水民谣/古风民谣,与花粥风格接近)、黄诗扶的九万字(抖音民谣,与花粥受众高度重合)、蒋雪儿的燕无歇(古风民谣,有一定延伸)、王朝1982与朱旭BooBoo的我本将心向明月(民谣/说唱风格,虽稍偏但仍在民谣圈内)。整体来看,A组紧紧围绕华语民谣/网络民谣这一核心风格,推荐质量较高。B组则问题严重:TIA RAY(雷婷)、Kelly Yu(于文文)、黄龄、刘惜君均属于偏主流华语流行/影视歌曲风格,与花粥那种独立民谣气质相去甚远;Jasmine Chen与Steve Sweeting的作品更是爵士/跨界风格。B组5首推荐几乎都是影视剧配套歌曲,与种子歌曲的风格圈子基本不重叠。综合来看,A组在风格相似度上全面胜出,多样性和发现性也更合理,A组毫无悬念地获胜。

---

### 第8轮(胜出: Embeat)

- **种子歌曲**: Portrait of Tracy - Jaco Pastorius
- **流派**: 爵士/融合爵士
- **热度**: 爵士乐爱好者中经典,大众相对冷门

#### Embeat 推荐结果(本轮标记为A组):

1. Donna Lee - ['Jaco Pastorius']
2. Solitude - ['John Patitucci']
3. Aldebaran - ['Björn Meyer']
4. Shhh / Peaceful - ['Miles Davis']
5. Textures - ['Herbie Hancock']

#### Netease 推荐结果(本轮标记为B组):

1. Happy as larry - ['Alex Hutchings']
2. Minor Swing - ['Eddie Higgins Trio']
3. Chicken - ['Jaco Pastorius']
4. The Chicken (Live) - ['Jaco Pastorius']
5. Donna Lee (Album Version) - ['Jaco Pastorius']

#### 评委评语:

A组在多样性维度上明显优于B组,综合表现更佳。Portrait of Tracy是Jaco Pastorius的标志性无伴奏电贝斯独奏作品,以泛音技巧和深沉的情感著称,属于融合爵士/当代爵士范畴。A组推荐了Jaco Pastorius本人的Donna Lee(同艺人,融合爵士经典)、John Patitucci的Solitude(当代爵士贝斯手,风格传承关系明显)、Björn Meyer的Aldebaran(当代实验性贝斯独奏,与Portrait of Tracy的独奏气质高度契合)、Miles Davis的Shhh/Peaceful(融合爵士先驱,Jaco曾与Miles Davis圈子有深厚渊源)、Herbie Hancock的Textures(融合爵士键盘大师,风格匹配)。A组跨越了多位不同艺人,涵盖贝斯独奏、融合爵士、当代爵士等多个维度,多样性合理且发现性强,特别是Björn Meyer这一推荐颇具惊喜感。B组问题严重:5首中有4首是Jaco Pastorius本人作品(Chicken、The Chicken Live版、Donna Lee专辑版),其中The Chicken和Chicken实质上是同一首曲子的不同版本,多样性极差,属于明显的懒惰推荐;Minor Swing是Django Reinhardt时代的爵士名曲,由Eddie Higgins Trio演绎,风格偏传统爵士,与Portrait of Tracy的融合爵士气质有一定距离。综合来看,A组全面胜出。

---

### 第9轮(胜出: Netease)

- **种子歌曲**: Natureza Morta - Seu Jorge
- **流派**: 巴西流行/MPB/Bossa Nova
- **热度**: 世界音乐爱好者中有一定知名度,大众较冷门

#### Embeat 推荐结果(本轮标记为B组):

1. Against All Odds (Take a Look at Me Now) - ['Maurício Manieri']
2. Como Eu Vou Viver - Ao Vivo - ['Tania Mara']
3. Pot Pourri NETINHO: Barracos (Escombros) / Beijo Na Boca / Preciso De Você - Live - ['Claudia Leitte']
4. Fim de Noite - Ao Vivo - ['Adryana E A Rapaziada']
5. Gritos de Guerra - ['Chiclete Com Banana']

#### Netease 推荐结果(本轮标记为A组):

1. Besame Mucho - ['小野リサ']
2. M'Envoyer des Fleurs - ['Sandrine Kiberlain']
3. Bibliothèque(图书馆) - ['Jasing Rye']
4. Bread (butter version) - ['Sofi Tukker', 'Seu Jorge']
5. Life On Mars? - ['Seu Jorge']

#### 评委评语:

两组推荐都存在明显问题,但A组整体表现优于B组。Natureza Morta是Seu Jorge的作品,属于MPB(巴西流行音乐)风格,融合了桑巴、民谣和世界音乐元素,Seu Jorge本人以温暖的嗓音和吉他演奏著称,其大卫·鲍伊葡萄牙语翻唱系列也是重要标签。A组中,Seu Jorge本人的Life On Mars?(大卫·鲍伊名曲的葡萄牙语翻唱,与Seu Jorge风格高度一致)和与Sofi Tukker合作的Bread(艺人身份一致)是合理推荐;小野リサ演绎的Besame Mucho(日本Bossa Nova名家,与种子歌曲的拉丁/巴西氛围有交集)也有一定合理性;但Sandrine Kiberlain是法语流行歌手,Jasing Rye的Bibliothèque风格存疑,与巴西音乐关联较弱。B组问题更严重:虽然推荐的都是巴西艺人,但风格方向完全偏离——Claudia Leitte是Axé/电子舞曲、Chiclete Com Banana是Axé、Adryana E A Rapaziada是Axé/Forró、Maurício Manieri是巴西流行(翻唱英文歌),这些都是截然不同的巴西音乐子流派,与Seu Jorge那种质朴、内敛的MPB/folk气质完全不符。B组似乎仅以'巴西'为关键词做了简单匹配,完全忽略了风格细分。综合来看,A组尽管不够理想,但在风格相似度和发现性上仍优于B组。

---

### 第10轮(胜出: Embeat)

- **种子歌曲**: 天外来物 - 薛之谦
- **流派**: 华语流行/动漫ACG
- **热度**: 大众热门

#### Embeat 推荐结果(本轮标记为A组):

1. 過了幾天 - ['郭家瑋']
2. 躍 - ['Joker Xue']
3. 亲爱的不要哭 - ['于冬然']
4. 小星星 - ['Silence Wang']
5. 看得最远的地方 - ['Mao Buyi']

#### Netease 推荐结果(本轮标记为B组):

1. 聊表心意 - ['薛之谦', '刘惜君']
2. 雨过后的风景 - ['Dizzy Dizzo (蔡诗芸)']
3. 慢冷 (Live) - ['李荣浩']
4. 12.31 - ['郑润泽']
5. 海屿你 - ['马也_Crabbit']

#### 评委评语:

两组推荐质量都较为一般,但A组整体略优。天外来物是薛之谦为动漫《斗罗大陆》创作的主题曲,融合了华语流行与动漫热血感,编曲较为大气,薛之谦本人风格偏抒情流行/情歌,但这首歌带有明显的动漫主题曲色彩。A组推荐了薛之谦本人的躍(Joker Xue,同艺人,风格一致性高)、毛不易的看得最远的地方(华语抒情流行,气质相近)、王silence的小星星(华语流行)、于冬然的亲爱的不要哭(华语流行)、郭家瑋的過了幾天(华语流行)。A组整体围绕华语流行/抒情男声这一核心,风格契合度较好,涵盖了薛之谦本人及同类型男歌手。B组问题在于:薛之谦与刘惜君的聊表心意是合理推荐(同艺人);李荣浩的慢冷(华语流行,风格接近)也合理;但Dizzy Dizzo蔡诗芸的风格偏R&B/嘻哈,郑润泽和马也_Crabbit是相对陌生的小众艺人,与种子歌曲的受众圈子契合度存疑。两组都没有很好地抓住天外来物动漫主题曲这一特色维度,但A组在同类型华语流行艺人的匹配上更为稳健,毛不易等艺人的推荐也有一定发现性价值。综合来看,A组略胜。

---

## 总结评价

经过10轮覆盖经典摇滚、华语流行、K-Pop、电子音乐、J-Pop、古典、民谣、爵士、巴西音乐等多种风格的盲评测试,Embeat以8:2的明显优势胜出,展现出整体更为稳健的推荐能力。

Embeat的核心优势在于风格精准度和艺人多样性的平衡。在大多数轮次中,它能够识别种子歌曲所属的细分流派,并在该风格圈内推荐多位不同艺人,避免过度依赖同一艺术家。以爵士轮次为例,它横跨Jaco Pastorius、Miles Davis、Herbie Hancock、John Patitucci、Björn Meyer五位艺人,兼顾了风格一致性与探索性;在电子音乐轮次,它同样拒绝了简单堆砌同一艺人作品的懒惰做法。此外,Embeat在部分轮次中提供了具有真实发现价值的小众推荐,如Feed Me、Björn Meyer等,体现出一定的算法深度。

Netease则在两个具体场景中胜出:华语民谣(花粥)和巴西音乐(Seu Jorge)。前者说明其在华语网络民谣的垂直生态中有较强的数据积累;后者的胜出则更多是因为Embeat的表现同样不理想,属于矮中选高。Netease暴露出的主要问题有三:其一,多样性严重不足,多次出现大量同艺人作品堆积的情况;其二,风格匹配粗糙,在巴西音乐轮次仅以地域标签而非子流派气质进行匹配;其三,推荐内容质量参差,出现虚拟歌手、版权模糊内容等影响用户体验的情况。

综合来看,Embeat是这次评测中更为可靠、更具专业性的音乐推荐系统,尤其在跨语言、跨文化的小众风格场景中优势突出。Netease在华语本土内容的深度挖掘上仍有一定价值,但整体推荐逻辑的精细度有待提升。

复制成功
  • Gemini Flash 3.5:9胜1负

代码块
Python
自动换行
复制代码
# Embeat vs Netease 音乐推荐系统盲评报告

- **评估日期**: 2026-05-27 19:01
- **评估模型**: google/gemini-3.5-flash
- **评测轮数**: 10

---

## 总体统计

| Embeat 胜 | Netease 胜 | 平局 |
|:---------:|:----------:|:----:|
| 9 | 1 | 0 |

---

## 各轮评测详情

### 第1轮(胜出: Embeat)

- **种子歌曲**: 玫瑰色的你 - 安溥
- **流派**: 华语独立摇滚
- **热度**: 中等热度(经典独立音乐)

#### Embeat 推荐结果(本轮标记为B组):

1. 藍天白雲 - ['Deserts Chang']
2. 星星堆滿天 - ['Naiwen Yang']
3. 四点的海棠花未眠 - Live - ['Ella Chen']
4. 80%完美的日子 - ['Cheer Chen']
5. 逆風飛行 - ['Zhao Chuan']

#### Netease 推荐结果(本轮标记为A组):

1. 七里香 - ['Xai小爱']
2. 稻香 - ['Lucky小爱']
3. 烟花易冷 - ['Xai小爱']
4. 告白气球(Cover) - ['Xai小爱']
5. 晴天 - ['Lucky小爱']

#### 评委评语:

本轮种子单曲《玫瑰色的你》是一首极具人文关怀和诗意表达的华语独立摇滚杰作。B组推荐表现极为优异:不仅精准抓取了同艺人(Deserts Chang即安溥/张悬)的《藍天白雲》,还推荐了在风格、时代背景和音乐态度上高度契合的台湾独立/另类女声代表,如杨乃文的经典摇滚《星星堆滿天》和陈绮贞的《80%完美的日子》,整体呈现出极高的风格相似度和优良的发现性。反观A组,全部为业余/翻唱歌手(Xai小爱、Lucky小爱)翻唱的周杰伦极度主流的商业流行歌曲,与种子单曲的独立摇滚风格毫无关联,且歌手和词曲作者高度重复,多样性和专业度极差。

---

### 第2轮(胜出: Embeat)

- **种子歌曲**: Awake - Tycho
- **流派**: 电子/后摇/氛围音乐
- **热度**: 中等偏冷门(独立电子/后摇乐迷熟知)

#### Embeat 推荐结果(本轮标记为B组):

1. Spectre - ['Tycho']
2. Feel It All Around - ['Washed Out']
3. Jets - ['Bonobo']
4. You As An Out-Of-Control Extension Of Me - ['Bogdan Raczynski']
5. Longtime - ['Yppah']

#### Netease 推荐结果(本轮标记为A组):

1. Into The Woods - ['Tycho']
2. Cirrus - ['Bonobo']
3. Liturgy - ['Phelian']
4. Dream 3 (Short Edit) - ['Max Richter', 'Ben Russell', 'Yuki Numata Resnick']
5. SimCity Theme - ['Chris Tilton']

#### 评委评语:

种子单曲《Awake》属于温暖、明亮的Chillwave和Downtempo电子流派。B组的推荐表现极其优秀:Washed Out的《Feel It All Around》是Chillwave的教科书级代表作;Bonobo的《Jets》和Yppah的《Longtime》则完美融合了电子律动与后摇的器乐质感,与Tycho的风格无缝衔接。虽然第4首Bogdan Raczynski风格略显实验,但整体依然高度契合。而A组虽然也有Tycho同艺人歌曲和Bonobo,但后续引入的Max Richter(现代古典/极简主义)和《模拟城市》原声带在听感和律动上与Tycho相去甚远。因此B组在风格一致性和推荐契合度上明显胜出。

---

### 第3轮(胜出: Embeat)

- **种子歌曲**: Lover, Please Stay - Nothing But Thieves
- **流派**: 欧美另类摇滚/慢跑抒情
- **热度**: 中等热度(摇滚乐迷高分单曲)

#### Embeat 推荐结果(本轮标记为B组):

1. If I Get High - ['Nothing But Thieves']
2. All We Have Is Now - ['Royal Blood']
3. SARAH - ['cleopatrick']
4. Warm Foothills - ['alt-J']
5. Song to the Siren (Recorded at Strongroom Studios, London) - ['Wolf Alice']

#### Netease 推荐结果(本轮标记为A组):

1. I'm Just A Country Boy - ['Don Williams']
2. cant sleep - ['lilspirit']
3. What Colour Is the Wind (Live) - ['Charlie Landsborough']
4. The Wearing of the Green - ['Wolfe Tones']
5. Do or Die (ft. DPR IAN) - ['DPR ARTIC', 'DPR IAN']

#### 评委评语:

B组获胜。B组推荐高度契合种子单曲的另类摇滚与感性慢歌特质,包含同艺人经典及Royal Blood、Wolf Alice等同风格英国独立名团的慢版曲目,兼顾高相似度与多样性。A组风格极其混乱,混杂了乡村、Emo Rap及传统民谣,与种子歌曲毫无关联。

---

### 第4轮(胜出: Embeat)

- **种子歌曲**: 打上花火 - DAOKO × 米津玄师
- **流派**: 动漫ACG/日系流行
- **热度**: 顶流热门

#### Embeat 推荐结果(本轮标记为B组):

1. Lemon - ['Kenshi Yonezu']
2. Subtitle - ['OFFICIAL HIGE DANDISM']
3. SAKURA - ['Ikimonogakari']
4. Ordinary days - ['milet']
5. 猫 〜THE FIRST TAKE ver.〜 - ['DISH//']

#### Netease 推荐结果(本轮标记为A组):

1. Show ON!(JP ver.) - ['70D', '早早']
2. 番凩 - ['KAITO', 'MEIKO']
3. 最高の片想い(yousa cover.) - ['泠鸢yousa']
4. 庭園にて(Cover acane_madder) - ['米白']
5. ODDS&ENDS - ['初音ミク']

#### 评委评语:

B组获胜。B组精准把握了种子单曲作为“J-Pop现象级大众流行”的定位,推荐了同艺人米津玄师的代表作《Lemon》,以及Official髭男dism、生物股长等日本一线主流音乐人的热门大作,兼具高水准制作与极高流行度。相反,A组偏向于小众的Vocaloid曲目及国内网络唱见翻唱,偏离了种子单曲主流大气的日系流行属性,整体质感和大众契合度不及B组。

---

### 第5轮(胜出: Embeat)

- **种子歌曲**: Nuvole Bianche - Ludovico Einaudi
- **流派**: 新古典/现代器乐/钢琴
- **热度**: 大众热门(器乐领域)

#### Embeat 推荐结果(本轮标记为A组):

1. Una Mattina - ['Ludovico Einaudi']
2. Paris s'enflamme - ['Joep Beving']
3. Dream 0 (till break of day) - Piano Short Edit - ['Max Richter']
4. Lag Fyrir Ömmu - ['Ólafur Arnalds']
5. Without You I Am A Lie - ["Dustin O'Halloran"]

#### Netease 推荐结果(本轮标记为B组):

1. Wind Song - ['Fabrizio Paterlini']
2. Ora - ['Ludovico Einaudi']
3. Vates - ['Otto A Totland']
4. In cerchio - ['Fabrizio Paterlini']
5. Lover's Tear - ['Teresa Perez']

#### 评委评语:

A组获胜。A组的推荐极为精准且分量十足,完美贴合新古典与极简主义钢琴流派。它汇聚了Ólafur Arnalds、Max Richter及Joep Beving等当代新古典乐界的殿堂级巨匠,整体艺术氛围高度统一,流畅性极佳。而B组虽表现优良,但存在同一艺人重复推荐的情况,且最后一首大提琴独奏在整体纯钢琴氛围中略显突兀,因此A组在多样性与调性统一上更胜一筹。

---

### 第6轮(胜出: Embeat)

- **种子歌曲**: Didi - Khaled
- **流派**: 民族/阿尔及利亚Rai音乐/世界音乐
- **热度**: 小众冷门(世界音乐乐迷经典)

#### Embeat 推荐结果(本轮标记为A组):

1. Abdel Kader - Live à Bercy, Paris / 1998 - ['Khaled']
2. Ma Tfakrouniche - ['Cheb Hasni']
3. Ghamza - ['Zouhair Bahaoui']
4. Sidi Mansour - ['Saber Rebai']
5. Baïda - ['Faudel']

#### Netease 推荐结果(本轮标记为B组):

1. Maghreb Gang (feat. French Montana & Khaled) - ['Farid Bang', 'French Montana', 'Khalèd']
2. C’est la vie - ['Khalèd']
3. Henna (Oriental Latin Pop) - ['Khalèd', 'Cameron']
4. Abdel Kader (Live à Bercy, Paris / 1998) - ['Rachid Taha', 'Faudel', 'Khalèd']
5. Baïna Fi Aïnik - ['Khalèd']

#### 评委评语:

A组显著优于B组。A组推荐表现出极高的专业度,完美拓展至阿尔及利亚Rai及北非音乐生态,引入了同流派传奇歌手Cheb Hasni、Faudel以及北非经典流行曲目,兼具高相似度与发现性。而B组过度依赖种子歌手Khaled个人及其合作曲目,虽然调性统一,但在多样性上几乎没有拓展,沦为单一歌手的精选辑。

---

### 第7轮(胜出: Embeat)

- **种子歌曲**: DALLA DALLA - ITZY
- **流派**: 韩国流行/K-Pop
- **热度**: 大众热门

#### Embeat 推荐结果(本轮标记为B组):

1. ICY - ['ITZY']
2. Party O'Clock - ['NMIXX']
3. Merry-Go-Round - ['IZ*ONE']
4. BITE ME - ["ICHILLIN'"]
5. Giddy - ['Kep1er']

#### Netease 推荐结果(本轮标记为A组):

1. Sticky (Live) - ['KISS OF LIFE']
2. Not Shy - ['ITZY']
3. I'm Your Girl - ['S.E.S.']
4. What is Love(Cover)live - ['ILLIT']
5. Into the Unknown (End Credit Version) (From "Frozen 2") - ['太妍']

#### 评委评语:

B组获胜。B组的推荐在风格和歌曲规格上高度统一,完美延续了种子单曲《DALLA DALLA》高能量、自信张扬的K-Pop现代女团舞曲路线(Teen Crush / Girl Crush)。所选推荐如NMIXX、Kep1er均为五代代表性女团的优质录音室正式版本。相比之下,A组推荐偏乱,包含了多个现场版(Live)和翻唱版(Cover),甚至混入了电影原声带,在播放流畅度和商业完成度上均不及B组。

---

### 第8轮(胜出: Embeat)

- **种子歌曲**: It Runs Through Me - Tom Misch feat. De La Soul
- **流派**: 欧美新灵魂乐/Neo-Soul/爵士嘻哈
- **热度**: 中等偏热门

#### Embeat 推荐结果(本轮标记为A组):

1. Rocket Fuel (feat. De La Soul) - ['De La Soul']
2. South of the River - ['Tom Misch']
3. Pharcyde - ['The Pharcyde']
4. Church - ['Awon']
5. 3 MC's - ['Q-Tip']

#### Netease 推荐结果(本轮标记为B组):

1. Lay It All on Me - ['Johnny Stimson']
2. Snitch - ['Jordan Rakei', 'REMI']
3. Can't Help Me - ['H.E.R.']
4. Surfin' - ['IAMDDB']
5. VOICE - ['Nulbarich']

#### 评委评语:

A组获胜。A组对种子单曲将“爵士吉他Neo-Soul”与“经典Alternative/Jazz Rap”完美融合的精髓拿捏得极其精准。首先推荐了Tom Misch另一首同风格金曲《South of the River》,并顺着De La Soul的线索,引入了Q-Tip、The Pharcyde、Awon等天花板级别的经典爵士嘻哈,整体律动一致性极好。B组虽包含了Jordan Rakei这样契合的音乐人,但在当代R&B和都市流行的选曲上略显发散,整体听感丝滑度不及A组。

---

### 第9轮(胜出: Netease)

- **种子歌曲**: 山鹰之歌 - 央吉玛
- **流派**: 华语原生态/民族/世界音乐
- **热度**: 冷门(高口碑艺术乐迷熟知)

#### Embeat 推荐结果(本轮标记为B组):

1. カリスマジャンボリー - ['カリスマ']
2. Last Man Standing - ['ヒプノシスマイク -D.R.B- (Bad Ass Temple)']
3. WISHING - ['Zach B']
4. Ain't About To Break - ['Shwabadi']
5. (Not) A Devil - ['shirobeats']

#### Netease 推荐结果(本轮标记为A组):

1. 花好月圆夜 - ['张璐']
2. 国风仙音 - ['凯枫']
3. 溪行桃花源 - ['李志辉']
4. 泉水丁冬 - ['杜聪']
5. 时光印记 - ['Alex']

#### 评委评语:

A组获胜。虽然两组都未能完全还原种子单曲《山鹰之歌》中央吉玛极具辨识度的原生态空灵女声,但A组依然在正确的航道上——其推荐了李志辉的中国新世纪音乐(New Age)及杜聪的排箫器乐等国风佳作,在文化背景和东方器乐调性上与民族/世界音乐相契合。而B组发生了灾难性的偏离,全组均为日本2.5次元企划(如催眠麦克风等)的二次元男声说唱及网络ACG曲目,与种子单曲的原生态世界音乐格格不入。

---

### 第10轮(胜出: Embeat)

- **种子歌曲**: 500 Miles - The Brothers Four
- **流派**: 欧美经典民谣/乡村民谣
- **热度**: 大众热门(怀旧金曲)

#### Embeat 推荐结果(本轮标记为B组):

1. Come to My Bedside, My Darlin' - ['The Brothers Four']
2. Tom Dooley - Remastered - ['The Kingston Trio']
3. When The Swallows Come Back To Capistrano - ['The Ink Spots']
4. The Old Lamplighter - ['The Browns']
5. My Dear Mary Anne - ['The New Christy Minstrels']

#### Netease 推荐结果(本轮标记为A组):

1. Yann Derrien - ['Carlos Núñez']
2. Alive (Acoustic Version) - ['Sick Individuals']
3. Wish You Were Here - ['Pink Floyd']
4. Every Breath You Take - ['The Police']
5. You Raise Me Up - ['Celtic Woman']

#### 评委评语:

B组呈现了一场教科书级别的完美推荐。它精准锁定了种子单曲《500 Miles》所属的20世纪50-60年代“美国民谣复兴运动”(Folk Revival)这一特定历史流派。推荐的The Kingston Trio、The Browns及The New Christy Minstrels,均为该时期最杰出的原声木吉他与多声部和声组合,时代感与声学质感极为协调。相比之下,A组只是将前卫摇滚、电子不插电和跨界美声拼凑在一起的怀旧乱炖,缺少对民谣灵魂的深入理解。

---

## 总结评价

本次评测中,Embeat以9比1的压倒性优势胜出。Embeat展现出了极高水准的算法精度与卓越的音乐美学品味。其核心优势在于对流派历史、艺人谱系及文化语境的深度理解,无论是50年代民谣复兴、新古典极简主义,还是精致的电子与另类摇滚,它均能实现“教科书级”的精准推荐,兼顾了听觉的一致性与高价值的挖掘性。不足之处在于其对极冷门地域文化(如华语原生态)偶有定位偏差。相比之下,网易云音乐(Netease)仅在本土偏国风新世纪领域依靠积累保有一丝底线,其整体推荐暴露出对商业热度、业余翻唱和大众流行神曲的过度依赖。在面对垂类风格时,网易云的推荐常流于表面、版本杂乱且缺乏流派逻辑。最终结论,Embeat在专业性、垂直挖掘力及音乐调性上全面碾压网易云,是审美更高级、更具专业度的音乐推荐系统。

复制成功
  • GPT 5.5:6胜4负

代码块
Python
自动换行
复制代码
# Embeat vs Netease 音乐推荐系统盲评报告

- **评估日期**: 2026-05-27 19:36
- **评估模型**: openai/gpt-5.5
- **评测轮数**: 10

---

## 总体统计

| Embeat 胜 | Netease 胜 | 平局 |
|:---------:|:----------:|:----:|
| 6 | 4 | 0 |

---

## 各轮评测详情

### 第1轮(胜出: Embeat)

- **种子歌曲**: 晴天 - 周杰伦
- **流派**: 华语流行/流行摇滚
- **热度**: 大众热门

#### Embeat 推荐结果(本轮标记为B组):

1. 說好不哭 - ['Jay Chou']
2. 突然好想你 - ['Mayday']
3. 生命線 - 電視劇《院長爸爸》片頭曲 - ['Bii']
4. 十面埋伏 - ['Eason Chan']
5. 天天晴朗(蘇打綠版) - ['sodagreen']

#### Netease 推荐结果(本轮标记为A组):

1. In Love - ['艺曈']
2. 褪色的梦 - Let It Be - ['帷幕VMusic', '洛天依Official']
3. Soft - ['罗灵Rollinggirl']
4. 沧海一声笑 (Live) - ['那英', '周杰伦', '刘欢', '陈奕迅']
5. 流浪猫 - ['爱男孩']

#### 评委评语:

B组整体明显更贴近《晴天》的华语流行/校园感/抒情摇滚气质:周杰伦《說好不哭》与种子艺人和流行抒情风格直接相关;五月天《突然好想你》、苏打绿《天天晴朗》都具备华语流行摇滚和青春抒情氛围;Bii《生命線》也在华语抒情流行范围内;陈奕迅《十面埋伏》虽偏港式流行叙事,但仍属相近语境。多样性方面,B组覆盖周杰伦、五月天、苏打绿、陈奕迅等不同华语流行/乐团/港乐路线,变化合理但不偏离主题。发现性一般,部分歌曲较热门,但搭配仍有效。A组问题较明显:多首歌曲与《晴天》的主流华语流行摇滚关联较弱,包含虚拟歌手/小众独立感内容,风格判断不稳定;《沧海一声笑 (Live)》虽然有周杰伦参与,但武侠/现场合唱气质与《晴天》差距较大。A组有一定发现性,但牺牲了最重要的风格相似度,因此B组更好。

---

### 第2轮(胜出: Netease)

- **种子歌曲**: Blinding Lights - The Weeknd
- **流派**: 欧美流行/Synth-pop
- **热度**: 全球大众热门

#### Embeat 推荐结果(本轮标记为B组):

1. Reminder - ['The Weeknd']
2. Kill Bill - ['SZA']
3. Aston Martin Music - ['Drake']
4. In My Feelings - ['Lana Del Rey']
5. Heartless - ['The Weeknd']

#### Netease 推荐结果(本轮标记为A组):

1. After Hours - ['The Weeknd']
2. Moth To A Flame - ['Swedish House Mafia', 'The Weeknd']
3. Love Me Harder - ['Ariana Grande', 'The Weeknd']
4. Lose My Mind (feat. Doja Cat) [From F1® The Movie] - ['Don Toliver', 'Doja Cat']
5. Creepin' - ['Metro Boomin', 'The Weeknd', '21 Savage']

#### 评委评语:

A组整体更接近《Blinding Lights》的The Weeknd式流行/R&B与电子流行语境。《After Hours》虽更阴郁但同专辑氛围相关,《Moth To A Flame》具备电子舞曲和夜色流行质感,《Love Me Harder》和《Creepin'》也与The Weeknd的流行R&B声线和都市氛围相连;Don Toliver/Doja Cat一曲提供了当代流行说唱/R&B方向的延展。A组的问题是The Weeknd相关歌曲占比过高,发现性和多样性一般,且并非都精准命中《Blinding Lights》的80年代synth-pop复古律动。B组包含《Reminder》《Heartless》等The Weeknd歌曲,但它们更偏暗黑R&B/Trap-pop,与《Blinding Lights》的明亮复古合成器流行差距更大;SZA、Drake、Lana Del Rey方向较分散,其中《Aston Martin Music》偏hip-hop/R&B,《In My Feelings》若指Lana Del Rey作品则更偏梦幻流行/另类流行,风格相似度不如A组。B组多样性略有,但整体关联松散。综合风格相似度和推荐合理性,A组胜出。

---

### 第3轮(胜出: Embeat)

- **种子歌曲**: Plastic Love - 竹内まりや
- **流派**: 日本City Pop/流行
- **热度**: 经典热门/网络再流行

#### Embeat 推荐结果(本轮标记为B组):

1. Mayonaka no Door / Stay With Me - ['Miki Matsubara']
2. SHYNESS BOY - ['Anri']
3. midnight cruisin' - ['Kingo Hamada']
4. 元気を出して - ['Mariya Takeuchi']
5. Midnight Pretenders - ['Tomoko Aran']

#### Netease 推荐结果(本轮标记为A组):

1. 真夜中のドア〜stay with me (シングルver.) - ['松原みき']
2. 幸せのものさし - ['竹内まりや']
3. 駅 - ['竹内まりや']
4. ミラクル・ラブ - ['竹内まりや']
5. 今夜はハーティー・パーティー - ['竹内まりや']

#### 评委评语:

B组更符合《Plastic Love》的日本City Pop语境,并且在风格相似度、多样性和发现性之间平衡更好。《Mayonaka no Door / Stay With Me》《SHYNESS BOY》《midnight cruisin'》《Midnight Pretenders》都属于或强烈关联80年代日本City Pop,具备相似的都市夜晚感、轻快律动、funk/disco/soul色彩和复古流行编曲;《元気を出して》虽偏竹内まりや的抒情流行,但仍在同一时代和艺人脉络中。A组有《真夜中のドア》这一高相关选择,也推荐了多首竹内まりや作品,但整体过度集中在同一歌手,且《駅》等歌曲更偏Adult Contemporary/抒情歌谣,与《Plastic Love》的groovy City Pop气质不完全一致。A组发现性和内部多样性都较弱,更像艺人电台;B组则涵盖松原みき、杏里、濱田金吾、亜蘭知子等代表人物,既相似又有合理扩展,因此B组胜出。

---

### 第4轮(胜出: Embeat)

- **种子歌曲**: Strobe - deadmau5
- **流派**: 电子/Progressive House
- **热度**: 电子乐圈经典热门

#### Embeat 推荐结果(本轮标记为B组):

1. The Veldt - ['deadmau5']
2. Innocence - ['NERO']
3. In the Dark - ['The Toxic Avenger']
4. Angel On My Shoulder - ['Kaskade']
5. Relocation - ['Feed Me']

#### Netease 推荐结果(本轮标记为A组):

1. Re_Jaded - ['deadmau5']
2. Pale Hill - ['Buckethead']
3. Blue Fear - ['Armin van Buuren']
4. Pets - ['deadmau5']
5. HR 8938 Cephei - ['deadmau5']

#### 评委评语:

B组整体更适合作为《Strobe》的推荐扩展。《The Veldt》与《Strobe》同属deadmau5偏旋律性、渐进铺陈的经典作品,风格高度相关;Kaskade《Angel On My Shoulder》也有progressive/melodic house的情绪化氛围;NERO、The Toxic Avenger、Feed Me虽然分别偏dubstep/electro/synthwave或更重的电子方向,但都保留了电子乐的空间感和夜间氛围,形成了合理的风格延展。B组多样性更好,既有直接相似曲,也有相邻电子子流派。A组包含《Re_Jaded》《Pets》《HR 8938 Cephei》等deadmau5作品,和种子在艺人与部分progressive house质感上较接近,《Blue Fear》作为早期trance经典也有渐进电子感;但Buckethead《Pale Hill》是器乐吉他/摇滚取向,与《Strobe》关联明显偏弱。同时A组过度依赖deadmau5,内部多样性不足,发现性有限。综合来看,A组在部分曲目上很贴近,但B组在风格相似、合理多样性和发现性之间更均衡,因此B组胜出。

---

### 第5轮(胜出: Embeat)

- **种子歌曲**: 紅蓮華 - LiSA
- **流派**: 动漫ACG/J-Rock
- **热度**: 动漫歌曲大众热门

#### Embeat 推荐结果(本轮标记为B组):

1. crossing field - ['LiSA']
2. 革命 - ['ReoNa']
3. BEASTFUL - ['GRANRODEO']
4. Welcome to the *fam - ['EGOIST']
5. いかれた世界だろ構わないぜ - ['ASCA']

#### Netease 推荐结果(本轮标记为A组):

1. 今夜かしら明日かしら - ['邓丽君']
2. ブルーバード - ['いきものがかり']
3. 群青 - ['YOASOBI']
4. 炎 - ['LiSA']
5. 絆ノ奇跡 - ['MAN WITH A MISSION', 'milet']

#### 评委评语:

B组整体更贴近《紅蓮華》的动漫ACG/J-Rock属性,且发现性更好。《crossing field》同为LiSA代表性动画歌曲,兼具高速摇滚编曲和热血女声;ReoNa、GRANRODEO、EGOIST、ASCA都与动漫歌曲圈层高度相关,整体保持了ACG摇滚/电子摇滚/燃向主题曲的语境,同时相较于只推热门番剧曲更有一定挖掘度。B组内部有女声摇滚、男性/乐队向热血摇滚、电子化ACG流行等差异,多样性合理。A组中《炎》和《絆ノ奇跡》与《鬼灭之刃》/热血动画歌曲关联较强,《ブルーバード》也是经典J-pop动画曲,《群青》则偏当代J-pop而非典型燃系J-Rock;但《今夜かしら明日かしら》是邓丽君日语歌谣,和《紅蓮華》的ACG摇滚能量差距很大,是明显不合适项。A组有大众热门曲优势,但风格稳定性和发现性不如B组。综合风格相似度、多样性和发现性,B组胜出。

---

### 第6轮(胜出: Netease)

- **种子歌曲**: Clair de Lune - Claude Debussy
- **流派**: 古典/钢琴独奏
- **热度**: 古典入门经典热门

#### Embeat 推荐结果(本轮标记为A组):

1. Suite bergamasque, L. 75: Clair de lune - ['Claude Debussy']
2. Nocturne in C Minor, Op. 48 No. 1 - Live - ['Frédéric Chopin']
3. Symphony No. 2 in E Minor, Op. 27: III. Adagio - ['London Symphony Orchestra']
4. Bach: Prelude in B minor, BWV 855a (Arr. by Alexander Siloti) - ['Johann Sebastian Bach']
5. Un sospiro - ['Franz Liszt']

#### Netease 推荐结果(本轮标记为B组):

1. Sonata for 2 pianos in D, K.448:3. Allegro molto - ['Ingrid Haebler', 'Ludwig Hoffmann']
2. Klavierstücke, Op. 118: II. Intermezzo: Andante teneramente - ['Johannes Brahms', 'Marc Pantillon']
3. Suite Bergamasque: III. Clair De Lune - ['Walter Gieseking', 'Claude Debussy']
4. 亚麻色头发的少女 - ['Claude Debussy']
5. Deux danses pour harpe et orchestre à cordes, L. 103: I. Danse sacrée - ['Anaïs Gaudemard', 'Claude Debussy', "Orchestre de l'Opéra de Rouen Normandie", 'Leo Hussain']

#### 评委评语:

B组整体更贴近《Clair de Lune》的德彪西印象主义、柔和抒情和精致音色取向。《Suite Bergamasque: III. Clair De Lune》是同曲不同演绎,相关性最高但发现性较低;《亚麻色头发的少女》同为德彪西钢琴名作,氛围和语言高度接近;《Deux danses》虽非钢琴独奏,但保留了德彪西的印象主义和朦胧色彩;勃拉姆斯《Intermezzo Op.118 No.2》也有内省、温柔的钢琴抒情感。B组的问题是莫扎特双钢琴奏鸣曲第三乐章较明快古典,与《Clair de Lune》的静谧月光感差距较大。A组在钢琴独奏比例上较高,《肖邦夜曲》《Bach/Siloti前奏曲》《Un sospiro》都可与浪漫抒情钢琴形成关联,但整体更偏浪漫派/炫技或传统调性,和德彪西的印象主义音色语言不如B组贴近;拉赫玛尼诺夫交响曲慢乐章虽抒情但非钢琴独奏,关联较弱。A组多样性尚可,但发现性和风格精准度略逊。综合来看,B组虽然也有不够贴合的曲目,但在德彪西语境和氛围相似度上更强,因此B组胜出。

---

### 第7轮(胜出: Netease)

- **种子歌曲**: Ditto - NewJeans
- **流派**: 韩国流行/K-pop/Alternative R&B
- **热度**: 亚洲及全球范围热门

#### Embeat 推荐结果(本轮标记为B组):

1. Super Shy - ['NewJeans']
2. UP - KARINA Solo - ['aespa']
3. HANN (Alone) - ['(G)I-DLE']
4. UNFORGIVEN (feat. Nile Rodgers) - ['LE SSERAFIM']
5. Connection - ['ANS']

#### Netease 推荐结果(本轮标记为A组):

1. I AM - ['IVE']
2. Lucky Girl Syndrome (Live) - ['ILLIT']
3. Deja Vu - ['RESCENE']
4. 캐치 캐치 - ['YENA (崔叡娜)']
5. 몰라요 (Live) - ['ARin', '叶舒华', '张员瑛(WONYOUNG)', '有娜 (YUNA)']

#### 评委评语:

A组整体虽然不算非常精准,但相较B组更接近《Ditto》所在的当代K-pop女团语境,并且有一定轻盈、青春、梦幻化取向。《Lucky Girl Syndrome》和《Deja Vu》与NewJeans式清爽、年轻化、柔和流行氛围较接近;《I AM》偏大歌式流行但仍是相邻的女团主流K-pop;YENA和现场合作曲则更偏明亮偶像流行,相关性一般。A组的问题是包含Live版本和部分风格偏差曲,整体并未很好抓住《Ditto》的Alternative R&B、怀旧UK garage/梦幻感。B组有《Super Shy》这一同艺人高相关曲,但其余推荐较分散:《UP》偏solo舞曲/hip-hop感,《HANN (Alone)》偏暗色异域感流行,《UNFORGIVEN》偏funk/pop-rock女团强节奏路线,和《Ditto》的低饱和、怀旧、细腻氛围差距较大;《Connection》发现性尚可但风格匹配度有限。B组多样性较强但偏离主题更明显。综合风格相似度优先,A组略胜。

---

### 第8轮(胜出: Netease)

- **种子歌曲**: Chan Chan - Buena Vista Social Club
- **流派**: 古巴Son/拉丁/世界音乐
- **热度**: 世界音乐经典热门

#### Embeat 推荐结果(本轮标记为B组):

1. Dos Gardenias - ['Buena Vista Social Club']
2. Boliviana - ['Ibrahim Ferrer']
3. Marieta - ['Sierra Maestra']
4. Words of Truth - ['Manu Chao']
5. Abicú - ['Roberto Anglero']

#### Netease 推荐结果(本轮标记为A组):

1. El Son No Ha Muerto - ['Cachao']
2. De Camino a la Vereda - ['Buena Vista Social Club']
3. Rico Vacilon - ['Vieja Trova Santiaguera']
4. Silencio - ['Ibrahim Ferrer', 'Omara Portuondo']
5. Dos Gardenias - ['Buena Vista Social Club']

#### 评委评语:

A组整体与《Chan Chan》的古巴Son、传统拉丁/世界音乐气质更贴近。《De Camino a la Vereda》《Dos Gardenias》同属Buena Vista Social Club核心语境,Ibrahim Ferrer、Omara Portuondo、Vieja Trova Santiaguera、Cachao也都与古巴传统音乐、son、bolero、descarga等脉络高度相关,能保持木吉他、打击乐、复古人声与古巴老派俱乐部氛围。A组虽然有两首来自Buena Vista Social Club,发现性不算特别强,但整体风格稳定,多样性也在son、bolero、trova、mambo/descarga之间合理展开。B组中《Dos Gardenias》、Ibrahim Ferrer《Boliviana》、Sierra Maestra《Marieta》都较合适,具备古巴传统音乐关联;但Manu Chao《Words of Truth》更偏全球化另类/雷鬼民谣流行,Roberto Anglero《Abicú》则更偏波多黎各/萨尔萨拉丁语境,与《Chan Chan》的古巴son氛围距离更远。B组有一定发现性和跨拉丁多样性,但风格相似度不如A组稳定。综合来看A组胜出。

---

### 第9轮(胜出: Embeat)

- **种子歌曲**: Breezeblocks - alt-J
- **流派**: 独立摇滚/另类流行
- **热度**: 独立音乐圈热门

#### Embeat 推荐结果(本轮标记为B组):

1. Every Other Freckle - ['alt-J']
2. Milkshake - ['Portugal. The Man']
3. Void - ['The Neighbourhood']
4. Enemy with JID (Opening Title Version) (from the series Arcane League of Legends) - ['Imagine Dragons']
5. This Orient - Starkey Remix - ['Foals']

#### Netease 推荐结果(本轮标记为A组):

1. Dissolve Me - ['Alt-J']
2. Fiona Coyne - ['Skylar Spence']
3. Chelsea Dagger - ['The Fratellis']
4. L.O.V.E. - ['YUNG BAE', 'EarthGang', 'Jon Batiste', 'Sherwyn']
5. We Three - ['Patti Smith']

#### 评委评语:

B组整体更贴近《Breezeblocks》的独立摇滚/另类流行语境。《Every Other Freckle》同为alt-J作品,保留了其怪异节奏、低音律动和艺术流行气质;Portugal. The Man、The Neighbourhood、Foals相关曲目都处在独立/另类流行摇滚的相邻范围内,虽然《Enemy》偏主流商业摇滚/流行说唱,和alt-J的实验感有差距,但整体仍比A组稳定。B组内部有alt-J本体、迷幻独立流行、暗色另类摇滚、电子化独立舞曲等变化,多样性合理。A组中《Dissolve Me》与种子高度相关,《Fiona Coyne》有一定独立流行/复古律动关联,《Chelsea Dagger》也算独立摇滚范围,但更偏车库摇滚派对感;《L.O.V.E.》偏future funk/hip-hop流行,《We Three》偏经典摇滚/诗性摇滚,与《Breezeblocks》的现代另类流行和怪诞编曲距离较远。A组发现性有一定优势,但风格一致性明显弱于B组。综合来看B组胜出。

---

### 第10轮(胜出: Embeat)

- **种子歌曲**: Svefn-g-englar - Sigur Rós
- **流派**: 后摇/冰岛艺术摇滚
- **热度**: 后摇圈经典/相对小众

#### Embeat 推荐结果(本轮标记为A组):

1. Starálfur - ['Sigur Rós']
2. Take Me Somewhere Nice - ['Mogwai']
3. Bosses Hang, Pt. II - ['Godspeed You! Black Emperor']
4. May Your Heart Be The Map - ['Epic45']
5. Six Days at the Bottom of the Ocean - ['Explosions In The Sky']

#### Netease 推荐结果(本轮标记为B组):

1. Ný batterí - ['Sigur Rós']
2. Starálfur - ['Sigur Rós']
3. Flugufrelsarinn - ['Sigur Rós']
4. Viðrar vel til loftárása - ['Sigur Rós']
5. Olsen olsen - ['Sigur Rós']

#### 评委评语:

A组在整体推荐质量上更好。风格相似度方面,A组《Starálfur》与种子同为Sigur Rós的梦幻、缓慢铺陈、冰岛艺术摇滚气质,高度贴合;Mogwai、Godspeed You! Black Emperor、Explosions In The Sky、Epic45也都处于后摇/氛围摇滚谱系中,虽然各自气质有差异,例如GYBE更宏大阴暗、EITS更器乐化情绪爆发,但都能合理延展《Svefn-g-englar》的长篇幅、氛围感和渐进结构。多样性和发现性方面,A组明显优于B组,覆盖不同后摇代表和相对小众的Epic45,既不重复又保持相关。B组全部为Sigur Rós作品,且多首来自相近时期/同一美学体系,风格贴近度非常高,《Ný batterí》《Starálfur》《Viðrar vel til loftárása》《Olsen olsen》等都与种子氛围高度一致;但其问题是几乎没有内部多样性,更像单一艺人歌单,发现性也较弱。考虑到评分中多样性和发现性合计权重较高,A组在保持足够风格相似的同时提供了更合理的扩展,因此A组胜出。

---

## 总结评价

总体看,Embeat以6:4小幅领先,优势在于更擅长围绕种子歌曲的风格标签、时代语境和圈层关系做横向扩展,尤其在City Pop、ACG摇滚、后摇、独立摇滚、电子等场景中,能兼顾相似度、多样性与发现性,不只是堆同艺人作品。不过它偶尔会为了多样化引入风格偏远曲目,导致稳定性下降。Netease的优势是命中核心艺人或同一音乐语境时非常稳,如The Weeknd、德彪西、古巴传统音乐、K-pop等轮次表现较好,风格安全感强;但不足是有时过度依赖同艺人/热门曲,像艺人电台,发现性和内部变化较弱,且个别轮次会出现明显不相关曲目。综合而言,Embeat整体推荐更具探索价值和歌单构建能力,Netease更偏保守精准。最终结论是Embeat综合表现略优,但领先幅度不大。

复制成功

主观对比与总结

  • Embeat基于Spotify的声学数据与用户歌单数据进行训练,以数据为导向的话,其实不管模型结构如何设计,Embeat推荐结果接近Spotify也在情理之中。在通过与Spotify单曲推荐实际对比后发现,Embeat的很多track推荐结果与Spotify相似,且来源命中related_track(即第五路输出:歌单协同过滤);同时artist推荐结果与Spotify相似,且来源命中related_artist(即第四路输出:相似歌手)

  • 我试过网易云、QQ、汽水的推荐,它们在较热门的歌曲推荐表现均不错,背靠大量用户UGC数据的支持,使得输入热门歌曲A,能很容易挖掘到热门歌曲B。且A、B两首歌存在的社会联系捕捉,是声学模型做不到的事情,这是协同过滤的优势。但面对冷门歌曲,由于可能出现用户聆听数据的匮乏与艺人标签的缺失,这些系统在这种情况下表现极不稳定,甚至可能出现驴头不对马嘴的推荐,比如输入是世界民族音乐,推荐ACG二次元音乐

  • Embeat微流派机制的引入,使得其在冷门歌曲的推荐上表现得很稳定。通过给每个已知流派的歌手或是通过已知流派歌手衍生的相关冷门歌手,为200多万歌手(约占Spotify上2/3)指定6600+个流派标签中的一个微流派,在流派空间做歌手级别的聚类。这样当冷门歌曲无法通过协同过滤这一路获得相似推荐,还能在歌手自己的流派空间找邻居,不至于完全失焦

  • EmbeatMLP是一个基于对比学习设计的MLP双塔模型,因为设计初期就是为了适配VPS的低性能CPU推理,所以输出维度也很低,只有64。它的作用是将Spotify Audio Features各种声学参数(danceability、energy、valance...)编码成向量,这样可以通过向量数据库快速找到其声学邻居。EmbeatMLP是Embeat项目的基底,几乎每一路输出都会用到这份声学向量,深入参与筛选、粗排、精排等环节。这个小模型的具体意义是:

  1. 通过微流派标签找到同流派歌手的所有歌曲,然后使用EmbeatMLP向量,找到声学相似度更高的输出(流派相似+声学相似)

  2. 找同歌手所有歌曲,然后使用EmbeatMLP向量筛选声学相似度更高的输出(相同歌手+声学相似)

  3. 找当前歌手的相关歌手的所有歌曲,使用EmbeatMLP向量筛选声学相似度更高的输出(相似歌手+声学相似)

  4. 在新歌的处理上,通过Spotify接口能获得其Audio Features,然后将这些数据输入给EmbeatMLP做前向推理(纯CPU的VPS可实时处理),能得到新歌的声学embedding,具备扩展性

  • Embeat在设计初期并没有第五路输出歌单协同过滤。在种子歌曲为热门歌曲的推荐上,虽说风格是一致了,但总觉得缺了点东西,同一种风格的音乐容易感觉到腻,用当时AI的评价来说就是,“Embeat是个品味极高的风格筛选机器”,缺乏了人性的味道。通过训练第二个模型Track2Vec则很好地弥补了这一缺点,通过学习大量Spotify用户歌单,了解用户真正的听歌偏好,Embeat在做到风格统一的基础上,能够给出一些我们更期望的结果,比如听到MJ能联想到火星哥,听到周杰伦能联想到陈奕迅

  • 至此,Embeat音乐推荐系统确切的定义是,Content-based的声学模型 + Collaborative Filtering的用户偏好模型 + 庞大干净的Metadata数据库。CB保证了听感风格相似,CF确保推荐的歌曲不会过于小众,数据库各种丰富的标签保障冷门歌曲不失焦。这也是为什么Embeat作为个人开发推荐系统能在多个维度击败网易云这种企业级推荐系统的原因。当然我们下结论不要一棒子打死,网易云在本土音乐推荐上仍保持有优势,其协同过滤模型十分强大,值得学习借鉴

未披露的细节

本章节内容

  • 这部分是我在开发GD音乐台应用时冒出的一些想法,更多的是工程实践上的内容,与Embeat这套系统关联度较低,所以不详细展开介绍

  • 如果对以下内容感兴趣,可以B站私信或TG群讨论,我将在私聊中分享更多实现细节

听歌助手EmbeatAgent

  • Embeat音乐推荐系统的扩展应用,本质上是做一个Agent应用,用户输入自然语言文本,如”我想听一首陈奕迅,比较平静的歌曲“,系统通过意图分析与各种tool use,最终返回《陈奕迅 - 红玫瑰》等歌曲

  • 目前GD音乐台已实现第一版,欢迎体验

自动换源

  • 几乎所有音乐平台的推荐系统都与自家的音乐数据库强绑定,因此输入输出都很具体唯一,比如网易云相似歌曲接口的输入是网易云歌曲ID,输出是一组网易云歌曲ID,不存在转换问题

  • GD音乐台是一个聚合音乐平台,输入五花八门,可能来自网易云、酷我、Tidal、Spotify等10个平台,我自然无法去做10平台曲库映射到Spotify ID这种操作,因此需要做一些工程设计,将10平台歌曲的元数据转换到Spotify平台,通过Embeat拿到推荐后,再将Spotify元数据重新映射回其它平台,实现比如“输入网易云ID -> Embeat推荐 -> 输出网易云ID”这样的转换功能

  • 经过三轮设计迭代,目前GD音乐台的自动换源系统已十分稳定,在热门与较冷门歌曲的匹配度上已高度一致。只要不是平台独占歌曲,基本都能找到匹配项

关于我

技能树

  • AI算法工程师(语音方向)

  • CG动画设计师

  • 业余编曲

联系方式

  • GitHub(GD Studio)

网页链接​

  • B站(GD-Studio)

GD-Studio GD-Studio

  • TG群(gdstudio_music)

https://t.me/gdstudio_music

GD Studio

2026.06.26