MoreRSS

site iconDerobukal修改

博客名:御坂研究所,一个偏软件开发和技术实践的博客。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Derobukal的 RSS 预览

《推荐系统实践》

2025-06-22 18:54:59

从某种意义上说,推荐系统和搜索引擎对于用户来说是两个互补的工具。搜索引擎满足了用户有明确目的时的主动查找需求,而推荐系统能够在用户没有明确目的的时候帮助他们发现感兴趣的新内容。

基于用户行为分析的推荐算法是个性化推荐系统的重要算法,学术界一般将这种类型的算法称为协同过滤(Collaborative filtering)算法。顾名思义,协同过滤就是指用户可以齐心协力,通过不断地和网站互动,使自己的推荐列表能够不断过滤掉自己不感兴趣的物品,从而越来越满足自己的需求。

用户行为分类

用户行为在个性化推荐系统中一般分两种——显性反馈行为(explicit feedback)和隐性反馈行为(implicit feedback)。显示反馈行为是用户主动做的,比如给视频点赞、给书籍打分等等;隐式反馈行为的代表就是用户浏览页面,这种行为显示出来的用户偏好不是那么明显,但是数据量更大。

常用算法

基于邻域的算法

  • 基于用户的协同过滤算法 这种算法给用户推荐和他兴趣相似的其他用户喜欢的物品。
    1. 找到和目标用户兴趣相似的用户集合(P45)。
    2. 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。
  • 基于物品的协同过滤算法 这种算法给用户推荐和他之前喜欢的物品相似的物品。
    1. 计算物品之间的相似度(P53)。
    2. 根据物品的相似度和用户的历史行为给用户生成推荐列表。

基于用户的协同过滤算法

计算两个用户的兴趣相似程度:给定用户u和用户v,N(u)表示用户u曾经有过正反馈的物品集合,N(v)表示用户v曾经有过正反馈的物品集合。可以使用Jaccard公式计算两个用户的兴趣相似程度

wuv=|N(u)N(v)||N(u)N(v)|

或者使用余弦相似公式计算相似程度

wuv=|N(u)N(v)||N(u)||N(v)|

以余弦相似公式为例,假设有用户ABCD,物品abcde,用户喜欢的物品如下

用户 物品 a 物品 b 物品 c 物品 d 物品 e
A ☑️ ☑️ ☑️
B ☑️ ☑️
C ☑️ ☑️
D ☑️ ☑️ ☑️

那么我们可以得到用户A和BCD的相似度

wAB=|{a,b,d}{a,c}||{a,b,d}||{a,c}|=16

wAC=|{a,b,d}{b,e}||{a,b,d}||{b,e}|=16

wAD=|{a,b,d}{c,d,e}||{a,b,d}||{c,d,e}|=13

具体计算过程以AD的相似度计算为例

  1. 分子为交集并且交集为 {d}|{d}| = 1,所以分子为1
  2. 分母为并集,3 x 3 = 9,开根号为3
    最终值为 1 / 3

以上逻辑可以用代码进行实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def similarity(users):
w = defaultdict(dict)
for u, v in combinations(users.keys(), 2):
r1 = len(users[u] & users[v])
r2 = math.sqrt(len(users[u]) * len(users[v]) * 1.0)
r = r1 / r2
w[u][v], w[v][u] = r, r # 保存两次,方便后面使用
return w

def main():
users = {
'A': {'a', 'b', 'd'},
'B': {'a', 'c'},
'C': {'b', 'e'},
'D': {'c', 'd', 'e'}
}
for k, v in similarity(users).items():
print(f'{k}: {json.dumps(v)}')

执行后得到结果如下

A: {"B": 0.4082482904638631, "C": 0.4082482904638631, "D": 0.3333333333333333}B: {"A": 0.4082482904638631, "C": 0.0, "D": 0.4082482904638631}C: {"A": 0.4082482904638631, "B": 0.0, "D": 0.4082482904638631}D: {"A": 0.3333333333333333, "B": 0.4082482904638631, "C": 0.4082482904638631}

据此我们就可以得到各个用户之间的兴趣相似度了。有了用户兴趣的相似度之后,我们可以给用户推荐和他兴趣最相似的K个用户喜欢的物品。我们可以使用如下公式计算用户u对物品i的感兴趣程度

p(u,i)=vS(u,K)N(i)wuvrvi

其中,S(u, K)包含和用户u兴趣最接近的K个用户,N(i)是对物品i有过行为的用户集合,wuv是用户u和用户v的兴趣相似度,rvi代表用户v对物品i的兴趣,因为使用的是单一行为的隐反馈数据,所以所有的rvi=1。

具体的逻辑实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def recommend(user, users, w, k):
"""
:param user: 计算指定用户的物品推荐程度
:param users: 数据集
:param w: 前一步计算得到的用户兴趣相似度
:param k: 取k个兴趣最相似的用户
:return:
"""
rank = defaultdict(float)
# 获取指定用户和其它用户的兴趣相似度,并按照相似度从大到小排序,取前k个数据
for v, wuv in sorted(w[user].items(), key=lambda item: item[1], reverse=True)[:k]:
# 取出指定用户的数据集
for i in users[v]:
# 如果这个数据已经在当前用户的数据集里面,跳过,因为已经感兴趣的数据不需要再次推荐
if i in users[user]:
continue
rank[i] += wuv
return rank

通过这个代码我们就可以计算得到指定用户的物品推荐程度了。完整的代码如下:

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
import json
import math
from collections import defaultdict
from itertools import combinations


def similarity(users):
w = defaultdict(dict)
for u, v in combinations(users.keys(), 2):
r1 = len(users[u] & users[v])
r2 = math.sqrt(len(users[u]) * len(users[v]) * 1.0)
r = r1 / r2
w[u][v], w[v][u] = r, r # 保存两次,方便后面使用
return w


def recommend(user, users, w, k):
"""
:param user: 计算指定用户的物品推荐程度
:param users: 数据集
:param w: 前一步计算得到的用户兴趣相似度
:param k: 取k个兴趣最相似的用户
:return:
"""
rank = defaultdict(float)
# 获取指定用户和其它用户的兴趣相似度,并按照相似度从大到小排序,取前k个数据
for v, wuv in sorted(w[user].items(), key=lambda item: item[1], reverse=True)[:k]:
# 取出指定用户的数据集
for i in users[v]:
# 如果这个数据已经在当前用户的数据集里面,跳过,因为已经感兴趣的数据不需要再次推荐
if i in users[user]:
continue
rank[i] += wuv
return rank


def main():
users = {
'A': {'a', 'b', 'd'},
'B': {'a', 'c'},
'C': {'b', 'e'},
'D': {'c', 'd', 'e'}
}

w = similarity(users)
for k, v in w.items():
print(f'{k}: {json.dumps(v)}')

rank = recommend('C', users, w, 3)
for k, v in sorted(rank.items(), key=lambda item: item[1], reverse=True):
print(f'{k}: {v}')


if __name__ == '__main__':
main()

执行代码得到的结果如下

A: {"B": 0.4082482904638631, "C": 0.4082482904638631, "D": 0.3333333333333333}B: {"A": 0.4082482904638631, "C": 0.0, "D": 0.4082482904638631}C: {"A": 0.4082482904638631, "B": 0.0, "D": 0.4082482904638631}D: {"A": 0.3333333333333333, "B": 0.4082482904638631, "C": 0.4082482904638631}d: 0.8164965809277261a: 0.4082482904638631c: 0.4082482904638631

由上面的结果我们可以知道,针对用户C,最推荐的物品是物品d

根据上面的例子我们已经简单了解了基于用户的协同过滤算法,不过这种算法存在问题,主要是

  1. 随着网站的用户数目越来越大,计算用户兴趣相似度矩阵将越来越困难,其运算时间复杂度和空间复杂度的增长和用户数的增长近似于平方关系
  2. 基于用户的协同过滤很难对推荐结果作出解释

因此,在实际的使用中,更常见的是基于物品的协同过滤算法

基于物品的协同过滤算法

为了挖掘长尾信息,避免热门物品对推荐产生影响,减小二八定律的出现。可以用如下公式计算物品之间的相似度

wij=|N(i)N(j)||N(i)||N(j)|

分子是同时喜欢物品i和物品j的用户数,分母是喜欢两个物品用户数的并集。为了减小计算量,我们可以构建一个矩阵来存储某个用户喜欢的物品集合。

举个例子,比如用户A喜欢物品 {a, b, d},那我们可以构建如下矩阵

    |  a  |  b  |  c  |  d  |  e  |----|-----|-----|-----|-----|-----|a   |  0  |  1  |  0  |  1  |  0  |b   |  1  |  0  |  0  |  1  |  0  |c   |  0  |  0  |  0  |  0  |  0  |d   |  1  |  1  |  0  |  0  |  0  |e   |  0  |  0  |  0  |  0  |  0  |

因为a、b、d可以组成ab、ad、bd,所以将矩阵中的对应位置都填上1。这是一个用户的物品信息,对于多个用户,只需要把这些矩阵相加即可。例如有5个用户,他们的物品信息和生成的对应物品矩阵如下

用户 1: {a, c, d}

    |  a  |  b  |  c  |  d  |  e  |----|-----|-----|-----|-----|-----|a   |  0  |  0  |  1  |  1  |  0  |b   |  0  |  0  |  0  |  0  |  0  |c   |  1  |  0  |  0  |  1  |  0  |d   |  1  |  0  |  1  |  0  |  0  |e   |  0  |  0  |  0  |  0  |  0  |

用户 2: {b, c, e}

    |  a  |  b  |  c  |  d  |  e  |----|-----|-----|-----|-----|-----|a   |  0  |  0  |  0  |  0  |  0  |b   |  0  |  0  |  1  |  0  |  1  |c   |  0  |  1  |  0  |  0  |  1  |d   |  0  |  0  |  0  |  0  |  0  |e   |  0  |  1  |  1  |  0  |  0  |

用户 3: {a, d, e}

    |  a  |  b  |  c  |  d  |  e  |----|-----|-----|-----|-----|-----|a   |  0  |  0  |  0  |  1  |  1  |b   |  0  |  0  |  0  |  0  |  0  |c   |  0  |  0  |  0  |  0  |  0  |d   |  1  |  0  |  0  |  0  |  1  |e   |  1  |  0  |  0  |  1  |  0  |

用户 4: {b, d}

    |  a  |  b  |  c  |  d  |  e  |----|-----|-----|-----|-----|-----|a   |  0  |  0  |  0  |  0  |  0  |b   |  0  |  0  |  0  |  1  |  0  |c   |  0  |  0  |  0  |  0  |  0  |d   |  0  |  1  |  0  |  0  |  0  |e   |  0  |  0  |  0  |  0  |  0  |

用户 5: {a, b, c, e}

    |  a  |  b  |  c  |  d  |  e  |----|-----|-----|-----|-----|-----|a   |  0  |  1  |  1  |  0  |  1  |b   |  1  |  0  |  1  |  0  |  1  |c   |  1  |  1  |  0  |  0  |  1  |d   |  0  |  0  |  0  |  0  |  0  |e   |  1  |  1  |  1  |  0  |  0  |

将这5个用户的物品信息相加,得到矩阵

    |  a  |  b  |  c  |  d  |  e  |----|-----|-----|-----|-----|-----|a   |  0  |  1  |  2  |  3  |  2  |b   |  1  |  0  |  3  |  2  |  3  |c   |  2  |  3  |  0  |  2  |  3  |d   |  3  |  2  |  2  |  0  |  2  |e   |  2  |  3  |  3  |  2  |  0  |

在这个矩阵中值越高,代表物品的相关度越高。接下来我们将这个规则用代码进行实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
number = 'number'
def item_similarity(train):
# c[i][number]表示使用物品i的用户数量
# c[i][j]表示同时交互物品i和j的用户数
c = defaultdict(lambda: defaultdict(int))
for user, items in train.items():
for i in items:
# 统计每个物品被交互的总次数
c[i][number] += 1
# 统计物品i与其他物品的共现次数
for j in items:
if i == j:
continue
c[i][j] += 1
# 计算最终的相似度矩阵 w
w = defaultdict(dict)
for i, related_items in c.items():
for j, cij in related_items.items():
if j == number: continue
# 余弦相似度公式
similarity = cij / math.sqrt(c[i][number] * c[j][number])
w[i][j] = similarity
return w

如上我们先计算每个物品各自被用户喜欢的次数,再计算每个物品和其它物品同时被某个用户喜欢的次数,之后根据物品相似度公式即可计算出物品之间的相关性。为了简单起见,如上代码只使用了一个字典变量,物品自己被喜欢的次数被保存在key为number的字段中,物品和其它物品同时被喜欢的次数则保存在key为其它物品ID的字段中。

有了如上逻辑之后,我们就可以计算物品相似度了,假设有用户如下

{  'A': {'a', 'b', 'd'},  'B': {'a', 'c'},  'C': {'b', 'e', 'a'},  'D': {'c', 'd', 'e'}}

计算得到的物品相似度

b: {'a': 0.8164965809277261, 'd': 0.5, 'e': 0.5}a: {'b': 0.8164965809277261, 'd': 0.4082482904638631, 'c': 0.4082482904638631, 'e': 0.4082482904638631}d: {'b': 0.5, 'c': 0.5, 'e': 0.5, 'a': 0.4082482904638631}c: {'e': 0.5, 'd': 0.5, 'a': 0.4082482904638631}e: {'b': 0.5, 'c': 0.5, 'd': 0.5, 'a': 0.4082482904638631}

可以看到物品a和物品b的相关度是最高的。在得到物品的相关度之后,我们可以使用如下公式计算用户u对一个物品j的兴趣

puj=iN(u)S(j,K)wjirui

N(u)是用户喜欢的物品集合,S(j, K)是物品j最相似的K个物品的集合,wji是物品j和i的相似度,rui是用户对物品i的兴趣,可令rui为1。我们可以把这个逻辑使用代码进行实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def recommend(interacted_items: Union[set, dict], w, k):
"""
:param interacted_items: 指定用户交互过的物品
:param w: 物品的相似度
:param k: 取最相似的k个物品
:return:
"""
if isinstance(interacted_items, set): # 如果只有物品,没有评分,那么将评分统一设置为1
interacted_items = {k: 1 for k in interacted_items}
rank = defaultdict(float)
# 用户交互过的物品,和用户对这个物品的评分
for item, score in interacted_items.items():
# 物品的相似度信息,得到related_item和item的相似度similarity,按照相似度的值从大到小排序,取k个值
for related_item, similarity in sorted(w[item].items(), key=lambda x: x[1], reverse=True)[:k]:
# 如果这个物品已经被用户交互过了,跳过
if related_item in interacted_items:
continue
# 计算相关的物品的相似度评分
rank[related_item] += score * similarity
return rank

有了计算用户和物品相关度的代码,我们就可以把逻辑结合起来,实现向用户推荐物品了。完整的代码实现如下

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
import math
from collections import defaultdict
from typing import Union

number = 'number'


def item_similarity(train):
# c[i][number]表示使用物品i的用户数量
# c[i][j]表示同时交互物品i和j的用户数
c = defaultdict(lambda: defaultdict(int))
for user, items in train.items():
for i in items:
# 统计每个物品被交互的总次数
c[i][number] += 1
# 统计物品i与其他物品的共现次数
for j in items:
if i == j:
continue
c[i][j] += 1

# 计算最终的相似度矩阵 w
w = defaultdict(dict)
for i, related_items in c.items():
for j, cij in related_items.items():
if j == number: continue
# 余弦相似度公式
similarity = cij / math.sqrt(c[i][number] * c[j][number])
w[i][j] = similarity

return w


def recommend(interacted_items: Union[set, dict], w, k):
"""
:param interacted_items: 指定用户交互过的物品
:param w: 物品的相似度
:param k: 取最相似的k个物品
:return:
"""
if isinstance(interacted_items, set): # 如果只有物品,没有评分,那么将评分统一设置为1
interacted_items = {k: 1 for k in interacted_items}
rank = defaultdict(float)
# 用户交互过的物品,和用户对这个物品的评分
for item, score in interacted_items.items():
# 物品的相似度信息,得到related_item和item的相似度similarity,按照相似度的值从大到小排序,取k个值
for related_item, similarity in sorted(w[item].items(), key=lambda x: x[1], reverse=True)[:k]:
# 如果这个物品已经被用户交互过了,跳过
if related_item in interacted_items:
continue
# 计算相关的物品的相似度评分
rank[related_item] += score * similarity
return rank


def main():
users = {
'A': {'a', 'b', 'd'},
'B': {'a', 'c'},
'C': {'b', 'e', 'a'},
'D': {'c', 'd', 'e'}
}
w = item_similarity(users)
for k, v in w.items():
print(f'{k}: {dict(sorted(v.items(), key=lambda item: item[1], reverse=True))}')

rank = recommend(users['B'], w, 3)
for k, v in sorted(rank.items(), key=lambda item: item[1], reverse=True):
print(f'{k}: {v}')


if __name__ == '__main__':
main()

以上代码的执行结果如下,可见用户B和物品d的相关度最高

b: {'a': 0.8164965809277261, 'd': 0.5, 'e': 0.5}d: {'b': 0.5, 'c': 0.5, 'e': 0.5, 'a': 0.4082482904638631}a: {'b': 0.8164965809277261, 'd': 0.4082482904638631, 'c': 0.4082482904638631, 'e': 0.4082482904638631}c: {'d': 0.5, 'e': 0.5, 'a': 0.4082482904638631}e: {'b': 0.5, 'c': 0.5, 'd': 0.5, 'a': 0.4082482904638631}d: 0.9082482904638631b: 0.8164965809277261e: 0.5

基于物品的推荐在工程中使用的比基于用户的推荐要多,因为UserCF(User Collaborative Filtering)的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度,而ItemCF(Item Collaborative Filtering)的推荐更加个性化,反映了用户自己的兴趣传承。

LFM(latent factor model)隐语义模型

隐语义模型核心思想是通过隐含特征(latent factor)联系用户兴趣和物品,它可以通过对数据进行分类来实现推荐。这种基于用户对数据的兴趣分类的方式,需要解决如下三个问题:

  1. 如何给物品分类
  2. 如何确定用户对哪些分类感兴趣,以及感兴趣的程度
  3. 对于一个分类,选择哪些物品推荐给用户,以及这些物品的权重如何

隐含语义分析技术(latent variable analysis)采取基于用户行为统计的自动聚类,来实现数据自动分类。

基于图的模型

冷启动:如何向刚刚注册的用户进行推荐

标签系统

利用上下文进行推荐

常用上下文:时间 & 地点

https://book.douban.com/subject/10769749/

《上瘾:让用户养成使用习惯的四大产品逻辑》

2025-06-19 01:44:52

Hooked: How to Build Habit-Forming Products

如何卖出更多的产品:产能 -> 营销/渠道 -> 产品设计

上瘾如何设计产品:触发 -> 行动 -> 多变的酬赏 -> 投入

习惯是大脑借以掌握复杂举动的途径之一。神经系统科学家指出,人脑中存在一个负责无意识行为的基底神经节,那些无意中产生的条件反射会以习惯的形式存储在基底神经节中,从而使人们腾出精力来关注其他的事物。当大脑试图走捷径而不再主动思考接下来该做些什么时,习惯就养成了。为解决当下面临的问题,大脑会在极短的时间内从行为存储库里提取出相宜的对策。
(就是基底核,有点像缓存的作用)

我们所要描述的体验更接近于“痒”,它是潜伏于我们内心的一种渴求,当这种渴求得不到满足时,不适感就会出现。那些让我们养成某种习惯的产品正好可以缓解这种不适感。比起听之任之的做法,利用技术或产品来”挠痒痒”能够更快地满足我们的渴求。一旦我们对某种技术或产品产生依赖,那它就是唯一的灵丹妙药了。

福格行为模型可以用公式来呈现,即B=MAT。B代表行为,M代表动机,A代表能力,T代表触发。要想使人们完成特定的行为,动机、能力、触发这三样缺一不可。1否则,人们将无法跨过”行动线”,也就是说,不会实施某种行为。

  • 稀缺效应:物以稀为贵
  • 环境效应:环境会影响人们的价值判断
  • 锚定效应
  • 赠券效应

多变的酬赏主要表现为三种形式:社交酬赏,猎物酬赏,自我酬赏

沉没成本:通过用户对产品的投入程度,留住用户

总体评价:很薄的一本书,有部分的观点有参考意义,但是大部分的论调都是老生常谈。大多数的观点在很多心理学的书籍里面已经讲过了,本书主要是讲怎么依赖于这些原理来进行实操,有一定的参考意义。

https://book.douban.com/subject/27030507/

Kotlin与Java对照手册

2025-04-24 22:21:27

Generated By AI

📗 1. 基本类型

类型 Kotlin 写法 Java 写法 简要说明
数字 Int, Long, Float, Double, Short, Byte int, long, float, double, short, byte Kotlin 数值类型映射到相应的原生/包装类型。
布尔 Boolean boolean 只能取 true/false,与数字不互通。
字符 Char char 单个 Unicode 字符,支持转义序列。
字符串 String String 不可变;支持多行文本块 """..."""
数组 Array<T>, IntArray T[] 提供原始类型专用数组如 IntArrayByteArray
无符号整型 UInt, ULong, UShort, UByte 编译时检查范围,运行时越界抛 IllegalArgumentException

📘 2. 语法对照

功能 Java 写法 Kotlin 写法 简要说明
变量定义 int x = 10; final String name = "Tom"; var x = 10 val name = "Tom" var 可变,val 只读;类型由编译器推断。
类 + 构造 public class P { P(String n) { ... } } class P(val name: String) 主构造中声明属性,自动生成字段 & 访问器。
数据类 手动写字段/构造/equals/toString data class User(val id: Int, val n: String) data 自动生成常用方法 & 解构组件。
函数定义 public int sum(int a, int b) { return a + b; } fun sum(a: Int, b: Int) = a + b 表达式函数可省略大括号和 return
空安全 if (s != null) len = s.length(); else len = 0; val len = s?.length ?: 0 String? 可空,?.?: 插入编译期空检查。
分支匹配 switch(x) { case 1: ... } when(x) { 1 -> ...; else -> ... } when 是表达式,支持范围 & 任意对象比较。
循环 & 集合 for(int i=0;i<10;i++)``list.stream().filter() for(i in 0 until 10)``list.filter{} 0 until 生成 IntRange;集合链式调用基于扩展函数。
单例 class S { private static S i=new S(); … } object S { fun foo() {} } object 编译时生成线程安全单例,无需额外样板。

✨ 3. 独有亮点

特性 示例 简要说明
默认 & 命名参数 fun g(msg: String = "Hi", name: String = "You") g(name="Tom") 编译器生成默认方法,命名参数避免重载歧义。
扩展函数 fun String.ex() = uppercase() 编译后为静态方法,第一个参数是接收者,调用如成员方法。
解构声明 val (x, y) = Point(1, 2) data class 自动生成 componentN(),一行取多值。
密封类 sealed class R; data class Ok(val d: String): R(); object Err: R() 限定子类范围,when 可做穷尽检查。
内联函数 inline fun <T> m(b: ()->T): T { … } 在调用处展开函数体,减少高阶函数的运行时开销。
集合构造器 listOf(1, 2), mutableListOf("A"), mapOf("a" to 1) 内建集合工厂函数,语法简洁;to 表示键值对。
数组构造器 arrayOf(1, 2), intArrayOf(1, 2) 支持泛型与原始类型数组,避免装箱。
表达式返回值 val max = if (a > b) a else b val result = try { … } catch { … } ifwhentry 都是表达式,可直接赋值。
区间语法 & 步进 for (i in 1..5), for (j in 1 until 5 step 2) .. 表闭区间,until 表半开,step 控制步长。
字符串模板 "Hello, $name" "Length: ${s.length}" $变量 可直接拼接,复杂表达式用 ${}
Lambda 尾随语法 list.filter { it > 0 }.map { it * 2 } 大括号可直接跟随函数调用,链式语法自然、简洁。

📚 4. 常用标准库函数

函数 用法示例 简要说明
let user?.let { print(it.name) } 非空时执行块,it 引用原对象。
apply User().apply { age = 18 } 在对象上执行块并返回该对象,常用于初始化。
also list.also { println("init") } 执行副作用并返回对象,常用于日志 / 调试。
run val r = run { compute(); result } 无接收者的作用域块,返回最后一行结果。
with with(cfg) { load(); validate() } 对象上下文块,this 指向接收者,返回结果。
takeIf str.takeIf { it.isNotBlank() } 条件为真返回对象,否则返回 null
sequence sequenceOf(1,2,3).map { … } 惰性集合处理,适合大规模数据管道。

🧩 5. 类型系统对比

功能 Java 写法 Kotlin 写法 简要说明
泛型 List<String> List<String> 支持协变 / 逆变(out / in)和 reified 泛型函数。
类型别名 typealias Name = String 简化复杂类型声明。
枚举类 enum Color { RED, GREEN } enum class Color { RED, GREEN } 支持在枚举中定义属性 & 方法。
内联类 @JvmInline value class USD(val amount: Int) 编译时包装或展开,零开销封装。

🔍 6. 类型检测与转换

功能 Java 写法 Kotlin 写法 简要说明
类型检查 if (obj instanceof String) if (obj is String) is 后自动智能转换,无需显式强转。
安全转换 (String) obj obj as String / obj as? String as? 安全转换失败返回 null
基本转换 Integer.parseInt(str) str.toInt(), toDouble(), toLong() 通过扩展函数提供常见类型转换。

🔄 7. 控制流程 & 异常

功能 Java 写法 Kotlin 写法 简要说明
条件 & 循环 if, switch, for, while, do-while if, when, for, while, do-while when 可做表达式,替代 switch
返回 & 跳转 return, break, continue, throw 同 Java 支持在 lambda 中局部返回,如 return@label
异常处理 try-catch-finally, checked exception try-catch-finally,无 checked exception Kotlin 不区分受检异常,简化错误处理。

📦 8. 包与导入

功能 Java 写法 Kotlin 写法 简要说明
包声明 package com.example; package com.example 不需要分号。
导入 import java.util.List; import java.util.List 支持导入顶层函数和属性。
别名导入 import foo.Bar as Baz 解决命名冲突或简化引用。

⚙️ 9. 面向对象相关

功能 Java 写法 Kotlin 写法 简要说明
接口默认实现 default void f() {} 接口中可直接写方法体 接口内方法可有实现,无需关键字。
抽象类 abstract class Shape { … } abstract class Shape { … } 抽象成员不需再加 abstract 前缀。
继承 & 覆写 class A extends B { @Override … } class A : B() { override fun … } : 表示继承,override 必显式标注。
可见性修饰符 public/protected/private public/protected/private/internal internal 表示同模块内可见。
内部类 class Outer { class Inner {} } class Outer { inner class Inner {} } 默认是静态嵌套,加 inner 变为非静态内部类。

🧵 10. 协程 vs 多线程

场景 Java 写法(线程/异步) Kotlin 写法(协程) 简要说明
启动任务 new Thread(() -> work()).start(); GlobalScope.launch { work() } 协程更轻量、省资源,适合大规模并发。
异步返回值 Future<Integer> f = exec.submit(...); val result = async { compute() }.await() 内建 async/await,语义更清晰。
延迟执行 Thread.sleep(1000) delay(1000) 非阻塞挂起,不占用线程。
结构化并发 手动管理线程池和生命周期 coroutineScope { … } 协程作用域自动管理生命周期,避免泄漏。

📦 11. 集合操作对比

功能 Java 写法(Stream) Kotlin 写法(扩展函数) 简要说明
过滤 list.stream().filter(x -> x > 0).collect(...) list.filter { it > 0 } 语法简洁,链式调用更直观。
映射 list.stream().map(x -> x * 2).collect(...) list.map { it * 2 } Lambda 简洁,扩展函数无额外依赖。
分组 Collectors.groupingBy(...) list.groupBy { it.key } 直接返回 Map<K, List<V>>,更易读。
排序 list.sort(Comparator.comparing(...)) list.sortedBy { it.prop } 函数式排序,链式可读性好。
聚合 reduce, sum, collect reduce, sumOf, fold 内建多种聚合函数,常用时无需额外导入。

持续更新中…

参考:

https://book.kotlincn.net/text/d-basics.html

ComfyUI的操作与使用

2025-03-31 19:21:10

简单起见,又或者是没有N卡或者显卡的配置比较低,可以使用腾讯云HAI直接搭建ComfyUI服务。

入门使用

可以先clear掉当前的workflow,之后右键新建模块。

首先需要新建一个采样器(KSampler),采样器的配置如下:

参数 说明
seed 0 随机种子,控制生成结果的随机性。相同种子会产生相同结果
control_after_generate randomize 生成后种子控制方式:randomize(随机化)、increment(递增)、decrement(递减)、fixed(固定)
steps 20 采样步数,通常15-30步较为合适。步数越多质量越好但耗时更长
cfg 8.000 CFG引导强度,控制AI对提示词的遵循程度。范围1-20,推荐7-12
sampler_name euler 采样算法:euler、euler_a、dpm_2、dpm_2_ancestral、lms、ddim等
scheduler normal 调度器类型:normal、karras、exponential、sgm_uniform等
denoise 1.000 去噪强度,1.0为完全去噪,0.0为不去噪。图生图时可调节此值

采样器的model可以选择Load Checkpoint。
positive是正向提示词,negative是反向提示词,都可以选择CLIPTextEncode。为了方便区分,可以给prompt修改一个有意义的标题。
latent_image可以设置图片的选项,例如EmptyLatentImage。
LATENT可以选择VAE Decode模块,然后可以添加一个图片预览模块。

多图合并,使用同一个背景

输出大小设置为图片背景总大小。
图片正向prompt输出拖拽,然后添加Conditioning(Set Area)节点,在其中设置图片的大小和图片在背景画布中的位置。
使用ConditioningCombine节点,合并多个Conditioning节点,之后把合并节点的输出连接到KSample。
但是,如果是单纯两个prompt直接连到采样器,两张图片会很割裂。解决办法是再新建一个prompt,之后把两张图片的输出和这个prompt使用一个combine进行合并,之后合并结果再输出到采样器。

模型

CheckPoints(检查点模型)是Stable Diffusion的核心基础模型,包含了完整的图像生成能力。它是一个预训练的神经网络模型,决定了生成图像的整体风格、质量和特征。

常用CheckPoints模型类型:

模型类型 特点 适用场景 推荐模型
SD 1.5系列 经典基础模型,兼容性好 入门学习,插件丰富 v1-5-pruned-emaonly.ckpt
SDXL系列 更高分辨率,质量更好 高质量出图 sd_xl_base_1.0.safetensors
写实人像 专注真实人物生成 人像摄影、写实风格 realisticVisionV60B1_v51VAE.safetensors
动漫二次元 卡通动漫风格 动漫插画、角色设计 anything-v5-PrtRE.safetensors
艺术绘画 艺术风格强烈 创意艺术、概念设计 dreamshaper_8.safetensors
建筑风景 专注场景和建筑 建筑设计、风景画 architectureExterior_v40.safetensors

模型文件格式:.ckpt:早期格式,文件较大;.safetensors:更安全的格式,加载速度快,推荐使用;.pt:PyTorch原生格式

使用LoRA模型:在主模型的MODEL节点拖拽,可以新增LoRA节点,之后把LoRA的模型替代主模型连接到采样器上面。同样的,主模型的CLIP需要连接到LoRA上面,之后把正向prompt连接到LoRA就可以了(反向prompt还是连接在CheckPoint的CLIP上面)。

LoRA和Checkpoint的定义区别如下

名称 定义
Checkpoint 模型的权重文件,是模型在某个训练时刻的完整状态的保存,通常包括整个模型的参数、优化器状态等。
LoRA(Low-Rank Adaptation) 一种参数高效的微调方法,不直接修改原始模型参数,而是在模型某些层中引入少量可训练参数,从而在不改变大模型主体的前提下实现微调。

它们有着各自不同的使用目的

比较项 Checkpoint LoRA
目标 保存和恢复训练过程或完整模型 节省参数、内存,快速高效地微调大模型
应用场景 模型训练中断恢复、部署模型 微调大语言模型、个性化调整、插件式部署
文件大小 通常非常大(几百MB到几十GB) 非常小(几MB到几十MB)

实际操作

首先我们打开腾讯云的HAI界面,并新建一个ComfyUI的服务

服务创建好了之后我们打开对应的ComfyUI服务地址,可以看到如下界面

这里我们禁用掉新版UI,并且安装Crystools节点

之后我们可以看到界面会变成如下样式

随后我们下载并安装realisticVision模型

创建好服务的密码等信息会通过站内信的方式进行发送,可以使用这些信息登录到机器上,之后在机器上面下载realisticVision模型

ComfyUI/models/checkpoints/文件夹里面下载该模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(base) root@VM-0-5-ubuntu:~# cd ComfyUI/models/checkpoints/
(base) root@VM-0-5-ubuntu:~/ComfyUI/models/checkpoints# wget 'https://huggingface.co/moiu2998/mymo/resolve/3c3093fa083909be34a10714c93874ce5c9dabc4/realisticVisionV60B1_v51VAE.safetensors'
--2025-06-03 02:31:39-- https://huggingface.co/moiu2998/mymo/resolve/3c3093fa083909be34a10714c93874ce5c9dabc4/realisticVisionV60B1_v51VAE.safetensors
Resolving huggingface.co (huggingface.co)... 3.164.110.3, 3.164.110.114, 3.164.110.77, ...
Connecting to huggingface.co (huggingface.co)|3.164.110.3|:443... connected.
HTTP request sent, awaiting response... 302 Found
...
HTTP request sent, awaiting response... 200 OK
Length: 4265096996 (4.0G) [binary/octet-stream]
Saving to: 'realisticVisionV60B1_v51VAE.safetensors'

realisticVisionV60B1_v51VAE.safetensors 100%[=====================================================================================================================================>] 3.97G 11.3MB/s in 5m 57s

2025-06-03 02:37:35 (11.4 MB/s) - 'realisticVisionV60B1_v51VAE.safetensors' saved [4265096996/4265096996]

之后点击Refresh按钮,可以在checkpoint节点里面看到我们刚刚下载的模型

我们使用这个模型根据一些prompt可以生成一张福特福克斯的图片

刚刚我们的模型是在huggingface上面下载的,civitai同样也可以下载模型

使用命令下载civitai的模型,这里需要注册账号生成token并把token填入到下载链接中

1
2
3
4
5
6
7
8
9
(base) root@VM-0-5-ubuntu:~/ComfyUI/models/checkpoints# wget --content-disposition 'https://civitai.com/api/download/models/501240?type=Model&format=SafeTensor&size=pruned&fp=fp16&token=${API_TOKEN}'
...
HTTP request sent, awaiting response... 200 OK
Length: 2132625894 (2.0G)
Saving to: 'realisticVisionV60B1_v51HyperVAE.safetensors'

realisticVisionV60B1_v51HyperVAE.safetensors 100%[=====================================================================================================================================>] 1.99G 11.5MB/s in 2m 59s

2025-06-03 02:54:00 (11.3 MB/s) - 'realisticVisionV60B1_v51HyperVAE.safetensors' saved [2132625894/2132625894]

刷新之后同样可以看到新的模型信息

之后我们使用civitai上面的推荐配置

使用推荐配置即可生成相似的图片

实际生成的图片如下,ComfyUI生成的图片会包含生成图片时用到的整个工作流,如果需要使用相同的工作流只需要在ComfyUI中导入该图片即可

如上图片的信息如下

prompt

1
{"4": {"inputs": {"ckpt_name": "realisticVisionV60B1_v51HyperVAE.safetensors"}, "class_type": "CheckpointLoaderSimple", "_meta": {"title": "Load Checkpoint"}}, "5": {"inputs": {"width": 512, "height": 512, "batch_size": 1}, "class_type": "EmptyLatentImage", "_meta": {"title": "Empty Latent Image"}}, "6": {"inputs": {"text": "instagram photo, front shot, portrait photo of a 24 y.o woman, wearing dress, beautiful face, cinematic shot, dark shot", "clip": ["4", 1]}, "class_type": "CLIPTextEncode", "_meta": {"title": "CLIP Text Encode (Prompt)"}}, "7": {"inputs": {"text": "(nsfw, naked, nude, deformed iris, deformed pupils, semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, anime, mutated hands and fingers:1.4), (deformed, distorted, disfigured:1.3), poorly drawn, bad anatomy, wrong anatomy, extra limb, missing limb, floating limbs, disconnected limbs, mutation, mutated, ugly, disgusting, amputation", "clip": ["4", 1]}, "class_type": "CLIPTextEncode", "_meta": {"title": "CLIP Text Encode (Prompt)"}}, "8": {"inputs": {"samples": ["10", 0], "vae": ["4", 2]}, "class_type": "VAEDecode", "_meta": {"title": "VAE Decode"}}, "9": {"inputs": {"filename_prefix": "ComfyUI", "images": ["8", 0]}, "class_type": "SaveImage", "_meta": {"title": "Save Image"}}, "10": {"inputs": {"add_noise": "enable", "noise_seed": 794547607621135, "steps": 6, "cfg": 1.5, "sampler_name": "dpmpp_sde", "scheduler": "normal", "start_at_step": 0, "end_at_step": 10000, "return_with_leftover_noise": "disable", "model": ["4", 0], "positive": ["6", 0], "negative": ["7", 0], "latent_image": ["5", 0]}, "class_type": "KSamplerAdvanced", "_meta": {"title": "KSampler (Advanced)"}}}

workflow

1
{"last_node_id": 10, "last_link_id": 16, "nodes": [{"id": 9, "type": "SaveImage", "pos": [1451, 189], "size": [210, 270], "flags": {}, "order": 6, "mode": 0, "inputs": [{"name": "images", "type": "IMAGE", "link": 9, "label": "images"}], "outputs": [], "properties": {}, "widgets_values": ["ComfyUI"]}, {"id": 4, "type": "CheckpointLoaderSimple", "pos": [26, 474], "size": [315, 98], "flags": {}, "order": 0, "mode": 0, "inputs": [], "outputs": [{"name": "MODEL", "type": "MODEL", "links": [10], "slot_index": 0, "label": "MODEL"}, {"name": "CLIP", "type": "CLIP", "links": [3, 5], "slot_index": 1, "label": "CLIP"}, {"name": "VAE", "type": "VAE", "links": [8], "slot_index": 2, "label": "VAE"}], "properties": {"Node name for S&R": "CheckpointLoaderSimple"}, "widgets_values": ["realisticVisionV60B1_v51HyperVAE.safetensors"]}, {"id": 7, "type": "CLIPTextEncode", "pos": [413, 389], "size": [425.27801513671875, 180.6060791015625], "flags": {}, "order": 3, "mode": 0, "inputs": [{"name": "clip", "type": "CLIP", "link": 5, "label": "clip"}], "outputs": [{"name": "CONDITIONING", "type": "CONDITIONING", "links": [12], "slot_index": 0, "label": "CONDITIONING"}], "properties": {"Node name for S&R": "CLIPTextEncode"}, "widgets_values": ["(nsfw, naked, nude, deformed iris, deformed pupils, semi-realistic, cgi, 3d, render, sketch, cartoon, drawing, anime, mutated hands and fingers:1.4), (deformed, distorted, disfigured:1.3), poorly drawn, bad anatomy, wrong anatomy, extra limb, missing limb, floating limbs, disconnected limbs, mutation, mutated, ugly, disgusting, amputation"]}, {"id": 8, "type": "VAEDecode", "pos": [1209, 188], "size": [210, 46], "flags": {}, "order": 5, "mode": 0, "inputs": [{"name": "samples", "type": "LATENT", "link": 16, "label": "samples"}, {"name": "vae", "type": "VAE", "link": 8, "label": "vae"}], "outputs": [{"name": "IMAGE", "type": "IMAGE", "links": [9], "slot_index": 0, "label": "IMAGE"}], "properties": {"Node name for S&R": "VAEDecode"}, "widgets_values": []}, {"id": 10, "type": "KSamplerAdvanced", "pos": [862.98046875, 167.984375], "size": [315, 334], "flags": {}, "order": 4, "mode": 0, "inputs": [{"name": "model", "type": "MODEL", "link": 10, "label": "model"}, {"name": "positive", "type": "CONDITIONING", "link": 11, "label": "positive"}, {"name": "negative", "type": "CONDITIONING", "link": 12, "label": "negative"}, {"name": "latent_image", "type": "LATENT", "link": 15, "label": "latent_image"}], "outputs": [{"name": "LATENT", "type": "LATENT", "links": [16], "label": "LATENT"}], "properties": {"Node name for S&R": "KSamplerAdvanced"}, "widgets_values": ["enable", 794547607621135, "randomize", 6, 1.5, "dpmpp_sde", "normal", 0, 10000, "disable"]}, {"id": 6, "type": "CLIPTextEncode", "pos": [415, 186], "size": [422.84503173828125, 164.31304931640625], "flags": {}, "order": 2, "mode": 0, "inputs": [{"name": "clip", "type": "CLIP", "link": 3, "label": "clip"}], "outputs": [{"name": "CONDITIONING", "type": "CONDITIONING", "links": [11], "slot_index": 0, "label": "CONDITIONING"}], "properties": {"Node name for S&R": "CLIPTextEncode"}, "widgets_values": ["instagram photo, front shot, portrait photo of a 24 y.o woman, wearing dress, beautiful face, cinematic shot, dark shot"]}, {"id": 5, "type": "EmptyLatentImage", "pos": [473, 609], "size": [315, 106], "flags": {}, "order": 1, "mode": 0, "inputs": [], "outputs": [{"name": "LATENT", "type": "LATENT", "links": [15], "slot_index": 0, "label": "LATENT"}], "properties": {"Node name for S&R": "EmptyLatentImage"}, "widgets_values": [512, 512, 1]}], "links": [[3, 4, 1, 6, 0, "CLIP"], [5, 4, 1, 7, 0, "CLIP"], [8, 4, 2, 8, 1, "VAE"], [9, 8, 0, 9, 0, "IMAGE"], [10, 4, 0, 10, 0, "MODEL"], [11, 6, 0, 10, 1, "CONDITIONING"], [12, 7, 0, 10, 2, "CONDITIONING"], [15, 5, 0, 10, 3, "LATENT"], [16, 10, 0, 8, 0, "LATENT"]], "groups": [], "config": {}, "extra": {"ds": {"scale": 1, "offset": [2, 0]}, "node_versions": {"comfy-core": "0.3.14"}}, "version": 0.4}

使用LoRA

首先我们下载sd_xl_base_1.0模型,LoRA需要使用与之对应的基础模型

1
2
cd ComfyUI/models/checkpoints/
wget 'https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors'

之后我们再下载LoRA模型,这里我们使用的是Expressionism Cartoons模型

1
2
3
4
5
6
7
8
9
10
(base) root@VM-0-5-ubuntu:~# cd ComfyUI/models/loras/
(base) root@VM-0-5-ubuntu:~/ComfyUI/models/loras# wget --content-disposition 'https://civitai.com/api/download/models/1608668?type=Model&format=SafeTensor&token=${API_TOKEN}'
...
HTTP request sent, awaiting response... 200 OK
Length: 228489836 (218M) [application/octet-stream]
Saving to: 'Expressionism_Cartoons.safetensors'

Expressionism_Cartoons.safetensors 100%[=====================================================================================================================================>] 217.90M 11.5MB/s in 19s

2025-06-03 09:32:01 (11.7 MB/s) - 'Expressionism_Cartoons.safetensors' saved [228489836/228489836]

之后我们根据LoRA模型在Civitai提供的推荐设置,就可以生成对应的图片了

生成的图片如下

使用ControlNet

首先需要安装comfyui_controlnet_aux节点,之后还要下载模型。ContrlNet模型是分版本的,与基础大模型要对应。如果基础模型是SD1.5,ControlNet模型也要选择SD1.5。我们从地址 https://huggingface.co/lllyasviel/ControlNet-v1-1/tree/main 可以下载模型,模型的不同类型含义如下

文件名 控制类型 输入图像类型 用途说明 推荐用途
control_v11p_sd15_canny.pth Canny Edge(边缘) 照片或图像 提取轮廓边缘 建筑、物体结构控制
control_v11p_sd15_depth.pth Depth(深度) 照片或图像 模拟三维深度结构 景深、构图调整
control_v11p_sd15_inpaint.pth Inpainting(重绘) 遮挡/蒙版图像 区域重绘 修图、局部创作
control_v11p_sd15_lineart.pth Line Art(线稿) 动漫风图像 提取黑白线稿 动漫风格建模
control_v11p_sd15_mlsd.pth MLSD(直线检测) 建筑/结构图像 提取直线 建筑、平面图等直线构图
control_v11p_sd15_normalbae.pth Normal Map(法线图) 三维物体图像 表面结构建模 高级3D拟合
control_v11p_sd15_openpose.pth OpenPose(姿态) 人物图像 骨架动作识别 指定人物动作/舞姿
control_v11p_sd15_scribble.pth Scribble(手绘草图) 简笔画、草图 草图转图像 草图创作、简笔画上色
control_v11p_sd15_seg.pth Semantic Segmentation(分割图) 分割图(每块颜色代表一种物体) 提供语义结构控制 多物体控制,如人物+背景
control_v11p_sd15_softedge.pth Soft Edge(柔边缘) 模糊边缘图 类似 Canny,但更柔和 人像、柔边轮廓控制
control_v11p_sd15s2_lineart_anime.pth Anime Line Art 动漫线稿 动漫草图 → 动漫图 高拟真二次元创作

我们这里只需要下载我们所需要的模型即可

1
2
3
4
5
cd ComfyUI/models/controlnet/
wget 'https://huggingface.co/lllyasviel/ControlNet-v1-1/resolve/main/control_v11f1p_sd15_depth.pth'
wget 'https://huggingface.co/lllyasviel/ControlNet-v1-1/resolve/main/control_v11p_sd15_canny.pth'
wget 'https://huggingface.co/lllyasviel/ControlNet-v1-1/resolve/main/control_v11p_sd15_openpose.pth'
wget 'https://huggingface.co/lllyasviel/ControlNet-v1-1/resolve/main/control_v11p_sd15_scribble.pth'

下载好了模型之后我们先新建一个默认工作流。之后创建如下节点:

加载原始输入图像,作为ControlNet的参考图像

Add Node -> image -> Load Image

使用Canny边缘检测算法提取输入图像的边缘轮廓,生成黑白边缘图

Add Node -> ControlNet Preprocessors -> Line Extractors -> Canny Edge

加载ControlNet预训练模型文件,如control_v11p_sd15_canny.pth等

Add Node -> loaders -> Load ControlNet Model

将ControlNet模型应用到条件编码上,结合边缘图控制生成过程

Add Node -> conditioning -> controlnet -> Apply ControlNet

预览最终生成的图像结果

Add Node -> image -> Preview Image

建好了工作节点之后,我们可以使用Canny Edge为图片生成线图

之后我们对工作流进行整合,从而根据原图生成类似的图片

实际生成的图片如下

使用SDXL版本的ControlNet

SDXL提供了多合一的ControlNet模型,不再需要下载多个文件,只需要使用一个模型即可,能有效节省磁盘空间。

cd ComfyUI/models/controlnet/wget 'https://huggingface.co/xinsir/controlnet-union-sdxl-1.0/resolve/main/diffusion_pytorch_model_promax.safetensors'

下载好了之后,我们还是先加载默认工作流,之后创建ControlNet的相关节点。在创建的时候可以使用鼠标左键快速双击,然后在输入框中搜索需要使用的节点。

因为这里需要使用SDXL的模型,所以我们还是使用sd_xl_base_1.0模型,并且搭配LoRA一起使用

参考

一口气学ComfyUI系列教程
ComfyUI基础教程

ComfyUI简介

2025-03-31 06:47:59

ComfyUI 是一个基于节点工作流的现代化 Stable Diffusion 图形用户界面。与传统的WebUI不同,ComfyUI采用节点连接的方式来构建图像生成工作流,让用户能够更精确地控制整个生成过程。

Stable Diffusion 是一款开源的 AI 图像生成技术,基于扩散模型构建。用户可以通过 Stable Diffusion WebUIComfyUI 等开源工具来运行它,只需下载相应的模型文件(通常为 .ckpt.safetensors 格式)即可开始使用。

核心概念

ComfyUI中的图像生成涉及三个关键组件,在CheckpointLoader中进行设置:

  • CLIP:将文本提示转化为主模型可以理解的向量形式
  • 主模型(Main MODEL):执行实际的图像生成计算
  • VAE(变分自编码器):将主模型的潜在空间格式转化为最终可视的图片

安装和配置

1. 下载和安装

GitHub 下载对应版本,解压后运行:

  • run_nvidia_gpu.bat(推荐NVIDIA GPU用户)
  • ./python_embeded/python -s ComfyUI/main.py --windows-standalone-build

2. 安装管理器和插件

下载 ComfyUI-Manager 放到 ComfyUI/custom_nodes 文件夹,然后在Manager的Custom Node Manager中安装所需插件(需要科学上网)。

3. 下载模型

将模型文件放置到 ComfyUI/models 文件夹中:

  • Checkpoint模型checkpoints 文件夹
  • LoRA模型loras 文件夹
  • VAE模型vae 文件夹

推荐入门模型:SD 1.5

模型资源网站:

4. 网络配置

如果遇到网络连接问题,可以使用SwitchHosts添加以下配置:

1
2
3
4
185.199.108.133 raw.githubusercontent.com
185.199.108.133 user-images.githubusercontent.com
185.199.108.133 avatars2.githubusercontent.com
185.199.108.133 avatars1.githubusercontent.com

扩展功能

ComfyUI_StoryDiffusion

通过Custom Node Manager安装,然后执行以下命令安装依赖:

1
2
./python_embeded/python.exe -m pip install -r ../ComfyUI_windows_portable/ComfyUI/custom_nodes/ComfyUI_StoryDiffusion/requirements.txt
./python_embeded/python.exe -m pip install opencv-python

API操作

HTTP API方式

  1. 启用开发者选项
  2. 将设置好的Workflow导出为API格式
  3. 创建任务:
    1
    2
    3
    curl -X POST 'http://127.0.0.1:8188/prompt' \
    -H 'Content-Type: application/json' \
    -d '{"prompt": API文件的内容}'
  4. 查询结果:curl -X GET 'http://127.0.0.1:8188/history/{prompt_id}'
  5. 获取图片:http://127.0.0.1:8188/view?filename=ComfyUI_00003_.png&subfolder&type=output

WebSocket方式

更简单的实时通信方式:

1
2
3
4
5
// 建立连接
ws://127.0.0.1:8188/ws?clientId=23333

// 提交任务
{"client_id": "23333", "prompt": "API文件的内容"}

提示:如果不想折腾本地环境,可以考虑使用腾讯云等平台提供的按时计费ComfyUI服务。

参考资料

官方资源

学习教程

利用whisper为视频自动生成字幕

2025-01-02 18:37:24

whisper是一个由openai开发的通用语言识别模型,我们可以使用它来为视频自动创建字幕。

环境安装

为了加速,我们需要使用GPU来进行计算,因此需要安装基于CUDA的pytorch。首先我们需要安装Miniconda,这里安装的时候直接点击下一步即可。

安装完毕之后,我们需要创建一个新的环境,这里我们创建一个名为whisper的环境:

conda create -n whisper python=3.8conda activate whisper

1. 安装CUDA

安装好了Miniconda之后,我们需要安装CUDA,执行nvidia-smi

$ nvidia-smiThu Jan  2 11:49:53 2025+-----------------------------------------------------------------------------------------+| NVIDIA-SMI 560.94                 Driver Version: 560.94         CUDA Version: 12.6     ||-----------------------------------------+------------------------+----------------------+| GPU  Name                  Driver-Model | Bus-Id          Disp.A | Volatile Uncorr. ECC || Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. ||                                         |                        |               MIG M. ||=========================================+========================+======================||   0  NVIDIA GeForce GTX 1060 6GB  WDDM  |   00000000:01:00.0  On |                  N/A ||  0%   39C    P8             10W /  120W |     505MiB /   6144MiB |      0%      Default ||                                         |                        |                  N/A |+-----------------------------------------+------------------------+----------------------+

通过这个命令可以看到Driver Version: 560.94CUDA Version: 12.6,因此我们需要安装12.6版本的CUDA,更加详细的版本对照表在这里。在安装的时候可以选择自定义安装选项,一般来说只要勾选CUDA下的 Development和Runtime即可。

安装完毕之后执行命令nvcc -V查看CUDA版本:

$ nvcc -Vnvcc: NVIDIA (R) Cuda compiler driverCopyright (c) 2005-2024 NVIDIA CorporationBuilt on Thu_Sep_12_02:55:00_Pacific_Daylight_Time_2024Cuda compilation tools, release 12.6, V12.6.77Build cuda_12.6.r12.6/compiler.34841621_0

2. 安装cuDNN

根据自己下载的CUDA来选择对应版本的cuDNN,下载地址在这里。下载完毕之后解压到CUDA的安装目录下,一般来说是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA{版本号},如果有重名的文件直接替换即可。

之后进入extras\demo_suite目录,执行如下命令:

bandwidthTest.exedeviceQuery.exe

如果出现了PASS的字样,说明安装成功。

3. 安装pytorch

切换到我们之前创建的whisper环境,使用如下命令安装CUDA版本的pytorch:

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

安装之后执行python命令进入python环境,执行如下代码:

1
2
import torch
torch.cuda.is_available()

如果显示True则说明CUDA版本的pytorch安装成功。

4. 安装whisper

切换到我们之前创建的whisper环境,执行如下命令安装whisper:

pip install -U openai-whisperpip install setuptools-rust

安装完毕之后执行如下命令就可以使用whisper了:

whisper 'C:/Users/raymond/Desktop/voice.aac' --language zh --model turbo

如上命令表示对C:/Users/raymond/Desktop/voice.aac文件进行中文语言的识别,使用turbo模型。第一次执行该命令会下载模型文件,模型文件较大,下载时请确保网络通畅。执行结果如下

[00:00.000 --> 00:03.060] 提到肉毒毒素[00:03.060 --> 00:04.540] 你会想到什么[00:04.540 --> 00:10.820] 你真的了解它吗[00:10.820 --> 00:12.540] 2017年[00:12.540 --> 00:14.180] 肉毒毒素以万能药标签[00:14.180 --> 00:15.500] 登上时代周刊方面[00:15.500 --> 00:17.280] 目前它在全球[00:17.280 --> 00:18.960] 已被应用于几十种适应症[00:18.960 --> 00:20.560] 仅在2019年[00:20.560 --> 00:23.000] 接受注射的就已超过620万例[00:23.000 --> 00:24.880] 但不要忘了[00:24.880 --> 00:26.780] 肉毒毒素更是一种神经毒素[00:26.780 --> 00:29.000] 还曾被当作生化武器使用... 省略 ...

生成字幕

我们可以使用ffmpeg将音频从视频中提取出来,然后使用whisper生成字幕,最后使用ffmpeg将字幕添加到视频中。

使用如下命令提取音频:

ffmpeg -i input.mp4 -vn -acodec copy output.aac

然后使用whisper生成字幕,我们先在pycharm中创建一个test-whisper项目,并且把python解释器设置为Miniconda创建的whisper环境。创建一个main.py文件,写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import whisper
from whisper.utils import get_writer

root = 'E:/'

# 使用turbo模型
model = whisper.load_model('turbo')
prompt = '如果使用了中文,请使用简体中文来表示文本内容'

# 选择声音文件,识别中文,并且打印详细信息
result = model.transcribe(root + 'output.aac', language='zh', initial_prompt=prompt, verbose=True)
print(result['text'])

# 保存字幕文件
writer = get_writer('srt', root)
writer(result, 'output.srt')

如上代码表示使用turbo模型,识别中文,打印详细信息,并且保存字幕文件。执行完毕之后我们可以在E:/目录下看到生成的字幕文件。

最后我们使用ffmpeg将字幕添加到视频中:

ffmpeg -i input.mp4 -i output.srt -c:s mov_text -c:v copy -c:a copy output.mp4

之后我们在播放这个视频的时候就会有字幕了。

参考

video-subtitle-generator
基于Anaconda的pytorch-cuda
CUDA与cuDNN的安装与配置
ffmpeg视频合并、格式转换、截图