首先需要将 变量数据 区分开。变量是语言层面的概念,数据是存储层面的概念。变量和值之间是所有制关系。变量拥有不同类型的值。

数据分为 基本数据类型复合数据类型。复合数据类型包含 引用。引用会在稍后介绍。

值为基本数据类型的变量相互赋值时,会直接进行拷贝。

值为复合数据类型的变量给对方赋值时,由于不确定拷贝的成本,采用默认懒惰的策略,只传递对方的 引用。这里引用是一个抽象概念,和物理地址类似。变量自己不拥有数据,只拥有数据的引用。每当进程访问变量时,会根据引用找到数据。关键在于这一份数据不是变量私有的,可以被多个变量通过一个引用共享。

1
a = [1,2]

在抽象层面上,a 可视为一个列表。但在物理层面上,a 是一个单一的值,即为引用。这种二元性是很多困惑的根源。

1
a = [[1,2],[3,4]]

a 的值是一个引用。这个引用指向两个引用组成的列表,两个引用分别指向 [1,2][3,4]

1
b = a

如此赋值时,b 的值也是一个引用,同样指向两个引用组成的列表。

1
b.append([5])

b 的值是一个指向列表的引用,b.append 会向该引用指向的列表添加一个元素 [5]

对 Gradle 自身下载的加速

打开 gradle-wrapper 文件

distributionUrl 的值替换为 https\://mirrors.cloud.tencent.com/gradle/gradle-8.9-all.zip

根据自己的 gradle 版本调整文件名。

尽量下载 all 而不是 bin,因为如果只下载 bin,IDEA 会用 google 的网站下载 src,速度很慢,最后不如一开始直接下载 all。

对依赖下载的加速

在 build.gradle 中加入

1
2
3
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
}

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
package org.example.springsecuritykotlin

import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class GreetingHandler {
fun hello(request: ServerRequest?): Mono<ServerResponse> {
return ServerResponse
.ok().contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(GreetingResponse("Hello World!")))
}

fun greetingToName(request: ServerRequest): Mono<ServerResponse> {
return request
.bodyToMono(NameRequest::class.java)
.flatMap { nameRequest ->
val response = GreetingResponse("Hello, ${nameRequest.user}!")
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(response))
}
.onErrorResume { exception ->
println("Greeting error: ${exception.message}")
exception.printStackTrace()
ServerResponse.badRequest().contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(mapOf("error" to "Invalid request", "details" to exception.message)))
}
}
}

data class GreetingResponse(val message: String)
data class NameRequest(val user: String)

报错 JSON decoding error

发现原因是 bodyToMono 不支持 data class 的解析。将 data 去掉即可。

问题

定义 Model 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
import jakarta.persistence.*

@Entity
@Table(name = "user")
class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Int = 0

var name: String = ""

var email: String = ""
}

在 insert 的时候报错 Table doesn’t exist

解决方案

使用了 Hibernate 用于 ORM。Hibernate 默认不会自动创建表。

需要在 application.property 中添加语句

1
spring.jpa.hibernate.ddl-auto=update

这样就可以自动创建表。

添加快捷方式

打开 “自动操作” - 新建文稿 - 选择 “快速操作” - 搜索并选择 shell

脚本内容

1
2
3
4
for f in "$@"
do
open -a "App name" "$f"
done

上方选项选择:工作流程接收到当前 文件或文件夹,位于 访达

右侧选项默认为 “作为 stdin”,应更改为 “作为自变量”

保存

在 Finder 右键点击文件夹,在 “快速操作” 中可找到

删除与重命名

打开 “自动操作” - 打开现有文稿 - 路径 Library/Services 下的文件

这篇文章是给我自己看的,起到备忘录的作用。

仓库

Github 上 tzh21.github.io 对应的仓库包含了当前博客的备份。

仓库有两个分支,main 分支是 Hexo 自动生成的静态 html 文件,不是很重要。hexo 分支包含了 markdown 格式的博客源文件、用于快捷操作的 shell 脚本、Hexo 和 Next 的配置文件,较为重要。

shell 脚本

1
sh newpost.sh

新建一个脚本。路径为当前日期,title 固定为 post,需要后续修改。

1
sh update.sh

写完博客后使用,用于更新 Github 仓库。主要功能有:

  • Hexo 生成静态文件
  • Git commit 到本地
  • Git push hexo 分支到 Github
  • Hexo 部署静态文件到 Github(push main 分支到 Github)

概念

认证(Authentication):服务器识别用户的身份。

授权(Authorization):在认证之后,服务器根据用户的身份,决定该用户可以访问哪些资源。

实现认证

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
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { sql } from '@vercel/postgres';
import { z } from 'zod';
import type { User } from '@/app/lib/definitions';

// 推荐使用 bcryptjs 包而不是 bcrypt 包。使用 bcrypt 包时可能会遇到错误,参考 https://github.com/kelektiv/node.bcrypt.js/issues/1017
const bcrypt = require('bcryptjs')

// 从数据库中获取指定 email 的用户,用于后续的比对
async function getUser(email: string): Promise<User | undefined> {
try {
const user = await sql<User>`SELECT * FROM users WHERE email=${email}`;
return user.rows[0];
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}

// next-auth 提供了一系列处理认证的成熟套件,只需要提供一些必要的参数即可。尽可能使用这些套件而不是手搓。
export const { auth, signIn, signOut } = NextAuth({
pages: {
signIn: '/login',
},
// 认证的逻辑。可以添加多个认证提供方,这里使用最原始的账号密码认证 Credentials。
providers: [
Credentials({
// credentials 包含账号和密码,将其与数据库中的进行对比。
// 如果对比成功,返回数据库中的账户,如果对比失败,返回 null。
async authorize(credentials) {
const parsedCredentials = z
.object({ email: z.string().email(), password: z.string().min(6) })
.safeParse(credentials);

if (parsedCredentials.success) {
const { email, password } = parsedCredentials.data;

const user = await getUser(email);
if (!user) return null;

const passwordsMatch = await bcrypt.compare(password, user.password);
if (passwordsMatch) return user;
}

console.log('Invalid credentials');
return null;
},
}),
]
});

前端登录表单

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
import { signIn } from '@/auth';
import { useActionState } from 'react';

// 提交登录表单
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
try {
// 这里用到了 next-auth 的登录组件
await signIn('credentials', formData);
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return 'Invalid credentials.';
default:
return 'Something went wrong.';
}
}
throw error;
}
}

export default function LoginForm() {
const [errorMessage, formAction, isPending] = useActionState(
authenticate,
undefined,
);
...
return (
<form action={formAction}>
<input>...</input>
<input>...</input>
</form>
)
}

实现授权

授权是通过 Next.js 的中间件机制完成的。

根目录 middleware.ts,文件名不能自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { auth } from "@/auth"

export default auth((req) => {
if (!req.auth && req.nextUrl.pathname !== "/login") {
const newUrl = new URL("/login", req.nextUrl.origin)
return Response.redirect(newUrl)
}
else if (req.auth && !req.nextUrl.pathname.startsWith("/dashboard")) {
const newUrl = new URL("/dashboard", req.nextUrl.origin)
return Response.redirect(newUrl)
}
})

// 必需;让前端页面可以正常地访问图片等资源,无需认证。
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}

动机

KGW 模型使用在生成和检测水印时使用的是同一个 key(包括 hash function 和 random number generator)。只有 key 的持有者才能检测水印。这意味着检测的过程是不能公开的。

如果在生成和检测水印的过程中使用不同的 key,这样就可以只公开检测水印的 key,即能让公众检测水印,也不会泄露生成水印的 key。

生成水印

已有一个语言模型,希望得到一个能够在输出中嵌入水印的新语言模型。

在生成文本的过程中,现在需要生成第 n 个 token $x_n$。

记窗口大小为 w,根据前面 w - 1 个 token 计算第 n 个位置的 logits,取最高的 K 个备选 token,记为 ${x_n^{(0)},…, x_n^{(K-1)}}$

在这 K 个备选 token 中,划分一部分作为绿列表(具体方法见后文),提高 logits 值。

修正 logits 后的新语言模型即为所求。

求绿列表

求窗口内所有 token 的 embedding,然后将它们拼接起来,输入全连接神经网络 FFN 中,得到最后一个 token 是否为绿列表中的元素。

$$
W(x_{n-w+1:n}) = FFN(E(x_{n-w+1})\oplus …\oplus E(x_{n}))
$$

难点是如何让神经网络保持生成 $\gamma$ 比例的绿列表。

利用 z 检验进行水印检测

考虑神经网络不能严格生成 $\gamma$ 比例的绿列表,只能在期望上等于 $\gamma$,所以需要在 z 检验中考虑绿列表比例的方差。

利用神经网络进行水印检测

采用二分类长短期记忆网络(LSTM)检测水印。

不可伪造性

神经网络是不可逆的。很难根据输出推断输入。

用水印生成模型的结果训练水印检测模型。但已知水印检测模型,很难反过来推测出水印生成模型的训练输入和输出。

嵌入水印

将整个单词表划分成 r 份。

我们的目标是将一段水印信息嵌入到生成的文本中。将这段水印信息编码为长度为 b,每位取值为 ${0,…,r-1}$ 的串。记这个编码串为 m。

生成一个 token 时,随机取 ${0,…,b-1}$ 中的一个数 p。倾向于从 $m[p]$ 单词表中选 token。

检测水印

m 中不同位置有不同数字,代表不同词汇表子集。数字出现的频率分布和词汇的分布正相关。假如 m 中很多位置的值都是 3,那么最后生成的文本中很多 token 都来自于第 3 个词汇表子集。可以通过检测词频来判断是否存在水印。

为 m 中每个位置 i 维护 r 个计数器 ${W_i[0],…,W_i[r-1]}$。如果一个 token 对应 m 中的位置 i,且位于 j 词汇表时,$W_i[j]$ 加 1。如果该文本中有水印,那么使得 $W_i$ 最大的词汇表 j 应该为 $m[i]$ 词汇表。

根据假设检验的知识,设零假设为无水印,备择假设为有水印。零假设下每个位置计数器的分布应该为一个0 到 r-1 的多项分布,每个值的概率均为 $\gamma$。如果能够发现明显的偏差,就能推翻零假设。

如何为每个 token 分配 位置 p(对应论文 Position Allocating 部分)

一个 Bad idea 是轮转分配。缺点是很容易被攻击破坏水印。

论文中采取的方法是 hash。先通过伪随机函数(相当于 hash function)根据窗口内的文本生成一个 hash value。然后再将这个 hash value 作为随机数产生器用于产生位置 p。

List Decoding

解码的结果可能有错。为了减少错误的影响,可以让解码算法不仅仅返回一个结果,而是返回一系列相近的结果。

讨论

内容承载量和检测效果之间存在 trade-off。

r 越大,意味着嵌入信息的长度越短,但是也意味着每个位置的可能指示的值。

List Decoding 是有效的。置信度和错误率是负相关的,越高的置信度代表越低的错误率。和随机输出相比,List Decoding 是有效的。