2023年3月

读写Excel基本代码

直接复制不一定能用

实体类

@ExcelIgnore 在导出操作中不会被导出
@ExcelProperty 在导入过程中 可以根据导入模板自动匹配字段, 在导出过程中可用于设置导出的标题名字

@Getter
@Setter
public class Material{
    @ExcelIgnore
    private Long id;

    /** 所属部门 */
    @ExcelProperty(value = {"一级合并标题","所属部门"})
    private String department;

    /** 所属部室/项目部/站区 */
    @ExcelProperty(value = {"一级合并标题","所属部室/项目部/站区"})
    private String area;
}

监听类

监听类控制导入数据的规则限制,监听类继承
AnalysisEventListener<Object>
类实现接口。
监听中无法调用Spring接口,使用反序列化注入Bean工厂中使用
public class MaterialListener extends AnalysisEventListener<Material> {
    // 反序列化注入bean工厂
    ISysDictDataService dictDataService = SpringUtils.getBean(ISysDictDataService.class);
    //可以通过实例获取该值
    private List<Material> datas = new ArrayList<>();
    //错误信息
    private List<String> errorInfo = new ArrayList<>();
    @Override
    public void invoke(Material material, AnalysisContext analysisContext) {
        doSomething(material,analysisContext);//根据自己业务做处理

        datas.add(material);//数据存储到list,供批量处理,或后续自己业务逻辑处理。
    }
    private void doSomething(Material object, AnalysisContext analysisContext) {
        //获取当前行数
        Integer row = analysisContext.readRowHolder().getRowIndex();
        SysDictData sysDictData=new SysDictData();
        if (ObjectUtils.isBlank(object.getType())){
            errorInfo.add(" 第" + (row+1) + "行:物资种类不能为空,请重新填写后再导入!<br>");
        }else {
            sysDictData.setDictType("material_type");
            sysDictData.setDictLabel(object.getType());
            List<SysDictData> list3= dictDataService.selectDictDataList(sysDictData);
            if (ObjectUtils.isBlank(list3)){
                errorInfo.add(" 第" + (row+1) + "行:物资种类:"+object.getType()+"未在数据字典中找到,请先填写后再导入!<br>");
            }
        }
    }
    //返回结果集对象
    public List<Material> getDatas() {
        return datas;
    }

    public void setDatas(List<Material> datas) {
        this.datas = datas;
    }

    //返回错误消息
    public List<String> getErrorInfo() {
        return errorInfo;
    }

    public void setErrorInfo(List<String> errorInfo) {
        this.errorInfo = errorInfo;
    }
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {

    }
}

控制器

导入数据方法


materialService.saveList(materialList)
中进行处理导入获取过来的数据,用于存取业务数据
    @PostMapping("/importData")
    @ResponseBody
    public AjaxResult  importData(@RequestParam MultipartFile file) throws Exception
    {
        MaterialListener excelListener = new MaterialListener();
        // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
        //EasyExcel.read(哪个文件).sheet(那张sheet表).head(表头什么样子).headRowNumber(表头占几行).registerReadListener(处理数据的监听器类).doRead()
        EasyExcel.read(file.getInputStream(), Material.class, excelListener).sheet().head(Material.class).headRowNumber(2).doRead();
        if(ObjectUtils.isBlank(excelListener.getErrorInfo())){
            if(ObjectUtils.isNotBlank(excelListener.getDatas())){
                List<Material> materialList = excelListener.getDatas();
                materialService.saveList(materialList);
            }
            return AjaxResult.success(1);
        }else{
            return AjaxResult.error("操作失败",excelListener.getErrorInfo());
        }
    }

导出数据方法

单sheet导出在注释的方法出,多sheet导出如下所示
 @GetMapping("/exportData")
    public void exportData(Material material, HttpServletResponse response){
        //换方法连表查询
//        List<Material> list = materialService.selectMaterialListByDict(material);
        List<Material> materialList=new ArrayList<>();
        List<Material> list = materialService.selectMaterialList(material);
        String[] ids=new String[list.size()];
        for (Material p:list) {
		//用于修改导出的数据
            String list7= dictDataService.selectDictLabel("material_type",p.getType());
            if (ObjectUtils.isNotBlank(list7)){
                p.setType(list7);
            }
            materialList.add(p);
        }
        List<MaterialSolid> materialSolidList=new ArrayList<>();
        List<MaterialSolid> materialSolidListS = materialSolidService.selectMaterialSolidListByIds(ids);
        for (MaterialSolid solid:materialSolidListS) {
            String list7= dictDataService.selectDictLabel("material_type",solid.getType());
            if (ObjectUtils.isNotBlank(list7)){
                solid.setType(list7);
            }
            materialSolidList.add(solid);
        }
        try {
            response.setContentType("application/vnd.ms-excel");
            response.setCharacterEncoding("utf-8");
            // 这里URLEncoder.encode可以防止中文乱码 当然和EASYEXCEL没有关系
            String fileName = URLEncoder.encode("物料信息台账", "UTF-8");
            response.setHeader("Content-disposition", "attachment;filename="+ fileName + ".xlsx");
            //调用方法进行写的操作
//            EasyExcel.write(response.getOutputStream(),Material.class).sheet("物料信息台账").doWrite(materialList);
            //多sheet页导出
            ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
            WriteSheet writeSheet1 = EasyExcel.writerSheet(0, "物料信息总账").head(Material.class).build();
            WriteSheet writeSheet2 = EasyExcel.writerSheet(1, "物料信息详情").head(MaterialSolid.class).build();
            excelWriter.write(materialList, writeSheet1);
            excelWriter.write(materialSolidList, writeSheet2);
            excelWriter.finish();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

需要完成功能

借助redis Stream 数据结构实现消息队列,异步完成订单创建,其中涉及到了缓存(击穿,穿透,雪崩),锁(Redisson),并发处理,异步处理,Lua脚本

IDE:IDEA 2022

1、读取库存数据 【Lua】

2、判断库存 【Lua】

3、扣减库存 【Lua】

4、创建队列和组 【Java】

5、发送队列消息 【Lua】

6、消息读取并处理 【Java】

在完成功能之前 ,需要了解一下redis 中有关stream 数据结构相关的命令

XACK:确认消息已经处理,redis 会在PEL(pending entries List )中移除一个或多个消息。一般情况下 一个消息被 XREADGROUP 或 XCLAIM之后会被写入PEL。

XADD: 把消息(Entry  ,key-value)追加到队列,默认如果队列不存在会创建,除非使用 NOMKSTREAM ,之后 可以通过XREAD ,XREANGE 等命令读取或通过XDEL,XTRIM移除消息

XCLAIM,XAUTOCLAIM 改变PEL 中的消息的所有者

XDEL:在队列中移除一个或多个消息(entry)

XGROUP CREATE :在指定的队列中创建一个消费者组,队列key有且仅能有一个,否则重复会提示:-BUSYGROUP 不存在会提示:ERR no such key ,可以通过选项 MKSTREAM 在不存在时创建

XGROUP CREATECONSUMER:在一个给定的队列和消费者组中创建一个消费者,不能重复。此外在任何使用到消费者的命令中,如果不存在则自动创建。如:XREADGROUP

XGROUP DELCONSUMER:在一个给定的队列和消费者组中移除一个消费者  在执行此命令之前 相关的PEL 要先执行 XCLAIM 或 XACK进行处理,否则将变得 unclaimable.

XGROUP DESTROY:在给定的队列中删除一个消费者组,相关的consumers 和 PEL 都会被 删除,所以执行之前要慎重

XGROUP SETID :重新设置指定的消费者组的最后一个处理的消息的ID,通常这个ID初始值 是XGROUP CREATE时指定的,每次执行XREDGROUP 也会修改(更新)这个ID,

例如:想让组内的消费者重新处理队列中的所有的entrys时,可以 XGROUP SETID streamkey groupkey 0,在redis 7.0增加了参数 ENTRIESREAD n ,n 为已读数量 ,

此时,xinfo groups streamKey 可以看到组信息如下:


127.0.0.1:6388> xread count 20 streams s1 0
1) 1) "s1"
   2) 1) 1) "1678852071712-0"
         2) 1) "key1"
            2) "vlaue1"
      2) 1) "1678852073882-0"
         2) 1) "key1"
            2) "vlaue1"
      3) 1) "1678852080406-0"
         2) 1) "key2"
            2) "vlaue2"
      4) 1) "1678852588261-0"
         2) 1) "key-1"
            2) "vlaue2"
      5) 1) "1678852591957-0"
         2) 1) "key-2"
            2) "vlaue2"
      6) 1) "1678852595467-0"
         2) 1) "key-3"
            2) "vlaue2"
      7) 1) "1678852599576-0"
         2) 1) "key-4"
            2) "vlaue2"
      8) 1) "1678852616566-0"
         2) 1) "key-4"
            2) "vlaue2"
      9) 1) "1678852946989-0"
         2) 1) "key-5"
            2) "vlaue2"
127.0.0.1:6388> xinfo groupss11)  1) "name"
    2) "g1"
    3) "consumers"
    4) (integer) 1
    5) "pending"
    6) (integer) 5
    7) "last-delivered-id"
    8) "1678852080406-0"
    9) "entries-read"
   10) (integer) 9
   11) "lag"
   12) (integer) 0
127.0.0.1:6388> xreadgroup group g1 lihui  count 1 streams s1 >
1) 1) "s1"
   2) 1) 1) "1678852588261-0"
         2) 1) "key-1"
            2) "vlaue2"
127.0.0.1:6388> xinfo groupss11)  1) "name"
    2) "g1"
    3) "consumers"
    4) (integer) 1
    5) "pending"
    6) (integer) 5
    7) "last-delivered-id"
    8) "1678852588261-0"
    9) "entries-read"
   10) (integer) 10
   11) "lag"
   12) (integer) -1
127.0.0.1:6388>xlen s1
(integer)
9 127.0.0.1:6388> xgroup setid s1 g1 1678852073882-0 ENTRIESREAD 2OK127.0.0.1:6388> xinfo groupss11) 1) "name" 2) "g1" 3) "consumers" 4) (integer) 1 5) "pending" 6) (integer) 5 7) "last-delivered-id" 8) "1678852073882-0" 9) "entries-read" 10) (integer) 2 11) "lag" 12) (integer) 7

View Code

XINFO [SRTREAM,GROUPS,CONSUMERS] :获取队列,组,消费者的信息

XLEN:返回 stream中entrys的数量

XPENDING :查询通过消费者组读取但未被确认的entrys

XRANGE :返回队列中的某个区间内的entrys

XREVRANGE: 顺序说XRANGE相反的读取队列中的entry

XREAD:从队列 内读取一个或多个entry ,支持block

XREADGROUP:在XREAD的基础上在队列与消费者之间增加了组的概念

XSETID:是一个redis 内部命令,用于记录master 与replicate之间的数据 同步的最后一个ID 的记录

好了,下面是是代码

Lua 脚本 完成 相关功能

--[[判断优惠券是否充足  优惠券  id ARGV[2] ,key KEYS[1]
判断当前用户是否已经下单 用户id ARGV[1]
如果库存充足,且用户没有下单 则 1、扣库存 2、保存用户下单信息 订单key KEYS[2] 订单ID ARGV[3]
stream 队列 key KEYS[3]
consumer group key KEYS[4]
利用redis 中set 数据 类型的不重复性,进行重复下单信息的记录
--
]] local sec_kc=redis.call('get',KEYS[1])--if(not sec_kc) then--redis.call('set',KEYS[1],2000)--sec_kc=2000--end if(not sec_kc or sec_kc=='{}') then --库存 不足 return 3 end if( tonumber(sec_kc)<=0) then --库存 不足 return 1 end --完成一人一单重复检测 if(redis.call('SISMEMBER',KEYS[2],ARGV[1])==1) then --订单重复 return 2 end --扣减库存 redis.call('incrby',KEYS[1],-1)--记录订单与用户之间关系 set 数据 redis.call('sadd',KEYS[2],ARGV[1])--获取stream队列中entry数量 --[=[local streamcount=redis.call('xlen',KEYS[3])
--此功能转到java 中完成 CreateStreamAndGroup
if(streamcount==0) then--没有stream 需要创建
-- XGROUP CREATE stream.order group1 0 mkstream
redis.call('XGROUP','CREATE',KEYS[3],KEYS[4],'0','mkstream')

end
--
]=] --判断队列是否存在 local streamExists=redis.call('exists',KEYS[3])if(streamExists==0) then return 4--队列不存在 end --创建消息entry--XADD S1 * KEY12 VALUE12 redis.call('XADD',KEYS[3],'*','userId',ARGV[1],'voucherId',ARGV[2],'id',ARGV[3])--key 的取值与对象 VoucherOrder 中变量属性对应,分别是,userId,voucherId,id(订单id)--满足下单条件 return 0

因为在redis 的stream 命令中 没有判断 consumer group 是否存在,所以需要变通的方法,解决这个问题,因为如果组不存的情况下,进行xadd 会报错。

创建队列和消费者组

 /*** 查询队列信息,如果不存在会出现异常,在异常中创建队列
*
@paramstreamKey
*
@paramgroupKey*/ private voidCreateStreamAndGroup(String streamKey,String groupKey)
{
try{//默认队列和组不存 创建队列 stringRedisTemplate.opsForStream().createGroup(streamKey, ReadOffset.from("0"), groupKey);
System.out.println(streamKey
+ "队列" + streamKey + "和组" + groupKey + "创建成功。");
}
catch( Exception ex)
{
String errx
=ex.getMessage().toString();if(errx.indexOf("BUSYGROUP Consumer Group name already exists")>0)//队列与组都已经存在 {//System.out.println(streamKey + "队列" + streamKey + "和组" + groupKey + "都已经存在"); }else{
log.debug(ex.getMessage());
}
}


}

判断队列是否存在

 /*** 判断consumer group 是否存在,如果队列不存在直接返回 false,
* 如果队列存在,不论组是否存,都直接创建。
*/ private booleanConsumerGroupExists(String streamKey,String groupKey)
{
boolean isok=false;try{//默认队列和组不存 创建队列 Collection<String> streamCollect=new ArrayList<>();
streamCollect.add(streamKey);
Long c_stream
=stringRedisTemplate.countExistingKeys(streamCollect);if(c_stream==0)
{
isok
= false;//队列不存在 returnisok;
}
//队列存在 ,那么直接创建消费者组, stringRedisTemplate.opsForStream().createGroup(streamKey, ReadOffset.from("0"), groupKey);
System.out.println(streamKey
+ "队列" + streamKey + "存在,组" + groupKey + "创建成功。");
isok
=true;//组创建成功 }catch( Exception ex)
{
String errx
=ex.getMessage().toString();if(errx.indexOf("BUSYGROUP Consumer Group name already exists")>0)//队列与组都已经存在 {
isok
=true;//组创建成功//System.out.println(streamKey + "队列" + streamKey + "和组" + groupKey + "都已经存在"); }else{
isok
=false;//未知异常 log.debug(ex.getMessage());
}
}
finally{returnisok;
}


}

订单创建入口函数(放在controller 或 seervice impl) 中

publicResult secKillVoucher(Long voucherId) {long userID =UserHolder.getUser().getId();

String vouchStockKey
= RedisConstants.SECKILL_VOUCHERSTOCK_CACHE_KEY +voucherId;
String userid_s
=String.valueOf(userID);
String vouchOrderKey
= RedisConstants.SECKILL_ORDER_CACHE_KEY +voucherId;

String streamKey
=RedisConstants.REDIS_STREAM_QUEUE_ORDER ;
String groupKey
=RedisConstants.REDIS_STREAM_GROUP_ORDER;

List
<String> keys = new ArrayList<>();
keys.add(vouchStockKey);
//库存key KEYS[1] keys.add(vouchOrderKey);//订单key KEYS[2] keys.add(streamKey);//队列key KEYS[3] keys.add(groupKey);//消费组key KEYS[4]//check sec_voucher_stock//1 查询优惠券 int count =seckillVoucherService.getRedisStock(voucherId);if (count <= 0) {return Result.fail("优惠券库存不存在 count:" +count);

}
long orderid =redisIdWorker.nextId(RedisConstants.ID_ORDER);
CreateStreamAndGroup(streamKey,groupKey);
//订单ID ARGV[3]//优惠券 id ARGV[2]//用户id ARGV[1]//执行lua 完成优惠券库存,一人一单,检查 并创建用户与优惠券ID之间的关联//脚本完成 创建消息队列 (执行此脚本之前确保队列 已经创建 Long sekResult =stringRedisTemplate.execute(SECKILL_SCRIPT,
keys
, userid_s,String.valueOf(voucherId),String.valueOf(orderid)
);
int ri =sekResult.intValue();
System.out.println(
"lua 脚本执行返回值 :"+ri);if(ri!=0)
{
String errtip="优惠券库存不足";switch(ri)
{
case 2:
errtip
="订单重复一人一单";break;case 3:
errtip
="库存数据不存在";break;case 4:
errtip
="消息队列不存在";break;case 1:
errtip
="优惠券库存不足";break;default:
errtip
="未知错误";

}
returnResult.fail(errtip);
}
    
/**注意:下面的代理对象要有
    
*/proxy=(IVoucherOrderService)AopContext.currentProxy();returnResult.ok(orderid);

}

创建线程获取消息队列并处理

 private static final ExecutorService SECKILL_ORDER_EXECUTOR=Executors.newSingleThreadExecutor();
@PostConstruct
private voidinit(){//本地阻塞队列的方式//SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());//读取redis消息队列并处理 SECKILL_ORDER_EXECUTOR.submit(newVoucherOrderHandlerRedisQueue());
}
//从redis 消息队列中获取消息 private class VoucherOrderHandlerRedisQueue implementsRunnable{

@Override
public voidrun() {while(true){//get orderinfo from blockingqueen try{
String streamKey
=RedisConstants.REDIS_STREAM_QUEUE_ORDER ;
String groupKey
=RedisConstants.REDIS_STREAM_GROUP_ORDER;
String cs
=RedisConstants.REDIS_STREAM_CONSUMER_ORDER;//判断组是否存在 if (!ConsumerGroupExists(streamKey,groupKey)) {//log.error("异步线程读取redis stream 失败:队列 未创建:"+streamKey); Thread.sleep(3000);continue;
}
//从消息队列中获取消息 List<MapRecord<String, Object, Object>> queueList =stringRedisTemplate.opsForStream()
.read(Consumer.from( groupKey,cs),
StreamReadOptions.empty().count(
1).block(Duration.ofSeconds(2))
, StreamOffset.create(streamKey,ReadOffset.lastConsumed())
);
if(queueList==null ||queueList.isEmpty() )
{
//未获取到消息 continue;//continue do next }//处理消息 MapRecord<String, Object, Object> record = queueList.get(0);
Map
<Object, Object> vq =record.getValue();
VoucherOrder voucherOrder
= BeanUtil.fillBeanWithMap(vq,new VoucherOrder(),false);//成功获取 创建订单到数据库 handlerVoucherOrder(voucherOrder);//确认消息 在PEL中移除 stringRedisTemplate.opsForStream().acknowledge(streamKey,groupKey,record.getId());

}
catch(Exception e) {

HandlePendingList();
log.error(e.getMessage().toString());
//throw new RuntimeException(e); }


}
}

异常处理 在redis stream pending List 中获取 队列

 private voidHandlePendingList() {
String streamKey
= RedisConstants.REDIS_STREAM_QUEUE_ORDER ;//stream String groupKey = RedisConstants.REDIS_STREAM_GROUP_ORDER;//consumer group String cs=RedisConstants.REDIS_STREAM_CONSUMER_ORDER;//consumer while (true)try{
{
//read from pel List<MapRecord<String, Object, Object>> queueList =stringRedisTemplate.opsForStream()
.read(Consumer.from ( groupKey,cs),
StreamReadOptions.empty().count(
1)
, StreamOffset.create(streamKey, ReadOffset.from(
"0"))
);
if(queueList==null ||queueList.isEmpty())
{
//未获取到消息 break;//continue do next normal }//处理消息 MapRecord<String, Object, Object> record = queueList.get(0);
Map
<Object, Object> vq =record.getValue();
VoucherOrder voucherOrder
= BeanUtil.fillBeanWithMap(vq,new VoucherOrder(),false);//成功获取 handlerVoucherOrder(voucherOrder);//确认消息 stringRedisTemplate.opsForStream().acknowledge(streamKey,groupKey,record.getId());

}
}
catch(Exception e) {
log.debug(
"消息队列--peding List 处理异常");try{
Thread.sleep(
50);
}
catch(InterruptedException ex) {throw newRuntimeException(ex);
}

}
}
}

订单处理

 privateIVoucherOrderService proxy;private voidhandlerVoucherOrder(VoucherOrder voucherOrder) {if(voucherOrder==null || voucherOrder.getUserId()==null)
{
log.debug(
"对象为空 或 属性用户ID 为空。");
}
//user ID long userID=voucherOrder.getUserId();

RLock lock
=redisson.getLock(RedisConstants.LOCK_VOUCHERORDER_KEY+StrUtil.toString(userID));boolean islock = lock.tryLock();//active watch dog if(!islock)
{
log.error(
"锁创建失败");
}
try{//IVoucherOrderService proxy=(IVoucherOrderService)AopContext.currentProxy();//获取spring 对当前对象的代理 proxy.createVoucherOrderByObj(voucherOrder);

}
catch(IllegalStateException e) {throw newRuntimeException(e);
}
finally{//lock.unLock();//my define simple redis lock lock.unlock();//redisson release lock }

}

实现类中的方法

 @Transactional//因为方法中 订单的操作 和 库存扣减,所以增加事务支持 为防止数据 不同步
    public voidcreateVoucherOrderByObj(VoucherOrder voucher) {//user ID
        if(voucher==null || voucher.getUserId()==null)
{
log.debug(
"对象为空 或 属性用户ID 为空。");return;
}
long userID=voucher.getUserId();int count=query().eq("user_id",userID).eq("voucher_id", voucher.getVoucherId()).count();if(count>0)
{
log.error(
"优惠券仅限每人一个 count:"+count);return;
}
//5扣减库存 boolean success=seckillVoucherService.update()
.setSql(
"stock=stock-1")
.eq(
"voucher_id", voucher.getVoucherId()).gt("stock",0)
.update();
if(!success)
{
log.error(
"扣减券库存失败Obj: voucher.getVoucherId():"+voucher.getVoucherId());return;
}
System.out.println(
"voucher saved");boolean ds =save(voucher);


}

The art of being wise is the art of knowing what to overlook .
智慧的艺术是知道该忽视什么。

——威廉·詹姆斯(William James)

1 导引

1.1 跨域推荐简介

推荐系统中常常面临用户
冷启动问题
[1]
,也即新注册的用户没有足够的交互记录,导致推荐模型不能学习到有效的表征。为了解决用户冷启动问题,近年来
跨域推荐(CDR)
得到了许多关注
[2]
。一般来讲,跨域推荐旨在利用从其它相关源域收集的用户-物品交互信息以提升目标域的推荐质量。许多跨域推荐的工作会假设大量的用户在两个域都出现过(即
重叠用户, overlapping users
)以搭建起源域和目标域之间的桥梁。只在源域中存在的用户(即
非重叠用户, non overlapping users
)可以被视为目标域的冷启动用户。

1.2 嵌入和映射的思路

为了解决冷启动用户问题,传统的跨域推荐方法常常基于
嵌入和映射(Embedding and Mapping,EMCDR)
的思路,也即学习一个映射函数将预训练的用户表征(embeddings)从源域迁移到目标域。如下图所示:

如上图所示,EMCDR首先用基于协同过滤的模型(CF-based model)来为每个领域生成用户/物品表征,之后训练一个映射函数来将源域和目标域的重叠用户表征。然后,再给定源域的非重叠冷启动用户表征,就能够根据训练好的映射函数来预测目标域的用户表征了,之后再用于目标域的物品推荐。

然而,正如我们上面所说的,这种方法在进行对齐操作之前,各领域需要先通过预训练以独立地得到用户/物品的embeddings。因此,
有偏的(biased)
预训练表征将无可避免地包含
领域特有的(domain-specific)
信息,从而会导致对跨领域迁移信息产生负面影响。

事实上,跨域推荐的关键问题就在于:
究竟需要在不同的域之间共享什么信息?也即如何让表征能够编码到领域间共享(domain-shared)的信息?

1.3 联合训练的思路

这种思路相比于EMCDR方法的优点在于,我们能够联合(jointly)学习跨领域的embeddings,从而能够进一步地关注于领域共享信息并限制领域特有的信息。

在具体的手段层面,这种方法该类方法的大多数工作首先采用两个基础的编码器来对每个领域的交互记录建模,之后再引入不同的迁移层来对称地融合不同编码器学得的表征。比如,CoNet
[3]
利用MLP做为每个领域的基础编码器,并设计了交叉连接(cross-connections)网络来迁移信息。DDTCDR
[4]
进一步扩展了ConNet:学习了一个潜在的正交投影函数来迁移跨领域用户的相似度。PPGN
[5]
使用堆叠的(stacking)GCN来直接聚合来自各领域的表征信息以学得用户/物品表征。BiTGCF
[6]
利用LightGCN
[7]
做为编码器来聚合每个领域的交互信息,并进一步引入特征迁移层来增强两个基础的图编码器。CDRIB
[8]
则采用信息瓶颈的视角来获得领域间共享的信息(不过该方法关注的是为目标域中的不重叠(冷启动)用户做推荐,与前面的方法又有所区别)。

1.4 解耦表征的思路

尽管以上的方法在一定程度上有效,但它们基本上仍然忽略了对领域共享信息和领域特有信息的解耦(CDRIB除外),而这大大限制了模型迁移的效率。

一个显著的例子如上图所示。对于Film和Book这两个领域,领域间共享的信息,比如“Story Topic”和“Category”能够为每个领域都提供有价值的信息。但领域特有的信息,比如Book领域的“Writing Style”可能会提供对于在“Film”领域做推荐无用的信息甚至会导致CDR领域的
负迁移
现象
[9]
。不幸的是,现有的CDR方法忽视了此问题并直接聚合领域间共享和领域特有的信息。这样的结果就是,学得的用户表征将不同领域的偏好纠缠(entangle)在一起,而这会导致获得次优(sub-optial)的推荐结果。

解决该问题的手段是解耦领域间共享的领域特有的表征,其代表为DisenCDR模型
[10]

如上图所示,DisenCDR模型将领域共享的和领域特有的表征进行解耦,以达到跨领域知识迁移的目的。

2 论文阅读

2.1 ICDE 2022《Cross-Domain Recommendation to Cold-Start Users via Variational Information Bottleneck》
[8]

本方法属于采用联合训练的跨域推荐方法。本其关注的场景为
当源域和目标域间的用户部分重叠时,为目标域中的不重叠(冷启动)用户做推荐
。该方法所要解决的问题在于,
究竟有哪些信息需要在领域间进行共享

为了解决该问题,
本文利用了信息瓶颈(information bottleneck)原理并提出了一个新的方法(CDIRB模型)来使表征编码到领域间共享的信息(domain shared information),从而用于各领域的下游推荐
。为了得到无偏的表征,作者设计了两种正则项,其中
信息瓶颈正则项
来同时建模跨域/域间的用户-物品交互,这样相比EMCDR方法,就能够同时考虑所有域的交互信息从而达到去偏的目的;而
对比信息正则项
则负责捕捉跨域的用户-用户之间的关系(对齐不同域之间的重叠用户表征)。

设有领域
\(X\)

\(Y\)
,设
\(D^X=(\mathcal{U}^X, \mathcal{V}^X,\mathcal{E}^X)\)

\(D^Y=(\mathcal{U}^X, \mathcal{V}^X,\mathcal{E}^X)\)
表示领域的数据,这里
\(\mathcal{U}\)

\(\mathcal{V}\)

\(\mathcal{E}\)
分别表示每个领域用户、物品和边的集合。特别地,用户集合
\(\mathcal{U}^X\)

\(\mathcal{U}^Y\)
包含重叠的用户子集
\(\mathcal{U}^o = \mathcal{U}^X \cap \mathcal{U}^Y\)
。接着,用户集合可以被形式化为
\(\mathcal{U}^X = \{\mathcal{U}^x, \mathcal{U}^o\}\)

\(\mathcal{U}^Y = \{\mathcal{U}^y, \mathcal{U}^o\}\)
,这里
\(\mathcal{U}^x\)

\(\mathcal{U}^y\)
为在每个领域中不重叠的用户集合。设
\(\boldsymbol{A}^X=\{0,1\}^{\left|\mathcal{U}^X\right| \times\left|\mathcal{V}^X\right|}\)

\(A^Y=\{0,1\}^{\left|\mathcal{U}^Y\right| \times\left|\mathcal{V}^Y\right|}\)
为存储用户-物品交互信息的两个二值矩阵。这样,本文的任务可形式化地描述为:给定来自源域
\(X\)
的非重叠的(冷启动)用户
\(u_i\in \mathcal{U}^x\)
,我们想要为其推荐来自目标域
\(Y\)
的物品
\(v_j \in \mathcal{V}^Y\)
(或为来自
\(\mathcal{U}^y\)
的用户推荐来自
\(\mathcal{V}^X\)
的物品)。

接下来作者借鉴了论文
[11][12]
提出的信息瓶颈理论,该理论旨在学习有效表征,这种有效表征能够在简洁性和广泛的预测能力之间做权衡(trade-off)
[13]
。形式化地,标准信息瓶颈有如下所示的目标函数:

\[\mathcal{L}_{I B}:=\beta I(\boldsymbol{Z} ; \mathbf{X})-I(\boldsymbol{Z} ; \mathbf{Y})
\]

该目标函数可以被解释为两部分:(1)最小化
\(I(Z; X)\)
旨在惩罚
\(Z\)

\(X\)
之间的互信息,也即使得
\(Z\)
尽量“忘掉”
\(X\)
的信息。(2) 最大化
\(I(Z; Y)\)
则鼓励
\(Z\)
去预测
\(Y\)
。综合来看,信息瓶颈原理的目标为压缩
\(X\)
以得到表征
\(Z\)
,该表征能够去除掉对预测
\(Y\)
无用的因素而保留相关因素
[14]
。这也就是说IB使得
\(Z\)
做为一个最小充分统计量
[15]
(在我们这个CDR应用中即领域间应该共享的信息)。在实践中,直接优化互信息是难解(intractable)的,因此变分近似
[16]
常常用于构建用于优化互信息目标函数的下界
[13][17]

本文提出的CDIRB模型包含变分子图编码器(variational bipartite graph encoder,VBGE)和两种的跨领域信息正则项,整体框架图如下图所示:

其中绿色部分的网格代表物品表征,黄色和蓝色颜色的网格分别代表重叠和不重叠的用户表征。信息瓶颈正则项(图中的Information Bottleneck)捕捉了领域间用户和物品的相关性,而对比信息正则项(图中的Contrastive Information)则捕捉了领域间重叠用户之间的相关性。

接下来我们叙述每个部分的细节。

嵌入层

嵌入层得到的领域
\(X\)
的用户/物品表征分别记作
\(\boldsymbol{U}^X \in \mathbb{R}^{|\mathcal{U}^X |\times F}\)

\(\boldsymbol{V}^X \in \mathbb{R}^{\left|\mathcal{V}^X\right| \times F}\)
;领域
\(Y\)
的用户/物品表征分别记作
\(\boldsymbol{U}^Y \in \mathbb{R}^{\left|\mathcal{U}^{Y}\right| \times F}\)

\(\boldsymbol{V}^Y \in \mathbb{R}^{\left|\mathcal{V}^Y\right| \times F}\)

变分二分图编码器
为了在原始用户/物品表征的基础上,进一步提炼出用户/物品的隐向量表征,论文提出了变分二分图编码器(VBGE)。比如,生成
\(X\)
领域的用户隐向量表征
\(Z_v^X\)
的过程如下:

\[\begin{gathered}
\widehat{\boldsymbol{U}}^X=\delta\left(\operatorname{Norm}\left(\left(\boldsymbol{A}^X\right)^{\top}\right) \boldsymbol{U}^X \boldsymbol{W}_u^X\right),\\
\boldsymbol{\mu}_u^X=\delta\left(\left[\delta\left(\operatorname{Norm}\left(\boldsymbol{A}^X\right) \widehat{\boldsymbol{U}}^X \widehat{\boldsymbol{W}}_{u, \mu}^X\right) \oplus \boldsymbol{U}^X\right] \boldsymbol{W}_{u, \mu}^X\right), \\
\boldsymbol{\sigma}_u^X=\varphi\left(\left[\delta\left(\operatorname{Norm}\left(\boldsymbol{A}^X\right) \widehat{\boldsymbol{U}}^X \widehat{\boldsymbol{W}}_{u, \sigma}^X\right) \oplus \boldsymbol{U}^X\right] \boldsymbol{W}_{u, \sigma}^X\right), \\
\boldsymbol{Z}_u^X \sim \mathcal{N}\left(\boldsymbol{\mu}_u^X,\left[\operatorname{diag}\left(\boldsymbol{\sigma}_u^X\right)\right]^2\right),
\end{gathered}
\]

\[\boldsymbol{z}_{u_i}^X=\boldsymbol{\mu}_{u_i}^X+\boldsymbol{\sigma}_{u_i}^X \odot \boldsymbol{\epsilon}, \quad \boldsymbol{\epsilon} \sim \mathcal{N}(0, \operatorname{diag}(\boldsymbol{I}))
\]

信息瓶颈正则项

接下来,论文引入了信息瓶颈正则项和对比信息正则项这两种正则项来捕捉领域间的相关性,以学得包含领域间共享信息的无偏表征。其中信息瓶颈正则化项旨在捕捉领域间用户和物品间的相关性,而对比信息正则化项旨在捕捉领域间的重叠用户和用户之间的相关性。


\(\mathbf{X}\)

\(\mathbf{X}^u\)

\(\mathbf{X}^v\)
分别为领域
\(X\)
中所观测到的交互信息、用户信息和物品信息。领域
\(X\)
的用户集合包括重叠用户
\(\mathcal{U}^o\)
和非重叠用户
\(\mathcal{U}^x\)
这两个群体,领域
\(Y\)
亦然。以领域
\(X\)
为例,将用户表征
\(\boldsymbol{Z}_u^X \in \mathbb{R}^{\left|\mathcal{U}^{X}\right| \times F}\)
也划分为两个群体:
\(\boldsymbol{Z}_u^{x o} \in \mathbb{R}^{\left|\mathcal{U}^o\right| \times F}\)

\(\boldsymbol{Z}_u^x \in \mathbb{R}^{\left|\mathcal{U}^x\right| \times F}\)

信息瓶颈正则项又可继续分为跨域(cross-domain)信息瓶颈正则项和领域内(in-domain)信息瓶颈正则项。首先我们来看跨域(cross-domain)信息瓶颈正则项,它包括包括信息压缩(即互信息最小化)和重构两部分,其结构化示意图如下:

正如上图(a)所示。
\(\boldsymbol{Z}_u^{x o}\)

\(\boldsymbol{Z}_u^{y o}\)
是编码了各领域用户信息的重叠用户表征,而图(b)中的
\(\boldsymbol{Z}_u^x\)

\(Z_u^y\)
是非重叠的(冷启动)用户表征。这里
\(\boldsymbol{Z}_v^{X}\)

\(\boldsymbol{Z}_v^Y\)
是物品表征,默认是不重叠的。


\(X\)
领域迁移到
\(Y\)
领域为例(图中标红部分),我们需要使重叠用户隐向量
\(\boldsymbol{Z}^{xo}_u\)
和同领域的用户表征
\(\mathbf{X}^u\)
互斥(信息压缩),而去接近于
\(Y\)
领域的交互信息
\(\mathbf{Y}\)
(跨域重构);此外,对于
\(Y\)
领域的物品隐向量
\(\boldsymbol{Z}^Y_v\)
也需要使其与物品表征
\(\mathbf{Y}^v\)
互斥,并去接近于
\(\mathbf{Y}\)
(因为不同领域物品不会重叠,这里采取域内重构)。

\[\begin{aligned}
\mathcal{L}_{o 2 Y}= & \beta_1 I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{X}^u\right)-I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{Y}\right) \\
& +\beta_2 I\left(\boldsymbol{Z}_v^Y ; \mathbf{Y}^v\right)-I\left(\boldsymbol{Z}_v^Y ; \mathbf{Y}\right)
\end{aligned}
\]

其中的跨域重构部分可以进一步通过互信息链式法则化简得到:

\[\begin{aligned}
I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{Y}\right)+I\left(\boldsymbol{Z}_v^Y ; \mathbf{Y}\right) & =I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{Y} \mid \boldsymbol{Z}_v^Y\right)+I\left(\boldsymbol{Z}_v^Y ; \mathbf{Y}\right) \\
& =I\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y ; \mathbf{Y}\right)
\end{aligned}
\]

(这里假设
\(\boldsymbol{Z}_u^{x o}\)

\(\boldsymbol{Z}_v^Y\)
独立)

最后,
\(X\)
领域导出的损失函数包括最小化(minimality)和跨域重构(reconstruction)两部分:

\[\mathcal{L}_{o 2 Y}=\underbrace{\beta_1 I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{X}^u\right)+\beta_2 I\left(\boldsymbol{Z}_v^Y ; \mathbf{Y}^v\right)}_{\text {Minimality }}-\underbrace{I\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y ; \mathbf{Y}\right)}_{\text {Reconstruction }}
\]

接下来我们来看领域内(in-domain)信息瓶颈正则项,其结构化示意图如下:

我们还是以
\(X\)
领域为例子(图中红色箭头部分),可以看到其损失函数同样也包括最小化和领域内重构两部分:

\[\mathcal{L}_{x 2 X}=\underbrace{\beta_1 I\left(\boldsymbol{Z}_u^x ; \mathbf{X}^u\right)+\beta_1 I\left(\boldsymbol{Z}_v^X ; \mathbf{X}^v\right)}_{\text {Minimality }}-\underbrace{I\left(\boldsymbol{Z}_u^x, \boldsymbol{Z}_v^X ; \mathbf{X}\right)}_{\text {Reconstruction }}
\]

对比信息正则项

在对比信息正则化项中,作者通过最大化
\(X\)
的重叠用户表征
\(\boldsymbol{Z}^{xo}_u\)
和来自领域
\(Y\)
的重叠用户表征
\(\boldsymbol{Z}^{yo}_u\)
间的互信息,以进一步提炼重叠用户的表征。对比信息正则化项的定义如下所示:

\[\begin{aligned} & \mathcal{L}_{\text {con }}=-\underbrace{I\left(\boldsymbol{Z}_u^{x o} ; \boldsymbol{Z}_u^{y o}\right)}_{\text {Contrastive }} \\
&=-I\left(\boldsymbol{Z}_u^{x o} ; \boldsymbol{Z}_u^{y o}\right)+\left[H\left(\boldsymbol{Z}_u^{x o} \mid \mathbf{X}\right)-H\left(\boldsymbol{Z}_u^{x o} \mid \boldsymbol{Z}_u^{y o}, \mathbf{X}\right)\right] \\
& = -I\left(\boldsymbol{Z}_u^{x o} ; \boldsymbol{Z}_u^{y o}\right)+I\left(\boldsymbol{Z}_u^{x o} ; \boldsymbol{Z}_u^{y o} \mid \mathbf{X}\right) \\
& = -I\left(\boldsymbol{Z}_u^{x o} ; \boldsymbol{Z}_u^{y o}; \textbf{X}\right) \\
& = -I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{X}\right)-I\left(\boldsymbol{Z}_u^{y o} ; \mathbf{X}\right)+I\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_u^{y o} ; \mathbf{X}\right)\end{aligned}
\]

可求解的目标函数

将上述的两种信息瓶颈正则项和对比信息正则项累加起来(同时包括
\(X\)

\(Y\)
领域的),就得到了目标函数:

\[\begin{aligned} \mathcal{L}= & \mathcal{L}_{x 2 X}+\mathcal{L}_{o 2 Y}+\mathcal{L}_{o 2 X}+\mathcal{L}_{y 2 Y}+\mathcal{L}_{c o n} \\ = & \beta_1\left(I\left(\boldsymbol{Z}_u^X ; \mathbf{X}^u\right)+I\left(\boldsymbol{Z}_v^X ; \mathbf{X}^v\right)\right) \\ & +\beta_2\left(I\left(\boldsymbol{Z}_u^Y ; \mathbf{Y}^u\right)+I\left(\boldsymbol{Z}_v^Y ; \mathbf{Y}^v\right)\right) \\ & -I\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y ; \mathbf{Y}\right)-I\left(\boldsymbol{Z}_u^x, \boldsymbol{Z}_v^X ; \mathbf{X}\right) \\ & -I\left(\boldsymbol{Z}_u^{y o}, \boldsymbol{Z}_v^X ; \mathbf{X}\right)-I\left(\boldsymbol{Z}_u^y, \boldsymbol{Z}_v^Y ; \mathbf{Y}\right) \\ & -I\left(\boldsymbol{Z}_u^{x o}\boldsymbol{Z}_u^{y o}\right)\end{aligned}
\]

要想求解该目标函数,接下来还需要将互信息其转换为KL散度,比如对于
\(I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{X}^u\right)\)
就有

\[I\left(\boldsymbol{Z}_u^{x o} ; \mathbf{X}^u\right)=\mathbb{D}_{K L}\left(p_\theta\left(\boldsymbol{Z}_u^{x o} \mid \mathbf{X}^u\right) \| p\left(\boldsymbol{Z}_u^{x o}\right)\right)
\]

该互信息项是难以求解的,这里需要转而去优化其上界:

\[\begin{aligned} I\left(\boldsymbol{Z}_u^{x o} ; X^u\right) & \leq \mathbb{D}_{K L}\left(q_{\phi_u^X}\left(\boldsymbol{Z}_u^{x o} \mid X^u\right) \| p\left(\boldsymbol{Z}_u^{x o}\right)\right) \\ \quad= & \mathbb{D}_{K L}\left(\mathcal{N}\left(\boldsymbol{\mu}_u^{x o},\left[\operatorname{diag}\left(\boldsymbol{\sigma}_u^{x o}\right)\right]^2\right) \| \mathcal{N}(0, \operatorname{diag}(\boldsymbol{I}))\right)\end{aligned}
\]

对于重构项,我们以
\(I\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y ; \mathbf{Y}\right)\)
为例,我们有

\[I\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y ; \mathbf{Y}\right)=\mathbb{E}_{p_\theta\left(\boldsymbol{Z}_u^{x o} \mid \mathbf{X}^u\right) p_\theta\left(\boldsymbol{Z}_v^Y \mid \mathbf{Y}^v\right)}\left[\log p\left(\boldsymbol{A}^Y \mid \boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y\right)\right]
\]

该优化函数同样是难解的,这里需要转而去优化其下界:

\[\begin{array}{r}I\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y ; \mathbf{Y}\right) \geq \mathbb{E}_{q_{\phi_u^X}\left(\boldsymbol{Z}_u^{x o} \mid \mathbf{X}^u\right) q_{\phi_v^Y}\left(\boldsymbol{Z}_v^Y \mid \mathbf{Y}^v\right)}\left[\log p\left(\boldsymbol{A}^Y \mid \boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_v^Y\right)\right] \\ =\sum_{\left(u_i, v_j\right) \in \mathcal{E}^Y} \log \left(s\left(\boldsymbol{z}_{u_i}^{x o}, \boldsymbol{z}_{v_j}^y\right)\right)+\sum_{\left(u_i, \widetilde{v}_j\right) \notin \mathcal{E}^Y} \log \left(1-s\left(\boldsymbol{z}_{u_i}^{x o}, \boldsymbol{z}_{\widetilde{v}_j}^y\right)\right)\end{array}
\]

对于对比互信息项,论文借鉴了infomax
[14][20]
小想法,利用神经网络来度量对比互信息。具体来说,论文定义了判别器
\(\mathcal{D}\)
来度量来自不同领域的重叠用户隐向量(来自领域
\(X\)

\(z^{xo}_{u_i}\)
和来自领域
\(Y\)

\(z^{yo}_{u_i}\)
)之间的相似度。因此,对比项的下界可表示如下:

\[\begin{aligned} & I\left(\boldsymbol{Z}_u^{x o} ; \boldsymbol{Z}_u^{y o}\right)=\mathbb{E}_{p_\theta\left(\boldsymbol{Z}_u^{x o} \mid \mathbf{X}^u\right) p_\theta\left(\boldsymbol{Z}_u^{y o} \mid \mathbf{Y}^u\right)}\left[\log \mathcal{D}\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_u^{y o}\right)\right] \\ & \geq \mathbb{E}_{q_{\phi_u^X}\left(\boldsymbol{Z}_u^{x o} \mid \mathbf{X}^u\right) q_{\phi_u^Y}\left(\boldsymbol{Z}_u^{y o} \mid \mathbf{Y}^u\right)}\left[\log \mathcal{D}\left(\boldsymbol{Z}_u^{x o}, \boldsymbol{Z}_u^{y o}\right)\right] \\ & =\sum_{\tilde{u}_i \in \mathcal{U}^o, \tilde{u}_i \neq u_i}\left[\log \left(\mathcal{D}\left(\boldsymbol{z}_{u_i}^{x o}, \boldsymbol{z}_{u_i}^{y o}\right)\right)+\log \left(1-\mathcal{D}\left(\boldsymbol{z}_{u_i}^{x o}, \boldsymbol{z}_{\tilde{u}_i}^{y o}\right)\right)\right] \\ & \end{aligned}
\]

这里

\[\mathcal{D}\left(\boldsymbol{z}_{u_i}^{x o}, \boldsymbol{z}_{u_i}^{y o}\right)=\operatorname{sigmoid}\left(\operatorname{MLP}\left(\boldsymbol{z}_{u_i}^{x o} \oplus \boldsymbol{z}_{u_i}^{y o}\right)\right)
\]

这样,我们就将原始目标函数转化为了最终完全可求解的目标函数。

2.2 SIGIR 2022 《DisenCDR: Learning Disentangled Representations for Cross-Domain Recommendation》
[4]

本方法属于采用解耦表征的跨域推荐方法。与2.1所讲的基于信息瓶颈视角的方法不同的是,本方法旨在
为两个领域中的重叠用户做推荐,因此在模型中只考虑两个领域中的重叠用户
。在本方法中,所要解决的关键问题在于
对于两个领域重叠用户的表征,如何分别出共享和不共享的部分?

为了解决该问题,
本文基于信息论提出了DisenCDR模型,该模型能够解耦领域间共享和领域特有的信息,从而只迁移领域间共享的信息以增强推荐表现
。该方法包含了两个互信息正则项(包括
用于解耦的正则项

用于信息增强的正则项
,详情参见后文),并据此导出了一个可以求解的解耦目标函数。

本文采用和上面的文章几乎一样的符号,就是需要注意此处领域
\(X\)
和领域
\(Y\)
的用户空间相同。设领域
\(X\)
和领域
\(Y\)
的数据分别表示为
\(\mathcal{D}^X=(\mathcal{U}, \mathcal{V}^X,\mathcal{E}^X)\)

\(\mathcal{D}^Y=(\mathcal{U}, \mathcal{V}^X,\mathcal{E}^X)\)
,这里
\(\mathcal{U}\)

\(\mathcal{V}\)

\(\mathcal{E}\)
分别表示每个领域用户、物品和边的集合。设
\(\boldsymbol{A}^X=\{0,1\}^{\left|\mathcal{U}\right| \times\left|\mathcal{V}^X\right|}\)

\(A^Y=\{0,1\}^{\left|\mathcal{U}\right| \times\left|\mathcal{V}^Y\right|}\)
为存储用户-物品交互信息的两个二值矩阵。

这里
\(Z^X_v\)

\(Z^X_u\)

\(Z^Y_u\)

\(Z^Y_v\)
是领域特有的用户/物品表征,且
\(Z^S_u\)
是用户的领域共享表征,则DisenCDR的框架图可表示如下:

注意,这里蓝色的KL意为使用先验分布
\(\mathcal{N}(0, \mathbf{I})\)
计算KL散度,绿色的KL意为计算输入之间的KL散度。隐变量
\(\widehat{Z}_u^S\)

\(\widetilde{Z}_u^S\)
用于计算我们的解耦目标函数。

下面我们来详细介绍该方法各个组成部分的细节:

嵌入层

嵌入层的作用同2.1中所述的方法相同,也即将用户和物品嵌入到低维空间中。不过还是正如我们前面所说的,这里
\(X\)
领域和
\(Y\)
领域的用户空间相同。设
\(\boldsymbol{U}^S\in \mathbb{R}^{|\mathcal{U}|\times F}\)
为领域
\(X\)
和领域
\(Y\)
的共享初始嵌入矩阵,
\(\boldsymbol{U}^X \in \mathbb{R}^{|\mathcal{U}|\times F}\)

\(\boldsymbol{V}^X \in \mathbb{R}^{\left|\mathcal{U}\right| \times F}\)
分别为领域
\(X\)

\(Y\)
特有的初始化嵌入矩阵。此外,
\(\boldsymbol{V}^X \in \mathbb{R}^{\left|\mathcal{V}^X\right| \times F}\)

\(\boldsymbol{V}^Y \in \mathbb{R}^{\left|\mathcal{V}^Y\right| \times F}\)
分别为领域
\(X\)
和领域
\(Y\)
的物品表征。

变分二分图编码器

DisenCDR和变分二分图编码器和我们 2.1 中讲的第一个基于信息瓶颈思想的模型一样,唯一的区别就是这里的共享隐向量同时利用了
\(X\)
领域的
\(\boldsymbol{\overline{\mu}}_{u}^X\)

\(Y\)
领域的
\(\overline{\boldsymbol{\mu}}_u^Y\)

\[\begin{gathered}
\boldsymbol{\mu}_u^S=\lambda_u \odot \overline{\boldsymbol{\mu}}_u^X+\left(1-\lambda_u\right) \odot \overline{\boldsymbol{\mu}}_u^Y, \\
\boldsymbol{\sigma}_u^S=\lambda_u \odot \bar{\sigma}_u^X+\left(1-\lambda_u\right) \odot \overline{\boldsymbol{\sigma}}_u^Y, \\
\lambda_{u_i}=\frac{N_{u_i}^X}{N_{u_i}^X+N_{u_i}^Y}, \quad Z_u^S \sim \mathcal{N}\left(\boldsymbol{\mu}_u^S,\left[\operatorname{diag}\left\{\sigma_u^S\right\}\right]^2\right)
\end{gathered}
\]

生成和推断

论文遵循VAE
[18]
的框架,这里假定所观测的交互信息
\(\mathcal{D}^X\)

\(\mathcal{ D}^Y\)
采自一个联合概率分布
\(p_{\mathcal{D}}(u, v^X, v^Y)\)
,每个元组
\(\left(u_i, v_j, v_k\right) \sim p_{\mathcal{D}}\left(u, v^X, v^Y\right)\)
描述了用户
\(u_i\)
和物品
\(v_j \in \mathcal{V}^X\)
和物品
\(v_k \in \mathcal{V}^Y\)
的交互信息。而交互数据正是经由领域共享表征(比如
\(Z_u^S\)
)和领域特有(比如
\(Z^X_u\)

\(Z^X_v\)

\(Z^Y_u\)

\(Z^Y_v\)
)表征生成,也即:

\[\begin{array}{r}
p_\theta\left(u, v^X, v^Y\right)=\int p_{\theta^X}\left(A^X \mid Z_u^S, Z_u^X, Z_v^X\right) p_{\theta^Y}\left(A^Y \mid Z_u^S, Z_u^Y, Z_v^Y\right) \\
p\left(Z_u^S\right) p\left(Z_u^X\right) p\left(Z_u^Y\right) p\left(Z_v^X\right) p\left(Z_v^Y\right) \mathrm{d} Z_u^S \mathrm{~d} Z_u^X \mathrm{~d} Z_u^Y \mathrm{~d} Z_v^X \mathrm{~d} Z_v^Y
\end{array}
\]

下图(a)正是描述了交互数据的生成过程,而图(b)则描述了反向推断步骤:

在推断过程中,直接最大化联合概率分布
\(p_\theta\left(u, v^X, v^Y\right)\)
的似然是难解的,因为后验分布
\(p_\theta\left(Z_u^X, Z_u^Y, Z_u^S, Z_v^X, Z_v^Y \mid \mathbf{X}, \mathrm{Y}\right)\)
未知。因此采用近似推断
[19]
来近似真实的后验分布。根据上图(b)中的结构化假设,论文将近似后验分布分解为:

\[\begin{array}{r}
q_\phi\left(Z_u^X, Z_u^Y, Z_u^S, Z_v^X, Z_v^Y \mid \mathbf{X}, \mathbf{Y}\right)=q_{\phi_u^X}\left(Z_u^X \mid \mathbf{X}\right) q_{\phi_u^Y}\left(Z_u^Y \mid \mathbf{Y}\right) \\
q_{\phi_v^X}\left(Z_v^X \mid \mathbf{X}\right) q_{\phi_v^Y}\left(Z_v^Y \mid \mathbf{Y}\right) q_{\phi_u^S}\left(Z_u^S \mid \mathbf{X}, \mathbf{Y}\right)
\end{array}
\]

解耦目标函数

接下来作者从信息论的角度来探究领域间表征纠缠的问题,并推导了一个解耦目标函数。

为了使领域间共享和领域特有的隐向量能够编码互斥的信息,作者引入了互斥正则项来最小化二者的互信息。为了分析最小化互信息的影响,作者又将互信息进行了进一步改写。我们以领域
\(X\)
为例,其对应的领域共享和领域特有隐向量的互信息
\(I(Z^X_u; Z^S_u)\)
可做如下改写:

\[\begin{aligned}
I\left(Z_u^X ; Z_u^S\right) & =I\left(Z_u^X ; Z_u^S\right)-\left(H\left(Z_u^X \mid \mathbf{X}\right)-H\left(Z_u^X \mid Z_u^S, \mathbf{X}\right)\right) \\
& =I\left(Z_u^X ; Z_u^S\right)-I\left(Z_u^X ; Z_u^S \mid \mathbf{X}\right) \\
& =I\left(Z_u^X ; Z_u^S ; \mathbf{X}\right) \\
& =I\left(\mathbf{X} ; Z_u^X\right)+I\left(\mathbf{X} ; Z_u^S\right)-I\left(\mathbf{X} ; Z_u^X, Z_u^S\right)
\end{aligned}
\]

接下来我们看另外一个用于信息增强的正则项(对应我们在2.2信息瓶颈方法中所介绍的重构正则项)。该正则项旨在使每个领域共享的表征
\(Z^S_u\)
信息更丰富,这里作者最大化互信息
\(I\left(Z_u^S ; \mathrm{X} ; \mathrm{Y}\right)\)
来使得
\(Z^S_u\)
编码领域共享的信息。我们以领域
\(X\)
为例,有:

\[\begin{aligned}
I\left(Z_u^S ; \mathbf{X} ; \mathbf{Y}\right) & =I\left(Z_u^S ; \mathbf{X}\right)-I\left(Z_u^S ; \mathbf{X} \mid \mathbf{Y}\right) \\
& =I\left(Z_u^S ; \mathbf{X}\right)-\left(I\left(Z_u^S ; \mathbf{X}, \mathbf{Y}\right)-I\left(Z_u^S ; \mathbf{Y}\right)\right)
\end{aligned}
\]

总目标函数
将上面所说的两个解耦目标函数(包括
\(X\)
领域和
\(Y\)
领域的)加起来,就得到了总的目标函数:

\[\begin{aligned}
\mathcal{L}= & I\left(Z_u^X ; Z_u^S\right)+I\left(Z_u^Y ; Z_u^S\right)-2 I\left(Z_u^S ; \mathbf{X} ; \mathrm{Y}\right) \\
= & I\left(\mathbf{X} ; Z_u^X\right)+I\left(Z_u^S ; \mathbf{X} \mid \mathrm{Y}\right)-I\left(\mathbf{X} ; Z_u^X, Z_u^S\right) \\
& +I\left(\mathbf{Y} ; Z_u^Y\right)+I\left(Z_u^S ; \mathbf{Y} \mid \mathbf{X}\right)-I\left(\mathbf{Y} ; Z_u^Y, Z_u^S\right)
\end{aligned}
\]

进一步将物品隐向量
\(Z_v^X\)

\(Z_v^Y\)
引入,可以将损失函数放缩为:

\[\begin{aligned}\mathcal{L}
& \leq I\left(\mathbf{X} ; Z_u^X\right)+I\left(\mathbf{X} ; Z_v^X\right)+I\left(\mathrm{Y} ; Z_u^Y\right)+I\left(\mathrm{Y} ; Z_v^Y\right) \\
& \quad+I\left(\mathbf{X}, \mathrm{Y} ; Z_u^S\right)+I\left(Z_u^S ; \mathbf{X} \mid \mathrm{Y}\right)+I\left(Z_u^S ; \mathrm{Y} \mid \mathbf{X}\right) \\
& \quad-I\left(\mathbf{X} ; Z_u^X, Z_u^S, Z_v^X\right)-I\left(\mathrm{Y} ; Z_u^Y, Z_u^S, Z_v^Y\right) \\
& \leq \mathrm{ELBO}+I\left(Z_u^S ; \mathrm{X} \mid \mathrm{Y}\right)+I\left(Z_u^S ; \mathrm{Y} \mid \mathrm{X}\right)
\end{aligned}
\]

这样,解耦目标函数中的一部分可以视为变分推断中标准的证据下界(Evidence Lower Bound, ELBO)。最后,论文按照VAE的思路,继续将其化为了可以求解的目标函数:

\[\begin{aligned}
\mathcal{L} \leq & \mathbb{D}_{K L}\left(q\left(Z_u^X \mid \mathbf{X}\right) \| p\left(Z_u^X\right)\right)+\mathbb{D}_{K L}\left(q\left(Z_v^X \mid \mathrm{X}\right)|| p\left(Z_v^X\right)\right) \\
& +\mathbb{D}_{K L}\left(q\left(Z_u^Y \mid \mathrm{Y}\right) \| p\left(Z_u^Y\right)\right)+\mathbb{D}_{K L}\left(q\left(Z_u^S \mid \mathrm{X}, \mathrm{Y}\right) \| p\left(Z_u^S\right)\right) \\
& +\mathbb{D}_{K L}\left(q\left(Z_v^Y \mid \mathrm{Y}\right) \| p\left(Z_v^Y\right)\right) \\
& -\mathbb{E}_{q\left(Z_u^X, Z_v^X \mid \mathrm{X}\right) q\left(Z_u^S \mid \mathrm{X}, \mathrm{Y}\right)}\left[\log p\left(A^X \mid Z_u^S, Z_u^X, Z_v^X\right)\right] \\
& -\mathbb{E}_{q\left(Z_u^Y, Z_v^Y \mid \mathrm{Y}\right) q\left(Z_u^S \mid \mathrm{X}, \mathrm{Y}\right)}\left[\log p\left(A^Y \mid Z_u^S, Z_u^Y, Z_v^Y\right)\right] \\
& +\beta \mathbb{D}_{K L}\left(q\left(Z_u^S \mid \mathrm{X}, \mathrm{Y}\right) \mid q\left(\widetilde{Z}_u^S \mid \mathrm{Y}\right)\right)+\beta \mathbb{D}_{K L}\left(q\left(Z_u^S \mid \mathrm{X}, \mathrm{Y}\right) \| q\left(\widehat{Z}_u^S \mid \mathrm{X}\right)\right)
\end{aligned}
\]

参考

  • [1] Lin X, Wu J, Zhou C, et al. Task-adaptive neural process for user cold-start recommendation[C]//Proceedings of the Web Conference 2021. 2021: 1306-1316.

  • [2] Zhu F, Wang Y, Chen C, et al. Cross-domain recommendation: challenges, progress, and prospects[J]. arXiv preprint arXiv:2103.01696, 2021.

  • [3] Hu G, Zhang Y, Yang Q. Conet: Collaborative cross networks for cross-domain recommendation[C]//Proceedings of the 27th ACM international conference on information and knowledge management. 2018: 667-676

  • [4] Li P, Tuzhilin A. Ddtcdr: Deep dual transfer cross domain recommendation[C]//Proceedings of the 13th International Conference on Web Search and Data Mining. 2020: 331-339.

  • [5] Kipf T N, Welling M. Semi-supervised classification with graph convolutional networks[J]. arXiv preprint arXiv:1609.02907, 2016.

  • [6] Meng Liu, Jianjun Li, Guohui Li, and Peng Pan. 2020. Cross Domain Recom- mendation via Bi-directional Transfer Graph Collaborative Filtering Networks. In ACM International Conference on Information and Knowledge Management (CIKM).

  • [7] Xiangnan He, Kuan Deng, Xiang Wang, Yan Li, Yongdong Zhang, and Meng Wang. 2020. Lightgcn: Simplifying and Powering Graph Convolution Network for Recommendation. In ACM International Conference on Research on Development in Information Retrieval (SIGIR).

  • [8] Cao J, Sheng J, Cong X, et al. Cross-domain recommendation to cold-start users via variational information bottleneck[C]//2022 IEEE 38th International Conference on Data Engineering (ICDE). IEEE, 2022: 2209-2223.

  • [9] Zang T, Zhu Y, Liu H, et al. A survey on cross-domain recommendation: taxonomies, methods, and future directions[J]. ACM Transactions on Information Systems, 2022, 41(2): 1-39.

  • [10] Cao J, Lin X, Cong X, et al. DisenCDR: Learning Disentangled Representations for Cross-Domain Recommendation[C]//Proceedings of the 45th International ACM SIGIR Conference on Research and Development in Information Retrieval. 2022: 267-277.

  • [11] Tishby N, Pereira F C, Bialek W. The information bottleneck method[J]. arXiv preprint physics/0004057, 2000.

  • [12] Tishby N, Zaslavsky N. Deep learning and the information bottleneck principle[C]//2015 ieee information theory workshop (itw). IEEE, 2015: 1-5.

  • [13] Alemi A A, Fischer I, Dillon J V, et al. Deep variational information bottleneck[J]. arXiv preprint arXiv:1612.00410, 2016.

  • [14] M. I. Belghazi, A. Baratin, S. Rajeshwar, S. Ozair, Y. Bengio, A. Courville, and D. Hjelm, “Mutual infor- mation neural estimation,” in International Conference on Machine Learning (ICML), 2018.

  • [15] Wu, H. Ren, P. Li, and J. Leskovec, “Graph infor- mation bottleneck,” in Annual Conference on Neural Information Processing Systems (NeurIPS), 2020.

  • [16] S. Gershman and N. Goodman, “Amortized inference in probabilistic reasoning,” in Proceedings of the Annual Meeting of The Cognitive Science Society, 2014.

  • [17] Wang Z, Chen X, Wen R, et al. Information theoretic counterfactual learning from missing-not-at-random feedback[J]. Advances in Neural Information Processing Systems, 2020, 33: 1854-1864.

  • [18] Kingma D P, Welling M. Auto-encoding variational bayes[J]. arXiv preprint arXiv:1312.6114, 2013.

  • [19] Gershman S, Goodman N. Amortized inference in probabilistic reasoning[C]//Proceedings of the annual meeting of the cognitive science society. 2014, 36(36).

  • [20] Hjelm R D, Fedorov A, Lavoie-Marchildon S, et al. Learning deep representations by mutual information estimation and maximization[J]. arXiv preprint arXiv:1808.06670, 2018.

一图胜千言:

flow和miniflow

在介绍之前先说一些概念:里面有两个结构很重要,一个是flow一个是miniflow这里介绍一下他们的数据结构和构造函数。

flow:

flow的特点是8字节对齐的,存储报文相关字段和其他原数据,用于匹配流表,数据包含四个层次:
  1. metadata: 入端口号,寄存器等信息
  2. l2: 源目的mac,vlan和mpls等信息
  3. l3: ipv4/ipv6源目的ip,ttl等信息
  4. l4: 源目的端口号,icmp code和type等信息。
flow的坏处就是占用了很大的字节,并且有很多字段都是0,在2.8版本中flow的大小是672字节。

miniflow

miniflow是flow的压缩版,因为flow占用字节很大,比如可以支持ARP,IP等报文,填充了arp字段,icmp报文就是空的了,浪费了很多信息。过程中用到hash作为key,也是根据miniflow计算hash值,不是用的flow。
structminiflow {structflowmap map;
};
structflowmap {
map_t bits[FLOWMAP_UNITS];
};
miniflow其包含两部分内容:
  1. struct flowmap map;是bit数组,使用其中的bit表示flow中哪个8字节存在有效数据,flow中占多少个8字节,那么就需要map中多个个bit,并且按照64bit向上取整。
  2. 第二部分是有效数据,有效数据动态分配,根据struct flowmap map;中1bit数个数进行分配,大小为bit数*8字节,该部分直接跟在map后面。该部分存储在netdev_flow_key结构中的buf数组。
miniflow数据结构:
//flow是8字节对齐的,除8得到flow中包含8字节的个数
#define FLOW_U64S (sizeof(struct flow) / sizeof(uint64_t))

//map大小为8字节,MAP_T_BITS 为64位
typedef unsigned long longmap_t;#define MAP_T_BITS (sizeof(map_t) * CHAR_BIT)

//每位表示一个u64,FLOWMAP_UNITS 表示最少需要几个64位
#define FLOWMAP_UNITS DIV_ROUND_UP(FLOW_U64S, MAP_T_BITS)

structflowmap {
map_t bits[FLOWMAP_UNITS];
};
structminiflow {structflowmap map;/*Followed by:
* uint64_t values[n];
* where 'n' is miniflow_n_values(miniflow).
*/};structnetdev_flow_key {
uint32_t hash;
uint32_t len;
struct miniflow mf; //bits uint64_t buf[FLOW_MAX_PACKET_U64S]; //就是上边所说的value };//有些字段是互斥的 #define FLOW_MAX_PACKET_U64S (FLOW_U64S \ /*Unused in datapath*/ -FLOW_U64_SIZE(regs) \-FLOW_U64_SIZE(metadata) \/*L2.5/3*/ - FLOW_U64_SIZE(nw_src) /*incl. nw_dst*/\-FLOW_U64_SIZE(mpls_lse) \/*L4*/ -FLOW_U64_SIZE(tp_src) \
)

什么是jsRPC

说实话在介绍 JSRPC 我向大家推荐一个库 Selenium-wire 感觉和JSrpc的原理很像
RPC指远程过程调用,APP里面的RPC大家比较熟悉了。
那什么是jsRPC,顾名思义,就是指利用js手段实现代码远程控制浏览器控制台的一种手段,需要借助websocket实现。
下面我们先简单的写一个小例子

var ws = require('nodejs-websocket');
console.log('开始建立连接...');

var cached = {}

var server = ws.createServer(function (conn) {
  conn.on('text', function (msg) {
    var key = conn.key
    console.log("将要发送代码:【" + msg + "】至浏览器/python");
    server.connections.forEach(conn => {
      conn.send(msg);
    })
  })
  conn.on('close', function (code, reason) {
    console.log('关闭连接', code, reason)
  });
  conn.on('error', function (code, reason) {
    console.log('异常关闭', code, reason)
  });
}).listen(8015)
console.log('WebSocket建立完毕');

python

# -*- coding: utf-8 -*-
# @Time    : 2022/12/2 14:17
# @Author  : lzc
# @Email   : hybpjx@163.com
# @Software: PyCharm
# @blog    : https://www.cnblogs.com/zichliang

import websocket

ws = websocket.WebSocketApp("ws://127.0.0.1:8015")

def on_message(ws,message):
    if message.split("_")[0]!= 'js':
        print(message)


ws.on_message = on_message
ws.run_forever()

在浏览器中注入

(function () {
    if (window.WebSocket) {
        ws = new WebSocket("ws://localhost:8015");
        ws.onopen = function (e) {
        };
        ws.onclose = function (e) {
            console.log("server closed", e)
        }
        ws.onerror = function (e) {
            "connection error"
        }
        ws.onmessage = function (e) {
            console.log(e)
        }
    }
})()

在浏览器中对其进行操作
比如发送什么数据

image.png
在python代码中就能接收到这些数据
image.png

基于sekiro的isRPC应用

简介: sekiro是基于长链接和代码注入的Android private API暴露框架

sekiro在安卓方面的应用请看课程 第六章
github:
https://github.com/virjar/sekiro
作者:邓维佳 virjar 渣总[都是一个人]
sekiro 在 web方向,使用 websocket协议注入
官方文档内容:
Sekiro 已支持 websocket 协议,使用本功能可以支持注入js 到浏览器后,调用浏览器环境的is 代码
Web 环境基于 WebSocket 实现
文档:
https://sekiro.iinti.cn/sekiro-doc/01_user_manual/1.quickstart.html
下载地址:
Go HTTP File Server (iinti.cn)
image.png
下载这个 到本地
并且下载好 JDK java的编译器

image.png
然后运行响应版本
注意:

使用

使用方法:将js文件
sekiroWebMini.js
注入到浏览器环境 ,
之后通过当前接口访问自己的浏览器服务:

ws://127.0.0.1:5612
image.png

然后注入我们的代码

var client = new SekiroClient("ws://127.0.0.1:5612/business/register?group=demo-ws&clientId="+Math.random());
client.registerAction("clientTime",function(request, resolve,reject ){
            resolve(""+new Date());
})

image.png
请注意,Sekiro JS RPC是Sekiro的扩展,依然需要遵循Sekiro的各种规则定义。

注入完成之后 使用我们的浏览器访问
http://127.0.0.1:5612/business/invoke?group=demo-ws&action=clientTime

必须与我们注入的代码相对应 不然就会失败 成功之后是这样的
image.png

可以看到 这里返回的就是我们resolve(""+new Date()); 这段代码 也就意味着可以进行任何的操作。

对于sekiro jsRPC的理解

提供了一种浏览器与代码以及接口交互的一种可行性理论上只要是网站都可以通过这种方案在is层面突破反爬限制,且稳定性要优于目前的一切自动化框架

使用描述:
可能性1:针对解密函数的接口暴露
可能性2:针对数据接口的接口暴露
可能性3:针对自动化采集的接口暴露
可能性4:其他的一切想你所想....

优势

sekiro驱动jsRPC的优势与缺陷分析

  1. 与传统自动化框架相比,稳定性更高且不需要考虑浏览器指纹。操作得当的话,性能不在一个数量级
  2. 可以通过js直接获取加密参数,对于全局变量的参数暴露具有极佳的体验。[一剑破光阴]
  3. 可以直接获取接口数据,对于获取数据接口暴露明显的网站具有极佳的体验。
  4. 不考虑风控的情况下,能达到高并发。

缺陷

  1. 内置不可更改的浏览器环境不可更改,对于风控突破而言是个极大的隐患[浏览器沙箱可能会解决这个问题]
  2. 需要对js有较深理解,需要寻找合适的注入时机和注入位置[这个算缺陷么? 菜是原罪]。
  3. 需要注入脚本,所以要依托于油猴脚本/autoResponse/手动注入[如果使用抓包工具需要注意端口问题]
  4. 对于全局js混淆、风控严格、is参数分布散乱、鼠标轨迹指纹、变量作用域复杂的网址的支持较差。
  5. 需要保持浏览器开启,所以多少会占用些资源。
  6. 如果不取参数,而是只取接口内容 或者 参数与IP绑定的加密,对代理池接入的支持不佳。