2024年10月

近年来,人们尝试增加卷积神经网络(
CNN
)的卷积核大小,以模拟视觉
Transformer

ViTs
)自注意力模块的全局感受野。然而,这种方法很快就遇到了上限,并在实现全局感受野之前就达到了饱和。论文证明通过利用小波变换(
WT
),实际上可以获得非常大的感受野,而不会出现过参数化的情况。例如,对于一个
\(k \times k\)
的感受野,所提出方法中的可训练参数数量仅以
\(k\)
进行对数增长。所提出的层命名为
WTConv
,可以作为现有架构中的替换,产生有效的多频响应,且能够优雅地随着感受野大小的变化而扩展。论文在
ConvNeXt

MobileNetV2
架构中展示了
WTConv
层在图像分类中的有效性,以及作为下游任务的主干网络,并且展示其具有其它属性,如对图像损坏的鲁棒性以及对形状相较于纹理的增强响应。

来源:晓飞的算法工程笔记 公众号,转载请注明出处

论文: Wavelet Convolutions for Large Receptive Fields

Introduction


在过去十年中,卷积神经网络(
CNN
)在许多计算机视觉领域占主导地位。尽管如此,随着视觉
Transformer

ViTs
)的出现(这是一种用于自然语言处理的
Transformer
架构的适应),
CNN
面临着激烈的竞争。具体而言,
ViTs
目前被认为相较于
CNN
具有优势的原因,主要归功于其多头自注意力层。该层促进了特征的全局混合,而卷积在结构上仅局限于特征的局部混合。因此,最近几项工作尝试弥补
CNN

ViTs
之间的性能差距。有研究重构了
ResNet
架构和其训练过程,以跟上
Swin Transformer
。“增强”的一个重要改进是增加卷积核的大小。然而,实证研究表明,这种方法在
\(7\times7\)
的卷积核大小处就饱和了,这意味着进一步增加卷积核并没有帮助,甚至在某个时候开始出现性能恶化。虽然简单地将大小增加到超过
\(7\times7\)
并没有用,但
RepLKNet
的研究已经表明,通过更好的构建可以从更大的卷积核中获益。然而,即便如此,卷积核最终仍然会变得过参数化,性能在达到全局感受野之前就会饱和。


RepLKNet
分析中,一个引人入胜的特性是,使用更大的卷积核使得卷积神经网络(
CNN
)对形状的偏向性更强,这意味着它们捕捉图像中低频信息的能力得到了增强。这个发现有些令人惊讶,因为卷积层通常倾向于对输入中的高频部分作出响应。这与注意力头不同,后者已知对低频更加敏感,这在其他研究中得到了证实。

上述讨论引发了一个自然的问题:能否利用信号处理工具有效地增加卷积的感受野,而不至于遭受过参数化的困扰?换句话说,能否使用非常大的滤波器(例如具有全局感受野的滤波器),同时提升性能?论文提出的方法利用了小波变换(
WT
),这是来自时频分析的一个成熟工具,旨在有效扩大卷积的感受野,并通过级联的方式引导
CNN
更好地响应低频信息。论文将解决方案基于小波变换(与例如傅里叶变换不同),因为小波变换保留了一定的空间分辨率。这使得小波域中的空间操作(例如卷积)更加具有意义。

更具体地说,论文提出了
WTConv
,这是一个使用级联小波分解的层,并执行一组小卷积核的卷积,每个卷积专注于输入的不同频率带,并具有越来越大的感受野。这个过程能够在输入中对低频信息给予更多重视,同时仅增加少量可训练参数。实际上,对于一个
\(k\times k\)
的感受野,可训练参数数量只随着
\(k\)
的增长而呈对数增长。而
WTConv
与常规方法的参数平方增长形成对比,能够获得有效的卷积神经网络(
CNN
),其有效感受野(
ERF
)大小前所未有,如图
1
所示。

WTConv
作为深度可分离卷积的直接替代品,可以在任何给定的卷积神经网络(
CNN
)架构中直接使用,无需额外修改。通过将
WTConv
嵌入到
ConvNeXt
中进行图像分类,验证了
WTConv
的有效性,展示了其在基本视觉任务中的实用性。在此基础上,进一步利用
ConvNeXt
作为骨干网络,扩展评估到更复杂的应用中:在
UperNet
中进行语义分割,以及在
Cascade Mask R-CNN
中进行物体检测。此外,还分析了
WTConv

CNN
提供的额外好处。

论文的贡献总结如下:

  1. 一个新的层
    WTConv
    ,利用小波变换(
    WT
    )有效地增加卷积的感受野。

  2. WTConv
    被设计为在给定的卷积神经网络(
    CNN
    )中作为深度可分离卷积的直接替代。

  3. 广泛的实证评估表明,
    WTConv
    在多个关键计算机视觉任务中提升了卷积神经网络(
    CNN
    )的结果。


  4. WTConv
    在卷积神经网络(
    CNN
    )的可扩展性、鲁棒性、形状偏向和有效感受野(
    ERF
    )方面贡献的分析。

Method


Preliminaries: The Wavelet Transform as Convolutions

在这项工作中,采用
Haar
小波变换,因为它高效且简单。其他小波基底也可以使用,尽管计算成本会有所增加。

给定一个图像
\(X\)
,在一个空间维度(宽度或高度)上的一层
Haar
小波变换由核为
\([1,1]/\sqrt{2}\)

\([1,-1]/\sqrt{2}\)
的深度卷积组成,之后是一个缩放因子为
2
的标准下采样操作。要执行
2D Haar
小波变换,在两个维度上组合该操作,即使用以下四组滤波器进行深度卷积,步距为
2
:

\[\begin{align}
\begin{split}
f_{LL} = \frac{1}{2}
\begin{bmatrix}
1 & 1 \\
1 & 1
\end{bmatrix},\,
f_{LH} = \frac{1}{2}
\begin{bmatrix}
1 & -1 \\
1 & -1
\end{bmatrix},\,
f_{HL} = \frac{1}{2}
\begin{bmatrix}
\;\;1 & \;\;1 \\
-1 & -1
\end{bmatrix},\,
f_{HH} = \frac{1}{2}
\begin{bmatrix}
\;\;1 & -1 \\
-1 & \;\;1
\end{bmatrix}.
\end{split}
\end{align}
\]

注意,
\(f_{LL}\)
是一个低通滤波器,而
\(f_{LH}, f_{HL}, f_{HH}\)
是一组高通滤波器。对于每个输入通道,卷积的输出为

\[\begin{align}
\begin{split}
\left[X_{LL},X_{LH},X_{HL},X_{HH}\right] = \mbox{Conv}([f_{LL},f_{LH},&f_{HL},f_{HH}],X)
\end{split}
\end{align}
\]

输出具有四个通道,每个通道在每个空间维度上的分辨率为
\(X\)
的一半。
\(X_{LL}\)

\(X\)
的低频分量,而
\(X_{LH}, X_{HL}, X_{HH}\)
分别是其水平、垂直和对角线的高频分量。

由于公式
1
中的核形成了一个标准正交基,逆小波变换(
IWT
)可以通过转置卷积实现:

\[\begin{align}
\begin{split}
X = \mbox{Conv-transposed}(&\left[f_{LL},f_{LH},f_{HL},f_{HH}\right],\\
&\left[X_{LL},X_{LH},X_{HL},X_{HH}\right]).
\end{split}
\end{align}
\]

级联小波分解是通过递归地分解低频分量来实现的。每一层的分解由以下方式给出:

\[\begin{align}
X^{(i)}_{LL}, X^{(i)}_{LH}, X^{(i)}_{HL}, X^{(i)}_{HH} = \mathrm{WT}(X^{(i-1)}_{LL})
\end{align}
\]

其中
\(X^{(0)}_{LL} = X\)
,而
\(i\)
是当前的层级。这导致较低频率的频率分辨率提高,以及空间分辨率降低。

Convolution in the Wavelet Domain

增加卷积层的核大小会使参数数量呈平方级增加,为了解决这个问题,论文提出以下方法。

首先,使用小波变换(
WT
)对输入的低频和高频内容进行过滤和下采样。然后,在不同的频率图上执行小核深度卷积,最后使用逆小波变换(
IWT
)来构建输出。换句话说,过程由以下给出:

\[\begin{align}
Y = \mathrm{IWT}(\mathrm{Conv}(W,\mathrm{WT}(X))),
\end{align}
\]

其中
\(X\)
是输入张量,
\(W\)
是一个
\(k \times k\)
深度卷积核的权重张量,其输入通道数量是
\(X\)
的四倍。此操作不仅在频率分量之间分离了卷积,还允许较小的卷积核在原始输入的更大区域内操作,即增加了相对于输入的感受野。

采用这种
1
级组合操作,并通过使用公式
4
中相同的级联原理进一步增加它。该过程如下所示:

\[\begin{align}
X^{(i)}_{LL},X^{(i)}_{H} &= \mathrm{WT}(X^{(i-1)}_{LL}),\\
Y^{(i)}_{LL},Y^{(i)}_{H} &= \mathrm{Conv}(W^{(i)},(X^{(i)}_{LL},X^{(i)}_{H})),
\end{align}
\]

其中
\(X^{(0)}_{LL}\)
是该层的输入,
\(X^{(i)}_H\)
表示第
\(i\)
级的所有三个高频图。

为了结合不同频率的输出,利用小波变换(
WT
)及其逆变换是线性操作的事实,这意味着
\(\mathrm{IWT}(X+Y) = \mathrm{IWT}(X)+\mathrm{IWT}(Y)\)
。因此,进行以下操作:

\[\begin{align}
Z^{(i)} &= \mathrm{IWT}(Y^{(i)}_{LL}+Z^{(i+1)},Y^{(i)}_{H})
\end{align}
\]

这将导致不同级别卷积的求和,其中
\(Z^{(i)}\)
是从第
\(i\)
级及之后的聚合输出。这与
RepLKNet
一致,其中两个不同尺寸卷积的输出被相加作为最终输出。


RepLKNet
不同,不能对每个
\(Y^{(i)}_{LL}, Y^{(i)}_H\)
进行单独归一化,因为这些的单独归一化并不对应于原始域中的归一化。相反,论文发现仅进行通道级缩放以权衡每个频率分量的贡献就足够了。

The Benefits of Using WTConv

在给定的卷积神经网络(
CNN
)中结合小波卷积(
WTConv
)有两个主要的技术优势。

  1. 小波变换的每一级都会增加层的感受野大小,同时仅小幅增加可训练参数的数量。也就是说,
    WT

    \(\ell\)
    级级联频率分解,加上每个级别的固定大小卷积核
    \(k\)
    ,使得参数的数量在级别数量上呈线性增长( $ \ell\cdot4\cdot c\cdot k^2 $ ),而感受野则呈指数级增长( $ 2^\ell\cdot k $ )。
  2. 小波卷积(
    WTConv
    )层的构建旨在比标准卷积更好地捕捉低频。这是因为对输入的低频进行重复的小波分解能够强调它们并增加层的相应响应。通过对多频率输入使用紧凑的卷积核,
    WTConv
    层将额外的参数放置在最需要的地方。

除了在标准基准上取得更好的结果,这些技术优势还转化为网络在以下方面的改进:与大卷积核方法相比的可扩展性、对于损坏和分布变化的鲁棒性,以及对形状的响应比对纹理的响应更强。

Computational Cost

深度卷积在浮点运算(
FLOPs
)方面的计算成本为:

\[\begin{align}
C\cdot K_W \cdot K_H \cdot N_W \cdot N_H \cdot \frac{1}{S_W} \cdot \frac{1}{S_H},
\end{align}
\]

其中
\(C\)
为输入通道数,
\((N_W,N_H)\)
为输入的空间维度,
\((K_W,K_H)\)
为卷积核大小,
\((S_W,S_H)\)
为每个维度的步幅。例如,考虑一个空间维度为
\(512\times512\)
的单通道输入。使用大小为
\(7\times7\)
的卷积核进行卷积运算会产生
\(12.8M\)
FLOPs
,而使用大小为
\(31\times31\)
的卷积核则会产生
\(252M\)
FLOPs
。考虑
WTConv
的卷积集,尽管通道数是原始输入的四倍,每个小波域卷积在空间维度上减少了一个因子
2

FLOP
计数为:

\[\begin{align}
C \cdot K_W \cdot K_H \cdot \left(N_W \cdot N_H + \sum\limits_{i=1}^\ell 4\cdot\frac{N_W}{2^i} \cdot \frac{N_H}{2^i}\right),
\end{align}
\]

其中
\(\ell\)

WT
层级的数量。继续之前输入大小为
\(512\times512\)
的例子,对一个
3

WTConv
使用大小为
\(5\times5\)
的多频卷积(其感受野为
\(40\times40=(5\cdot 2^3) \times (5\cdot 2^3)\)
)会产生
\(15.1M\)
FLOPs
。当然,还需要添加
WT
计算本身的成本。当使用
Haar
基底时,
WT
可以以非常高效的方式实现。也就是说,如果使用标准卷积操作的简单实现,
WT

FLOP
计数为:

\[\begin{align}
4C\cdot \sum\nolimits_{i=0}^{\ell-1} \frac{N_W}{2^i} \cdot \frac{N_H}{2^i},
\end{align}
\]

因为这四个卷积核的大小为
\(2\times2\)
,在每个空间维度上的步幅为
2
,并且作用于每个输入通道。同样,类似的分析表明,
IWT

FLOP
计数与
WT
相同。继续这个例子,
3

WT

IWT
的额外成本为
\(2.8M\)
FLOPs
,总计为
\(17.9M\)
FLOPs
,这仍然在相似感受野的标准深度卷积中有显著的节省。

Results




如果本文对你有帮助,麻烦点个赞或在看呗~
更多内容请关注 微信公众号【晓飞的算法工程笔记】


喊了多年的互联网寒冬,今年的寒风格外凛冽,还在坚守安卓开发的朋友着实不容易。因为能转行的早就转了,能转岗的也早就转了,那么安卓程序员比较迷茫的就是,我该学什么安卓技术才好呢?还是直接扔了安卓再去搞别的技术吗?

下面探讨下安卓程序员还能在哪些方面进阶修炼,主要有以下三个方向。

一、纵向钻研谷歌爸爸推出的最新技术

谷歌就是安卓的爹,只要谷歌不倒,安卓开发就不会倒。今年的谷歌开发者大会都放到北京举办了,可见爸爸真的非常疼咱们爱咱们。
爸爸每段时间就推出新款Android的预览版、测试版和稳定版,各种新特性就像挤牙膏似的总也挤不完,咱们就慢慢吮吸总也吸不完。
爸爸每段时间就推出新款Android Studio的Alpha版、Beta版、Canary版,然后还有Release版,这些ABC加上R版本,如此顺口方便咱们细嚼慢咽。
爸爸推出了Kotlin语言,谁让当年采用Java语言的爸爸吃了官司呢?既然爸爸吃了Java官司,那么咱们赶紧把Kotlin囫囵吞下去。
爸爸推出了Flutter跨平台框架,即使爸爸裁了FuchsiaOS团队,裁了Python团队,连Go团队的技术负责人都一脚踢飞,但仍然保留着Flutter和Dart团队,所以咱们要放下顾虑,继续无脑向Flutter冲呀。
爸爸推出了Jetpack和Compose套件,这些年来Jetpack和Compose不断推陈出新,可见爸爸唯恐咱们饿了没东西啃,所以咱们年年啃月月啃。
该方向的学习难度系数为★★★,保饭碗指数为★★
。理由:爸爸的App开发技术都是公开的,而且简单易学门槛低。
嗯,学习Jetpack套件与最新的Android开发推荐这本书《Android Studio开发实战:从零基础到App上线(第3版)》,该书基于Android12和Android Studio Dolphin,介绍了包含DataStore、Room、RecyclerView、ViewPager2、WorkManager、Glide、CameraX、ExoPlayer等等在内的Jetpack套件。

二、横向拓展安卓开发的新功能新应用

除了谷歌爸爸推出的组件库,还有其他专业领域的第三方库,能够实现与众不同的新功能。
比如初级安卓只会调用HTTP的POST接口,但是物联网方面更需要Socket通信与蓝牙通信,那么SocketIO、WebSocket、Bluetooth LE就是必须掌握的。具体参见《Android Studio开发实战:从零基础到App上线(第3版)》一书的“13.4  即时通信”和“17.3  低功耗蓝牙”。
又如初级安卓只会使用画布Canvas和画笔Paint作图,但是AI视觉方面更需要三维制图和动态追踪,那么OpenGL、OpenGL ES、OpenCV就是必须掌握的。具体参见《Android App开发进阶与项目实战》一书的“5.1  OpenGL”、“5.2  OpenGL ES”和“12.2  基于计算机视觉的人脸识别”。
再如初级安卓只会通过VideoView和ExoPlayer播放视频,但是音视频方面更需要实时交互和动态剪辑,那么WebRTC、FFmpeg、RTMP/SRT(直播协议)就是必须掌握的。其中WebRTC的App开发参见《Android Studio开发实战:从零基础到App上线(第3版)》一书的“20.2  给App集成WebRTC”,FFmpeg的App开发参见《FFmpeg开发实战:从零基础到短视频上线》一书的“第12章  FFmpeg的移动开发”,手机App的直播技术参见之前的文章《利用RTMP协议构建电脑与手机的直播Demo》和《利用SRT协议构建手机APP的直播Demo》。
该方向的学习难度系数为★★★★,保饭碗指数为★★★。
理由:以上技能涉及计算机科学的专业领域知识,具备一定的技术门槛。
嗯,学习音视频和FFmpeg编程技术推荐这本书《FFmpeg开发实战:从零基础到短视频上线》,该书详细介绍了如何在Windows系统和Linux系统分别搭建FFmpeg的开发环境,第12章还介绍了如何通过Android Studio+FFmpeg开发手机App。

三、打开思路迈向Android+N的新天地

除了常见的App应用开发之外,安卓与其他行业结合还能产生更多的就业岗位。
比如Android+汽车行业就产生了车机开发,那要学习车载系统Automotive OS,以及外景系统EVS、娱乐系统IVI等等。其中Automotive OS是谷歌爸爸基于AOSP开发的,目前已经迭代到了Automotive OS 14。
又如Android+游戏行业就产生了手游开发,那要学习Unity3D、Cocos2d-x、Unreal4、CryEngine3等游戏引擎。其中Unity3D是国外研发的历史悠久游戏引擎,而Cocos2d-x是国产的后起之秀游戏引擎。
再如Android+安全行业就产生了网安开发,那要学习逆向工具Frida、系统框架工具LSPosed、全局注入管理工具RxPosed、脱抽取壳工具dumpDex、自定义APK模块加载器HideApk,以及逆向观测技术jvmti、Native层观测技术gdbinjec等等.
该方向的学习难度系数为★★★★★,保饭碗指数为★★★★。
理由:以上属于细分行业的专门技能,独特的行业经验拥有高门槛。
嗯,学习Android原生App的安全和逆向技术推荐这本书《Frida Android SO逆向深入实践》,该书详细介绍了如何使用Frida揭示原生App的逆向、分析和破解之奥秘,还探讨了ARM/ELF的文件格式和反编译工具IDA。

更多详细的FFmpeg开发知识参见
《FFmpeg开发实战:从零基础到短视频上线》
一书。


title: Nuxt.js 应用中的 ready 事件钩子详解
date: 2024/10/12
updated: 2024/10/12
author:
cmdragon

excerpt:
ready 钩子是 Nuxt.js 中一个重要的生命周期事件,它在 Nuxt 实例初始化完成后被调用。当 Nuxt 已经准备好并准备开始处理请求或渲染页面时,这一钩子会被触发。

categories:

  • 前端开发

tags:

  • Nuxt.js
  • 生命周期
  • ready钩子
  • 应用初始化
  • 前端开发
  • Nuxt实例
  • 请求处理


image
image

扫描
二维码
关注或者微信搜一搜:
编程智域 前端至全栈交流与成长

ready
钩子是 Nuxt.js 中一个重要的生命周期事件,它在 Nuxt 实例初始化完成后被调用。当 Nuxt
已经准备好并准备开始处理请求或渲染页面时,这一钩子会被触发。通过使用
ready
钩子,开发者可以在应用初始化后执行一些必要的操作。


目录

  1. 概述
  2. ready 钩子的详细说明
  3. 具体使用示例
  4. 应用场景
  5. 实际开发中的最佳实践
  6. 注意事项
  7. 关键要点
  8. 练习题
  9. 总结


1. 概述

ready
钩子在 Nuxt 应用完成初始化并准备好接收用户请求或渲染页面时被调用。这使得开发者可以在这个阶段进行一些后期的设置或配置。

2. ready 钩子的详细说明

2.1 钩子的定义与作用

ready
钩子的主要功能包括:

  • 执行应用启动后的初始化逻辑
  • 设定全局变量或配置
  • 进行日志记录或监测

2.2 调用时机

  • 执行环境
    : 可在客户端和服务器端使用。
  • 挂载时机
    : 当 Nuxt 实例完成初始化并准备处理请求时,
    ready
    钩子会被调用。

2.3 返回值与异常处理

钩子没有返回值。钩子内部的异常应被妥善处理,以避免影响应用的正常运行。

3. 具体使用示例

3.1 基本用法示例

假设我们希望在 Nuxt 初始化完成后进行一些全局设置,比如初始化一个 API 客户端:

// plugins/readyPlugin.js
export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.hooks.ready(() => {
        console.log('Nuxt app is ready!');
        // 初始化 API 客户端等
        nuxtApp.$api = createApiClient();
    });
});

在这个示例中,我们在 Nuxt 实例准备好后输出日志并初始化一个 API 客户端。

3.2 与其他钩子结合使用

ready
钩子可以与其他钩子结合使用,以实现复杂的初始化逻辑:

// plugins/readyPlugin.js
export default defineNuxtPlugin((nuxtApp) => {
    nuxtApp.hooks.ready(() => {
        console.log('Nuxt app is ready!');
        // 设置全局状态
        nuxtApp.$store.dispatch('initGlobalState');
    });

    nuxtApp.hooks('page:transition:finish', () => {
        console.log('Page transition finished.');
    });
});

在这个例子中,我们在 Nuxt 准备好后初始化全局状态,同时监听页面过渡完成的事件。

4. 应用场景

  1. 全局配置
    : 在应用启动时进行全局变量或配置项的设定。
  2. 服务初始化
    : 初始化第三方服务,比如 Analytics、API 客户端等。
  3. 性能监测
    : 在应用准备好后开始性能监测。

5. 实际开发中的最佳实践

  1. 简洁明了
    : 在
    ready
    钩子中只执行必要的初始化逻辑,避免过于复杂的操作。
  2. 错误处理
    : 钩子内部应充分捕获可能出现的异常,以提高应用的健壮性。
  3. 模块化
    : 将不同的初始化代码分散到不同的插件中,以提升可维护性。

6. 注意事项

  • 性能考虑
    : 确保在钩子中执行的操作不会显著影响应用的加载时间。
  • 依赖管理
    : 确保在
    ready
    阶段的时候,所有需要的依赖已经准备好。

7. 关键要点

  • ready
    钩子在 Nuxt 实例完成初始化后被调用,用于执行基本配置和启动逻辑。
  • 合理利用此钩子可以提高应用的启动效率和用户体验。
  • 处理钩子中的异常非常重要,以确保应用的正常运行。

8. 练习题

  1. 全局状态初始化
    : 在
    ready
    钩子中实现全局状态的初始化逻辑。
  2. API 请求检测
    : 在应用准备好后,自动发送一次 API 请求以检测 API 是否正常。
  3. 性能日志
    : 在
    ready
    钩子中记录应用的启动时间,以分析性能瓶颈。

9. 总结

ready
是一个非常有用的钩子,它允许开发者在 Nuxt 应用完成初始化后执行必要的操作。合理利用这一钩子可以增强应用的可用性和用户体验。

余下文章内容请点击跳转至 个人博客页面 或者 扫码关注或者微信搜一搜:
编程智域 前端至全栈交流与成长
,阅读完整的文章:
Nuxt.js 应用中的 ready 事件钩子详解 | cmdragon's Blog

往期文章归档:

  • 实在是不知道标题写什么了 可以在评论区给个建议哈哈哈哈 先用这个作为标题吧

尝试使用 国内给出的 AI 大模型做出一个 可以和 AI 对话的 网站出来

    <dependency>
        <groupId>cn.bigmodel.openapi</groupId>
        <artifactId>oapi-java-sdk</artifactId>
        <version>release-V4-2.0.0</version>
    </dependency>
  • 使用 普通的 java -- Maven项目 只能在控制台 查看结果 也就是 说没有办法在其他平台 使
    用 制作出来的 AI ChatRobot
  • 思来想去 不如 将这个东西写成 QQ 机器人
  • 但是因为我找到的 那个 不更新了 或者 腾讯不支持了 让我放弃了 写成 QQ 机器人的想法
  • 于是我就尝试将这个写成一个本地的 AI 对话机器人 但是 在翻看 官方给出的 Demo 我偶然发现了一个方法 他的 输出似乎是一个 json 转换成的 String
  • 这个方法并没有将这个String 返回出来 而是 直接在控制台打印
package com.codervibe.utils;

import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.zhipu.oapi.ClientV4;
import com.zhipu.oapi.Constants;
import com.zhipu.oapi.service.v4.image.CreateImageRequest;
import com.zhipu.oapi.service.v4.image.ImageApiResponse;
import com.zhipu.oapi.service.v4.model.*;
import io.reactivex.Flowable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class ChatAPIUtils {
    private static final String API_KEY = "cb11ad7f3b68ce03ed9be6e13573aa19";

    private static final String API_SECRET = "nG7UQrrXqsXtqD1S";

    private static final ClientV4 client = new ClientV4.Builder(API_KEY, API_SECRET).build();

    private static final ObjectMapper mapper = defaultObjectMapper();


    public static ObjectMapper defaultObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        mapper.addMixIn(ChatFunction.class, ChatFunctionMixIn.class);
        mapper.addMixIn(ChatCompletionRequest.class, ChatCompletionRequestMixIn.class);
        mapper.addMixIn(ChatFunctionCall.class, ChatFunctionCallMixIn.class);
        return mapper;
    }

    // 请自定义自己的业务id
    private static final String requestIdTemplate = "mycompany-%d";



    /**
     * 同步调用
     */
    public static String InvokeApi(String content) throws JsonProcessingException {
        List<ChatMessage> messages = new ArrayList<>();
        ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), content);
        messages.add(chatMessage);
        String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
        // 函数调用参数构建部分
        List<ChatTool> chatToolList = new ArrayList<>();
        ChatTool chatTool = new ChatTool();
        chatTool.setType(ChatToolType.FUNCTION.value());
        ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters();
        chatFunctionParameters.setType("object");
        Map<String, Object> properties = new HashMap<>();
        properties.put("location", new HashMap<String, Object>() {{
            put("type", "string");
            put("description", "城市,如:北京");
        }});
        properties.put("unit", new HashMap<String, Object>() {{
            put("type", "string");
            put("enum", new ArrayList<String>() {{
                add("celsius");
                add("fahrenheit");
            }});
        }});
        chatFunctionParameters.setProperties(properties);
        ChatFunction chatFunction = ChatFunction.builder()
                .name("get_weather")
                .description("Get the current weather of a location")
                .parameters(chatFunctionParameters)
                .build();
        chatTool.setFunction(chatFunction);
        chatToolList.add(chatTool);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model(Constants.ModelChatGLM4)
                .stream(Boolean.FALSE)
                .invokeMethod(Constants.invokeMethod)
                .messages(messages)
                .requestId(requestId)
                .tools(chatToolList)
                .toolChoice("auto")
                .build();
        ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest);
        try {
        // 这里返回出去是一个 json 
            return mapper.writeValueAsString(invokeModelApiResp);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return mapper.writeValueAsString(new ModelApiResponse());
    }

    public static void CreateImage(String content) {
        CreateImageRequest createImageRequest = new CreateImageRequest();
        createImageRequest.setModel(Constants.ModelCogView);
        createImageRequest.setPrompt(content);
        ImageApiResponse imageApiResponse = client.createImage(createImageRequest);
        System.out.println("imageApiResponse:" + JSON.toJSONString(imageApiResponse));
    }

}

  • 工具类中 InvokeApi 方法 最后获得的是一个 ModelApiResponse类 这个类有点类似于 统一返回类型 但是我在这里 只需要里面的具体方法 请求状态和 信息 并不需要 (有另外一个统一返回类型定义 ) 所以在 后面我将这个方法 修改 改为 将我需要的数据返回给controller
  • 实际上这是不应该直接返回给 controller 的 而是 应该 通过 service 的 因为service中才是真正的业务代码
  • 修改后的方法 代码如下
    /**
     * 同步调用
     */
    public static ModelData InvokeApi(String content) throwsJsonProcessingException{
        List<ChatMessage> messages = new ArrayList<>();
        ChatMessage chatMessage = new ChatMessage(ChatMessageRole.USER.value(), content);
        messages.add(chatMessage);
        String requestId = String.format(requestIdTemplate, System.currentTimeMillis());
        // 函数调用参数构建部分
        List<ChatTool> chatToolList = new ArrayList<>();
        ChatTool chatTool = new ChatTool();
        chatTool.setType(ChatToolType.FUNCTION.value());
        ChatFunctionParameters chatFunctionParameters = new ChatFunctionParameters();
        chatFunctionParameters.setType("object");
        Map<String, Object> properties = new HashMap<>();
        properties.put("location", new HashMap<String, Object>() {{
            put("type", "string");
            put("description", "城市,如:北京");
        }});
        properties.put("unit", new HashMap<String, Object>() {{
            put("type", "string");
            put("enum", new ArrayList<String>() {{
                add("celsius");
                add("fahrenheit");
            }});
        }});
        chatFunctionParameters.setProperties(properties);
        ChatFunction chatFunction = ChatFunction.builder()
                .name("get_weather")
                .description("Get the current weather of a location")
                .parameters(chatFunctionParameters)
                .build();
        chatTool.setFunction(chatFunction);
        chatToolList.add(chatTool);
        ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
                .model(Constants.ModelChatGLM4)
                .stream(Boolean.FALSE)
                .invokeMethod(Constants.invokeMethod)
                .messages(messages)
                .requestId(requestId)
                .tools(chatToolList)
                .toolChoice("auto")
                .build();
        ModelApiResponse invokeModelApiResp = client.invokeModelApi(chatCompletionRequest);
        ModelData data = invokeModelApiResp.getData();
        return data;
  • 而这里的信息实际上是一层层 抽丝剥茧 剥离出来的
    List<Choice> choices = data.getChoices();
    System.out.println("choices.toString() = " + choices.toString());
    for (Choice choice : choices) {
        ChatMessage message = choice.getMessage();
        System.out.println("message.getContent() = " + message.getContent());
        //本来这里想返回具体的信息类但是发现 上面的的那个ModelApiResponse类 也是一个 统一返回类型 也包含这 请求状态码 之类的定义
        return message;
    }
    return new ChatMessage();
    try {
        return mapper.writeValueAsString(invokeModelApiResp);
    } catch (JsonProcessingException e) {
            e.printStackTrace();
    }
    return mapper.writeValueAsString(new ModelApiResponse());    
  • 可以看到我的这段代码 有多个 return 所以这实际上是一段假 代码
  • 每一个return 实际上官方都 对应的 model 或者说 resoponse
  • controller 代码
    @PostMapping("/chat")
    public R chat(@RequestParam("content") String content) throws JsonProcessingException {
        /**
         * data 中的 choices 是一个 List<Choice> 类型但是实际上只有一个所以索性直接获取数组下标0的对象
         */
        logger.info(ChatAPIUtils.InvokeApi(content).getChoices().get(0).getMessage().getContent().toString());
        return R.ok().data("content", ChatAPIUtils.InvokeApi(content));
    }
  • 修改 由 service 层 调用 工具类
  • service 代码
  • service 接口
package com.codervibe.server.service;

import com.zhipu.oapi.service.v4.image.ImageResult;
import com.zhipu.oapi.service.v4.model.ModelData;

public interface ChatService {
    /**
     * AI 对话
     */
    ModelData AIdialogue(String content);

    /**
     * AI  画图
     */
    ImageResult AIcreateimage(String content);
}
  • service 接口实现

package com.codervibe.server.Impl;

import com.codervibe.server.service.ChatService;
import com.codervibe.utils.ChatAPIUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zhipu.oapi.service.v4.image.ImageResult;
import com.zhipu.oapi.service.v4.model.ModelData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service("chatService")
public class ChatServiceImpl implements ChatService {
    Logger logger = LoggerFactory.getLogger(ChatServiceImpl.class);
    /**
     * AI 对话
     * @param content
     */
    @Override
    public ModelData AIdialogue(String content) {
        logger.info(ChatAPIUtils.InvokeApi(content).getChoices().get(0).getMessage().getContent().toString());
        return ChatAPIUtils.InvokeApi(content);
    }

    /**
     * AI  画图
     *
     * @param content
     */
    @Override
    public ImageResult AIcreateimage(String content) {
        logger.info(ChatAPIUtils.CreateImage(content).getData().get(0).getUrl());
        return ChatAPIUtils.CreateImage(content);
    }
}

  • controller 层调用 service
****package com.codervibe.web.controller;

import com.codervibe.server.service.ChatService;
import com.codervibe.utils.ChatAPIUtils;
import com.codervibe.web.common.response.R;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;

@RestController
@RequestMapping("/chat")
public class ChatController {
    Logger logger = LoggerFactory.getLogger(ChatController.class);
    @Resource
    private ChatService chatService;
    @PostMapping("/content")
    public R chat(@RequestParam("content") String content) {
        return R.ok().data("content", chatService.AIdialogue(content));
    }
    @PostMapping("/AIcreateimage")
    public R AIcreateimage(@RequestParam("content") String content){
        return R.ok().data("image",chatService.AIcreateimage(content));
    }
}

  • 现在 虽然可以 和 AI 进行对话 但是 数据返回的速度实在是太慢 所以我打算 将 常见的问题和答案 存储在本地的数据库中以提升 数据返回的速度 这只是一个初步的想法
  • 最后的想法 还未实现 先这样
  • 粉丝群 企鹅 179469398

大家好,我是汤师爷~

今天聊聊开放平台架构设计。

为什么需要搭建开放平台

增强产品能力

开放平台能够让三方开发者和合作伙伴开发新的应用或服务,增加原有SaaS产品能力。这样就可以满足更多用户需求,从而提高用户的满意度和黏性。

促进创新

三方开发者能够在SaaS标准产品的基础上,创造新的解决方案,为平台带来创新的业务模式,这些可能为SaaS企业带来更多的盈利机会。

构建生态系统

开放平台能够建立一个以SaaS标准产品为中心的生态系统,吸引开发者、合作伙伴和其他相关方参加,共同构建一个互惠互利的生态圈。

降低开发和运营成本

通过邀请三方开发者来创造和扩展产品能力,他们可以有效分担SaaS企业的开发、运营成本,更聚焦于核心产品的优化和创新。

开放平台的服务对象是谁?

SaaS企业的开放平台通常包括以下关键用户角色:

第三方开发者

他们期望能快速入驻开放平台,并构建应用,通过用户订购应用获取收益。因此,他们需要API文档、SDK工具和开发者后台,帮助开发者构建、测试和部署应用,并且利用平台资源推广自己的应用。

定制客户

定制客户一般为拥有自研能力的企业客户,有定制化功能需求,例如与内部系统(如ERP、CRM)进行打通,实现企业自身的业务流程。

平台运营人员

平台运营人员需要为三方开放者和企业客户服务,帮助他们解决问题,因此,需要客户管理,应用申请流程管理、服务配置、参数配置、角色分配、财务对账管理等产品能力。

开放平台的运营流程

SaaS开放平台的运营流程涉及平台的管理和维护,为企业客户、三方开发者提供服务,包括吸引与管理三方开发者,提供必要的开发工具和支持,对开发者创建的应用进行审核和上线管理,通过数据监控和分析评估平台的健康度和用户活跃度,确保提供有效的服务支持,和维护平台的安全和合规性。

下图展示了开放平台的整体运营流程,实际的开放平台项目可以基于该流程做变更。

开放平台整体架构设计

管理后台

层针对不同角色,提供不同的管理后台:

  • 开发者后台:为三方开发者提供的工作台,包括应用管理、API接入、开发工具、数据分析和测试工具等。
  • 平台运营后台:用于平台运营团队管理整个系统的工作台,包括客户管理、权限控制、计费管理、系统监控等功能。
  • 商家后台:SaaS企业客户的后台系统,主要用于订购应用市场的三方应用,授权应用,并使用其提供的服务能力。

服务层

服务层为上层的管理后台提供核心服务能力:

  • 开发者接入:提供API文档、SDK工具、开发指南,应用的注册、管理等。
  • 运营管理:包括平台用户信息管理、权限设置、用户资料管理。对开发者提交的应用进行审核,确保应用的质量和安全性。管理平台计费、结算,收集和分析平台的运营数据。
  • 监控中心:包括服务器、应用、网络、数据库、安全、中间件和存储监控。这些功能确保开放平台的稳定性、性能和安全,通过实时监控、告警支持技术团队进行有效管理和维护。

API网关

API网关是整个开放平台的流量入口,它提供的能力确保了平台操作的安全、稳定和高效管理。

业务开放能力

业务开放能力由各个业务域系统提供,这些开放能力提供了核心业务数据/功能的交互能力。

开放能力设计

开放能力可以分为以下几种类型:

  • 前端扩展:开发者可创建个性化的前端H5/小程序页面,满足企业客户不同场景不同行业的需求。
  • API 接口/消息推送:API接口允许开发者通过定义的接口与平台系统交互,实现数据和功能的集成,例如商品创建接口。消息推送是指平台系统主动通知三方系统,如订单状态变更通知。
  • 后端扩展:开发者可以通过扩展点,自由嵌入自定义流程节点,构建个性化的业务逻辑。
  • 数据模型扩展:允许将三方系统的数据模型整合到平台系统中,在平台系统中可以查看或处理三方数据。

以商品系统为例,列出不同类型开放能力的使用场景:

  • 前端扩展:页面串联、定制商详组件、定制商品详情、定制B端管理页面。
  • API 接口/消息推送:商品发布接口、同步分店接口、查询商品详情接口、商品价格修改接口、商品修改接口、商品属性接口、商品上下架接口、商品类目接口、商品创建消息、商品变更消息。
  • 后端扩展:商品校验类扩展点(例如,商品创建时,校验商品编码是否符合定制需求的规范)、商品的定制信息计算扩展点(例如,通过外部接口计算出商品重量信息)。
  • 数据模型扩展:商品模型扩展并存储个性化的字段信息。

开放API设计原则

RESTful风格API

RESTful API 是一种遵循 REST 原则的 API 设计方式。REST 是一组约束条件和原则,由 Roy Fielding 在 2000 年的博士论文中提出。

RESTful API 的设计依赖于网络协议,主要是 HTTP,并且它使用 HTTP 的原生功能(比如 HTTP 的动词和状态码)来执行操作。以下是 RESTful API 的一些主要特点:

  • 面向资源:在REST架构中,所有内容都被视为"资源",每个"资源"都有一个独特的URI(统一资源标识符)。
  • 无状态:RESTful API不保存状态,这意味着每个请求都应包含执行请求所需的信息。服务器不会保存客户端的任何信息。
  • 统一接口:RESTful API应该有一个统一的接口,客户端和服务器基于接口交互,实现解耦。交互通常通过HTTP动词实现,如GET(获取资源)、POST(创建资源)、PUT(更新资源)、DELETE(删除资源)。

RESTful API的三个显著优势如下:

  • 它建立在HTTP协议上,协议简洁易用,得到了广泛的应用。
  • 接口设计以资源为中心,让接口易于理解和使用,比较直观。
  • 数据交换采用XML或JSON格式,大大简化了数据的处理和传输过程。

但严格遵循 RESTful API风格,也有一些缺陷:

HTTP协议的动词受限

当业务需求变得复杂时,仅依赖于HTTP的动词方法来对资源操作,可能不足以满足需求,这时往往需要通过接口名称来进一步区分。此外,一些特定的HTTP请求,如PUT和DELETE,可能会在网络传输过程中被某些防火墙设备拦截。

URL包含参数,可读性差

在URL中嵌入参数占位符(例如:GET /Api/Orders/{id}/OrderItems/{id})会降低其可读性。如果需要基于URL统计接口的调用次数,需要对具有相同URL的不同参数进行额外的处理。

HTTP状态码的表达性差

使用如20X、30X、4XX、5XX等标准的HTTP状态码,不足以描述复杂的业务场景的状态。

建议接口设计遵循以下准则:

  • 限制HTTP方法的使用,仅采用GET和POST。
  • 避免在URL中包含参数占位符,尽量使用URL的参数传参。
  • 使用自定义的业务状态码来提供更丰富的响应信息。

API分组原则

根据业务领域,对开放API进行分组。例如店铺API、商品API、库存API、订单API、物流API、客户API、营销API。

SaaS标准产品一般都基于DDD进行架构设计,根据业务领域组织开放API,是普遍采用的最佳实践。当需要改进或变更某个特定业务领域的功能时,开发人员可以直接找到相关的API组进行修改,不会影响到其他领域的API。

对于三方开发人员,可以更容易地找到与某个业务功能相关的API,因为它们通过业务域的划分逻辑组织在一起。

版本管理

为了统一和清晰地标识不同版本的相同接口,建议将版本号放置在接口路径的末尾,示例如下:

返回数据

每个接口的响应数据,应遵循统一的JSON或XML格式规范,并且至少应包含以下关键字段:

  • 状态码 (code):表示请求的总体结果,通常用于标识操作的成功或异常状态。
  • 消息 (msg):提供关于状态码的详细描述,以帮助理解请求的具体结果。
  • 数据 (data):包含与请求相关的具体业务信息和数据。
{
	"code":200,
	"msg":"OK",
	"data":{
	"item_id":"123456",
	"product_name":"奶油蛋糕"
	}
}

安全措施

接口签名

为开发者分配AccessKey(开发者标识,确保唯一)和SecretKey(用于接口加密,确保不易被穷举,生成算法不易被猜测)。

按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串A。

在字符串A最后拼接上Secretkey得到字符串B。

对字符串B进行MD5运算,得到Sign值。请求时,携带参数AccessKey和Sign,只有拥有合法的身份AccessKey和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到SecretKey(仅作本地加密使用,不参与网络传输),也无法伪造合法的请求。

数据加密

敏感数据,如用户信息,应使用加密算法进行保护,常用的加密方法包括RSA和AES。

访问控制

在接口访问的API网关,应设置访问控制,仅允许来自被商家授权的白名单的请求。商家可以通过商家后台系统自主管理其白名单。

消息推送

消息推送是平台主动通知三方系统,提供数据更新的一种机制,满足三方系统对信息实时性的需求。例如,当商家成功创建订单后,三方系统可以通过订单查询接口来获取订单的当前状态。

三方系统若想实时获取订单状态,可以选择定时查询接口,但这样效率低并消耗大量资源。通过系统主动推送订单状态信息,可以有效地解决这一问题。但消息推送也带来了一些挑战:

  • 顺序性问题:订单可能经历多个状态,且这些状态在业务上有特定顺序。网络延迟可能导致状态送达三方系统时,顺序错乱,这时三方系统需要通过校验订单状态,判断变更是否符合业务逻辑,来确保订单状态的准确性。
  • 消息丢失风险:目前系统通常采用消息队列异步发送消息推送,尽管有消息中间件的机制确保消息的可靠性,但三方系统出现网络问题,仍有可能导致推送失败。解决方案:三方系统可以定期全量查询订单状态,对双边的订单状态进行对账处理,确保数据的完整性。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。