算法说明
综合用户浏览(权重1 )和购买记录(权重2 )
进行归一化处理,消除品类数量差异
示例:购买电子产品的权重是浏览书籍的2 倍
价格区间计算:
基于用户历史行为计算平均价格和标准差
使用高斯函数计算价格相似度,考虑价格分布的集中趋势
推荐评分:
综合品类匹配度(70 % )和价格匹配度(30 % )
使用流式处理进行高效计算和排序
优化方向:
冷启动问题:新用户无历史记录时,可结合热门商品推荐
实时更新:使用滑动窗口机制处理用户行为的时效性
深度学习:对于大规模数据可升级为神经网络模型
多维度特征:加入品牌、用户画像等更多特征
在计算商品相似度时,对于每个候选商品:
类别得分:该商品的每个类别的权重之和,根据用户偏好中的类别权重。例如,商品有类别A和B,用户偏好中A的权重是2 ,B的权重是3 ,那么得分是2 + 3 = 5 。或者可能需要归一化处理,比如用户的总权重是某个值,然后计算相似度。
或者,使用余弦相似度,将用户偏好向量和商品向量表示为TF- IDF类型的权重,然后计算点积。
例如,将所有可能的类别收集为一个词汇表,然后用户偏好向量是每个类别的出现次数(或TF- IDF值),商品向量是该商品是否属于每个类别(0 或1 )。然后计算余弦相似度。
价格相似度可以单独计算,比如使用高斯核函数:exp( - ( price_diff) ^ 2 / ( 2 * sigma^ 2 ) ) ,其中sigma是标准差,根据用户购买记录中的价格计算得出。
然后,总相似度是类别相似度乘以0.7 + 价格相似度乘以0.3 ,或者其他权重组合。
在代码中,可能需要先遍历所有用户的购买和浏览记录,统计类别出现的次数,计算每个类别的权重。购买次数和浏览次数可能需要不同的权重,比如购买一次算2 分,浏览一次算1 分。
然后,对于每个候选商品,计算其类别得分,即该商品所属类别在用户偏好中的权重之和。然后归一化这个得分,例如除以最大可能的得分,使其在0 到1 之间。
价格方面,计算该商品的价格与用户平均价格的差异,然后转化为相似度得分。例如,如果用户平均价格是100 元,商品价格是120 元,差异是20 元,价格相似度可以是max ( 0 , 1 - ( 20 / 100 ) ) = 0.8 ,如果差异超过100 ,则得分为0 。
或者,价格区间可以设定为用户购买记录中的最低和最高价格,然后如果商品价格在此区间内,得1 分,否则按距离扣分。
处理冷启动的常见方法包括:
1. 推荐热门商品:当用户没有历史记录时,推荐销量或浏览量高的商品。
2. 基于人口统计学的推荐:比如根据用户的年龄、性别等信息,但当前User 类没有这些属性,可能需要扩展。
3. 随机推荐:但可能不够精准。
4. 混合推荐:结合热门和随机推荐。
由于当前代码中的User 类没有其他属性,可能采用推荐热门商品作为冷启动策略。需要修改推荐方法,当检测到用户没有历史记录时,切换到热门商品推荐。
新增功能说明:
冷启动用户判断:
通过isColdStartUser( ) 方法检测用户是否没有浏览和购买记录
自动切换推荐策略
全局特征分析:
getGlobalCategoryPreferences( ) :分析当前候选商品的品类分布
getGlobalPriceRange( ) :计算全体候选商品的价格分布
混合推荐策略:
优先推荐高频品类商品(平台热门商品)
结合价格中位数附近商品(大众接受度高的价格区间)
权重调整为品类60 % + 价格40 %
动态适应机制:
根据当前可推荐商品实时计算特征
自动适应商品上下架变化
public class ProductRecommender {
private static final double CATEGORY_WEIGHT = 0.7 ;
private static final double PRICE_WEIGHT = 0.3 ;
private static final double PRICE_RANGE_FACTOR = 0.3 ;
private static final double COLD_START_CATEGORY_WEIGHT = 0.6 ;
private static final double COLD_START_PRICE_WEIGHT = 0.4 ;
private final Map< UserShopGoods, Map< String, Double >> userCategoryCache = new WeakHashMap<> ( ) ;
private final Map< UserShopGoods, double [ ] > userPriceRangeCache = new WeakHashMap<> ( ) ;
private final Map< List< ShopGoodsItem> , Map< String, Double >> globalCategoryCache = new WeakHashMap<> ( ) ;
private final Map< List< ShopGoodsItem> , double [ ] > globalPriceRangeCache = new WeakHashMap<> ( ) ;
private Map< String, Double > getCategoryPreferences( UserShopGoods user ) {
return userCategoryCache. computeIfAbsent( user , k - > computeCategoryPreferences( user ) ) ;
}
private double [ ] getPriceRange( UserShopGoods user ) {
return userPriceRangeCache. computeIfAbsent( user , k - > computePriceRange( user ) ) ;
}
private Map< String, Double > getGlobalCategoryPreferences( List< ShopGoodsItem> items) {
return globalCategoryCache. computeIfAbsent( items, k - > computeGlobalCategoryPreferences( items) ) ;
}
private double [ ] getGlobalPriceRange( List< ShopGoodsItem> items) {
return globalPriceRangeCache. computeIfAbsent( items, k - > computeGlobalPriceRange( items) ) ;
}
public List< ShopGoodsItem> recommend( UserShopGoods user , List< ShopGoodsItem> allItems, int topN) {
Set < ShopGoodsItem> purchasedItems = new HashSet<> ( user . getPurchaseHistory( ) ) ;
List< ShopGoodsItem> candidates = allItems. stream( )
. filter( item - > ! purchasedItems. contains ( item) )
. collect( Collectors. toList( ) ) ;
if ( candidates. isEmpty( ) ) return Collections. emptyList( ) ;
if ( isColdStartUser( user ) ) {
return coldStartRecommend( candidates, topN) ;
}
else {
Map< String, Double > categoryPrefs = getCategoryPreferences( user ) ;
double [ ] priceRange = getPriceRange( user ) ;
return candidates. parallelStream( )
. map( item - > new AbstractMap. SimpleEntry<> ( item,
calculateScore( item, categoryPrefs, priceRange) ) )
. sorted( ( e1, e2) - > Double . compare( e2. getValue( ) , e1. getValue( ) ) )
. limit ( topN)
. map( Map. Entry::getKey)
. collect( Collectors. toList( ) ) ;
}
}
private boolean isColdStartUser( UserShopGoods user ) {
return user . getBrowseHistory( ) . isEmpty( )
&& user . getPurchaseHistory( ) . isEmpty( ) ;
}
private Map< String, Double > computeCategoryPreferences( UserShopGoods user ) {
Map< String, Double > preferences = new HashMap<> ( ) ;
user . getPurchaseHistory( ) . forEach( item - >
item. getCategories( ) . forEach( cat - >
preferences. merge ( cat, 2.0 , Double ::sum) ) ) ;
user . getBrowseHistory( ) . forEach( item - >
item. getCategories( ) . forEach( cat - >
preferences. merge ( cat, 1.0 , Double ::sum) ) ) ;
double max = preferences. values ( ) . stream( )
. max ( Double ::compare)
. orElse( 1.0 ) ;
preferences. replaceAll( ( k, v) - > v / max) ;
return preferences;
}
private double [ ] computePriceRange( UserShopGoods user ) {
DoubleStream priceStream = DoubleStream. concat(
user . getPurchaseHistory( ) . stream( ) . mapToDouble( a- > a. getPrice( ) . doubleValue( ) ) ,
user . getBrowseHistory( ) . stream( ) . mapToDouble( a- > a. getPrice( ) . doubleValue( ) )
) . filter( p - > p > 0 ) ;
double [ ] stats = priceStream. collect(
( ) - > new double [ 3 ] ,
( acc, p) - > {
acc[ 0 ] + = p;
acc[ 1 ] + = p * p;
acc[ 2 ] + + ;
},
( acc1, acc2) - > {
acc1[ 0 ] + = acc2[ 0 ] ;
acc1[ 1 ] + = acc2[ 1 ] ;
acc1[ 2 ] + = acc2[ 2 ] ;
}
) ;
long count = ( long) stats[ 2 ] ;
if ( count = = 0 ) {
return new double [ ] {0 , Double . MAX_VALUE};
}
double avg = stats[ 0 ] / count;
double variance = ( stats[ 1 ] / count) - ( avg * avg) ;
double stdDev = Math. sqrt( variance) ;
return new double [ ] {
Math. max ( 0 , avg - stdDev * PRICE_RANGE_FACTOR) ,
avg + stdDev * PRICE_RANGE_FACTOR
};
}
private double calculateScore( ShopGoodsItem item,
Map< String, Double > categoryPrefs,
double [ ] priceRange) {
double categoryScore = item. getCategories( ) . stream( )
. mapToDouble( cat - > categoryPrefs. getOrDefault( cat, 0.0 ) )
. average( )
. orElse( 0 ) ;
double priceScore = calculatePriceScore( item. getPrice( ) . doubleValue( ) , priceRange) ;
return ( CATEGORY_WEIGHT * categoryScore)
+ ( PRICE_WEIGHT * priceScore) ;
}
private List< ShopGoodsItem> coldStartRecommend( List< ShopGoodsItem> candidates, int topN) {
Map< String, Double > globalCats = getGlobalCategoryPreferences( candidates) ;
double [ ] globalPriceRange = getGlobalPriceRange( candidates) ;
return candidates. stream( )
. map( item - > new AbstractMap. SimpleEntry<> (
item,
calculateColdStartScore( item, globalCats, globalPriceRange) ) )
. sorted( ( e1, e2) - > Double . compare( e2. getValue( ) , e1. getValue( ) ) )
. limit ( topN)
. map( Map. Entry::getKey)
. collect( Collectors. toList( ) ) ;
}
private Map< String, Double > computeGlobalCategoryPreferences( List< ShopGoodsItem> items) {
Map< String, Double > freq = new HashMap<> ( ) ;
items. forEach( item - >
item. getCategories( ) . forEach( cat - >
freq. merge ( cat, 1.0 , Double ::sum) ) ) ;
double max = freq. values ( ) . stream( )
. max ( Double ::compare)
. orElse( 1.0 ) ;
freq. replaceAll( ( k, v) - > v / max) ;
return freq;
}
private double [ ] computeGlobalPriceRange( List< ShopGoodsItem> items) {
DoubleSummaryStatistics stats = items. stream( )
. mapToDouble( p- > p. getPrice( ) . doubleValue( ) )
. summaryStatistics( ) ;
if ( stats. getCount( ) = = 0 ) return new double [ ] {0 , Double . MAX_VALUE};
double stdDev = Math. sqrt( items. stream( )
. mapToDouble( p - > Math. pow( p. getPrice( ) . doubleValue( ) - stats. getAverage( ) , 2 ) )
. average( )
. orElse( 0 ) ) ;
return new double [ ] {
Math. max ( 0 , stats. getAverage( ) - stdDev * PRICE_RANGE_FACTOR) ,
stats. getAverage( ) + stdDev * PRICE_RANGE_FACTOR
};
}
private double calculateColdStartScore( ShopGoodsItem item,
Map< String, Double > globalCats,
double [ ] priceRange) {
double catScore = item. getCategories( ) . stream( )
. mapToDouble( k- > globalCats. getOrDefault( k, 0.0 ) )
. average( )
. orElse( 0 ) ;
double priceScore = calculatePriceScore( item. getPrice( ) . doubleValue( ) , priceRange) ;
return ( COLD_START_CATEGORY_WEIGHT * catScore)
+ ( COLD_START_PRICE_WEIGHT * priceScore) ;
}
private double calculatePriceScore( double price, double [ ] range) {
double mid = ( range[ 0 ] + range[ 1 ] ) / 2 ;
double width = range[ 1 ] - range[ 0 ] ;
double variance = Math. pow( width / 4 , 2 ) ;
if ( variance = = 0 ) return 1.0 ;
double exponent = - Math. pow( price - mid, 2 ) / ( 2 * variance) ;
return Math. exp( exponent) ;
}
public static void main( String[ ] args) {
List< ShopGoodsItem> allItems = Arrays. asList(
new ShopGoodsItem( "A1" , new HashSet<> ( Arrays. asList( "Electronics" , "Smart Home" ) ) , BigDecimal. valueOf( 299.99 ) ) ,
new ShopGoodsItem( "B2" , new HashSet<> ( Arrays. asList( "Books" , "Education" ) ) , BigDecimal. valueOf( 19.99 ) ) ,
new ShopGoodsItem( "C3" , new HashSet<> ( Arrays. asList( "Electronics" , "Accessories" ) ) , BigDecimal. valueOf( 89.99 ) ) ,
new ShopGoodsItem( "D4" , new HashSet<> ( Arrays. asList( "Fashion" , "Men" ) ) , BigDecimal. valueOf( 129.99 ) ) ,
new ShopGoodsItem( "E5" , new HashSet<> ( Arrays. asList( "Home" , "Kitchen" ) ) , BigDecimal. valueOf( 49.99 ) )
) ;
UserShopGoods activeUser = new UserShopGoods(
Arrays. asList( allItems. get( 1 ) ) ,
Arrays. asList( allItems. get( 0 ) )
) ;
UserShopGoods newUser = new UserShopGoods( Collections. emptyList( ) , Collections. emptyList( ) ) ;
ProductRecommender recommender = new ProductRecommender( ) ;
System. out . println( "===== 活跃用户推荐 =====" ) ;
List< ShopGoodsItem> rec1 = recommender. recommend( activeUser, allItems, 100 ) ;
rec1. forEach( System. out ::println) ;
System. out . println( "\n===== 冷启动用户推荐 =====" ) ;
List< ShopGoodsItem> rec2 = recommender. recommend( newUser, allItems, 100 ) ;
rec2. forEach( System. out ::println) ;
}
}
商品实体类
@Data
@EqualsAndHashCode( callSuper = false )
public class ShopGoodsItem implements Serializable {
private final String id ;
private final Set< String> categories;
private final BigDecimal price;
}
@Data
public class UserShopGoods {
// 浏览记录
private final List< ShopGoodsItem> browseHistory;
// 购买记录
private final List< ShopGoodsItem> purchaseHistory;
public UserShopGoods( List< ShopGoodsItem> browseHistory, List< ShopGoodsItem> purchaseHistory) {
this.browseHistory = Collections.unmodifiableList( new ArrayList<> ( browseHistory)) ;
this.purchaseHistory = Collections.unmodifiableList( new ArrayList<> ( purchaseHistory)) ;
}
}