开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

用不了ChatGPT?快试试免费又强大的Anthropic

发表于 2024-04-27

image.png

一、Claude 简介

Anthropic 官方: www.anthropic.com/product

Claude 是最近新开放的一款 AI 聊天机器人,是世界上最大的语言模型之一,比之前的一些模型如 GPT-3 要强大得多,因此 Claude 被认为是 ChatGPT 最有力的竞争对手。Claude 的研发公司是专注人工智能安全和研究的初创公司 Anthropic,由前 OpenAI 员工共同创立的。今年 3 月份 Anthropic 获得了谷歌 3 亿美元的投资,谷歌也因此获得其 10% 股份。

image.png

image.png

Anthropic 官网

据官方介绍,Claude 的核心模型经由训练,目标是变得有用、诚实和无害。此外 Claude 更能理解和接受自然语言,和它对话无需复杂的技巧,可以轻松得到详细且易于理解的答案。它目前有两种型号 Claude-v1 和 Claude Instant:

① Claude-v1:功能强大的模型,可以处理复杂的对话、生成创意内容和详细说明。

② Claude Instant:更快更便宜的模型,可以处理偏随意的对话,对文本进行分析和摘要以及根据文档进行问答。

支持订阅Claude的虚拟卡,点击获取

与 ChatGPT 等大型语言模型一样,Claude 的应用场景非常广泛,信息搜索、内容总结摘要、写作协助、创意生成、问答、编程这些任务它都能轻松完成。目前 Claude 已经被应用在多个知名产品中,比如知识笔记工具 Notio AI 就是用 Claude 协助用户进行智能写作,国外问答社区 Quora 也在自己的 AI 聊天应用程序 Poe 中置入了 Claude。

image.png

虽然是用英语语言训练的,但是 Claude 也能很好的理解中文等其他语言。为了测试 Claude 的中文理解及创作能力,我让它写了一首赞美设计师的诗。它的反应速度很快,几秒内就给出了内容,创作的质量也很不错,运用比喻修辞手法,还知道中英文结合使用,感觉比我厉害多了。使用过程中我发现如果聊天时间间隔太长,Claude 会自动断线,发消息它没有反应,需要刷新网页后才能重新连接上。

二、Claude 使用途径

Slack-Claude 官方网址: www.anthropic.com/claude-in-s…

Claude 已经被置入一款团队协作沟通应用 Slack 中,目前可以免费使用。具体的注册使用方法如下:

image.png

三、Claude 的不足

Claude 也依旧存在着很多和其他 AI 聊天机器人一样的缺陷,首先是它没有接入互联网,模型训练的内容来自于 2021 年春季以前,所以无法提供日期、天气、新闻等实时信息或据此提供进一步的判断。

Claude 也一直在聊天的过程中提醒用户自身存在的局限性,比如在处理高度抽象的概念、类比、隐喻方面有困难;会产生“幻觉”,编造不存在的对象和内容;不擅长处理复杂数学和推理问题;无法根据过往的聊天信息更新自身内容或纠正目前存在的问题等,所以使用 Claude 时我们还是要保持谨慎,对重要的信息要进行再次验证。

支持订阅Claude的虚拟卡,点击获取

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

轻量级代码仓库平台:CodeFever,解锁超速代码协作方式

发表于 2024-04-27

CodeFever_:_跨越传统边界,赋予团队无限协作力量 ,您的下一代开源Git服务平台 —— 精选真开源,释放新价值。

image.png

概览

CodeFever是由经验丰富的蒲公英团队经过精心研发和打磨推出的一款注重实效、高性能且极具灵活性的开源Git服务器系统。该平台摒弃了冗余繁复的设计元素,更侧重于构建一套坚固耐用、稳定可靠的底层基础架构,从而充分满足现代软件开发团队在日常工作中对版本控制、协同编程、权限配置以及自动化工作流等方面的核心需求。

在版本控制方面,CodeFever提供了直观且详尽的历史记录追溯能力,使得团队成员能够方便地查看、对比和回滚代码变更,有力保障项目的迭代演进过程有迹可循。

协同编辑功能上,CodeFever不仅允许多人同时在线编辑同一份代码,还通过分支管理机制以及全面的合并请求(Pull Request)功能,鼓励团队进行更为有序、高效的代码审阅与整合。

针对权限管理,CodeFever特别设计了一套细致入微的角色与权限模型,确保每一位团队成员都能根据其职责范围获得相应的访问和操作权限,有效避免了潜在的安全风险,同时也提高了团队间的信息透明度。

此外,CodeFever无缝对接各类流行的CI/CD工具和其他第三方服务,简化了从代码提交到最终部署的全流程管理,让自动化工作流的构建变得轻而易举。

image.png


主要功能

你可以在线体验:http://114.132.251.156/repositories

  • 一键部署

CodeFever采用简洁直观的安装部署方式,只需简单几步操作,无论是技术新手还是资深开发者,均能在短时间内搭建起专属的私有Git仓库服务器。无需复杂的环境配置和服务器设置,几分钟内就能完成从零到一的快速部署,极大地节省了团队宝贵的时间成本,使团队可以迅速投入到实质性的开发工作中去。

image.png

  • 强大协作

CodeFever深度优化了多人协作体验,多个团队成员可以实时在同一代码库中进行编辑和提交操作,大大提升了工作效率。系统内置完善的分支管理机制,使得团队可以根据不同任务或阶段创建独立的工作分支,每个分支之间互不影响。同时,CodeFever支持基于合并请求的功能,通过详细的代码审查和讨论,确保每次代码合并的质量和一致性。

image.png

  • 精细权限控制

为了确保代码资产的安全性及团队内部操作的透明公正,CodeFever建立了一套健全且灵活的角色与权限管理体系。团队管理员可以根据成员角色和职责分配不同的访问权限,包括但不限于读取、写入、合并代码等,做到权限细分至文件层级,真正实现了对代码资源的精细化管控。

image.png

  • 无缝集成

CodeFever巧妙地集成了各类主流CI/CD工具及第三方服务,如Jenkins、Travis CI、GitHub Actions等,让团队能够轻松实现从代码提交到测试、构建、部署的全程自动化。这种端到端的持续集成与交付流程不仅降低了人为错误的发生概率,还极大提高了软件开发生命周期的管理效率。

image.png

  • 轻量高效

考虑到大型代码库在处理速度和响应时间方面的挑战,CodeFever采用了先进的性能优化技术,使其在承载海量代码时仍能保持出色的运行效率。无论是克隆、拉取、推送还是搜索操作,CodeFever都能够确保操作流畅,带给开发者无延迟的优质使用体验。

image.png


信息

截至发稿概况如下:

  • 软件地址:github.com/PGYER/codef…
  • 软件协议:MIT
  • 编程语言:
语言 占比
PHP 64.7%
JavaScript 33.4%
Shell 0.6%
CSS 0.5%
HTML 0.4%
Go 0.3%
Other 0.1%
  • 收藏数量:2.6K

CodeFever以其务实的态度和强大的功能特性,正在逐步成为众多软件开发团队的理想选择。然而,在实际运用过程中,如何进一步提升CodeFever在大规模团队协作场景下的表现?又如何将更多先进的DevOps理念融入产品设计,使之更好地服务于企业的全栈开发流程?这些都是值得我们深入探讨和研究的问题,期待您加入CodeFever社区,共同推动这款开源Git服务器解决方案的发展与完善。

各位在使用 CodeFever 的过程中是否发现了什么问题?或者对 CodeFever 的功能有什么提议?热烈欢迎各位在评论区分享交流心得与见解!!!


声明:本文为辣码甄源原创,转载请标注”辣码甄源原创首发***“**并附带原文链接。***

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Poe!集齐4大 AI 聊天工具的神器,再也不同担心用不上

发表于 2024-04-27

自从 ChatGPT、New Bing、Claude等智能聊天机器人出现之后, 信息搜集、写作等工作变得前所未有的高效和便捷,ChatGPT 还可以扮演不同行业的专家,对我们在工作和生活中遇到的问题给出建议;或者扮演不同的历史人物角色和我们进行对话,让人与机器的沟通变得有趣和深刻。

由于各种原因可能有些朋友到现在都没有办法在 Open AI 官网上使用 ChatGPT,今天就为大家推荐一款 AI 聊天应用 Poe,它集成了 ChatGPT、GPT-4、Claude 等多款目前最强大的聊天机器人, 让我们轻松实现在一个平台上同时与多个不同的机器人进行对话

一、Poe 简介

网址直达: poe.com (用 Chrome 或者 Edge 浏览器打开)

Poe 是国外问答社区 Quora 推出的一款 AI 问答应用,Poe 并没有自己的大语言模型,而是将目前最强大的一些 AI 聊天机器人集合到一起,形成了一个 AI 问答互动平台,用户可以同时对多个聊天机器人提出问题,并快速获得相应的回答。

image.png

Poe 目前一共收录了 Sage、ChatGPT、GPT-4、Claude+、Claude-Instant-100k、Claude-Instant、Dragonfly 7 款聊天机器人。其中 ChatGPT 和 GPT-4 大家应该都非常熟悉了,它们是目前最强大的 2 款 AI 聊天机器人;Claude 之前我也有专门向大家介绍过,Poe 里有三个不同版本的 Claude 机器人,它们的区别如下:

① Claude+:功能强大完整,特别擅长创意写作和提供详细的回复,处理复杂困难任务的效果要明显优于 Claude-instant。

② Claude-Instant:也擅长处理创造性的写作任务,并且往往会给出更长、更深入的答案,适合对文本进行分析摘要、根据文档进行问答。在处理非英语语言时速度更快,效果也明显更好。

③ Claude-Instant-100K:可以接受 75000 个英文单词的超长上下文,这意味这我们可以发送给它数百页的资料,让它帮我们分析总结资料的内容。

点击获取支持ChatGPT、POE、Claude的虚拟卡

二、Poe 的注册

Poe 的注册十分简单,进入首页之后就是登录页面,我们可以选择手机、邮箱和 Apple ID 三种注册方法。注册完成后,就能直接与 ChatGPT 和 Claude-Instant 的机器人聊天,无需与 Open AI 或 Claude 的账号进行关联,这对没有办法注册 ChatGPT 账号的朋友来说是一种不错的解决方案。

image.png

Poe 除了网页版还有 iOS 和 Android 的 APP,如果你使用的是苹果手机,可以去应用商店搜索名称看看是否对应的程序,Android 则是需要去 Google Play 下载。

Poe 虽然一共有 7 个聊天机器人,但是能免费使用的只有前面提到的 ChatGPT、Claude-Instant 和 DragonFly 几个。没有开通会员的话,GPT-4 和 Claude+ 每天只有 1 条的免费提问额度,而 Claude-Instant-100k 则必须要付费后才能使用。

POE开通会员需要使用到虚拟卡,点击获取支持Poe的虚拟卡

微信图片_20240108105643.png

image.png

Poe 的会员收费换算成人民币大概是 150左右 元/月或 1300 元/年,比 GPT-4 的要贵一点。在 Poe 上我们可以同时和多个不同的 AI 智能机器人对话,但即使付费后也无法使用 GPT-4 的插件,所以大家可以根据自己实际需要选择。如果使用要求不高的话,Poe 里免费的 ChatGPT 和 Claude-Instant 也做够我们使用了。

除了这些 AI 聊天机器人,Poe 里还有其他各种好用的工具应用。我在它左侧工具栏的「Explore Bots」里发现了一个“Midjourney Photo Prompter”机器人,发给一段简单的描述给它,它就能返回一段详尽的文本提示词,帮助我们在 midjourney 生成更丰富精致的画面。

image.png

image.png
POE开通会员需要使用到虚拟卡,点击获取支持Poe的虚拟卡

如果你还没办法用上 ChatGPT, 可以尝试在 Poe 中使用。喜欢本期推荐的话记得点赞收藏支持一波,感谢!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android 15 新 API:内存追踪利器 Profil

发表于 2024-04-27

android15-base-profiling.png

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

我们都知道 Android Studio 里内置了 Profiler tool 供大家对 App 在 Memory、CPU、Network、Power 等角度进行 dump 和分析。

但如果一个内存相关的 bug 是运行时发生的,而且很难复现,那么后期就很难准确定位发生时的具体状况。

值得兴奋的是 Android 15 将直面这个痛点:引入了 ProfilingManager API,允许 app 对 Memory 进行动态的、随时随地的 dump。

生成的文件默认存在本地,也可以通过网络传递到 offboard,方便开发者事后回溯。

API 说明

ProfilingManager 主要提供了 3 个方法。

registerForAllProfilingResults

注册 profiling 请求的回调和执行的线程,需要如下两个参数:

Parameters Descriptions
executor Executor: 回调执行的线程池实例,不可为空
listener Consumer: 携带 profiling 结果的 listener 实例,不可为空

unregisterForAllProfilingResults

注销 profiling 请求的回调,如果没有指定 listener 参数的话,将移除所有 callback。

Parameters Descriptions
listener Consumer: 待移除的回调,null 的话移除所有

requestProfiling

请求进行一次 profiling 操作,有非常详细的参数可供设置:

Parameters Descriptions
profilingType int: profiling 操作的类型,主要包括 dump Java 堆的 PROFILING_TYPE_JAVA_HEAP_DUMP,dump 堆的 PROFILING_TYPE_HEAP_PROFILE, dump 栈的 PROFILING_TYPE_STACK_SAMPLING 和 dump 系统 trace 的PROFILING_TYPE_SYSTEM_TRACE,参数不能为空。
parameters Bundle: 携带请求额外的相关参数, 如果包含了未定义的参数类型,请求会失败,在 callback 当中以 ERROR_FAILED_INVALID_REQUEST 结果进行返回,参数可为空。
tag String: 回来识别 dump 输出的 tag 标签,其中的前 20 个字符将会以小写的形式拼接到 dump 文件名中,参数可为空
cancellationSignal CancellationSignal: 支持请求侧用来取消 dump 的 cancellation 实例,如果 dump 结果已出来的话,会被返回。参数可为空,此时将执行系统默认的超时时间,之后结束 dump。
executor Executor: 回调执行的线程池实例,参数可为空。但如果没有其他 executor 注册的话,该请求会被无视。
listener Consumer: 监听操作结果的实例,registerForAllProfilingResults() 注册的 callback 同样也会被回调,参数可为空。但如果没有其他 listener 注册的话,该请求会被无视。

需要说明的是:

  • 很多时候,并不推荐直接使用该 API 进行 dump,相反可以采用 androidx 中封装好的高层级接口进行请求。该接口内部会依据可用选项和简化的参数进行正确地请求。
  • 并非所有情况都会得到响应
  • 需要同时考虑 result 的监听和执行它的线程池两个参数,要么都设置,要么都不设置交给 registerForAllProfilingResults() 一起设置

ProfilingResult

在上述提到的 registerForAllProfilingResults() 里会回调 ProfilingResult 参数过来,它用来封装单次请求 profiling 操作的结果。

主要提供了这几个方法来获得信息:

  • getErrorCode():获取 profiling 请求失败的原因,如果成功的话,值为 ERROR_NONE 即 0,其他的还有 ERROR_FAILED_RATE_LIMIT_SYSTEM 等值
  • getErrorMessage():获取额外的失败信息
  • getTag():请求时传入的参数 tag,方便回溯
  • getResultFilePath():获取 profiling 结果文件的路径

实战

留意一下,需要将 Android 15 的 SDK 升级到 revision 3 才可以看到该 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
kotlin复制代码     class ProfilingActivity : AppCompatActivity() {
     private val singleThreadExecutor = Executors.newSingleThreadExecutor()
 
     private val profilingResultConsumer = Consumer<ProfilingResult> {
         Log.d(TAG_PROFILING, "accept profilingResult:${it.printProfilingResult()}")
    }
 
     private val profilingManager by lazy {
         getSystemService(ProfilingManager::class.java)
    }
 
     override fun onCreate(savedInstanceState: Bundle?) {
        ...
         binding.dump.setOnClickListener {
             Log.d(TAG_PROFILING, "button dump tapped")
 
             profilingManager.registerForAllProfilingResults(
                 singleThreadExecutor,
                 profilingResultConsumer
            )
        }
 
         binding.request.setOnClickListener {
             Log.d(TAG_PROFILING, "button request tapped")
 
             profilingManager.requestProfiling(
                 // ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE,
                 // ProfilingManager.PROFILING_TYPE_JAVA_HEAP_DUMP,
                 // ProfilingManager.PROFILING_TYPE_STACK_SAMPLING,
                 ProfilingManager.PROFILING_TYPE_HEAP_PROFILE,
                 null,
                 "TEST_FOR_PROFILING_MANAGER",
                 null,
                 singleThreadExecutor,
                 profilingResultConsumer
            )
        }
 
         binding.stop.setOnClickListener {
             Log.d(TAG_PROFILING, "button stop tapped")
 
             profilingManager.unregisterForAllProfilingResults(profilingResultConsumer)
        }
    }
 }

可以看到我们在代码里设置的 tag 为 *”TEST_FOR_PROFILING_MANAGER”* 。

转存失败,建议直接上传图片文件
我们点击 “register dump profile” button 开始注册回调,然后点击 “request dump profile” button 开始请求。

看下 Profiling 的 log 输出。

点击完 request 之后需要等待一段时间(系统默认的 dump 超时为 120s 左右)才会看到 dump 结果。

注意,不要重复点击,否则会收到如下错误的 callback。其中 3 对应的是 ERROR_FAILED_PROFILING_IN_PROGRESS,表示仍在 dump 中,该重复请求被拒绝。

ProfilingResult{errorCode:3 errorMessage:null resultFilePath:null tag:TEST_FOR_PROFILING_MANAGER}

下面我们以如下 4 种 dump 类型看下具体的 ProfilingResult 输出内容。

PROFILING_TYPE_JAVA_HEAP_DUMP

1
bash复制代码04-21 19:37:32.220  7184  7270 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-37-26.perfetto-java-heap-dump tag:TEST_FOR_PROFILING_MANAGER}

可以看到成功输出了 Java heap 的 dump 文件,并且咱们的 tag 被拼接到了文件名中,而且将_删除并限制了 20 的长度。

所以咱们设置的 tag 最好不要包含字母以外的字符,并且不要过长,不然不方便定位 tag。

PROFILING_TYPE_HEAP_PROFILE

1
bash复制代码04-21 19:47:16.810  7474  7552 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-45-11.perfetto-heap-profile tag:TEST_FOR_PROFILING_MANAGER}

和上面的 Java dump 一样,成功输出了 heap 全量的 dump 文件。

PROFILING_TYPE_STACK_SAMPLING

1
bash复制代码04-21 19:53:01.043  7704  7790 D Profiling: accept profilingResult:ProfilingResult{errorCode:0 errorMessage:null resultFilePath:/data/user/0/com.ellison.osvdemo/files/profiling/profile_testforprofilingmana_2024-04-21-19-51-55.perfetto-stack-sample tag:TEST_FOR_PROFILING_MANAGER}

dump stack 的记录也是一样,不再赘述。

PROFILING_TYPE_SYSTEM_TRACE

1
bash复制代码04-21 15:43:33.926  4730  4775 D Profiling: accept profilingResult:ProfilingResult{errorCode:7 errorMessage:Trace is not supported until redaction lands resultFilePath:null tag:null}

和前面 3 中 type 不同,这种 dump 系统 trace 的请求总是会直接失败。

ProfilingResult 结果对应的 errorCode 为 7,常量名称为 ERROR_FAILED_INVALID_REQUEST,表示请求失败了:这是一个不合法的 ProfilingRequest。

The request failed due to invalid ProfilingRequest.

单从这个 error 定义来看,压根不知道问题出在哪。

在 API 章节里我们提到过,如果调用 requestProfiling() 时传递的 Bundle 参数包含了不支持的 key-value,会造成 ERROR_FAILED_INVALID_REQUEST 错误。

但可以看到:实际上咱们的 DEMO 代码里啥 bundle 都没携带,所以应该不是这个原因。

好在,ProfilingRequest 结果还打携带了 errorMessage,其内容为:Trace is not supported until redaction lands。

笔者尝试依据该 message 找到蛛丝马迹,但无论在 Android 官网,还是在 Android 源码里,抑或是在 AOSP issue 的首页上,都没找到合相关的记录。

我猜测是 PROFILING_TYPE_SYSTEM_TRACE 需要某个 DOC 里没说明的 Bundle 参数,造成了失败。当然也有可能是 beta 版阶段的系统 bug。

后续再看。

perfetto 支持

以 PROFILING_TYPE_JAVA_HEAP_DUMP 的文件为例,从 data 目录里 pull 之后,在 ui.perfetto.dev 中打开,可以进行分析。

转存失败,建议直接上传图片文件
DEMO 源码


AndroidOSVDemo

结语

ProfilingManager API 允许 App 直接进行各种 Memory 数据的 dump,相信这能在一定程度上帮助开发者进行内存相关问题的回溯,不再像以前那样被动。

从上面的实战也能看到目前部分功能还存在些问题,期待正式版本能够修复。

感兴趣的朋友可以在 Android 15 稳定版的时候再行体验。

笔者猜测因为 Memory 是最高优的关注点,所以目前 ProfilingManager 只支持 dump Memory。但我相信随着该 API 的成熟和普及,以及确实有 dump 其他点强劲需求的话,肯定会逐步支持更多 dump 的内容。

参考资料

  • ProfilingManager
  • ProfilingResult

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

JavaScript动手实现2FA动态认证码 前言 2FA认

发表于 2024-04-27

前言

好久没登陆Github了,发现Github需要经过2FA认证才能正常使用功能。Github推荐用可以用认证软件来扫描二维码来生成动态密码,或者利用secretKey专属密钥来生成动态密码。

我试了试用github推荐的软件,但是他们要么要付费,要么国内手机号无法注册,行不通呀根本行不通。

所以我就在百度里面搜免费2FA动态密码生成器,真的找到了好心人提供的生成器,输入动态密码后丝滑进入Github。

2FA认证是什么

2FA,2 Factor Authentication,双因子验证/双因素验证,是一种安全密码验证方式。区别于传统的密码验证,由于传统的密码验证是由一组静态信息组成,如:字符、图像、手势等,很容易被获取,相对不安全。2FA是基于时间、历史长度、实物(信用卡、SMS手机、令牌、指纹)等自然变量结合一定的加密算法组合出一组动态密码,一般每60秒刷新一次。不容易被获取和破解,相对安全。 ————以上来自百度百科

在Github这里,2FA采用的双因子一般是时间和个人密钥。Github会根据时间和账号的密钥比对我们填写的动态密码的一致性,一致才能通过验证。

PS:

  1. Github中动态密码长度为6位;
  2. Github中动态密码的有效期为30s,也就是说一次性密码每半分钟会变化。

TOTP和HOTP

TOTP(Time-Based One-Time Password Algorithm) 是基于时间的一次性密码算法,可以根据时间因子和密钥生成一次性密码。TOTP算法是基于HOTP算法的,HOTP算法是根据计数器(移动因子)和密钥生成一次性密码。

HOTP

算法公式:HOTP(K,C) = Trancate(HMAC-SHA-1(K,C))

  • K:密钥,两端之间共享,不同的用户密钥应该保证唯一且不同
  • C:计数器(移动因子),是一个8字节的值
  • Digit:一次性密码的位数

HOTP算法是根据计数器(移动因子)和密钥生成一次性密码。

在经过 HMAC-SHA-1算法用 密钥K 加密 计数器C 后,我们会得到20个字节的十六进制字符串。

Trancate算法会对加密得到的十六进制字符串进行处理。首先会选取最后1个字节的16进制串,将其转化为十进制数字offset; 然后会从offset开始选取4个字节的字符串,将其转化为十进制数字;最后会根据 Digit位数 对十进制数字取模,获取最后几位数字,如果位数不够就在前方补0。

image.png

TOTP

算法公式:TOTP(K,T) = HOTP(K,(T-T0)/X)

  • K:密钥,两端之间共享,不同的用户密钥应该保证唯一且不同
  • T:当前时间戳(以秒为单位)
  • T0: 初始时间戳,一般为0
  • X:时间步长,一般为30s、60s,这里是30s
  • Digit:一次性密码的位数

从TOTP算法公式可以看到,我们只需要获取时间因子,将时间因子替换HOTP中的计数器即可。

JavaScript实现代码

第一步:密钥解码

Github 2FA认证提供的是经过base32(RFC3548)编码后的secretKey,所以在这里我们需要对其进行解码,解码的encoding类型选择RFC3548。

因为Node crypto中的加密的参数需要传递binaryLike形式的值,所以解码的内容我们要用Buffer来进行接收。

1
2
js复制代码const secret = 'Github 2FA认证提供的经过base32(RFC3548)编码后的secretKey'; // 这是你的秘钥,需要保密
const secretKey = Buffer.from(base32Decode(secret, 'RFC3548'));

第二步:实现HOTP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
js复制代码// 生成HMAC-based One-Time Password (HOTP)
function generateHOTP(key, counter, digits = 6) {
// 将计数器转换为8字节的Buffer
const counterBuffer = Buffer.alloc(8);
counterBuffer.writeBigInt64BE(BigInt(counter), 0);

// 使用HMAC-SHA1算法计算HMAC
const hmac = crypto.createHmac('sha1', key).update(counterBuffer).digest();

// 获取HMAC的最后一个字节的低四位作为偏移量
const offset = hmac[hmac.length - 1] & 0x0F;

// 将动态密码的部分转换为整数
let dynamicPassword = (hmac[offset] & 0x7F)<< 24
| (hmac[offset + 1] & 0xFF) << 16
| (hmac[offset + 2] & 0xFF) << 8
| (hmac[offset + 3] & 0xFF);

// 限制密码长度
dynamicPassword = dynamicPassword % Math.pow(10, digits);

// 根据指定的位数格式化密码
return dynamicPassword.toString().padStart(digits, '0');
}

第三步:获取和处理时间戳

1
2
3
4
5
js复制代码// 获取当前时间的时间戳,并以时间步长划分为计数器
function getCounter(timeStep = 30) {
const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位:秒)
return Math.floor(currentTime / timeStep);
}

第四步:得到TOTP动态密码

1
2
3
4
5
6
js复制代码// 生成 TOTP
function generateTOTP(key, timeStep = 30, digits = 6) {
const counter = getCounter(timeStep);
console.log(counter)
return generateHOTP(key, counter, digits);
}

全部代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
js复制代码const crypto = require('crypto');
const base32Decode = require('base32-decode')
// 生成HMAC-based One-Time Password (HOTP)
function generateHOTP(key, counter, digits = 6) {
// 将计数器转换为8字节的Buffer
const counterBuffer = Buffer.alloc(8);
counterBuffer.writeBigInt64BE(BigInt(counter), 0);

// 使用HMAC-SHA1算法计算HMAC
const hmac = crypto.createHmac('sha1', key).update(counterBuffer).digest();

// 获取HMAC的最后一个字节的低四位作为偏移量
const offset = hmac[hmac.length - 1] & 0x0F;

// 将动态密码的部分转换为整数
let dynamicPassword = (hmac[offset] & 0x7F)<< 24
| (hmac[offset + 1] & 0xFF) << 16
| (hmac[offset + 2] & 0xFF) << 8
| (hmac[offset + 3] & 0xFF);

// 限制密码长度
dynamicPassword = dynamicPassword % Math.pow(10, digits);

// 根据指定的位数格式化密码
return dynamicPassword.toString().padStart(digits, '0');
}

// 获取当前时间的时间戳,并以时间步长划分为计数器
function getCounter(timeStep = 30) {
const currentTime = Math.floor(Date.now() / 1000); // 获取当前时间戳(单位:秒)
return Math.floor(currentTime / timeStep);
}

// 生成 TOTP
function generateTOTP(key, timeStep = 30, digits = 6) {
const counter = getCounter(timeStep);
console.log(counter)
return generateHOTP(key, counter, digits);
}

// 示例
const secret = 'Github 2FA认证提供的经过base32(RFC3548)编码后的secretKey'; // 这是你的秘钥,需要保密
const secretKey = Buffer.from(base32Decode(secret, 'RFC3548'));
const totp = generateTOTP(secretKey);
console.log('Generated TOTP:', totp);

参考文档

  1. blog.csdn.net/weixin_4279…
  2. 2FA百度百科

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Android Native线程找不到class的原因

发表于 2024-04-27

在native创建线程,想调用java层时,通常会去获取到java层class,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码// native线程
void ThreadTest::callJava(void *data) {
   ThreadTest *threadTest = (ThreadTest *) data;
   JNIEnv *env= nullptr;
   threadTest->vm->AttachCurrentThread(&env, nullptr);
   // 查找class
   jclass clazz = env->FindClass("com/hyc/jni_demo/NativeCall");
                                         className);
   jmethodID methodId = env->GetStaticMethodID(clazz, "callStatic", "()I");
   jint result = env->CallStaticIntMethod(clazz, methodId);
   LOGD("result1: %d", result);
   jmethodID methodId2 = env->GetMethodID(clazz, "callNormal", "()I");

   jfieldID field = env->GetStaticFieldID(clazz, "INSTANCE", "Lcom/hyc/jni_demo/NativeCall;");
   jobject nativeCall = env->GetStaticObjectField(clazz, field);
   jint result2 = env->CallIntMethod(nativeCall, methodId2, nullptr);
   LOGD("result2: %d", result2);
   threadTest->vm->DetachCurrentThread();

}

但是却发生了如下崩溃,报Didn’t find class “com.hyc.jni_demo.NativeCall” ,下面我们来探究一下出现这个问题的原因。

1
2
3
4
typescript复制代码Abort message: 'JNI DETECTED ERROR IN APPLICATION: JNI GetStaticMethodID called with pending exception java.lang.ClassNotFoundException: Didn't find class "com.hyc.jni_demo.NativeCall" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /system/lib64, /system_ext/lib64]]
                                                                                                     at java.lang.Class dalvik.system.BaseDexClassLoader.findClass(java.lang.String) (BaseDexClassLoader.java:259)
                                                                                                     at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String, boolean) (ClassLoader.java:379)
                                                                                                     at java.lang.Class java.lang.ClassLoader.loadClass(java.lang.String) (ClassLoader.java:312)

在这之前我们还需要了解一下什么是JavaVM和JNIEnv。

JavaVM和JNIEnv

JNIEnv可以单纯的理解为java层和native层之间的桥梁,每个java线程都有一个自己的JNIEnv。而JavaVM是管理JNIEnv的,它它可以创建新的JNIEnv,获取当前线程的JNIEnv,以及销毁JNIEnv。

JavaVM创建

JavaVM在虚拟机里面只有一个实例,JavaVM在虚拟机启动的时候创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
arduino复制代码// 调用栈
// art/runtime/runtime.cc Runtime::Init
// art/runtime/runtime.cc Runtime::Create
// art/runtime/jni/java_vm_ext.cc JNI_CreateJavaVM
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::startVm
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::start
// frameworks/base/cmds/app_process/app_main.cpp main


bool Runtime::Init(RuntimeArgumentMap&& runtime_options_in) {
........
 java_vm_ = JavaVMExt::Create(this, runtime_options, &error_msg);
........  
}

std::unique_ptr<JavaVMExt> JavaVMExt::Create(Runtime* runtime,
                                            const RuntimeArgumentMap& runtime_options,
                                            std::string* error_msg) NO_THREAD_SAFETY_ANALYSIS {
 std::unique_ptr<JavaVMExt> java_vm(new JavaVMExt(runtime, runtime_options, error_msg));
 if (java_vm && java_vm->globals_.IsValid() && java_vm->weak_globals_.IsValid()) {
   return java_vm;
}
 return nullptr;
}

JavaVMExt::JavaVMExt(Runtime* runtime,
                    const RuntimeArgumentMap& runtime_options,
                    std::string* error_msg)
  : runtime_(runtime),
    ..................
     // 配置接口    
     unchecked_functions_(&gJniInvokeInterface),
    ..................
     old_allocation_tracking_state_(false) {
 functions = unchecked_functions_;
 SetCheckJniEnabled(runtime_options.Exists(RuntimeArgumentMap::CheckJni) || kIsDebugBuild);
}

const JNIInvokeInterface gJniInvokeInterface = {
 nullptr,  // reserved0
 nullptr,  // reserved1
 nullptr,  // reserved2
 JII::DestroyJavaVM,
 JII::AttachCurrentThread,
 JII::DetachCurrentThread,
 JII::GetEnv,
 JII::AttachCurrentThreadAsDaemon
};

// App层可以调用到的接口
struct _JavaVM {
   const struct JNIInvokeInterface* functions;

#if defined(__cplusplus)
   jint DestroyJavaVM()
  { return functions->DestroyJavaVM(this); }
   jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
  { return functions->AttachCurrentThread(this, p_env, thr_args); }
   jint DetachCurrentThread()
  { return functions->DetachCurrentThread(this); }
   jint GetEnv(void** env, jint version)
  { return functions->GetEnv(this, env, version); }
   jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
  { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }
#endif /*__cplusplus*/
};
SystemClassLoader的创建

在创建JavaVM后,会创建SystemClassLoader,并设置给JavaVM,这个就是在native线程,我们能拿到的默认ClassLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
scss复制代码// 调用栈
// art/runtime/runtime.cc Runtime::Start
// art/runtime/jni/java_vm_ext.cc JNI_CreateJavaVM
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::startVm
// frameworks/base/core/jni/AndroidRuntime.cpp AndroidRuntime::start
// frameworks/base/cmds/app_process/app_main.cpp main

bool Runtime::Start() {
  .............
   system_class_loader_ = CreateSystemClassLoader(this);
  .............
}


static jobject CreateSystemClassLoader(Runtime* runtime) {
 if (runtime->IsAotCompiler() && !runtime->GetCompilerCallbacks()->IsBootImage()) {
   return nullptr;
}

 ScopedObjectAccess soa(Thread::Current());
 ClassLinker* cl = Runtime::Current()->GetClassLinker();
 auto pointer_size = cl->GetImagePointerSize();

 StackHandleScope<2> hs(soa.Self());
 Handle<mirror::Class> class_loader_class(
     hs.NewHandle(soa.Decode<mirror::Class>(WellKnownClasses::java_lang_ClassLoader)));
 CHECK(cl->EnsureInitialized(soa.Self(), class_loader_class, true, true));
 // 获取到ClassLoader的getSystemClassLoader方法
 ArtMethod* getSystemClassLoader = class_loader_class->FindClassMethod(
     "getSystemClassLoader", "()Ljava/lang/ClassLoader;", pointer_size);
 CHECK(getSystemClassLoader != nullptr);
 CHECK(getSystemClassLoader->IsStatic());
 // 执行getSystemClassLoader方法
 JValue result = InvokeWithJValues(soa,
                                   nullptr,
                                   getSystemClassLoader,
                                   nullptr);
 JNIEnv* env = soa.Self()->GetJniEnv();
 // 获取到local Ref
 ScopedLocalRef<jobject> system_class_loader(env, soa.AddLocalReference<jobject>(result.GetL()));
 CHECK(system_class_loader.get() != nullptr);
 // 保存
 soa.Self()->SetClassLoaderOverride(system_class_loader.get());

 Handle<mirror::Class> thread_class(
     hs.NewHandle(soa.Decode<mirror::Class>(WellKnownClasses::java_lang_Thread)));
 CHECK(cl->EnsureInitialized(soa.Self(), thread_class, true, true));

 ArtField* contextClassLoader =
     thread_class->FindDeclaredInstanceField("contextClassLoader", "Ljava/lang/ClassLoader;");
 CHECK(contextClassLoader != nullptr);

 // We can't run in a transaction yet.
 contextClassLoader->SetObject<false>(
     soa.Self()->GetPeer(),
     soa.Decode<mirror::ClassLoader>(system_class_loader.get()).Ptr());
 // 返回global ref
 return env->NewGlobalRef(system_class_loader.get());
}

java层逻辑,根据启动jvm传入参数创建PathClassLoader。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typescript复制代码    public static ClassLoader getSystemClassLoader() {
       return SystemClassLoader.loader;
  }
   static private class SystemClassLoader {
       public static ClassLoader loader = ClassLoader.createSystemClassLoader();
  }

  private static ClassLoader createSystemClassLoader() {
       String classPath = System.getProperty("java.class.path", ".");
       String librarySearchPath = System.getProperty("java.library.path", "");

       // String[] paths = classPath.split(":");
       // URL[] urls = new URL[paths.length];
       // for (int i = 0; i < paths.length; i++) {
       // try {
       // urls[i] = new URL("file://" + paths[i]);
       // }
       // catch (Exception ex) {
       // ex.printStackTrace();
       // }
       // }
       //
       // return new java.net.URLClassLoader(urls, null);

       // TODO Make this a java.net.URLClassLoader once we have those?
       return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
  }
查找class

在查找Class时,先去获取当前的ClassLoader:

查找当前调用的java方法,如果能拿到方法,那么返回方法所在的Class的ClassLoader,如果为空,那么使用虚拟机创建时创建的SystemClassLoader。此时是native线程刚绑定jvm虚拟机,所以方法为空,返回SystemClassLoader。

而这个ClassLoader时在Zygote进程创建时,传入虚拟机配置参数路径创建的PathClassLoader,只会包含系统相关路径,不会有上层App的dex,所以我们就不能通过这个ClassLoader获取到我们自己的Class,理所当然出现上面那个崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
scss复制代码  static jclass FindClass(JNIEnv* env, const char* name) {
   CHECK_NON_NULL_ARGUMENT(name);
   Runtime* runtime = Runtime::Current();
   ClassLinker* class_linker = runtime->GetClassLinker();
   std::string descriptor(NormalizeJniClassDescriptor(name));
   ScopedObjectAccess soa(env);
   ObjPtr<mirror::Class> c = nullptr;
   if (runtime->IsStarted()) {
     StackHandleScope<1> hs(soa.Self());
     // 查找当前class loader  
     Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader<kEnableIndexIds>(soa)));
     c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
  } else {
     c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
  }
   return soa.AddLocalReference<jclass>(c);
}

template<bool kEnableIndexIds>
static ObjPtr<mirror::ClassLoader> GetClassLoader(const ScopedObjectAccess& soa)
   REQUIRES_SHARED(Locks::mutator_lock_) {
 // 查找当前调用的java方法,此时是native线程刚绑定jvm虚拟机,所以为空
 ArtMethod* method = soa.Self()->GetCurrentMethod(nullptr);
 // If we are running Runtime.nativeLoad, use the overriding ClassLoader it set.
 if (method ==
     jni::DecodeArtMethod<kEnableIndexIds>(WellKnownClasses::java_lang_Runtime_nativeLoad)) {
   return soa.Decode<mirror::ClassLoader>(soa.Self()->GetClassLoaderOverride());
}
 // If we have a method, use its ClassLoader for context.
 // 如果不为空,那么获取方法所在的class的class loader  
 if (method != nullptr) {
   return method->GetDeclaringClass()->GetClassLoader();
}
 // 如果为空,那么获取SystemClassLoader
 ObjPtr<mirror::ClassLoader> class_loader =
     soa.Decode<mirror::ClassLoader>(Runtime::Current()->GetSystemClassLoader());
 if (class_loader != nullptr) {
   return class_loader;
}
 // See if the override ClassLoader is set for gtests.
 class_loader = soa.Decode<mirror::ClassLoader>(soa.Self()->GetClassLoaderOverride());
 if (class_loader != nullptr) {
   // If so, CommonCompilerTest should have marked the runtime as a compiler not compiling an
   // image.
   CHECK(Runtime::Current()->IsAotCompiler());
   CHECK(!Runtime::Current()->IsCompilingBootImage());
   return class_loader;
}
 // Use the BOOTCLASSPATH.
 return nullptr;
}

结合上面的代码,我们验证一下加载系统的Class是否可行,发现时可以的,没有报错。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码void ThreadTest::callJava(void *data) {
   ThreadTest *threadTest = (ThreadTest *) data;
   JNIEnv *env= nullptr;
   threadTest->vm->AttachCurrentThread(&env, nullptr);
   jclass clazz = env->FindClass("java/lang/Object");
   jmethodID methodId = env->GetMethodID(clazz, "<init>", "()V");
   jobject obj = env->NewObject(clazz, methodId);
   jmethodID methodId2 = env->GetMethodID(clazz, "toString", "()Ljava/lang/String;");
   jstring result = (jstring)env->CallObjectMethod(obj, methodId2);
   const char *str = env->GetStringUTFChars(result, nullptr);
}
// 打印 result: java.lang.Object@e7b9a76
如何解决?

知道出现的原因后就很好解决了,我们不能在一个线程调用另一个线程的JNIEnv,所以就不能缓存有正确ClassLoader的JNIEnv,然后调用其FindClass方法。我们在java线程中初始化时就去获取出相应的jclass,进行缓存。这种方法不是很通用,我们可以在正确的线程下对ClassLoader进行缓存,然后再在另一个线程调用这个ClassLoader的loadClass方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
ini复制代码extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
   // 此时运行在一个java线程中(真正绑定了jvm环境的线程),其ClassLoader是调用loadLibrary所在的Class对应的ClassLoader,在这里就是加载MainActivity的ClassLoader
   JNIEnv* env = nullptr;
   if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
       return JNI_ERR;
  }
   threadTest = new ThreadTest(vm);
   threadTest->initClassLoader(env);
   return JNI_VERSION_1_6;
}

void ThreadTest::initClassLoader(JNIEnv *env) {
   jclass clazz = env->FindClass("com/hyc/jni_demo/TestClassLoader");
   jmethodID methodId = env->GetStaticMethodID(clazz, "getClassLoader", "()Ljava/lang/ClassLoader;");
   jobject loader = env->CallStaticObjectMethod(clazz, methodId);
   classLoader = env->NewGlobalRef(loader);
}

void ThreadTest::callJava(void *data) {
   ThreadTest *threadTest = (ThreadTest *) data;
   JNIEnv *env= nullptr;
   threadTest->vm->AttachCurrentThread(&env, nullptr);

   jclass classLoaderClass = env->GetObjectClass(threadTest->classLoader);
   jstring className = env->NewStringUTF("com.hyc.jni_demo.NativeCall");
   jmethodID loadClassMethod = env->GetMethodID(classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
   jclass clazz = (jclass)env->CallObjectMethod(threadTest->classLoader, loadClassMethod,
                                                className);
   jmethodID methodId = env->GetStaticMethodID(clazz, "callStatic", "()I");
   jint result = env->CallStaticIntMethod(clazz, methodId);
   LOGD("result1: %d", result);
   jmethodID methodId2 = env->GetMethodID(clazz, "callNormal", "()I");

   jfieldID field = env->GetStaticFieldID(clazz, "INSTANCE", "Lcom/hyc/jni_demo/NativeCall;");
   jobject nativeCall = env->GetStaticObjectField(clazz, field);
   jint result2 = env->CallIntMethod(nativeCall, methodId2, nullptr);
   LOGD("result2: %d", result2);
   threadTest->vm->DetachCurrentThread();

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

window 安装大模型 chatglm-6b 一、 前言

发表于 2024-04-27

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:

  1. 了解大厂经验
  2. 拥有和大厂相匹配的技术等

希望看什么,评论或者私信告诉我!

一、 前言

有大模型以来一直想尝试通过本地安装大模型,主要的原因是GPT众所众知的原因没有办法通过 API 访问,而国内的所谓的开发平台一方面要么不兼容 openai 的 api 要么就是价格不够友好,要么两者兼有,另外的话,开发平台也有所谓的隐私问题。另外公司内部虽然有已经部署好的开源大模型,但相应的服务中的 Temperautre 或者 Top 都已经被固定了,用起来特别没意思。

所以想自己搞一套,喜欢自己说了算。

二、准备工作

2.1 电脑

在这里插入图片描述
这是我工作使用的电脑配置,16G内存,Intel(R) 集成显卡。这里要吐槽一下,程序员千万不要使用 window,随便装点什么东西就很麻烦,用不了 mac 就用 linux,我这是公司电脑,后悔没早点装 unbantu 系统

2.2 组件安装

VS studio 2022
在这里插入图片描述

cmake

TDM-GCC,注意,安装的时候直接选择全部安装就好。安装完在cmd中运行”gcc -v”测试是否成功即可( 我的电脑需要重启后才能执行 gcc -v )

2.3 开始安装

因为公司电脑性能不行,所以我选择了 ChatGLM-6B,另外ChatGLM-6B完整版本需要13GB显存做推理,但是INT4量化版本只需要6GB显存即可运行,这里选择 INT4量化版

2.3.1下载官方代码,安装Python依赖的库

首先,我们需要从GitHub上下载ChatGLM的requirements.txt来帮助我们安装依赖的库。大家只需要在GitHub上下载requirements.txt即可。下载地址:github.com/THUDM/ChatG…
在这里插入图片描述这个文件记录了ChatGLM-6B依赖的Python库及版本,大家点击右上角Code里面有Download ZIP,下载到本地解压之后就能获取这个文件。然后执行如下命令即可:

1
python复制代码  pip install -r requirements.txt

注意,这是从cmd进入到requirements.txt文件所在的目录执行的结果,这部分属于Python基础,就不赘述了。

需要注意的是,ChatGLM依赖HuggingFace的transformers库,尽管官方说:

1
复制代码使用 pip 安装依赖:pip install -r requirements.txt,其中 transformers 库版本推荐为 4.27.1,但理论上不低于 4.23.1 即可。

但是实际上,必须是4.27.1及以上的版本才可以,更低版本的transformers会出现如下错误:

AttributeError: ‘Logger’ object has no attribute “‘warning_once’”

所以,一定要查看自己的transformers版本是否正确。

另外,ChatGLM-6B依赖torch,如果你有GPU,且高于6G内存,那么建议部署GPU版本,但是需要下载支持cuda的torch,而不是默认的CPU版本的torch。具体可参考 :
关于 AssertionError: Torch not compiled with CUDA enabled 问题

2.3.2 下载INT4量化后的预训练结果文件

在上述的依赖环境安装完毕之后,大家接下来就要下载预训练结果。

INT4量化的预训练文件下载地址:huggingface.co/THUDM/chatg…,需要魔法,如果没有魔法,可去 modelscope 搜索合适的版本

需要注意的是,在GitHub上,官方提供了模型在清华云上的下载地址,但是那个只包含预训练结果文件,即bin文件,但实际上ChatGLM-6B的运行需要模型的配置文件,即config.json等,如下图所示:

因此建议大家全部从HuggingFace上下载所有文件到本地。上述文件全部下载之后保存到本地的一个目录下即可,比如:

D:\LLM

2.3.3 Windows+CPU部署方案

我的机器不支持CUDA,所以我们直接来看CPU方式

运行部署CPU版本的INT4量化的ChatGLM-6B模型

CPU版本量化模型的代码与GPU版本稍微有点差异,代码如下:

1
2
3
4
5
6
python复制代码from transformers import AutoTokenizer, AutoModel
tokenizer = AutoTokenizer.from_pretrained("D:\LLM\chatglm-6b-int4", trust_remote_code=True, revision="")
model = AutoModel.from_pretrained("D:\LLM\chatglm-6b-int4",trust_remote_code=True, revision="").float()
model = model.eval()
response, history = model.chat(tokenizer, "你好", history=[])
print(response)

一般都会报错

在运行中遇到了如下错误提示:

1
2
3
4
yaml复制代码No compiled kernel found.
Compiling kernels : C:\Users\在、xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.c
Compiling gcc -O3 -fPIC -pthread -fopenmp -std=c99 C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.c -shared -o C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.so
Kernels compiled : C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\quantization_kernels_parallel.so

CPU版本的ChatGLM-6B部署比GPU版本稍微麻烦一点,主要涉及到一个kernel的编译问题。

在安装之前,除了上面需要安装好requirements.txt中所有的Python依赖外,torch需要安装好正常的CPU版本即可。

安装这个主要是为了编译之前下载的文件中的quantization_kernels.c和quantization_kernels_parallel.c这两个文件。如果大家

那么就是这两个文件编译出问题了。那么就需要我们手动去编译这两个文件:

在C:\Users\xxx.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\本地目录下进入cmd,运行如下两个编译命令:

1
2
ini复制代码gcc -fPIC -pthread -fopenmp -std=c99 quantization_kernels.c -shared -o quantization_kernels.so
gcc -fPIC -pthread -fopenmp -std=c99 quantization_kernels_parallel.c -shared -o quantization_kernels_parallel.so

如下图所示即为运行成功

然后就可以在C:\Users\xxx.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\目录下看到下面两个新的文件:quantization_kernels_parallel.so和quantization_kernels.so。说明编译成功,后面我们手动载入即可。

在原来代码的基础上添加

1
python复制代码model = model.quantize(bits=4, kernel_file="C:\Users\xxx\.cache\huggingface\modules\transformers_modules\chatglm-6b-int4\\quantization_kernels.so")

一行手动加载的内容。

接下来你就可以看到如下界面:
我这里把 quantization_kernels.so 放在了 D:\LLM\chatglm2-6b-int4 下面了

输出结果

你好👋!我是人工智能助手 ChatGLM2-6B,很高兴见到你,欢迎问我任何问题。

也就是CPU版本的ChatGLM-6B运行成功了!但很慢,所以可以考虑 CPP 加速!待后续输出!

三、总结

通过本文,读者可以了解到如何在个人电脑上部署ChatGLM-6B的INT4量化版本,同时作者也分享了他在安装过程中遇到的问题和解决方法,帮助读者顺利完成安装并运行大模型

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【AI大模型应用开发】【RAG评估】0 综述:一文了解RA

发表于 2024-04-27

大家好,我是同学小张,日常分享AI知识和实战案例,欢迎 点赞 + 关注 👏,持续学习,持续干货输出。


前面我们学习了RAG的基本框架并进行了实践,我们也知道使用它的目的是为了改善大模型在一些方面的不足:如训练数据不全、无垂直领域数据、容易出现幻觉等。那么如何评估RAG的效果呢?本文我们来了解一下。

推荐前置阅读

  • 【AI大模型应用开发】3. RAG初探 - 动手实现一个最简单的RAG应用
  • 【AI大模型应用开发】3.2 RAG实战 - RAG应用+UI实现加载本地文件并对话
  1. RAG效果评估的必要性

  • 评估出RAG对大模型能力改善的程度
  • RAG优化过程,通过评估可以知道改善的方向和参数调整的程度
  1. RAG评估方法

1.1 人工评估

最Low的方式是进行人工评估:邀请专家或人工评估员对RAG生成的结果进行评估。他们可以根据预先定义的标准对生成的答案进行质量评估,如准确性、连贯性、相关性等。这种评估方法可以提供高质量的反馈,但可能会消耗大量的时间和人力资源。

1.2 自动化评估

自动化评估肯定是RAG评估的主流和发展方向。

1.2.1.1 LangSmith

在我的这篇文章中 【AI大模型应用开发】【LangSmith: 生产级AI应用维护平台】1. 快速上手数据集与测试评估过程 介绍了如何使用LangSmith平台进行效果评估。

  • 需要准备测试数据集
  • 不仅可以评估RAG效果,对于LangChain中的Prompt模板等步骤都可进行测试评估。

在这里插入图片描述

1.2.1.2 Langfuse

Langfuse作为LangSmith的平替,也具有自动化评估的功能。在我的这篇文章中 【AI大模型应用开发】【LangFuse: LangSmith平替,生产级AI应用维护平台】0. 快速上手 - 基本功能全面介绍与实践(附代码) 介绍了如何使用Langfuse平台进行效果评估。

  • 需要准备测试数据集
  • 不仅可以评估RAG效果,对于LangChain中的Prompt模板等步骤都可进行测试评估。

在这里插入图片描述
以上两个平台对RAG的评估,都可以自定义自己的评估函数。当然其也支持一些内置的评估函数。

1.2.1.3 Trulens

TruLens是一款旨在评估和改进 LLM 应用的软件工具,它相对独立,可以集成 LangChain 或 LlamaIndex 等 LLM 开发框架。它使用反馈功能来客观地衡量 LLM 应用的质量和效果。这包括分析相关性、适用性和有害性等方面。TruLens 提供程序化反馈,支持 LLM 应用的快速迭代,这比人工反馈更快速、更可扩展。

  • 开源链接:github.com/truera/trul…
  • 使用手册:www.trulens.org/trulens_eva…

在这里插入图片描述
使用的步骤:

(1)创建LLM应用

(2)将LLM应用与TruLens连接,记录日志并上传

(3)添加 feedback functions到日志中,并评估LLM应用的质量

(4)在TruLens的看板中可视化查看日志、评估结果等

(5)迭代和优化LLM应用,选择最优的版本

其对于RAG的评估主要有三个指标:

  • 上下文相关性(context relevance):衡量用户提问与查询到的参考上下文之间的相关性
  • 忠实性(groundedness ):衡量大模型生成的回复有多少是来自于参考上下文中的内容
  • 答案相关性(answer relevance):衡量用户提问与大模型回复之间的相关性

在这里插入图片描述

其对RAG的评估不需要有提前收集的测试数据集和相应的答案。

1.2.4 RAGAS

考虑标准的RAG设置,即给定一个问题q,系统首先检索一些上下文c(q),然后使用检索到的上下文生成答案as(q)。在构建RAG系统时,通常无法访问人工标注的数据集或参考答案,因此该工作将重点放在完全独立且无参考的度量指标上。

四个指标,与Trulens的评估指标有些类似:

  • 评估检索质量:
    • context_relevancy(上下文相关性,也叫 context_precision)
    • context_recall(召回性,越高表示检索出来的内容与正确答案越相关)
  • 评估生成质量:
    • faithfulness(忠实性,越高表示答案的生成使用了越多的参考文档(检索出来的内容))
    • answer_relevancy(答案的相关性)

在这里插入图片描述

  1. 常用评估指标

在上文评估方法中已经介绍了几种常用的评估指标:

2.1 Trulens 的RAG三元组指标

  • 上下文相关性(context relevance):衡量用户提问与查询到的参考上下文之间的相关性
  • 忠实性(groundedness ):衡量大模型生成的回复有多少是来自于参考上下文中的内容
  • 答案相关性(answer relevance):衡量用户提问与大模型回复之间的相关性

2.2 RAGAS的四个指标

四个指标,与Trulens的评估指标有些类似:

  • 评估检索质量:
    • context_relevancy(上下文相关性,也叫 context_precision)
    • context_recall(召回性,越高表示检索出来的内容与正确答案越相关)
  • 评估生成质量:
    • faithfulness(忠实性,越高表示答案的生成使用了越多的参考文档(检索出来的内容))
    • answer_relevancy(答案的相关性)

2.3 其它指标

参考论文:arxiv.org/pdf/2309.01…

(1)噪声鲁棒性(Noise Robustness)

衡量从噪声文档中提取有用的信息能力。在现实世界中,存在大量的噪声信息,例如假新闻,这给语言模型带来了挑战。

(2)否定拒绝(Negative Rejection)

当检索到的文档不足以支撑回答用户的问题时,模型应拒绝回答问题,发出”信息不足”或其他拒绝信号。

(3)信息整合(information integration)

评估模型能否回答需要整合多个文档信息的复杂问题,即,当一个问题需要查找多个文档,综合信息之后才能回答时,模型的表现。

(4)反事实鲁棒性(CounterfactualRobustness)

模型能否识别检索文档中已知事实错误的能力,即当索引的文档信息原本就是与事实相背时,大模型能否识别出不对。

在这里插入图片描述

  1. 总结

本文主要总结了当前比较流行的评估方法和指标。当前AI技术的快速发展,RAG和RAG评估是当前比较有前景的发展方向,不断有新的评估工具和理论被提出,让我们持续跟进,了解这些工具和理论,从而在使用时知道如何选择。

参考

  • mp.weixin.qq.com/s/Si8rb0L1u…
  • mp.weixin.qq.com/s/z18J2l_b-…
  • mp.weixin.qq.com/s/YFji1s2yT…
  • mp.weixin.qq.com/s/TrXWXkQIY…
  • maimai.cn/article/det…

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~


  • 大家好,我是同学小张,日常分享AI知识和实战案例
  • 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
  • +v: jasper_8017 一起交流💬,一起进步💪。
  • 微信公众号也可搜【同学小张】 🙏

本站文章一览:

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

没有前端能抵抗住的酷炫效果,带你用Threejs Shad

发表于 2024-04-27

前言

上一篇文章「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」里古柳带大家用 Three.js Shader 粒子系统实现了这个非常漂亮的星系效果。文章在上周三发布后上了掘金热榜,并且截至目前点赞数、收藏数双双破百,成为本 Shader 系列教程里阅读量最高的一篇(马上破万)。果然大家更喜欢看这种实现完整、实际、酷炫效果的文章,这倒是和我设想的一样。

  • 链接:actium.co.jp/
  • 链接:codepen.io/GuLiu/pen/W…

正好上篇讲到粒子系统,我想不妨继续趁热打铁讲解下这个带我入坑 Shader 的效果 Pepyaka,想来这应该也是很多前端、程序员梦寐以求想要实现的酷炫效果吧。下图是本文最终实现的效果,GIF 不够清晰,大家可以去 Codepen 查看源码和效果,代码后续也会同步到 GitHub。

  • 链接:codepen.io/GuLiu/pen/L…
  • 链接:github.com/DesertsX/th…

Pepyaka 这个效果本来出自于 Grant Yi 的个人网站,其实我一直不知道这个词到底啥意思,但就当成该效果的代名词这么叫着。可惜原网站换成了这个效果,虽然仍是 Shader 实现、同样酷炫,但看不到 Pepyaka 还是可惜。

  • 链接:www.grantyi.com/

在「断更19个月,携 Three.js Shader 归来!(上)- 牛衣古柳 - 20230416」一文里我提过,当初入门 Three.js 后因为对粒子系统感兴趣,于是在油管搜教程,然后看到 Yuri Artiukh 复现 Pepyaka 的教程——「#s3e6 ALL YOUR HTML, Making Pepyaka with Three.js - 20191201」——在视频里残存的片段里窥见到 Pepyaka 如此丝滑、酷炫、漂亮的效果,于是入坑 Shader,再然后有了现在输出 Shader 教程这桩事。

时隔两年古柳终于可以在原视频的基础上融入自己会的一些 Shader 效果,更近一步实现出更贴近原作的各种效果,并通过文章教给大家,让大家也能上手实现这样酷炫的效果。

中心球体

闲言少叙,进入正题。让我们同样从线框模式下的白色球体开始讲起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
js复制代码import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

let w = window.innerWidth;
let h = window.innerHeight;

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(75, w / h, 0.01, 1000);
camera.position.set(0, 0, 4);
camera.lookAt(new THREE.Vector3());

const renderer = new THREE.WebGLRenderer({
antialias: true,
// alpha: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(w, h);
renderer.setClearColor(0x0a0a0f, 1);
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);

const vertexShader = /* GLSL */ `
uniform float uTime;

void main() {
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const fragmentShader = /* GLSL */ `
void main() {
gl_FragColor = vec4(vec3(1.0), 1.0);
}
`;

const sphereMaterial = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
uTime: { value: 0 },
},
// wireframe: true,
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);

const clock = new THREE.Clock();
function render() {
const time = clock.getElapsedTime();
sphereMaterial.uniforms.uTime.value = time;
sphere.rotation.y = time;
renderer.render(scene, camera);
requestAnimationFrame(render);
}

render();

顶点偏移

接着我们用 noise 噪声函数对顶点位置进行偏移从而改变球体形状、并用 noise 值控制 HSV 颜色模式里的 Hue 色相值,这些内容在「手把手带你入门 Three.js Shader 系列(六) - 牛衣古柳 - 20231220」、「手把手带你入门 Three.js Shader 系列(七) - 牛衣古柳 - 20240206」两篇文章里已经详细地讲过,大家可以去自行学习。

这里简单过一遍,我们谷歌搜索 GLSL noise function,从这里拷贝 Simplex 4D Noise 函数,接着对每个顶点产生一个 noise 数值(不同 noise 函数返回的值范围可能是0-1,也可能是-1-1,大家可以用第七篇里讲的方法去查看,这里不太重要也就不带大家看了),将该数值乘上法线 normal 作为在该偏移方向上偏移的程度,再加上原始 position 就是偏移后的顶点坐标。

  • 链接:gist.github.com/patriciogon…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
C#复制代码// vertex shader
uniform float uTime;

// Simplex 4D Noise
// by Ian McEwan, Ashima Arts
vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);}
float permute(float x){return floor(mod(((x*34.0)+1.0)*x, 289.0));}
vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;}
float taylorInvSqrt(float r){return 1.79284291400159 - 0.85373472095314 * r;}

vec4 grad4(float j, vec4 ip){
const vec4 ones = vec4(1.0, 1.0, 1.0, -1.0);
vec4 p,s;

p.xyz = floor( fract (vec3(j) * ip.xyz) * 7.0) * ip.z - 1.0;
p.w = 1.5 - dot(abs(p.xyz), ones.xyz);
s = vec4(lessThan(p, vec4(0.0)));
p.xyz = p.xyz + (s.xyz*2.0 - 1.0) * s.www;

return p;
}

float snoise(vec4 v){
const vec2 C = vec2( 0.138196601125010504, // (5 - sqrt(5))/20 G4
0.309016994374947451); // (sqrt(5) - 1)/4 F4
// First corner
vec4 i = floor(v + dot(v, C.yyyy) );
vec4 x0 = v - i + dot(i, C.xxxx);

// Other corners

// Rank sorting originally contributed by Bill Licea-Kane, AMD (formerly ATI)
vec4 i0;

vec3 isX = step( x0.yzw, x0.xxx );
vec3 isYZ = step( x0.zww, x0.yyz );
// i0.x = dot( isX, vec3( 1.0 ) );
i0.x = isX.x + isX.y + isX.z;
i0.yzw = 1.0 - isX;

// i0.y += dot( isYZ.xy, vec2( 1.0 ) );
i0.y += isYZ.x + isYZ.y;
i0.zw += 1.0 - isYZ.xy;

i0.z += isYZ.z;
i0.w += 1.0 - isYZ.z;

// i0 now contains the unique values 0,1,2,3 in each channel
vec4 i3 = clamp( i0, 0.0, 1.0 );
vec4 i2 = clamp( i0-1.0, 0.0, 1.0 );
vec4 i1 = clamp( i0-2.0, 0.0, 1.0 );

// x0 = x0 - 0.0 + 0.0 * C
vec4 x1 = x0 - i1 + 1.0 * C.xxxx;
vec4 x2 = x0 - i2 + 2.0 * C.xxxx;
vec4 x3 = x0 - i3 + 3.0 * C.xxxx;
vec4 x4 = x0 - 1.0 + 4.0 * C.xxxx;

// Permutations
i = mod(i, 289.0);
float j0 = permute( permute( permute( permute(i.w) + i.z) + i.y) + i.x);
vec4 j1 = permute( permute( permute( permute (
i.w + vec4(i1.w, i2.w, i3.w, 1.0 ))
+ i.z + vec4(i1.z, i2.z, i3.z, 1.0 ))
+ i.y + vec4(i1.y, i2.y, i3.y, 1.0 ))
+ i.x + vec4(i1.x, i2.x, i3.x, 1.0 ));
// Gradients
// ( 7*7*6 points uniformly over a cube, mapped onto a 4-octahedron.)
// 7*7*6 = 294, which is close to the ring size 17*17 = 289.

vec4 ip = vec4(1.0/294.0, 1.0/49.0, 1.0/7.0, 0.0) ;

vec4 p0 = grad4(j0, ip);
vec4 p1 = grad4(j1.x, ip);
vec4 p2 = grad4(j1.y, ip);
vec4 p3 = grad4(j1.z, ip);
vec4 p4 = grad4(j1.w, ip);

// Normalise gradients
vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3)));
p0 *= norm.x;
p1 *= norm.y;
p2 *= norm.z;
p3 *= norm.w;
p4 *= taylorInvSqrt(dot(p4,p4));

// Mix contributions from the five corners
vec3 m0 = max(0.6 - vec3(dot(x0,x0), dot(x1,x1), dot(x2,x2)), 0.0);
vec2 m1 = max(0.6 - vec2(dot(x3,x3), dot(x4,x4) ), 0.0);
m0 = m0 * m0;
m1 = m1 * m1;
return 49.0 * ( dot(m0*m0, vec3( dot( p0, x0 ), dot( p1, x1 ), dot( p2, x2 )))
+ dot(m1*m1, vec2( dot( p3, x3 ), dot( p4, x4 ) ) ) ) ;
}

void main() {
float noise = snoise(vec4(position, 0.0));
vec3 newPos = position + 0.8 * normal * noise;
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

这里用接收 vec4 的 snoise 函数方便后续把 uTime 作为第4个参数,让形状动起来。

用法线 normal 作为颜色方便看变形后的形状。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C#复制代码// vertex shader
uniform float uTime;
varying vec3 vNormal;

float snoise(vec4 c){
// ...
}

void main() {
vNormal = normal;

float noise = snoise(vec4(position, 0.0));
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

// fragment shader
varying vec3 vNormal;

void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
gl_FragColor = vec4(vNormal, 1.0);
}

给 position 乘不同值,改变传给 noise 函数的顶点相邻程度,使形状变化更加“剧烈”。

1
2
3
C#复制代码// float noise = snoise(vec4(position * 1.0, 0.0));
// float noise = snoise(vec4(position * 0.3, 0.0));
float noise = snoise(vec4(position * 10.0, 0.0));

增加球体细分数,使球体上有更多顶点可以被用来偏移位置。

1
2
js复制代码// const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
const sphereGeometry = new THREE.SphereGeometry(1, 200, 200);

将 uTime 作为第4个参数使形状实时发生变化。

1
C#复制代码float noise = snoise(vec4(position * 10.0, uTime * 0.2));

noise 值作为颜色

noise 数值除了用来偏移顶点坐标,还能用来设置颜色。将 vec3(noise) 灰度值颜色作为 vColor 传到片元着色器进行使用,那么 noise 值越大偏移高度越高、颜色越白;反之值越小高度越低、颜色越黑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
C#复制代码// vertex shader
uniform float uTime;
varying vec3 vNormal;
varying vec3 vColor;

void main() {
vNormal = normal;

float noise = snoise(vec4(position * 10.0, uTime * 0.2));
vColor = vec3(noise);
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

// fragment shader
varying vec3 vNormal;
varying vec3 vColor;

void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
// gl_FragColor = vec4(vNormal, 1.0);
gl_FragColor = vec4(vColor, 1.0);
}

也可以设置成 rgb 里的 red 值,就是红黑效果。

1
C#复制代码vColor = vec3(noise, 0.0, 0.0);

还可以把 noise 值设置到 HSV 模式里的 hue 色相值(第七篇里都讲过,不过那里用的 HSL 这里用的 HSV,需要注意 HSV=HSB!=HSL),然后转换回 rgb 模式。谷歌搜索 glsl hsv2rgb function 找到现成的实现,拷贝后就能使用,直接把 noise 作为 hue 会是五彩斑斓的效果,因为 noise 值0-1的话会把所有色相值覆盖到。虽然也挺好看,但不是我们这里想要的。

  • 链接:gist.github.com/983/e170a24…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C#复制代码vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

void main() {
vNormal = normal;

float noise = snoise(vec4(position * 10.0, uTime * 0.2));
// vColor = vec3(noise);
// vColor = vec3(noise, 0.0, 0.0);
vColor = hsv2rgb(vec3(noise, 1.0, 1.0));
vec3 newPos = position + 0.8 * normal * noise;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}

缩小 noise 范围,使颜色变化不那么剧烈。

1
C#复制代码vColor = hsv2rgb(vec3(vNoise * 0.1, 1.0, 1.0));

最后微调 hue,改下饱和度,至效果满意即可。

1
C#复制代码vColor = hsv2rgb(vec3(noise * 0.1 + 0.04, 0.8, 1.0));

球形粒子系统

中心的效果完成后,接着在外面加一层球形的粒子,之前有群友在学第七篇时就有问到,这次终于可以讲解下(欢迎加我「xiaoaizhj」,备注「可视化加群」,一起交流)。

上一篇文章「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」对粒子系统已经做过介绍,这里就不再重复。

我们通过 BufferGeometry() 设置粒子的 position,使 radius 稍大于中心球体,这样能包裹着球体。这里想要粒子在球体上均匀分布,从网上找现成的公式即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js复制代码const particleGeometry = new THREE.BufferGeometry();

const N = 4000;
const positions = new Float32Array(N * 3);

const inc = Math.PI * (3 - Math.sqrt(5));
const off = 2 / N;
const radius = 2;

for (let i = 0; i < N; i++) {
const y = i * off - 1 + off / 2;
const r = Math.sqrt(1 - y * y);
const phi = i * inc;

positions[3 * i] = radius * Math.cos(phi) * r;
positions[3 * i + 1] = radius * y;
positions[3 * i + 2] = radius * Math.sin(phi) * r;
}

particleGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);

视频里用的是某博客文章里的公式,由于文章已看不到,这里放截图方便感兴趣的小伙伴看眼,总之把这里 python 代码改成上面 JS 代码即可。

我们也可以搜 evenly distribute points on a sphere、fibonacci spiral sphere 等关键词,能找到其他大同小异的实现方式,下面是另一种方案,作为参考,可以看到效果都差不多。后续演示仍沿用第一种方案。

  • 链接:medium.com/@vagnerseib…
1
2
3
4
5
6
7
8
9
10
11
js复制代码// 另一种生成球体上均匀粒子坐标的方式
for (let i = 0; i < N; i++) {
const k = i + 0.5;
const phi = Math.acos(1 - (2 * k) / N);
const theta = Math.PI * (1 + Math.sqrt(5)) * k;
const x = Math.cos(theta) * Math.sin(phi) * radius;
const y = Math.sin(theta) * Math.sin(phi) * radius;
const z = Math.cos(phi) * radius;

positions.set([x, y, z], i * 3);
}

材质用 ShaderMaterial,粒子颜色设置了透明度,然后和 particleGeometry 一起丢给 Points 就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
js复制代码const particleVertex = /* GLSL */ `
uniform float uTime;

void main() {
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = 6.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
`;

const particleFragment = /* GLSL */ `
void main() {
// gl_FragColor = vec4(vec3(1.0), 1.0);
gl_FragColor = vec4(vec3(1.0), 0.6);
}
`;

const particleMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
},
vertexShader: particleVertex,
fragmentShader: particleFragment,
transparent: true,
blending: THREE.AdditiveBlending,
});

const particles = new THREE.Points(particleGeometry, particleMaterial);
scene.add(particles);

function render() {
// ...
sphereMaterial.uniforms.uTime.value = time;
particleMaterial.uniforms.uTime.value = time;
}

不设置透明度的话效果如下。还是设置透明度、弱化粒子视觉效果后看着更舒服。

1
C#复制代码gl_FragColor = vec4(vec3(1.0), 1.0);

粒子上下波动

接着让粒子运动起来,可以通过对 y 坐标取 sin 值再加回到 y 上,这样同一高度的粒子会一起随 sin 波浪上下偏移,整体上就是波浪的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C#复制代码// particleVertex
uniform float uTime;

void main() {

vec3 newPos = position;
newPos.y += 0.1 * sin(newPos.y * 6.0 + uTime);
// newPos.z += 0.05 * sin(newPos.y * 10.0 + uTime);

// vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
gl_PointSize = 6.0 / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}

还可以对 z 坐标进行一定偏移,虽然这里可能效果不明显,但常常会组合的对 xyz 进行 sin/cos 等操作,以后大家也会碰到(实际上后文就多次重复用到),所以这里先演示下。大家也可以自由发挥、随意尝试,不必局限本文所讲到的方法。

1
C#复制代码newPos.z += 0.05 * sin(newPos.y * 10.0 + uTime);

效果已经很漂亮了,大家可以休息下,喝杯奶茶或咖啡,好好欣赏享受下自己的成果。

背景用随机粒子进行点缀

上面都是油管教程里涉及到的内容,休息结束后,这次让我们加个餐、更进一步把原作里其他一些效果也简单实现下。

首先可以看到背景有点空,我们可以用随机粒子进行点缀,丰富画面效果。

通过 r 使得粒子在中心球体和球形粒子之外的范围,xyz 坐标随机在立方体空间内分布,这里都是简单的设置,所以怎么方便怎么来;sizes 控制每个粒子的随机大小并且会用作粒子移动速度,为了使值不为0,这样粒子速度不为0就不会停着不动,所以加了0.4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码const firefliesGeometry = new THREE.BufferGeometry();
const firefliesCount = 300;
const positions1 = new Float32Array(firefliesCount * 3);
const sizes = new Float32Array(firefliesCount);

for (let i = 0; i < firefliesCount; i++) {
const r = Math.random() * 5 + 5;
positions1[i * 3 + 0] = (Math.random() - 0.5) * r;
positions1[i * 3 + 1] = (Math.random() - 0.5) * r;
positions1[i * 3 + 2] = (Math.random() - 0.5) * r;

sizes[i] = Math.random() + 0.4;
}

firefliesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions1, 3)
);
firefliesGeometry.setAttribute("aSize", new THREE.BufferAttribute(sizes, 1));

顶点着色器里 gl_PointSize 乘上 aSize 改变大小,片元着色器里用每个粒子离中心的距离通过一个反比例函数 0.05/d-0.05*2.0 使得靠近中心为1,往外逐渐变成0,再设置成透明度值,从而实现出光斑、模糊圆形的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
js复制代码const firefliesVertexShader = /* GLSL */ `
uniform float uTime;
attribute float aSize;

void main() {
vec3 newPos = position;
// newPos.y += sin(uTime * 0.5 + newPos.x * 100.0) * aSize * 0.2;
// newPos.x += sin(uTime * 0.5 + newPos.x * 200.0) * aSize * 0.1;
vec4 mvPosition = modelViewMatrix * vec4(newPos, 1.0);
gl_PointSize = 70.0 * aSize / -mvPosition.z;
gl_Position = projectionMatrix * mvPosition;
}
`;

const firefliesFragmentShader = /* GLSL */ `
void main() {
float d = length(gl_PointCoord - vec2(0.5));
float strength = clamp(0.05 / d - 0.05 * 2.0, 0.0, 1.0);
gl_FragColor = vec4(vec3(1.0), strength);
}
`;

const firefliesMaterial = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
},
vertexShader: firefliesVertexShader,
fragmentShader: firefliesFragmentShader,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
});

const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial);
scene.add(fireflies);

通过曲线图看看透明度 strength 计算方式,当距离d=0.5时数值为0也就是圆圈边缘;d>0.5时数值为负数,通过clamp会变成0,完全透明;d=0.05/1.1=0.045时数值为1;d<0.045时数值大于1会被clamp取1,完全不透明,距离从0.045到0.5透明度快速降为0,从而实现模糊效果。

1
2
3
C#复制代码float d = length(gl_PointCoord - vec2(0.5));
float strength = clamp(0.05 / d - 0.05 * 2.0, 0.0, 1.0);
gl_FragColor = vec4(vec3(1.0), strength);

这种粒子实现方式还蛮常见。记得2022年4-5月份刚接触 shader 那会,碰上浙大125周年校庆,发现当年很特别官方搞了个求是星海的网站效果,就是粒子系统为主,里面粒子颜色透明度就是用这种反比例函数实现。

通过 Chrome 浏览器的 Spector.js 插件就能看到这里粒子的着色器里的 starIntensity() 函数,就是如此实现的。

在该网站输入校友专业和名字、认证通过后会生成每个校友专属的由“灿若繁星”的浙大人姓名的粒子系统组成的效果。网页还在,但需要输入信息才能生成,所以非校友的话看不到,只能看这里截图。

  • 链接:125.zju.edu.cn/universe

扯回来,和上面球形粒子一样这里用 sin 函数使背景随机粒子也运动起来,参数可以任意修改看效果。

1
2
3
C#复制代码vec3 newPos = position;
newPos.y += sin(uTime * 0.5 + newPos.x * 100.0) * aSize * 0.2;
newPos.x += sin(uTime * 0.5 + newPos.x * 200.0) * aSize * 0.1;

显示文字

接着模仿原作在中心球体和球形粒子之间放上文字。这里通过在长度2宽度1的 Plane 上显示纹理图片实现,准备一张白色文字、背景透明、1024x512 的图片。下面是截图的效果,而不是原图,不要保存这张图去直接使用。原图已传到 postimg 这个网站,下面是链接直接用就行。如果后续想将本文的效果修改后放自己的个人网站,那么这里就可以和原作一样换成自己的名字。

  • 链接:i.postimg.cc/nrSTmrZk/te…

将图片通过 TextureLoader().load() 加载后作为 uTexture 传给 shader,然后通过 uv 采样纹理图,再搭配 textMaterial 设置 transparent 即可显示文字。text mesh 通过设置 position.z=1.7 移动到所需位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
js复制代码const textGeometry = new THREE.PlaneGeometry(2, 1, 100, 100);

const textVertex = /* GLSL */ `
uniform float uTime;
varying vec2 vUv;

void main() {
vUv = uv;

vec3 newPos = position;
// newPos.y += 0.06 * sin(newPos.x + uTime);
// newPos.x += 0.1 * sin(newPos.x * 2.0 + uTime);
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPos, 1.0);
}
`;

const textFragment = /* GLSL */ `
uniform sampler2D uTexture;
varying vec2 vUv;

void main() {
vec4 color = texture2D(uTexture, vUv);
gl_FragColor = color;
}
`;

const textMaterial = new THREE.ShaderMaterial({
vertexShader: textVertex,
fragmentShader: textFragment,
uniforms: {
uTime: { value: 0 },
uTexture: {
value: new THREE.TextureLoader().load("https://i.postimg.cc/nrSTmrZk/text.png"), // './assets/text.png',
},
},
transparent: true,
});

const text = new THREE.Mesh(textGeometry, textMaterial);
text.position.z = 1.7;
scene.add(text);

function render () {
// ...
sphereMaterial.uniforms.uTime.value = time;
particleMaterial.uniforms.uTime.value = time;
firefliesMaterial.uniforms.uTime.value = time;
textMaterial.uniforms.uTime.value = time;
}

接着再一次应用 sin 函数改变顶点位置使 plane 发生扭曲,顶点移动后 uv 也随之移动,用 uv 采样纹理后就会有文字飘浮的效果。

1
2
3
C#复制代码vec3 newPos = position;
newPos.y += 0.06 * sin(newPos.x + uTime);
newPos.x += 0.1 * sin(newPos.x * 2.0 + uTime);

上方光线

最后古柳观察到原作顶部有光线效果,于是也顺带实现了下,不过因为文章篇幅已经不短,加上这部分解释起来比前几个效果复杂些,就暂时不在本文讲了,后续其他文章有机会再讲。大家这里看眼效果即可,感兴趣可自行实现。

小结

至此,古柳带大家把这个自己入坑 shader 的酷炫效果“简单”复现了下,中间偷懒多次用了 sin 函数,所以其实并没有大家想的那么复杂。

虽然和原作比起来还是很粗陋,各种参数、粒子动画、颜色搭配都有很大优化空间,效果远远不如原作漂亮,但是作为一篇教程里的例子,或许已足够让大家学到些东西,其他的优化就留给大家去自由发挥了。

还是可惜原作早就看不到了,只能从油管视频里瞥见一些片段效果,无法自己去交互、去体验、去学习。但两年前初见 Pepyaka、初识 shader 时的那份惊艳却伴随古柳至今,希望更多人看到本文后也能感受到那份惊艳,并能借本教程之力让那份惊艳不再局限于观赏,而是可以自己去一步步实现、一步步靠近,相信大家跟着古柳的系列文章一点点学下来,就不会再觉得这样的效果是自己无法理解、无法实现的了吧。

最后完整源码可见 Codepen。

  • 链接:codepen.io/GuLiu/pen/L…

相关阅读

「手把手带你入门 Three.js Shader 系列」目录如下:

  • 「断更19个月,携 Three.js Shader 归来!(上)- 牛衣古柳 - 20230416」
  • 「断更19个月,携 Three.js Shader 归来!(下)- 牛衣古柳 - 20230421」
  • 「手撸一个星系,送给心爱的姑娘!(Three.js Shader 粒子系统实现)- 牛衣古柳 - 20240417」
  • 「手把手带你入门 Three.js Shader 系列(八)- 牛衣古柳 - 20240229」
  • 「手把手带你入门 Three.js Shader 系列(七)- 牛衣古柳 - 20230206」
  • 「手把手带你入门 Three.js Shader 系列(六)- 牛衣古柳 - 20231220」
  • 「手把手带你入门 Three.js Shader 系列(五)- 牛衣古柳 - 20231126」
  • 「手把手带你入门 Three.js Shader 系列(四)- 牛衣古柳 - 20231121」
  • 「手把手带你入门 Three.js Shader 系列(三)- 牛衣古柳 - 20230725」
  • 「手把手带你入门 Three.js Shader 系列(二)- 牛衣古柳 - 20230716」
  • 「手把手带你入门 Three.js Shader 系列(一)- 牛衣古柳 - 20230515」

照例

如果你喜欢本文内容,欢迎以各种方式支持,这也是对古柳输出教程的一种正向鼓励!

最后欢迎加入「可视化交流群」,进群多多交流,对本文任何地方有疑惑的可以群里提问。加古柳微信:xiaoaizhj,备注「可视化加群」即可。

欢迎关注古柳的公众号「牛衣古柳」,并设置星标,以便第一时间收到更新。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

多线程(75)乐观读策略

发表于 2024-04-27

乐观读策略是一种常见的并发控制方法,尤其在读多写少的场景中表现出色。这种策略基于一个假设:冲突发生的概率很低,所以在大多数情况下,无需在读操作时加锁。乐观读策略通常通过版本号或时间戳来实现,每次写操作会改变这个版本号或时间戳,而读操作会检查这个版本号或时间戳以确保数据的一致性。

下面以一个简单的使用版本号实现的乐观读策略为例,通过Java代码来演示这一概念。

示例代码

假设有一个共享资源Resource,这个资源有一个数据字段和一个版本号字段。每次写操作都会更新数据和版本号,而读操作则需要检验版本号以确认在读取数据过程中数据没有被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
java复制代码public class OptimisticReadExample {
private static class Resource {
private volatile int data = 0; // 共享资源数据
private volatile int version = 0; // 版本号

// 写操作
public synchronized void write(int newData) {
data = newData;
version++; // 写操作更新版本号
}

// 乐观读操作
public int read() {
int currentVersion = version;
int result = data; // 读取数据
// 检查在读取数据之后版本号是否发生了变化
if (currentVersion != version) {
throw new IllegalStateException("Data was modified during reading");
}
return result;
}
}

public static void main(String[] args) {
final Resource resource = new Resource();

// 模拟写操作
new Thread(() -> {
for (int i = 0; i < 5; i++) {
resource.write(i);
System.out.println("Write: " + i);
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();

// 模拟读操作
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
System.out.println("Read: " + resource.read());
} catch (IllegalStateException e) {
System.out.println("Read failed due to modification");
}
try {
Thread.sleep(50); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
}

在这个例子中,如果在读取数据的过程中数据被另一个线程修改(即版本号发生变化),则抛出IllegalStateException。这就是乐观读策略的核心:它假设在大多数情况下不会发生冲突,所以不加锁,但通过版本号检查来确保数据的一致性。

特点与考虑

  • 性能优势:乐观读在读多写少的场景中可以显著提高性能,因为它减少了锁的使用。
  • 冲突处理:当检测到版本冲突时,需要有相应的策略来处理,比如重试读操作、回滚操作等。
  • 适用场景:乐观读策略特别适用于读操作远远多于写操作的场景。

实际应用

在实际应用中,许多高性能并发库和框架使用了乐观读策略,例如Java的java.util.concurrent包中的StampedLock就提供了一种基于时间戳的锁机制,支持乐观读锁。

乐观读策略是理解现代并发控制技术的一个重要环节。通过合理使用乐观读策略,可以在保证数据一致性的同时,显著提高应用程序的并发性能。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…789…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%