1. 项目概述与核心价值
这个基于Python+Django的个性化新闻推荐系统,本质上是通过协同过滤算法解决信息过载问题的典型应用。我在实际开发中发现,现代用户平均每天接触的新闻量超过300条,但真正感兴趣的不足5%。系统通过分析用户历史行为(点击、停留时长、收藏等),建立新闻物品相似度矩阵,实现"千人千面"的智能推荐。
技术栈选择上,Django框架的全栈特性非常适合毕业设计类项目——自带ORM简化数据库操作、Admin后台开箱即用、模板引擎便于前后端混合开发。而协同过滤算法作为推荐系统领域的经典方案,其优势在于不需要复杂的特征工程,仅凭用户-物品交互数据就能产生不错的效果。
2. 系统架构设计
2.1 技术栈选型依据
- Django vs Flask:虽然Flask更轻量,但Django自带用户认证、Admin后台等组件,能节省30%以上的基础功能开发时间
- SQLite vs MySQL:考虑到毕业设计的单机部署场景,SQLite无需服务部署的特性更合适,实测可支持10万级新闻数据存储
- 传统协同过滤 vs 深度学习:基于物品的协同过滤(IBCF)计算复杂度为O(n²),在新闻推荐场景下(物品数通常<10万)完全可在消费级硬件上实时运算
2.2 数据流设计
mermaid复制graph TD
A[用户行为数据] --> B(行为日志收集)
B --> C{实时处理}
C -->|批量| D[相似度矩阵计算]
C -->|实时| E[个性化推荐]
D --> F[物品相似度库]
E --> G[推荐结果]
F --> E
G --> H[前端展示]
注意:实际开发中建议用Redis缓存用户最近行为,避免频繁查询数据库
3. 核心算法实现
3.1 相似度计算优化
原始余弦相似度计算存在热门物品主导问题,采用改进的加权公式:
python复制def adjusted_cosine(item1, item2, interactions):
# 获取同时对item1和item2有过行为的用户
common_users = set(interactions[item1].keys()) & set(interactions[item2].keys())
numerator = sum(interactions[item1][u] * interactions[item2][u] for u in common_users)
item1_norm = sqrt(sum(pow(interactions[item1][u], 2) for u in interactions[item1]))
item2_norm = sqrt(sum(pow(interactions[item2][u], 2) for u in interactions[item2]))
# 加入惩罚因子(0.3是可调参数)
return numerator / (item1_norm * item2_norm) * pow(len(common_users), 0.3)
3.2 冷启动解决方案
- 新闻冷启动:基于TF-IDF提取关键词,计算文本相似度作为初始推荐依据
- 用户冷启动:采用混合策略:
- 注册时选择兴趣标签
- 初期展示热度榜(点击量+时间衰减)
- 收集至少5次点击后启用个性化推荐
4. 关键代码实现
4.1 推荐引擎核心类
python复制class NewsRecommender:
def __init__(self):
self.similarity_threshold = 0.15 # 相似度阈值
self.top_k = 20 # 每个种子新闻最多取20个相似项
def train(self, user_news_interactions):
"""训练相似度矩阵"""
self.news_sim = defaultdict(dict)
news_users = defaultdict(dict)
# 构建倒排索引
for user, news in user_news_interactions.items():
for nid, score in news.items():
news_users[nid][user] = score
# 计算相似度
news_ids = list(news_users.keys())
for i in range(len(news_ids)):
for j in range(i+1, len(news_ids)):
sim = adjusted_cosine(news_ids[i], news_ids[j], news_users)
if sim > self.similarity_threshold:
self.news_sim[news_ids[i]][news_ids[j]] = sim
self.news_sim[news_ids[j]][news_ids[i]] = sim
def recommend(self, user_history, max_recommend=10):
"""生成推荐结果"""
candidates = defaultdict(float)
for nid, score in user_history.items():
if nid not in self.news_sim:
continue
similar_items = sorted(self.news_sim[nid].items(),
key=lambda x: x[1], reverse=True)[:self.top_k]
for related_nid, sim in similar_items:
if related_nid in user_history:
continue
candidates[related_nid] += score * sim
return sorted(candidates.items(), key=lambda x: x[1], reverse=True)[:max_recommend]
4.2 Django视图集成
python复制# views.py
def get_recommendations(request):
if not request.user.is_authenticated:
return JsonResponse({'status': 'error', 'msg': '请先登录'})
# 获取用户最近100条交互记录
user_actions = UserAction.objects.filter(user=request.user)\
.order_by('-action_time')[:100]\
.values('news__id', 'action_type')
# 行为量化(点击=1,阅读超30秒=2,收藏=3)
user_history = defaultdict(int)
for action in user_actions:
news_id = action['news__id']
if action['action_type'] == 'click':
user_history[news_id] += 1
elif action['action_type'] == 'read':
user_history[news_id] += 2
elif action['action_type'] == 'collect':
user_history[news_id] += 3
# 获取推荐
recommender = cache.get('news_recommender') # 从缓存获取训练好的模型
if not recommender:
recommender = NewsRecommender()
interactions = get_all_interactions() # 从数据库加载全量数据
recommender.train(interactions)
cache.set('news_recommender', recommender, 3600*12) # 缓存12小时
recommendations = recommender.recommend(user_history)
# 获取新闻详情
news_ids = [item[0] for item in recommendations]
news_list = News.objects.filter(id__in=news_ids)\
.annotate(
collect_count=Count('useraction',
filter=Q(useraction__action_type='collect'))
)
return render(request, 'recommend.html', {'news_list': news_list})
5. 可视化实现方案
5.1 用户画像雷达图
javascript复制// 使用Echarts生成
function drawUserProfileChart(interests) {
const chart = echarts.init(document.getElementById('profile-chart'));
const option = {
radar: {
indicator: [
{ name: '时政', max: 100 },
{ name: '科技', max: 100 },
{ name: '财经', max: 100 },
{ name: '体育', max: 100 },
{ name: '娱乐', max: 100 }
]
},
series: [{
type: 'radar',
data: [{ value: interests }]
}]
};
chart.setOption(option);
}
5.2 新闻热度趋势图
python复制# 在Django视图中处理时间序列数据
def get_news_trend(news_id, days=7):
end_date = timezone.now()
start_date = end_date - timedelta(days=days)
trends = (
UserAction.objects
.filter(news__id=news_id,
action_time__range=(start_date, end_date))
.annotate(date=TruncDate('action_time'))
.values('date')
.annotate(count=Count('id'))
.order_by('date')
)
# 补全缺失日期
date_range = [start_date + timedelta(days=i) for i in range(days+1)]
trend_dict = {item['date']: item['count'] for item in trends}
return [
{'date': d.strftime('%Y-%m-%d'),
'count': trend_dict.get(d.date(), 0)}
for d in date_range
]
6. 性能优化实践
6.1 相似度矩阵压缩存储
采用稀疏矩阵存储策略:
- 只保存相似度>0.15的物品对
- 使用
scipy.sparse.dok_matrix格式 - 序列化后存入Redis,体积减少70%
python复制from scipy.sparse import dok_matrix
import pickle
def save_similarity_matrix(sim_dict, news_index):
"""将相似度字典转为稀疏矩阵存储"""
n = len(news_index)
matrix = dok_matrix((n, n), dtype=np.float32)
for i in range(n):
for j in range(i+1, n):
if sim_dict.get(news_index[i], {}).get(news_index[j], 0) > 0.15:
matrix[i,j] = sim_dict[news_index[i]][news_index[j]]
matrix[j,i] = matrix[i,j]
# 序列化存储
return pickle.dumps({
'matrix': matrix,
'index': news_index
})
6.2 实时推荐优化
-
层次化召回:
- 第一层:基于用户最近点击的3个新闻找相似
- 第二层:基于用户长期兴趣画像
- 第三层:热度补充
-
多线程计算:
python复制from concurrent.futures import ThreadPoolExecutor
def parallel_recommend(user_history, recommender):
with ThreadPoolExecutor(max_workers=3) as executor:
futures = []
for nid in list(user_history.keys())[:3]: # 取最近3个
futures.append(executor.submit(
recommender.get_similar_items,
nid, top_k=10
))
candidates = defaultdict(float)
for future in futures:
for item, sim in future.result():
candidates[item] += user_history[item] * sim
return candidates
7. 部署注意事项
- 定时任务配置:
bash复制# crontab -e
0 3 * * * /path/to/python /project/manage.py update_similarity_matrix
- Django生产配置:
python复制# settings.py
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/1',
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor',
}
}
}
# 推荐模型更新信号
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
@receiver(post_save, sender=UserAction)
def on_action_update(sender, instance, **kwargs):
cache.delete('user_history_' + str(instance.user_id))
8. 评估与改进
8.1 离线评估指标
| 指标 | 计算公式 | 目标值 |
|---|---|---|
| 准确率 | 推荐列表中用户实际点击的比例 | >15% |
| 覆盖率 | 被推荐过的新闻占总新闻的比例 | >40% |
| 新颖度 | 推荐结果的平均热度分位数(1-最热) | <0.3 |
8.2 AB测试方案
python复制# middleware.py
class ABTestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if 'recommend_version' not in request.session:
request.session['recommend_version'] = random.choice(['A', 'B'])
response = self.get_response(request)
return response
# views.py
def recommend_view(request):
version = request.session.get('recommend_version', 'A')
if version == 'A':
# 原算法
results = original_recommend(request.user)
else:
# 新算法
results = new_recommend(request.user)
# 埋点统计
track_event(request.user, 'show', version, results)
return render(...)
9. 项目扩展方向
-
多模态融合:
- 结合新闻封面图CNN特征
- 使用BERT提取文本embedding
- 混合协同过滤+内容特征
-
实时兴趣漂移:
python复制def time_decay(actions):
"""时间衰减函数"""
now = time.time()
return sum(
1 / (1 + 0.5 * (now - action.time) / 3600) # 半衰期2小时
for action in actions
)
- 社交增强:
- 融合好友的点击行为
- 基于社交网络传播热度
这个项目最让我惊喜的是协同过滤在新闻场景的适应性——通过合理设置时间衰减因子(我最终采用2小时半衰期),即使没有复杂的深度学习模型,也能获得不错的实时推荐效果。建议初次实现时先完成基础版本,再逐步添加冷启动、多策略融合等高级特性。
