MoreRSS

site iconShadow Walker | 松烟阁修改

Where other men are limited by morality or law, remember, everything is permitted. I walk in the darkness to serve the light.
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Shadow Walker | 松烟阁的 RSS 预览

Obsidian基于AI自动为文章配图

2024-12-16 22:30:42

我在 Obsidian 中管理 Newsletter 特别是文章发布之前经常需要花很长的时间在网络上搜索与文章主题匹配的图片作为文章的 featured image,同时还需要考虑版权、匹配度等等问题,如果 AI 能自动帮我完成这件事情那就太好了,顺着这个思路我发现 wordpress 已经支持类似的功能了,唯一的缺点就是得花钱🤪。于是我就想着在 personal assistant 插件中支持这个功能,这篇文章就介绍一下我是怎么在 Obsidian 插件中设计和构建这个能力的。

方案设计

基于众所周知的原因,ChatGPT、Claude、Stable Diffusion、DALL-E 这些最优秀的 AI 服务,在国内基本上是没发正常访问的,所以目标对象肯定是国内可用的大模型服务,同一个大模型服务既要支持文本生成,又要支持图片生成,通义千问服务可能唯一能满足我要求的了。我想这在公有云领域,这就是所谓的「Vendor Lock-in」。

personal assistant 基于通义千问大模型的 featured image 生成的工作流程如下:

  1. 完成文档的编辑
  2. 根据文档内容生成能够匹配文档内容的图片描述
  3. 根据图片描述生成图片艺术风格、技术参数、图片描述等内容
  4. 根据图片提示词生成图片
  5. 下载图片到本地并在 Obsidian 展示

效果演示

使用 AI 生成 featured image 的步骤:

  1. 需要在 Obsidian 中安装 personal assistant 插件;
  2. personal assistant 插件配置 Qwen API token(参考 README);
  3. Obsidian 文档处于 Edit View;

效果如下所示:

0:00
/1:47

后续计划

  • 类似微信公众号目前支持的听书功能,打算在 Obsidian 和 Ghost 也支持上
  • AI 自动为 vault 的笔记文档建立关联关系

小本本系列:大模型中的文本向量text embeddings

2024-12-08 19:39:43

作为人类,我们可以阅读并理解文本(至少可以理解一部分),然而计算机“用数字思考”,因此它们无法自动理解单词和句子的含义。如果我们要让计算机理解自然语言,就需要将这些信息转换成计算机可以处理的格式——数字向量。这篇文章就研究一下科学家是如何一步一步让计算机理解和认识人类的语言的。

人们早在多年前就学会了如何将文本转换为机器可理解的格式(最早的版本之一是ASCII)。这种方法有助于渲染和传输文本,但并不编码单词的意义。当时,标准的搜索技术是关键词搜索,即仅查找包含特定单词或N元语法的所有文档。

然后,在几十年后,嵌入式表示(embeddings)出现了。我们可以计算单词、句子甚至图像的嵌入式表示。嵌入式表示也是数字向量,但它们能够捕捉意义。因此,你可以使用它们进行语义搜索,甚至处理不同语言的文档。

词袋模型 bag of words

将文本转换为向量的最基本方法是词袋模型。让我们以理查德·P·费曼的一句名言为例:“我们很幸运生活在一个我们仍在不断发现的时代”。我们将使用这句话来说明词袋模型的方法。

获得词袋向量的第一步是将文本拆分为单词(标记),然后将单词还原为其基本形式。例如,“running”将转换为“run”。这个过程称为词干提取(stemming,即将文本拆分为单词并进行词干提取)。

如下代码所示,用 NLTK 实现词袋模型:

from nltk.stem import SnowballStemmer
from nltk.tokenize import word_tokenize
import nltk
import os

# Resource punkt_tab not found. Try using the NLTK Downloader
if not os.path.exists("./tokenizers/punkt_tab.zip"):
    nltk.download("punkt_tab", download_dir="./")

text = "We are lucky to live in an age in which we are still making discoveries"

# tokenization - splitting text into words
words = word_tokenize(text)
print(words)
# ['We', 'are', 'lucky', 'to', 'live', 'in', 'an', 'age', 'in', 'which',
#  'we', 'are', 'still', 'making', 'discoveries']

stemmer = SnowballStemmer(language="english")
stemmed_words = list(map(lambda x: stemmer.stem(x), words))
print(stemmed_words)
# ['we', 'are', 'lucki', 'to', 'live', 'in', 'an', 'age', 'in', 'which',
#  'we', 'are', 'still', 'make', 'discoveri']

import collections
bag_of_words = collections.Counter(stemmed_words)
print(bag_of_words)
# {'we': 2, 'are': 2, 'in': 2, 'lucki': 1, 'to': 1, 'live': 1,
# 'an': 1, 'age': 1, 'which': 1, 'still': 1, 'make': 1, 'discoveri': 1}

实际上,如果我们想将文本转换为向量,不仅要考虑文本中的单词,还要考虑整个词汇表。假设我们的词汇表中还包括“i”、“you”和“study”,我们就可以从费曼的这句名言创建一个向量。

# bag of words vector
{'we': 2, 'are': 2, 'in': 2, 'lucki': 1, 'to': 1, 'live': 1,'an': 1, 'age': 1, 'which': 1, 'still': 1, 'make': 1, 'discoveri': 1, 'i': 0, 'you': 0, 'study': 0}

这种方法相当基础,它不考虑单词的语义意义,因此句子“the girl is studying data science”和“the young woman is learning AI and ML”在向量空间中不会接近彼此。

TF-IDF 模型

词袋模型的一个稍作改进的版本是TF-IDF(Term Frequency — Inverse Document Frequency,词频-逆文档频率)。它是两个度量值的乘积。
$$TF-IDF(t,d,D) = TF(t,d) \times IDF(t, D) $$

  • 词频 (Term Frequency, TF) 表示单词在文档中出现的频率。最常见的计算方法是将该词在文档中的原始计数(类似于词袋模型)除以文档中的总词数。
    $$TF(t,d)=\frac{number\ of\ times\ term\ t\ appears\ in\ the\ document\ d}{number\ of\ terms\ in\ document\ d}$$
  • 逆文档频率 (Inverse Document Frequency, IDF) 表示单词提供的信息量。例如,像“a”或“that”这样的词不会给你关于文档主题的任何额外信息。相反,像“ChatGPT”或“生物信息学”这样的词可以帮助你确定领域(但不适用于这个句子)。它通过计算包含该词的文档数与总文档数的比例的对数来得到。IDF值越接近0,表示这个词越常见,提供的信息越少。
    $$IDF(t,D)=log(\frac{total\ number\ of\ document\ in\ corpus\ document\ D}{number\ of\ documents\ containing\ term\ t})$$

最终,我们会得到一些向量,其中常见的词(如“I”或“you”)具有较低的权重,而多次出现在文档中的罕见词则具有较高的权重。这种策略会带来稍微更好的结果,但它仍然无法捕捉语义意义。

这种方法的另一个挑战是它会产生非常稀疏的向量。向量的长度等于语料库的大小。英语中大约有47万独特的单词(来源),因此我们将得到巨大的向量。由于一个句子通常不会超过50个不同的词,因此向量中99.99%的值将是0,不携带任何信息。鉴于这一点,科学家们开始思考密集向量表示。

词语潜在信息(Latent Information)与神经网络方法(Neural Network Method)
英文语境下,当我们谈论狗(dog)时,可能会使用除了“dog”之外的其他词语。我们是否应该在我们的分类方案中考虑像“canine”(犬类)或“feline”(猫科)这样的术语?我们需要为这些词添加一个新的项,例如 (dogx, caty, caninez, felinei)。

在英语中,一个大约30,000个词的词汇表对于这种词袋模型来说效果很好。在计算机世界中,我们可以比实体图书馆更平滑地扩展这些维度,但原则上问题类似。在高维度下,事情变得难以管理。随着组合爆炸,算法运行缓慢,稀疏性(大多数文档对大多数术语的计数为0)对统计和机器学习构成了问题。

因此,要将一本书投影到潜在空间(latent space)中,我们需要一个大矩阵,定义词汇表中每个观察到的术语对每个潜在术语的贡献程度。

有几个不同的算法可以从足够大的文档集合中推断出这一点:潜在语义分析(Latent Semantic Analysis, LSA),它使用术语-文档矩阵的奇异值分解(基本上是高级线性代数),以及潜在狄利克雷分配(Latent Dirichlet Allocation, LDA),它使用一种称为狄利克雷过程的统计方法。

我们使用词频作为某种更为模糊的主题性的代理。通过将这些词频投影到嵌入空间中,我们不仅可以降低维度,还可以推断出比原始词频更好地指示主题性的潜在变量。为此,我们需要一个定义良好的算法,如LSA,它可以处理文档语料库,找到从词袋输入到嵌入空间向量的良好映射。基于神经网络的方法使我们能够推广这一过程,并突破LSA的限制。

word2vec 模型

最著名的密集表示方法之一是word2vec,这是谷歌在2013年提出的,论文标题为“Efficient Estimation of Word Representations in Vector Space”,作者是Mikolov等人。

在这篇论文中提到了两种不同的word2vec方法:

  1. Continuous Bag of Words (CBOW):在这种方法中,目标是根据上下文中的单词预测中间的单词。具体来说,给定一个单词序列,CBOW模型试图根据周围的单词预测中心单词。这种方法的优点是训练速度较快,因为每次更新都利用了更多的信息。
  2. Skip-gram:与CBOW相反,Skip-gram的目标是根据一个中心单词预测其周围的单词。也就是说,给定一个单词,模型试图预测这个单词周围的上下文单词。Skip-gram模型在捕捉罕见词的语义方面表现更好,但训练速度较慢,因为它每次更新只利用了一个单词的信息。

这两种方法都能生成词的密集向量表示,这些向量不仅包含了词的频率信息,还捕捉了词之间的语义关系。例如,使用word2vec生成的向量可以反映出“king”和“queen”之间的关系类似于“man”和“woman”之间的关系。

word2vec的成功在于它能够有效地将高维的词袋模型转化为低维的密集向量,同时保留了词的语义信息。这使得在自然语言处理任务中,如语义搜索、情感分析和机器翻译等,可以更高效地使用这些向量。

高维词向量的密集表示的核心思想是训练两个模型:编码器(encoder)和解码器(decoder)。以skip-gram模型为例,我们可以将单词“christmas”传递给编码器。编码器会生成一个向量,然后将这个向量传递给解码器,期望解码器能够输出“merry”、“to”和“you”等上下文单词。

具体步骤如下:

  1. 输入层
    • 在skip-gram模型中,输入是一个单词(例如“christmas”)。
  2. 编码器
    • 编码器将输入单词映射到一个固定长度的密集向量。这个向量通常被称为词嵌入(word embedding)。例如,如果词嵌入的维度是100,那么“christmas”会被映射到一个100维的向量。
  3. 解码器
    • 解码器接收这个100维的向量,并尝试预测该单词的上下文单词(例如“merry”、“to”、“you”)。解码器通常是一个多层神经网络,最后一层是一个softmax层,用于输出每个可能的上下文单词的概率分布。
  4. 损失函数
    • 模型的训练目标是最小化预测的上下文单词与实际上下文单词之间的差异。常用的损失函数是交叉熵损失(cross-entropy loss)。

训练过程:

  • 前向传播
    • 输入单词“christmas”通过编码器生成一个100维的向量。
    • 这个向量通过解码器,解码器输出一个概率分布,表示每个可能的上下文单词出现的概率。
  • 反向传播
    • 计算预测的上下文单词与实际上下文单词之间的损失。
    • 使用反向传播算法调整编码器和解码器的参数,以最小化损失。

通过这种方式,skip-gram模型能够生成高质量的词嵌入,这些嵌入不仅保留了词的频率信息,还捕捉了词之间的语义关系,为各种自然语言处理任务提供了强大的支持。这个模型开始考虑单词的意义,因为它是在单词的上下文中进行训练的。然而,它忽略了形态学(我们从单词部分可以获得的信息,例如,“-less”意味着缺乏某物)。这一缺点后来通过查看子词skip-grams在GloVe中得到了解决。

此外,word2vec只能处理单词,但我们希望对整个句子进行编码。因此,让我们进入下一个演进步骤,即transformers。

transformer 模型

下一次演变与 Vaswani 等人在论文“Attention is All You Need”中引入的变压器方法有关。变压器能够生成信息丰富的密集向量,并成为现代语言模型的主要技术。

变压器允许使用相同的“核心”模型,并针对不同的用例进行微调,而无需重新训练核心模型(这需要大量时间和成本)。这导致了预训练模型的兴起。其中一个早期流行的模型是Google AI的BERT(基于变压器的双向编码器表示)。

内部而言,BERT仍然像word2vec一样在词元级别上操作,但我们仍然希望获得句子嵌入。因此,一个简单的方法可能是取所有词元向量的平均值。不幸的是,这种方法的表现不佳。

这个问题在2019年随着Sentence-BERT的发布得到了解决。Sentence-BERT在语义文本相似性任务上超越了所有先前的方法,并允许计算句子嵌入。

text embeddings 计算验证

我使用阿里云大模型服务的text-embedding-v2来生成文本嵌入向量。结果,我们得到了一个1536维的浮点数向量。现在我们可以对所有数据重复这一过程,并开始分析这些值。

from dotenv import load_dotenv, find_dotenv

_ = load_dotenv(find_dotenv("./env/.env"))

import dashscope
from http import HTTPStatus
from pprint import pprint

resp = dashscope.TextEmbedding.call(
    model=dashscope.TextEmbedding.Models.text_embedding_v2,
    input="We are lucky to live in an age in which we are still making discoveries.",
    dimension=1536,
)
pprint(resp['output']) if resp.status_code == HTTPStatus.OK else print(resp)

# output
# {'embeddings': [{'embedding': [0.022378576171554372,
#                               -0.027432455162420308,
#                               -0.00355793080956962,
#                               -0.030121118785560987,
#                               ...
#                               ],
#                'text_index': 0}]}

向量距离

嵌入实际上是向量。因此,如果我们想了解两个句子之间的相似程度,可以计算它们之间向量的距离。距离越小,表示它们的语义意义越接近。

可以使用不同的度量来测量两个向量之间的距离:

  • 欧几里得距离(L2)
  • 曼哈顿距离(L1)
  • 点积
  • 余弦距离

欧几里得距离(L2)

定义两点(或向量)之间距离的最标准方法是欧几里得距离或L2范数。这种度量在日常生活中最常用,例如,当我们谈论两个城镇之间的距离时。

以下是L2距离的视觉表示和公式:

$$\text{L2距离} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2 + \cdots + (z_2 - z_1)^2}$$

曼哈顿距离(L1)

另一种常用的距离是L1范数或曼哈顿距离。这种距离是以纽约的曼哈顿岛命名的。该岛的街道布局呈网格状,两个点之间的最短路径将是L1距离,因为需要沿着网格行走。

$$\text{L1距离} = \sum_{i=1}^{n} |x_i - y_i|$$

点积

另一种查看向量之间距离的方法是计算点积或标量积。以下是公式,我们可以轻松实现它:

$$\text{点积} = \vec{a} \cdot \vec{b} = |\vec{a}||\vec{b}|cos\theta= \sum_{i=1}^{n} a_i b_i$$

这种度量的解释有点棘手。一方面,它显示了向量是否指向同一方向。另一方面,结果高度依赖于向量的大小。例如,让我们计算两对向量之间的点积:

  • (1, 1) 和 (1, 1)
  • (1, 1) 和 (10, 10)

在这两种情况下,向量都是共线的,但在第二种情况下,点积大十倍:2 对 20。

余弦相似度

余弦相似度经常被使用。余弦相似度是点积除以向量的模长(或范数)的归一化结果。

让我们谈谈这种度量的物理意义。余弦相似度等于两个向量之间的夹角的余弦值。向量越接近,度量值越高。

$$\text{余弦相似度} = \frac{\vec{a} \cdot \vec{b}}{|\vec{a}| |\vec{b}|}$$

距离计算方法选择

你可以使用任何距离来比较你得到的嵌入。例如,我计算了不同聚类之间的平均距离。无论是L2距离还是余弦相似度,都展示了类似的结果:

  • 同一聚类内的对象比与其他聚类的对象更接近。解释我们的结果时需要注意,对于L2距离,更接近意味着距离更低;而对于余弦相似度,更接近的对象的度量值更高。不要混淆。
  • 我们可以发现某些主题彼此非常接近,例如,“政治”和“经济”或“人工智能”和“数据科学”。

然而,对于NLP任务,最佳实践通常是使用余弦相似度。背后的原因包括:

  • 余弦相似度的范围在-1到1之间,而L1和L2距离是没有界的,因此更容易解释。
  • 从实际角度来看,计算点积比计算欧几里得距离的平方根更有效。
  • 余弦相似度受维度诅咒的影响较小(维度越高,向量之间的距离分布越窄)。

向量可视化

理解数据的最佳方式是将其可视化。不幸的是,嵌入有1536个维度,因此直接查看数据非常困难。然而,有一种方法:我们可以使用降维技术将向量投影到二维空间中。

注意: 以下可视化数据来自 Stack Exchange Data Dump,由于数据量是92.3G,我就没有自己本地跑可视化结果,本文可视化视图来自 Mariya Mansurova 的Text Embeddings: Comprehensive Guide

PCA

最基本的降维技术是主成分分析(PCA)

t-SNE

PCA是一种线性算法,而在现实生活中大多数关系是非线性的。因此,由于非线性问题,我们可能无法很好地分离聚类。让我们尝试使用一种非线性算法——t-SNE,看看它是否能显示出更好的结果。

根据t-SNE可视化的推理

来源内容:

Is it safe to drink the water from the fountains found all over 
the older parts of Rome?

When I visited Rome and walked around the older sections, I saw many 
different types of fountains that were constantly running with water. 
Some went into the ground, some collected in basins, etc.

Is the water coming out of these fountains potable? Safe for visitors 
to drink from? Any etiquette regarding their use that a visitor 
should know about?

t-SNE嵌入:

我们可以在t-SNE可视化中找到这条内容,并发现它实际上靠近咖啡聚类。

这句话的意思是,在t-SNE可视化中,这条关于罗马喷泉水的问题被映射到了一个与咖啡相关的话题附近。这可能表明,尽管这两者看似不相关,但它们在某些方面存在一定的语义相似性,或者在数据集中它们经常一起出现。

References

  1. Text Embeddings: Comprehensive Guide | by Mariya Mansurova | Towards Data Science
  2. Transformers, Explained
  3. The illustrated transformer
  4. Sentence embeddings
  5. why should euclidean distance not be the default distance measure
  6. An intuitive introduction to text embeddings

小本本系列:大模型中的向量嵌入Vector embeddings

2024-11-30 23:16:35

Vector embeddings are a method to convert non-structured data, such as text, images, and videos, into numerical representations that capture their meanings and relationships. This allows computers, which only understand numbers, to process and interpret these data more effectively. Embeddings are crucial for tasks like semantic similarity and are used in various AI models, including LLMs, RAG, and multimodal processing.

vector embeddings 的感性理解

vector embeddings 的出现最本质的原因是科学家为了解决一个问题 —— 让只认识0、1数字,只会做逻辑运算和浮点运算的计算机,能够理解人类语言、文字、图像、视频等数据包含的语义以及它们之间的关系。例如,「泰迪」、「狗」、「犬」、「哺乳动物」这几个单词在不同的上下文中其实是同一个意思,即一种四足、有尾巴、有尖牙的哺乳动物,这就是文字、图像、视频中包括的语义信息,科学家希望计算机可以理解这些。

既然计算机只认识数字,只会做运算,科学家的想法很直接,那就我们就将人类使用的文字、句子、文章、书本、图像、视频等等非结构化的数据转换成数字来描述,帮助计算机高效的理解和处理它们,于是就有了 vector embeddings。

vector embeddings 是什么

vector embeddings(向量嵌入)是一种将单词、句子和其他数据转换为数字的方法,这些数字捕获了它们的含义和关系。它们将不同类型的数据表示为高维空间(多维空间)中的点,其中相似的数据点聚集在一起。这些数值表示有助于机器更有效地理解和处理这些数据。

向量嵌入在处理语义相似度时至关重要。vector(向量)仅仅是一系列数字;vector embedding(向量嵌入)是一系列代表输入数据的数字。通过使用 vector embeddings,我们可以结构化非结构化数据或通过将其转换为一系列数字来处理任何类型的数据。这种方法使我们能够对输入数据执行数学运算,而不是依赖定性比较。

vector V.S. embedding

在 vector embeddings (向量嵌入)的上下文中,embedding(嵌入)和 vector(向量)是同一回事。两者都指数据的数值表示,其中每个数据点都由高维空间中的向量表示。

“vector 向量”仅指具有特定维数的数字数组。在 vector embedding(向量嵌入)的情况下,这些向量表示上述任何数据点在一个连续的空间中。

“embedding 嵌入”专门指将数据表示为 vector(向量)的技术,以捕获有意义的信息、语义关系或上下文特征。embedding 嵌入旨在捕获数据的底层结构或属性,通常通过训练算法或模型来学习。

虽然嵌入和向量可以在向量嵌入的上下文中互换使用,“嵌入”强调以有意义和结构化的方式表示数据的概念,而“向量”则指数值表示本身。

如何构建 vector embeddings

vector embeddings 是深度学习模型中输入数据的内部表示,也称为嵌入模型或深度神经网络。那么,我们如何提取这些信息呢?

我们通过移除最后一层并从倒数第二层获取输出,来获得 vector 向量。神经网络的最后一层通常输出模型的预测,因此我们取倒数第二层的输出。vector embedding(向量嵌入)是馈送到神经网络预测层的数据。

vector embedding(向量嵌入)的维数等于模型中倒数第二层的尺寸,因此与向量的尺寸或长度可互换。常见的向量维数包括384(例如 Sentence Transformers Mini-LM 生成的 vector)、768(例如 Sentence Transformers MPNet 生成的 vector)、1,536(例如 OpenAI 生成的 vector)和2,048(例如 ResNet-50 生成的 vector)。

奇怪的 vector embeddings 维数 768、1024、1536、2048

最早的 GPT-2 设置中,头的数量是 12(dimension heads),它可以整除 768。

这个数字来自于超参数优化(hyperparameter optimization)。使用 4096 大小的嵌入和 1 层的神经网络,或者使用 16 大小的嵌入和 2B 参数的神经网络是没有意义的,这些值之间需要一个良好的平衡。

那么为什么是 768 而不是 769 呢?我们通常使用 2 的幂(或接近的值)来尝试超参数,因为它们在计算上更快,并且更适合 GPU 内存分配(就像你的屏幕分辨率一样,GPU 只是一个计算矩阵的大型机器)。768 = 512 + 256 = 2**9 + 2**8

vector embeddings 维度的含义

我曾经查找资料并试图弄懂 vector embeddings(向量嵌入)中每个维度的含义。最终的答案是,单个维度没有任何意义。vector embeddings 中的单个维度过于抽象,无法确定其含义。然而,当我们将所有维度放在一起时,它们提供了输入数据的语义含义。

向量的维度是不同属性的高级抽象表示。表示的属性取决于训练数据和模型本身。文本和图像模型生成不同的嵌入,因为它们针对根本不同的数据类型进行训练。即使是不同的文本模型也会生成不同的嵌入。有时它们在大小上不同;其他时候,它们在表示的属性上不同。例如,在法律数据上训练的模型将学习与在医疗保健数据上训练的模型不同的事物。

图像 vector embeddings

2012 年 AlexNet 的出现标志着图像识别技术的飞跃。自那时以来,计算机视觉领域取得了无数进展。最新的知名图像识别模型是 ResNet-50,它是一个基于前代 ResNet-34 架构的 50 层深度残差网络。

尝试使用 microsoft/resnet-50 · Hugging Face生成图像的 vector embeddings:

# Load model directly
from transformers import AutoFeatureExtractor, AutoModelForImageClassification
from PIL import Image


extractor = AutoFeatureExtractor.from_pretrained("microsoft/resnet-50")
model = AutoModelForImageClassification.from_pretrained("microsoft/resnet-50")

image = Image.open("<image path>")
# image = Resize(size=(256, 256))(image)

inputs = extractor(images=image, return_tensors="pt")
# print(inputs)

outputs = model(**inputs)
vector_embeddings = outputs[1][-1].squeeze()

文本 vector embeddings

人工智能对自然语言的处理已经从基于规则的嵌入发展到了一个新的高度。从最初的神经网络开始,我们通过 RNN 添加了递归关系来跟踪时间步长。从那时起,我们使用 Transformer 来解决序列转导问题。

Transformer 由编码器、注意力矩阵和解码器组成。编码器将输入编码为表示状态的矩阵,注意力矩阵和解码器对状态和注意力矩阵进行解码,以预测正确的下一个标记来完成输出序列。GPT 是迄今为止最流行的语言模型,它由严格的解码器组成。它们对输入进行编码并预测正确的下一个标记。

尝试使用 sentence-transformers生成文本的 vector embeddings:

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("<model-name>")
vector_embeddings = model.encode(“<input>”)

多模态 vector embeddings

以图生文为例,使用开源模型 CLIP VIT 来生成 embeddings:

# Load model directly
from transformers import AutoProcessor, AutoModelForZeroShotImageClassification


processor = AutoProcessor.from_pretrained("openai/clip-vit-large-patch14")
model = AutoModelForZeroShotImageClassification.from_pretrained("openai/clip-vit-large-patch14")
from PIL import Image


image = Image.open("<image path>")
# image = Resize(size=(256, 256))(image)


inputs = extractor(images=image, return_tensors="pt")
# print(inputs)


outputs = model(**inputs)
vector_embeddings = outputs[1][-1].squeeze()

语音 vector embeddings

以语音生成文字为例,使用开源模型 Whisper 模型来获取 embedding:

import torch
from transformers import AutoFeatureExtractor, WhisperModel
from datasets import load_dataset


model = WhisperModel.from_pretrained("openai/whisper-base")
feature_extractor = AutoFeatureExtractor.from_pretrained("openai/whisper-base")
ds = load_dataset("hf-internal-testing/librispeech_asr_dummy", "clean", split="validation")
inputs = feature_extractor(ds[0]["audio"]["array"], return_tensors="pt")
input_features = inputs.input_features
decoder_input_ids = torch.tensor([[1, 1]]) * model.config.decoder_start_token_id
vector_embedding = model(input_features, decoder_input_ids=decoder_input_ids).last_hidden_state

视频 vector embeddings

视频的 embeddings 比语音、图片的 embeddings 更加复杂,它需要多模态的处理来保证语音与图片的同步,以 DeepMind 开源模型 multimodal perceiver 为例生成视频的 vector embeddings(注意代码 outputs[1][-1].squeeze()):

def autoencode_video(images, audio):
     # only create entire video once as inputs
     inputs = {'image': torch.from_numpy(np.moveaxis(images, -1, 2)).float().to(device),
               'audio': torch.from_numpy(audio).to(device),
               'label': torch.zeros((images.shape[0], 700)).to(device)}
     nchunks = 128
     reconstruction = {}
     for chunk_idx in tqdm(range(nchunks)):
          image_chunk_size = np.prod(images.shape[1:-1]) // nchunks
          audio_chunk_size = audio.shape[1] // SAMPLES_PER_PATCH // nchunks
          subsampling = {
               'image': torch.arange(
                    image_chunk_size * chunk_idx, image_chunk_size * (chunk_idx + 1)),
               'audio': torch.arange(
                    audio_chunk_size * chunk_idx, audio_chunk_size * (chunk_idx + 1)),
               'label': None,
          }
     # forward pass
          with torch.no_grad():
               outputs = model(inputs=inputs, subsampled_output_points=subsampling)


          output = {k:v.cpu() for k,v in outputs.logits.items()}
          reconstruction['label'] = output['label']
          if 'image' not in reconstruction:
               reconstruction['image'] = output['image']
               reconstruction['audio'] = output['audio']
          else:
               reconstruction['image'] = torch.cat(
                    [reconstruction['image'], output['image']], dim=1)
               reconstruction['audio'] = torch.cat(
                    [reconstruction['audio'], output['audio']], dim=1)
          vector_embeddings = outputs[1][-1].squeeze()
	 # finally, reshape image and audio modalities back to original shape
     reconstruction['image'] = torch.reshape(reconstruction['image'], images.shape)
     reconstruction['audio'] = torch.reshape(reconstruction['audio'], audio.shape)
     return reconstruction


     return None

References

  1. Motivation to use 768 dimensional embeddings from Transformers?
  2. How to Get the Right Vector Embeddings
  3. What are Vector Embeddings?

如何在人工智能领域建立自己的职业生涯

2024-11-23 22:12:01

随着 LLM 技术的兴起,日常工作会频繁的碰到 LLM/AI 相关的技术和项目,我所在的 OS Security Team 会研究 AI for System & System for AI 这两个领域,这就被迫需要补充学习 LLM/AI 相关的知识。吴恩达的书《How to Build Your Career in AI》,解决了很多学习 LLM/AI 的疑惑,例如:

  • LLM/AI 的学习路线的规划是什么样的
  • 成为 LLM/AI 工程师我需要学那些东西
  • 我要学多少数学才能成为一名 LLM/AI 工程师
  • ……

这篇文章想分享了吴恩达给予我在学习 AI 这条路上的一些启发和思考。

AI 学习的关键领域

现如今已经发表的关于 AI 研究的论文,任何人一生能不可能读完。因此在学习时,选择主题至关重要。

机器学习基础技能

理解线性回归、逻辑回归、神经网络、决策树、聚类和异常检测等模型很重要。除了特定的模型之外,理解机器学习工作原理的核心概念更为重要,如偏差/方差、成本函数、正则化、优化算法和误差分析。

深度学习

深度学习已经是机器学习中非常重要的一部分,以至于没有一定的理解就很难在这个领域取得优异成绩!了解神经网络的基础知识、使它们发挥作用的实际技能(如超参数调整)、卷积网络、序列模型和 transformers 是非常有价值的。

与机器学习相关的数学

线性代数(向量、矩阵及其各种操作)以及概率和统计(包括离散和连续概率、标准概率分布、基本规则如独立性和贝叶斯法则,以及假设检验)。此外,探索性数据分析(EDA)——使用可视化和其他方法系统地探索数据集——是一项被低估的技能,它在以数据为中心的 AI 开发中特别有用,其中分析错误和获得见解可以真正帮助推动进展!最后,对微积分的基本直观理解也会有所帮助。

软件开发

虽然仅凭机器学习建模技能就能找到工作并做出巨大贡献,但如果你还能编写良好的软件来实现复杂的 AI 系统,你的就业机会将会增加。这些技能包括编程基础、数据结构(尤其是与机器学习相关的,如数据框)、算法(包括与数据库和数据操作相关的算法)、软件设计、熟悉 Python 以及熟悉关键库如 TensorFlow 或 PyTorch 和 scikit-learn。

建立学习习惯的方法

BJ Fogg 在他的《Tiny Habits: The Small Changes That Change Everything》中解释说,建立新习惯的最佳方式是从小事做起并取得成功,而不是一开始就定下过高的目标导致失败。例如,与其每天试图锻炼 30 分钟,他建议先设定一个做一次俯卧撑的目标,并坚持去做。

这种方法可能对那些想要花更多时间学习的你们有所帮助。如果你从每天坚持观看,比如说,10 秒的教育视频开始——并且你能够持续这样做——每天学习的习惯就会自然地养成。即使在这 10 秒内你什么都没学到,你也正在建立每天学习一点的习惯。有些日子,也许你会最终学习一个小时或更长时间。

数学与 AI

数学是人工智能的基础技能吗?多了解一些数学总是好的!但是要学习的东西太多了,实际上,有必要进行优先排序。以下是如何加强你的数学背景的方法。

为了弄清楚什么是重要的知识,有必要问一问,为了完成你想要做的工作所需的决策,你需要知道什么 —— “为了实现目标,人们需要知道什么?”。

理解你所使用的算法背后的数学通常是有帮助的,因为它使你能够调试它们。但是,有用的知识深度会随着时间而变化。随着机器学习技术的成熟和变得更加可靠和即用,它们需要较少的调试,对涉及的数学有较浅的理解可能就足以使它们工作。

例如,在机器学习的早期,用于求解线性方程组(用于线性回归)的线性代数库还不成熟。我必须了解这些库的工作原理,以便在不同的库之间做出选择并避免数值舍入误差。但随着数值线性代数库的成熟,这一点变得不那么重要了。

深度学习仍然是一项新兴技术,因此当你训练神经网络时,如果优化算法难以收敛,理解梯度下降、动量和 Adam 优化算法背后的数学原理将帮助你做出更好的决策。同样地,如果你的神经网络表现异常——比如,在某些分辨率的图像上预测效果差,而在其他图像上则不然——理解神经网络架构背后的数学原理会使你处于更有利的位置来解决问题。

当然,我也鼓励由好奇心驱动的学习。如果某件事让你感兴趣,不妨去学习它,不管它可能有多么实用!也许这会激发你的创造力或技术突破。

识别 AI 的用武之地

步骤一,确定一个业务问题(而不是 AI 问题)

找到一个领域专家并询问:“你希望哪些事情能够变得更好?为什么这些事情还没有得到解决?”。例如,如果你想将 AI 应用于气候变化,你可能会发现电网运营商无法准确预测未来风能和太阳能等间歇性能源可能产生的电力。

步骤二,头脑风暴 AI 解决方案

不要因为一个想法令人兴奋就立马去执行,有时可能存在一个不需要更多努力就能实现的更好想法。一旦理解了问题,就可以更有效地头脑风暴潜在的解决方案,有时没有好的 AI 解决方案那也没关系。

步骤三,评估潜在解决方案的可行性和价值

可以通过查看已发表的论文、竞争对手的做法,或者也许构建一个快速的概念验证实现来确定一种方法在技术上是否可行,也可以通过咨询领域专家来确定其价值。

步骤四,确定里程碑

一旦你认为一个项目足够有价值,下一步就是确定要追求的指标。这包括机器学习指标(如准确率)和业务指标(如收入)。机器学习团队通常最擅长优化学习算法的指标。但我们可能需要走出舒适区,提出与用户参与度、收入等相关的业务指标。不幸的是,并不是每个业务问题都能归结为优化测试集的准确率!如果你无法确定合理的里程碑,这可能是一个信号,表明你需要更多地了解这个问题。一个快速的概念验证可以帮助提供缺失的视角。

步骤五,为资源制定预算

考虑项目所需的一切,包括数据、人员、时间以及可能需要的其他团队的任何集成或支持。

找到与自己匹配的 AI 项目

一些有用的原则

  • 花一个月时间来决定是否要开展一个只需一周就能完成的项目是不合逻辑的。
  • 在你的职业生涯中,你会参与多个项目,因此你将有充足的机会去完善你对什么是有价值的项目的思考。
  • 鉴于可能的人工智能项目数量众多,与其采用传统的“准备、瞄准、开火”方法,不如用“准备、开火、瞄准”来加速你的进展:
    • “准备,瞄准,开火”:仔细计划并进行仔细验证。只有在对某个方向有高度信心时才投入和执行。
    • “准备,开火,瞄准”:跳入开发并开始执行。这使您能够快速发现问題并在必要时中途调整方向。

如果你没有任何项目想法

  1. 加入现有的项目。如果你发现别人有好的想法,可以请求加入他们的项目。
  2. 保持阅读并与人交流。每当我花大量时间阅读、上课或与领域专家交谈时,我总会产生新的想法。我相信你也会这样。
  3. 专注于一个应用领域。许多研究人员正试图推进基础人工智能技术——比如,发明下一代 transformer 或进一步扩大语言模型的规模。因此,虽然这是一个令人兴奋的方向,但也是非常困难的。但是,机器学习尚未应用的领域是广阔的!如果你的公司或学校关心某个特定的应用,探索机器学习的可能性。这可以让你率先了解一个可能具有创意的应用——一个你可以做出独特工作的地方——而其他人还没有做到这一点。
  4. 发展一项副业。即使你有全职工作,一个有趣的项目可能会或不会发展成更大的事业,但可以激发创造力并加强与合作者的关系。只要不与雇主产生冲突,这些项目可以成为通往重大事业的垫脚石。

如果你有一些项目想法

  1. 这个项目会帮助你技术上成长吗?理想情况下,它应该具有足够的挑战性来提升你的技能,但又不至于太难,让你几乎没有成功的可能。这将使你走上掌握更高技术复杂性的道路。
  2. 你有好的队友一起工作吗?如果没有,有可以讨论事情的人吗?我们从周围的人身上学到很多,好的合作者会对你的成长产生巨大影响。
  3. 它能成为一块垫脚石吗?如果项目成功,它的技术复杂性和/或业务影响是否会使其成为更大项目的有意义的垫脚石?如果这个项目比你之前参与的项目更大,那么它很可能就是这样的垫脚石。

最后

用好 AI:Claude 帮我完成 Memos SQLite 到 MySQL 的数据库迁移

2024-11-17 00:16:46

近期阿里云双十一活动 RDS 99/年,非常有性价比,于是我就买了一个香港区的 MySQL RDS 用做我的 Memos 数据服务。这就不可避免的碰到了数据库迁移的问题,即需要将 SQLite 数据库迁移到 MySQL,这种生产环境的数据库迁移一般都非常谨慎,需要严格 SQL 开发和验证,需要花费不少的时间。由于 SQLite 文件有备份,刚刚购买的 MySQL 资源充足,心血来潮之下想玩一波大的 —— 试试 AI 来帮我完成生产数据库的迁移工作。

我完成这些事情是 prompt tune 的思路,主要包括:

  1. 找到当前版本 memos 的库表创建的 sql 文件;
  2. 创建测试用的本地 mysql 数据库服务;
  3. prompt 进行提示词问询,生成 python 迁移工具;
  4. 执行迁移测试,收集脚本报错以及数据库表问题;
  5. 根据测试发现的问题,进行 prompt tuning,生成新的 python 迁移工具;
  6. 根据第1步的 sql 文件完成库表创建,然后执行 python 迁移工具完成生产数据库的迁移;

前期准备

查找库表创建 SQL 文件

这里主要是确认当前生产数据创建了哪些库表,有两种方法:

  1. 去 SQLite 数据库文件中通过 SQL 命令查询:show create table ${table name}
  2. 去 memos sql 文件中查找,edonyzpc/memos/store/migration/sqlite/prod/LATEST.sql(如果非自定义镜像可以去 usememos 查找

本地 mysql 测试环境

1. 准备 init.sql 文件

CREATE DATABASE IF NOT EXISTS memos-migrate-test;

2. 准备 docker compose 文件

version: '3'
services:
  db:
    image: mysql:latest
    ports:
      - 3306:3306
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: your_root_password
    volumes:
      - ./data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

3. 启动本地测试 mysql

docker pull mysql

docker compose up -d

prompt 初始化

I have a notes service and the data is stored on sqlite database, now i want to migrate all the data to mysql, please help me to write a script in python. Here is the schema definition of the sqlite database
<<<LATEST.sql>>>

注意:替换 LATEST.sql 的内容。

Prompt Tuning

当前具备 code 分析能力的 LLM 均可以用来做迁移工具的 prompt tune,由于 Claude 在代码分析和生成等领域有非常不错的口碑,所以我这次使用了 Claude 3.5 Sonnet 作为调优模型。

1. 初始化 prompt

Here's a Python script that can help you migrate data from SQLite to MySQL based on the schema you provided:

\`\`\`python
import sqlite3
import mysql.connector
import json

# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="your_mysql_host",
    user="your_mysql_user",
    password="your_mysql_password",
    database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

for table in tables:
    print(f"Migrating table: {table}")
    
    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()
    
    if not rows:
        print(f"No data in table {table}")
        continue
    
    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]
    
    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
    
    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            else:
                processed_value = value
            processed_row.append(processed_value)
        
        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")
\`\`\`

Before running this script, make sure to:

1. Install required packages: `pip install mysql-connector-python`
2. Replace `'path_to_your_sqlite_database.db'` with the actual path to your SQLite database file.
3. Update the MySQL connection details (host, user, password, database) with your actual MySQL server information.

This script does the following:

1. Connects to both SQLite and MySQL databases.
2. Iterates through each table in the given list.
3. Fetches all data from the SQLite table.
4. Prepares an insert query for MySQL.
5. Inserts the data into the corresponding MySQL table, handling JSON fields if necessary.
6. Commits the changes and moves to the next table.

Note that this script assumes that you have already created the tables in MySQL with the same schema as in SQLite. If you haven't, you'll need to create the tables in MySQL first using the CREATE TABLE statements you provided.

Also, be aware that this script does not handle auto-increment fields specially. If you have auto-increment fields and want to preserve the exact IDs from SQLite, you may need to modify the script to handle this (e.g., by temporarily disabling auto-increment in MySQL during the migration).

2. prompt tuning

prompt tuning 就是在测试环境中执行 Claude 生成的工具,并将测试的报错内容反馈给 Claude 让它继续进行优化调整,并重新生成迁移工具。

库表没有创建

测试初始化 prompt 步骤中的迁移工具,发现有如下报错:

  • prompt tuning: 这一步不需要 prompt 优化,只需要在测试的 MySQL 中执行建表语句,完成库表的创建(LATEST.sql

时间戳表列类型不对

进行测试发现有时间戳相关报错:

  • prompt tuningmigrate error: Incorrect datetime value: '1698320364' for column 'created_ts'
  • prompt result
I apologize for the oversight. The error you're encountering is due to a difference in how SQLite and MySQL handle datetime values. SQLite often stores timestamps as Unix timestamps (seconds since epoch), while MySQL expects a formatted datetime string.

To resolve this issue, we need to convert the Unix timestamp to a datetime format that MySQL can understand. Here's an updated version of the script that handles this conversion:

\`\`\`python
import sqlite3
import mysql.connector
import json
from datetime import datetime

# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="your_mysql_host",
    user="your_mysql_user",
    password="your_mysql_password",
    database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']

def convert_timestamp(value):
    try:
        return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
    except ValueError:
        return value

for table in tables:
    print(f"Migrating table: {table}")
    
    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()
    
    if not rows:
        print(f"No data in table {table}")
        continue
    
    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]
    
    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
    
    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields and timestamps
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            elif column in timestamp_columns:
                processed_value = convert_timestamp(value)
            else:
                processed_value = value
            processed_row.append(processed_value)
        
        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")
\`\`\`

The main changes in this updated script are:

1. We've added a list of `timestamp_columns` that need to be converted.
2. We've added a `convert_timestamp` function that converts Unix timestamps to MySQL datetime format.
3. In the data processing loop, we now check if a column is in the `timestamp_columns` list and convert it if necessary.

This script should handle the timestamp conversion correctly. If you encounter any other issues or if some tables have different datetime formats, you may need to adjust the `convert_timestamp` function or add more specific handling for certain tables or columns.

Also, make sure that your MySQL tables are set up to accept NULL values for timestamp columns if applicable, as some of your SQLite data might contain NULL values for these fields.

关键字语法错误

再次进行测试发现有保留关键字导致语法相关的报错:

  • prompt tuningError inserting into user_setting: 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'key, value) VALUES (1, 'USER_SETTING_ACCESS_TOKENS', '{\"accessTokens\":[{\"acce' at line 1
  • prompt result
I apologize for the oversight. The error is occurring because 'blob' and 'type' are also reserved keywords in MySQL. We need to escape these column names as well. Here's an updated version of the script that handles this issue:

\`\`\`python
import sqlite3
import mysql.connector
import json
from datetime import datetime

# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="your_mysql_host",
    user="your_mysql_user",
    password="your_mysql_password",
    database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']

# List of reserved keywords that need to be escaped
reserved_keywords = ['key', 'value', 'blob', 'type']

def convert_timestamp(value):
    try:
        return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
    except ValueError:
        return value

def escape_column_names(columns):
    return [f"`{column}`" if column.lower() in reserved_keywords else column for column in columns]

for table in tables:
    print(f"Migrating table: {table}")
    
    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()
    
    if not rows:
        print(f"No data in table {table}")
        continue
    
    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]
    
    # Escape column names if necessary
    escaped_columns = escape_column_names(columns)
    
    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(escaped_columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
    
    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields and timestamps
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            elif column in timestamp_columns:
                processed_value = convert_timestamp(value)
            else:
                processed_value = value
            processed_row.append(processed_value)
        
        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            print(f"Problematic row: {processed_row}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")
\`\`\`

The main changes in this updated script are:

1. We've added 'blob' and 'type' to the list of reserved keywords that need to be escaped.
2. The `escape_column_names` function now checks against this expanded list of reserved keywords.
3. I've added a line to print the problematic row when an error occurs, which can help with debugging if you encounter any other issues.

This change should resolve the SQL syntax error you encountered with the `resource` table. The script now properly escapes the 'blob' and 'type' column names, which should allow the insertion into the `resource` table to proceed without errors.

If you encounter any other issues, please let me know, and I'll be happy to help further. Also, remember that if your `blob` column contains large binary data, you might need to

3. 完成 prompt tuning

调优完成之后,就可以向正式的 MySQL 数据库实例进行数据迁移了,主要注意点的:

  • RDS 中创建好数据库实例之后,需要用 LATEST.sql 进行初始化库表
  • prompt tuning 步骤在测试环境迁移完成之后,最好用指定 driver 的方式测试一下 memos 服务是否正常,参考:Database Drivers - Memos
  • 还可以让 Claude 帮你增加建库表的过程,省去了自己初始化库表的事情了

附录

import sqlite3
import mysql.connector
import json
from datetime import datetime

# SQLite connection
sqlite_conn = sqlite3.connect('~/.memos/memos_prod.db')
sqlite_cursor = sqlite_conn.cursor()

# MySQL connection
mysql_conn = mysql.connector.connect(
    host="xxxxxx.rds.aliyuncs.com",
    user="xxxxxxxxxxxxxxxxx",
    password="xxxxxxxxxxxxx",
    database="xxxxxxxxxxxxx"
)
mysql_cursor = mysql_conn.cursor()

# List of tables to migrate
tables = [
    "migration_history", "system_setting", "user", "user_setting", "memo",
    "memo_organizer", "memo_relation", "resource", "activity", "idp",
    "inbox", "webhook", "reaction"
]

# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']

# List of reserved keywords that need to be escaped
reserved_keywords = ['key', 'value', 'blob', 'type']

def convert_timestamp(value):
    try:
        return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
    except ValueError:
        return value

def escape_column_names(columns):
    return [f"`{column}`" if column.lower() in reserved_keywords else column for column in columns]

for table in tables:
    print(f"Migrating table: {table}")

    # Fetch data from SQLite
    sqlite_cursor.execute(f"SELECT * FROM {table}")
    rows = sqlite_cursor.fetchall()

    if not rows:
        print(f"No data in table {table}")
        continue

    # Get column names
    columns = [description[0] for description in sqlite_cursor.description]

    # Escape column names if necessary
    escaped_columns = escape_column_names(columns)

    # Prepare MySQL insert query
    placeholders = ', '.join(['%s'] * len(columns))
    columns_string = ', '.join(escaped_columns)
    insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"

    # Insert data into MySQL
    for row in rows:
        # Convert any JSON fields and timestamps
        processed_row = []
        for value, column in zip(row, columns):
            if column in ['tags', 'payload']:
                try:
                    processed_value = json.dumps(json.loads(value))
                except:
                    processed_value = value
            elif column in timestamp_columns:
                processed_value = convert_timestamp(value)
            else:
                processed_value = value
            processed_row.append(processed_value)

        try:
            mysql_cursor.execute(insert_query, processed_row)
        except mysql.connector.Error as err:
            print(f"Error inserting into {table}: {err}")
            print(f"Problematic row: {processed_row}")
            continue

    mysql_conn.commit()
    print(f"Migrated {len(rows)} rows for table {table}")

# Close connections
sqlite_conn.close()
mysql_conn.close()

print("Migration completed")

References

  1. Local Storage - Memos

我的消费决策模型

2024-11-10 00:17:32

双十一了,好物清单、什么值得买之类的内容铺天盖地,到底我们在消费的时候受到哪些东西影响,我觉得需要研究一下消费决策模型。根据恩格尔(Engel)等提出的 EBK 模型可以看出消费时原来是这么样复杂,哪怕买瓶水、买个包子都无形中被这么多因素影响着。为了自己的钱包,我觉得很有必要建立属于自己的消费决策模型。

消费是如何产生的

根据 EKB 模型,消费决策是由五步构成的:

1. 认识问题

当人们认为理想与实际之间有差距时,便产生了问题,问题产生后则整个系统(内外)便开始运作,目标也化成了具体的行动。

例如:工作了半天感觉肚子饿了,继续饿着就没力气干活了,看来需要找点吃的了。

2. 收集信息

当人们发现了问题的存在,便会去搜寻相关的信息,信息搜寻又可分为内部搜寻与外部搜寻两种,所谓内部搜寻是指在其现有资料或过去的购买经验中去寻找。当内部搜寻无法满足其需要时,便会转由外部寻找,所谓的外部搜寻诸如大众传播媒体、行销人员及亲友等。

例如:

  • 上次朋友推荐说办公室楼下的面条,赶紧去尝尝解决一下饿肚子问题;
  • 办公的地点人生地不熟的,不知道去哪里找吃的,群里问问有没有人知道或者直接美团上搜搜看;

3. 评估方案

收集到信息之后就需要判断一下哪个更好,哪个跟适合。

例如:

  • 面馆人太多了,要排队等很久,包子铺人少而且味道不错;
  • 不行,最近工作太幸苦了,需要好好补补,奢侈一下买份豪华牛排套餐犒劳一下自己;
  • ......

4. 选择执行

当评估了各种可能的方案后,便会选择一个最能解决原始问题的方案并采取消费行为。

例如:

  • 买份豪华牛排套餐加上星巴克咖啡;
  • 买份性价比高的大排面;

5. 结果反馈

当消费并使用了某产品后,可能发生满意或购买认知失调两种结果,并存储于记忆当中。

例如:

  • 楼下的西餐店的牛排太难吃了,而且等餐化了很长时间,下次再也不去了;
  • 大排面量大、料足,味道出奇的好,还能加面,下次还去;

我的消费决策模型

知彼

商家为了刺激消费,常常会巧妙地运用心理学知识。这些心理技巧潜移默化地影响着我们的消费决策。下面就来详细介绍一些常见的商家常用的心理学知识:

1. 稀缺性原理:

  • 限时限量: 创建一种紧迫感,让人们觉得如果不马上购买就会错过。
  • 独家产品: 强调产品的独特性和稀缺性,让人们产生“拥有感”。
  • 库存告急: 通过展示库存数量,营造抢购氛围。

2. 社会认同感:

  • 明星代言: 利用名人效应,让人们相信产品值得信赖。
  • 用户评价: 展示其他用户的正面评价,增加产品的可信度。
  • 从众心理: 强调产品受欢迎程度,让人们跟风购买。

3. 锚定效应:

  • 原价标注: 先展示一个高价,再标出折扣价,让人觉得捡了便宜。
  • 对比效应: 将产品与更昂贵的同类产品进行比较,凸显其性价比。

4. 损失厌恶:

  • 免费试用: 提供免费试用,让人们产生使用习惯,一旦停止使用就会感到损失。
  • 退货保证: 提供无风险购买,降低购买门槛。

5. 互惠原则:

  • 赠品: 提供小礼品或赠品,让人们产生回报的心理。
  • 免费试用: 通过免费试用建立关系,为后续销售做铺垫。

6. 喜欢原则:

  • 相似性: 强调产品与消费者之间的相似性,增加好感度。
  • 赞美: 给予消费者赞美,让他们产生愉悦感。

7. 权威原则:

  • 专家背书: 引用专家意见,增加产品的权威性。
  • 认证标志: 使用各种认证标志,提高产品的可信度。

8. 承诺一致性:

  • 小额承诺: 先让消费者做出一个小小的承诺,再引导他们做出更大的承诺。
  • 公开承诺: 让消费者公开自己的决定,增加其坚持的可能性。

知己

理解消费决策的心理过程并据此做出明智的消费选择是至关重要的。下面提供了一个优化后、更具可操作性的消费决策分析和思考方法,整合了心理学原理和实践指导:

1. 内在需求与外部影响分析:

  • 识别核心需求:深入分析自己寻求解决的是哪一种内在需求,如基本需求、社交需求还是个人成长需求。需求是指那些必须满足以维持基本生活水平的必需品,而欲望则更多关联于提升生活质量的选择性购买。
  • 评估外部因素:反思购买决策是否受到外部因素如广告、同伴压力或社会趋势的影响。

2. 信息搜集与处理:

  • 全面信息搜集:通过多种渠道搜集关于产品或服务的全面信息,包括正反两面的评价和数据。
  • 理性评估:比对不同的选项,从功能性、性价比、可持续性等多个角度进行客观评估。

3. 情绪因素与理性平衡:

  • 情绪自我检查:认知到目前的情绪状态,并审视这些情绪是否可能影响消费决策。学会管理这些情绪,避免它们在决策过程中起重要作用。
  • 避免冲动消费:实施“冷却期”原则,给自己一段时间(例如24小时)来考虑是否真的需要购买。

4. 财务评估与计划:

  • 制定严格预算:确保消费决策符合个人或家庭的长期财务规划与预算。
  • 成本效益分析:从长期角度考虑购买的成本与其带来的效益和价值。

5. 反思、学习与调整:

  • 后置反馈循环:消费后,进行深度反思,比如满意度、是否满足了预期的需求等。
  • 持续信息更新与学习:定期回顾和更新自己对市场的理解以及消费决策的影响因素。

消费决策检查清单

最后分享一个我自己正在尝试使用的简单消费决策逻辑:

使用说明:

  • 在每次消费前,逐条认真思考并诚实回答。
  • 如果有一条或多条无法勾选,说明你可能需要重新考虑这个消费决定。
  • 对于大额消费,建议多次重复几次。
  • 建立一个消费日记,记录每次购买的物品、金额、原因以及是否满意。
  • 了解基本的财务规划和投资知识,帮助你做出更明智的消费决策。
  • 这份检查清单可以根据个人需求进行调整和扩展。 例如,如果你非常关注可持续发展,可以增加一条“这个产品是否环保?”;如果你对健康生活方式很在意,可以增加一条“这个产品是否健康?”
  • 不要被情绪冲昏头脑,多问自己是否真的需要。
  • 不要只看一家,多比较几家,选择最适合自己的。免费的东西往往伴随着隐藏的成本。

References

  1. 消费者购买决策理论 - MBA智库百科