nlp_structbert_sentence-similarity_chinese-large 项目实战:构建基于相似度的智能推荐系统
nlp_structbert_sentence-similarity_chinese-large 项目实战构建基于相似度的智能推荐系统你有没有过这样的体验刚在新闻App里看完一篇关于新能源汽车的深度报道首页马上就给你推了好几篇相关的行业分析和技术解读感觉它比你自己还懂你想看什么。或者在电商平台浏览了一款运动相机接下来几天商品推荐里就出现了各种配件、同类竞品甚至相关的旅行攻略。这背后往往藏着一个聪明的“大脑”——推荐系统。今天我们不谈那些复杂的算法黑盒就来聊聊一个非常直接、有效而且特别适合中文场景的思路基于语义相似度的推荐。简单来说就是让机器真正“读懂”内容然后帮你找到“意思上”最接近的东西。我们将手把手带你用nlp_structbert_sentence-similarity_chinese-large这个强大的中文语义理解模型从零搭建一个能落地的推荐系统。整个过程就像搭积木我们会一步步完成离线特征准备、在线服务部署最后还会聊聊怎么验证它到底有没有用。无论你是想优化现有的推荐效果还是好奇这套技术怎么运转这篇文章都能给你一个清晰的路线图。1. 为什么是语义相似度推荐在深入代码之前我们先花点时间聊聊“为什么”。市面上推荐算法那么多协同过滤、矩阵分解、深度学习模型层出不穷为什么我们要选择语义相似度这条路径想象一下传统的协同过滤。它很像朋友推荐“喜欢A商品的人也喜欢B商品”。这个方法很有效但它有个“冷启动”难题一个新商品刚上架没人买过系统就不知道推荐给谁一个新用户刚注册没任何行为系统也不知道该推什么。它依赖历史行为数据数据稀疏时效果就打折扣。而语义相似度推荐走的是另一条路。它不关心“谁和谁行为相似”它关心“这个东西和那个东西在意思上像不像”。系统会先“阅读”并理解所有内容比如新闻标题、商品描述把它们变成一串有意义的数字向量。当用户对某个内容产生兴趣时系统就去海量内容库里快速找出和这个内容意思最接近的其他内容。这种方法有几个实实在在的好处不怕冷启动一个新商品只要你有描述文本模型就能理解它并找到相似商品进行推荐。可解释性强推荐理由不再是“神秘算法”而是“因为您刚看的《特斯拉自动驾驶技术解析》与这篇《激光雷达在自动驾驶中的新进展》内容高度相关”。这让推荐结果更容易被用户理解和接受。跨领域泛化模型对语义的理解能力是通用的。今天训练它理解新闻明天它也能用来理解商品描述、短视频标题甚至技术文档迁移成本相对较低。当然它的效果高度依赖于一个核心组件语义理解模型。它必须足够聪明能区分“苹果手机”和“吃的苹果”能明白“深度学习框架”和“机器学习库”是近义词。而这正是nlp_structbert_sentence-similarity_chinese-large的用武之地。它是一个在大规模中文语料上训练好的模型专门用于计算句子之间的语义相似度效果在中文领域相当出色。2. 实战第一步准备你的“素材库”理论说再多不如动手干。我们假设你是一个内容平台的技术负责人现在有一个包含10万篇文章的库需要为每篇文章生成一个语义向量作为后续推荐的“素材”。这个过程我们称之为“离线特征计算”它不要求实时可以每天或每周跑一次。核心任务就是用模型把每一篇文章的标题或关键内容变成一个固定长度的向量。首先确保环境准备好。我们需要安装一些基础的包。pip install transformers torch接下来是核心的向量化脚本。我们会把过程写得清晰一些方便你理解每一步在做什么。# offline_feature_extraction.py import torch from transformers import AutoTokenizer, AutoModel import numpy as np import pandas as pd from tqdm import tqdm import pickle import os # 1. 加载模型与分词器 print(正在加载模型与分词器...) model_name IDEA-CCNL/Erlangshen-SimCSE-110M-Chinese # 使用一个公开可用的、效果类似的中文相似度模型作为示例 tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) model.eval() # 设置为评估模式关闭dropout等训练层 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) print(f模型已加载至设备: {device}) # 2. 定义一个函数用于将单条文本转换为向量 def text_to_vector(text, max_length128): 将输入文本编码为语义向量。 使用均值池化mean pooling来获得句子级别的表示。 if not isinstance(text, str) or not text.strip(): # 如果文本无效返回零向量 return np.zeros(model.config.hidden_size) # 分词并转换为模型输入 inputs tokenizer(text, max_lengthmax_length, truncationTrue, paddingmax_length, return_tensorspt) inputs {k: v.to(device) for k, v in inputs.items()} # 前向传播不计算梯度以节省内存 with torch.no_grad(): outputs model(**inputs) # 获取最后一层隐藏状态并取均值作为句子向量 # shape: (batch_size, seq_len, hidden_size) - (batch_size, hidden_size) last_hidden_state outputs.last_hidden_state attention_mask inputs[attention_mask] # 扩展attention_mask的维度用于计算加权平均 mask_expanded attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float() # 将padding部分mask为0的贡献置零然后求和 sum_embeddings torch.sum(last_hidden_state * mask_expanded, 1) sum_mask torch.clamp(mask_expanded.sum(1), min1e-9) # 防止除零 sentence_vector sum_embeddings / sum_mask # 转换为numpy数组并返回 return sentence_vector.cpu().numpy().squeeze() # 3. 加载你的内容数据 # 假设你有一个CSV文件包含‘article_id’和‘title’两列 print(正在加载内容数据...) df_content pd.read_csv(./data/articles.csv) # 请替换为你的实际数据路径 texts df_content[title].tolist() ids df_content[article_id].tolist() print(f共需处理 {len(texts)} 条内容。) # 4. 批量处理生成向量 batch_size 32 # 根据你的GPU内存调整 all_vectors [] all_ids [] for i in tqdm(range(0, len(texts), batch_size), desc生成向量): batch_texts texts[i:ibatch_size] batch_ids ids[i:ibatch_size] batch_vectors [] for text in batch_texts: vec text_to_vector(text) batch_vectors.append(vec) all_vectors.extend(batch_vectors) all_ids.extend(batch_ids) # 5. 保存向量和ID的映射关系 print(正在保存特征向量...) feature_dict {aid: vec for aid, vec in zip(all_ids, all_vectors)} # 保存为pickle文件方便后续加载 os.makedirs(./model_features, exist_okTrue) with open(./model_features/article_vectors.pkl, wb) as f: pickle.dump(feature_dict, f) # 也可以保存为numpy格式便于其他系统读取 np.save(./model_features/article_ids.npy, np.array(all_ids)) np.save(./model_features/article_vectors.npy, np.array(all_vectors)) print(离线特征计算完成) print(f向量维度: {all_vectors[0].shape}) print(f特征已保存至 ./model_features/ 目录)跑完这个脚本你的每篇文章就都有了独一无二的“语义身份证”向量。这个文件就是我们推荐系统的基石。3. 搭建实时推荐服务离线特征准备好了接下来就要解决实时问题用户刚刚点击了一篇文章我们怎么在毫秒内给他推荐出最相关的其他文章这就需要搭建一个在线服务。这个服务需要做两件事1. 快速将用户当前感兴趣的内容变成向量2. 在海量向量中快速找到最相似的几个。第二点涉及大量计算直接遍历10万条是不现实的我们需要一个“搜索引擎”来加速这里引入FaissFacebook开源的向量相似度搜索库。首先安装必要的库pip install fastapi uvicorn faiss-cpu # 如果使用GPU安装 faiss-gpu然后我们创建一个简单的FastAPI应用来提供推荐服务。# online_recommendation_service.py from fastapi import FastAPI, HTTPException import numpy as np import pickle import faiss from typing import List import torch from transformers import AutoTokenizer, AutoModel import asyncio from contextlib import asynccontextmanager import time # --- 全局变量在服务启动时加载--- feature_dict None index None id_list None tokenizer None model None device None # 1. 服务启动和关闭时的生命周期管理 asynccontextmanager async def lifespan(app: FastAPI): # 启动时加载资源 print(正在启动服务加载模型与索引...) load_resources() print(资源加载完毕服务就绪。) yield # 关闭时清理资源可选 print(服务关闭。) app FastAPI(lifespanlifespan) # 2. 资源加载函数 def load_resources(): global feature_dict, index, id_list, tokenizer, model, device # 加载离线计算好的特征 print(加载文章特征向量...) with open(./model_features/article_vectors.pkl, rb) as f: feature_dict pickle.load(f) # 准备Faiss索引 # 首先我们需要知道向量的维度和数量 sample_vec next(iter(feature_dict.values())) vector_dim sample_vec.shape[0] num_vectors len(feature_dict) print(f构建Faiss索引维度: {vector_dim}, 数量: {num_vectors}) # 创建一个Flat索引精确搜索适合百万级以下数据 # 对于更大数据量可以考虑IVFFlat, IVFPQ等索引 index faiss.IndexFlatIP(vector_dim) # 使用内积点积作为相似度度量余弦相似度需先对向量做L2归一化 # 将向量添加到索引中 id_list [] vector_matrix [] for aid, vec in feature_dict.items(): id_list.append(aid) vector_matrix.append(vec) vector_matrix np.array(vector_matrix).astype(float32) # 对向量进行L2归一化这样内积就等于余弦相似度 faiss.normalize_L2(vector_matrix) index.add(vector_matrix) print(f索引构建完成共添加 {index.ntotal} 个向量。) # 加载模型用于实时文本编码与离线阶段相同 model_name IDEA-CCNL/Erlangshen-SimCSE-110M-Chinese tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModel.from_pretrained(model_name) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) model.eval() print(f语义模型已加载至 {device}。) # 3. 定义文本到向量的函数与离线阶段类似但适配单条处理 def encode_query(text: str, max_length128) - np.ndarray: 将查询文本编码为向量 inputs tokenizer(text, max_lengthmax_length, truncationTrue, paddingmax_length, return_tensorspt) inputs {k: v.to(device) for k, v in inputs.items()} with torch.no_grad(): outputs model(**inputs) last_hidden_state outputs.last_hidden_state attention_mask inputs[attention_mask] mask_expanded attention_mask.unsqueeze(-1).expand(last_hidden_state.size()).float() sum_embeddings torch.sum(last_hidden_state * mask_expanded, 1) sum_mask torch.clamp(mask_expanded.sum(1), min1e-9) sentence_vector sum_embeddings / sum_mask vec_np sentence_vector.cpu().numpy().squeeze().astype(float32) # 同样进行L2归一化 faiss.normalize_L2(vec_np.reshape(1, -1)) # Faiss的normalize_L2是in-place操作需要reshape为2D return vec_np # 4. 定义核心的推荐API接口 app.get(/recommend/by_text) async def recommend_by_text(query_text: str, top_k: int 10): 根据用户输入的文本进行实时推荐。 - query_text: 用户当前感兴趣的文本如文章标题 - top_k: 返回最相似的文章数量 start_time time.time() if not query_text: raise HTTPException(status_code400, detail查询文本不能为空) try: # 步骤1: 将查询文本编码为向量 query_vector encode_query(query_text) # 步骤2: 在Faiss索引中搜索最相似的向量 # Faiss搜索要求输入是2D数组 distances, indices index.search(query_vector.reshape(1, -1), top_k) # 步骤3: 将索引位置映射回实际的文章ID recommended_ids [id_list[idx] for idx in indices[0]] similarity_scores distances[0].tolist() # 余弦相似度分数 # 组装返回结果 results [] for aid, score in zip(recommended_ids, similarity_scores): results.append({article_id: aid, similarity_score: round(score, 4)}) latency round((time.time() - start_time) * 1000, 2) # 毫秒 return { query: query_text, recommendations: results, latency_ms: latency } except Exception as e: raise HTTPException(status_code500, detailf推荐过程中发生错误: {str(e)}) app.get(/recommend/by_article_id) async def recommend_by_article_id(article_id: str, top_k: int 10): 根据用户历史点击的文章ID进行推荐。 直接从预计算的向量库中获取该文章的向量然后进行搜索。 start_time time.time() if article_id not in feature_dict: raise HTTPException(status_code404, detailf未找到ID为 {article_id} 的文章) try: # 获取预计算的文章向量 query_vector feature_dict[article_id].astype(float32).copy() # 需要重新归一化因为存储的向量可能未归一化取决于离线保存方式 # 这里假设我们保存的是未归一化的原始向量所以先归一化 faiss.normalize_L2(query_vector.reshape(1, -1)) distances, indices index.search(query_vector.reshape(1, -1), top_k) recommended_ids [id_list[idx] for idx in indices[0] if id_list[idx] ! article_id] # 过滤掉自己 similarity_scores distances[0].tolist() results [] for aid, score in zip(recommended_ids, similarity_scores): results.append({article_id: aid, similarity_score: round(score, 4)}) latency round((time.time() - start_time) * 1000, 2) return { seed_article_id: article_id, recommendations: results, latency_ms: latency } except Exception as e: raise HTTPException(status_code500, detailf推荐过程中发生错误: {str(e)}) # 5. 健康检查接口 app.get(/health) async def health_check(): return {status: healthy, index_size: index.ntotal if index else 0} if __name__ __main__: import uvicorn uvicorn.run(app, host0.0.0.0, port8000)现在你的推荐引擎就搭好了。启动服务后你可以通过访问http://localhost:8000/recommend/by_text?query_text人工智能未来发展趋势top_k5来获取推荐结果。服务会先理解“人工智能未来发展趋势”这句话然后从你的文章库中找出语义上最接近的5篇。4. 让推荐系统越用越聪明A/B测试框架系统上线了但你怎么知道它真的比原来的推荐策略好是点击率提升了还是用户阅读时长增加了这就需要一套科学的评估方法——A/B测试。A/B测试的核心思想很简单将用户随机分成两组A组和B组。A组实验组使用我们新建的语义推荐系统B组对照组使用旧的推荐策略。在相同时间段内对比两组的核心业务指标如点击率、转化率、人均阅读数等。搭建一个完整的A/B测试平台是个大工程但我们可以先设计一个轻量级的框架来验证想法。关键在于流量分割和数据埋点。第一步在推荐服务中集成分流逻辑修改我们的在线服务让它能根据用户ID或请求ID来决定使用哪种推荐策略。# 在 online_recommendation_service.py 中新增 import hashlib def get_user_bucket(user_id: str, experiment_name: str semantic_rec_v1) - str: 根据用户ID和实验名称决定用户进入哪个分组。 使用哈希取模的方式确保分流均匀且稳定。 hash_obj hashlib.md5(f{experiment_name}_{user_id}.encode()) hash_int int(hash_obj.hexdigest(), 16) bucket_num hash_int % 100 # 分成100个桶 # 假设我们分配50%的流量到实验组A50%到对照组B if bucket_num 50: return A # 实验组使用语义推荐 else: return B # 对照组使用旧策略例如热门推荐 # 修改推荐接口增加user_id参数和分流逻辑 app.get(/recommend/feed) async def get_recommendation_feed(user_id: str, current_article_id: str None, top_k: int 10): 获取用户推荐流的主接口内部进行A/B分流。 bucket get_user_bucket(user_id) # 记录日志用于后续分析这里简化成打印实际应写入日志系统或消息队列 print(f[AB-TEST] user_id: {user_id}, bucket: {bucket}, seed_article: {current_article_id}) if bucket A: # 实验组使用语义推荐 if current_article_id: # 基于当前文章推荐 rec_result await recommend_by_article_id(current_article_id, top_k) rec_result[strategy] semantic_similarity else: # 如果没有当前文章可以回退到热门推荐或使用用户画像这里简化 # 我们先返回一个空结果实际项目需要更完善的逻辑 rec_result {recommendations: [], strategy: semantic_similarity_fallback} else: # 对照组使用旧策略例如返回近期热门文章 # 这里模拟一个热门推荐函数 rec_result get_popular_recommendations(top_k) rec_result[strategy] popularity_baseline rec_result[user_bucket] bucket return rec_result def get_popular_recommendations(top_k: int): 模拟一个基于热门的推荐函数你需要实现从数据库或缓存中获取热门文章列表 # 这里仅作示例随机从id_list中选一些作为“热门” import random if id_list: sampled_ids random.sample(id_list, min(top_k, len(id_list))) results [{article_id: aid, score: 1.0} for aid in sampled_ids] # 热门推荐没有相似度分数 return {recommendations: results} return {recommendations: []}第二步关键的数据埋点推荐结果返回给前端后前端需要记录用户的每一次点击行为并将这些行为数据包含user_id,article_id,strategy,timestamp等发送到你的数据分析平台。这是评估效果的黄金数据。一个简化的数据上报格式可能如下{ event: article_click, user_id: user_12345, article_id: article_67890, recommend_strategy: semantic_similarity, // 或 popularity_baseline user_bucket: A, timestamp: 2023-10-27T10:30:00Z, position: 3 // 推荐列表中的位置 }第三步分析与得出结论收集一段时间比如一周的数据后你就可以进行分析了。核心是比较实验组A和对照组B在核心指标上的差异。点击率 (CTR) 推荐点击次数 / 推荐曝光次数人均点击次数文章深度阅读率阅读时长30秒的比例你可以使用SQL或PythonPandas进行简单的分析# 假设你有一个存储点击日志的DataFrame df_clicks # 计算各分组的点击率 impression_counts df_log[df_log[event]impression].groupby(user_bucket).size() click_counts df_clicks.groupby(user_bucket).size() ctr_by_bucket (click_counts / impression_counts).to_dict() print(f实验组(A) CTR: {ctr_by_bucket.get(A, 0):.4f}) print(f对照组(B) CTR: {ctr_by_bucket.get(B, 0):.4f}) print(f相对提升: {(ctr_by_bucket.get(A,0) - ctr_by_bucket.get(B,0)) / ctr_by_bucket.get(B, 1e-9):.2%})如果实验组的指标显著优于对照组并且统计上可信可以通过假设检验如t-test来确认那么恭喜你你的语义推荐系统通过了实战检验可以逐步扩大流量最终全量上线。5. 回顾与展望走完这一趟我们从理解“为什么用语义相似度”到动手把十万篇文章变成向量再到搭建一个能实时响应、毫秒级推荐的服务最后还设计了验证它是否有效的科学方法。整个过程就像完成了一个完整的工程项目闭环。用下来感觉这套方案最大的优势就是直观和灵活。直观在于它的原理好理解出了问题也容易排查——无非是模型没理解对文本或者向量找得不准确。灵活在于它不挑食只要是文本内容新闻、商品、帖子、视频标题都能用同一套流程来处理维护起来也相对简单。当然在实际业务中一个成熟的推荐系统远比我们今天搭建的demo复杂。它可能需要融合用户画像、实时行为序列、多目标优化既要点击率也要互动率等。我们今天的语义相似度模型可以作为一个非常强大的“内容理解”模块嵌入到更庞大的系统中与其他策略协同工作比如作为召回阶段的一路重要渠道。如果你正准备尝试我的建议是先从一个小而具体的场景开始。比如专门为你的“科技频道”文章做相似推荐或者为“商品详情页”的“看了又看”模块换上新引擎。控制好影响范围快速实验用A/B测试的数据说话。效果好就扩大战果遇到问题也能及时调整。技术永远是为业务目标服务的。nlp_structbert_sentence-similarity_chinese-large这类模型给了我们一把锋利的“语义理解”之刃但怎么用好它切出什么样的菜还得看我们厨师对业务的理解和匠心。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。