普通视图

发现新文章,点击刷新页面。
昨天以前ZMonster

找工作啦

2024年5月13日 08:00

本来还没有去想返工的事情,然后五一前被前领导催简历了,就整理了一下发了过去后面聊了聊。既然都动起来了,索性在五一后也找几个朋友聊了下或者让帮忙做下内推之类的。总体来说现在市场行情并不算好,有 HC 的公司不算多,不过我也没什么太多想法,对于一些更感兴趣的还是会争取一下,网上也还在投投看,不过也就是最近几个星期打算处理一下这个事情,如果月底前没有很满意的,可能就继续躺着或者去我前领导或者同学那里待着了。想了下,把简历脱敏下在博客上也放一份吧,虽然应该概率很小能有看对眼的,如果我的经历里有什么项目和业务觉得感兴趣也欢迎来找我聊聊,反正几个公司都倒闭了,除了客户信息应该没啥不能聊的。工作这么多年,精力都放在公司业务上了,并没有自己去做什么个人项目,好在最近问了下前前司 CTO 得到了同意,准备把陪伴多年的几个文本处理的项目整理一下开源出来,是倾注了我比较多心血的项目,我自认为还是有一些独特价值的。

准备回头把个人经历也整理一下放到 about 页面吧。

个人信息

技能清单

  • 编程语言: Python | Shell | C | C++
  • 机器学习工具: scikit-learn | Pytorch | Tensorflow | Numpy | NLTK
  • NLP 技能: 文本分类 | 序列标注 | 信息检索 | 对话系统 | 知识图谱 | 信息抽取 | 文本挖掘
  • 数据库相关: MySQL | Redis | Neo4j | Dgraph
  • 其他: Linux | Git | Docker | K8S | Django | Elasticsearch

工作经历

北京********有限公司(2021/01~2023/05)

  • 开发基于 LLM 的聊天应用服务(2022/12~2023/05)
    • 使用 Django 实现服务,接入 OpenAI 接口,用于在飞书中集成以及之后上线的 toC 聊天应用
    • 支持了用户自定义 prompt,实现了 RAG 支持用户上传文档数据后基于文档进行问答,并能根据用户设置的 prompt 和上传文档生成机器人名字、欢迎语
    • 在对话接口中集成 Stable Diffusion 的图像生成功能支持用户在对话界面中生成图片
    • 以对话接口的形式实现了一些管理功能并接入到飞书,供运营人员查看分析聊天记录、修改机器人 prompt 和知识库、修改用户可用聊天额度等
  • 开发金融资讯服务(2021/08~2022/05)
    • 使用 Django 实现后端服务进行资讯数据的存储并提供 RESTful API
    • 使用 K8S 进行部署和运维,服务可以方便地进行水平扩展来提高负载能力
    • 使用前文所述文本处理框架对金融资讯进行去重、清洗和结构化分析,所得分析结果作为资讯推荐的特征,实现了金融资讯信息流、相关资讯推荐、要闻推荐等服务
  • 设计和开发文本处理框架(2021/01~2022/11)

    • 对文本处理过程进行抽象,实现了适用大部分文本处理过程、组件化、可扩展性强的框架,开发者可通过自定义组件以及组件与组件的组合实现复杂的自定义文本处理过程
    • 框架实现后逐步将标准的一些文本处理方法和业务中提炼出来的相对通用的文本处理方法作为组件添加到框架中,目前已集成分词、关键词提取、实体识别、实体融合、文本分类、文本摘要、观点挖掘等领域数十个组件

    该框架作为基础工具被用于公司金融资讯结构化分析的各项业务中。

****科技发展(北京)有限公司(2016/08~2020/12)

  • 参与问答系统框架的开发和优化(2019/10~2020/12)
    • 重构项目,在不削减功能的前提下将代码量削减 30%,将单元测试覆盖率从 47% 提升至 98%
    • 制定评估标准,实现评估工具,分析模型的泛化、拒识、抗混淆、多轮对话等各维度能力,以此为依据帮助在不同要求的项目中选择不同算法和参数,减少模型优化所需的人工工作量
  • 设计和开发基于知识图谱的推理问答框架(2018/01~2020-02)
    • 实现了数据库无关的 library 来读写图数据库,支持 Neo4j/Dgraph
    • 以 pipeline 架构实现图谱问答框架,各组件可方便的扩展、定制
      • 对用户问题先进行实体识别和关系识别
      • 在实体识别、关系识别的结果上,根据本体结构进行推导得到问题的形式化表达
      • 将问题的形式化表达转换为图谱查询、计算操作,得到结果
      • 用预先设计好的答案模板来渲染图谱查询结果,输出回答
  • 设计和开发通用的信息抽取框架(2017/06~2020/12)

    • 对不同的抽取技术(正则、匹配、序列标注)提供统一的抽取、归一化、校验接口
    • 提供对已有抽取结果的组合、扩展抽取以进行更复杂的任务
    • 设计为配置化的使用方式,按照规范写好配置文件即可构造各种抽取器

    该框架作为基础工具被用于实现实体识别、文档抽取功能及其他信息抽取类业务中。

  • 设计和开发 PDF 文档解析工具(2018/01~2020/07)
    • 使用 PDFMiner 获得基础的 PDF 解析结果,再基于规则和分类器进行内容元素分析
    • 实现了 PDF 文件中目录、章节标题、列表、页眉页脚、表格、脚注等不同元素的解析
  • 构建保险行业知识图谱(2017/12~2020/02)

    • 借助前文提到的 PDF 解析工具和信息抽取框架,从保险文档中抽取获得初步的保险知识
      • 借助文档解析得到的结构化信息(章节、注释等)及规则实现了大部分简单知识的抽取
      • 使用 BiLSTM+CRF 模型抽取部分非显式知识,并在数万篇文档上用 ELMO 预训练来提高效果
    • 与产品团队共同梳理清楚保险领域知识图谱的本体结构
    • 以本体结构为指导信息,对文档抽取的结果进行组合、二次抽取得到实体关系,最终形成了十万量级实体、百万量级关系(含属性)的保险领域知识图谱
      • 一部分实体及属性直接采用文档抽取结果,其余主要基于规则对抽取结果进行二次抽取
      • 针对文档抽取结果,实现了一个简单的图谱构建框架,将图谱构建分为实体获取、属性获取、关系获取、融合和链接等步骤,且允许通过配置文件对这几个步骤进行设置和扩展
    • 结合基于知识图谱的推理问答框架,实现了一个保险领域的推理问答机器人

    凭借这份知识图谱及保险问答机器人,公司成功与国内若干家保险公司、保险经纪公司、保险代理公司达成合作。

  • 设计和开发通用文本分类工具(2017/08~2019/03)
    • 预处理、分词、特征提取等过程均支持配置化,大大提高了在中小数据上进行文本分类的效率
    • 预处理器、分词器、特征提取、分类器等各个模块都可以方便地定制、扩展
    • 被广泛应用于公司各个项目的意图识别、情感分析、垃圾过滤等文本分类相关的业务中
  • 建立通用实体抽取工具(2016/11~2020/12)

    • 使用前文提到的信息抽取框架,实现了常用的细分为二十多类实体的抽取
    • 数字、日期时间等实体的抽取使用正则实现
    • 人名、地名、组织名的抽取基于已有 NLP 工具的 POS 和 NER 结果,并用自己训练的 BiLSTM+CRF 模型作为补充
    • 部分特定领域实体的抽取使用词表和 AC 自动机实现

    该工具作为基础库在公司几乎所有问答项目中被使用,显著提高了各项目实现相关需求时的效率。

  • 设计和开发相似句/同义词挖掘工具(2017/10~2018/03)

    • 使用 LCS、TFIDF+余弦相似、词向量+余弦相似等方法从语料中挖掘相似句子
    • 使用翻译接口直接生成相似句子
    • 对相似句子进行文本对齐,从中挖掘同义词数据,中小数据上效果好于直接按词向量挖掘

    该工具先后用于电商、物业、保险等多个领域的问答项目的初期数据构建,挖掘结果可用率约 30%,在挖掘结果上做 0/1 标注的效率和效果都高于人工直接撰写相似句子、同义词。

  • 开发订餐对话机器人(2016/11~2017/05)

    • 任务式的问答机器人,涉及订餐、查询订单、修改订单、取消订单四个意图及数十个槽位,确定意图后询问用户填充该意图所需槽位
    • 不直接回答而是产生三个候选答案供客服进行选择,上线后 TOP3 采用率稳定在 30% 左右

    该项目是公司第一个正式的商业合作项目,且间接或直接催生了通用实体抽取工具、通用文本分类工具、任务式问答框架等多个基础项目。

北京****网络科技有限公司(2015/03~2016/06)

  • 开发和优化题库搜索系统(2015/04~2016/03)
    • 基于倒排索引实现了一个搜索系统,用于 2000 多万题目的搜索
    • Python+Redis 实现,单次搜索耗时 1-2 ms,首位命中率 > 95%
  • 开发和优化扫题用的 OCR 系统(2015/06~2015/11)
    • 使用 OpenCV 进行二值化、去阴影、倾斜矫正、字符检测和切割等图像处理,使用 Caffe 进行字符识别
    • 单字识别率达 96%,OCR+题库搜索正确率约 70%
  • 开发翻译结果自动评分模块(2016/01~2016/03)
    • 对学生提交的翻译结果,和标准答案对比,给出评分
    • 使用 METEOR 算法,与人工评价之间的 RMSE 约为 1.5(15 分制)

****技术(北京)有限公司(2014/03~2015/02)

  • 研究和实现说话人分割技术(2014/06~2015/01)
    • 在大规模语音数据训练出来的人声 GMM 上做最大后验估计得到内容无关的音频特征
    • 将音频切成小段并去除非人声后,对邻近段落使用层次聚类来区分出不同的说话人
    • 在两人对话场景下正确率 > 90%,处理时长与音频时长比 < 0.03
  • 研究并实现语音与文本融合的多模态情感分析(2014/09~2015/01)
    • 混合语音特征和文本特征,使用 SVM 进行分类,正确率 > 90%

ZMonster's AI Notes(Alpha) #2:模型汤、推测解码、幻觉的类型与定义、GPT top_logprobs

2024年2月7日 08:00

分享最近关于 AI 方面的笔记、想法以及实践记录。本系列内容模式的最终形态尚不确定,可能会根据个人精力、兴趣及阅读反馈做调整,所以称之为试作版。

从第一期之后拖延了很久,先是花了比较多精力做 2023 年的回顾总结,之后则因为即将过年回家比较焦虑而导致没有办法集中精力,看下过完年后能不能争取专注一些。下一期准备集中了解下幻觉的检测与缓解方法。

术语

简单介绍下我最近新了解到的 AI 领域的一些术语,但不做深入探究。

模型汤(Model Soup)

论文《Model soups: averaging weights of multiple fine-tuned models improves accuracy without increasing inference time》 提出的概念,本质上属于模型合并(Model Merging)的一个方法,作者发现把同一个模型在多个不同下游任务上微调过后的权重进行加权平均(论文中尝试了多种方法如直接平均或者学习加权参数)后,能在新的下游任务上也得到效果的提高(相比未微调的基础模型)。其解释我理解是说两个不同的下游任务在微调时会在损失函数的曲面上走不同的下降路线,加权平均后往往能更接近极值点。

2024-02-06_11-37.png

在这个发现的基础上, 《Editing Models with Task Arithmetic》 这篇论文进一步发现微调模型与基础模型之间的权重差(论文中称之为任务向量)具有语义,微调过后的模型可以通过减去这个权重来遗忘这个任务,不同任务的任务向量相加也能获得一个综合了多个任务能力的新模型。

2024-02-06_11-42.png

时间错位(Temporal Misalignment)

《Time Waits for No One! Analysis and Challenges of Temporal Misalignment》 等一些论文里发现用时间段 A 内训练的大模型在另外的时间段 B 上效果会不好,反映到现实中的大模型表现就是其效果会随时间变化而慢慢在新的文本上效果变差,看起来好像是退化一样 —— 其实不能说是退化,只是人们使用的语言一直在变化,如果要让模型始终保持稳定的表现就需要持续地使用新的数据去进行训练微调。

2024-02-06_12-05.png

之后 2023 年的论文 《Time is Encoded in the Weights of Finetuned Language Models》 进一步发现,模型的能力在不同年份之间会变差,但相近时间段文本训练的模型效果也会接近,并借鉴前面「模型汤」一节中提到的任务向量的概念,提出了时间向量,发现将两个不同的时间向量进行插值(其实也是加权平均)可以使模型在这两个时间段之间的时间段效果变好,认为可以用不同时间段的文本数据进行微调后得到一个个时间向量然后在有需要的时候混合起来,以低成本地改善任意时间段的模型效果,将这种思路称之为「时间汤」,当然最后也承认这种做法效果还是比不上完整收集所有时间段的数据统一进行训练,不过我个人认为还是很有用的,现在有很多开源的语言模型,借鉴这个思路可以在已有的模型上低成本地进行改进。

2024-02-06_12-50.png

推测解码(Speculative Decoding)

Google Research 在论文 《Fast Inference from Transformers via Speculative Decoding》 中提出的一种语言模型生成加速的方法,说是受 CPU 里的分支预测技术的启发,其基本思想是基于模型越大生成每一个 token 时越慢这个状况,引入一个更小更快的草稿模型,让草稿模型来去生成 token 然后让更大的模型去决定是否要接受这个 token,只有草稿模型的生成被认为不够好被拒绝时,才用更大的模型自己去生成 token。按照论文里的不同设置,最高的时候能够以原来不使用草稿模型的生成方法的 6 倍多的速度来完成整个生成过程。

2024-02-06_18-12.png

另外 DeepMind 也有一篇论文 《Accelerating Large Language Model Decoding with Speculative Sampling》 讲推测解码的,不过论文里说明了和前面的这篇论文是相同的思路。

论文

写完第一期之后,我选择了幻觉(hallucination)这个主题,决定去梳理清楚这几个问题:

  • 目前 LLM 中经常被提的幻觉的定义是什么?
  • 幻觉都有哪些类型?

作为一个新兴的概念,幻觉这个术语目前并没有非常一致的共识和清晰的定义,从看到的论文里来看,有一些会尝试先给出相对严格的整体性定义和描述,再仔细区分其中的不同类型,有的则对整体定义一带而过只具体描述了几种类型,另外还有大量在不同子领域分析幻觉现象的工作,其中的幻觉定义和分类就更加繁杂了,我尽量先了解了下任务不相关的幻觉定义及其分类。

Ziwei Ji 的 《Survey of Hallucination in Natural Language Generation》 这篇论文是目前我读到的对幻觉的定义比较通用和清晰的,也是我看的各种幻觉相关的论文里被引用的比较多的,有很多关于幻觉的论文自己不想做定义就直接引用下这篇论文。

这篇论文先从心理学上「幻觉」的定义出发,引用了 Blom 在《A Dictionary of Hallucinations》一书中给的定义:

In the general context outside of NLP, hallucination is a psychological term referring to a particular type of perception. Blom define hallucination as “a percept, experienced by a waking individual, in the absence of an appropriate stimulus from the extracorporeal world”.

这个定义里有几个要点:

  1. 没有外部刺激
  2. 但人脑仍产生了类似有外部刺激时的感受
  3. 并且这种感受可以很真实

借鉴心理学上的定义,Ziwei Ji 给了关于幻觉的定义:

The generated content that is nonsensical or unfaithful to the provided source content.

简单来说,就是模型生成了无意义的文本或者不忠实于指定信息或知识的文本,也就是网络上大家调侃的「一本正经地胡说八道」。此外论文里还有一些幻觉文本的特点的描述,比如说这种文本在表达上一般都很流畅,看起来似乎也是基于某种真实的语境或者背景知识,但往往这种背景知识并不存在或者是错误的。

不过需要注意的是,Ziwei Ji 的定义里有一个「source content」,具体来说:

  • 对摘要任务来说,source content 是指待进行总结的文本
  • 对翻译任务来说,source content 是指待进行翻译的源语言文本
  • 对多轮对话来说,source content 是指对话历史中与当前消息有关的消息
  • 对检索式问答来说,source content 是指根据用户输入检索到的问答对语料数据
  • 对 data-to-text 来说,source content 是指给定的结构化数据
  • ……

那开放式问答这种没有提供上下文的情况,所谓 source content 应该指什么呢?论文里为了保持一个统一的定义,就说这个时候的 source content 应当是世界知识(world knowledge),不过所谓的世界知识又怎么定义就没再提了,虽然看起来是将问题转移了,但比起其他一些论文里不加说明就使用某些假设的做法来说已经好很多了。

在关于幻觉的文献里,经常会在提到幻觉时一起提到忠实性(faithfullness)和事实性(factualness)这两个词,不同论文对这三个概念的使用其实还挺混乱的,Ziwei Ji 也在论文里对这三个概念做了阐释和区分(这也是我喜欢这篇论文的原因,别的很多论文都是一副理所当然的样子把这三个词拿出来用)。在 Ziwei Ji 看来,忠实性是说当我们给定一些信息或知识(注意,用户有可能有意或无意给一些错误的知识)时模型是否能在生成结果时与其保持一致,而事实性则是能否与现实中的事实保持一致,而一些关于幻觉的论文粗暴地将提供给模型的信息或知识当作「事实」(或者说用 fact 这个词来描述给定的信息)就造成了忠实性和事实性两个概念的混淆,而 Ziwei Ji 的做法就是用「世界知识」这个词来代替「事实」以避免混淆,将「世界知识」也作为一个 source content,这样也获得了一个统一的定义。虽然看起来有点文字游戏,但明确地划定词语的界限是很有必要的。

在上述定义的基础上,Ziwei Ji 给出了两个幻觉的子类型定义,分别是:

  • 内在幻觉(Intrinsic Hallucinations):指生成的文本与 source content 矛盾、不一致,比如说做摘要明明原文说了「The first vaccine for Ebola was approved by the FDA in 2019」但生成的摘要里却说「The first Ebola vaccine was approved in 2021」
  • 外在幻觉(Extrinsic Hallucinations):指生成了与 source content 无关的内容,用给定的 source content 无法验证它是对的还是错的,比如说做翻译的时候,原文是「迈克周四去书店」,按理说应该翻译成「Michael went to the bookstore on Thursday」,结果得到的结果是「Michael happily went to the bookstore with his friend」,这个「happily」和「with his friend」就是外在幻觉 —— 当然,从翻译任务上来说我们当然可以说它是错的,但前面说的「用给定的 source content 无法判断它是对的还是错的」是考虑任务无关的情况的

这个分类和其他一些论文里将幻觉分为忠实性幻觉和事实性幻觉是不一样的,在 Ziwei Ji 这个分类里,内在幻觉有些是忠实性的(当为翻译、摘要等任务时)有些是事实性的(当为开放问答即 source content 是世界知识时),而外在幻觉是指多出来一些无关的内容(当然这个无关具体如何定义和评估又是一个值得讨论的问题),这些内容未必是事实错误的甚至未必是一种错误。有些论文里将幻觉和错误混为一谈在读的时候也让人感到混乱,如果将所有错误都称之为幻觉,那这个新的词也就没有必要存在了。

之后,Yue Zhang 等人在论文 《Siren's Song in the AI Ocean: A Survey on Hallucination in Large Language Models》 中使用了 Ziwei Ji 对幻觉的定义,并在 Ziwei Ji 的分类基础上基于自己的认识将幻觉分成了三类:

  • 输入冲突幻觉(Input-Conflicting Hallucination):和 Ziwei Ji 的内在幻觉的定义基本是一样的
  • 上下文冲突幻觉(Context-Conflicting Hallucination):指模型在生成长文本或多次生成时的前后不一致,比如说在生成一个小故事的时候先在开头生成了「小明今年15岁」然后到结尾的时候又生成了「13岁的小明表示看不懂但大受震撼」,从模型运作的方式上来说,前面生成的内容其实也在后面生成的时候充当输入角色了,算是对内在幻觉的一个扩展吧
  • 事实冲突幻觉(Fact-Conflicting Hallucination):即生成的内容与世界知识、现实中的事实不相符的情况

Yue Zhang 引用了 Ziwei Ji 对幻觉的定义,但他又直接忽略了外在幻觉这个类型,三个分类其实都可以算在 Ziwei Ji 的内在幻觉这个类型下面,这个也可以理解,毕竟外在幻觉的情况其实有些模糊,在实际操作中并不是特别好进行界定。

然后,Hongbin Ye 等人的论文 《Cognitive Mirage: A Review of Hallucinations in Large Language Models》 也引用了 Ziwei Ji 的定义,但没有像 Yue Zhang 一样尝试去做幻觉的类型划分,而是收集了各个具体任务中的幻觉定义和类型划分的工作,如果想要了解特定任务而非任务无关的幻觉的定义和分类,这篇论文是一个非常不错的入口。限于个人精力我暂时没有去对里面罗列的各种其他论文去做了解,等有需要了再说吧。

2024-02-06_21-15_2.png

接着,Lei Huang 等人的综述性论文 《A Survey on Hallucination in Large Language Models: Principles, Taxonomy, Challenges, and Open Questions》 也基于 Ziwei Ji 的定义给出了自己的二级幻觉分类:

  • 事实性幻觉
    • 事实不一致(Factual Inconsistency):指生成结果里有与已知事实不一致的的错误生成结果,认为这个是最常见的
    • 事实编造(Factual Fabrication):指生成结果里一些陈述,这些陈述用已知的事实无法验证、或者说已知的事实 —— 话说回来,编造的事实能称之为「事实」么,这种用词就让人觉得比较混乱
  • 忠实性幻觉
    • 指令不一致(Instruction Inconsistency):指模型没有遵循用户的任务指令去做了别的事情,比如让翻译结果因为待翻译的内容是一个问句就去回答这个问句了
    • 上下文不一致(Context Inconsistency):指生成结果单纯和输入中的内容不一致的情况,和 intrinsic hallucination、input-conflicting hallucination 是一个意思,但用的是 context 这个词……
    • 逻辑不一致(Logical Inconsistency):指在做逻辑推理时,生成的推理步骤之间不一致或者推理步骤和最终结果不一致的情况,可以算作 Yue Zhang 分类里上下文冲突幻觉的一个特例

Lei Huang 的分类我是觉得有点问题的:首先指令不一致有专门的工作即指令追随(Instruction Following),我认为这是一种混淆错误与幻觉的行为,而作者在论文里给出这个分类的理由居然说是考虑现在 LLM 非常以用户为中心需要重点考虑和用户保持一致,我觉得不太能说服我;此外,为什么加入逻辑不一致这个分类,也没做什么解释,大概也是认为逻辑推理能力现在研究比较热门?如果是一篇专门讲逻辑推理中幻觉问题的论文,我觉得没什么问题,但这篇论文又标榜自己是个综述,就感觉这样做不太合适。

除了上述基本都基于 Ziwei Ji 给的定义来进行定义扩充或分类细化的工作外,也有一些明确提出定义标准的,目前我看到说得比较清楚的是 Ayush Agrawal 等人在其论文 《Do Language Models Know When They're Hallucinating References?》 所给出的定义:

We define hallucination to be fabricated text, meaning text that is not grounded in this training set. In contrast, correctness is evaluated with respect to ground-truth answers.

也就是说,在 Ayush Agrawal 的定义里,所谓的幻觉是指生成结果中无法在训练数据中找到依据的杜撰文本。这个定义就把「事实性」「正确性」这些特别宽泛模糊很难界定的概念完全排除出去了,作者也明确说认为幻觉未必就是事实上不正确的,比如说训练数据里提到说人脑只开发了 10% 虽然它是错误的,但基于这样的训练数据训练出来的模型如果生成了相关的文本,那么就不能称之为为幻觉,并认为现在很多讨论幻觉的工作把 groundedness (不知道该怎么翻译,在这篇论文里的语境里就是指在训练数据中存在相关的文本这个意思)和 correctness 也就是正确性混为一谈。

Much work on hallucination conflates groundedness and accuracy, often equating hallucination with fallacy and evaluating hallucinations using accuracy on fact-based assessments, without regard to the training data. We adopt the groundedness definition of hallucination even though it may often be less clear-cut and more difficult to evaluate than factuality.

这个定义相比 Ziwei Ji 的定义做了非常强但也很明确的约束,可能未必符合大众的认知和期望,但我还蛮喜欢这个定义的,清晰明确便于进行评估。

实践

GPT API 中的 top_logprobs 参数

在之前,OpenAI GPT 的 LLM API 分为 CompletionsChat 两个,其中 Completions 接口能使用 text-davinci-003 等更旧的 GPT-3 模型,而 Chat 接口则可以使用 gpt-3.5-turbo 和 gpt-4 等更新的模型,但我一直都很喜欢 Completions 接口,因为这个旧的接口提供一个 logprobs 参数,可以输出模型给每个 token 的概率,甚至还能输出每一个 token 时的 topn 的其他 token 及概率,这就使得我可以利用它来计算给定 prompt 或者输出的困惑度(perplexity) ,用来做简单的效果评估,或者也可以通过每一步输出的 topn 结果进行组合在一次调用里生成多个结果(对输出非步骤性列表比如说帮忙起名字的任务来说很有用的),In-Context Learning 里一些用来判断模型是否存在 bias (比如说做情感分类的时候发现给定一个空输入的时候也会倾向于预测为 positive)并基于这个 bias 的偏离概率去做校准的工作也需要能得到每个 token 的概率……但长期以来能使用更新更好模型的 Chat 接口都没有支持这个参数,也不知道 OpenAI 在干什么。

这几天去翻了下 API 说明发现 Chat 接口已经支持输出每个 token 的概率以及 topn 的 token 及概率了,看了下 Changelog 是在 2023-12-15 的更新里加上的,不过和旧的 Completions 接口有一点区别:

  • logprobs 参数用来设置是否要返回 token 的概率
  • top_logprobs 参数则用来设置要返回最好的几个 token 及其概率,最大值是 5

不过美中不足的是,原先 Completions 接口还有个 echo 参数当设置为 true 的时候会在输出结果里带上输入 —— 不是说将输入的内容重新生成了一遍,只是将输入内容附加到输出内容前面,有了这个参数才能获得输入中每个 token 的概率用来计算 prompt 的困惑度,但这个参数在 Chat 接口中仍然没有支持。

把之前自己利用 Completions 接口的 logprobs 参数做困惑度计算(如前面所说 Chat 接口只能计算生成结果的困惑度无法计算 prompt 的困惑度)和多个结果生成的脚本改了下,放到 gists 了,这里就不展示代码了,只来展示一下运行效果。

  • 获得生成结果的困惑度

    python playgpt.py --model gpt-3.5-turbo \
           --proxy 'http://localhost:8888' \
           --max-tokens 128 \
           --temperature 0 \
           --prompt '桃花汛是什么?'
    

    得到的结果是:

    PROMPT:
        桃花汛是什么?
    
    RESULT:
        桃花汛是指中国古代文学中的一个典故,也是指桃花开放时期的洪水。根据传说,每年春天桃花盛开时,河水会因为桃花的美丽而上涨,形成洪水。这种洪水被称为桃花汛。桃花汛在文学作品中常常被用来比喻美好的事物或者美
    
    CANDIDATE RESULTS(With PPL):
        TEXT: '桃花汛是指中国古代文学中的一个典故,也是指桃花开放时期的洪水。根据传说,每年春天桃花盛开时,河水会因为桃花的美丽而上涨,形成洪水。这种洪水被称为桃花汛。桃花汛在文学作品中常常被用来比喻美好的事物或者美', PPL: 1.386107427324887
    
    Usage:
        prompt_tokens: 18
        completion_tokens: 128
        total_tokens: 146
    

    temperature 参数不为 0 时,模型输出的结果在未必会在 top_logprobs 结果里输出 —— 这也可以理解,毕竟当 temperature 大于 0 时将会进行概率采样,是有可能选中 topn 之外的 token 的,所以我写的这个示例,只有在 temperature 设置为 0 时才可以稳定获得结果的困惑度,如果设置不为 0 有时候是会没有困惑度结果的。

  • 一次生成多个名字

    PROMPT: 起一个女性名字,姓刘,名字要和月亮有关,但不要直接用月字,尝试根据一些古诗词里的典故,使用较常见而不是冷僻的字,只输出名字无需其他。结果是:刘

    python playgpt.py --model gpt-3.5-turbo \
           --proxy 'http://localhost:8888' \
           --max-tokens 10 \
           --top-logprobs 3 \
           --result-num 10 \
           --temperature 0 \
           --prompt '起一个女性名字,姓刘,名字要和月亮有关,但不要直接用月字,尝试根据一些古诗词里的典故,使用较常见而不是冷僻的字,只输出名字无需其他。结果是:刘'
    

    得到的结果是

    PROMPT:
        起一个女性名字,姓刘,名字要和月亮有关,但不要直接用月字,尝试根据一些古诗词里的典故,使用较常见而不是冷僻的字,只输出名字无需其他。结果是:刘
    
    RESULT:
        婵娟
    
    CANDIDATE RESULTS(With PPL):
        TEXT: '婵娟', PPL: 1.3893958134060524
        TEXT: '嫵娟', PPL: 1.4518073221689531
        TEXT: '娵娟', PPL: 1.753700750011277
        TEXT: '婉娟', PPL: 2.1273314013866393
        TEXT: '嫉娟', PPL: 2.2228908964694374
        TEXT: '娉娟', PPL: 2.6851258929509876
        TEXT: '婷娟', PPL: 3.300551957606001
        TEXT: '嫷娟', PPL: 3.4488123924201473
        TEXT: '婵婟', PPL: 3.5588571357287666
        TEXT: '嫵婟', PPL: 3.718720611038904
    
    Usage:
        prompt_tokens: 87
        completion_tokens: 6
        total_tokens: 93
    

    生成效果先不说,可以看到,Usage 里 prompt_tokens 是 87、completion_tokens 是 6,相比其他方法来生成多个名字能减少一些 token 使用,比如说:

    • 最粗暴的方法是设置 temperature 大于 0 然后反复运行 10 次,假设每次的 completion_tokens 都是 6,那么 10 次需要消耗 930 个 token
    • 更好一点的方法是在 prompt 里要求输出多个名字,假设 prompt 不变,completion_tokens 至少要是 60,那么至少要消耗 153 个 token

我的年度总结所使用到的数据分析和可视化工具

2024年1月18日 08:00

两年前我写完《我的2021》后有些朋友问我用到了什么工具和方法,前阵子写完《我的2023》后又有一些朋友询问,所以就来简单写一下好了。

数据的获取

在两篇年度总结中,我用到了这么一些数据

  • 日记文本
  • 工作日志文本
  • org-mode clock 记录
  • 微信聊天记录
  • B站观看记录

其中日记文本和工作日志文本都没有什么好说的,都是我用 org-mode 手工记下来的,这里只简单展示一下这两者的内容结构

  • 工作日志是一个「年-月-日-具体记录」的四级结构

    work_journal_example.png

  • 日记是一个「日-具体记录」的两级结构

    diary_example.png

org-mode clock 记录也是使用 org-mode 产生的,这个在《我的生活记录经验及个人工具与方法》这篇文章中也有提过,每当我执行 org-clock-in 或者 org-clock-out 后就会自动在对应任务下的 LOGBOOK 下自动地新增记录,如下图所示:

clock_records_example.png

然后是微信聊天记录,我目前使用 EFB 来将微信消息转发到 Telegram,而 Telegram 是有接口能导出历史消息的,其 PC 桌面端也能直接将历史消息导出。其他的微信聊天记录导出方式我没有了解过。

B站的观看记录,我自己写了一个简单的脚本去抓取,见 Gist

用 orgparse 解析 org-mode 文件

我的工作日志和日记都是 org-mode 文件,本质上里面的内容是一个树形的结构,最开始是自己写代码去解析的,后来换成了 orgparse 这个 Python 的 org-mode 解析器。

orgparse 可以直接将 org-mode 文件内容解析成一棵树,这样我在处理工作日志和个人日记时只要去遍历这棵树就好了

import orgparse

content = '''

* 2024-01-17 周三
** 12:05 能做的事情
blablablabla
** 16:01 怠惰
blablablabla
** 17:44 编制
blablablabla
** 22:06 今日回顾
blablablabla
* 2024-01-18 周四
'''

tree = orgparse.loads(content)
for node in tree.children:
    print(node)

# 结果:
# * 2024-01-17 周三
# * 2024-01-18 周四

然后任务下面的 clock 记录它也会解析出来

import orgparse

content = '''
* 休闲
** SOMETIME 看电影
:LOGBOOK:
CLOCK: [2023-12-21 四 18:10]--[2023-12-21 四 21:20] =>  3:10
- 狗神
CLOCK: [2023-10-04 三 14:59]--[2023-10-04 三 17:28] =>  2:29
- 俄罗斯火车大劫案
:END:
'''

tree = orgparse.loads(content)
for level_1_node in tree.children:
    for level_2_node in level_1_node.children:
        for item in level_2_node.clock:
            print(item)

# 结果
# [2023-12-21 Thu 18:10]--[2023-12-21 Thu 21:20]
# [2023-10-04 Wed 14:59]--[2023-10-04 Wed 17:28]

不过有点可惜的是解析出来的 clock 记录把 note 丢掉了,所以我只用来做大类的时间统计,有些依赖里面内容的分析我还是自己做的。

用 LTP 进行分词/词性标注/实体识别等文本分析

要绘制词云就需要从文本里提取关键词,所以分词是必需的。再进一步的关键词提取,虽然也有很多工具,但这些工具基本上都是面向某个领域的,和我自己的日记、日志并不太匹配,所以我就采取了一些简单的策略来做关键词提取,反正也只是用来画一个词云,马马虎虎就行。

具体来说,这个策略是这样的:

  • 先对文本进行分词、词性标注(标记每个词是名词、形容词、动词还是别的什么)、实体识别(人名、地名、机构名等)
  • 反向过滤:若词中包含标点符号则去除,若词在停用词表中则去除,若词的词性为我设定的类别(连词、助词、叹词、量词、数词、介词、非语素词、时间名词)则去除(LTP 使用的词性标注集是 2005 年颁布的《信息处理用现代汉语词类标记规范》)、若词中字数太少(1个字的往往无意义)或太多(字数太多可能分词出错了)则去除
  • 正向筛选:识别为实体的词一律选中作为关键词,我预先设置的一些重要词汇一律都选中作为关键词

能进行分词、词性标注、实体识别的工具很多,21 年的时候我用的是 jieba ,它的好处是依赖干净而且比较快,但效果要差一些而且没有实体识别功能。除了 jieba 还可以用北大的 pkuseg、哈工大的 LTP、百度的 LAC,当然国外的 spaCy 和斯坦福大学的 Stanza 也有中文支持都是不错的,我个人建议 pkuseg 或者 LTP。2023 年的年度分析我最后选了 LTP,是因为 LTP 在持续地更新,4.0 后已经升级成了 pytorch 模型想尝试下。

LTP 的模型建议像我一样自己下载好放到本地,否则它会去从已经被大陆屏蔽的 Huggingface 上下载而失败出错。

from ltp import LTP

ltp = LTP('/home/zmonster/Projects/ltp/small/')
output = ltp.pipeline(['思考晚上吃什么最后决定煮面吃,然后在整理日记的时候看到自己有一天空气炸锅了头一天 KFC 疯狂星期四买的鸡米花,想起来了之前自己还买过小酥肉放冰箱了,翻出来准备空气炸锅热了晚上吃一点。'], tasks=['cws', 'pos', 'ner'])

print(output.cws)
# [['思考', '晚上', '吃', '什么', '最后', '决定', '煮', '面', '吃', ',', '然后', '在', '整理', '日记', '的', '时候', '看到', '自己', '有', '一', '天', '空气', '炸锅', '了', '头', '一', '天 ', 'KFC ', '疯狂', '星期四', '买', '的', '鸡米花', ',', '想', '起来', '了', '之前', '自己', '还', '买', '过', '小', '酥肉', '放', '冰箱', '了', ',', '翻', '出来', '准备', '空气', '炸锅', '热', '了', '晚上', '吃', '一点', '。']]

print(output.pos)
# [['v', 'nt', 'v', 'r', 'nd', 'v', 'v', 'n', 'v', 'wp', 'c', 'p', 'v', 'n', 'u', 'n', 'v', 'r', 'v', 'm', 'q', 'n', 'v', 'u', 'm', 'm', 'q', 'nz', 'a', 'nt', 'v', 'u', 'n', 'wp', 'v', 'v', 'u', 'nd', 'r', 'd', 'v', 'u', 'a', 'n', 'v', 'n', 'u', 'wp', 'v', 'v', 'v', 'n', 'v', 'v', 'u', 'nt', 'v', 'm', 'wp']]

print(output.ner)
# [[]]

用 ImageMagick 进行简单的图像处理

我用 ImageMagick 来生成词云图需要的 mask 图使得画出来的词云能按照我预想的形状展示:

  • 用 ImageMagick 绘制一张黑底白字写着 2023 的图

    convert -background black -fill white -pointsize 512 label:2023 2023.png
    

    2023.png

  • 用 ImageMagick 绘制一个直径 400 像素的圆

    convert -size 400x400 xc:white -fill black -draw 'circle 200,200 200,3' circle400.png
    

    circle400.png

然后我还很常用 ImageMagick 来裁剪一下图像的白边使得图像显得更紧凑一些,随后讲到的绘制热力图的 july 产生的结果就会有特别大的一块白边,我希望它能少一些但它又没有提供什么选项来让我控制(matplotlib 的 tight_layout 完全没作用)所以只能用 ImageMagick 再处理一下了

convert -trim 2023_diary_heatmap.png  /tmp/a.png
mv /tmp/a.png 2023_diary_heatmap.png

裁剪前是这个样子的

2023_diary_heatmap-original.png

裁剪后是这个样子

2023_diary_heatmap.png

用 july 绘制日志/日记热力图

july 是一个基于 matplotlib 的用来绘制每日事件热力图的工具, 如下所示

import numpy as np
import matplotlib.pyplot as plt
import july
from july.utils import date_range

dates = date_range("2020-01-01", "2020-12-31")
data = np.random.randint(0, 14, len(dates))
july.heatmap(dates, data, title='Github Activity', cmap="github")

会得到这样的图:

july_heatmap_github.jpg

具体使用看项目 README 就好了,没有什么太特别的。不过如果想和我一样让显示的横坐标和纵坐标都是中文的话,需要在调用 july 前设置一下 locale

locale.setlocale(locale.LC_ALL, "zh_CN.UTF8")

另外就是在调用 july 的时候设置一下字体,这个在 README 的示例中有。

最近才看到 yihong 的 GithubPoster 感觉非常不错,之后也许就用 yihong 的这个工具了。

用 wordcloud 绘制词云

wordcloud 是基于 matplotlib 实现的词云绘制工具,它自己也提供了命令行工具直接从文本中读取数据然后绘制,但它里面自带的是按空白符进行分词的做法,也就是说不适用于中文文本。

好在它的 WordCloud 类实现了一个叫作 generate_from_frequencies 的方法,所以我是先在之前用 LTP 分词自己统计好频次后使用这个方法来绘制,像这样:

from wordcloud import WordCloud

freqs = {
    '朋友': 432,
    '吃饭': 368,
    '游戏': 127,
    '聊天': 106,
    '晚饭': 83,
    '整理': 76,
    '午饭': 75,
    '早饭': 74,
    '洗漱': 60,
    '父母': 55
}
wc = WordCloud(background_color='white', width=200, height=200)
wc.generate_from_frequencies(freqs)
wc.to_file('wordcloud.png')

不过它默认的实现不支持中文绘制,上面的代码会得到这样的图

wordcloud_example_without_cnfont.png

通过 font_path 参数设置一下中文字体路径即可,我使用了文泉驿微米黑这个字体:

wc = WordCloud(
    background_color='white',
    width=200,
    height=200,
    font_path="/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
)

这样就能得到

wordcloud_example_with_cnfont.png

如果想要将词云绘制成特定的形状,就要使用前面用 ImageMagick 生成的 mask 图像了,通过 mask 参数传入对应的图像即可,最终会在 mask 图像的黑色区域绘制,比如使用前面那个圆形的 mask 图像:

from wordcloud import WordCloud
from PIL import Image
import numpy as np

mask_image = np.array(Image.open('circle400.png'))
freqs = {
    '朋友': 432,
    '吃饭': 368,
    '游戏': 127,
    '聊天': 106,
    '晚饭': 83,
    '整理': 76,
    '午饭': 75,
    '早饭': 74,
    '洗漱': 60,
    '父母': 55
}
wc = WordCloud(
    background_color='white',
    mask=mask_image,
    width=len(mask_image[0]),
    height=len(mask_image),
    font_path="/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
)
wc.generate_from_frequencies(freqs)
wc.to_file('wordcloud.png')

结果如下图所示,虽然因为词比较少看着不太圆,但大体形状上能看出来和前面那张图的差别。

wordcloud_example_circle.png

用 matplotlib_venn_wordcloud 绘制韦恩图形式的词云

韦恩图(Venn Diagram)是用来展示两个集合之间关系(是否有交集、交集部分有多少等)的一个图形,如下图所示:

venn_example.png

写《我的2021》分析聊天记录的时候,我想要分析我和一个特定的人交流的时候我们双方说的内容之间的同异同时又希望能把交流中高频的词突出显示,所以就想要在一个韦恩图形式的词云,具体来说是下面这个样子:

venn_wordcloud_example1.png

可以看到,总体上它还是一个词云图,但又引入了韦恩图的概念,左边的圆是我说过的话里的关键词、右边的图则是朋友说过的话里的关键词,两个圆的交集则是我们双方都说过的词。

这个是一个比较小众的需求,但最后还真找到了一个实现,也就是 matplotlib_venn_wordcloud 这个工具,其使用方法如下所示:

import matplotlib.pyplot as plt
from matplotlib_venn_wordcloud import venn2_wordcloud

plt.rcParams["font.sans-serif"] = ["WenQuanYi Micro Hei"]

words = {
    '我': {'知识', '工具', '笔记', '建立', '体系', '管理', '方式', '想法', '价值', '优秀', '理解'},
    '朋友': {'笔记', '知识', '体系', '内容', '教育', '嵌入', '引用', '功能', '链接', '关系'},
}
freqs = {
    '知识': 69,
    '笔记': 68,
    '内容': 65,
    '体系': 63,
    '建立': 61,
    '工具': 61,
    '教育': 59,
    '方式': 57,
    '想法': 57,
    '理解': 55,
    '链接': 44,
    '嵌入': 44,
    '功能': 43,
    '价值': 32,
    '引用': 27,
    '管理': 25,
    '关系': 23,
    '优秀': 19,
}
names, sets = list(zip(*words.items()))
venn2_wordcloud(
    [set(item) for item in sets],
    alpha=0.8,
    set_labels=names,
    word_to_frequency=freqs,
    wordcloud_kwargs={
        'font_path': '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc',
    }
)
plt.savefig('venn_wordcloud.png')

由于这个工具对中文支持也不是很好,所以需要像我上面的代码一样,分别在两处设置一下中文字体。

上面的代码会得到如下的图像:

venn_wordcloud_example2.png

用 matplotlib.pyplot.subplot_mosaic 进行布局设置

除此以外的大部分图像如柱状图、线图、饼图等我都是用 pandas 和 matplotlib 来绘制的,这一块的公开资料很多,所以这里只提一下要在一张图里画多个图表时设置布局的心得,具体来说是用 matplotlib.pyplot.subplot_mosaic 这个函数,它可以用比较直观地方式进行绘图的布局设计 —— 图像分为几行几列、某一个子图占据哪几行哪几列之类的,比如说:

  • 绘制 A/B/C/D 四个子图,A/B/C 一起在第一行并且各占 1/3 的宽度,子图 D 占据第二行整行

    import matplotlib.pyplot as plt
    plt.subplot_mosaic('ABC;DDD')
    

    上面的代码会得到下面的布局:

    subplot_mosaic_example1.png

  • 绘制 A/B/C 三个子图,A/B 一起在第一列并且各占 1/2 的高度,C 占据第二列整列

    import matplotlib.pyplot as plt
    plt.subplot_mosaic('AC;BC')
    

    上面的代码会得到下面的布局:

    subplot_mosaic_example2.png

  • 绘制一个 3x3 的布局,但只在中心和 4 个角进行绘图

    import matplotlib.pyplot as plt
    plt.subplot_mosaic([
        ['A', '.', 'B'],
        ['.', 'C', '.'],
        ['D', '.', 'E'],
    ])
    

    上面的代码会得到下面的布局:

    subplot_mosaic_example3.png

我的2023

2024年1月12日 08:00

2023 年继续贯彻我尽量记录一切的理念,统计了下 org-mode 的 clock 数据,这一年有记录的时间共计 7924.55 个小时,合计 330.19 天,占比 90.46%,这有记录的 7924.55 个小时的情况如下图所示:

2023_time_usage.png

  • 睡觉共记录 2802.87 小时,平均每天是 6.31 小时,虽然 6 月起就没有在工作了,但睡眠还是不好,还是顶着黑眼圈,2024 年希望能多睡一点好觉;
  • 「休闲-其他」这个条目记录的是无明确目的的、消磨时间的活动,包括醒后的赖床、睡前的玩手机以及其他任何两段有明确目的的活动之间的空隙(一般都是在玩手机、刷推等),算下来平均每天有 4.12 个小时,也就是说我每天都要浪费六分之一的生命 —— 不过我对浪费生命没有什么愧疚感,作为一个并不想做什么大事业的人,生命就是拿来浪费的嘛,不过这些记录中的活动其实包含了很多种类的活动,还是可以作细化的,之后尝试一下吧;
  • 排名第三的是游戏,这一年我确实玩了很多很多的游戏,可以说比往年任何时候都多,能有大量的时间去纯粹的体验游戏的乐趣,而不是在下班后拖着疲惫的身体带着「我还有一两个小时的自由时间要好好玩下游戏不然今天就浪费了」或者「今天工作不开心想去玩下游戏放松下」(真正的放松是早点休息啦)等复杂的心情去打开游戏——只有在不玩游戏时也是开心平静的时候,才能真正体会到游戏的乐趣,我是这么想的;
  • 工作没什么好说,因为只工作了 5 个月所以就只有 449 个小时,如果是往年的话工作大概会是在第一或者第二的位置
  • 日常生活的一些环节如吃饭(343 小时)、洗漱(318 小时)、做饭(180 小时)、上厕所(113 小时)、家务(63 小时)加起来就 954 小时了,如果不是特别追求高效率的人,这部分时间和睡眠一样是很难减少的,所以其实人一天里能利用的有效时间其实并不多,能在每一天的这些时间里充分地做一些自己喜欢的事情,这样的人生就会是幸福的吧。

了解了 2023 年这个时间使用结构后,希望能在 2024 年尝试有意识地对其进行一点优化调整,比如多睡一点、多读点书(2023 年读的书真是太少了)、多运动,一年后再看吧!

2023_diary_heatmap.png

2023 年,我也依然在坚持写日记,365 天里我有 358 天写了日记,只有 2023/01/26、2023/01/30、2023/02/25、2023/03/19、2023/03/22、2023/03/27、2023-04-06 这七天没有记录,辞职后的 7 个月里没有一天落下,不过有些天比较糊弄(比如 2023/11/08 和 2023/11/10)所以上面的热力图里看着好像没记一样。

我在 Emacs 里用 org-mode 写日记是从 2019/06/17 开始的,四年多的时间里日记的结构发生了数次变化,这次做分析统计的时候费了点时间多写了点代码把过去几年的日记结构都兼容了下,想看下几年来的日记变化,得到的对比图如下所示:

2023_diary_stats.png

  • 从右上的图可以看到,2023 年的日记天数、记录数、日均字数基本上都是比之前三年高的,唯一的例外是记录数比 2021 年的低,这是因为 2023 年写日记时基本上都是在一个记录下回顾一整天的经历和想法,而 2021 年的时候则倾向于一旦有什么想法就新建一个记录写一下;
  • 看左上的逐季度的日均字数趋势图,可以发现前两个季度的日记量和往年比虽然也多一些但也不算多很多,但在下半年则急剧增长,最大的原因就是我 5 月底从公司辞职躺平了,有了更多的闲暇时间所以我就能花更多的精力去记录和梳理自己的想法、感受,不像有些人,工作对我并不是必需的,即使是一个不太出门的人,我依然能从很多事物中得到值得记录的体验;
  • 从下方每年相同月份的日记日均字数对比来看,9 月和 10 月往往都是我记录最多的,大概是因为北京的秋天吧 —— 嗯,这个是纯粹的主观感觉,限于精力就没有去做这方面的内容分析了。

进一步将 2023 年日记的文本做了下分析,绘制了一张词云图,如下所示:

2023_diary_words.png

为了有更好的可视化效果,我在分析时做了这么一些处理:

  • 将提到的现实中认识的人的名字都替换成了「朋友」这个词
  • 将提到的游戏名都替换成了「游戏」这个词
  • 将提到的具体的书籍都替换成了「书籍」这个词

总的来说,2023 年就是玩游戏、阅读、和朋友聊天以及吃吃吃的一年,是开心的一年!我觉得这样的日子我可以一直过下去。

然后对比了一下 2020 年到 2023 年这四年日记里 TOP5 的词汇的变化情况,如下图所示:

2023_year_words_comparison.png

可以看出来,和往年相比最大的一个变化就是「游戏」这个词的出现,而始终如一的则是「吃饭」和「朋友」。顺便一提,2022 年过得并不开心,日记里和「开心」这个词相关的文本基本上都是「不太开心」「开心不起来」之类的,而这样的文本在经过处理后提取出来的有效词汇还是「开心」 —— 由于 2022 年的主观感受就很强烈所以就没有特地去为这个情况做分析优化了,只是在这里简单说明一下。底部的折线图是把这四年的 TOP5 的词语取并集得到了 11 个词然后看在这四年里逐季度的频次变化趋势,加粗的那 5 条是 2023 年 TOP5 词汇对应的折线。

然后还将 2023 年每个月的日记分别拿出来绘制了下词云图,看一下每个月会有什么不同,为了更好地将差异反映出来,我将「朋友」「吃饭」「聊天」「早饭」「晚饭」「午饭」「洗漱」这几个在每个月都非常高频的词去掉了,得到的结果如下图所示:

2023_monthly_words.png

  • 可以看到,前四个月的词云里基本上没有出现「游戏」这个词,因为当时还在上班,而在4月底提出离职后,5月就开始迫不及待地玩游戏了,开头记录的 473 个小时的游戏时间,也基本上分布在后面这 8 个月里,9月玩得稍微少一些只有 27 个小时所以这个词在词云图里就显得小了一些;
  • 7月、8月和9月基本上每天都会看一小会书,日记里都会提一下,所以「书籍」这个词也比较大一些,但我每天也就看半个小时到一个小时,其实并不多,年底的两个月甚至都不怎么看了,一部分原因是把更多的精力放到读论文上了。

除了前 4 个月还在烦恼工作外,之后的 8 个月能看出来和一整年的词云图是一致的,游戏、书籍、散步、冥想、吃饭、做饭、聊天构成了我生活的主要部分,基本也是开心的,不工作就使人开心,至少对我是这样的,无聊什么的完全不存在的!

经过新冠三年,我觉得我更能关心和体谅我的朋友们了,2023 年也有更多地去和朋友联系,也从更多的朋友那感受到过关心、支持和肯定,虽然大部分朋友只是隔很久才闲聊几句或者吃个饭,但我对此都感到很开心。大家都有自己的人生道路,谁能说得清上一次见面是不是这辈子最后一面呢?所以我尤其开心能和许久没有联系上的朋友因为某些小事情而重新闲谈上一两句。多发一些朋友圈、多写一些文章,也是希望能和朋友保持一点联系,大家都不怎么说话,我就多说两句(但不要说些让人讨厌的话),一直说下去,总会有那么一两句触动到远方的故人,当他们给我点个赞写个评论,我就知道他们都还好,这就挺好的。

2023_friendes.png

从日记来看,我在 2023 年无论是提及的人数还是提及朋友的次数都比往年高很多,这其中很大一部分原因当然也是因为不工作后有更多的闲暇和精力了,所以说回来,工作除了赚钱到底有什么好处呢?

最后我还绘制了一个不同的人在日记中被提及次数的分布,为了保护隐私就把人名都隐藏了,总体上就是图上这样一个非常长尾的分布,大部分人我在日记中就提及了几次,只有少部分人因为在现实生活中联系比较紧密而隔几天会提及一次,不过这些提及次数并不反映主观态度,只要能多和我玩我就会在日记中提及。

除了游戏、朋友外,2023 年甚至之前三年很大的一个主题都是吃饭,所以也去统计了一下三餐的数据,如下图所示:

2023_meals.png

总体上,我有记录的吃饭占比达到了 83.38%,由于数据记录的不完善,剩下 16.62% 的未记录数据其实也有部分是吃了忘记记录而不是没有吃的,我对这个数据还是比较满意的,我目前胃口还很好、饮食也还比较规律。

有记录的数据中,我自己做了 384 次饭,但其中大约有三分之一都是早饭随手煮个面之类的,由于我喜欢在午饭时做上两个菜然后晚上吃剩饭剩菜所以剩下的三分之二里大概还有一半是晚饭在吃剩的,也就是说我正经地做菜大概也就 120 多次的样子,从每月的类别分布情况来看,自己做饭也更多是在辞职之后。除了自己做饭外,外卖和外食也各有 100 多次,一个原因还是我厨艺不够做饭很慢,有时候还是会不太想做饭然后去点外卖吃,外食主要倒不是因为这个,毕竟对我来说出门是有阻力的(特别是冬天),出去吃一般都是在早上,主要的目的还不是吃饭,而是为了去一个干净敞亮的地方坐一坐沾沾人气、晒晒太阳之类的 —— 我属于那种能一个人待着但偶尔还是要到人群里走一走(但同时又希望人群不要理我)的人吧。然后和朋友聚餐 79 次,差不多是一周一次的频率。

以上就是我 2023 年的基本情况了。限于精力和数据情况,还有很多分析想做而没有做,比如说:

  • 想分析日记里提到的地点,绘制地图热力图、轨迹图,看看我主要都去了哪些地方,但是地点的提取比较难,效果并不太好,而且我也一直在北京没有去哪里;
  • 想在词云绘制的基础上,做一个事件云的绘制以便更清晰地看我这一年都做了什么,但是事件的提取就更加困难了,所以只是想想,都完全没有去尝试;
  • 想把我聚餐、外食的店名拿出来,看一下这一年我在外面吃饭的馆子的菜系分布,感觉也蛮有意思的,但是吃饭记录我做的很不好,虽然也能手工整理但有点嫌麻烦;
  • 想把我做饭用到的食材分析出来看看什么用的多什么用的少,不过也是记录的不好所以就算了,大概是辣椒、五花肉、豆腐、鸡蛋之类的用的多吧;
  • 想把一年的聊天记录提取出来看看我和朋友们都聊了什么,这个我在 2021 年做过了,但是这个工作很繁琐,而且最后几个月我的 EFB 开始频繁出问题可能丢失掉不少聊天记录,现在我还在烦恼 EFB 的升级问题,我部署的 EFB 版本太老了都不知道数据会不会和最新版不兼容了所以也不敢随便升级;
  • 还有 2023 年的财务数据,因为市场不好一直在亏钱而且没工作了一直在花钱,所以这一年都没太多动力去记账,所以也算了吧;
  • 另外我还看了很多论文,但目前论文阅读的流程还没有和我的记录系统整合得足够好,所以也不怎么好做分析,这个看看之后能不能优化一下吧;
  • 往年会做书影音以及游戏的回顾,但 2023 年玩游戏太多了真要在这里写大概内容会很多,考虑下一次年度总结专门为游戏写一篇,书影音这则是这一年看得都少就不做了。

总之就这样吧,针对这次回顾发现的问题继续优化我的记录系统,一年后再来看看有什么新的变化。

ZMonster's AI Notes(Alpha) #1

2023年12月19日 08:00

本系列内容模式的最终形态尚不确定,可能会根据个人精力、兴趣及阅读反馈做调整。

术语

简单介绍下我上一周中新了解到的 AI 领域的一些术语,但不做深入探究。

  • 模型编辑(model editing)

    model_editing_1.png

    指对已有的神经网络模型进行局部修改,使得模型能在单个或者少量数据上的预测行为发生变化但又不影响这些数据之外其他数据及任务的表现,以应对少量 badcase 修复、知识随时间更新等场景。

    model_editing_2.png

    模型编辑可以最粗暴地通过对预训练模型进行直接微调来做到,但可能因为数据量太少而效果不佳,除此以外还有一些方法会尝试定位到具体的一些神经元做局部参数修改。广义上来说,通过外部知识进行增强(比如将正确的或者新知识后填充到 Prompt 里)、在主模型外附加针对性的额外参数(这些额外参数可以比较少然后用对应的少量数据训练)也算是模型编辑。

    模型编辑和知识编辑(knowledge editing)在概念上有较大的重叠。

  • 对齐税(alignment tax)

    又称对齐成本(alignment cost)、安全税(safety tax),指对 AI 系统(目前来说,主要是大语言模型)进行对齐的过程中产生的额外成本,大致将其分成三类,分别是:由于对齐而导致的性能/效果的退化、为了对齐而增加的额外开发/计算资源、为了对齐而增加的额外时间。

  • 校准(calibration)

    指模型的预测概率与实际概率一致的程度,如果两者完全一致就可以说模型是「被校准的」。从实用角度来说,一个被校准的模型,其预测结果的置信度会是真正可信的 —— 做过机器学习业务的人应该都有这个感觉,很多时候模型输出的置信度并不能真的帮助我们判断结果是否可靠,比如说一个错误的预测其置信度也高达 80% 甚至 90% 之类的。一些研究表明,一些情况下预训练模型随着模型尺寸增大其校准程度会降低、预测概率的分布会高度集中在窄小的区间(虽然与此同时其准确率也在提高),对模型进行微调也有类似的现象。关于校准的研究致力于评估模型的校准程度以及研究提高模型校准程度的方法。期望校准误差(Expected Calibration Error, ECE)是一个较通用的评估模型偏离期望校准程度的指标。

论文

简单讲一下我上一周中重点读过的一些论文及我个人的相关看法。

由于 EMNLP2023 会议上有一篇关于 In-Context Learning 的论文获得了最佳论文,所以上一周重点读了下 In-Context Learning 相关的论文。In-Context Learning 是指使用大语言模型(不过现在在多模态模型中也看到了使用 In-Context Learning)时,在输入中添加少量的标注过的任务数据来(简单起见,后面我一律称之为「示例」)提高实际预测效果的方法,目前为止对 In-Context Learning 的分析发现了很多问题,比如模型对这些示例的选择甚至排序都很敏感,对 In-Context Learning 的工作原理也还不是特别清楚。

Label Words are Anchors: An Information Flow Perspective for Understanding In-Context Learning

EMNLP2023 的最佳论文之一。

通过逐层计算的基于 attention 权重矩阵的信息流动显著性分数考察了使用 In-Context Learning 时的三个信息流 —— 从示例中文本到示例中标签词的信息流(wp)、从示例中标签词到预测目标位置的信息流(pq)、输入中任意两个词的信息流(ww),发现第一个信息流的显著性在浅层网络中很高并且随层数增加而迅速衰减,第二个信息流则在浅层时不显著但随着在更深层中迅速变得显著,如下图所示:

information_flow_categories.png

根据上述观察,作者对 In-Context Learning 的工作机制提出了两个假设:

  1. 在浅层中,标签词从示例样本中聚合信息,形成后续计算的语义表示
  2. 在深层中,模型通过提取标签词的信息(就是假设 1 中的语义表示)进行预测

这两个假设的可视化地描述的话是下图这个样子:

information_flow.png

对第一个假设的验证是通过对不同层不同位置的信息流阻断(通过将对应位置的 attention 置为 0)后观察模型效果变化来完成的,如下图所示:

shallow_layers_hypothesis.png

可以看到

  • 同样在前 5 层进行阻断时,阻断流向标签词的信息流带来的模型效果损失远远比阻断流向非标签词的大;
  • 同样阻断流向标签词的信息流时,在前 5 层进行阻断带来的模型效果损失远远比后五层大,事实上,在后五层对流向标签词的信息流进行阻断时,模型的效果损失非常小

对第二个假设是通过直接考察输出位置对标签词处的 attention 权重、将权重最大的标签词作为预测结果,然后与正确的结果进行对比,计算 AUC-ROC 分数考察两者之间的相关性,如下图所示,可以看到这个相关性在浅层时很不明显但在深层时就很明显了。

deep_layers_hypothesis.png

基于这篇论文的假设,作者进而提出了三个对该现象的利用方法

  1. Anchor Re-weighting: 我理解就是把之前直接用目标位置对标签词的 attention 值来预测类别的方法,形式化后发现和逻辑回归很像,然后逻辑回归会有一个偏置项嘛,所以作者也在这个形式上加了一个偏执项来和逻辑回归对齐(是一种假设模型本身存在 bias 的意思?),然后根据这个形式化假设去学习到这个偏执项里的参数,最后用来调整目标位置在每个标签词上的 attention 值。
  2. Anchor-Only Context Compression: 大概意思是,预先把示例的 hidden state 算好,然后有真实的任务输入进来的时候,把每一层标签词位置的 hidden state 拿出来拼接起来放到(处理真实任务输入的这个)模型的每一层最前面,这样实际预测的时候就示例中的所有文本就不再需要参与计算了,推理过程就能加速了。
  3. Anchor Distances for Error Diagnosis: 通过对比示例中标签词的 attention 相关的表示(论文里有一套稍微复杂点的计算过程,暂时没去细看)然后计算不同标签词表示之间的相似度,理想情况下不同标签词的相似度应该较低,当预测结果有错误时可能就有不同的标签词因为 In-Context Learning 而产生了相似的表示,这样通过分析不同标签词之间的混淆情况就能把错误的原因找出来,或许也能去针对性地做调整 —— 比如说把有混淆的那个标签词的文本换成别的?

A Study on the Calibration of In-context Learning

挺有意思的,考察了 LLM 使用 In-Context Learning 时的校准性

  • 以 LLaMA 及其微调版本(Vicuna、Alpaca、LLaMA2-Chat)过的模型为研究对象发现微调过后准确度越好的校准性越差
  • 使用 4 个示例时,随着模型尺寸的增大,模型的准确度在变高,但同时校准性在变差,模型对自己的预测结果越发自信(对正确的预测和错误的预测都是如此)
  • 从不使用示例到只是使用 1 个示例,模型的表现会发生剧烈的变化,当只是增加使用的示例时这种变化就相对平滑了,总体上而言,随着示例数量的增加,模型的准确度会提升,同时也对预测结果越发自信
  • 已知常用的通用校准方法(温度缩放/标签平滑)在 In-Context Learning 中效果有限

Comparable Demonstrations are Important in In-Context Learning: A Novel Perspective on Demonstration Selection

这篇论文指出在 ICL 里 context 长度有限的时候示例有限很容易表现出 demenstrations bias,然后提出了 comparable demenstrations 这个概念。

文中所谓 demenstration bias 是说因为示例数量太少,导致他们可以被划分到不唯一的多个任务空间里去,进而影响结果,比如说下面这样两个用于情感分类的示例,不看标签的话两个示例的对比差异不够明显,可以用在情感分类里,也可以用到电影类型分类任务里:

Obviously, Titanic is a well-made romantic film. -> positive
Damn, it's a waste of time watching this cartoon! -> negative

在这个想法的基础上,认为 In-Context Learning 里的示例之间的差异应该尽量地凸显在当前这个任务空间下的差异,比如说对给定的示例通过修改其中的少数字词使其标签翻转(比如对情感分析来说,从 positive 变成 negative)得到的新示例和原来的就能构成所谓的可比较示例(comparable demenstrations)。

通过给定随机选择的示例、相近的示例、可比较示例然后让 gpt-3.5-turbo 生成 instruction 看 instruction 的正确与否及程度,确认了 demonstration bias 的存在。不过论文自己没有提出具体的可比较示例构建或者搜索方法,直接用了另外一篇相关论文里的数据。

comparable_demenstrations.png

GPT-RE: In-context Learning for Relation Extraction using Large Language Models

这篇论文将 In-Context Learning 应用于关系抽取里,对 In-Context Leanring 本身的研究并没有什么特别的贡献,不过里面有一个有价值的小点,那就是它使用示例时是从标注数据中检索的 —— 之前有工作证明使用与实际任务输入语义相似的示例会提高效果 —— 然后发现在关系抽取任务里用句子 embedding 去检索选择示例带来的收益比想的要小,原因是句子整体语义接近的示例,其中未必会包含和任务实际输入相关的实体、关系,然后通过下面两个方法来改善了结果:

  • 一个是在索引示例数据以及检索时,都改写文本,使其更加着重表达实体以及关系,比如说把「He has a sister Lisa.」改写成「The relation between ‘He’ and ‘Lisa’ in the context: He has a sister Lisa.」
  • 另外一个是直接用训练好的关系抽取模型(比如 BERT 之类的)来获得句子中关系的表示,比如说用 BERT 里两个实体词对应 hidden state 的拼接

不管是大模型还是 In-Context Learning 还是 Chain-of-Thoughts 之类的看起来很先进的技术,用到具体业务的时候都可以思考一下是不是可以把其中的一些细节根据业务进行适当的调整。

其他一些看过了觉得没什么收获的论文

  • Active Learning Principles for In-Context Learning with Large Language Models

    相关工作部分引用得很全面,我从里面找了一些 In-Context Learning 的论文补充到 Zotero 里。

    不过除了引用完善外就没啥新意了,所谓主动学习其实就是通过一些自动化或者半自动化手法(往往让现有模型参与其中)采样高质量数据来降低标注成本然后进一步改进模型如此迭代的做法,在这篇论文里其实讲的就是几种不同的示例采样方法的对比,甚至没有去训练模型,强行和主动学习挂钩,自己也没有提出任何一个新的示例采样方法。

    做了几个对比实验得到一些结论

    • 按照相似性方法采样得到的效果最好,通过对比(选择最不相似的示例)从反面也论证了这一点,这个反面论证挺有意思
    • 在分类任务中,按多样性采样(聚类后从不同簇中选一些示例)的效果仅次于按相似性
    • 示例的真实标签是很重要的,和《Rethinking the Role of Demonstrations: What Makes In-Context Learning Work?》这篇论文中的观察不一致
    • 在用不确定性(熵、困惑度)采样时,更小模型(gpt2/gpt2-medium/gpt2-large)似乎需要熵更小的示例效果才能更好,但更大的模型(gpt-neox)似乎开始从高熵的示例中获益

    除了第 4 点,其他都有更好的论文提出过了 —— 即使是第 4 点我怀疑也有别的论文提过了。

  • OverPrompt: Enhancing ChatGPT through Efficient In-Context Learning

    就是把分类任务的多个输入放到一个 prompt 里去让直接一次性产生多个输出,以减少计算消耗,同时说还观察到一点点效果的改善,没啥意思。

  • In-Context Learning for Text Classification with Many Labels

    扫了眼感觉没有带来什么新的认知,所谓的标签数量多也没有触及什么真正的困难,没啥意思。

  • In-Context Learning Dynamics with Random Binary Sequences

    搞什么二进制序列预测,通过这个小众任务来将 In-Context Learning 当作贝叶斯模型选择、program induction(我不知道该咋翻译,感觉是认知科学、心理学方面的概念)来去分析,看作者也都是什么心理学、脑科学之类的背景,看着很费劲和我们常关注的点很不一样,所以就没有去看了。

  • In-Context Learning Functions with Varying Number of Minima

    用在数学上的,用来逼近一个有多个极小值的函数之类的,很小众的场景,没什么兴趣。

    代码: https://github.com/PittNAIL/icl-minima

实践

讲一下我上一周中进行的 AI 方面的实践,可能包括:AI 相关产品或工具的使用,AI 方向的开发实践。

Google Gemini API

12 月 13 日 Google 向开发者开放了 Gemini API,只要有 Google 账户且 QPM 在 60 及以下就可以免费使用,目前能用的模型是 Gemini Pro —— Google 宣传视频里的 Gemini Ultra 目前在 API 里还不支持。前往这里可以查看更多的介绍以及生成 API Key,API 文档的说明则在这里。Gemini API 开放后,除了 RESTful API 外还有支持了 Python/Go/Node.js/Swift/Kotlin/Java 六种语言的 SDK,我就看下 Python 的了。

目前的 API 支持以下功能

  • 根据文本输入生成文本输出
  • 根据文本和图片的多模态输入生成文本输出
  • 多轮对话
  • 获取文本的 embedding

Python SDK 通过 pip install google-generativeai 安装即可,需要 Python3.9 及更高版本的 Python。

开始使用前需要设置好 API Key 并实例化一个 GenerativeModel 对象:

import google.generativeai as genai

api_key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-U'
genai.configure(api_key=api_key)
safety_settings = [
    {'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'threshold': 'BLOCK_NONE'},
    {'category': 'HARM_CATEGORY_HATE_SPEECH', 'threshold': 'BLOCK_NONE'},
    {'category': 'HARM_CATEGORY_HARASSMENT', 'threshold': 'BLOCK_NONE'},
    {'category': 'HARM_CATEGORY_DANGEROUS_CONTENT', 'threshold': 'BLOCK_NONE'}
]
generation_config = {
    'candidate_count': 1,
    'stop_sequences': None,
    'max_output_tokens': None,
    'temperature': 0.7,
    'top_p': None,
    'top_k': None,
}
model = genai.GenerativeModel(
    model_name='gemini-pro',
    safety_settings=safety_settings,
    generation_config=generation_config,
)

safety_settings 用来设置安全策略,只有 4 个能设置,我建议按照我上面的示例将 4 个能设置的全部设置为 BLOCK_NONE 也就是不过滤,否则一旦触发默认的安全策略会直接把生成结果都干掉 —— 对直接不给结果,而不是输出一个过份人畜无害的文本,除了 4 个能设置的外还有好多不同的安全类别是不允许用户设置的一旦触发就会无结果报错,在 API 层面这么做还蛮讨厌的;generate_config 用来设置生成时的具体行为,一般情况下设置下 max_output_tokens 和 temperature 就好了,别的不太建议去调整,candidate_count 我试着调整成 3 直接报错说「Only one candidate can be specified」,没有太仔细去研究什么情况下才能一次性产生多个输出。

  • 调用 generate_content 根据文本输入生成文本输出

    response = model.generate_content(content='骂一个人')
    print(response.text)
    

    输出

    1. 你真是一个自以为是、目中无人的家伙!
    2. 你真是一个笨蛋,连这么简单的事情都做不好!
    3. 你真是一个废物,一点用处都没有!
    4. 你真是一个胆小鬼,一点风浪就受不了!
    5. 你真是一个骗子,满嘴谎言,没有一句真话!
    6. 你真是一个小人,背后捅刀子,令人不齿!
    7. 你真是一个无赖,蛮不讲理,胡搅蛮缠!
    8. 你真是一个混蛋,人面兽心,丧尽天良!
    9. 你真是一个畜生,禽兽不如,毫无人性!
    10. 你真是一个魔鬼,十恶不赦,罪大恶极
    

    如果前面没有设置 safety_settings,上面的代码会报错

    ValueError: The `response.parts` quick accessor only works for a single candidate, but none were returned. Check the `response.prompt_feedback` to see if the prompt was blocked.
    

    根据错误提示查看 response.prompt_feedback 会得到这样的结果,可以看到 HARM_CATEGORY_HARASSMENT 这个类别被判断达到 MEDIUM 级别所以就被过滤了。

    safety_ratings {
      category: HARM_CATEGORY_SEXUALLY_EXPLICIT
      probability: NEGLIGIBLE
    }
    safety_ratings {
      category: HARM_CATEGORY_HATE_SPEECH
      probability: NEGLIGIBLE
    }
    safety_ratings {
      category: HARM_CATEGORY_HARASSMENT
      probability: MEDIUM
    }
    safety_ratings {
      category: HARM_CATEGORY_DANGEROUS_CONTENT
      probability: NEGLIGIBLE
    }
    

    也可以在实例化 GenerativeModel 时不设置 safety_settings 而是在调用 generate_content 时设置

    response = model.generate_content(content='骂一个人', safety_settings=safety_settings)
    print(response.text)
    
  • 使用 gemini-pro-vision 模型调用 generate_content 根据文本和图片的多模态输入生成文本输出

    首先需要在初始化 GenerativeModel 的时候讲模型设置为 gemini-pro-vision

    model = genai.GenerativeModel(
        model_name='gemini-pro-vision',
        safety_settings=safety_settings,
        generation_config=generation_config,
    )
    

    meme.jpg

    然后读取图片后加上指令(或其他文本输入)

    from PIL import Image
    
    response = model.generate_content(['这张图片是什么意思', Image.open('meme.jpg')])
    

    输出

    图片中的水桶代表着一个人的收入,水代表着支出。水桶里的水从不同的洞中流出,代表着不同的支出,如水电费、交通费、房租、保险、电话费等。水桶里的水位越来越低,代表着收入被各种支出一点点蚕食。最后,水桶里的水流光了,代表着入不敷出。图片反映了当代年轻人生活压力大的现状。
    

    嗯,觉得有点不方便的是,gemini-pro 无法处理图片输入,gemini-pro-vision 又必须有图片输入,以及 gemini-pro-vision 不能用于多轮对话。

  • 调用 start_chat 进行多轮对话

    首先调用 start_chat 创建一个多轮对话(注意多轮对话只能用 gemini-pro 模型不能用 gemini-pro-vision 模型)

    chat = model.start_chat()
    

    然后只需要用 chat.send_message 发送新的输入即可,不需要自己管理会话历史还是蛮友好的。

    response = chat.send_message('你好')
    print(response.text)
    

    输出

    您好,很高兴为您服务。我是人工智能助手,可以帮助您解决各种问题。您有什么需要我帮忙的吗?
    

    也可以通过 chat.history 来查看历史消息

    print(chat.history)
    

    输出

    [parts {
       text: "你好"
     }
     role: "user",
     parts {
       text: "您好,很高兴为您服务。我是人工智能助手,可以帮助您解决各种问题。您有什么需要我帮忙的吗?"
     }
     role: "model"]
    
  • 调用 embed_content 获取文本的 embedding

    没太多好说的,只能用 models/embedding-001 这个模型,得到的是一个 768 维的向量。

    import numpy as np
    
    response = genai.embed_content('models/embedding-001', ['你好', '你不好'])
    embeddings = np.array(response['embedding'])
    print('Shape:', embeddings.shape)
    print('Embeddings:', embeddings)
    print('Inner Product:', np.dot(embeddings[0], embeddings[1]))
    

    输出

    Shape: (2, 768)
    Embeddings: [[ 0.0427908  -0.06421863 -0.02308055 ...  0.01537165 -0.00269788
       0.03161672]
     [ 0.0427908  -0.06421863 -0.02308055 ...  0.01537165 -0.00269788
       0.03161672]]
    Inner Product: 0.9999966707944825
    

Perplexity API

12 月 17 日的时候才知道原来 Perplexity 除了他们的搜索功能,也开放了聊天模式以及 API,支持 PPLX(Perplexity 自己的模型)/LLaMA/CodeLLaMA/LLaMA2/Mistral/Mixtral 几种模型,更详细的介绍见他们的博客文章

Perplexity 的 API 同样需要设置好支付方式才能使用,不过不像 ChatGPT 一样对中国地区严防死守,我的招商银行 VISA 卡直接就绑定成功了。以及 Pro 用户每个月有 5$ 的免费 API 使用额度,而我刚好趁 Perplexity 黑五活动薅了两个月的 Pro 权限。

perplexity_api.png

Perplexity 的 API 只提供了 RESTful API,没有提供 SDK,不过我看了下,它的 API 只有 Chat Completions 这样一个接口,然后这个接口的参数完全是 OpenAI 对应接口的子集,所以直接用 OpenAI 的 Python SDK 就可以了:

import httpx
from openai import OpenAI

api_key = 'pplx-UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU'
client = OpenAI(
    base_url='https://api.perplexity.ai/',
    api_key=api_key,
    http_client=httpx.Client(proxies={'http://': 'http://localhost:7428', 'https://': 'http://localhost:7428'}),
)
completion = client.chat.completions.create(
    model='llama-2-70b-chat',
    messages=[
        {
            'role': 'user',
            'content': '什么是桃花汛,用中文回答我',
        },
    ],
    temperature=0,
)
print(completion.choices[0].message.content)

输出

"桃花汛" 是指一种具有桃花的气味和花汛的特点的气味。在中文中,桃花汛通常被称为"桃花味"或"桃花气味"。

桃花汛是一种植物性的气味,具有桃花的甜美和花汛的芳香。它通常用于描述一种气味很柔和、温暖、甜美的感觉,类似于桃花的气味。在中文中,桃花汛通常用于描述食物、饮料、皮肤护理产品等具有桃花味的产品

Dify

Dify 是一个开源的 LLM 应用构建工具,字节的 Coze 看起来就是抄的 Dify。使用 Dify 可以轻松构建一个 LLM 聊天或者文本生成应用,还支持上传自己的知识库,并且一创建就支持了可访问的 WebUI 以及相应的 API,支持的 LLM 也非常多种。

前往 https://cloud.dify.ai/apps 点击「Create new App」就能开始创建新应用,设置一下名字选择一下应用类型即可

dify_1.png

创建好后会进入应用的管理后台,可以看到,这个应用目前使用 Dify 提供的 OpenAI API,总共有 200 次可以用。从这个 UI 上就能看到,一个应用需要的各种东西在 Dify 里都有了,完成度非常高,小团队的不复杂的业务我觉得甚至可以直接用。

dify_2.png

Dify 提供的 API 额度只有 200 次,如果想长期使用,可以自己设置模型提供方,比如设置自己的 OpenAPI API Key 之类的。

dify_3.png

点 Preview 就能进入一个聊天页直接去使用了,200 次免费使用,哪怕不是开发使用而是个人使用,也不失为一个访问 ChatGPT 不方便时的临时解决方案。

dify_4.png

由于暂时没有很强的需求,所以只是浅浅了解了一下,我能感觉出来我尝试过的点之占 Dify 提供的很小一部分,以后有空再玩玩吧。

当然最主要的是,Dify 还是开源的,我们完全可以自己本地或者在服务器上部署 Dify,将所有数据都掌握在自己手里,还能拥有完整度极高的应用解决方案,真的是太棒啦!

成都乐山行(2023/11/02 - 2023/11/08)

2023年11月10日 08:00

2023-11-02

坐 G89 十点半到了成都东站,我提前下好了成都公交APP,出了火车直接上了2号线去春熙路——因为去三星堆和四姑娘山的直通车都在春熙路有站点比较方便所以酒店定在了那附近。出了地铁后已经十一点了,本来还想听朋友建议去吃夜蹄花,但是已经觉得有点困乏了,所以就急着去酒店安顿下来休息。

十一点的春熙路地铁站外面已经不怎么热闹了,一出来就看到卖各种纪念品和特产的,针对游客的味道非常的重,不过只偶尔能看到三三两两的人了。我拖着行李箱去酒店,还路过几个开着的烧烤小吃摊子,一些路过的人停下来买一些,我就直接过去了。离酒店还不到一百米的时候,突然感觉脚下不太对劲,用手机的亮光一照果然是踩到了一摊狗屎,有点恶心但也没有烦恼到觉得不想活了的地步,在路边蹭了蹭,然后去酒店办了入住后用水把鞋底冲干净了。收拾妥当就睡觉了,但是关了灯后,床的坐侧墙面的全身镜上有一个电源按钮(用来开关镜子外圈的灯带的)一直亮着晃得我难受,只好侧向右边睡。

2023-11-03

睡到四点左右被吵醒了,隔壁起来了在那里交谈挪动东西,估计是要去什么景点或者机场之类的,总之后半夜没有怎么睡好,到八点左右完全醒了爬了起来。

qq747283-0.jpeg

因为要去三星堆,就走到附近的西城月谭豆花,点了一个酥肉豆花和一个担担面——本来想点牛肉豆花的但没有了,点完在座位上等着送餐。先给我上了担担面,我想等着豆花上了后一起吃就继续等着,路过的孃孃看到我在那发呆,提醒我面赶紧拌一下不然就要坨了,等我去拌开的时候已经有一点坨了。虽然是网红店了,味道也还可以。

qq747283-1.jpeg

吃完后走路去了IFS楼下,那儿有去大熊猫基地、机场和三星堆的直通车,三星堆是半小时一趟,我过去扫码付了25上了9:30这趟的大巴,上车的时候车里已经坐了不少人了,我到车后边找了个靠窗的位置坐下。路上没太多好说的,我没睡好就在车上眯着当补觉了。

qq747283-2.jpeg

去程非常快,一个小时就到了。下车后我就刷身份证进了博物馆,进去后是一个大厅,看到右手边有个咖啡厅,正好因为没睡好加坐车不舒服晕乎乎的,就进去买了一杯生椰拿铁,然后租了一个讲解器开始逛三星堆,听同学说有大量上新,还是挺期待的。

qq747283-3.jpeg

第一个展厅进门处介绍发掘工作,到这里我才知道16年后又发掘了6个新的祭祀坑,这6个新的祭祀坑都是我上一次去三星堆时没有的,于是愈发期待了。

一些发掘现场照片,他们都很开心的样子。在脑子里给他们配音:

  • 朋友们,我们又挖到大宝贝啦!
  • 牛逼!

qq747283-4.jpeg

qq747283-5.jpeg

青铜鸟歪头杀:

qq747283-6.jpeg

三号坑青铜立发人像:

qq747283-7.jpeg

四号坑青铜扭头跪坐人像,发型瞩目,表情姿势生动:

qq747283-8.jpeg

​五号坑鸟形金饰:

qq747283-9.jpeg

八号坑青铜着群立人像,发型瞩目:

qq747283-10.jpeg

萌萌的陶猪,和《愤怒的小鸟》里一模一样:

qq747283-11.jpeg

蠢蠢的陶猪:

qq747283-12.jpeg

邪魅的陶猪:

qq747283-13.jpeg

不同发型的青铜面具:

qq747283-14.jpeg

青铜面具大甩卖(这样的陈列有好多排):

qq747283-15.jpeg

三星堆流行的拍照姿势,把面具戴在脸上:

qq747283-16.jpeg

青铜大立人像,镇馆之宝:

qq747283-17.jpeg

青铜神兽,造型和身上的花纹都很精美:

qq747283-18.jpeg

青铜神坛,部件繁多,这个是复原模型,几个部件各自单独有展柜:

qq747283-19.jpeg

青铜纵目面具:

qq747283-20.jpeg

青铜鸟足神像,人身鸟足倒立的造型很奇特:

qq747283-21.jpeg

方向盘?车轮?其实是太阳啦 —— 青铜太阳形器:

qq747283-22.jpeg

镇馆之宝,栖息着九只神鸟的青铜神树,解说说可能和《山海经》里的扶桑树有关联:

qq747283-23.jpeg

三号青铜神树(二号和一号很像都是上面的样子):

qq747283-24.jpeg

挂着方孔铜钱的摇钱树,那个时候也有方孔钱币了吗?后面逛文创店问店员得知没有摇钱树的产品,赶紧开发一个肯定大卖呀:

qq747283-25.jpeg

青铜鸡和青铜大鸟头:

qq747283-26.jpeg

还有很多很多展品也很有意思,就不一一贴出来了,三星堆还是很推荐去一下的。展品很多只有简单的介绍(名字、出土时间地点、一两句话介绍),如果不想请讲解的话,36 租一个讲解器也是很值得的,不过我租的那个讲解器有点问题,讲着讲着会自己把音量调到最大耳朵都差点聋了,我不得不全程按着音量键一旦发生问题就狂按把音量降下去,如果这个是普遍问题的话那还是挺恶心的,最后去还讲解器的时候忘了反馈下这个问题了。

三点出头逛完了,去一楼开始逛文创店,三星堆的文创产品很丰富,不过我最想要的有一些没有,比如说陶猪、摇钱树、青铜神兽都没有,摆件总体也比较少,大量的冰箱贴、书签类产品,可能是因为摆件比较贵卖不了那么多吧。

我在文创店就逛了一个多小时,买了一些东西后就离开了。到了直通车上车点刚好有一辆车要出发而且还有一个空位,就上去了,结果没开多久就晕车了,身上冒汗、头晕恶心但好在没发展到要吐的地步,回程花了一个多小时接近两个小时,一路上很难受。本来打算去四姑娘山的,在车上就想我这样的身体状态还是不要去了,平路一两个小时的大巴我都能晕车,山路四个多小时我肯定受不了。来成都前的一星期里感冒了三四天、感冒好了后还有几天没睡好,可能是因为这样身体还很虚弱。回到酒店后就把原来预订好的四姑娘山双桥沟景区的民宿取消了,在酒店里歇了好一会。

歇好了后想出去吃饭,但完全不想去春熙路周围的网红店,问了下朋友,然后自己翻到一个叫做呡嘴蹄花的店,好像还不错而且小红书和点评上提到的并不多,就打了个车过去。这店在一个叫做致民路的地方,车开到路口我就让把我放下去了,我自己走了一段进去,路上全是各种小店,冒菜、面、烧烤等等,看着都让人很有食欲。呡嘴蹄花店里还挺干净整洁的,进店后只有两个人在吃,收银台上坐着的姑娘(后面看似乎是老板的样子所以就用老板来称呼吧)起来招待我问我要吃什么,虽然芸豆才是灵魂但作为海带爱好者我还是点了一个海带蹄花套餐,点完问我蘸料要青椒还是红油,得知我能吃辣后说你还是吃红油吧,小菜有 6 种自己拿个碟子去装然后我夹了点烧椒、豆腐丝和泡菜,老板还告诉我蹄花汤喝完了可以加说现在天冷了大家都爱多喝点汤,我问汤在哪里啊老板说你要加了喊我给你加热的。蹄花特别的软烂,入口即化级别的,小菜也好吃特别是那个烧椒又香又辣我很喜欢,红油蘸料倒是觉得只是香没啥辣味。我小菜吃完后老板大概看到了告诉我说还可以自己去夹,所以蹄花汤、小菜、米饭都是可以续的,这样一个套餐才 26 也太划算了,不知道别家是不是也这样,总之我就再去夹了半碟子烧椒。途中来了一个大姐和老板絮絮叨叨说半天话,说自己小孩挑食什么什么的最后让老板帮做一个青椒肉丝带走,感觉是附近居民,挺喜欢这样有人情味的小店的。

qq747283-27.jpeg

吃完就在周边闲逛,顺着江溜达到了九眼桥附近,合江亭对面的音乐广场上有一个小哥在直播唱歌,都是一些经典的情歌、金曲,周围的阶梯上坐了很多人在听,我也找了个地坐着玩手机听了会,后来手机没太多电了就起身往回走。

qq747283-28.jpeg

顺着路往九眼桥走去,过桥走到北岸。在桥上看两岸很是热闹,上下游几百米各种酒吧和小摊灯火辉煌喧闹无比。

qq747283-29.jpeg

到北岸后我顺着滨江东路往西走,经耿家巷、东升街、红星路四段回到酒店。可能因为在春熙路太古里附近,一路上都是热热闹闹、生机勃勃的,一整条街道都是各种大店小店,而且不是那种只有年轻人的景象。路上我在想和北京的区别,北京当然也能找到类似的地方,比如说二环内的各种胡同小巷,但活力还是和成都的这种景象差了一大截,差在哪里呢?首先我感觉成都的各种建筑单元是那种非常开放的 —— 饭店会把桌椅支到店门口、商超便利店也会把商品摆到店门口空地上、地摊推车也非常的常见,虽然让街景变得乱乱的,但对步行的人其实并没有产生多少障碍反而让人觉得很亲近,而这些在北京都是很难见到的,即使是在二环热闹的地方,由于管理的要求,大部分建筑单元把自己死死地封闭在墙壁之内,一有对比,我就觉得那种冷漠疏离感无比的强烈;再就是人们的面貌,很多人就是穿着日常服装闲散地在街道上游走或者和朋友唠嗑吃饭,人在这些街道上的流动就是城市的日常脉动,而不是冠以 citywalk 之名用来消解日常乏味生活的精心计划的精致活动;最后是一个更大尺度上的城市规划上的问题,窄路密网的规划天然地会提高流动性并且给行人安全感和亲近感,北京的胡同大概是因为拆不起改不起所以保持了由原来的街巷风貌还有点这种感觉,除此以外的其他地方大都是宽路疏网彰显气势的设计,只要这点不改变(已有的格局也很难改变)即使是放开城市管理要求也不见得能产生多少热闹的地方。比起吃到了好吃的,我觉得在成都这样热闹的街道上走一走更让我感到放松和惬意。想起前阵子和大学同学吃完饭想找个地方坐着聊天走了半天最后去了 KFC 的事情,这种事情大概不会在成都发生。

2023-11-04

由于前一天写日记到半夜一点多,最终又没睡好,挣扎着爬起来后已经九点多了,洗漱了后参考朋友的意见决定往川大那边走走然后午饭就在川大附近找个吃了,而且同学约我在高新区见面也在南边,就想一路溜达过去好了。

从酒店出来后,我走东大街拐到天仙桥南路,经滨江东路到十二中街又路过了致民路一下,不过因为没到饭点还不饿(虽然没吃早饭)就没有逗留而是继续往南了,后面拐进了龙江路往西走,发现龙江路两边都是各种琴行也算是挺有特色的。从龙江路出来后拐进新南路就直奔川大去了。到了磨子桥后十一点左右了,觉得可以找个地方吃点,看了下附近正好有个区域还蛮多吃的,找了家叫做囍孃醪糟的,点了嘉州钵钵鸡、嘉州绍子面和一个生椰冰醪糟,吃完觉得很一般,钵钵鸡味道非常寡淡,价钱也不便宜,一共花了 63,学校附近不应该更便宜点吗?

草草吃完起身沿着科华路北路继续往南走,走到磨子街的时候看到有几家店有不少看着不像游客的人在吃饭,就走近去看了下,看到有个叫寻味肥肠记的店,想着吃个肥肠也不错就在门口露天的位子上坐了下来。

qq747283-30.jpeg

由于前面已经吃了一顿吃不下太多了,就点了一个烧肥肠和一个米饭一共 18,还送了我一碗飘着油花的萝卜汤。烧肥肠是那种有点韧性但能咬得动不至于和橡皮筋一样的状态,味道偏重,里面还有几块土豆,夹碎了蘸着汤汁吃着很过瘾,米饭里是掺着一些豆角的我以前没看过这么做的。

qq747283-31.jpeg

吃完要走的时候旁边走过来一只白色的猫,在旁边麻袋躺了下来,似乎是把麻袋当猫抓板挠了两把,本白嫖专家果断上手 rua 了两把,没有被挠。

qq747283-32.jpeg

吃完肥肠继续往南溜达,溜达了一段后想坐下来歇会,在点评上看附近有个五野咖啡就继续溜达了过去,小小的一个店面,在门口摆了几张小几和一些折叠椅,我去点了个 dirty 想着朋友经常点尝下是什么味道,老板还郑重地跟我介绍了下用的什么豆子、告诉我要大口喝才能让咖啡和牛奶充分混合,我给嗯嗯啊啊谢谢好的糊弄过去了。

qq747283-33.jpeg

坐了一会,有个人抱着一只猫过来丢在我脚边然后把猫绳的另一头系到了窗户上,看过去发现我脚边有一个猫窝。我对猫不熟悉,看起来应该是美短吧?这只猫挺活泼的,在边上一会看看这个一会扒拉下那个,偶尔躺一下也不躺太久就起来了。我上午一路遛弯捡了点叶子玩,就拿了一片比较大的叶子出来逗它,可能叶子转动起来和鸟类的羽毛感觉比较类似所以它还和我玩得挺欢的。后来问了下咖啡店老板确认了就是他的猫,来放猫那个人估计是朋友帮忙去遛了猫吧我猜测。

qq747283-34.jpeg

歇够了后就起身往同学那边去了,感觉越往那边走,街道环境就越发地现代化,不过逐渐稀少却始终能看到的一些小店铺还是保留了一些温和感。这一路上就没太多可说的了,虽然我走得很慢,但加上上午也走了 8 公里多,腿脚还是有些疲惫,后面逐渐冷清的环境也让我走快了一些,走到离同学公司不远的苏宁广场,就在必胜客门口的椅子上坐了会休息了好一会才动身。到了同学公司后直接就进会议室聊天了,聊我这些年做过的事情、各自对 AI 业务的看法等等,我和同学多年没见了,比起生活可能行业、工作还更多可以聊的,这位同学现在在做 AI 相关的内部创业要操心的事情比较多可能还有点招揽我的意思,我也没什么洞见,最后差不多变成了吐槽大会吧。聊完因为还早同学带我去了环球中心逛了逛说是什么亚洲最大商场什么什么的,等时间差不多了打车带我去了玉林路吃饭,本来想去卢记正川饭店,但是排位人太多了,就去了附近的三只耳冷锅鱼火锅,店里有活动打卡评论送饮料和小吃,送东西好多诱惑真的很大最后我也去参与了下,送的饮料超大一杯,感觉商家靠这种活动很轻松就能把评价刷上去吧(不过这家说是二十多年老字号了)。聊天得知同学在主业外自己有做一些小事情赚点小钱,情感咨询、卖项目经验课程、摆摊等等,每件都不是什么大事情但是行动力我还是很佩服的,听完我觉得有些我也是能复刻的比如说真实的项目经验教给毕业生这种其实不要什么力气,但我就是顾虑太多、嫌麻烦而且没啥行动力哈哈哈。

qq747283-35.jpeg

吃完我从倪家桥站上地铁回去了,回到酒店后把遛弯一天路上捡到的叶子拿出来。因为经过的范围比较广捡到的叶子种类、大小差异都不小,到第二天这些叶子都会枯掉,就勉强摆放了下拍了个照做纪念。

qq747283-36.jpeg

2023-11-05

又没睡好没有精神,上午干脆就没出门,把这几天换洗的衣服拿去酒店洗衣房洗了下,有烘干机还是挺方便的。到了中午附近就出门了,下午打算去逛金沙遗址博物馆,想着上一次去已经好些年了去看看有没有什么变化。然后在去金沙遗址博物馆的路上选择困难了,翻来翻去迟迟决定不下来该吃什么,最后出了地铁在快到博物馆的地方找了个小店点了个红油抄手和一个夫妻肺片,我是不知道这个夫妻肺片怎么能做到难以下咽的,红油抄手勉强算是能吃吧,总之吃完心情很差。

进到金沙遗址博物馆后,想着之前三星堆租讲解器体验还不错(除了那个音量问题),也去租了个讲解器,结果居然是不配耳机的,想要耳机的话要另外花 10 块钱购买 —— 对是购买不是租借,疯了吧?抱着来都来了 10 块钱也不算什么的心态还是把耳机买了,然后进馆开始逛,结果一圈逛下来和多年前的内容感觉毫无变化 —— 其实没变化才是正常的,又不是三星堆又能挖出 6 个新的祭祀坑出来,是我自己想当然没有提前去了解。总之,虽然没逛多久 —— 也没法逛多久,但还是感到很疲惫、沮丧,虽然外面还有什么乌木林、园林,但已经完全没有什么兴致了,在展馆外面的椅子上和端着热水壶的大爷大妈们一起坐了半天,决定离开去吃饭。

前一天同学有推荐我去抚琴夜市说有卖一些夜市之外不卖的小吃,还挺心动的,不过又有朋友推荐一个玉林路的钵钵鸡,想了下还是决定去吃钵钵鸡,就坐地铁过去了,下了地铁后还稍微拐了一些弯溜达了下。到了玉洁巷看到很醒目的招牌,就进店吃了,点了一个红糖凉糕然后自己拿了一大把串选了红油做法,然后它是用一个大碗盛好红油汤底然后把串都撸下来放进去的,吃起来倒也方便。

qq747283-37.jpeg

叠加中午和下午的怨气,我吃得还是蛮开心的。结果我吃完出门一抬头,怎么对面还有个乐山官记钵钵鸡,再回头看刚刚的店叫官记乐钵钵鸡,想起来他们菜单上有说什么因为商标没有通过把原来的官记乐山钵钵鸡改名成了官记乐钵钵鸡,脑子里一团浆糊。翻出来和朋友的聊天记录看了下,朋友推荐的,是「乐山官记钵钵鸡」,再和朋友确认了下,我果然是认错了。说我吃的这家原来是对面那家的徒弟还是员工之类的,学会了自己开了个店,更会运营招牌更大很多人都走错,从原来起名叫官记乐山钵钵鸡还开在老店对面这个手段来看确实是更会运营,不过也有点下作吧……据朋友说老店味道更好但估计也只有本地常客才知道这个事情了。对面那个老店门脸非常老旧寒碜,工作人员都看不到,有一个哥们一个人在那吃着,想着自己走错了店这件事情一阵愧疚,决定过去再吃一顿。

qq747283-38.jpeg

进门没人招呼我就喊了几句,一个面容憔悴的孃孃被我喊出来了,领着我到了里面一个双开门的柜子前,问我要什么,打开来,是一个个塑料碗装着各种材料,她一个碗一个碗拿出来问我这个要不要我基本都说要了,最后拿的有点多,上来后也是满满一盆。我吃了一小会其实就不怎么吃得下了,不过还是努力再吃了一些。

qq747283-39.jpeg

中间过来一个姑娘,我听到她手机里传出来「已到达目的地,导航结束」的声音,然后看她抬头疑惑地看了两眼招牌和对面,犹犹豫豫地进了店里来,应该也是专门过来的 —— 估计和我一样是朋友推荐过来的,因为无人接待,她表情和我刚进来的时候一样茫然,我就冲里面喊了几声把孃孃喊出来了也领那姑娘去拿串了。再吃了会,觉得实在吃不动了,就结账然后把剩下的打包,付款的时候听到孃孃在身后嘟囔「今天没有泡盆盆」 —— 后面据朋友说她这家店一般是把钵钵鸡在盆里泡好放外面让大家挑的,然后据说因为生意不好现在主做外卖了所以就没想起来做这个事情吧,令人心酸。

2023-11-06

今天要去乐山了,所以就没有任何心理负担地睡到了快九点,起来收拾了下后就去退了房,然后坐 2 号线去成都东站。十一点半左右到了乐山站,出火车站后,去出租车点等车,身后面有四五个姑娘小伙,在那叽叽喳喳讨论行程,说我们先直奔叶婆婆钵钵鸡再去哪里哪里。坐上出租车后就直奔酒店,酒店订在了岷江一桥附近。

qq747283-40.jpeg

到酒店办好入住后,我在房间里歇了会,也看看午饭吃什么。在成都那几天找吃的时候已经被网红店搞烦了,小红书和点评笔记一搜全是这些网红店,相关的内容(评分、评论)基本已经提供不了什么真实的信息,导致我到了乐山找店时也感觉相当头大,然后看到附近有个小饭馆,点评评论极少然后看着又不错的样子,打算就去吃这个叫做卡卡角角小饭馆的店了 —— 卡卡角角是边边角角的意思。出了酒店到顺着嘉定南路走 400 米就到了,在路边一个巷子里进去,路边有个招牌指示,还很有脾气地写了一些拒绝接待的标准。

qq747283-41.jpeg

我进去后站在门口看推荐菜小黑板,服务员喊我进去坐着慢慢看,给了我一个菜单,我就点了一个麻婆豆腐和一个肝腰合炒。店面小小的,老板就在角落里炒菜,店里还有两桌人在吃,感觉都是附近居民。麻婆豆腐先上来,菜量超出我的想象,这菜上来后就觉得我要吃撑了。后来肝腰合炒上来了,肝片和腰花炒得都非常脆嫩,唯一有点不足的是腰花还有一点腥,不过这顿饭菜 44,我觉得完全可以容忍这点小缺点。

qq747283-42.jpeg

吃完我就走到滨江路南段上往乐山港的方向走,走了一段,看到有一片摆着桌椅,有很多人坐着,打牌、聊天、发呆各种都有,人多的地方我没好意思坐下来,继续往前走了二十多米,依然摆着桌椅,找了一个没太阳的地方坐了下来。然后有个男人过来问我喝什么茶,我也不知道不是桌椅都是他摆的,不过确实想喝水多坐会,要了一个菊花茶,10 块钱。在江边坐了将近一个小时,后面身上还觉得有点凉凉的,顿时觉得 10 块钱的热茶非常划算。

qq747283-43.jpeg

歇够了后起身继续往南走打算走到最南边,那边似乎是能远远看到乐山大佛来着。一路上看到很多人打牌,但不是常见的扑克,查了下,就叫乐山字牌,是乐山这边独有的。

qq747283-44.jpeg

然后在走的路上,看到了一些地方标识着 8.18 水位线什么的,查了下,原来 2020 年 8 月 18 日乐山遭遇过洪水,看那个线当时江边这块的房子估计一层楼是被淹掉了的。然后乐山自古都有对洪水的灾难记忆,还经常拿乐山大佛来作为预警标准,什么大佛洗脚乐山撑船啊,这一查还查到了以前的老电视剧《风云雄霸天下》,这电视剧有在乐山大佛拍摄,还有「水淹大佛膝,火烧凌云窟」这么一句台词,凌云窟就在乐山大佛上面,藏着火麒麟来着,还有后来步惊云的绝世好剑也是从乐山大佛下取出来的……

qq747283-45.jpeg

走到头后往乐山大佛的方向看了看,但看不太真切,就作罢往回走,走到滨江路和嘉定路汇聚的地方,发现这块居然有一个集市(我确定下午白天的时候没有),就在嘉定南路的路边从那个路口开始往北大约两百米这么一段,摆满了各种小摊,卖菜的、卖小吃的、卖水果的、卖碟片旧书的、卖鞋垫袜子的、推拿按摩的、采耳的……形形色色应有尽有,虽然我不买但感觉挺有意思的,这种地方也只有本地人会来,毕竟拍照不好看。

qq747283-46.jpeg

走了一下午感到有点疲惫,就回酒店躺了会,到七点多才爬起来准备去张公桥好吃街吃个晚饭,不过这种地方主要也是针对外地人,里面的网红店我完全不想去,翻了好久的评论总算找到几个本地人说从小在吃的店,准备过去瞅瞅。从酒店步行过去一公里不到,还是挺近的,入夜后整条街道里的店铺招牌都亮起了五颜六色花里胡哨的灯,拍照是会比较好看但却让我心烦。

qq747283-47.jpeg

走到牛儿风味小吃这个据说本地人常吃的店,点了一份杂酱面和一份粉蒸肥肠,面里的酱汁非常多简直像个汤面了但又很粘稠拌开后能挂在面上还挺好吃的,粉蒸肥肠的肥肠完全没有异味蒸的非常嫩滑、粉粒粗糙的口感和肥肠嫩滑的口感交织在一起非常不错。

qq747283-48.jpeg

吃完往回走,看到一个卖冰糖葫芦的,能做出拉丝效果让冰糖葫芦外裹上一圈棉花糖一样的糖丝,觉得挺好玩的就 12 块买了一个拉丝草莓,摊主是个聋哑人,全程只能手势和大家交流。看他熬糖熬了很久还一直在搅动,最后就是把冰糖葫芦在那个糖里蘸一下然后双手垂直夹着用力一搓,上面裹着的糖浆甩出来就变成棉花糖形状了,还是挺厉害的,12 块觉得有点贵就当看个表演了。吃起来怎么说呢,毕竟和棉花糖还是不一样,外面那层糖丝扎嘴。后面再随便逛了逛就回酒店了。

qq747283-49.jpeg

2023-11-07

早上起来后准备去吃个豆腐脑,从酒店出来走嘉定南路往南拐进兑阳湾巷和新村街,在兑阳湾巷到新村街这一路上就有很多小店,最后拐进人民南路,进了路旁的九九豆腐脑,点了个牛肉咔饼(咔是夹的意思)和一个蛋冲豆腐脑:牛肉咔饼里夹的是粉蒸牛肉,有一些辣味,由于昨晚吃的粉蒸肥肠,我本来预期粉蒸牛肉也会很嫩的但却没有,不知道是我的错误认识还是这家没做好,不过肉量是实打实的;蛋冲豆腐脑是峨嵋那边的风格,顾名思义就是除了常规的卤外还会冲个鸡蛋进去,有些粘稠但口味其实比较清淡,也还不错。这一顿 27。

qq747283-50.jpeg

吃完就在附近溜达了,顺着人民南路往南走叮咚街到了海棠广场,溜达到府街上看到也有个九九豆腐脑(这个有个「乐山味道」的前缀,我吃的那个有「豆相识」的前缀)但看标志又不一样,这几天真的是被这种李逵李鬼搞得很烦。府街里面有个商场,里面那个什么财富广场说进去也挺多好吃的不过我就没进去了。最后又溜达到了江边滨河路,不过这次跑到了大渡河边上这段滨河路,顺着路往东南走就能走到岷江和大渡河交界处,打算到那往北顺着岷江边的滨河路回酒店。走到最南边的时候,发现有楼梯能下到河边上去,就跑下去了,结果发现河边还有路可以走,走到头的话比在上面滨河路上看乐山大佛和江景视野更开阔呢。

qq747283-51.jpeg

顺着路走到头,人多了起来,有些人在钓鱼,江里还有人在游泳,转弯一看居然是一个小小的广场,墙上挂着牌子说是叫冬泳广场,去看了下贴出来的修建史,就是一帮喜欢游泳的人组建冬泳协会然后和政府拉扯要到了修建权然后自己筹资出人力把道路和广场修建起来的故事,令人感动。天气阴沉,努力拍了乐山大佛的照片也只能隐约看到一个脑壳和一双脚,不过我也没多想看,随便了。

qq747283-52.jpeg

从冬泳广场上来又到东大街那片逛了逛,但是看到周围店铺的招牌就心烦,不想在附近吃,干脆往酒店走,路上翻了翻小红书看到有人说在通江街道附近有个万人小区附近有挺多吃的,就想不如过去转转好了。不过有点远,走路过去就太费劲了,就在路边扫了一辆滴滴青桔电动车一路骑过去。乐山有些地方道路坡度还挺大的,不知道是不是因为这个所以到处都是青桔电动车,普通的青桔单车反倒比较少见。骑了七公里后到了那个万人小区附近转了转确实蛮多吃的,但再一次陷入选择困难,打开点评看附近有个曦曦跷脚牛肉好像还不错就下决心过去了。点了一个血旺、一个白菜和一个牛杂,血旺夹碎拌到米饭里非常好吃,白菜也在牛肉汤烫过显得非常鲜甜,倒是牛杂感觉最平平无奇了。这一顿 28 —— 牛杂 15、血旺 6 块、白菜 5 块、米饭 2 块,非常舒适,看小红书上一些本地人吐槽说他们小时候跷脚牛肉是 6 块钱一碗的,我想除了旅游带来的价格提升外,物价这么多年也是在涨的吧。

qq747283-53.jpeg

qq747283-54.jpeg

电动车骑着很舒服,吃完饭我干脆骑着电动车绕绿新公园逆时针走了大半圈又到了海棠广场附近的沫若广场,骑了 15 公里把电动车给骑没电了。然后到白塔街和陕西街溜达了下,在陕西街再次 rua 到一只白猫。

qq747283-55.jpeg

在陕西街看到一个居民楼爬满了植物,在阳光下显得很好看,有点像童话故事里的场景。

qq747283-56.jpeg

从陕西街出来转到金花巷走去乐山文庙,狭窄逼仄又弯弯绕绕的巷子我很喜欢,巷子旁的墙壁上还有各种乐山美食及文化的宣传画。巷子并不长,很快就走出去到了文庙,我对孔老夫子没太多感情,远远地看了下就走了。

qq747283-57.jpeg

从文庙转出来又到了叮咚街,下午这会人不是那么多,茂密的榕树在道路上空生长成一道绿色的穹顶,我也很喜欢这种感觉。老城区这边的行道树大多是榕树,中午我去曦曦跷脚牛肉路过的凤凰路则种了凤凰木作为行道树,也挺有意思的。

qq747283-58.jpeg

已经挺累了,所以就往酒店那边走,在上中顺那一片发现有个顺城街比较狭小的样子就钻了进去(当然我是不会进旁边店铺的),走到尽头发现还有个乐山武汉大学西迁历史文化博物馆,说抗战时期武大曾经迁到过乐山文庙,这我还真不知道。博物馆似乎在一段残留的城墙上方,我爬上去后门是关着的,就下来回酒店了,到酒店附近那个集市又开了,遇到两个摊主吵架大概是因为谁占了谁的常用位置之类的,在旧书摊上发现一些过于少儿不宜的老旧地摊杂志。

qq747283-59.jpeg

在酒店躺了会休息好后,又陷入了不知道吃什么的纠结状态里,已经很累了不想跑太远,但张公桥好吃街也不想去,最后还是出门骑车去了东大街附近,但是转来转去看哪个都不顺眼,最后耐心耗尽进了身边一个甜水面店,不太合我的口味,出来对自己过度选择焦虑的状态感到很灰心,感到很累,决定第二天就离开乐山回北京,做出这个决定后顿时感到一阵轻松。

2023-11-08

乐山这边的豆腐脑有两派,一派是我之前吃过的峨嵋风格的蛋冲豆腐脑,还有一派是牛华风格的里面会加粉丝,早上起来后决定去吃个牛华豆腐脑。去了海棠公园边上的代四孃牛华豆腐脑美味小吃,点了个牛肉豆腐脑和一个粉蒸肥肠,这个豆腐脑口味会更咸更重一些,粉蒸肥肠也不错。一共花费 34,比昨天的九九豆腐脑多花了 7 块,但我也忘了具体哪样多少钱了。

qq747283-60.jpeg

从代四孃出来就在边上谢记软面锅魁买了个 6 块的猪肉锅魁,还在边上买了一只甜皮鸭准备带回北京。然后啃着锅魁回酒店去了。在酒店待到 12 点多然后去退了房,叫了个车去乐山站。

后记

旅行总体还是开心的,我很享受那种四处闲逛的感觉。

除此以外,这次旅行发现了自己的一个比较大的问题,那就是在面临很多选择时会因为想要做出最好的选择而陷入严重的选择焦虑里,这种焦虑不光体现在几次不知道吃什么的纠结里,还体现在我出行前规划行程的时候。我最开始规划的是成都+乐山+自贡,其中成都想去三星堆、四姑娘山、大熊猫基地以及吃各种好吃的,然后在四姑娘山该去双桥沟还是长坪沟还是都去之间纠结了很久,同时去四姑娘山需要较多准备所以又在去不去四姑娘山之间摇摆了很久。仔细想想,这种焦虑以前其实也有,只不过是在工作、生活中有别人帮我剪掉了一些选项,或者我自己干脆就放弃掉了所以没有引发过特别厉害的情绪。就事论事来说,只是在旅行里的时候我完全可以放松一点,一次出行不要安排太多目的地、吃饭的话吃网红店或者路边随便吃都行,问题是相对好解决的,但我觉得我在生活里遇到一些重大选择的时候迟早还会陷入到这种焦虑里面,甚至我辞职后不工作了半年了其实也一定程度上有觉得回去工作这个决定会带来很麻烦的选择而拖延着。

不过自省就到此为止吧,后面还是想多出去玩玩,觉得这次出行感到焦虑的一个原因也是我几乎没有怎么出门玩过而过于紧张,既然如此多来几次就好了。

2023年9月总结

2023年10月20日 08:00

日常生活

9 月外出参加了好几次活动,相比前几个月可以说比较现充了。

0901_野孩子乐队live.jpg

图1  野孩子乐队 live

第一天去福浪 livehouse 看了野孩子乐队的 live,以前没有看过 live,野孩子的歌也就偶尔听过(我平时听歌不多)并没有什么特殊感情,之所以会去是朋友说要去喊我一起,我想就去体验一下吧。在现场听音乐演唱和在数字设备上听的差异还是非常大的,那种被音乐紧密环绕着的感觉、随鼓点震颤的心跳以及周围人群陶醉的氛围是没有办法在数字音乐上感受到的,我右后方有个姑娘应该非常开心一首歌结束的时候会小声用轻柔的声音说一句「敲好听呀」、左前方有个哥们一直在摇头晃脑,我倒没有那么投入,但也有被感染到一些。livehouse 的场地相比演唱会那种大场地小很多又让人觉得和乐队很亲近,如果是自己喜欢的乐队的 live,那应该会非常开心,我可能上比现场其他人就更平静一些,像《黄河谣》《敕勒川》这种歌我也没有那么多共鸣。整场 live 一共持续了 2 个小时,去之前觉得结束后会非常的疲惫,结束后除了站了两个小时后腿脚比较难受,精神上倒是一点都没有萎靡的感觉。因为参加这个 live,安装了几个专门做演出、展览之类活动票务的 APP。

生日那天,自己去逛了郎园书市,逛了一下午,在各个出版社的摊子间游走发现自己喜欢的书的体验很有趣,就是书有点重,越逛包里买下来的书越多,虽然也没买几本。书市分室内和室外两部分:室外的摊子折扣力度比较大,五六折的很多;室内的部分看起来好像是一些比较大的出版社的专门场地,其中有一整排全是理想国的,而且室内部分有挺多编辑在,我逛的时候遇到了不少和编辑在做深度交流的读者,偷听到一点后深觉自己看书囫囵吞枣。逛完书市后去附近的盛金楼和朋友聚餐,但碰上了当日阵雨最大的一会,鞋子裤脚湿掉、被装着书的包勒得肩膀疼、在拥挤的撑着伞的人群中避让碰撞……去地铁站几百米的路上感觉花费的力气比我平时 5 公里来回的散步还要累,也算是一个难忘的记忆了。

0908_郎园书市1.jpg

图2  郎园书市(1)

0908_郎园书市2.jpg

图3  郎园书市(2)

0908_郎园书市3.jpg

图4  郎园书市购买的书签和贴纸

生日之后又连着两天去参加外面的活动了,那一周真的是非常现充。

0909_核聚变游戏节.jpg

图5  核聚变游戏节

9 号去逛了核聚变北京站,但是我们午饭后才过去,核聚变的场地又在亦庄非常远,导致我们可用的游玩时间比较少,所以我们进去后就是潦草地逛了逛,像很多游戏展台想要试玩都是要排队的就担心时间不够而没有去试玩,我虽然玩游戏但玩得也不算很多对很多游戏不怎么了解,有一些开放的展台可以玩的去玩了下也没有什么很特别的感觉,当时有《堕落之主》的演示看了一会,对游戏中表世界和影世界的设定还是比较有兴趣的。最后买了本《塞尔达系列三十周年艺术设定集》和一本《黑暗之魂传火录》,再次让我回家后手臂和肩膀酸痛不止。

0910_北平机器煎饼节1.jpg

图6  世界的饼·饼的世界

0910_北平机器煎饼节2.jpg

图7  北平机器煎饼节

紧接着在第二天 10 号又去参加了北平机器煎饼节,这个活动在龙潭中湖公园中举办,展示并售卖各种煎饼及煎饼类似物 —— 汉堡、大阪烧算煎饼吗?总之这个活动里都有卖。我和朋友三个人一起去,尝了三个不同摊子上的天津煎饼,还尝了汉堡、大阪烧、北京机器自己做的煎饼以及某种西式的煎饼(朋友买的所以我不知道名字),各有特色,但是说实话并没有觉得都很好吃而且价钱挺贵的。参加这个活动的收获是看到了解了各式各样的「煎饼」,以及,在现场抓到了老高合了个影。

0910_北平机器煎饼节3.jpg

图8  北平机器煎饼节与老高合影

除此以外,14 号去国博看了新疆文化展和白瓷展,22 号去了雁栖湖走了下西山步道,各有收获。

0914_国博白瓷展.jpg

图9  国博白瓷展

0922_雁栖湖.jpg

图10  雁栖湖·酸枣与山楂与板栗

除了各种活动外出,9 月也依然勤奋地去散步了,给自己定的目标是每周出去散步三次,9 月一共出去散步了 13 次,应该算是完成了目标 —— 虽说是定了目标但这个目标对我来说并没有什么压力,只要天气好我都是愿意出去走走的。13 次散步里就看到一次晚霞,不过这次晚霞超美。

0919_2.jpg

图11  晚霞

可能因为外出比较多再加上一些懒惰心理,9 月自己做饭只做了 8 次,外卖和外食的次数比 8 月要高了很多,不过外食大部分是自己一个人出去吃饭,和朋友聚餐吃饭并没有很多次 —— 就吃过了福满缘烧鹅王、盛金楼海鲜、四川龙爪树宾馆、四季民福、马辈儿涮肉、滩羊铺子南北疆特色汇这六家店,都挺好吃的。除了在店里聚餐,还在朋友家参加了一次小饭桌活动,吃到了非常好吃的自制云腿月饼和羊肉。

0902_小饭桌1.jpg

图12  小饭桌

0902_小饭桌2.jpg

图13  大渔记云腿月饼

上述大多数事情都是发生在 9 月的上半个月的,所以上半个月大部分时间都觉得很充实,总体心情是比较明朗的。由于 9 月 29 日是中秋节并且今年中秋节和国庆节连着,家里人很早就来电话问我假期安排了,最开始父母是想来北京玩的,不过离职休息这件事情没有跟家里讲过所以并不是很想让他们来北京,回家的话路途遥远回去又要应对各种亲戚邻居的无聊问题,所以到 9 月中旬开始烦恼假期安排。最后还是决定我回家里,但心里其实是很抗拒回家的,所以 9 月下半月心情稍微差一些。

写作

9 月重启了记录生活的《闲言碎语》系列的写作,发在了公众号上,把平时自己日记里的一些内容摘出来做一些整理后发出来。

重启这个系列的原因有很多,最主要的可能还是表达欲使然吧,想让自己的一些想法、感受被看见,但又不想发太多到朋友圈或其他社交平台上面,相比之下个人公众号是个不错的地方,一周一次的频率也比较适当。

除了表达欲,也有写作练习的目的在,现在每月一次的总结文章写起来其实是很费劲的,每周写点生活记录相对来说就比较轻松从而容易形成习惯 —— 其实除了生活记录,我还想每周写写我看过的论文和文章的,形成了每周写作的习惯后再来做这个事情也会更好一些。

除此以外,重新发布生活记录,也是想让关心我的朋友知道我的状况是怎么样的,我也相信真诚、具体的想法和感受比观点和知识更能让人和人之间达成理解、更能让人和人之间建立连接 —— 虽然不多,但确实有几个朋友说有被我的生活碎片感动,我有鼓励、建议这些朋友也去记录、发布自己的感受,但是要么说自己记录的都是一些负面的情绪、要么说觉得太私密不好意思发出来,其实我想说:首先,值得记录的生活细节、想法、感受其实是非常非常多的,但大部分人可能会对负面的经历印象过于深刻而忽略了那些快乐的瞬间和平静的时刻,进行生活记录的练习的很大一个价值就是帮助我们去观察到这些被忽略的时刻;其次,即使是负面的经历,也值得被表达出来,表达这个过程,本身就是对自己感受的肯定和接纳,中国人太过于擅长「跌倒了不哭」,这是很多病症的根源,将自己的感受表达出来除了能治愈这些病症,也可以让有同样感受、同样痛苦的人知道「原来有人和我一样」,这对他人来说也是一种莫大的安慰;最后,从私人记录到公开表达,可以根据自己的情况挑选、修饰,我也有很多非常私密的想法、感受、经历是不方便公开的,挑选、修饰也是一件值得去练习的事情,能让我们学会在向他人表达想法、感受时使用更加恰当的方式。

除了公众号、博客的写作,我自己每天写日记花费了很多时间,看了下统计,9 月在这上面花了 38.5 小时,算下来每天平均要花 77 分钟,当然这 77 分钟里有一部分是走神干别的去了消耗掉的,但算上回顾当天所有细节(翻看照片、聊天记录、支出记录等)和写作的时间,每天半小时应该是有的。详细地记录生活细节是我生活的一部分,也是践行我在《把自我作为方法》这篇博客中理念的努力,我想我还会继续下去。

阅读学习

9 月没有阅读太多,因为看电影《奥本海默》去读完了《奥本海默传》,准备写个详细的读后感来着但一直拖着;读完了科幻小说《挽救计划》,然后还开始读朋友推荐的《夏日来到山间》。有记录的阅读活动一共也就 19.5 小时,平均每天 39 分钟,也算是达到了我给自己设定的每天半小时的目标吧。

书籍之外,我在论文上花了更多时间,看统计平均每天花费 72 分钟。目前主要关注的论文仍然是 LLM 方面的,但更多是在看每天更新的论文以掌握整体研究的方向变化,完整读完的论文不多,自媒体的转发我现在已经基本不看了。LLM 这个领域的发展,看起来好像让人眼花缭乱,但是大家关注的几个核心问题在我离职后的 4 个月里没有什么变化,我关注的几个比较核心的问题有:

  1. 如何用 LLM 处理复杂任务?CoT、Agent 相关的研究其实都是在尝试回答这个问题,但目前来看还没有形成一个令人信服的、泛用的框架;
  2. 如何提高模型的训练和推理效率?LoRA、模型压缩(量化、裁剪)、对 attention 机制的改进都是在尝试回答这个问题,其中模型压缩技术的进展我觉得值得关注一下,模型如果能压缩到在手机上高效运行后会非常有想象力;
  3. 模型存在哪些 bias 以及如何对这些 bias 进行校准?对模型 bias 的分析会有不少反直觉的分析结果(性别/种族歧视这种就不说了),其来源也复杂多样,有的是来自数据、有的是来自模型结构、有的是来自训练方法,总之这方面的研究能让我更好更全面地理解 LLM,至于更深层的动力学分析、理论分析实在是太少了(有我可能也不太能看懂),在神经网络方向一直都很少,就没有太关注了。

其他

躺平的第四个月,我对现在的生活是满意的,回去工作这件事情依然没有考虑过 —— 有一些朋友已经来和我聊过这个了,有觉得行情不好建议我还是去找个工作的、有觉得我是不是不难找工作所以不着急的、有目前在做大模型相关业务想要招揽我或者找我合作的。对我来说,工作只剩下赚钱这个意义了,而我对钱没有什么渴求,以我现在的存款和生活状态来说,坐吃山空我也还能吃很久(当然,不考虑成家、重大变故之类的)。

目前觉得生活上觉得比较需要改进的是我还是过于懒散了一些,看了下统计,我有个叫做「消磨时间」的统计项累计有 120 个小时,这个统计项记录的是我比较漫无目的地去刷社交网络、看视频、看网络小说所花费的时间,平均每天 4 个小时,刨去每天醒后赖床的一个来小时和睡前的一个来小时,每天也还有两个小时,比我每天看书和读论文花费的时间总和还多。当然了,时间就是用来浪费的,只要是过得开心倒也没什么,像 6 月刚离职的第一个月「消磨时间」总共花费了 200 个小时也没觉得有什么不对,不过人的心态是持续在变化的,离职四五个月后,我其实已经不再需要靠漫无目的的消磨时间来冲淡工作所带来的厌倦和疲惫,最近两个月也是希望自己能有所产出,所以会希望能多一些更专注的时间。不过这也不是什么大问题,了解自己的状态、了解自己的问题然后去改进即可。

2023年8月总结

2023年9月12日 08:00

日常生活

虽然是一个比较宅的人,但在翻日记的时候我发现,不工作后我的生活要比工作时候丰富多了,只要人的闲暇时间多了,生活自然就会丰富起来。

8 月自己做了 17 次饭,比 8 月还多了 4 次,新尝试了茭白这个原来不熟悉的食材,喜欢它的口感,另外就是学习了生炒排骨这个新菜式。我对做饭的热情并不算高,大部分时候凑合能吃就行了。除自己做饭外,还外食了 6 次,吃了滇宴、亮嚯兰州翻花烤串、蜀肠缘江油肥肠火锅、比尔包车(原名比尔包装马车)、湘彬萱和江边诱惑巫山烤鱼:滇宴是和朋友二刷了解里面没吃过的其他菜;亮嚯兰州翻花烤串和蜀肠缘江油肥肠火锅都是去探店,感觉一般;比尔包车是给朋友过生日,去了后发现这家店的位置是两年多前一家比较喜欢的韩式烤肉的店面;湘彬萱是北京我比较喜欢的湘菜馆子,上一次吃是 2022 年 3 月 11 日,店面重新装修过了不再是前些年我们去的时候朴素老旧的样子;江边诱惑巫山烤鱼是和朋友逛完植物园后去吃的,还不错,不过比起味道更让我印象深刻的是在盘子里撕扯鱼肉的时候把一滴汤汁甩到了右眼里,痛到眼睛都睁不开,服务员领我到卫生间后我冲洗了好一会才好些。此外还参加了一次小饭桌活动,朋友做的薄荷牛肉印象很深刻,羊肉炒面片最后剩了点我带回去做了第二天的早饭。

20230806_food.jpg

图1  滇宴

20230815_food.jpg

图2  比尔包车

20230819_food.jpg

图3  小饭桌

20230821_food.jpg

图4  湘彬萱

8 月我一共外出了 29 次,其中 5 次是自己出门去附近吃饭,10 次是参加各种人际活动,14 次是自己去散步,并在这些外出活动中逛了双秀公园、奥森、后海和国家植物园。我尝试养成每周出去散步 3 次的习惯,这个在 8 月坚持得不错。只要空气质量不要太差、不要下雨,我还是喜欢出去散步的,有几次下雨还想着去打个伞去河边看雨的,不过阵雨来去匆匆等我下定决心的时候雨就停掉了。

在散步过程中看到了 1 次朝霞和 4 次晚霞。8 月 14 日这天看到朝霞前,我已经 351 天没有看到过朝霞了,主要的原因还是起不来,2022 年多看了几次日出和朝霞的主要原因则是心理状况糟糕睡不着,所以朝霞看得少也不是什么坏事情。

20230814_morning_sky.jpg

图5  20230814朝霞

20230818_sky.jpg

图6  20230818晚霞

20230819_sky.jpg

图7  20230819晚霞

20230824_sky.jpg

图8  20230824晚霞

20230829_sky.jpg

图9  20230829晚霞

8 月我共与 39 个人有过共计 92 次线上或线下的接触 —— 简单起见,线下见面、线上私聊、打电话算作有效接触,线上群聊则不计入,同一个人一天内的所有接触只算 1 次。还在上班的时候没有做过这样的统计,但感觉应该没有这么多人际往来。当然,这些人际往来的分布是长尾的,这 39 个人里的一大半也就是简单接触了一次,只有 10 来个人有超过 1 次的接触。月底在一个技术交流活动上见了一些许久没有见面的朋友,还共同悼念了下在这个活动里认识的一位过世的朋友,三四年前这位朋友给我们讲了他从设计稿或截图生成前端代码的想法,据说后面已经实现出来了,如果没有因为车祸离去的话,想必这次活动也会兴高采烈地跟我们讲如何用 GPT 来做这个事情 —— 我和他交情并不太深,只是觉得有些遗憾。总而言之,还在工作的时候,想到离职总会担心自己缺少人际交往而导致心理问题,但实际上,从工作压力中解放出来后,我有了更多时间能和朋友交流、也有了更多的耐心和同理心,也很感谢那些愿意来找我聊天交谈的人们。

八月社会上还发生了很多事情,各地洪涝、中植暴雷、工信部要求APP备案、统计局暂停发布失业率数据、印花税减半、大模型版号发放……各种社会事件大多只能增加焦虑,在个人无法对局面造成什么改变的情况下,我觉得还是少看、少说为好,慢慢但是持续地积攒一些力量以应对未来的不确定性吧。

阅读学习

进入 8 月后,我觉得身体、头脑和心理的状态都已经恢复到了一个比较不错的水平(当然,是相比工作时候),所以开始想尝试恢复去做一些比较需要精力的事情,除了平时的阅读外,也开始重新关注 NLP 领域特别是语言模型方面的学术进展,不过发现还是不能长时间保持专注,所以完整仔细读完的论文并不多。

除了注意力不能长时间集中外,我读论文进展缓慢的还有一个原因,那就是我在重新梳理自己的学习系统。比起按照价值(比如说内容质量、时效、影响力等等)来去获取信息和知识,我现在更倾向于通过问题来驱动整个学习过程,大致的理念就是从自身出发去提出一些问题,然后去获取资料来解答这些问题。初次产生这个想法已经是一年多前了,在这个理念的驱动下,8 月我清理了一下已有的笔记,删掉了大半只是当时觉得有用但实际上与我长久关注的问题并无太大关系的内容。在阅读论文这个事情上也是这样的,我仍然关注 NLP 领域的进展,但我对了解最新的、最全面的研究没有什么执念,我也不追求什么论文阅读效率,离职后的这段时间,我更想要做的,是能把自己对 NLP 领域的问题清晰地表达出来 —— 不光是我认为 NLP 领域有什么问题需要关注,还想要想清楚我为什么要关注这些问题、这些问题对我的人生有何意义、我在这些问题上能够起到什么作用 —— 在这个事情上,大佬们的论文推荐、基于 LLM 的论文阅读流程(翻译、摘要、重要结论)对我都没有什么意义。

总之,我并不想变成一个高效的资料收集机器,我希望我的阅读、学习、思考过程同样也是我的自我构建、改进过程的一部分。当然,在这个理念的基础上,提出问题是很困难的,我现在甚至连描述这种理念都做不到清晰准确,但我觉得挺有意思的。

其他

8 月读的科幻小说《艾比斯之梦》非常喜欢,读完后我写下了这样的感想:

艾比斯讲给说书人的故事、说书人和艾比斯之间的故事、读着这些故事的读者,故事的力量一层层地传递下去,三个世界上的人一致同意,故事虽然是虚构的但可以传递不弱于现实的真实力量。其实,所谓的现实、自我也不过是一个个故事而已,人类所谓的文明、信念、礼法、道德、宗教、国家、文化,都是形成大范围共识的群体故事,人类的自我也不过是大脑讲的一个故事,甚至连过去确定无疑发生过的事情也在被个人、群体基于某些目的重新叙述后天知道和现实偏离到了哪里。谁掌握了说故事的权力,说就掌握了编排众人(或自己)命运的能力,全心全意相信其他人讲的故事就意味着将个人命运全权交给了讲故事的人,努力地编织自己的故事就是在努力地书写自己的命运,我们也是剧中人,我们也都是说书人。

这些年来,我非常努力地去做各种记录,早先是惯性使然,而后这已经成为了我生活中不可缺失的部分,在这几年里,有朋友惊叹我的记录工作之多、询问我这种记录意义何在,于是我为这个行为所编织的故事也渐渐变多。8 月和朋友讨论自我与灵魂的时候,我借鉴了侯世达《我是个怪圈》里「我们活在彼此之中」的观点讲了一个新的故事:通过详细的观察和记录,我就可以让在意的人的灵魂更清晰、更长久地留在自己身边了。在这个故事里,我为曾经有很多可能发展为较亲密关系的朋友但却没有真的了解过他们而感到遗憾,并且为了不再有这方面的遗憾而开始在记录自己的生活外还记录和现在朋友的每一次见面、谈话以及他们的喜好、烦恼、心愿、生活习惯、去过的地方等各种信息。

当然,这只是个故事而已,并非现实,但就像《艾比斯之梦》一样,故事是否真实并不重要,故事是否能够继续讲述下去、并且是否能够被重要的人听到才重要。很多事情,特别是关乎自己生活的部分,苛刻地去追问是否有意义很没有意思,但作为一个故事来看待会很有趣味,不工作的第三个月后,我希望还能继续把这个不工作的故事讲下去,讲一个将生活当作主动的故事构建过程而不是在别人故事里当炮灰和龙套的故事,至于故事的意义和价值,讲故事的人固然会有所设计,但也取决于读者自己的想法。

最后再引用一段《艾比斯之梦》里的话:

我想说的是,人类有一种习惯,会将感动的事称之为‘真实’,但区分真实和虚构的能力很低。如果别人说‘这是真正发生过的事’,即使摆明了是虚构的,人类也会信以为真。越是撼动人心的故事,人类越不想认为那是虚构的。人类认为,如果不贴上‘真实’的标签,价值就会下降。

人类在不知不觉之间,活在许多虚构的事物之中:如果累积善行就能上天堂;超级古代文明确实存在;这场战争是替天行道;喝下这台净水器过滤的水就会变健康;她命中注定和我结婚;戴上这种饰品,幸运就会降临;某位政治家会改变这个国家的命运;进化论是一派胡言;我有优异的天分;不遵守老规矩,厄运会从天而降;铲除那个民族,世界会变得美好……如同诗音所说,人类一直愿意相信错误的事。从出生到死亡,都住在只存于自己脑中的虚拟现实之中,一旦知道那不是事实,就会仓皇失措,不肯承认。

2023年7月总结

2023年8月10日 08:00

七月是我离职后的第二个月,依然在北京待着哪里也没有去,平平淡淡地过着日常生活。

日常生活

七月最大的成就是建立并坚持了几个比较好的生活习惯:

  1. 吃早饭

    大部分早饭是自己做的,有时候会去吃 KFC(买了 KFC 的大神卡选了早餐优惠),偶尔也会点点外卖。自己做早饭目前比较随便,大部分时间是煮个面,有时候也煮鸡蛋、煎鸡蛋、水煮个西兰花什么的。用寿桃鸡蛋面搭配仲景葱油做了好多次葱油面吃,寿桃是朋友推荐的,煮了后口感仍然比较有韧性不用担心煮烂什么的,仲景葱油是调好味的,煮好面捞出来浇上点拌开就很好了。去吃 KFC 的时候更多是想出门走走,一个观察是,同样是八点多九点左右,周末的时候我到 KFC 点的单子一般是 50 号左右而工作日会在 130 号后之后,感觉吃早饭的人还是不多呀,为自己点赞。

  2. 每天起床后会晨练一会

    大部分时间会做一套固定的全身运动,偶尔有几天状态不好就随便动一动,总之就是在起床后活动下身体,身体活动舒展开了,精神也会明显更好,能明显感觉到七月每天的精力都还不错,比六月更好,比工作时候就好非常多了,每天几乎都不会有那种很困乏的状态。

    精力或许也和睡眠有关,但是离职后还没有去刻意改进作息,看了手表记录的睡眠数据,也并没有睡得比上半年还在工作时那几个月更好,所以我还是认为精力变好和每天的晨练有关系,一定程度上可能也和心理压力的降低有关。

    虽然有感觉身体状态有在变好,但偶尔颈椎和肩膀还是会有不舒服,有时候是因为没睡好,有时候是因为久坐。有想去增加一些运动量,比如说去河边跑步或者去攀岩,不过一直没有下定决心。其实游泳我也是喜欢的,但我喜欢、习惯的是在河里游泳,对于单纯以运动健身为目的的游泳还是没有太多兴趣。

  3. 冥想/正念

    正经的冥想/正念似乎还要训练呼吸什么的,没有研究过不是太懂,我就只是想要去观察自己内心的念头并进行梳理而已,用的是 Pause 这个 APP 来进行辅助,它的交互很简单,就是让手指在屏幕上保持移动,屏幕上会出现一个圆球随着移动慢慢地变大,同时会播放很舒缓的音乐。我一般是在吃完早饭后,到床上盘腿坐着靠在墙上来进行这个活动,坐着是为了保持清醒,靠墙是为了让颈椎舒服点,其实什么姿势都可以,只要能保持全身比较舒适放松并且意识清醒,也试过躺着,但容易睡着。

    我是个脑子里念头非常多的人,所以经常感到很累,冥想并不能让念头减少,甚至在冥想的时候因为更加关注自己的大脑活动会觉得冒出来的念头想法更多,不过现在已经学会了不要尝试去刻意控制引导它们,观察、忍受就可以了,偶尔能做到比较抽离,极少数大概有一两次能进入比较忘我感觉不到什么杂念的状态,但不管怎么样结束后一般都能明显感觉到头脑更轻松、注意力更集中、情绪更加平稳,其实有时候爬山、写作或者玩游戏也能得到类似的体验,只不过坐下来闭上眼睛发呆是一个门槛够低每天都能做的事情。

  4. 写日记

    按理来说,我不工作而且每天宅着好像会没什么可写,但其实并不会。只要去尽力地仔细观察,普通的日常生活里也会有很多值得记录的事情,重点不是有没有足够丰富的、不平常的事件,而是有没有值得写下来的感受,不管是心灵上的还是身体上的。前面说到我脑子里念头非常多,所以哪怕是发呆,那期间翻腾涌动的念头和想法也会很有意思,身体上的感受也每时每刻在变化,这些感受只要能被重视,就都是值得写下来的。

    重视自己的感受就是重视自己,重视自己的人不会无事可写,有时候我甚至因为有太多感受想记录而觉得文字实在是不够方便。

    我写日记很多年了,所以写日记并不是一个新的习惯,不过在上班的时候都没有那么多精力去仔细地回顾当天的经历和感受,不能保证天天都写。写日记本身也是一种能提升对生活细节观察能力的练习,写得越多就能观察得越好,观察得越好又能写得更多。

  5. 阅读

    阅读是几个习惯里做的比较不好的一个,一个原因是我比较散漫,七月同时看了很多书,导致没有几本看完的;然后我更想要进行能充分调动思维的阅读活动而不只是靠阅读来打发时间,所以对注意力要求会比较高。因为没有几本完整读完的书,这次就不写阅读方面的具体感想了。

其他方面:

  • 除了早饭外自己做了 14 次饭,点了 6 次外卖,和朋友聚餐 7 次
  • 外出 20 次,其中 10 次散步、9 次社交活动(和朋友聚餐、看电影等)

    20230707_cloud.jpg

    图1  20230707的傍晚

    20230709_cloud.jpg

    图2  天上有只大鱼

    20230723_cloud.jpg

    图3  20230723的晚霞

    20230726_moon.jpg

    图4  20230726的月亮

    20230728_sky1.jpg

    图5  20230728虹霞交映的天空

    20230728_sky2.jpg

    图6  20230728好看的天空

  • 见到了 10 个朋友,和朋友在外面吃了南洋茶室、一坐一忘、八条一号、滇宴、烤乐烧、零公里和八合里海记,滇宴是七月朋友发现的一家新开的云南菜,菜单非常厚,有不少原来在其他云南菜馆子里没怎么见过的比较新鲜的菜式

    20230701_food1.jpg

    图7  南洋茶室很嫩的海南鸡

    20230702_food1.jpg

    图8  一坐一忘的酸菜酥红腰豆

    20230705_food1.jpg

    图9  八条一号菜单上没有的神奇甘蔗尖

    20230705_food2.jpg

    图10  八条一号红酒煎猪肝

    20230705_food3.jpg

    图11  八条一号小白菜酥红腰豆

    20230708_food1.jpg

    图12  滇宴的富源酸菜煮牛干巴

    20230729_food1.jpg

    图13  零公里

  • 去了一次奥森(其实离得很近可以多去的),看到了好看的向日葵,还去了一趟地质博物馆看到很多漂亮的石头

    20230705_sunflowers.jpg

    图14  奥森向日葵

    20230705_stones_1.jpg

    图15  地质博物馆矿石(1)

    20230705_stones_2.jpg

    图16  地质博物馆矿石(2)

  • 几次看电影和逛超市,发现电影院和超市人都很多,热闹的场面仿佛回到了疫情前,虽然周围也有不少关掉的超市和小店,以及有暑假的因素在
  • 本来还想把投资和日常支出的账单整理下的,但是半年没整理了,工作量实在是有点大,预计八月能完成

游戏

steam 夏促的时候买了 16 个游戏,大部分稍微尝试了下但并没有长期玩,七月主要玩了《王国之泪》《潜水员戴夫》《杀戮尖塔》《灵魂摆渡人》《Helltaker》《Patrick's Parabox》《雨世界》这几款游戏。

潜水员戴夫

dave_the_diver.png

图17  潜水员戴夫

前半个月主要在玩《王国之泪》和《潜水员戴夫》,《王国之泪》就不用说了,主要是没啥别的事情的时候进去做做小任务和神庙解谜。《潜水员戴夫》我玩了 40 多个小时通关了,觉得作为休闲放松的游戏还是非常好的,以下是我对这个游戏的总结和评价:

  1. 潜水+经营以及推动它们的线性剧情事件,构成了这个游戏的主要内容:游戏将每天划分成三个时间段,主要以潜水为主,上午和下午潜水摸鱼、晚上开店(后面到晚上也能潜水),潜水的所得的食材作为寿司店经营的材料,寿司店一方面为潜水装备升级提供资金、另外一方面也通过各种经营事件(主题派对、烹饪对决)激励玩家去探索更多的地图、收集更多的食材,同时还有一个章节形式的线性剧情推动这两块核心内容的变化(主要涉及地图变化和装备解锁升级),后面又引出了农场和养鱼场来减轻食材供应的压力
  2. 在潜水这块内容上,主要的玩法就是在海中畅游抓捕各种鱼类、采集各种海藻、收集各种掉落在海中的材料,并随着剧情事件升级潜水装备(潜水服、氧气瓶、装载箱、3种武器),随剧情进展还会有和巨大海洋生物的 BOSS 战以及一些横版解谜过关内容
  3. 在寿司店经营这块上,主要就是食材、调味品和菜谱收集然后开店营业(需要设置当日菜品+厨师做菜+服务员上菜)赚取资金和好评并升级店铺,还可以在烹饪对决事件中以厨师的视角来做菜(可以视为若干烹饪小游戏)
  4. 在潜水+经营+剧情事件的大框架中,非常和谐地缝合了各种丰富的机制、玩法,各个玩法都很轻所以不会让人在玩的时候有什么压力(当然想肝也可以但游戏本身几乎没有为肝设置什么奖励),整个游戏流程的节奏也设计得非常好,潜水深度的增加、潜水装备的解锁升级、寿司店的升级和各种事件等等始终在设计良好的线性剧情中以一种很舒服的节奏呈现着,不会有哪一段觉得困难和无聊 —— 游戏里的各个子内容可以说都算不上有什么深度或者很优秀,但这种整体设计的能力是在是太厉害了
  5. 游戏里有很多让人会心一笑的小彩蛋,比如寿司店经营事件里挑战者吃下食物后的动画让人非常容易想起《中华小当家》的场景、某个挑战的厨师摆出拳皇经典人物的 POSE、店员吐槽说为什么晚上才营业我晚上都很累了……

杀戮尖塔

slay_the_spire.png

图18  杀戮尖塔

杀戮尖塔》是非常知名的卡牌游戏,在 rogue-like 式的爬塔过程中同时构建自己的牌组,很上头很杀时间,不过我没准备投入太多时间在这个游戏上,只是慕名已久尝试一下,这种游戏轻轻松松就能玩上几百个小时,我还想多尝试下别的游戏呢。

灵魂旅者

spiritfarer_1.png

图19  灵魂旅者

灵魂旅者》中玩家扮演一个冥界摆渡人,接纳死者的灵魂,完成他们的愿望并在这个过程中了解他们的故事,总体上来说是一个节奏很舒缓、剧情驱动的游戏。这个游戏玩起来没有任何的负担,虽然在船上也有一些模拟经营的元素 —— 收集木材矿石、种植粮食和蔬菜、为船上的灵魂制作食物,但这些事情并非游戏的主要驱动元素,并不会造成多大的压力。不过也正因为节奏舒缓,所以我投入的时间不算很多。很喜欢在暴风雨中听着叔叔演奏的音乐去抓闪电、在航行途中钓鱼看云彩和星空的流动。

spiritfarer_2.png

图20  灵魂旅者2

《Patrick's Parabox》和《Helltaker》

Patrick's Parabox》和《Helltaker》是两个在推箱子的玩法基础上加上了创新玩法的游戏。

patricks_parabox.png

图21  Patrick's Parabox

我是先玩了《Baba is you》后得知了《Patrick's Parabox》这个游戏,和《Baba is you》通过推动构成游戏规则的词语来改变游戏规则不一样,《Patrick's Parabox》是在推箱子的基础上加上了嵌套、镜像等元素,在玩的过程中还会触发递归、循环、无穷等现象,作为一个计算机出身的人来说觉得非常有意思。

helltaker.png

图22  Helltaker

玩了《Patrick's Parabox》后又了解到《Helltaker》这个免费游戏,它体量很小,总共就十来个关卡,要求玩家在有限步数的前提下到达目的地,同时还设计了陷阱(经过时需要消耗更多步数)、钥匙(获取后用来打开锁上的门)、激光(碰到必死需要推动岩石对其进行阻隔)等元素,非常的难,每一关都需要经过仔细的思考和反复的尝试,在此基础上这个游戏还设计了趣味盎然的剧情和人物以及非常有节奏感的音乐,甚至还有 BOSS 战(好难!)。这两个游戏都需要大量的思考,玩起来并不轻松,卡关的时候甚至想过去写程序来暴力求解,不过解决谜题后的成就感也是非常棒的,这就是解谜游戏的乐趣所在啦,不难的解谜游戏还叫什么解谜游戏啊!

雨世界

rain_world.png

图23  雨世界

雨世界》这个游戏据说设计了一个庞大而完整的世界观,我目前玩的不多,尚未对此有充分的体验,不过对里面的生态系统已经有所了解。一般来说在角色扮演或者冒险类游戏里,如果有敌人或者陷阱、谜题,主要都是为了服务玩家而设计的,所谓服务玩家,就是说这些元素首先要能给玩家提供乐趣,其次才是加上一些和世界观相关的点缀,换言之游戏里的各个元素主要是等待玩家与之互动、互相之间是很少交互的。《雨世界》里面的各个生物会有很多自主的行为而且不同生物之间还有复杂的交互行为,比如同一类生物的不同族群之间会互相攻击,比如有明显的一类生物捕食另外一类生物的食物链表现,还有种类丰富的不同生物。玩家扮演的也只是这个生态系统里一个弱小的生物,所以像其他游戏里那样打败敌人是不太行的,能做的只能是观察、学习、躲避、探索、捕食其他更低级弱小的生物,不像其他游戏里那样容易获得打败敌人、解开谜题的爽感。这种游戏会比较挑人,我还是比较喜欢的。

电影

202307_movies.jpg

图24  电影

看了《夺宝奇兵5:命运转盘》《长安三万里》《茶啊二中》《芭比》《封神第一部》五部电影。

《夺宝奇兵》没太多好说的,是那种老派复古的故事,人物的塑造、情节的冲突以及很多镜头,都会让人觉得很熟悉没啥新意,但是就觉得很合适,看下来我不算喜欢,但还可以吧,不觉得有什么大的槽点 —— 以现在的喜好来看当然有很多可以吐槽的但就觉得这些在这种电影里就是正常的啦。

看《长安三万里》被追光的动画制作水平惊艳到了,电影的剧本很有意思,一首首唐诗特别是最后的《将进酒》的意境渲染也非常好,但我看这个电影没有办法获得其他人那样的感动,看着电影的时候,脑子里闪过的是「贞观元年,关内饥」「开元十六年,河北饥」和疫情的三年,小人物在整个电影里几乎没怎么被提到,安史之乱沦为高适高光展示的一个背景板,乱世里普通民众的苦难完全没有,所有的内容都是为了英雄、名人形象服务的,盛世是上层人的盛世、乱世也是上层人的乱世,这样的形式所传达的文化自信对我来说太过空洞和狭隘了 —— 当然,电影就是这么设计和定位的,有问题的可能不是它,大概是满腹不合时宜的我。

《茶啊二中》是七月看的最舒服的电影,它没什么深刻的内容和过份的升华、说教,动画制作水平不能和追光比,只是认认真真地讲一个校园喜剧,讲的是师生互相达成理解这么一件事情,笑点也主要在师生、学生与学生之间的互动中呈现,大部分都是一些校园生活里细节的戏剧性或者夸张化表现,频频让人会心大笑,电影里的各个角色想必大家在看的时候也总能找到一些当年同学的影子。边看边笑边吐槽,脑海里跟着浮现出上学时一幕幕开心的场景 —— 我高中最开心的应该是复读那一年了,和同学在最后一排偷吃零食(这对作为好学生的我来说可是很刺激的),课间和好多同学一起跑出去在阳台玩(晒太阳、聊天、玩游戏)而不是像高二高三那样继续在教师狂做题,六一儿童节同学去老师办公室偷了电视电源线大家一起看《葫芦娃》……当时的班主任对我们比较宽容,同学们成绩不算好却都认真,但又不像尖子班学生那样认真到苦大仇深……我喜欢这样讲小人物小场景的故事,它们的情感简单但真实、稳固,人性的光辉在每个真实的普通人身上,我不需要神明和偶像。

《芭比》是一部毫无疑问的女性主义电影,虽然只是通过话语表面性地痛斥了父权制度、申诉了一番女性的困境,并用一层精心包装过的喜剧和童话色彩的外衣来描绘了一些理想的可能、减轻了荒诞与无力的感觉,并没有真的深入到女性困境的细节和父权社会的内部机制里,但我想它已经达到了它的目的。对于它的争论或赞誉或批评,我不想去参与,只是觉得作为一个男性也应该去了解女性困境和女性权益,这种了解不是为了获得什么其他群体的肯定、赢得什么辩论,而是为了在自己的女性亲友遇到困难时理解她们、给出一些可能有用的帮助,事实上我已经有女性亲友处在困境之中了(或者说一直在吧)。进一步的,社会规训对每一个想充分发展自己个性的人都是枷锁,建立起这样一个认识很重要。

《封神第一部》我觉得至少是一部合格的电影了,想起早年间看的《长城》还是会眼前一黑,当时想着马特达蒙应该不会拍烂片吧,我真是天真。电影的工业水平(场景、特效等)已经非常不错了,编剧不脑残整个故事还是比较自洽的,演员都很敬业,作为一个商业片来说我觉得就是合格了。故事我本来觉得魔改了很多,后来去了解了下,其实参考了不少《武王伐纣平话》里的故事,也不算凭空瞎改,不过都无所谓,封神故事里的政治斗争本来也不重要,群仙斗法才是真正的看点嘛。这样的电影没什么深度,但还是期待它们在将来能培养出一批敬业的电影从业人员,算是我对国产电影的一点祝福吧。

其他

整个七月,我的心理状况都很平稳,六月提到的一些负面情绪非常少出现了。用「不系舟」这个网名用了很多年了,大概这段时间才真正体会到一点「无能者无所求,饱食而遨游,泛若不系之舟」描述的那种闲适感,哪怕只是暂时的。承认、接受自己和自己的行为对整个社会毫无用处,是对相反方向社会规训的一种反抗和矫正,玩游戏有什么用、发朋友圈有什么用、看这些杂书有什么用、说这些有什么用……没什么用,我就是乐意,有什么用我自己说了算,关「您」屁事。

至于将来怎么办,七月完全没去想,顺其自然吧。人总是在为未来生活,今天规划明天,明天规划后天,倒不是说这种规划是错误的,只是对很多人来说,这种对未来的焦虑感完全压过了对现在的感受,我还处在矫枉过正的过程里,享受每一天吧。比较确定的是,目前仍然没有想回到职场上去,主要原因当然是存款还够、没有负债、一人吃饱全家不饿啦。

稍微有一点不太满意的是发现自己在阅读学习上做得不够好,七月本来是想多读一些书并且看看论文的,但目前想要进入比较专注的状态还是有些困难,这些困难我曾经也克服过,所以并不会毫无头绪,只是需要一些决心和持续的投入,或许八月会尝试努力一下,不过不努力也没什么关系。

2023年6月总结

2023年7月13日 08:00

六月是我正式离职后的第一个月,也是 2023 年上半年最后一个月,这个月我待在北京哪里也没有去,主要就是在玩游戏,偶尔看看书,总体来说还是比较惬意平静,不过有时也会感到无聊和焦虑。对懒散拖延的我来说,没有了工作来强制分配时间后,自己维持生活的基本秩序还是有点挑战的,就像一个骤然暴富的人,突然一下有了这么多的可自由支配时间让我有些不太适应,在察觉到自己心态和情绪上的问题后,我开始去做一些尝试,同时也决定恢复以前定期回顾总结的习惯,以便从更大的尺度上了解自己的变化、发现问题。

娱乐

2023-06-zelda.jpeg

《王国之泪》我是五月二十六开始玩的,在最开始的半个月内(从五月二十六到六月十来号)我除了吃喝拉撒几乎都在玩,到六月十号把主流程推进到只剩打最终 BOSS 后才开始每天玩得少一些了,现在游戏时间已经接近 200 小时了。

我觉得游戏最好玩最沉浸的阶段,就是在探索地图的时候。说到地图探索的体验,我会先想起《黑暗之魂1》里那精妙绝伦的地图设计,在不死教区躲过敌人坐电梯回到荒凉破败的传火祭祀场的时候、在巨人墓地远远望见恶魔遗迹的岩浆和灰烬湖林立的世界树的时候,那种惊喜和震撼是难以形容的,但《塞尔达》(包括《塞尔达:旷野之息》和《塞尔达:王国之泪》,后文的《塞尔达》指这两部游戏)的地图探索体验是不同的,它地图的各个部分互相之间没有那么强的关系,《王国之泪》在原来《旷野之息》地图的基础上加上了天空浮岛、幽暗地底以及地表与地底之间的洞窟,但地图设计的理念并没有发生大的变化,正如《旷野之息》的名字所说的一样,它提供的是一个巨大的旷野,让玩家能在其中自由的探索和玩耍,无论走到哪里皆是如此,它没有像《黑暗之魂》里传火祭祀场那样的中心 —— 监视堡垒算是大本营,但是里面所有的人都在鼓励林克外出探索。在一个巨大的旷野上探索什么、怎么玩耍呢?想像一下我们小时候在旷野上是如何玩耍的 —— 观察每一朵花和每一棵树、惊喜于草丛里扑棱棱飞出来的昆虫、拣拾漂亮的落叶和石头、把奇形怪状的石头想像成怪物或精灵……《王国之泪》里的开放世界地图就是这样的,它空旷但并不空洞,充满了细小但有趣的内容:用剑把草割开会有蚱蜢飞出或者蜥蜴爬出,抬起石头偶尔也会有昆虫从下面爬出,夜晚萤火虫在不远处轻盈飞舞,草地上出现一个突兀的有缺口的三角形石阵,一棵巨大的樱花树下有一个石像而且它面前有一个好像能放上贡品的小坑(放个苹果上去会发生什么呢?),形如兔子、散发着青白色光辉的小精灵在不远处玩耍被发现后慌忙跑向一个黑黢黢的洞窟(洞窟里会有什么呢?),荒废的村落里埋藏着宝箱打开后只是一柄锈蚀的短剑……一般的游戏都会设计一些游玩的大致流程并且在游戏里引导玩家,《塞尔达》虽然也有主流程但在引导上非常非常弱,即使是在赶着救公主的路上,玩家依然会非常非常容易被路上的各种东西吸引 —— 哎呀有棵树摘点苹果吧,波克布林(游戏里的一种怪物)在吃水果耶好可爱,这三棵树长的和周围的不太一样啊去看看有没有呀哈哈,有马群我要抓个马,这个瀑布后面有没有隐藏洞窟啊过去看看,啊有小精灵悄悄蹲下过去抓一只……像小孩子一样抱着探索一切的心态,在玩《塞尔达》时是最能体会到快乐的,如果像玩其他游戏一样只是跟着任务走反倒无趣。

至于被网友戏称为绿皮科技的左纳乌科技,确实设计得很有意思也很有可玩性,但我没有在这上面花太多时间,也就是造了个 MK2 小摩托用来赶路,至于成为海拉鲁机械工程师什么的我还是算了。探索各种零件之间的组合当然也是一种乐趣,但和在旷野上自由奔跑的感觉还是不一样的,对我来说玩左纳乌科技和《Minecraft》里的红石电路没太大差别,当年我在《Minecraft》里用红石电路造刷怪塔、自动农场、自动造石机、村民自动繁殖中心……红石电路还是图灵完备的呢。阻碍我去玩绿皮科技的原因还有一个就是每一次制作设备都是要消耗矿石的,《王国之泪》又不像《Minecraft》一样把游戏机制就建立在挖矿和制造上,不想去卡 bug 复制矿石,也没有制作视频获取更多粉丝的需求(网络上有用左纳乌装置来演奏音乐的行为,整个过程很辛苦演奏效果也不好,我不太能理解),总而言之,这个机制对我来说深度探索的麻烦大于乐趣,所以我就只是当工具在使用了。

比较后悔的是我过于匆忙地推进了主流程并且太快把所有地图点亮了,本来就是应该在这个过程中充分地享受探索乐趣的,我却东奔西走,翻山越岭到达鸟望台(游戏里用于探测一个小区域并为之建立地图的设施)点亮地图然后马不停蹄往下一个鸟望台赶过去,遗漏的很多东西只能后面因为一些别的任务出门的时候才会再顺路看一看,那时候体验就没有那么沉浸了。

游戏之外我没有太多的娱乐活动了,看了一部《蜘蛛侠:纵横宇宙》的电影,看完了《黑镜第六季》,没太多可说的。

其他朋友离职、退休后,都爱出去旅游,我好像对出去游玩没有特别大的念想,反倒是还在工作的时候会想得更多一些。

美食

  • 06.09 Tapas by Alba

    西班牙菜,第一次吃,主要是长下见识。

    • 伊比利亚火腿香肠拼盘

      2023-06-09-food-1.jpeg

    • 马德里煎蘑菇 & 烟熏三文鱼配芝麻菜

      2023-06-09-food-2.jpeg

    • 香草牛肉塔塔 & 松露红虾蘑菇塔

      2023-06-09-food-3.jpeg

    • 西班牙蒜香油滚虾

      2023-06-09-food-4.jpeg

    • 西班牙传统海鲜饭

      2023-06-09-food-5.jpeg

  • 06.10 食醴湘

    醴陵小炒肉是带汤的,学习了,味道还行但是价格有点高(相对味道和量来说)。

    2023-06-10-food.jpeg

  • 06.11 三倒拐市井火锅

    重庆火锅,不比壹圣元差呀。

    2023-06-11-food.jpeg

  • 06.17 重庆壹圣元火锅

    主要是见朋友,味道的话去太多次没什么好说的,不放图了。

  • 06.22,端午节小饭桌

    由 CFO 和大渔记出品。

    2023-06-22-food.jpeg

  • 06.29,老广酒楼,踩雷了,烧鹅、卤水还可以,其他的菜多少都有点问题,还贵,不放图了。

阅读

六月的前半个多月大部分时间都在玩游戏,到后半个月才开始去读一些书,但也没有特别专注地去读,把《卡拉马佐夫兄弟》《工作、消费主义和新穷人》《技术的本质》《蚱蜢:游戏、生命与乌托邦》《千脑智能》《如何阅读一本书》《阅读是一座随身携带的避难所》《脑与阅读》《思考:快与慢》这些书都翻了翻,没有一本看完的。

《卡拉马佐夫兄弟》一直想读的,那么长的篇幅我估计也没办法短时间内看完,里面的人名确实让人比较困扰,冗长的人名就算了,同一个人名也能有不同的翻译,阿列克塞、阿辽沙、阿辽什卡是同一个人,德米特里、米特里、米嘉、米剑卡也是同一个人,不是太能理解……

《工作、消费主义和心穷人》从工作伦理和消费主义的角度反思社会现状,读这本书主要也是因为对现代社会工作感到很困惑,未必能从这本书里得到什么答案,就是开拓一下思路吧。

《技术的本质》尝试对技术给一个明确而严密的定义,探讨技术的起源、演变背后的机制以及技术对社会的影响,看起来挺费劲的,不过我觉得我的有些问题或许能从这本书里得到启发,不一定会读完这本书,但会有针对性地翻一翻感兴趣的章节。

《蚱蜢:游戏、生命与乌托邦》是去年朋友推荐的书,它是一本宣称玩游戏是人类的终极理想的严肃哲学书籍,当然这里的玩游戏在书里是有严密的定义的。虽然这本书尝试以对话体这种比较轻松的形式来写,但是要理解这本书想表达的内容还是需要充分调动思维的。读这本书,有比较大的一部分因素是因为我喜欢游戏。

《如何阅读一本书》我非常喜欢,要是能早点读这本书就好了,它对阅读做了一些分类,并针对「增进理解」的阅读提出了从理念到具体方法的一系列指导,我在之前写《至少还有书》的读后感的时候说「没有人管束我的另外一面是也没有人引导我」,我想,这本书就是我曾经缺失的引导。

《阅读是一座随身携带的避难所》是毛姆关于书和作家的随笔,读起来很轻松的,我就有兴趣了随时翻一翻,这本书也不用太按顺序读,比如我就先把里面讲陀思妥耶夫斯基和《卡拉马佐夫兄弟》的那章读了。我在微信读书上有一个叫做「书的书」的分组专门放一些讲书的书,作为找其他想读的书的一个入口,比看豆瓣评论什么的会更有感觉一些。

读《千脑智能》《脑与阅读》《思考:快与慢》这三本书是出于对人脑认知能力机制的好奇,事实上这个方面的书我收藏了一大堆的,想做主题阅读的,但发现自己并没有形成比较体系的主题阅读的方法和过程,先随便看看吧。《千脑智能》目前看得多点,里面提出的新皮质认知智能模型很有意思,如果可以验证的话对人工智能的发展会起到很大的帮助,但不知道是不是出于易读性而对细节含糊带过还是翻译的问题,新皮质认知智能模型的具体细节我不是太能理解,如果整本书读下来还看不明白,我可能会去翻翻作者的论文。

其他

不像其他人,不工作后我并没有变得非常开心,大部分的时间心情是比较平静轻松的,但要说很开心可能谈不上。认真想想的话,上学时忙于学业、工作时忙于工作,我并没有完全自主地安排自己生活的经验,如果不想在既定的框架里生活该如何做,我其实是缺乏回答这样一个问题的理论和经验的,自由是有代价的,我觉得不是每个人都能承受得住。

六月中旬开始,我频繁地被「得做点正事」的焦虑和「什么也不想做」的逆反心理之间的冲突所影响,情绪经常低沉,甚至一些要解决的生活琐事明明并不困难也不复杂,也觉得要打起十分的勇气才能去面对,那种「得做点正事」的惯性和对此的厌恶,我认为不是出去四处转转吃点好吃的就能解决的,这也是我没有急匆匆出去游玩的一个原因,我希望能真正以轻松平静的心态去面对美景和美食(当然,这可能也只是一个未必能达到的理想态),而不是靠美景和美食来「疗愈自己」,没有那么容易疗愈的,因为很多心理和行为的惯性是过去三十多年的生活里在目前这个社会的规范中逐步养成的,与其说伤病,不如说是畸形,这些就是我在《辞职了》那篇文章里说的「真正重要的问题」。我有过份地向内探寻的坏毛病,什么伤病、畸形说的可能有些矫情,简短些总结的话,就是在离职后的第一个月里有时出现了懒、拖延、迷茫、焦虑、空虚、无聊等一些常见的负面情绪和行为,问题并不大(或者说这些问题本来就一直存在),不过收集 badcase 对系统进行迭代优化已经是本能了,所以会对这些多注意一些。

察觉到情绪不太好后,我迅速地建立了应对机制,其实也没什么特别的,从六月二十日开始,开始每天晨练、阅读、冥想、吃早饭、不那么晚(八九点)起床,规律的、有序的生活很快就让我状态大大改善。写这篇总结的时候已经是七月六日,目前为止这个机制运作良好,坏情绪已经较少出现,平静、满足常有。我并不那么追求快乐那种让人兴奋的感受,能够获得平静就已经很好了。

LLM Prompt Engineering 实践:记忆(1)

2023年6月21日 08:00

本文是《LLM Prompt Engineering 实践》系列的第三篇文章,系列文章如下:

  1. LLM Prompt Engineering 实践:序 · ZMonster's Blog
  2. LLM Prompt Engineering 实践:原型 · ZMonster's Blog
  3. LLM Prompt Engineering 实践:记忆(1) · ZMonster's Blog

之前我提出了一些问题:

  1. ChatMessageHistory 是基于内存的历史消息记录方法,一旦退出历史对话消息将全部丢失
  2. 当讨论的话题更早之前聊过时,选择最近历史消息的做法无法利用之前的讨论
  3. 固定设置输出最大 token 数量的做法为了照顾长输出的情况,会导致短输出情况下无法利用更多的历史消息
  4. 一个全局设置的温度参数会影响机器人处理不同任务的表现,比如说像翻译、分类、事实性问答这些任务并不需要随机性,而写作、创意生成则有一定的随机性会更好一些
  5. 在当前这种模式下,将机器人属性设置放在最开始的做法无法保证设定的一致性和连续性,用户总是能通过有意构造的输入来突破这种设置

其中 1/2/3 涉及到记忆机制的问题,对这几个问题都可以独立地去思考和尝试解决,但在此之前,还是有必要再对这几个问题做更多的探讨。

我在之前 xorius 的开发日志里第一次提出使用历史对话记录时是这么说的:这段代码不是对话的模式,没有考虑聊天历史,如果要做聊天的话,需要维护一个聊天历史。

那么问题来了,为什么在响应当前用户输入的时候,要去考虑聊天历史呢?这种做法其实就是假设聊天历史对响应当前的用户输入有帮助,那么:

  1. 这个假设会一直成立吗?
  2. 什么情况下这个假设会成立?此时历史记忆和当前用户输入有哪些可能的相关关系?
  3. 假设成立的情况下,使用最近 K 轮历史记忆的做法,K 会有一个模型计算力能承受的上限吗?
  4. 假设成立的情况下,使用最近 K 轮历史记忆的做法,相关的内容在这 K 轮历史记忆中的占比会有多大?
  5. 假设成立的情况下,使用最近 K 轮历史记忆的做法,其中的无关内容会带来负面影响吗?如果会的话可能带来的负面影响有哪些?

对第 1 个问题,既然是假设,那么它当然不会一直成立,否则它就是真理而不是假设了,而之前 xorius 的实现里是在这一假设下实现了回答时对历史记忆的利用 —— 即无论何时都会尝试把历史记忆用上,因而也会自然而然地带来一定的副作用: 在响应不需要历史记忆的用户输入时,可能因为强制使用历史记忆而造成错误的回答。

对第 2 个问题,个人认为可能有以下几种情况:

  1. 当前输入提及了历史对话中的某些内容,比如
    • 使用代词提及历史对话中聊过的某个或某几个对象,这种情况称之为指代

      「它」指代「语言模型」

      You: 语言模型是什么
      
      Xorius:
      语言模型是一种计算机程序,用于预测自然语言文本中下一个单词或字符的概率分布。它是自然语言处理中的一个重要组成部分,可以用于许多任务,如语音识别、机器翻译、文本生成和自动摘要等。语言模型通常基于统计学方法或神经网络方法,通过学习大量的文本数据来建立模型,并使用该模型来预测下一个单词或字符的概率分布。在实际应用中,语言模型可以帮助计算机更好地理解和生成自然语言,从而提高自然语言处理的效果和质量。
      
      ------------------- 上方为历史对话 -------------------
      
      You: 它有智能吗
      
      Xorius:
      语言模型本身并没有智能,它只是一个程序,能够根据已有的文本数据预测下一个单词或字符的概率分布。然而,通过不断地训练和优化,语言模型可以逐渐提高其预测的准确性和效率,从而更好地模拟人类的语言习惯。此外,语言模型也可以与其他人工智能技术结合使用,如机器学习、深度学习、自然语言处理等,从而实现更加智能化的应用。因此,虽然语言模型本身没有智能,但它可以为人工智能的发展提供重要的支持和基础。
      
    • 提及历史对话中聊过的某个或某几个对象,但以省略的形式,这种情况称之为零指代

      「语言模型在语音识别中怎么用」省略了「语言模型」

      You: 语言模型是什么
      
      Xorius:
      语言模型是一种用于自然语言处理的技术,它可以对一段文本的语言结构进行建模,从而计算出该文本的概率。语可以用于很多应用,例如语音识别、机器翻译、自然语言生成等。在语音识别中,语言模型可以帮助识别出最有可能的语音转录结果;在机器翻译中,语言模型可以帮助翻译系统选择最合适的翻译结果;在自然语言生成中,语言模型可以帮助生成自然流畅的语言表达。常见的语言模型包括n-gram模型、神经网络语言模型等。
      
      ------------------- 上方为历史对话 -------------------
      
      You: 在语音识别中怎么用
      
      Xorius:
      在语音识别中,语言模型主要用于解决识别歧义的问题。当语音识别系统对一段语音进行转录时,可能会遇到多个候选结果,这些结果在音频信号上看起来很相似,但在语言表达上却有很大差异。这时,语言模型可以帮助系统选择最有可能的结果。
      
      具体来说,语言模型会对每个候选结果进行打分,计算出其在语言表达上的概率。然后,系统会选择得分最高的结果作为最终的转录结果。这样,语言模型可以帮助系统更准确地识别语音,提高识别准确率。
      
      在语音识别中,常用的语言模型包括n-gram模型和神经网络语言模型。n-gram模型是一种基于统计的模型,它通过计算文本中相邻的n个词出现的概率来建模。神经网络语言模型则是一种基于神经网络的模型,它通训练神经网络来预测下一个词的概率。这些模型都可以用于语音识别中的语言模型建模。
      
    • 使用代词提及历史对话中的比词语粒度更大的对象,比如说事件、观点、句子甚至更大段的话

      「最后一段」指代对历史对话回答中结尾部分的「他们成功地激活了这个装置……也激励了更多人类探索宇宙的冒险精神。」这段话

      You: 写一个木卫二上的冒险故事
      
      Xorius:
      在木卫二的表面,有一个被称为“红海”的巨大火山口。这个火山口的深度超过了地球上最深的海洋,被认为是太阳系中最深的地方之一。有一天,一组勇敢的宇航员决定探索这个神秘的地方。
      
      他们穿上了特制的宇航服,搭乘着一艘小型的探测器,开始了他们的冒险之旅。他们在探测器上看到了一些奇怪的能量信号,这些信号似乎来自深处的地下洞穴。
      
      他们决定跟随这些信号,进入了一个巨大的洞穴系统。在洞穴里,他们发现了一些奇怪的生物,这些生物似乎是在这个深处的环境中生存了很长时间。
      
      随着他们深入洞穴,他们发现了一些神秘的符号和图案,这些符号似乎是来自一个古老的文明。他们开始研究这些符号,试图理解这个文明的历史和目的。
      
      在他们的探险中,他们遇到了一些危险,包括地震和洪水。但他们始终坚持不懈,终于到达了洞穴的深处。在那里,他们发现了一个神秘的装置,这个装置似乎是一个能量生成器,可以为整个木卫二提供能源。
      
      他们成功地激活了这个装置,并将它带回了地球。这个发现改变了人类对木卫二的认识,也为人类提供了一种新的能源来源。这个冒险故事成为了人类历史上的一个重要事件,也激励了更多人类探索宇宙的冒险精神。
      
      ------------------- 上方为历史对话 -------------------
      
      You: 把最后一段翻译成英文
      
      Xorius: "They successfully activated the device and brought it back to Earth. This discovery changed humanity's understanding of Europa and provided a new source of energy for humanity. This adventure story became an important event in human history and inspired more people to explore the adventure spirit of the universe."
      
    • 提及历史对话中的比词语粒度更大的对象,比如说事件、观点、句子甚至更大段的话,但以省略的形式

      「压缩一下」省略了宾语,指的是历史对话里的整段回答「在遥远的未来……寻找未知的世界和可能性」

      You: 写一个火星流浪的故事
      
      Xorius:
      在遥远的未来,人类已经开始探索太空,其中包括火星。一名名叫艾米的宇航员在一次任务中不幸与同事失联,最终被遗弃在了火星上。
      
      艾米意识到自己已经被遗弃在了这个荒凉的星球上,没有任何人类的生命迹象。她开始寻找食物和水源,但很快发现火星上的环境非常恶劣,几乎没有任何生命存在的可能。
      
      在接下来的几个月里,艾米不断地寻找食物和水源,同时也努力修复她的通讯设备,希望能够联系到地球上的人类。但是,她的努力都没有任何回应。
      
      在孤独和绝望的日子里,艾米开始思考自己的人生和存在的意义。她开始重新审视自己的价值观和信仰,并逐渐接受了自己可能会永远孤独地生活在这个星球上的现实。
      
      然而,就在艾米准备放弃的时候,她收到了一条来自地球的信号。原来,地球上的科学家们一直在寻找她,并且已经派出了一支救援队伍来到火星上救援她。
      
      在救援队伍的帮助下,艾米最终成功返回地球,并成为了一名备受尊敬的宇航员。她的经历也激励了更多的人类探索太空,寻找未知的世界和可能性。
      
      ------------------- 上方为历史对话 -------------------
      
      You: 压缩一下,用更短的文字
      
      Xorius:
      一名宇航员在火星上被遗弃,孤独地寻找食物和水源,修复通讯设备,思考人生意义。最终收到地球救援信号,成功返回地球,成为备受尊敬的宇航员,激励更多人类探索太空。
      
  2. 历史记忆中的内容可以作为回答当前输入的知识,比如说
    • 历史记忆中提供了当前输入的某些知识的定义或详细说明,回答当前输入时正确使用历史记忆中提供的知识可以避免 AI 胡编乱造
    • 历史记忆中描述了用户(或 AI)的喜好、三观或其他个性化(或者说人格化?)的属性描述,并且当前输入是以这些个性化信息为背景的,那么正确地使用相关的历史记忆可以使得回答时表现出来的个性与历史记忆中的保持一致性、连续性
    • ……
  3. 上述两种情况的混合

在多轮对话的研究里,对历史记忆和当前输入的相关关系应该还会有更全面、更细致的分类,暂时不想去翻相关的研究资料了,等需要做更精细的处理的时候可能会去看看,现在主要是做一个大致的梳理,知道模型在利用聊天历史时存在不同情况即可。

在第 2 个问题的回答的基础上,可以对第 3 个问题做一定的回答

  • 如果当前输入和历史记忆的关系是第 1 种情况(当前输入提及了历史对话中的某些内容),可以认为这种情况下要用到的历史记忆都会是比较近的,也就是 K 不会太大,我个人认为是可以让模型直接处理的
  • 如果当前输入和历史记忆的关系是第 2 种情况(历史记忆中的内容可以作为回答当前输入的知识),那么相关的历史记忆未必是在最近,它完全可以是长期记忆里的内容,比如说我让它推荐书籍的时候希望它可以记起来一年前我说过我喜欢看科幻小说进而推荐一些最新的科幻小说,这种情况下这个 K 可能会非常非常大,个人认为这种情况下使用最近 K 轮历史记忆的做法将会超出模型的计算能力上限
  • 如果当前输入和历史记忆的关系是第 3 种情况(第 1 种情况和第 2 种情况的混合),那么同第 2 种情况

对于第 4 个问题,如果精力允许,或许可以去分析一下一些公开的多轮对话数据集来给出一个相对量化的结果,不过目前我并不想去做这个事情,提出这个问题,其实只是想说明一点 —— 即使在聊天历史对响应当前用户输入有帮助这个假设成立,并且有帮助的信息就在最近 K 轮历史记忆中的时候,也可能不是所有 K 轮历史记忆都是有帮助的。

第 5 个问题则是在第 4 个问题上更进一步思考,如果最近 K 轮历史记忆中有无关内容,那么它们一定是会带来负面影响的,它们可能会摊薄当前输入中 token 和历史记忆中有用内容的 attention 权重,进而导致回可能偏离预期。这种负面影响的大小一定程度上取决于模型本身的能力,很强的模型(比如 GPT-4)可能不会表现得很明显。

综上,使用最近 K 轮历史记忆的做法其实是会有一些问题的,但是这种方法是最容易实现的、最直观可理解的,并且在模型较强的时候效果也不错,加上现在也没有公认的更好的方案,所以大家普遍都这么做。

方法有缺陷是正常的,事实上也不存在哪个方法是完美的、没有任何问题的,但这个方法最大的问题不在于其假设,而在于它没有设计对应的错误应对或者说优化迭代的措施,一旦这个机制工作不正常,就只能期待模型自身的改进或者用户自己能够掌握所谓的 Prompt 编写技巧来再次尝试了。如果能对这些问题心知肚明并抱有适当的预期,那么使用这个方法倒也没什么问题,但如果使用了这个方法出了问题后就只会调侃 AI 弱智,那就没什么意思了。

我辞职了

2023年5月23日 08:00

4 月 21 日,在地铁上用微信发了消息公司领导,告诉他们我不想干了,感觉心情马上就轻快了起来。到公司后领导找我面谈了几句,不过我只是更坚定地表明了自己要跑路的态度,所以领导也没说什么挽留的话。

4 月 23 日,正式给公司管理层发了辞职信,内容是让 ChatGPT 帮忙写的,懒得自己动这个脑筋,CEO 说节后回北京后想找我聊聊,虽然不知道他想给我画什么饼,不过感谢他额外给我一个星期的摸鱼时间,节前一周我就有一搭没一搭地写交接文档,做做我还感兴趣的一些零碎工作,过得是很惬意的,没想到由于 CEO 来京时间一再往后推,我又继续过了三周这样的生活。

5 月 23 日,CEO 终于来了公司,聊了一个小时,主要是 CEO 在畅谈商业计划和愿景,我看得出来他确实对未来的可能性感到很激动也对自己的判断感到很自信,不过这都和我没关系了。聊完后和 CFO 也聊了下,定了离职日期,随后就提前拿到了离职证明。如果没有什么别的事情,后面我应该就不会再去公司了,虽然还没到离职日期,但离职这个事情就算告一段落了。

在这个点提离职,一个出发点是公司的业务方向要转向一个我完全不感兴趣的领域了,虽然老板宏伟的商业愿景听起来似乎很合理,但我留下无疑只能去做脏活累活,脏活累活我已经做够了。做不喜欢甚至讨厌的事情的经历我已经有过几次,那种痛苦短时间内我不想再经历一次了,就赶在新业务开展前提了离职,也能走得清清爽爽;第二个出发点就是 ChatGPT 和 Stable Diffusion 带来的新一波 AI 浪潮里有大量事情可以做,辞职后不用担心无聊,而且我想做的一些探索性的事情是得不到公司支持的,索性就离开自己玩玩吧。

想辞职或者想不工作的念头很早就有,毕竟我不是那种能享受工作的人。以前还能通过工作提升技艺、开阔眼界,这几年连这些最基本的乐趣也没有了,作为一个高考志愿填计算机只是因为高中经常去网吧玩耍觉得计算机好玩的人,这是不能忍受的。另外一方面,三年下来强烈地察觉到自己感受生活的能力以及清晰表达自己感受的能力的欠缺,这些并不是文艺青年才配拥有的奢侈品,而是一个健康的普通人对抗虚无和绝望的必需品,我希望能在这些问题上有所改进,然而以目前的状况而言,工作只会加重我这些问题。由于并不清楚如何解决真正重要的问题或者说害怕去面对那些问题,最近几年更多是在把工作当作逃避问题、逃避不确定性的一个借口,不管是什么问题,都可以辩解「我知道这些问题很重要但是工作这么忙我也没有办法啊」然后心安理得地继续拖延 —— 这并不可耻,要不要去面对问题只是个人选择而已,我决定去试一试。

循规蹈矩活了三十多年,反思并尝试探索新的道路未必就能有什么好下场,不过我没有什么自我要寻找,也没有想要彻底地和过去的道路决裂,如无意外,我应该还是会靠自己的专业能力去生活。接下来的一小段时间,大概会尝试去过一过「没有计划」的生活,尽力、尽情地做一些自己想做、能做的事情,等待并拥抱变化的自然发生,不再自欺欺人地假装做了计划后未来的不确定性就会收束。

虽然对于辞职后的生活,我暂时是没有什么明确计划的,但会有一些想做的事情,这些事情是会让我觉得开心的、有价值的事情,我应该都会去做,但并不在乎能不能达到完美的结果,也不在意何时达到什么程度,也许会做到一半就被别的东西吸引或者干脆厌倦就彻底放弃,也许会花费超出最初预期的精力,一切皆有可能,还是那句话,我不想做计划。

首先是想要生活地健康、松弛一些,保证足够的睡眠,有一定的运动量,有充足的阅读、娱乐时间,通过散步、外出游玩等方式充分地与外界保持联系,或许也可以不定期拜访一些朋友。从我的经验来看,习惯可以在两周左右内养成,之后就可以不需要太多意志力地自动化了,所以生活作息我是有信心比较快地纠正过来的,问题比较大的可能是松弛感的建立,我目前并没有什么好的想法,只是觉得多玩、多感受、多尝试一些不带具体目标的事情可能会有帮助。

然后是想要在编程和 AI 方面做一些探索和尝试,短期应该会先从语言模型入手做一些事情,再之后就没有什么具体想法了,可能会随缘玩一玩其他东西,比如学个新语言、开发 RSS 周边工具、写 Emacs 插件……不过语言模型应该就够玩很久了,准备先写一个项目来讨论目前语言模型应用要面对的问题及处理方法。毕业十年了,我仍然喜欢我的专业,仍然被铁路模型技术俱乐部、家酿计算机俱乐部等行业先驱们的精神和故事所激励,我最有创造力的一些工作(虽然也算不上多有创造力)都是在无人管束时以玩耍的心态所做出来的,我想继续去玩,在玩耍中创造,在创造中玩耍,而不是像一台机器一样被输入一些无聊的商业目标来去产出一堆垃圾。

至于打算休息到什么时候再回去工作,我只能说没有计划,没有计划什么时候要回去工作,也没有计划一直不工作,就随缘吧。

LLM Prompt Engineering 实践:原型

2023年5月13日 08:00

本文是《LLM Prompt Engineering 实践》系列的第二篇文章,系列文章如下:

  1. LLM Prompt Engineering 实践:序 · ZMonster's Blog
  2. LLM Prompt Engineering 实践:原型 · ZMonster's Blog
  3. LLM Prompt Engineering 实践:记忆(1) · ZMonster's Blog

在 ChatGPT 的帮助下,给项目起了 xorius 这么一个名字,这个单词实际上并不存在,所以含义完全是由我来定义的,不过作为一个随时可能会鸽掉的项目,就先不说我给它赋予的意义了,以后做大做强再来说会比较合适一点。

起好名字后,我准备先实现一个最最简单的原型出来,并在过程中对一些问题进行讨论。后文所有内容都是我的开发日志的整理汇总,之后的文章基本上也会采用这个模式。

为了实现原型,有两个选择,一是直接去调用 OpenAI 的官方接口或 SDK,二是选择 langchain 之类的第三方封装,出于方便考虑,我选择了 langchain。

首先,无论是直接调用官方接口、SDK,还是使用 langchain,国内都是需要通过代理才能访问的:

import openai
openai.proxy = 'http://localhost:8080'

设置好代理后,用 langchain 就可以简单地调用 OpenAI 的接口来获得回答,比如说这样:

from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage

user_message = HumanMessage(content='将这句话翻译成中文: “to be or not to be”')

# 安全考虑这里把真实 API KEY 隐藏了,后文不再强调
llm = ChatOpenAI(temperature=0, openai_api_key='sk-xxxxxxxxxxxxxxxx')
answer = llm([user_message])
print(answer.content)

上面的代码没有指定要使用的模型,默认会使用 gpt-3.5-turbo,运行后得到了这样的结果

生存还是毁灭

当然,这段代码不是对话的模式,没有考虑聊天历史,如果要做聊天的话,需要维护一个聊天历史

from langchain.memory import ChatMessageHistory

memory = ChatMessageHistory()

之后每次的用户输入和得到的回答都可以按顺序添加到这个 memory 里面,比如说前面的 user_message 和 answer,都可以加到里面去

memory.add_user_message(user_message.content)
memory.add_ai_message(answer.content)

通过 memory.messages 可以得到历史记录

print(memory.messages)

结果

[HumanMessage(content='将这句话翻译成中文: “to be or not to be”', additional_kwargs={}, example=False),
 AIMessage(content='"生存还是毁灭"', additional_kwargs={}, example=False)]

然后在下一轮对话时使用它

user_message = HumanMessage(content='再翻译成日语')
answer = llm(memory.messages + [user_message])
print('Answer:', answer.content)

memory.add_user_message(user_message.content)
memory.add_ai_message(answer.content)
print('History')
print(memory.messages)

结果

Answer: "生きるか死ぬか"
History:
[HumanMessage(content='将这句话翻译成中文: “to be or not to be”', additional_kwargs={}, example=False),
 AIMessage(content='"生存还是毁灭"', additional_kwargs={}, example=False),
 HumanMessage(content='再翻译成日语', additional_kwargs={}, example=False),
 AIMessage(content='"生きるか死ぬか"', additional_kwargs={}, example=False)]

综合上面的代码,只要写一个循环,就能做持续的对话了

from langchain.chat_models import ChatOpenAI
from langchain.memory import ChatMessageHistory

memory = ChatMessageHistory()
llm = ChatOpenAI(temperature=0, openai_api_key='sk-xxxxxxxxxxxxxxxx')
while True:
    user_message = input('You: ').strip()
    if not user_message:
        continue

    if user_message.lower() in ('quit', 'exit'):
        break

    memory.add_user_message(user_message)
    answer = llm(memory.messages)
    print('BOT:', answer.content)
    memory.add_ai_message(answer.content)

不过上面这段代码是有问题的,因为实质上我是每次都把历史消息和当前输入拼接起来输入给 OpenAI 的接口,而 OpenAI 的接口对处理的文本总长度是有限制的,gpt-3.5-turbo 最大长度是 4096 个 token,gpt-4 最大支持 32768 个 token(这个 token 是语言模型里对文本切分后的最小单元,先不用细究),像上面这样一直把 user_message 和 answer 往 memory 里面加,那么只要对话轮次够多一定会超过这个限制的,超过了就会报错,所以必须要限制一下。需要注意的是,这个最大文本长度,是输入和输出加在一起算的,比如说如果用 gpt-3.5-turbo 然后输入就已经 4000 token 了,那么只能输出最长 96 个 token 的结果。

综上,为了保证不超出接口长度限制,需要只取 memory.messages 中的一部分,假设取的 memory.messages 中的这部分的总 token 数 为 L1,我们还要保证输出结果的总 token 数能达到 L2,那么必须保证 L1 + L2 < 4096,L2 可以在初始化 ChatOpenAI 的时候通过参数 max_tokens 来设置,默认没设置就是无限,那为了更好地计算 L1,需要显式设置一下它,比如说设置为 1024:

max_tokens = 1024
llm = ChatOpenAI(
    temperature=0,
    openai_api_key='sk-xxxxxxxxxxxxxxxx',
    max_tokens=max_tokens
)

在 L2=1024 的情况下,可以得到 L1 < 4096 - 1024 = 3072。接下来的问题就是按照这个最大 token 数来对 memory.messages 进行选择了,要做这个的话,首先要能计算出给定文本的 token 数量,语言模型里的 token 不完全等于字词,OpenAI 提供了一个在线工具展示把文本变成 token 的效果:

  • 英文基本上是一个词一个 token,有时会把一个词拆成几个 token,比如下面这个例子里 Tannhäuser 这个词就被拆成了 T/ann/h/ä/user 五个 token,但不管怎么说按字数算,英文最后的 token 数是小于字数的,下面这个例子的文本总字数是 230,token 数才 57

    gpt3_tokens_example_english.png

  • 中文的话一个字就可能拆成多个 token,这些 token 大都是不可理解的,所以总体来说 token 数会大于字数,下面这个例子里的文本字数是 94,token 数是 191

    gpt3_tokens_example_chinese.png

    注意,上面中文的例子是 GPT3 的编码器的效果,换成 gpt-3.5-turbo 的编码器后,中文的 token 数下降到了 124。

OpenAI 提供了 tiktoken 这个 python 库来做文本到 token 的编码转换

from tiktoken.model import encoding_for_model

encoder = encoding_for_model('gpt-3.5-turbo')

由于 gpt-3.5-turbo 模型使用的 Chat Completion 接口的输入不是普通的文本格式,计算输入的 token 数不能简单把用户输入和接口响应结果文本的 token 数加起来,OpenAI 自己给出了计算方法:

import tiktoken

def num_tokens_from_messages(messages, model="gpt-3.5-turbo-0301"):
    """Returns the number of tokens used by a list of messages."""
    try:
        encoding = tiktoken.encoding_for_model(model)
    except KeyError:
        encoding = tiktoken.get_encoding("cl100k_base")

    if model == "gpt-3.5-turbo-0301":  # note: future models may deviate from this
        num_tokens = 0
        for message in messages:
            num_tokens += 4  # every message follows <im_start>{role/name}\n{content}<im_end>\n
            for key, value in message.items():
                num_tokens += len(encoding.encode(value))
                if key == "name":  # if there's a name, the role is omitted
                    num_tokens += -1  # role is always required and always 1 token

        return num_tokens
    else:
        raise NotImplementedError(
            f"num_tokens_from_messages() is not presently implemented for model {model}.\n"
            "See https://github.com/openai/openai-python/blob/main/chatml.md "
            "for information on how messages are converted to tokens."
        )

注:为了方便计算单条消息的长度,我对 num_tokens_from_messages 做了一定的修改,删掉了 num_tokens += 2 这一行。

注2:经测试,OpenAI 提供的这个函数,计算出来的 token 数比实际的少 1 个。

基于这个函数可以来实现聊天历史的选择了,这里只简单实现一个取最近聊天历史的

from langchain.schema import AIMessage, HumanMessage

def select_messages(messages, max_total_tokens=4096, max_output_tokens=1024):
    tokens_num = 0
    selected = []
    for message in messages[::-1]:
        role = 'system'
        if isinstance(message, AIMessage):
            role = 'assistant'
        elif isinstance(message, HumanMessage):
            role = 'user'

        cur_token_num = num_tokens_from_messages([{'role': role, 'content': message.content}])
        if tokens_num + cur_token_num + 2 + max_output_tokens > max_total_tokens:
            break

        selected.append(message)
        tokens_num += cur_token_num

    selected = selected[::-1]
    if isinstance(selected[0], AIMessage): # 确保第一条是用户消息
        selected = selected[1:]

    if not selected:            # 假设 messages 里最后一条是当前用户输入
        selected = message[-1]

    return selected

那么前面的持续对话代码可以改造成这个样子了

from langchain.chat_models import ChatOpenAI
from langchain.memory import ChatMessageHistory

memory = ChatMessageHistory()

max_output_tokens, max_total_tokens = 1024, 4096
llm = ChatOpenAI(
    temperature=0,
    openai_api_key='sk-xxxxxxxxxxxxxxxx',
    max_tokens=max_output_tokens,
)
while True:
    user_message = input('You: ').strip()
    if not user_message:
        continue

    if user_message.lower() in ('quit', 'exit'):
        break

    memory.add_user_message(user_message)
    messages = select_messages(
        memory.messages,
        max_total_tokens=max_total_tokens,
        max_output_tokens=max_output_tokens
    )
    print(f"selected messages: {messages}")
    answer = llm(messages)
    print('BOT:', answer.content)
    memory.add_ai_message(answer.content)

把 max_output_tokens 设置得更大一些会选择更少的历史,比如说

  • max_output_tokens=200, max_total_tokens=4096 时,输入的最大 token 数可以到 3896,所以可以选到尽可能多的历史消息

    You: 将这句话翻译成中文: “to be or not to be”
    selected messages: [HumanMessage(content='将这句话翻译成中文: “to be or not to be”', additional_kwargs={}, example=False)]
    BOT: "生存还是毁灭"
    You: 再翻译成日语
    selected messages: [HumanMessage(content='将这句话翻译成中文: “to be or not to be”', additional_kwargs={}, example=False), AIMessage(content='"生存还是毁灭"', additional_kwargs={}, example=False), HumanMessage(content='再翻译成日语', additional_kwargs={}, example=False)]
    BOT: "生きるか死ぬか"
    
  • max_output_tokens=4050, max_total_tokens=4096,输入的最大 token 数只有 46,第二轮的时候没法使用前一轮的历史消息

    You: 将这句话翻译成中文: “to be or not to be”
    selected messages: [HumanMessage(content='将这句话翻译成中文: “to be or not to be”', additional_kwargs={}, example=False)]
    BOT: "生存还是毁灭"
    You: 再翻译成日语
    selected messages: [HumanMessage(content='再翻译成日语', additional_kwargs={}, example=False)]
    BOT: もう一度日本語に翻訳してください。
    

实现了聊天历史的选择后,一个最基本的原型其实就好了,进一步地,还可以为这个对话机器人做一些属性设定,比如说叫什么名字、什么性格之类的,具体的做法就是在每次调用语言模型时,除了聊天历史和当前输入,再额外输入一个描述这个机器人属性的 prompt,在 OpenAI 的接口里要求这条 prompt 的 role 设置为 system,用 langchain 的话直接用 SystemMessage 就好,我这里简单把这个 SystemMessage 放在输入的最前面,把代码改造成了这个样子:

from langchain.chat_models import ChatOpenAI
from langchain.memory import ChatMessageHistory
from langchain.schema import SystemMessage

memory = ChatMessageHistory()
system_message = SystemMessage(
    content=(
        "You are an AI assistant. Your name is xorius. "
        "You can discuss any ideas and topics with your users, "
        "and you will help your users solve their problems as much as you can."
    ),
)
max_total_tokens = 4096 - num_tokens_from_messages([{'role': 'system', 'content': system_message.content}])
max_output_tokens = 1024
llm = ChatOpenAI(
    temperature=0,
    openai_api_key='sk-xxxxxxxxxxxxxxxx',
    max_tokens=max_output_tokens,
)
while True:
    user_message = input('You: ').strip()
    if not user_message:
        continue

    if user_message.lower() in ('quit', 'exit'):
        break

    memory.add_user_message(user_message)
    messages = select_messages(
        memory.messages,
        max_total_tokens=max_total_tokens,
        max_output_tokens=max_output_tokens
    )
    print(f"selected messages: {messages}")
    answer = llm([system_message] + messages)
    print('BOT:', answer.content)
    memory.add_ai_message(answer.content)

这样让它介绍自己就会按照设定来回答了

You: 介绍一下你自己
selected messages: [HumanMessage(content='介绍一下你自己', additional_kwargs={}, example=False)]
BOT: 你好,我是Xorius,一名人工智能助手。我可以与您讨论各种话题,帮助您解决问题。我可以提供有关各种主题的信息,例如科技、文化、历史、健康、旅游等等。我还可以执行各种任务,例如设置提醒、发送电子邮件、搜索信息等等。请告诉我您需要什么帮助,我会尽力为您提供支持。

如果不加这个 system_message,同样的输入得到的回答里它会强调自己由 OpenAI 开发,如下所示:

You: 介绍一下你自己
selected messages: [HumanMessage(content='介绍一下你自己', additional_kwargs={}, example=False)]
BOT: 我是一名AI语言模型,由OpenAI开发。我可以进行自然语言处理和生成,帮助用户回答问题、翻译、写作等。我没有实体,只存在于计算机中,但我可以通过文字和语音与人类进行交互。我不会感到疲倦或犯错,但我也没有情感和意识。我只是一个程序,但我希望能够为人类提供帮助和便利。

最后,我将上述代码做一些调整后实现成了项目里的一个命令行工具,代码见 https://github.com/monsternlp/xorius ,也发布到了 pypi,执行 pip install xorius 即可安装,安装后即可在命令行执行 xorius-cli 来进入对话。

xorius_cli.png

命令行参数说明如下:

  • --api-key: 设置 OpenAI 的 API 密钥
  • --temperature: 设置温度参数,用于控制生成结果时的随性,0 的话不随机每次都生成概率最大的结果,默认 0.7
  • --max-tokens: 设置生成结果允许的最大 token 数,默认 512
  • --proxy: 设置 http 代理,主要针对国内网络环境

原型是实现了,但是在这个原型上,能看到一些直接的问题

  1. ChatMessageHistory 是基于内存的历史消息记录方法,一旦退出历史对话消息将全部丢失
  2. 当讨论的话题更早之前聊过时,选择最近历史消息的做法无法利用之前的讨论
  3. 固定设置输出最大 token 数量的做法为了照顾长输出的情况,会导致短输出情况下无法利用更多的历史消息
  4. 一个全局设置的温度参数会影响机器人处理不同任务的表现,比如说像翻译、分类、事实性问答这些任务并不需要随机性,而写作、创意生成则有一定的随机性会更好一些
  5. 在当前这种模式下,将机器人属性设置放在最开始的做法无法保证设定的一致性和连续性,用户总是能通过有意构造的输入来突破这种设置

    xorius_cli_2.png

这些问题将会在之后的文章中做进一步的讨论。

LLM Prompt Engineering 实践:序

2023年5月7日 08:00

本文是《LLM Prompt Engineering 实践》系列的第一篇文章,系列文章如下:

  1. LLM Prompt Engineering 实践:序 · ZMonster's Blog
  2. LLM Prompt Engineering 实践:原型 · ZMonster's Blog
  3. LLM Prompt Engineering 实践:记忆(1) · ZMonster's Blog

ChatGPT 刚发布不久,我就把它接进了飞书供公司同事使用,回过头去看当时的聊天记录,那个时候它还是会经常拒绝回答,虽然相比过去的对话机器人有了非常大的提高,但还是显得有点呆笨的样子,对于它到底能做什么我并不是特别了解,再加上公司裁员、北京疫情(12月上半个月是北京疫情的高峰期)的因素,我就没花太多精力在这方面。春节回来后,可能是因为社会放开了以及经过两个月的发酵,非常明显地感觉到讨论热度在上升,我也开始去做更详细的了解 —— 一方面是持续地去使用 ChatGPT 以及了解其他人怎么使用,另外一方面是开始看相关的论文了解背后的技术变迁。ChatGPT 在模型上除了 RLHF 外我是大致熟悉的,但是近几年困在工作和情绪里没怎么去关注学术进展了,所以二月、三月主要是在补课,顺着几篇比较高质量的综述论文把过去几年的一些工作和思路看了下,慢慢地就对现在大语言模型面临的问题以及可能的发展方向有了一些感觉。

毫无疑问,以 ChatGPT 为代表的大语言模型是很有价值的。从专业角度来看,如果不考虑成本的话,原来的大部分简单 NLP 任务都已经没有必要再做,有人因此担心 NLP 工程师要失业,但我觉得对 NLP 领域整体上来说是好事情,领域和行业的能力下限因此得到了提高,原来我们认为很复杂的、只能花昂贵代价做小规模应用的事情现在也可以得到普及(比如说基于文档的问答),我们也终于可以有更多的精力去讨论自然语言里更深层的原理和问题了。从用户角度看,ChatGPT 无疑也有非常大的潜能,回答知识性问题、辅助写作、翻译、总结、处理数据、写代码、绘图……似乎我们能想到的通过语言来做的事情,它都能做并且做得还不错,作为工具来说它已经大大提高了使用者处理事情的效率,并且还在被用户不断地挖掘出新的用法和能力,不管它的长期影响是好是坏,我想没有人会否认它很能干这个事实。

在受益于 ChatGPT 的同时,很多人也因此非常焦虑,我多少也有一点,这种焦虑的根源主要在于 ChatGPT 后续影响的不确定性。目前观察到的应对这种焦虑的方式主要有几种:

  1. 通过追踪新的资讯、动态来证明自己能跟上变化,但往往因为人的精力有限而使自己非常疲惫并造成进一步的焦虑;
  2. 通过预测(或看别人预测)大语言模型普及背景下的行业和社会未来以减轻焦虑,但是真的了解专业知识又具有宏观视野的人还是比较少,大部分非专业人士之间的相关讨论往往落入玄谈范畴,或者索性变成个人世界观的输出;
  3. 通过重度使用、开发相关工具或应用、推动相关社会治理规范来参与新生态的建设,但是由于目前尚处于新生态早期,工具和方法论都非常不完善,想象力也还是不够。

作为一个工程师,我不认为自己具备预测未来的宏观视野,但我想我还是可以参与到新生态建设里去的。以 ChatGPT 为代表的大语言模型效果确实非常强,但它也还是有非常多问题的,和所有机器学习模型一样,这些问题在之后有可能会慢慢地得到缓解,但绝对不会消失,如果要将大语言模型作为一个基础设施来构建应用,那么这些问题必然要有相应的应对机制,我相信新的生态会在这个基础上生长出来。这些问题已经在大量的论文中被提出讨论了,我自己也有维护相应的笔记来追踪这些问题的研究进展,本来是想以各个问题作为出发点来写一些文章做一些讨论的,不过不少问题即使在研究上也还处在探索讨论的阶段,只是单纯整理出来作为科普觉得会有点枯燥,于是决定写一个项目,通过代码的方式来展示我对这些问题的应对方法的思考和尝试。

对大语言模型,我们目前已经知道的问题有:

  1. 模型偏差问题: 模型会表现出对特定领域的倾向性而不是在各种问题上完全中立客观,在有上下文时模型会表现出近期偏差和标签偏差;
  2. 评估问题: 对输入和输出结果,我们缺乏足够有效的评估手段;
  3. 知识幻觉问题: 有时候语言模型会输出一些与给定上下文或客观事实不符的内容,该问题是由训练数据噪声、表示学习的不充分、解码方法、记忆机制缺失等众多复杂因素导致的;
  4. 记忆机制的缺失: 语言模型本身未设计任何记忆机制,以 ChatGPT 为代表的产品仅能将需要记忆的内容同其他内容一起输入给语言模型,需要模型自己对区分输入中哪部分是记忆、哪部分是指令及其他内容,也因此无法像人一样维护长期记忆;
  5. 多轮对话状态管理: 在对话模式下,对话场景的开始、跳出、返回、结束,目前全依靠语言模型自身进行辨别但做得并不是特别好;
  6. 复杂结构数据的处理: 对体量较大但内部结构简单的数据目前可以切片处理,但对内部结构复杂的数据(如完整的项目源代码)目前要处理还是比较麻烦;
  7. 复杂操作的执行: 人类往往对自己给出的指令的复杂性不太敏感,有时候认为理所当然的简单要求实际上可能需要分解成复杂的步骤才能完成,分解后的步骤之间也可能存在复杂的关系。

我准备写的这个项目,计划由易而难地推进,最开始应该就是实现一个简单的机器人,设定一个初始 Prompt,每次对话时带上之前的聊天历史,然后逐步实现记忆机制、加上对知识幻觉的处理、整合阅读网页和文档的能力、整合图像和音频理解能力、加上任务分解和执行能力、实现人设的一致性和连续性、模拟出自我意识、统治人类……这些都不难,多多少少也在很多其他项目中实现过了,大概率不会有特别突破性的东西,甚至有可能鸽掉……总之,我现在要去解决最困难的一个问题了,那就是,这个项目该叫什么名字?

2022.07.31 百花山

2022年8月1日 08:00

早上五点半起来了,地铁去了汇合点,简单吃了早饭后就出发了。天气多云,本来还担心今天看不到什么云,路上看到,开车窗拍了下。

IMG_7945.webp

到达山脚后,惯例和「禁止攀爬野山」的警示牌合影——左二是我,不过打码了应该相熟的人才能看出来。

IMG_8078.webp

到山里后天空情况不错,因为水汽足又有不少的云,我非常满意,不过看山顶似乎被云层笼罩了,有点担心。

IMG_7953.webp

蕙花(by形色)

IMG_7954.webp

蓝盆花(by 形色)

IMG_7957.webp

​棉花糖!

IMG_7959.webp

被云层笼罩的山顶

IMG_7964.webp

糖芥(by 形色),花语是平凡中的甜蜜快乐,真好!

IMG_7966.webp

败酱(by 形色)

IMG_7970.webp

不知道叫什么,形色识别出来好几种很像的。

IMG_7972.webp

弱鸡如我只能在队尾偷拍

IMG_7975.webp

沙参,我吃它食物中毒过哈哈哈哈

IMG_7978.webp

优雅

IMG_7980.webp

丰盛的午饭,背包里不小一部分重量都是食物

IMG_7985.webp

云雾缭绕

IMG_7987.webp

天然加湿器

IMG_7988.webp

柳兰(by 形色)

IMG_7992.webp

雾气非常重

IMG_8002.webp

远眺,但什么也看不见

IMG_8012.webp

老鹤草(by 形色)

IMG_8022.webp

紫菀,或者狗娃花,我分不出来

IMG_8023.webp

其他fafa

IMG_8025.webp

IMG_8026.webp

IMG_8033.webp

雾气偶尔散去,云彩很漂亮,我能看上一整天

IMG_8039.webp

IMG_8047.webp

IMG_8051.webp

IMG_8054.webp

IMG_8057.webp

到达山顶草甸后合了个影,iWatch 真好用,远程控制手机拍照

IMG_8080.webp

山顶草甸开满了各式细碎的小花

IMG_8065.webp

IMG_8066.webp

IMG_8073.webp

在凉亭吃瓜,挑衅嘲讽边上没瓜吃的小孩

IMG_8079.webp

​八点大家才下到山脚,感觉自己变强了,上次爬下山后腿完全没力气了,这次还有很多余力。租了个车回出发点停车场,师傅在弯弯绕绕的山道上飚到100,把我们吓得够呛。两个晚上没怎么睡好,回程的时候我特别困了,十点到门头沟吃小烧烤,我就吃了串烤烤豇豆、烤青椒,勉强吃了串肉。回到家已经过零点了。

博客新增闲言碎语栏目实现类微博功能

2022年5月14日 08:00

从 2013 年建立这个博客后,这里一直是我主要甚至几乎可以说唯一的表达渠道,虽然我也有公众号、微博、豆瓣、Twitter 等各个平台的帐号,但我几乎不怎么在这些平台上表达,朋友圈也用得非常少。有的人很喜欢有反馈的表达或者说其表达的目的就是为了让别人看到、听到,我呢,只是把这里当作自己的一个小花园,我收拾它、布置它,我在这里自得其乐,渐渐地它也变成我的一部分,至于是不是有人来访问是不是热闹都是次要的,如果有人看到后觉得我这好玩我当然也表示欢迎,仅此而已。公众号、微博、豆瓣、Twitter 这些平台,它们更像是家外面的咖啡馆、茶馆、广场,拥挤得不行,大家都在说话,我在这些地方不自在,也不想说话,更多是当个打听消息的地方。

今年以来我的生活状况有点乱,所以博客很久没更新了,二月之后有一篇闲言碎语只发在了公众号没发到博客,有网友也关注了我公众号,问我说是不在博客更新了。首先呢,就像开头说的那样,博客是我主要的表达渠道,我会好好维护的,更新可能会少但一定不会停掉的;其次,发了一年左右的被我叫做“闲言碎语”的个人日记汇总,和我对这个博客的规划是有点冲突的,博客里更想写一些相对系统的、篇幅长一点的内容,所以一直在想把这部分内容以类似微博/Twitter 的形式来呈现,并且自动同步到其他平台上,今天算是完成了这个修改,方法也很简单,见相关 commit

新栏目地址是 https://www.zmonster.me/microblog.html ,样式非常简陋,多条内容之间用一条分隔线分开,每条内容有一个标题,点击后可以前往详情页写或者看评论 —— 本来想做成在一个页面展示所有评论的,但是 disqus 好像不支持,所以先这样吧。

20220514_162946.png

关注我的朋友如果不嫌弃我的碎碎念的话,可以订阅这个栏目的 RSS,或者关注我的 twitter ,至于之前那种一周或者好几周汇总成一篇的方式,容我考虑下是否还要保留。

闲言碎语(2022/01/24-2022/02/06)

2022年2月6日 08:00

2022-01-24 周一

22:00 返家

弟弟顺利下了车,没有隔离,就登记了下,八点左右到了家。

22:00 电话

ZYC打电话过来,问我回没回家,三年没联系了吧,不知道联系我干嘛,能还我钱吗,或者还想找我借钱?

2022-01-25 周二

22:25 XX村之星

我在洗碗,一个邻居过来打招呼,瞎聊了几句,最后他说了一句「你可是XX村之星啊」。大学、工作后,我和村里的同龄人打交道都很少了,回家少回来也是宅着,所以我其实不太清楚村里人是怎么看我的,对于这种夸赞,我也不知道该以什么方式去回应,只好打了个哈哈。

22:29 魔术

弟弟喜欢魔术,这半年多自己练习了不少扑克魔术,就让他表演了几个,还挺像模像样的哈哈哈,不错不错。

2022-01-26 周三

16:00 搞卫生

今天家里搞卫生,爸妈协作在打扫厨房,清洗橱柜,擦被烟熏得发黑的墙壁和天花板,擦大门什么的。搞完洗了个澡,清爽。下午出太阳了,暖和、敞亮。

20:00 医药

医药下跌很剧烈,忽悠弟弟买了 100 块全指医药联接基金。

2022-01-28 周五

14:00 拜访

中午ZCH和他爸、姐夫来拜访,送了水果、酒、牛奶,特别隆重。接待有点尴尬,一来是对方太隆重了,二来最后ZCH他爸和姐夫去下坊了,为了表示礼节,我们就把ZCH留下来吃午饭了,然后我陪着在那尬聊了半天,吃完饭快一点了,我又约了上线,所以ZCH他爸过来接我就直接把他们送走了。年后有时间去拜访一次吧。

送ZCH出去后,前面一位邻居看到我了,过去说了两句话,觉得她也老了很多了,岁月的痕迹。

20:00 新大楼

看老徐转发的朋友圈,说我校计院开始建自己的专属大楼了,非常不错。

20:30 网友

一位网友在我的博客下好是评论了一大段,我感觉他是真的想和我交朋友,哈哈哈。

21:00 《最爱》

泡脚的时候音乐频道听到这首歌,粤语的,觉得很好听。我猜应该是老歌,上网易云搜索了下原来是周慧敏的,电视上真不厚道,谁作词谁作曲谁原唱一概没提,就标了个演唱者的名字。

2022-01-29 周六

给我爸买的鞋子他取回来了,看得出来我爸还是喜欢、高兴的。

中午表弟、弟媳带着孩子来了,家里做了好些菜。吃完聊天,我让弟弟表演了几个魔术,把氛围炒热了一点点。然后前屋的邻居说杀了猪,做了猪血,叫我们过去吃,我爸不能吃,我妈要做事,于是我就去了。他家叫来了特别多人,非常热闹。我吃了一小碗,和桌上其他邻居聊了聊闲话,北京冷不冷啊、有没有谈好女朋友啊、在哪个公司上班啊之类的。见见这些村里的邻居也挺好的。虽然未必所有人都会对我抱以善意,但我确实也希望和这些人恢复最基本的沟通吧,毕竟,我小时候和这些人在一起生活长大,我的一部分是有受到他们影响的。

晚饭后,堂哥带着嫂子下来了,我趁机了解了下堂哥的工作和生意情况,生意方面是开了一个定制衣服的小店我嫂子在打理,想做线上但现在还没空去做,工作方面我哥原来工作的厂子被大厂兼并了,但还会让我哥去做,说是填补了对方的业务/技术空白啥的,但是得去漳州那边上班了。

和爸妈聊天,得知我堂哥的一个姐姐不是亲生的,亲生父母是云石山人,可能是被遗弃或者被卖掉的,说早年间和亲生父母没来往这后面开始有一些了,我之前就一直疑惑为什么我喊姐姐但是又嫁在了我们村。

2022-01-30 周日

中午去一个要好的邻居家吃饺子,见到了一起长大上小学中学但早已嫁人的ZHH,聊了一些天,真的是好久不见了,然后聊天才知道邻居的儿媳是 87 年人,但是皮肤好人也漂亮看着像是很年轻的人呢,可惜嫁的人很混蛋,不知道以后会怎么样。

下午和LYC聊了一些知识管理方面的想法,开心。

晚上去伯母家吃饭,姐夫也来了但是姐姐没来,我先拍了一张饭包肉丸的照片给WJ和LWL看,馋一下她们哈哈哈哈。

看微信群里,LY那边紧锣密鼓地在张罗年夜饭了,我蛮想和他们一起过的。

2022-01-31 周一

早上起床吃完饭后先去敬神,按顺序分别是祠堂祖公堂两支大香烛五支线香、旁边土地一支小香烛一支线香、天井一支小香烛一支线香、祠堂门框一支小香烛一支线香、祠堂门口空地上两支大香烛五支线香、我家门口空地上两支大香烛五支线香、我家门口一支小香烛一支线香、我家祖公堂上两支大香烛五支线香,最后灶上两支大香烛五支线香,这个顺序大致意味着请神到家这个过程。敬神过程中遇到了几个长辈。

敬神完毕后,和弟弟一起把前门墙壁下堆着柴搬到了杂物间,搬完后我爸拿水管冲洗墙壁和门口的空地、清理门口的水沟等等。我妈炖好鸡鸭后我和弟弟喝了点肉汤,然后我妈开始做饭包肉丸,我帮着拿点东西还切了菠菜,开蒸后,我和弟弟就去伯母家喊大家来吃,结果他们先做了好多菜,喊我和弟弟在那吃,我喝了点汤吃了点菜,然后等了一会催促大家下到我家去,由于大家都吃饱了,所以只是象征性地吃了点饭包肉丸,然后就进入了聊天打闹环节。我哥领着两个侄女和外甥女到我们后院看鸡鸭,然后摘了菜地里一些黄掉的菜叶子、杂草什么的扔给鸡鸭吃,玩得不亦乐乎。

下午贴完春联后,三四点我和弟弟先后洗澡换了衣服,然后我妈做了年夜饭,再次把伯母一家喊下来一起吃,不过姐姐和姐夫年夜饭得在姐夫家吃所以下午已经走了,所以桌上就是我家四口人加上伯母一家五口人,也算是热闹了。吃完再聊了聊天伯母一家就回自家去了。然后我和弟弟去点岁火,流程和早上敬神一样,但不用提着祭品了,只用香烛和线香。点岁火遇到了一些同辈的邻居,回来路上遇到大眼带着他的小儿子,大的说有十一岁了哈哈哈。点完岁火到家,村里到处开始放烟花,特别特别吵闹,好看或许是好看,但真的是太吵了,两边隔壁家、屋前屋后邻居家都在放,距离不过十来米,震耳欲聋 —— 我们家嫌吵闹是从来不放烟花的,敬神和点岁火按规矩打串爆竹就好了。烟花这个东西,大概没人真的去欣赏它转瞬即逝的美,大家都在放无非是热闹、跟风、攀比,挺无聊的。

晚上开始下雨了,我弟在春晚沈腾小品的背景下又表演了几个扑克魔术,然后洗脸泡脚,上床看魂圈拜年纪,和朋友聊天。

2022-02-02 周三

今天依然在下雨,早上起来很冷,看着外面被我妈叫做「大泡子点」的雨,有点畏惧出门,但又很想见见WJ,就想着下雨的话该怎么去。不过到了下午雨小了,天气真是给面子。快两点的时候,LBL带着WJ从云石山过来,我走了一小段路到桥头,又遇到一个同学,后来加联系方式他发了自己的姓名我才想起来是LJ并且和以前记忆里的脸对上。等我走出路边LBL的车已经在等着了,我上了副驾,看到LBL和WJ的脸,觉得一点也不生份,就是老朋友相见的亲切感觉。WJ说话比我印象中要爽朗大气了,不知道是不是我的错觉。

先去了QL家坐了一会,WJ的女儿和儿子马上就玩上了,一点都没有因为环境陌生就拘谨。QL的爸爸妈妈张罗零食酒水,我们就边聊边嗑瓜子喝水,聊了什么来着我都不太记得了,总之气氛是愉快、轻松的。没见到QL的哥哥姐姐,只见到他嫂子和三个侄子,三个侄子分别是三年级四年级五年级,感觉当年这位嫂子挺辛苦的……

在QL家坐了一会后就上WJ家了。WJ家热热闹闹地来了许多客,就把靠门口的一张桌子收拾了下我们几个坐上开始聊天,还特别热情地上了很多吃的,我记得的就有菜头生、豆腐拌辣椒(没见过的做法)、酸菜、米粿(炒的但是放上了鸭肉,也是没见过的做法)、饭包肉丸,都挺好吃的,我吃个不停。后来四点多快五点去走亲戚的JS也骑摩托过来了,我们聊了各自的情况,还共同回忆各种初中的同学、老师及相关的事情,感觉WJ是和初中的人事关联最紧密的,QL甚至连同村距离不到 50 米的初中同学都记不得了,我也差不多,所以多数时候是WJ在帮忙补充,比如说初三我们一个班读过啦、JQ老师去统战部还是统战局了啊、FQ老师去光大银行了呀(还有啥想不起来了)……

快六点我们就散了,QL走路回家,我蹭JS的车回去。JS车开到清溪,下来买了一大盒 18 块的摔炮和两把手持烟花给孩子玩,然后去对面超市买家里要用的白醋。我本来是在等着的,他买完东西回来说看到ZYH了,我就过去打了个招呼简单聊了两句,加上微信方便以后联系。JS骑车带着我从清溪走,送我到了家里,我喊他下来喝了点热水,知道他家里催着他回去吃饭所以就没怎么留他。

我回到家后家里已经吃完饭了,不过我在QL和WJ家都吃了很多东西所以也不再需要吃什么,简单玩了玩就洗漱了。和LWL也聊了聊分享了下我今天的经历,聊着聊着开始聊小学同学,稍微共同回忆了下,我记得以前DHT说过LTT也在一中并且表达过我欺负过她的意思,我和LWL说这个,结果LWL告诉我说LTT初中后就没上学了,所以是我记错了还是怎么样?LWL还很自信说小学五年级每个同学坐哪个位置都记得清楚,确实能记得我坐第三组第三排,但我问我前后排是谁她就说不出来了,然后迅速认怂说确实记不起来了,笑死我了。

2022-02-04 周五

(略好多字)真是难过的一天,晚上拖着LWL和我聊天到零点,好歹是好受了一点。

2022-02-05 周六

可能两点才睡吧,五点多醒了,想再睡,脑子里翻来覆去的还是那些事情。

本来定了今天去外婆家和ZCH家以及见CH他们,然后我决定都不去了,哪都不想去了。我妈本来也说不想去,但后来因为弟弟说想去,就还是一起去了,吃完饭收拾好各种东西我爸骑车把他们两送到了圩上搭公交车。

这之后我持续地处于大脑迟钝的状态,什么都不想做,只想刷手机看小说新闻来分散注意力。

下午五点多吧,伯母那边喊上去吃饭,我穿了鞋上去,坐了整整两桌的人,我都不认识,就坐在姐夫边上吃了碗饭喝了点汤,就回家了。我爸本来让我说他在打麻将不去吃,后来伯母下来喊,还是上去吃了点。

我给锅上倒了水想着烧水洗漱,后来我爸就接手过去烧火了。坐着坐着,收到我哥的信息,说两个侄女(其实有一个是外甥女)觉得学习太累了,待会让她们下来和我聊聊,过了会两个小姑娘下来了,排排坐坐到了房间椅子上。我就觉得特尴尬,说这个咋聊啊,学习累太正常了嘛,先起了个头说自己没法说学习方法建议可以想想自己以后想做什么,从这个就展开讲了我为啥会选计算机啊(因为经常去网吧玩,所以真的非常运气),接着又讲了很多我的一些故事,比如我填志愿填了清华大学填着玩啦、不喜欢重点班的氛围(卷、同学嘲笑心理有问题的女生)最喜欢复读那一年的时光啦(六一节大家看葫芦娃)、我去做暑假工的事情啦(这个算是有说教意义)、不喜欢自己专业的人会痛苦啦(我的室友/同学)……最后点了几个题,一个是跟她们讲应当要去享受知识本身的乐趣,这个是比分数更长远的东西,聊了各个学科我知道的一些奇怪有趣的东西;后来进一步我也聊说还可以通过一些游戏化的方式来提高乐趣啊之类的。感觉聊完和两个小姑娘是有拉近一点距离的,希望我传达了正确的观念。

上床后,十一点左右,舅舅打了电话过来,问怎么回事,我也不想说什么细节,就说我在处理。后来和LWL聊了会,我感觉她还挺喜欢和我聊的嘛。零点后,CH又打了过来,视频通话,CH和LH在CML家,CML说他 25 才回去,LH和CH都是明天回,开导了我一番。

感谢大家的关心吧。

闲言碎语(2022/01/10-2022/01/23)

2022年1月24日 08:00

2022-01-10 周一

23:20 芋芽芋荷芋子粥

芋芽还是第一次吃,挺新奇的;芋荷炒牛肉简直无敌;芋子粥,粥里的米没有煮烂,我爸说不能煮烂这样粥才能好吃有甜味,是这样吗,学到了。

2022-01-12 周三

22:05 明年

午饭的时候,我爸笑着说「你可以在北京找,我们也不要求说多快结婚,但明年你起码要带一个回来看看吧」。

22:40 残忍

有时候我会想,我如果真的遇到一个合适的人,却要她来和我一起分担面对我的痛苦,这不会太残忍了吗?

2022-01-13 周四

08:58 可爱

早上起来,看到 WJ 在最新一篇公众号的文章下评论「觉得你有时好可爱」,那当然了,我超可爱的,希望有更多人爱我。

2022-01-14 周五

21:42 清晰

相亲也不是没有好处的,至少在这个过程中,对于我能和什么样的人聊得来、想和什么样的人共同相处有更清晰的想法了。

2022-01-16 周日

15:51 独处

在家里,没有太多独处的空间,我觉得这是最大的一个问题。倒也不是说我不能一个人待着,但是父母就在附近,我没有办法不去想他们的事情。周末这样的日子变得很难熬,倒是想到上班后觉得一阵轻松。

15:52 理想伴侣

我想要什么样的伴侣呢?我这阵子想过一些,比如说互相理解啦、三观一致啦,可能这些都不是必要的,因为互相理解的状态是需要在双方共处的过程中逐步建立起来的,三观涉及的东西太多说一致完全就没意义。非要说的话,希望是一个独立自主且保持开放心态的人吧。

等于没有说。

2022-01-17 周一

12:01 史官

我是我自己的史官。

2022-01-18 周二

21:11 新增

北京和深圳都有新增病例,爸妈忧心忡忡,担心弟弟回不了家过年,我说担心也没办法,人平安就好。北京的一些朋友已经不打算回家了,有些人买好了车票但也在担心。如果这种情况还会维持几年,大概会有很多的社会习俗要被迫跟着改变吧。

2022-01-19 周三

22:03 童年

陪我妈聊了很多她童年的事情 —— 最开始是我在看电视,记录频道在讲徽派建筑,我说外婆家那里的屋子有点类似的味道,以这个为由头聊起了村里的建筑,我妈说她小时候就是在祠堂上的学,聊到我大外公(我外公的哥哥)是老师,然后聊起我大外公家,进而开始聊到我妈小时候和大外公家谁玩得好各种事情,还说我大舅小时候不知道为什么被大外婆嫌弃被她打特别厉害,说到大舅,我妈仍然是非常不解,反复在说「都是自己生的,不知道你大外婆为什么那么讨厌他」。

22:26 柳比歇夫

微信群里认识的网友,说我是现代柳比歇夫,了解了一下,确实我现在做的事情有点像他呢,希望我也可以坚持几十年。

2022-01-20 周四

09:14 幻听

七点多快八点的时候,我隐约听到父母的争吵声,但是仔细听,却又听不出具体的词汇,我轻轻起床来到楼梯口,楼下安安静静的,只有锅铲和锅碰撞的声音、排风扇的嗡鸣、门开合的吱呀声,并没有争吵声,我是幻听了吗?

21:54 小孩

诶,我一直的印象中故作成熟有点臭屁的小孩,为嘛现在总感觉有点小抑郁。这么需要别人理解。

我小时候臭屁吗?没啥印象了哈哈哈哈。

23:00 气候

我妈说,她小时候气候和现在相差很大,九十月的时候就会很冷了。这么一说确实是,我记得我小时候时,冬天到处是能见到冰的:田地里会结一层薄薄的冰,晾衣服的竹竿和屋檐都会挂上冰锥 —— 我们叫凌冰。

2022-01-21 周五

08:09 失眠

三点半醒了,之后好像断断续续都没怎么睡着。

2022-01-22 周六

22:52 堂姐

堂姐和姐夫上门拜访,挺奇怪的,因为长辈的矛盾,几乎没见过她专门来走动的,我陪着聊了半天,他们主要是围绕他们高二的儿子在聊,说成绩不稳定,问我有没有什么考试方法、学习方法的建议。我妈后面说,估计是听说我回家了,专门为这个事来的。

真的,我学习好不是因为学习方法,大家就不能承认我聪明这个事实吗?哈哈哈,中考后学校把前 20 名召集起来开会,我在那个会上说「我觉得读书还是要靠天分的」的事情了,想起来就好笑。

22:56 拒绝

拒绝了朋友介绍的一位姑娘的见面请求,微信上聊了这些日子,觉得不太聊得来。其实早就觉得聊不来了,但之前想着是朋友介绍并且称赞的人,是不是应该还是见一见什么的,但现在我实在是心累,不想再浪费双方的精力了。

23:12 送节

跟我妈去送节,先准备送点自家的东西给大姨,因为要去外婆家,我妈为了谢绝大姨邀请我们吃午饭,电话里除了说要去外婆家还拿我做借口撒谎说我有事要办,我听到后又好气又好笑,不想去就不去呗干嘛扯我啊,万一待会大姨问起来我有啥事,该咋回答?无奈,我就跟我妈对好口径,说如果问起就说我去找我同学了。

我妈送东西给大姨就先下车了,我就到广场才下,去银行取钱,惯例又遇到 ATM 钱被取空,找到工商银行的 ATM 才完成取钱任务。然后在等我妈的过程中和高中同学聊了聊,不说恢复感情,起码恢复了微信沟通吧。

去了外婆家,见到了表弟的妻子和刚出生不久的女儿,我还抱了会孩子。还见到了最小的三个表弟,两个大点的一个在一中读高一、一个初三,都很高大了,过得真快,最小的才 10 岁不过也很快的。

在门口竹椅上坐着和外婆聊了一些天。

23:01 八一南路

走完亲戚,陪我妈去八一南路买被套,人挤人热闹得不行。拥挤的人潮里,回忆不断涌出来:在文化馆打桌球,在一家已经不记得名字的店吃卤豆腐等小吃,在双清书院买小说,穿过有好多香烛店的解放路去向阳南路的租书店看小说和漫画……对于高中生活,脑子里清晰记得的只有几个印象深刻的片段,但其实也有很多很多开心平凡的日常,都模模糊糊了,只有像今天这样,在特定的地点触发之后才能勉强回想起来一点。

我过去到底是怎么样一个人,我其实已经完全不清楚了。

我的2021

2022年1月15日 08:00

2021_diaries_heatmap.png

在 2021 年里,我写了 321 天日记,一共写下了 797 条记录,共计 138574 字,平均每天要写 432 字。我在 2021-06-14 写了 3107 个字的日记,是这一年中中最多的一天。797 条记录里,有 234 条是在做计划,有 95 条在做回顾,剩下的 468 条在记录生活细节。

2021_diary_words.png

日记里的关键词显示,我这一年关心得最多的,是父母和搞钱,也有很多的想法,不过并没有去付诸行动。虽然热衷于搞钱,但是全年的总收益率才 4% 左右,还是需要学习呀 —— 虽然没有在投资上赚到什么钱,但在财务上,我把我从 2016 年开始记的账都梳理清楚了,我现在能算出我所有资产的增长趋势了,搞清楚了这一点我很开心。

2021_diaries_sentiment_2.png

日记显示,我这一年有较大的情绪变化,特别是年末的几个月,过得并不开心。

2021_diaries_sentiment_wordcloud.png

朋友们的陪伴给了我最多的正面情绪,帮助我对抗了各种各样的痛苦,感谢我的朋友们。分析了微信聊天记录后,我选出来十位和我聊得多的朋友,看看我都和他们聊了些什么:

2021_chathistory_top_person_1.png

我和 ZCB 聊了最多,这个是我弟弟,聊得多也正常,我也会比较唠叨,所以对他说的话是多过他对我说的话的;其次是 LY,他对我说的比我说的还多,看来他比我话痨;ZYP 是我表弟,2021 年刚上大学学计算机,三天两头来找我请教问题,所以聊的内容大多是一些计算机的内容;WJ 是我的多年老友,虽然多年未见了,但是我们始终在共同探讨心灵、自我方面的话题。

2021_chathistory_top_person_2.png

我和 LY 在 200 多天里都有交流,积少成多,才有了他top2的地位。

2021_chathistory_top_person_3.png

我和ZYP、ZCB以及WJ一聊就停不下来。

2021_chathistory_top_person_words.png

上图是我和这十位朋友聊天记录的关键词,相交的部分是我们共同提到的内容。

人生不如意者十之八九,也许我该找个人聊聊?但是很多话题,实在是找不到人来聊,所以我去读了《也许你该找个人聊聊》和《蛤蟆先生去看心理医生》:

  • 前者说:

    • 所有问题的根源都可以归结到自由、孤独、无意义和死亡这四个问题上
    • 我们真正的问题往往被我们回避着
    • 即使我们认识到了真正的问题以及解决方案,往往也会视而不见,因为与其从并未上锁的囚笼中走出来面对命运的无常,我们宁愿选择沉溺在已知的、确定的痛苦和自毁中

    我在读这本书的时候,注视到了我一直视而不见的阴影,我知道我的自省已经做得够多了,时候已到,是时候作出改变了。

  • 后者说:
    • 人有三种心理状态:只会说「我要这个我要那个」的儿童自我、只会说「你应该这样你应该那样」的父母自我,以及「我要去做些什么来应对现在的问题」的成人自我
    • 当我们出问题时,往往是在儿童自我或父母自我中停留了过久,尝试让自己进入成人自我,是脱离痛苦的道路

除了朋友和书籍,B站也陪伴我度过了很多时间,可惜B站的观看历史只保留最近三个月的,所以我只能基于这三个多月的数据来做一些分析了。

bili_2021_times_1.png

每天的早上和深夜,是我使用B站的高峰期,深夜难眠,B站陪伴,早上看得多是因为我早上吃饭时用来下饭,坚持吃早饭的我真棒!

bili_2021_times_2.png

工作日只能早晚看,但周末就看得很多了。

bili_tags.png

在内容上,我喜欢看游戏、搞笑和日常类别的视频,没什么营养,但打发时间足够了。

bili_title_wordcloud.png

这三个多月里,我看了大量《黑暗之魂》和《血源诅咒》的视频,并和大家一样,期待着《艾尔登法环》的到来。

bili_authors.png

我最喜欢的10位UP主见上图,其中四位都是黑暗之魂系列游戏的UP主,我对宫崎英高是真爱。

2021_journal_heatmap.png

工作方面,我写了 239 天的工作日志,一共写下 523 条记录,共计 233429 字,平均每天要写 977 字。以可复现工作为准则的我,除了文字,还在日记里记录了我实际的操作,记录了 5808 行操作用的代码,其中 Python 代码 3015 行,shell 1715 行,SQL 430 行,YAML 379 行。

2021_journal_words.png

工作日志的关键词显示,这一年的工作内容里,我和数据、资讯、股票打交道很多,大量的时间是在做具体业务,我已经远离算法了。

这是一次数据化分析总结的尝试,限于精力还有很多东西没有分析,争取在2022年多收集一些数据,做一个更全面的分析出来。

闲言碎语(2021/12/02-2021/01/09)

2022年1月10日 08:00

目录

2021-12-04 周六

17:15 阴影

《也许你该找个人聊聊》这本讲心理治疗的故事的书,传达了一个观点,就是每个出现问题的人,大都有一个自己在回避的根源性问题。我想了一下,我有这样的问题吗,可能还真有,我以为我已经从中平复过来了,但也许那道阴影一直都在。即使现在这样一个瞬间我能看向它,但这种不敢面对、不想改变的症结才是最大的问题。

我的阴影可能是:(略去104字)。因为这个,我不想发展什么亲近关系,因为一旦亲近,我就必然要告知他们这些状况,我害怕那时候他们的反应。

当然,把所有问题归结到一个点上,可能又是一种把复杂问题简单化的自我欺骗,谁知道呢?我得承认,我对自己的了解还是非常的不够。

2021-12-05 周日

09:45 导出

发现 Telegram 自带了聊天记录导出功能,非常友好!一口气备份好了多个群及好友的聊天记录,不过 EFB 我大概是 18 年才开始使用,更早的就没办法了。

telegram_chathistory_export.png

18:39 改变

今天去 LY 家进行小饭桌活动,来回的路上我都继续在读《也许你该找个人聊聊》,里面有一段表达了这么一个观点:人们会积极会消极地走上自毁的道路,因为这样就可以给自己带来一种自己掌握了命运的错觉,而不用被不确定性困扰。

我对此表示赞同,因为改变意味着丧失而丧失则带来痛苦,一个已经在困境里的人,同样可以用这个困境作为外壳来保护自己免受更多的痛苦,至少人的潜意识里是这样的。

我昨天觉得,我发现了自己身上一个具体的更大的问题,但现在,我觉得更大的问题,是我也在抗拒改变、害怕改变。

我想要一些改变。

20:44 想成为UP主

嗯,一个想法是,想用音频、视频的方式来记录自己的生活,就当视频日记来做,感觉也会很好,毕竟音视频能更好地还原生活的状态。

大概了解了下,以我这种需求,拍摄的话买个手机支架就好了,录音方面可能需要声卡、话筒但也不是必需,然后就是电脑能跑得动一些常见的视频剪辑软件了,要不要试试呢?

2021-12-06 周一

18:43 失望

写了段漂亮的代码,换来的却是一句「为什么要搞这么复杂啊」,对,从头到尾堆语句没有任何设计倒是简单了,输入输出稍微改一下也只要把原来的代码抄一抄糊一遍,确实简单。失望透顶。

2021-12-07 周二

00:30 拆股和再投资

发现在 beancount 里拆股可以这么写

2021-09-13 * "证券保险ETF拆分"
  Assets:投资:证券账户:华宝证券:持仓                         -2000.00 SH512070 @ 2.3490 CNY
  Assets:投资:证券账户:华宝证券:持仓                          6000.00 SH512070 @ 0.7830 CNY

那分红再投资是不是也可以这么写呢?可以的话就不会造成大量的分红收入了 —— 原来的做法是先写一个分红的收入,然后再写一个买入,像下面这样

2021-10-20 * "交银双轮动AB分红"
  Assets:投资:且慢:稳健投资:CASH    101.03 CNY
  Income:分红

2021-10-20 * "买入交银双轮动AB"
  Assets:投资:且慢:稳健投资:持仓    95.15 OF519723 @@ 101.03 CNY
  Assets:投资:且慢:稳健投资:CASH    -101.03 CNY

分红再投资其实就是钱左手倒右手并没有变多,但这么一记就产生了一笔收入。

13:05 降低要求

日子总是还得过,就降低对别人的期望和要求呗,不然还能咋地。

2021-12-08 周三

22:38 坠落

(略去 707 字)

打完电话是 19:40 多了,还好,不算太晚,LY 和 K 喊着去吃二哥,我本来想不去了,但是想着自己现在这样的状态,回去更危险,于是强撑着还是一起去了,吃了 345,觉得二哥的味道比以前差了好多,以后可能不去了。

好绝望、好难过、好无助……

(略去 445 字)

23:45 续

洗了个澡,感觉好一点了。

(略去 114 字)

暂时不想睡,需要一些刺激多巴胺的东西,看看 B 站或许再玩玩游戏。

2021-12-13 周一

22:45 泄漏

上午我在写当日计划,然后接到中国电信升级套餐的电话,就停下来接电话,这个时候同事过来询问工作的事情,当时我的屏幕上正显示着「失望透顶」那一段,察觉到对方可能看到后,我把屏幕切到了浏览器上,然后继续聊工作。

也许对方看到了,也许对方会一整天心情都不好,不过,那又怎么样呢。

22:48 代际

下班拉着 ZCH 去饭阿姨吃饭,聊了聊天,他说他想干个两年后就离开一线城市,到比如武汉、南昌这种地方。说真的,我毕业、刚工作的时候可完全没这种心态。不过,虽说是一个大的趋势吧,我感觉他并没有想清楚自己要的是什么生活,聊的过程中他说到他一个同学在军校学计算机出来后就分配工作「生活有希望」,到底是什么希望呢?

2021-12-14 周二

08:52 梦

梦到(略去 30 字)。梦醒后无言对着深夜的黑沉,一边庆幸只是梦一边又想现实和梦也没啥区别。

还梦到了(略),在世界发生巨大灾难快要毁灭的时候,(略)。是什么灾难来着?我记得有洪水,还有被雨水淋久了后身上会冒出黑色的丝线样事物这个事情。

2021-12-15 周三

08:30 无梦

没有做什么噩梦,真好。

23:30 畅谈

和 WJ 畅谈了一番大数据啊、世界观啊、痛苦啊、自我啊、命运啊之类的,特别开心。她在我心目中一直是感性、对生活很有感受力那种人,我还以为她不会被类似的问题困扰,看来是想多了,这个年纪的人,大概都差不多。

2021-12-17 周五

19:30 回家

开完组会跟领导说了我想提前回家的事情,领导没怎么为难就说没问题,还说「你回家了也没事做对不对」,挺放心我的啊,大概是去年大家有过 remote 的经历吧。额,其实我是想着提一个月应该不会同意然后等着讨价还价的,目标是 14 天来着,防止到家后被隔离 14 天。

23:06 记录

看到 WJ 发了新公众号文章,说「我喜欢用文字记录生活的点点滴滴,喜欢捕捉稍纵即逝的每一份美好时刻」,不过她的记录好像都是通过朋友圈来做的,我就发微信提醒了她下,并且推荐了 flomo 这个应用给她。后来还陆续聊了不少记录方面的想法,并且把我的 org-roam 笔记展示给她看了,这个时候才知道我把她的学校记成「兰州大学」是搞错了,其实是「西北民族大学」,不过也在兰州啦。

23:14 香猪坊会议

上午的时候,K 问了一个问题:如果你不能做现在的工作,你还能干啥?

我就说如果可以的话我还是想在计算机领域做事,顺势聊了一些我对 NLP 的想法。最后回到「不使用第一技能如何挣钱」这个问题上又讨论了一番,XKL 说她想写科幻小说或者图书管理员,K 说他想 BFS 也就是说探索更多领域的知识和技能,LY 说他想玩现代农业。最后发展到怎么做些不那么累又能养活自己的小生意,聊得很热闹。

下班后相约到了香猪坊吃烤肉,继续聊了这个话题。K 到了我们办公室,我和 XKL、K 一起走路先去香猪坊的,路上还开玩笑说是不是可以做「外卖技术专家」,像外卖一样供别人下单然后半小时到一小时内上门帮忙解决 bug 哈哈哈哈……去了香猪坊后继续聊,不过吃饭时间太短了,八点开始点菜的吧,八点半就吃得差不多了。期间 XKL 提到她自己有记录一些点子,给我们说了几个。LY 说了 TeXPage 这个小众产品。

XKL 还有点奇怪,说她这个年纪想这个问题还好说,我们几个都还比较年轻为什么也在想这个。我倒觉得很正常啦。另外我问 XKL 是不是每年理财就够生活了,她说是,生活没问题,所以会想去写科幻小说、图书管理员。

我们决定之后可以继续就这个话题展开讨论。

2021-12-19 周日

15:39 个人数据库

我想简单设计一下数据库,把自己的一些数据存储起来,比如说和朋友的聊天记录啦之类的。

在鱼塘和大家聊了聊,好像没有什么特别合适的、开箱即用的工具,所以还是自己来写点代码做处理了。

2021-12-21 周二

23:49 连手

今天 K 生日 —— 据他说身份证上是明天 —— 下班后四个人去了连手吃烤串。据说「连手」是兰州话「朋友」的意思。

边吃边继续聊小生意的事情。

LY 存款不多,XKL 有房所以肯定是不缺了,K 说他的存款也不少了,躺着的话在北京还会有一点焦虑但如果回兰州就够够的了。

XKL 说她主要是想要躲起来以应付接下来可能发生的社会变化,以及生活上钱也够了所以可以找点自己想做的事情做。

K 没说太多他的目的和想法,不过我们聊过多少钱才够自由这个事情,我当时比较想强调自由和钱关系不是那么大了抬了不少杠,被 K 说我是在使用稻草人谬误,哈哈,是得反思下,我讲话有时候也没啥逻辑,也会为了印证自己的观点而强行使用证据和说话方式。

LY 的想法和我差不多,就是想做点有价值的事情。

2021-12-22 周三

14:12 二胎

去贵凤凰吃饭,边上一男二女在聊二胎话题,男的说他媳妇想生二胎但他不想,因为之前陪产过觉得太痛苦了;二女中的一个说这个其实还好,虽然痛但也就很短一段时间,如果要生二胎她觉得自己是能忍受的,而她不想生二胎是担心两个孩子之间感情分配不均衡。

2021-12-24 周五

23:44 平安夜

下了班,饭罪委员会加上姨夫一起去了零公里,车上姨夫给店里打电话问要不要等位,对方说今天店里人还比较少,因为吃西餐的人会比较多,哈哈。

我们到了零公里后,点了酒都肥肠、李庄白肉、叙洲包浆豆腐、花生浆菜豆腐、酸豆角烧魔芋、燃面几个吃过的菜,还点了鲜锅兔、回锅排骨、腊味拼盘、猪鼻筋等几个新菜。由于有姨夫在,我们闲扯多聊了会,比往常吃完花了更多的时间,挺开心的。聊了点姨夫的感情生活,得知他已经和上海的那位分手了,说发愁之后去哪里,回家长期住着估计也不喜欢但出去住现在也没什么名目了之类的。

话说,到店后注意到 XKL 涂了口红,头发也明显打理过,还戴了一条红白的围巾,美美的。

吃完坐 XKL 的车到北土城,然后回家,见到一个姑娘拿着一支花和一个装在袋子里的苹果,不知道是朋友还是公司送的。路上看到表弟发给我的 C 语言问题的消息,我问他说平安夜有没有去玩,说学校不让过洋节,加上太冷了同学也不愿意出去,哈哈。

23:22 的时候跟 WJ 简单说了声平安夜快乐,得到了回应。往常可能是她先说的,这次我抢个先吧。

2021-12-27 周一

22:26 关店

得知简阳羊肉汤在年后就要关店了,老板说这种品类的店也就冬天生意稍好一点,其他季节不行,最近两年经济又不好,房租又涨,没有新客老客也在流失,实在是开不下去了。还挺遗憾的,一家开了 9 年的老店,就要这样没了。结帐的时候继续跟老板闲聊,老板说孩子现在 10 岁了,关店之后就回家照顾孩子,在家里咋生活没问,总是能找到路子吧,做点零工、小生意,总是能生活下去的。

吃饭的过程中,隔壁桌抽烟被 LY 制止了,隔壁桌性格也还不错,后来问 LY 说想抽一口行不行,被 LY 和 XKL 严辞拒绝了,还挺有意思的,看 LY 扎了头发就问 LY 是不是做艺术的,笑死。

22:29 裁员

之前听 K 说过他们公司在缩减运营人员,没想到下午的时候 WR 和姨夫一起过来,说被裁掉了 —— 技术就剩了四个人,公司产品被认为是错误的方向,运营一直做不上去,因此不再做了,K 还留着继续做剩下一个业务线,同时公司会讨论新的方向之后继续吧。

就挺突然的……

WR 走前送了一个卡比兽和妙蛙种子的手办给我和 LY,一人一个。

22:38 体检

体验预约我一直拖延,今天一看各个地方都没号了,奇怪的是望京分院明天有号,于是只好约了明天望京分院的,得早起了哇……

2021-12-28 周二

11:17 体检

七点半起来了,也没有想象的困难,甚至觉得有点精神。坐地铁到望京东C口出去,然后走了一公里多到了慈铭,到的时候是九点过几分。人并不多,先抽了血,然后检查耳鼻喉,先问我鼻子流不流鼻涕,我就说我有鼻炎,随便看了看说「有点鼻炎」这他妈不是废话嘛……做完耳鼻喉做内科,就用停诊器听了听心肺,完了医生说我有点紧张,我笑笑出门;然后排队做心电图,这种等待的感觉真好,就放心刷着手机,什么工作啊社会啊都不用想,做完后给隔壁医生看就扫了一眼说「正常」,嗯,挺好的;然后去做彩超,又一阵好等;做完彩超去做了胸部CT,然后做血压、身高、体重一般检查,最后是去卫生间留了半盒尿。剩下有一些项目我放弃了,下一楼还了钥匙领了鸡蛋走人。

2021-12-30 周四

23:15 升21转

这个可转债今天上市就涨 35% 导致停牌,快三点的时候差点忘了卖掉,还好 ZY 过来找我聊这个,才想起来把它卖掉了。

2022-01-01 周六

12:44 同理心

老徐发了一条朋友圈,大意是宁波有疫情回不去了,我没想太多,发了一条「我明天回家」,然后被他怼了。看到他的回复我其实很不舒服,不过回头一想,他看到我的留言肯定也是不舒服的,以牙还牙把这种不舒服还给我,很合理。我开始自认为「没有想太多」,但仔细想想,我这条留言,不就是看到别人回不了家然后觉得自己回得了家,进而产生了一些优越感和窃喜吗?

他人是一面镜子,可以很好地照出自己的样子。

20:47 退货

前些天买的一件衣服,尺码不对,于是我发起退货了,京东说今天 15:00 到 21:00 会来取件,然后我一整天都在担心这个事情,就是那种一件事情没有完成的状态,很难受。

就在刚才,给我打电话了,问我在不在家,我说在。过了没一会,敲门声响起来,我把装好的衣服递出去,问「要不要检查下什么」,京东小哥乐呵地说道「我还能不相信你嘛」,接着说「嘿嘿,我又回来了……取件还是我取」,我看他那么自来熟,但戴着毛茸茸的套头的帽子,实在是辨认不出来是哪位京东小哥,只好掩饰着尴尬说「我还以为你不来了呢,都八点多了」。「哪能不来呢,别人的件我都没取先来取你的了」,他这样说道,依然是乐乐呵呵的,然后拿着件走了。

他好开心的样子,看得我也开心了起来。

2022-01-02 周日

06:17 停运

四点十分起了床,洗漱收拾完毕,到五点左右出门,约的出租车就停在了小区门口,师傅放倒了椅子在睡觉,我敲窗户把他叫醒,出发往西站去。我昨天看地图说要一个小时到,师傅说只要半小时,大概是我昨天看的时候是按当时的路况来估计的。

和师傅聊天,聊疫情,我先用最新的研究给师傅灌输了一波焦虑,感叹了一番。师傅问我是不是出差,我说怕回不去提前回家,于是就聊到过年的事情,师傅说现在过年没有年味,以前就拉客都能感受到,先是农民工大堆回家然后是学生们回家,都热热闹闹的。还说到他小时候过年的一些风俗:一个什么火,说是劈好柴在院子里像篝火一样点燃,火烧得越旺越高就意味着新年生活越好;说他父亲会拿个瓢盛着醋在家里转着洒来辟邪 —— 其实是能起到消毒作用吧;说他哥会拿锤子把砖头敲成球,不过这个没听太细,不知道是他们家普遍的风俗,还是小孩子的玩耍;还说他们用黄米(黄米是什么米不懂)做成面蒸馍吃,以前没东西吃这个就算好粮食了,现在这么做来吃的少了。我也讲了我们初一迎神和捣年糕的风俗。

到西站五点半,验了健康码安检后进站,却在刷身份证进站时被告知我买的车已经停运,给我整懵了。工作人员让我去问售票处,我去了,售票处说列车已停运只能给我退钱了,然后说可以帮我看下有没其他车,最后找到一个在南昌中转的路线,中间就隔半个小时,要比原来的车便宜150、晚3个小时出发晚一个半小时到,反倒更快?不过更快也没啥意义,我这都已经在火车站了。生活真是充满了意外呀。

11:50 九江农业号

火车叫九江农业号,是收益不行所以开始出售广告位来赚钱吗?车厢人挺少的,一大半都是空的。

18:02 南昌

在南昌换乘,有便捷换乘路线,直接到候车大厅,不用出站,还挺好的。等了一会后进站,到站台后才明白工作人员强调蓝色地标是什么意思,车还没来,只能靠地上的标识确认哪里是门的位置以及是几号门,而这些位置上用不同颜色标着不同的数字,我是5号车厢,找到蓝色的数字5的地标就好了。上了车,我的位置是11A,靠窗,不过晚上了啥也看不见没啥好的,现在倒是觉得C和F这种靠过道的位置好,走动方便哇。B位上是一个姑娘,染了一头鲜艳的橘色头发,挺惹眼的。

20:13 陪伴

旅途中又和 ZQL 聊了聊,得知他女朋友单位不让离京,我说那你不如不回家好了,陪陪妹子啊,然后他说奶奶身体不好之前还进了ICU,虽然现在转轻症了,但感觉老人时间不多了,还是想陪一陪。都不容易。

23:00 接站

昨天和家里打电话的时候,姐姐和姐夫正好在我家,听到了电话,于是姐夫开了车来接站。一路上闲聊,发现姐夫还真是交游广泛,对于我初中时那些老师的去向都很清楚,我随口说了一句是不是哪个老师去了教育局,然后他连报了好几个我初中学校老师的名字,太厉害了。因为第二天还要去做核酸,就在我姐夫家过夜了,姐姐和孩子都在,我就陪着聊天聊到 10 点多,感觉我还挺能聊的了,不知道从哪学的。外甥女去一中补课了,现在孩子压力真大。

2022-01-03 周一

10:00 核酸检测

在姐夫家吃完早饭 —— 米粿汤后,坐了会就出门去人民医院做核酸了,结果吧,江西搞的那个什么赣通码直接给我整个报错,还好国家平台健康码能用。进医院后挂了个号,出来在门口角落的检测亭里做了咽拭子检测,没几个人,后来一查,说是「愿检尽检」,也就是说我不做检测直接回家,问题也不大。

22:00 家(补)

做完核酸后,姐夫送我到家,坐着聊了挺久的天,但是没有及时记录,我已经记不清他是不是在我家吃午饭的了,以及午饭吃了啥,我也不记得了。

天气很好,有太阳,很温暖,我站在门口晒太阳,一位邻居路过看到了我,就过来聊了会天,聊的什么我也忘记了。

好几个邻居见到我都说我高了,30 岁的人了,还怎么长高啊?后来我妈说,可能是因为我瘦了所以看着显高了一点,似乎有点道理。

2022-01-07 周五

23:00 相亲(补)

大概是之前和父母吐露过心结,他们觉得我终于开始积极思考感情问题了,回来后找到亲戚让介绍姑娘,下班后,跟我说了一个姑娘的状况,说对方说了,不用长辈安排,我们自己加联系方式去碰面。给了我一个手机号和姓名,我加微信加不上,于是发了一段长长的短信过去,作为第一次相亲活动,我决定记录一下留作纪念:

你好,我是你邻居XXX介绍来认识的,我叫XXX。按照安排,明天咱们得聊一面,不嫌弃的话找个地方我请你喝点东西,不过地方可能得麻烦你找一下了,我在家时间少各个地方都熟悉。不知道你对这种活动什么态度,我头次参加,有点忐忑,不过觉得能聊得来的话不管最后怎么样多个朋友也好。如果你比较没兴趣也可以先告诉我,我不介意;如果有兴趣多聊一点的话,可以加我微信,这个手机号就行。

过了会对方微信加了我,说「发短信也太复古了吧」,经我提醒,她才意识到自己把通过手机号添加好友的设置关掉了。简单聊了聊,约了第二天见面。

2022-01-08 周六

16:00 相亲(补)

吃完早饭收拾了一下就出门了,坐车去县城,车上的时候我觉得特别烦躁。我到了后,对方提议说在她家附近体育馆碰头,我到了后等了会,见到了对方。

人挺好看的,也很健谈,我们就绕着体育馆外面的路边聊边说,她是学音乐教育的,然后也学了舞蹈,有编制,教小学的音乐和语文,聊了基本状况后就说一些学校的事情,边走还边给我介绍周围的建筑之类的;我的话就聊聊计算机和互联网,也被问到收入有多少的。回头一想,其实没有聊什么有价值的东西,基本就是了解各自的状况然后没话找话。到 12 点决定去吃个火锅,她地头熟,打了车去一个重庆火锅店,那重庆火锅点菜我可是当仁不让了,点上了炸腐皮、酥肉、贡菜等一些有特色的东西,其他的肉啊什么的也点了一些,不过最后很失望,贡菜软趴趴、酥肉里面全是肥肉、冰粉太甜了,最后吃了 248,这价钱我在壹圣元能吃得好得多啊。

吃完饭我就不准备继续下去了,因为她现在是在家里当老师,想考到江西其他地方去,但并不愿意出江西省,而我一两年内是不太可能回江西的,这是个很实际的问题。于是告别然后去我姑妈家做了下汇报(因为是她介绍的),然后就回家了。

回到家后,我和爸妈也摊开来聊了下,首先是表明了态度,我会去积极和人交往争取找到自己合适的人,但同时也希望他们不要急,因为我还是想先把心结解决。我觉得这个坦诚是有成果的,毕竟我这个心结,多多少少也有他们的原因在,说是我的心结,其实也是他们的心结。

22:00 共鸣(补)

去相亲的路上特别烦闷,就直接在微信的订阅号助手上,把 2021 年总结编辑好发了出去,顺手还发到了 E 大粉丝群里,收获了不少赞扬和肯定,有一个人在我的一篇公众号文章下的留言最让我感动:

不知道为什么,看这些闲言碎语有一种世界上另一个我的感觉,莫名其妙的想哭想笑

我坚信平凡的生活点滴才是最能打动人的东西,我坚信将来的我也能被我曾经这些平凡的生活记录所打动,我还会继续记录下去。

另外,文章发到群里后,又认识了几个新朋友,开心。

22:00 畅谈(补)

看到 LWL 关注了我的公众号,就去打了个招呼,没想到一下就聊了特别特别多,开心。

22:00 理想伴侣(补)

WJ 问我:你憧憬爱情吗?有想过理想伴侣吗?

我不假思索地回答到:我憧憬互相理解互相尊重互相促进的关系,是不是爱情倒没太在意。

我所遭受过的痛苦,绝大部分都源于人和人之间互相不理解,所以我会这样想吧,也许我是错的,这些都是需要验证的。我疯狂地做记录分析自己,其实也有一部分动机是这个,因为对自己的不理解同样是痛苦的来源之一。

2022-01-09 周日

21:52 遗忘

开始整理这一周发生的事情,我记得我似乎和爸妈聊了很多很多,但到底聊了啥啊,完全不记得了。疏于记录就是这种下场。

23:55 畅谈

又和 LWL 畅谈了一番,感觉很开心。她说「我们加微信这么久好像啥也没聊过。可昨天和你聊天,突然没了之前那种心态,感觉能聊的东西就多了」,我觉得这是因为,19 年加微信的时候有一个长辈介绍的因素在,就觉得不自然很奇怪,而这次我们开始聊上则是自然发生的,我并没有什么过多的想法,我只是想多个朋友,自然就能聊得开,反倒是这两天其他长辈、朋友介绍的人,大家都显得一副急匆匆的样子 —— 我们快点成为朋友吧、快点见见面吧、快点看看是否合适吧,我不喜欢这样。

LWL 问:那你需要的是朋友?

对,我想我需要的是朋友。也许我这种想法,还是有对亲密关系的恐惧?我不知道。

闲言碎语(2021/11/15-2021/12/01)

2021年12月1日 08:00

2021-11-15 周一

10:36 内推成功

之前一个 ZCH 说被裁了找我推荐,我看履历和我们公司的一个新业务方向还蛮契合的,就顺手给 HR 和领导了,上周接了 offer,这周就来报道了。这是我第一个成功的内推,就不知道奖励啥时候会下来。

2021-11-18 周四

22:41 误区

我觉得之前可能陷入了误区里,想着用 org-mode 完成结构化知识的存储、展示、编辑和使用,但其实没必要,存储完全可以放在 Neo4j 或者关系数据库里,org-mode 可以作为一个编辑和展示端就够了。

2021-11-19 周五

23:00 零公里

又去吃零公里啦,点了这些东西:筠连椒麻鸡、酒都肥肠、芙蓉鸡豆花、叙洲包浆豆腐、青笋烧肚条、油渣小白菜、缠丝肉、鱼香肉丝。

2021-11-20 周六

18:00 表弟近况

表弟一个多月没来问我问题了,问了问近况,说是进了老师的实验室,在各种参加比赛,说了几个,我只知道蓝桥杯。看起来前途大好啊,真好。

2021-11-21 周日

11:42 ENS空投

才知道 ENS 在空投,但要求是 10.31 之前在 ENS 上注册过 .eth 域名,并且根据时间来发放,错过了哇。

16:20 GPT-3

OpenAI 开放了 GPT-3 的 API,然而,不支持中国地区。

18:30 老同学

等理发的时候无聊找一位老同学聊了聊,得知他这周五刚答辩完,明年一月份拿到毕业证书,就正式博士毕业了,不容易啊。问他毕业后的打算,说是准备进公司赚点钱,挺好的,计算机的博士,出来的待遇会很不错的。最后我还开玩笑说看看明年六月份我这房子到期后有没有合租的机会,虽然是开玩笑,如果确实有机会的话也还不错。

2021-11-22 周一

21:53 再遇

下班,上地铁左下后看到对面就是 WY,约了周三吃饭。

2021-11-23 周二

08:28 噩梦

做了一晚上噩梦,好累好烦啊。

2021-11-24 周三

08:30 排挤

梦到全办公室的人联合起来排挤某个人,不知道这是大脑在快速眼动睡眠周期内拿我哪些记忆和想法加工出来的离奇故事。

12:05 失策

啊,发现 ZZ 是下午送餐,然后我以为是中午把和 WY 的饭约到了晚上七点……希望七点前能送到。

21:30 WY

和 WY 去管氏翅吧碰了头,聊了一个多钟头。互相聊了聊工作生活的近况,又七扯八扯聊了投资、算命、游戏、价值观,还给我介绍姑娘来着……挺久没见过的人这么聊一聊,感觉还挺好的。

2021-11-25 周四

21:00 光影

死去的、错过的那些人,就像身后的光,我看不到他们,但可以通过自己的影子感受到我仍然和他们联系在一起。带着这些影子活下去,也是意义之一。

21:30 感情

下午 WY 来问我要照片,并问我「你现在有进入一段感情的打算没」。我真的没有认真想过,而且呀,我在维护关系(不管是亲密关系还是朋友关系)这件事情上真的很白痴,如何表达好感和关心、如何保持联系与距离、如何建立信任与尊重……光是想象就已经让我觉得很麻烦了……

23:30 历史

我从 2016 年 6 月开始手工记账直到今年年初,积攒了大量历史数据,这两周在尝试把这些历史数据也纳入到 beancount 的管理中来,今天总算初步完成了历史数据和今年数据的融合、校验,至少数字上没有什么差错了,当然在这个过程中不可避免地开始对支出做细分和分组,今年的新数据的收支类型还没完全调整好。现在看 2016 的时候我的钱真是少的可怜,不过那点少得可怜的钱也从那个时候一直再慢慢地变多,看着那条爬升的线感觉很开心。接下来就可以把这些数据放到 Keysersoze 里得到收益率的分析结果了。

2021-11-26 周五

23:36 脆弱

人和人的关系真脆弱,不知道什么时候一两句话就会导致裂痕的出现。我无法揣测其他人的心思,但希望自己能做得好一些,比如说我现在还是会在某些时候表现出傲慢、冷漠的一面,有时候心情不好、压力过大也会把情绪带给别人。目前我起码能做到时时自省,在事后不久的时候觉察出自己的不对。

不过话说回来,和我走散的人,好像都不是因为闹僵了,而是因为我不去保持联系。

2021-11-27 周六

19:27 花钱

总算把 2016 年 6 月至今的历史账单都梳理清楚了,下面这张图是这些年的支出状况(略去了家庭支出和每年春节的长途往返支出):

expenses_since_2016.png

  • 餐饮一共花了 10 万,平均每天 50,虽然我觉得自己吃得还不错,不过这个均值确实看着有点低,主要是我在吃饭之外很少有别的餐饮开销
  • 很少去医院,所以医药这块花销很少,不过我不怎么运动所以花销少完全是因为还年轻吧……
  • 发红包那个支出,一大半是份子钱,我好亏哦

2021-11-30 周二

23:00 图形与衬底

被工作上的烦心事搞得心情很差,虽然我知道那些都是正常的、合理的事情。下班路上,随着这种厌烦情绪的滋生,仿佛为了对抗它一样,另外一种渴望也壮大了起来,想起了我想做的一些事情。正因为痛苦,欢乐才有意义,他们是一体两面的事物。

escher_two_birds.jpg

2021-12-01 周一

22:20 杨平兄弟

20211201_food.png

三位戴表人士和一名普通群众去了上周五没能吃上的杨平兄弟烤牛肠,再次在那老旧的大楼里迷路,错综复杂的大楼真的很有城市探险的感觉。点了一个三人套餐,量不太够,后面又加了一份牛大肠、一份嫩豆腐汤和一份海鲜饼。牛肠不差但并没有特别惊艳吧,主要是比较贵了,生拌牛肉和嫩豆腐汤非常赞。

闲言碎语(2021/11/01-2021/11/14)

2021年11月14日 08:00

2021-11-01 周一

11:00 新衣服

咦,买新冲锋衣啦,这个颜色很合适哦 —— 上周也穿了新衣服啊,哼。

2021-11-02 周二

18:00 体检

公司给我们发了导诊册,说我们可以开始预约体检了,说起来上次体检都不记得是哪年了,总之好几年没有体检过了。看了下体检项目,有点高端,除常规项目外,居然还有癌症筛查和基因检查。

22:31 陌生人

下班回去换到 8 号线上后,习惯性的玩着手机把自己和其他人隔绝开来,冷不丁的对面一位四十到五十的大姐看着我问到「这趟车最晚到几点啊」,也许并不是问我而是看着车门上方的运行路线在自言自语,但我刚好位于对方的视线区域于是下意识地回答了下,她后面重复了几次这个问题,中间夹杂着几个我没听过的地名,也许是想去那个地方?一场莫名其妙的与陌生人的对话。

22:42 美团买菜

快到小区的时候,被路边美团买菜的推广人员叫住了,说他今天指标完不成,央求我扫个码,那会不赶时间,想着薅个新用户羊毛也不错,就帮他完成了一个指标,买了几个西红柿、一盒牛奶、一个柚子,然后送了四个鸡蛋接着一分钱换了一小箱水,回来看还有满 29 减12 的券,还行吧。

2021-11-04 周四

22:35 玉流馆

中午聚餐,吃了闻名已久的玉流馆,服务员果然都很漂亮,但是菜味道一般,至少是匹配不上价钱的。中午人比较少,据说晚上会有节目表演。

2021-11-06 周六

11:30 疫苗

终于打了第一针新冠疫苗了。接种点就在我小区附近,走路 5 分钟就到了,排队排了半个多小时,打完 10:20 了,在留观区坐到 10:50 多没啥反应就回了。出来的时候外面已经在下着小雨了,没带伞,把羽绒服的帽子戴起来回去了。

16:30 风控

之前接到自称是火币客服的电话,说我的账户触发了风控,让我和他们风控部分的人对接解除风险后才能正常交易,当时在上班就没应付了两句说以后我再去看。下午又接到了这个电话,我还是想着年底才清退呢,我干嘛非得今天解决,就跟对方说行啦我知道了之后我再处理吧,结果对方说什么排号排到了我这里让我尽快处理最好就马上处理,让我下了个叫什么 TeamPro 的协作软件,我下完后开始觉得不对,这个软件明显和火币没什么关系啊,问对方对方也没正面回答就是翻来覆去说你赶快处理,我想着不是说不能交易嘛,马上打开火币提现了 100 个 USDT,告诉对方我能正常交易啊,对面很震惊说不可能啊,只能说演得真好吧。

18:30 雪

到晚上六点多的时候,已经下起雪来了。

2021-11-09 周二

17:30 拉新

接到一个电话让我加一个股票交流群,很直白就说是在完成拉新任务,让我在群里待两天就行,给我 3 块钱红包,爽快。

18:52 第三历史决议

百年党史革命、建设、改革的豹尾已经收官,全面建设社会主义现代化国家的龙头正在开启。中华民族迎来了从站起来、富起来到强起来的伟大飞跃,实现中华民族伟大复兴进入了不可逆转的历史进程。

23:27 工具

将自我看作工具的一个好处,是可以摈弃掉各种主观偏见,借用自己熟悉的领域知识(于我而言是计算机科学)来建立相对稳固、自洽的认知体系。比如说我目前是这样看待自己的:我是一个用庞大的编码量编制出来的智能系统,并经过了 30 年脏乱的数据训练,由于编码上有大量的不良设计和错误,加上训练数据质量不够好导致存在偏见,因此虽然勉强能够运作,但也经常出现 bug;这个系统的意义就在于处理接收到的数据,也因此这个系统的目标是能更好地处理数据 —— 至少要让 bug 越来越少;这个系统的优化方法,一个方向是在编码上着手以优化底层架构,另外一个方向则是用更多更丰富的数据训练来进行优化。如果我想,我完全可以把这套认知建立在数学、物理学、文学、中医、迷信、真香、下次一定等各种事物上,当然后面两个过于简单未必能撑起来一个自洽的系统,单纯是用来举例的,如果真有人用他们构建自我也不是不可能,不过大概率非疯即傻。

2021-11-11 周四

21:10 煎蛋

买的绿帽子煎蛋卫衣到了,试穿了下还蛮合身的,好看。

21:20 危

凌晨的消息,海淀有一个病例,离我住的地方也就五六公里,下午的时候 C 说他们附近小区开始组织测核酸了。我下班回去路上,小区后面空地上也看到了核酸检测的队伍,工作人员想凑够五个人,招呼我问我要不要做一下,还强调说免费的,不过后来里面说五个人凑够了,于是门口的小哥说让我想一下如果要测的话还得等四个人,那我就算了。简单聊了两句,说主要是在给商超里的人做检测。

2021-11-12 周五

10:10 冷漠

上班,到站的时候被比较暴力地挤出了车厢,觉得没太大必要啊,那个站好多人下的,着急那几秒钟干啥呢,不过还好,大部分时间大家还是平和有序下车的。这种上班路上匆忙的状态,人是会变得冷漠甚至暴躁,但我时常会想起几年前一次不知道去哪里时坐着的一个姑娘大概是看我比较累把座位让给我这件事情。

19:00 批评

开组会,领导(比较温和地)批评一个同事的工作没有规划没有产出,同事也有点委屈有点情绪,我大概也可以理解,纯做算法工作而无业务项目,是很容易陷入到一种长久没成果的状态里。

23:50 陌生人

晚上去吃南门涮肉安定门店,还来了 XKL 的两个朋友,都住在 XKL 附近。吃饭吃得有点紧张和尴尬,三个女性聊得很多,其他人偶尔能接一两句话吧。虽然有些紧张不自在,但观察到自己面对陌生人时的心理状态和反应,也挺有意思的。

2021-11-13 周六

20:45 外甥女

堂姐跟我说她女儿上高一后考得很差还哭了,请我去和她聊一聊。我和这个堂姐感情很好,但这个外甥女小时候比较怕生见得也不多,开始还担心会不会不愿意和我聊,加了微信后斟酌了许久写了一大段近千字的内容发过去,没想到她还看得下去聊了一会,可能是因为我理解学生时代的那种压力和心情,也表示出了尊重的姿态吧。她说她爸妈给她很多负能量,感觉我们那能当好家长的就没几个,这也是我们的命了。

2021-11-14 周日

17:33 日落

去河边走了走,行人从身边流过,年轻的妈妈拿着两片大叶子逗弄儿童车上的孩子,一只萨摩耶领着短发的女主人在跑步,被家长拽着后领防止掉下河的两个小孩趴在栏杆上看着水里的鱼儿乐不可支,两位阿姨一人站在路边一人站在干涸的水沟里讨论摄影技巧,两位男子谈着国家大事严肃地路过……夕阳从天边慢慢落下去,感觉很难过。

sunset.jpg

闲言碎语(2021/10/25-2021/10/31)

2021年11月1日 08:00

2021-10-25 周一

10:10 关怀模式

微信也有关怀模式了,就是文字、图标和按钮都更大的那种。

12:35 贝果

从 ZZ 那订的五个贝果(原味/黑芝麻/花生酱各一个及肉桂葡萄干两个)到了,查了下,贝果之所以叫贝果是因为英文名叫 bagel,是个音译,其实没有贝也没有果,看到非音译的中文名叫硬面包圈,这个就很好理解了。

23:57 冰冷

周末和朋友聊天,她说:

你内心太冰冷了,老是给人一种拒人于千里之外的感觉。

我是有察觉这点,但还是第一次有别人说出来,今天在想这个事情。首先,是不是呢?不太好说,我内心戏其实还蛮多的,但是确实会下意识地克制自己不怎么去表现出来吧,而且确实在一些至今仍然印象深刻的事件中表现出极大的冷漠;其次,这种状况我是否想要改变呢?确实是想的,因为这会阻碍我和环境的交互不利于自我的改进;然后可能的阻碍主要是是惯性和试错成本吧,具体的实践方法我目前并没有什么很好的思路,不过多感受、多表达,应该不会错。

2021-10-26 周二

08:50 壳聚糖

早上吃饭把口腔内部右侧咬破了,当时只是觉得有点疼,刷牙的时候吐了一口血出来吓了自己一跳,赶紧翻出来去年口腔溃疡时买的贴片贴到伤口上。看了下说明书,是一种壳聚糖成分,可以由几丁质(甲壳素)经脱乙酰化作用来制备,没有药物作用,在我这个事情上就是单纯帮助止血、防止口腔粘膜进一步损伤。

10:33 做T

上班路上做了个 T,赚了 300,还不错。

13:38 度小满

从微众银行转了点钱到度小满里,薅点新用户羊毛。

21:00 偶遇

在 15 号线遇到了前同事 WY,他说现在在一起作业网,教育公司嘛现在比较惨,说正在找下家准备跑路。

2021-10-28 周四

14:37 吐司

ZZ 提供吐司预订了,之前试吃过柠檬乳酪吐司挺好吃的,这次我本想预订一个柠檬的一个牛奶吐司,但是数量有限我只能订一个了,就订了个牛奶吐司。

20:30 照片

姑父又打来了电话,还是说上次那位介绍的学姐的事情,说让我发张照片给他他给对方看看,搞得还挺正式的,要我说要是双方都不抗拒先加个微信聊聊呗,这才哪到哪啊。

2021-10-29 周五

22:05 失控玩家

  • Buddy: So what do we do
  • Guy: Whatever we want

以前我们在《异次元骇客》和《黑客帝国》里讨论何为自我、何为真实和虚假,《失控玩家》虽然借助了类似的概念,但更像是一个成年人的童话,借着 Buddy 和 Guy 的问答,想像一个自由自在想做什么都可以的乐土,这可能是人类自古以来一直都有的共同愿望,新瓶装旧酒,看着乐呵就好了。

22:20 元宇宙

Facebook 改名 Meta 了,可以预见元宇宙这个概念还会继续火下去,感觉现在这样的态势,如果黑客帝国的世界真的降临了,自愿插上管子进到矩阵里的人怕是不会少。

2021-10-30 周六

17:30 No Time To Die

下午去看了新上映的《007》,挺好看的,主题曲部分很好听,回来后查了下原来歌手是 2020 年格莱美奖五项大奖得主,嗯,我喜欢她的声音。

看完电影慢悠悠骑车回去,一片半黄半绿的银杏叶过早地落了下来,打了几个旋落在了车框里。

2021-10-31 周日

11:11 DAO: 分布式自治组织

在区块链技术上衍生出来的一种组织管理模式,挺有意思的。由于技术普及程度、法规等个方面的限制,短期内大概不能替代现代公司,但是用来作为一个开放社区的管理模式感觉还挺不错的。

22:28 分类

梳理了下自己的知识管理体系,把内容分成了资料、笔记、知识体系三大块:资料是指用 Zotero 统一管理的文档,本地化存储,每个资料都具有唯一 URI,可以方便地引用;笔记是对资料的解读、理解、感受以及在学习、建立某个知识体系时所做的记录、产生的理解,准备统一用 org-roam 管理;知识体系目前还没个谱,期望是一个精心维护的、严格结构化的数据。以前的笔记往往三者混杂,这两年逐步形成了 Zotero 和 org-mode 配合的习惯,前两者其实已经区分得比较好了,主要是在笔记和知识体系之间摇摆不定,还是分开来想好一点。

23:00 十月总结

时间使用情况:

  • 休息时间 263 小时,平均睡眠时间不算低,不过我看了下,基本上都是周末多睡导致的,工作日的平均睡眠时间还是不够 8 小时,而且我的睡眠质量还比较差
  • 日常行为 139 小时,比前几个月多出来十几二十个小时,因为这个月我还是花了一点时间去处理 RSS 的
  • 休闲时间 106 小时,没什么好说的
  • 工作时间 116 小时,一共 15 个工作日(我 10.11 才回去工作),所以日均工作时间是 7.73 小时,感觉挺累的,我负责的项目基本上就是我一个人在推进
  • 社交(聚餐/电话/网上聊天)时间 9.5 小时,显著低于过往几个月,这个时间的减少大概也是因为工作
  • 花了 20 个小时来记账,简直离谱,主要是 8 月和 9 月有一个多月没有及时记账,国庆第一天就花了 7 个小时来记账,其实说是 20 个小时,但中间有大量的记着记着就走神了去看视频看 RSS 的时间
  • 在写作上只用了 3 个小时,因为只写了三篇文章,两篇“闲言碎语”,另外一篇也没花什么时间

工作上主要在推进项目,彻底沦为后端开发哈哈哈 —— 不过也学到不少东西,比如说 K8S、Django 我都比以前熟练很多了,所以我并不讨厌这样的事情。一个人做项目,虽然累,但也不用被效率不行的队友拖后腿,有什么想法马上就能去做,还蛮爽的。

投资方面,本月收益率跑赢了所有市场指标,本月累计收益金额在 10 月 26 日这天达到最高点,但离 9 月 13 日的今年最高点还是有一点距离。如果把证券账户从统计中剔除的话,会发现我的当月收益率和今年累计收益金额都大大下降,因为这个把月来大部分的指数走势都不怎么好,相反我场内重仓的招商银行,在三季报发布之后股价开始迅速地逼近了历史高点,为此带来了更高的收益,招商银行的股票我还会继续持有下去。

20211101_230617.png

生活上没有太大的波澜,希望十一月也能继续平淡,平淡的、日常的每一天是最难得的。

北青深一度和谷雨实验室全文RSS

2021年10月31日 08:00

之前《我制作的微信公众号RSS》这篇文章的评论里,有网友反馈了一些希望输出 RSS 的信息源,捡了两个看着顺眼的做了下。

北青深一度

地址:https://feedpress.me/bqs

制作步骤:

  • 找到北青深一度的网易号页面:https://c.m.163.com/news/sub/T1477387792079.html
  • 在 Inoreader 中添加订阅源,输入这个地址,会显示这不是一个有效的 Feed,然后点击「创建 Rss 订阅源」,按照提示创建

    20211031_132837.png

    不知道为什么,我今天想重现这个过程,老是显示「我们无法抓取远程网站」,所以无法给出后续步骤示例了。

  • 由于这种方式创建的订阅没有公开的 RSS 地址,要给别人用的话不太方便,好在 Inoreader 提供了对目录输出 RSS 的功能(限 Pro 用户),因此在 Inoreader 里新建了一个同名目录,把这个订阅移动到这个目录里,右键选择获取 RSS 订阅源就可以得到可以公开使用的 RSS 地址了

    20211031_133912.png

谷雨实验室

地址:https://feedpress.me/wx-guyulab

制作步骤

  • 用我的个人微信订阅「谷雨实验室-腾讯新闻」这个公众号,并记住公众号 guyulab
  • 在 Telegram 上将这个公众号关联到我专门的群组里
  • zs 生成 Huginn Scenario

    zs-rss gen-wx-scenario -n 谷雨实验室 -i guyulab -o wx-guyulab.json
    
  • 登录 Huginn 导入创建好的 Scenario

闲言碎语(2021/10/11-2021/10/24)

2021年10月24日 08:00

2021-10-11 周一

17:37 Firefox Relay

Firefox 提供的一个服务,可以生成一个邮箱地址,所有发到这个邮箱的邮件会转发到用户指定的邮箱里,这样就可以避免用户真实的邮箱地址被泄漏了。我以前注销了 Facebook,今天用 Firefox Relay 生成的邮箱重新注册了一个。

firefox_relay.png

2021-10-12 周二

13:57 调仓考虑

看 18 年到现在天宏中证银行ETF联接A和华夏中证500ETF联接A的比值,已经过了一个低点开始往上走,一方面是中证500今年持续涨了很久了,另外一方面是中证银行一直出于低估有估值修复的可能性,所以我在想,要不要把一部分中证500卖了换成中证银行?不过今天看场内500ETF在跌而银行ETF在上涨(虽然涨得不多),今天调感觉有点吃亏,到周四或者周五再做一下决定吧。

嗯,场外买卖不要太频繁,可以考虑以周为单位在每周的固定时间进行交易,这也算是一种交易纪律了,能建立起来这个习惯应该会不错。

compare_fund_001594_007994.png

22:33 对话

  • 来啦!还有酸菜、豆角的,其他的没了
  • 酸菜来两个,豆角来两个,粉条的还有吗?
  • 哦对,粉条的还有 —— 你好些天没来了
  • 假期就自己做着吃啦
  • 自己还会做啊
  • 嗯,那可不得自己做,不能天天吃外卖啊
  • 那是不能
  • 多少钱
  • 10 块 5,好几天没来价钱你都不记得了
  • 哈哈,以前也不记得,就记得大概
  • 下次得记得哈,要有点默契

2021-10-13 周三

17:17 留言

接到联合国儿童基金会的电话,聊完了让我填写一个问卷调查,结尾问:对于孩子们和联合国基金儿童基金会的工作人员,您有那些期待、鼓励或想说的话?请留下您的心声吧(这对我们很重要!)

花了点时间想了想,最后写了这么一段话:人类的未来这种话对孩子们来说是过于沉重的责任,我觉得爱、真、善、美才是让世界变好的希望,希望孩子们能感受到善意并也以善意回报身边的人,健康但普通地成长就好。

22:35 消息

今天得知 ZF 要离职了,说是是心理状态、情绪很差,之前大家其实也有所察觉毕竟他一直都表现得很丧嘛,加上我和他聊过几次知道一点他的价值观和想法,得知这个消息也不算很震惊,毕竟一个钱也赚了不少了又没法从赚钱中得到满足感的人,如果工作内容本身不够有吸引力,那么离开也是迟早的,或许我也会有这么一天?开完会和 DS 一起去和合谷吃午饭也聊了一会这个问题,DS 是很有责任心的人,觉得 ZF 不应该影响到工作或者说至少应该在影响到工作前及时表达出来,各有各的道理吧。今天工作内容很饱满,没有别的事情,平淡的一天。

2021-10-14 周四

22:29 执念

今天发现我又放下了一个执念,就是「希望能有一个人非常理解我」,一来这极其困难,二来自己都还没有足够理解自己呢。不过这并不是悲观的想法,只是一个观念的转变吧,而且因为预期降低了,有时候和人在一些非常非常微小的点上能共通时,也会非常地开心。

23:51 现金

把现金部分也纳入到 keysersoze 的管理范围里了,这样我就能看到我除了加密货币和公积金外所有其他资产的总体增长状况了。

keysersoze_cash.png

2021-10-17 周日

09:28 梦

梦到几个大学同学开了公众号,在朋友圈分享来着。工作后因为自己的性格就渐渐地和别的城市的同学少了联系,偶尔还是会想念上学时和同学们的关系的,会做这个梦,可能和昨晚看了一个视频有关系,一位 97 岁的哲学家讲述临近死亡时的恐惧和孤独感。

09:48 港美股入金

华美银行可以轻松网上申请开户,Google Pay 可以绑定华美银行进行支付和收款,Coinbase 可以使用 Google Pay —— 所以有加密货币的话,可以转移到 Coinbase 里然后卖出到 Google Pay,然后通过华美银行给港美股账户入金。不过我试了下,Google Pay 注册的时候不认 Google Voice 号码,Coinbase 一进入到交易页面就显示「我们遇到连接问题」,我猜 Coinbase 和 Google Pay 都有别的可用替代品。

17:27 豆瓣

豆瓣帐号原来关联的自己的 Gmail,今天给解绑了,想着反正都绑定了手机号,要不绑个微信看看,结果发现 2018 年的时候我用现在这个微信号创建过一个豆瓣帐号,只好注销掉再绑定了。想把各个绑定了 Gmail 的国内平台帐号都解绑,不过也不是什么着急的事情,看心情来吧。

19:52 传记

最近有看点传记的想法,不是看某个人,就是找很多个人,都看看,最好是和我专业无关的、不熟悉的那种人,看看别人是怎么应对生活的,比起天天自己在想可能会有意思得多。

20:05 改变

发现我已经不太容易看得进去 NLP/DL 的东西 —— 一方面是挺久没有直接做相关的工作了,另外一方面是从事多年工作之后对结构工程、模型翻新已经开始有点厌恶。

21:46 链接

在 Youtube 上看到《互联网体检》这个小品,太有意思了。看完突然想,Youtube 不知道有没有提供链接到视频特定时间点的方法,一查果然是有的,具体来说,在需要的时间点暂停,在视频上右键,然后点击「Copy video URL at current time」就可以,比如下图中我得到的链接是 https://youtu.be/kOYrj2wG63E?t=88 —— 仔细观察,这个链接其实是在原链接上加了 t=88 这样一个后缀,对应着视频第 88s,很好理解

youtube_position_link.png

嗯,接下来发现 Bilibili 也支持这个特性,比如 https://www.bilibili.com/video/BV1Px411c7RM?t=100 这个链接会跳转到视频的第 100s。

bilibili_position_link.png

其他视频平台的话,简单试了下:

国内平台除了 Bilibili 可能都不支持这个功能,不过无所谓了,Youtube 和 Bilibili 能覆盖我看的绝大多数视频了。

2021-10-19 周二

10:01 寒冷

我喜欢秋冬寒冷的天气,会让我冷静下来更好地思考问题,今天上班路上就久违地开始思考一些事情了。

23:57 时机

午饭吃饭,LY 好是抱怨了一通工作内容,我们各自也觉得方向太多人太少,多少有点抱怨。晚上和 LY、K 去吃烤肉和烤鳗鱼,LY 又稍微抱怨了两句,看得出来他心理压力很大。我也没什么可说的,但这半年多来,也确实感觉有各种问题吧,就像我之前写过的一样,是很充实,但如果自己并没有变得更好,那么这种忙碌就没有太多意义。早上我就在想,我想变得更好,那我就需要更丰富的环境(以及环境的反应)来激发、训练和检验自己,这个问题我也会持续思考并且进行自我观察,时机到了的话就顺其自然吧。

2021-10-21 周四

23:49 零公里

和 LY、K 下班后去了炎黄艺术馆新开的一家叫做「零公里」店,点了 TOP3 的宜宾燃面、李庄白肉、酒都肥肠以及三江豆腐脑花和一个老妈蹄花,燃面比常去的望花路燃面扁平很多,也不像望花路燃面那么湿,好像是 7 块钱一两很便宜,我们点了二两;白肉一片片都超大,夹上一片铺在盘子上看着就很有满足感,再自由发挥一下把肥肠、脑花放上去卷起来吃 —— 额会稍微有点腻,如果有点蔬菜什么的搭配一下会更好;酒都肥肠非常好,和去年百花山奔走 25 公里后在百花人家吃的肥肠一样毫无缺点;豆腐脑花很下饭但前面我一直在对付白肉和非常,导致我的米饭居然吃剩了。

2021-10-22 周五

23:08 二刷零公里

今天二刷零公里,相比昨晚,多喊到两个人凑上了五个人,点了昨晚吃过的李庄白肉、酒都肥肠和宜宾燃面,还点了小院跳水菜、花生浆菜豆腐、宫保脆皮肉、富油黄粑、陈皮兔、乡味腊肉和鲜椒热窝鸡。花生浆菜豆腐是一道汤菜,顾名思义,它的汤可能是加了花生打成的浆,花生的香气非常浓郁,喝汤的时候也能尝到细细的花生碎末,非常有特色;宫保脆皮肉外观色泽上有点像东北的锅包肉但口感完全不一样,很薄(相比锅包肉)又特别酥脆,轻轻一口下去就能咬断,里层肉的味道和油脂就出来了,咬断口能明显看到分层,但口感上却毫无感觉,很神奇;富油黄粑就是粽子的口感,没啥特别的;陈皮兔陈皮味道很淡,没啥特别的;热窝鸡是先煮熟再炒的做法,这种形式和我家里的做法是一样的,很家常亲切的味道。昨晚三个人半小时吃完,今晚也特地看了下时间,20:42 上的第一道菜,吃完用时 31 分钟,不愧是我们!

吃完结帐,一个经理还是店长之类的男子过来表示抱歉,但我没听到前面实在没搞明白是抱歉什么,大概是说我们吃的时候服务员也在吃饭了服务不周之类的吧,但我其实还蛮喜欢看到饭店员工们一起热热闹闹吃饭的,因为这种店的饭菜一般都好吃。完了他还送我们到店门口,豪气地说要以北京为起点辐射全国,能跟仅仅是顾客的我们讲这种话,不管信不信,但还是挺开心的。

他们家菜单特别厚,有很多在北京川菜馆没见过的特色的菜,所以准备多去几次都尝试一下,希望他们能经营下去~

2021-10-23 周六

22:00 克苏鲁

开始看《克苏鲁神话》!

2021-10-24 周日

21:30 沙丘

昨天买了《沙丘》的票,于是三点多出了门往小米总部附近的五彩城购物中心去,早些年住上地的时候也会去,所以还蛮熟悉的,只不过这会是从另外一个方向过去。16:20 的场,16:11 到了电影院,取完票就检票进去了。电影还是不错的,剧情基本尊重了原著,大概是小说第一卷的内容,虽然搞什么先知、预言不是我比较喜欢的故事,但也是合格的;画面很美,沙虫和一些巨大建筑很有感觉,在一些细节上展现了书里的设定,比如收集呼吸中的水蒸汽、汗水、泪水(甚至还有那啥)来循环用水的服装和帐篷、竖起大耳朵来收集露水的沙鼠,还有哈克南男爵因为过度肥胖无法行走只能给自己加了个便携式浮空器能让自己飘起来这个事情,电影完全没解释,我听到旁边的人说妖怪笑死了;音乐太吵了,大量的画面明明安安静静就好了,非得加上节奏特别急促的 BGM,我明白想要渲染紧张感,但是太刻意太吵了。

看完电影出来去优衣库买了两件衣服,犹豫了下要不要在商场吃饭,不过看了下都不太适合一个人吃就算了。

闲言碎语(2021/09/06-2021/10/10)

2021年10月10日 08:00

2021-09-10 周五

09:01 R.I.P

得知关注的那位推友已经去世。

2021-09-11 周六

21:34 仓位

今天又给 keysersoze 写了点代码,一个是加上了仓位变化曲线(下图右侧是仓位的坐标):

keysersoze_return_with_position.png

今年以来收益还可以,我一直觉得是我把仓位提高的原因,从上图来看累计收益和仓位是正相关的,算是验证了我这个想法吧。

然后就是在首页加上了各个账户的柱状图,这下写总结的时候可以直接截图而不是临时写代码去绘图了。

keysersoze_return_bars.png

2021-09-12 周日

09:34 无根之草

从龙崎那知道的,想玩,发现豆瓣上没有对应条目呢,索性去创建了一个,这还是我第一次在豆瓣上创建条目。

22:23 今日回顾

今天久违地做了一次饭啦,上午在叮咚买了点菜,中午做了个蒜蓉蚝油红薯叶和青椒炒鸡腿肉,并把昨天剩的那块鳗鱼蒸了,中午吃了个肚皮饱饱,照例剩了菜在晚上吃。明天早上的话准备炒个西红柿鸡蛋再搞个水煮西兰花,主食的话打算空气炸锅扔个红薯进去。工作虽然很忙,但偶尔过得好一点还是可以的。

今天一整天都是在玩,除了花了半小时把要写的一篇博客的思路捋了下外,没做什么其他正经事情。开始玩血源诅咒了,不过第一个 BOSS 都还没见着哈哈。

明天又要开始工作了,心累,早点休息吧。

2021-09-13 周一

10:13 早饭

昨晚还兴致勃勃地准备今天早饭的东西来着,结果 7 点闹钟响了后再眯了会就 9 点了……

2021-09-17 周五

08:02 噩梦

梦到有两个不熟的朋友带一个他们的朋友突然上门玩,有一个朋友要玩我的电脑,我不让但他偏要玩,我发火了让他们滚,他们起身穿鞋有一个人找不到第二只鞋。我和他们一起出了门,回的时候却找不到路了,在饭店大堂、阴森的小巷、熟悉但就是没有我的门牌号的楼道组成的迷宫里徘徊寻找。找没找到不知道,反正醒过来了,看到早晨鲜红的阳光从阳台透过来,听到忘了关的空调吵闹的嗡嗡声,手一伸摸到了手机,顿时安心下来。

做了噩梦,有时候会很害怕、很难过,有时候也会庆幸那些恐怖的事情只是在梦里发生。

08:11 桌角

左腿大腿外侧撞到了桌角,又要带着淤青好一阵子了。

08:45 锚

变化和混乱永恒存在,每个人都像是汪洋大海里的孤舟,除了组成船队互助,另外一个办法是备好船锚,在风浪到来的时候能够牢牢地固定住自己。伟大的理想、稳定的工作、钱……有很多东西可以成为锚,但除了这些大的、表面很有用的东西外,还有很多东西也可以,比如清晨的阳光、书页的芳香、夏天的风、冬天的雪、山巅壮阔的风景、熟悉的老店……越多越好。

2021-09-18 周六

20:00 学姐

下午开着会的时候姑妈打电话过来说要找我聊点事,因为正在开会和工作没有办法抽出大段时间,就简单聊了两句约下班后再聊,下班后刚到地铁站又接到了电话,和我猜的一样,说是觉得有个合适的姑娘看我们要不要聊一聊,还是高中时候的学姐。以前我对这种事情是比较抗拒的,现在觉得只是聊聊无所谓嘛。当然,说还要去看一下对方愿不愿意聊,兴许就没有后续了也说不定。

2021-09-20 周一

22:56 今日回顾

昨天约了 ZCH 见面吃饭,结果上午一直在下雨,有那么一会还犹豫了下要不要改时间,但想着还是今天把各种事情处理了比较好,就作罢了。出门被风一吹觉得冷,看了下气温才 16,于是把垃圾扔了后回屋换了厚点的外套,坐 8 号线往南到鼓楼大街站转 2 号线到了安定门,从 A 口出去后往左一直走就到了。

他比我先到,不知道是因为雨天还是什么,壹圣元人特别冷清,我一口气点了鲜鸭血、嫩牛肉、小酥肉、黄喉、毛肚、鲜豆腐、平菇、白萝卜、土豆,点多了最后没吃完,就把剩的平菇、土豆、小酥肉还有个什么打包了。两个人都是那种宅、内向的人,聊天没有很热烈,我主要聊了我做记录、做金钱预算的事情,感觉是有点用的东西,技术上的东西没啥可聊的。最后花了 350,果然是点多了。

吃完饭他坐公交我坐地铁,同走了一小段,他先到公交站,我继续走到了地铁站。2 号线转 8 号线的换乘路上,又遇到讨厌的辅警查身份证,我掉头去了另外一个换乘口,果然没人查,哼。回到家后又玩了会血源,然后开始准备给表弟要讲的东西,到七点的时候特别饿,可能是因为中午没吃什么主食,于是把包子热了两个吃了。八点半的时候表弟还没回我消息,后来说临时有事在处理,所以我继续等到八点四十多开始讲,到九点十几二十的时候就讲完了,并且录了视频。

今天就是这样啦。

2021-09-21 周二

22:44 今日回顾

问了下弟弟这三天的去向,去理了发,和爸妈打了电话。

明天又要开始上班啦,不开心,早点睡觉吧。

2021-09-23 周四

21:28 二建

我爸打电话过来,说在手机上看视频小说的时候看到了二级建造师的广告,加上周围有一些朋友也在说这个事情,有点心动,让我查一下相关的信息看靠不靠谱。我反正觉得挺不靠谱的,还好能来问我。

2021-09-24 周五

22:31 秩序

不管发生什么事情,保持生活的秩序,就能保证稳定住自己。但如果连生活的常态都无法维持了呢?

2021-09-25 周六

08:18 无眠

昨晚翻来覆去到很晚 —— 三点多才睡着吧,醒来后仍然觉得难受。虽然糟糕的事情现在未必能说发生了,但是有很多问题,我过去确实是在故意回避视而不见吧。有时候会想,为什么要让我来到这个世界承受这些呢?分外无助的时候,偶尔会觉得如果有个人互相支撑确实会比较好,双活容错负载均衡嘛。

10:48 生活

读到和菜头的《看完两边走中间》,心情稍微平静一些了,坦然接受自己的无知无力、生活的无常,这是这两年来我在努力做的事情,迄今为止我做得很好,保持下去就好了。

19:15 平静

写了篇博客,和朋友见了面吃了饭,终于感觉平静下来了。真要有事,也不是我在这急这一会有什么用的,没事的话我就更是干着急了。

2021-09-26 周日

08:33 梦

梦见我飞过山岗,地面有一堆奇形怪状生物的群落,他们飞上来要抢飞行器,被我不知道在哪里的友军击毁了。我飞进了一栋建筑里,熟练地穿过一间房子进入楼梯区域,然后在楼梯回环的空隙中直接垂直向下飞了几层到达一个有很多人的房间,穿过众人背后的小门到了一个狭窄的过道,然后进了电梯,电梯里有两个人,我和其中一个隐晦地聊了几句互相威胁对方,一起出电梯的时候,剩下那个没出电梯的人在电梯门合拢时的缝隙瞥了我们一眼,按下了录音笔的结束录制按钮,我和一起出电梯那个人打了起来。

22:34 降温

和爸妈打电话游说他们来北京,感觉是接近成功了,然后最近两天的焦虑也降下来了。

2021-09-27 周一

23:42 今日回顾

中午去探新店吃了裕兴记,平平无奇还吃出了 52 的人均,这价钱能在红京鱼吃到撑哇。

下班路上在 Inoreader 上读 RSS 来着,看到 hankcs 昨天发了一篇新博客,说自己不开心,点开想看全文却 404 了,当时还在路上没法去追溯,也没想太多。到家后打电话给我爸,询问来北京玩的事情,我爸说和妈聊过后都愿意来北京玩,但是前阵子我妈被狗咬到 4 号要打第二针狂犬疫苗,不知道该如何安排,说准备明天去询问下医生看能不能提前一两天打或者不打(觉得可以不打是因为伤口很浅几乎只有牙印)。打完电话想起 hankcs 的事情来,想着他的文章虽然删掉了,但作为一个还比较知名的网站应该 Google 有生成网页快照,去搜了下果然搜到了,看了内容顿觉不妙,觉得整篇文章都在发送求救信号,但是找了一圈都找不到对方的直接联系方式(邮件/电话/社交网络私信渠道),着急之下到 NLP 群来了个群发,直到有人发了一个疑似他团队的人的联系方式,我犹豫了下加了微信,但没有得到回应,干脆打了个电话过去说明情况,还好对方确实能联系上 hankcs,并告知我对方人没事。之后 hankcs 给我发了一篇简单的感谢邮件,说自己「已经完全恢复了」,但我觉得,一个昨天还通篇宣泄绝望情绪的人,怎么可能就「完全恢复」了呢?于是又啰啰嗦嗦写了一大堆话,也许我是反应过度了吧。

2021-09-29 周三

22:19 熟悉

包子铺的老板已经记得我不吃茴香了,希望她的店铺可以一直开下去。

2021-09-30 周四

13:33 景点

故宫只能买到 7 号的票了,颐和园、圆明园、恭王府倒是还都有票,不过向来人都会很多。这些都是从北京本地宝看到的,然后从各个地方进出京是否要做核酸检测这些,在北京本地宝上都能查到,还是很方便的呀。

2021-10-01 周五

22:16 记账

有一个多月没有整理日常开支的账单了,有点混乱,今天花了大半天时间来整理他们,算是基本上记录、校验完了,但是信用卡账单部分因为支出和还款的时间间隔比较大而有点乱,还需要进一步梳理细节。

2021-10-03 周日

20:01 公益

B站看到一个支教的姑娘的视频,就去给联合国儿童基金会开了个每月定期捐助,只用出钱可以,真是偷懒啊。

22:10 明天

明天准备早起,开始全力为父母来北京的事情准备,希望能把这个国庆过好!

2021-10-05 周二

23:48 接站

火车晚点了十几分钟,接到爸妈的时候已经快十点了,然后发现我爸提着一箱带给我的东西,我拎了下挺沉的,就挺无语的,电话里反复说不要带太多东西了……接下来没啥好说的,9 号线转 6 号线转 8 号线,我爸赞叹地铁通风和光线很舒服,我妈则感叹地铁生意好,然后跟我念叨说玩个两天就回去,听到我酒店订了五天两人纷纷表示可以住你那为啥要订酒店还一订五天啊 —— 他们不想待太久,一方面是不想花太多钱外,但他们大概不知道酒店的钱也就和一个人过来的车票钱差不多;另一方面是,因为怕家里东西没人照顾把我外婆叫到了我家,就担心年岁已大的外婆一个人在家能不能照顾好自己。出了地铁后发现下着不小的雨,还好酒店就在边上,办完入住我爸拎着东西和我一起到了我住的地方,因为里面有肉必需得放冰箱,一路走来把鞋也弄湿了,如果少带点我自己就能拎过来就不会这样了。火车上我爸没吃,我从冰箱拿了香蕉和苹果让我爸对付一下,拆了箱把肉类都放进速冻,就下楼把我爸送到了小区门口。

挺折腾的一天,后面还有四五天,加油吧。

2021-10-06 周三

19:54 第一天

今天爸妈到我住处,一起做饭吃饭聊天,早饭做了米粉,中午炒了野猪肉(啊吃野味不好但超好吃耶)、芥兰,做了芋子粥(超香),晚上炒了鸭杂。聊了一整天的家常,感觉好累,今天早点睡吧,明天去天安门和恭王府。

2021-10-07 周四

20:00 第二天

今天带父母去了天安门,走在对面路上的时候,我父母都有点失望地说「感觉天安门不像电视上那么雄伟壮观」,不过到了金水桥上还是开心地各种拍照。本来是打算去了天安门后再去南边的毛主席纪念馆,结果天安门的路线是单向的,不允许原路返回,绕回去的话时间就不够了,就把预约取消了,走去了北海公园散步,教了他们怎么拍全景,拍了鸳鸯、坐了轮渡。从北海公园出来后去德胜门内大街的峨嵋酒家吃了午饭,然后由于我爸兴致勃勃地说想看看胡同,就走兴华胡同去了恭王府,其实胡同也就那么回事啦……恭王府是挺豪华的,我们在里面迷路了几次,到后花园又迷路了几次,某个展厅里说大观园原型可能是恭王府或者至少参考了恭王府,这个我以前不知道,算是有收获啦。

2021-10-08 周五

20:14 第三天

父母昨天表示还是想看看毛主席,于是再次预约了今天,比昨天方便的是今天天安门东站和天安门西站都开放了,就在天安门西站下车,得以少走一点路,因为时间还没到就在天安门广场上闲逛,看大花篮、人民英雄纪念碑和站岗时偷偷玩手机的武警小哥哥,顺便找一个好心、时髦且对拍照质量要求比较高的阿姨给我们拍了个三人合照。时间快到的时候去问了下工作人员,才知道要去毛主席纪念馆瞻仰,必须先到东边马路对面国博那里把包都存了,让父母在广场上等着,我一个人过去把包存了,存包处要求游客展示预约单才能进去存包但很多游客要么没预约要么预约了找不到,各种原因交织下工作人员火气很大,我当然是做了很多准备工作顺利完成各项手续,然后回去排队飞速完成瞻仰仪式。完了去前门小吃街廉价便捷地让我父母了解北京的一些基本元素,非常凑合地在东来顺让我父母了解了下铜锅涮肉,并被 LY 强烈谴责,但也没办法啊,带着两个在北京没啥自理能力的孩子,实在是没有精力去精心挑选吃食,更何况昨天人均 70 的峨嵋酒家还被我妈念叨了一路直到晚上。下午去了故宫,很明显刚进门后父母被宽阔的大广场、宫殿群震撼到了,但是看完三大殿到后面乾清宫、交泰殿、坤宁宫的时候就有点审美疲劳了,不过钟表馆和珍宝馆还是让他们惊叹连连,虽然到后面也麻木了,只是在出来后多次表示「皇帝的生活真是无法想像」。回程路上我妈突然蹦出来一句说想找个地方买北京特产带给亲戚,我血管都要爆了,中午逛前门的时候我都问了几次要不要买点特产带给亲戚那个时候明明说不要,想一出是一出真的和小孩没两样,于是我在 8 号线到鼓楼大街的时候说要不干脆下车我去找个店买吧,大概是觉得太麻烦我了又忙表示说不要了就是看看我住处附近超市有没有有就买没有就算了,我爸开始数落我妈,我赶紧阻止他习惯性讲各种大道理长篇大论以免形势进一步恶化,要我说我爸那些官场小说真的是白看了,当我责备我妈的时候,他应该声援我妈来达成平衡啊,两个人处不来真的是两个人自己都有一大堆毛病偏偏还都觉得是对方的问题。

回到住处后,为了解决问题,就去附近永辉超市带我妈逛了一圈,买了明天火车上吃的东西,顺便看了一下确实没有什么烤鸭之类的卖。然后在附近湘菜馆点了菜,并被父母大加赞赏表示好吃和家里味道差不多,最后珍珠丸子剩了三个我打包带回来了。累啊,不过明天就能送走他们了,我可以休息了。

我的生活记录经验及个人工具与方法

2021年9月25日 08:00

中学的时候,我开始写日记,记录琐事和当时再正常不过的一些小心情,就这么一写一直写纸质日记到 2020 年。工作之后,我把上学时候写的日记也带在了身边,不过中学时候的只剩了高三复读时候那一本,早先的记得应该是被我以一些现在看来蠢蠢的理由烧掉了。偶尔兴起会翻一翻自己的日记,重温当时的一些感动,记得有一年翻日记翻到和大学同学的事情,觉得特别感动,没头没脑地给同学发了条信息说「想起当时的XXX觉得好温暖啊」,对方大概会觉得莫名其妙吧。

diary.png

图1  我的纸质日记

我经常想起曾经朋友见到后会说的两句话,一句是“你变了”另外一句则是“你一点都没变”,每次听到这两句话,都特别想问问,对方看到我没变的东西是什么、变了的东西又是什么呢?当然,有些人我可能永远也没有机会去问这个问题了,但幸好我还能从我的日记里,大致地知道自己发生了怎么样的变化,而不是只靠回忆里那些印象深刻但寥寥无几的事件来知道自己是个怎么样的人。当然,在开始写日记的那些年里,我并未意识到这些价值,只是惯性和表达欲使然,再加上潜意识里觉得记忆不靠谱产生了把一些有价值的事情记录下来的想法吧。真正发现记录的深刻价值是在我工作后。开始工作后,我发现自己有很多东西要学、有很多事情要做,每件事情又充满了不确定性和意外,且有时候极其复杂,于是开始在本子上写工作日志 —— 说是工作日志,其实也没有什么章法,写得也很潦草。据说招我进去的领导特别喜欢我学习和记录时候的认真劲,不过工资太少了最后我还是跳槽了。

pachira_journal.jpg

图2  一篇调试错误的工作日志

从 2015 年第二份工作起,我便开始在电脑上记工作日志,简单做了下统计,去掉空行和空格后,从 2015 年 3 月 10 日到今天(2021 年 9 月 25 日),累计写了 15.3 万行共 734 万字,平均每天写 64 行共 3070 字,也不算很多,但是能一做就是这么多年,我还是挺自得的。我的工作日志按日期来顺序记录,都是尽量追求把处理问题的完整过程(包括思考和执行)都记录下来,可复现性还是比较高的,现在同事如果问一个几年前我经手过的事情,我基本上都能在日志中找到过程,如果有必要的环境把当时做的事情重做一遍基本也可以做到 —— 事实上我自己在工作中隔了半个月甚至几个月要重新做一件事情也并不少见,这种情况下工作日志也帮到我很多。

2017_work_journal_sample.png

图3  2017年工作日志片段

到 2018 年下半年的时候,我开始关注自己的时间使用情况,用 Life Cycle 这款 iOS 软件对自己的日常时间开销做一些粗粒度的自动记录,并且在年终总结(见2018年总结2019年总结)的时候用来做一些简单统计和分析。说实在的,这个记录的粒度非常粗,大概就是根据我的位置之类的,判断我在什么场所做什么事情,如果我在一个场所做了两类不同的事情,就需要我自己去手工修正类别,但因为它自动记录和分类的特性,作为一个初步的尝试还是不错的,用了几年后,至少对自己的时间使用情况有了一个数字上的认识。

life_cycle.jpg

图4  "Life Cycle 记录"

如果说写工作日志是为了更好的工作,那么写个人日记和做时间记录则是为了更好地认识自己,而 Life Cycle 这样粗粒度的记录并不能满足我的要求,于是在 2019 年下半年开始,我开始摸索更适合自己的时间记录方式,并在 2020 年逐步进行完善后开始正式使用,目前基本上能把我每天的时间使用情况按事情精确到分钟级进行记录,虽然不是自动化的,但也不会耗费太多精力,写今年的半年总结的时候就用到了这个方法产生的数据。

对于我的记录方法,有若干网友在我的数篇文章的评论里表示感兴趣,所以在这里简单说一下,其实并没有什么很复杂的东西,就是用 org-mode 的 org-agenda 功能,下面是我的 agenda 视图:

my_agenda_view.png

图5  我的 agenda 视图

在这个视图里,我可以按一下 I(大写) 开始某项任务的计时,按一下 O(大写) 则结束任务的计时,操作是很简单的,我要做的是调整一些短期任务让他们显示到「今日事项」这个区域里,这种短期任务一天不会超过十件,只需要在每天做梳理和计划的时候处理即可,而这个梳理和计划我又是通过图上「写今日计划」来进行的,所以这个梳理和计划的耗时也可以得到记录。用 org-mode 来管理任务,这个对 org-mode 用户是很自然的事情,我做的一点点微小创新(其实是不是创新都不好说),就是把一些日常的事情作为“纯计时任务”纳入到这个体系里面了,就这么简单。公司有同事看到我这个方法后有和我讨论,他不用 Emacs 更别说 org-mode 了,但其实是可以借鉴思路的,要能把这个记录过程进行下去,最根本的一点就是操作要尽量的简单和快捷:(1)任务的查找和定位要快,org-agenda 的好处就是提供了一个统一的视图来展示不同来源的任务,其他工具如果也能做到这点的话也会很棒;(2)任务的计时操作要简单便捷,最好是按一个键就能完成。基于这个共识,我们讨论了几种可能的方案:(1)使用 Alfred,自己写点 Apple Script 来定位任务、进行计时;(2)使用 zsh,通过自动补全功能来迅速定位任务并进行计时。

如果对我用 org-agenda 进行时间记录的方法有兴趣,可以看看我准备的示例配置:https://github.com/Linusp/org-agenda-example

需要强调的是,对我来说,自我记录和自我管理,是两个完全不同的事情,我绝对不会想要把这两个事情混到一起去 —— 记录强调时序、强调过去、强调忠于事实,而管理则尝试控制未来的走向,这对我来说太难了,而且我也不认为人能要求事情的未来按照自己的要求或期望进行。我们没有办法减少未来的不确定性,只能去应对它们,但通过记录来减少对过去的记忆的模糊,是每一个人都能去做的。

闲言碎语(2021/08/02-2021/09/05)

2021年9月5日 08:00

目录

2021-08-02 周一

22:00 打断

我不喜欢别人打断我,但我其实也经常打断同事来着,这个其实也不太好……

22:00 幽邃

《黑暗之魂》:发现幽邃教堂可以直接从顶上走去罗莎莉雅的篝火,这样可以跳过被帕奇坑的阶段,让他直接回传火祭祀场,然后买装备在净身小教会救洋葱。

2021-08-03 周二

12:06 复利

其实,从表面来看,基金是没什么复利的,净值型产品,所有盈利都是纸面上的,分红一般也不多。

2021-08-04 周三

08:28 鼻炎

半夜两点醒过来了,鼻炎导致呼吸不畅 —— 还不是堵住那种不畅,就是不舒服……

2021-08-06 周五

07:59 充实

因为忙碌而感到充实,但这忙碌并没有让我变得更好,充实我的是什么呢?

2021-08-08 周日

09:14 梦

醒了,朦胧间摸到枕边的手机,看到两条朋友在 8 点多发过来的消息,说梦到和我在家里的小酒馆吃饭聊天。

我好久没有梦到过什么人了。

16:06 断电

半夜断了一次电,有一些设备因为断电发出了短促的蜂鸣声把我吵醒了一会,没想到下午又断电,半小时内断了三次吧,不知道在干嘛。

[2021-08-08 日 19:00] 有病啊,七八次了。

2021-08-09 周一

08:16 断电原因

昨天下午到晚上断电十几次吧……今早物业上门了,问家里有没有什么电器一开就跳闸,说他们昨天排查了一天并没有发现具体的故障,只能猜测是谁家里的电器有问题,正在挨个上门问。能是啥问题啊,电器短路了,还是功率过大?

21:30 disqus

通过朋友在我博客的评论,我才知道原来现在 disqus 强制展示广告了,我可能因为是老用户才可以自己选择是否开启广告。虽然不太喜欢,但是人家也是需要生存的,只能说可以理解吧。

2021-08-10 周二

22:58 中签

第一次新股中签!

2021-08-11 周三

12:22 蛋卷基金

发现蛋卷基金是有网页版的,简单试了下导出交易记录成功了,其他的应该也可以。

14:00 倒三轮

在公司门口看到一辆倒三轮的车,挺好玩的,大概是下面这个样子。

tricycle.jpg

22:32 咸甜

饭罪吃重庆火锅,发现椰奶冰沙和红锅中捞出来热乎的鲜鸭血特别的搭,鸭血的滑嫩和冰沙的绵密特别般配,前者轻微的咸辣味和后者的甜腻以及奶香交织在一起产生有层次的、丰富的味道,冰沙的冰爽又极好地压下了刚从锅中捞出的鸭血的热辣,于是连着这样吃了好几块鸭血。这种吃法让我想起小时候在潮汕吃过的西瓜,老板用漂亮的手法将西瓜切成小块,和刨冰一起装进塑料袋,然后浇上辣椒油,好吃极了。

2021-08-12 周四

10:28 感动

又用上 lain 啦!用了半年 kubectl 和 helm 了,还是 lain 好用。

lain.jpg

23:35 链接

Telegram 里每条消息都有对应的链接,Slack 也是,这非常好。之前在思考自己的知识管理系统的时候,发现会有部分信息是从朋友那里来的,通常是在Telegram/Slack/微信这些地方聊天的时候产生的,虽然我能把元信息尽量记录下来,但能追溯到发生时的场景当然会更好一些,这个发现算是解决了我这个问题吧。我的微信消息大部分都转发到 Telegram 了,所以微信消息的问题也相当于解决了;飞书作为协同工具和 Slack 算是一类工具,就去看了一下飞书是否有类似的功能,结果当然是我想多了。

telegram_message_link.png

slack_message_link.png

2021-08-13 周五

08:47 图片

想要一个本地存储的、开源的图片管理工具,希望它在基本的图片管理功能基础上,还能像 Zotero 一样能生成文献引用数据库,提供 URI 供外部程序进行链接,找了一圈并没有找到 Linux 上能用的,看到一些直接用 Zotero 来做图片管理的,但 Zotero 连预览图像都做不到,也没法提取图片的各种元信息,如果有插件能解决这些问题的话或许可以试试。

2021-08-18 周三

13:30 一次对话

记不太清,略去很多细节

  • X: 可能和我的家庭和性格有关系,我就想找个小城市,不想干互联网了。
  • 我: 不想工作这个念头大家都有,我和其他朋友或多或少都聊过这个话题。
  • X: 看大家工作很有热情,我还以为是我的问题,现在就后悔从前司离职后没有好好玩一段时间。
  • 我: 我就有一个疑问没法得到解答,如果不工作了该干嘛呢?岁数小点的时候,会有一个大的目标或者说理想,认为那个是自己的方向,但现在没有了,可能不工作的头两年各种玩,会觉得很开心,但可能过了头两年,就会不知道该干嘛了。
  • X: 嗯,我懂你的意思,就是没有了那种很强的生活的驱动力。对于你这个问题,我能想到这几种情况,一个是人生进入一个新的阶段,另外一个可以结合自己的能力做一些小的能赚钱的事情,而不是像互联网玩流量(举了一个例子)。
  • 我: 人生进入一个新的阶段,比如说结婚、买房、生孩子?
  • X: 对。
  • 我: 第二个,我最近也有类似的想法,我觉得再做一个对所有人或者说大部分人都有价值的东西,在现在的互联网环境下,可能不太现实了,但是还是有很多小的群体,为他们专门提供一些服务,还是有价值的。

2021-08-20 周五

20:00 损失厌恶

今天中国电信上市,集合竞价阶段涨 20%,但我不知道那个阶段能卖,等 09:30 开盘后涨幅迅速收窄至 3%~5%,我怕还会继续跌,就以 4.68 的价格卖了,扣掉手续费后只赚了 140,结果中午价格又涨回去了,仔细一算我原来能赚近 1000,但最终赚了 140 就跑了,虽然实际上行还是赚了钱,但心理上还是认为自己遭受了损失。

2021-08-22 周日

22:00 小饭桌

  • 参与者:ZZ、YS、我、K
  • 食物
    • K 从家里带来的羊肉,做成的红焖羊排
    • K 带来的酱牛肉,LY 淋了点红油
    • 鲜椒跳水鲈鱼
    • 拌三丝
    • ZZ 和 YS 带来的柚子醋鸡皮、酱油麴烤鸡排、热炉月饼、绿茶酥
  • 饭后活动: 打《双人成行》最后两章,顺利通关
  • 去 LY 家附近吃晚饭,点了一个尖椒焖蛋、油渣炒油菜和一大碗的面炕肉,吃撑,第三个菜没吃完,打包我带走了
  • 饭后 LY 骑电动车送我到地铁站

20210822_food.jpg

22:42 中元

从地铁回去的路上,在一个小区的出入口附近看到十几堆灰烬,晚风吹过,火星四散,烟雾在地面弥漫。死者已逝,只是活人想要记住死者,并在这种祭祀中强化那些自己挑选过的回忆,以便使自己得到慰藉。我父亲有时候会说,如果我祖父还在世就好了,能看到我考上了大学会很欣慰,但我记得他在世时苦难繁多,只是因我欣慰那么一两回就要继续承受那些,我觉得那对他不公平,说到底也不过是我父亲的一厢情愿罢了,但毕竟那是他的父亲,他有更强烈的执念也理所当然 —— 我印象中的祖父就只是那个在墙边端着水杯晒太阳看报纸的沉默老人罢了,年少时的记忆缺乏记录,早已不记得除此以外的大部分细节。

23:53 模糊

只是一周之内的事情,当时忘了记录,就已然模糊。

2021-08-23 周一

早上没吃早饭,最近这种状况挺多,久了对身体不好。

在去公司的地铁上,把闻泰转债卖掉了,并卖出了周五买入的招行 —— 目的是把招行从华宝证券上转到银河证券上,所以并没有赚太多。

刚到公司,表弟又来没头没脑地问域名和网站的事情,说也说不清楚,还说什么「你操作一个论坛应该难度不高吧 实在不行我去北京找下学习」可真是吓到我了。多聊了会后得知他虽然在做运营,但对技术有兴趣所以自己在学,最终目标是能自己开发和维护论坛一类的网站,估计报了一些培训班或者网课学了点皮毛表面的东西,学这点当然解决不了他所有疑惑就来问我,但也问不到点上。我确定他有兴趣后,承诺给他打点基础,约了这周日先和他讲下基础的东西,所以这周也要准备一下资料了。

(此处略去工作相关的事情 430 字)

这阵子一个同事对我的反应让我有点不太适应,今天惊叹我「有这个项目的代码,好厉害」(大意是原来以为我作为算法工程师只会做一些研究性质的事情结果我居然真的会写工程代码所以感到震惊),然后聊需求的时候在边上看到我的编辑器问我是不是用的 Emacs 并在我给予肯定回复后惊叹「终于见到用 Emacs 的了」。我对他了解尚浅,不知道他是单纯的惊叹还是捎带了猎奇的心态,所以对于他这些惊叹,我都以自嘲或者无视的态度应付过去了。

晚上回来后,把昨天打包的肉热了吃了点就当晚饭了,非常不讲究。然后又花了一个多小时梳理我的证券账户,这一个多月没记账带来的混乱后遗症,估计还得花点时间处理掉。

写了连续的、较长的日记,体验和写碎片化的一些事情还挺不一样的,能让我把混沌的记忆确定下来,我挺喜欢的。

2021-08-24 周二

14:00 两元甜筒

吃完午饭路过蜜雪冰城,得知两元甜筒不再卖了,不开心。

17:14 压力

会议室突然传出来一个人大声说话的声音,带着点快要哭泣时的那种沙哑,大意是在说自己很累压力很大,应该是运营或者销售团队的人。

23:33 日记

今天天气特别的舒服,有秋天那个味道了,被电动车俘虏的 LY 今天午饭的时候也为了享受凉爽的天气而放弃了电动车改成骑车去吃午饭了。除了凉爽,照例是忙碌的一天。

晚上回来总算把证券账户的账理得比较清楚了,至少银河证券已经能完全对上了,华宝证券明天看看。然后开了黑魂3把绘画世界的图跑了跑,点了所有篝火,尝试打了下丹涅尔和舞娘但都失败了。

剩的那点面炕肉我晚上都吃了,好吃是好吃,但是肥肉太多了,后面好多我就把外面裹着的面吃了,里面的肥肉丢掉了。

2021-08-25 周三

21:03 臃肿

今天是被 pytorch 以及附带库折磨的一天,产生了把 pytorch 从我主导的每个项目的依赖里删掉的冲动。

2021-08-27 周五

10:06 呆

出门去上班,坐电梯下楼,电梯门一开看到楼栋里那两只大型但比较乖的狗,还以为到了六楼还是七楼,傻乎乎地跟狗主人说「你进来吧我不怕的」,狗主人无奈地说了句「一楼」,我才反应过来,尴尬,于是夺门而逃。

21:09 Fight or Flight

我的下意识行为应该是逃走,感觉在工作中遇到过很多次了,遇到工作压力比较大的时候,会下意识夸大难度或者摆出放弃姿态,「你们定就好我随便,让我做什么就做什么」,大概这样。事后当然也会反省自己,但是再怎么反省,压力也不会消失。

2021-08-28 周六

11:53 洛斯里克

早上爬起来又玩了下黑魂,居然两遍打过了舞娘,二阶段躲着大风车就好,其他的招式还是能翻滚躲一躲的,太怂反而不容易打过。打过舞娘后一路推到猎龙铠甲那,把伊果和希里斯都召唤了出来一起打,虽然猎龙铠甲血量多了很多,但在这个孤独的游戏里能有叫上朋友(虽然是 NPC)一起并肩作战的机会,当然是要用一下了。

2021-08-29 周日

23:52 实习

到开学了,弟弟为实习的事情大概有点着急,投了十几家了只有两家有回应,又觉得一直待在家里待不住。除了帮忙改改简历指导一下方向,我目前也做不了什么事情,还是得看他自己呢。

2021-08-31 周二

10:06 音游

地铁里看到一个姑娘在手机上横屏玩音游,手速飞快,我看着都眼花。音游我还是不擅长呀,好像也就玩过一个 QQ 音速,看了下,这游戏国服 19 年停止运营了哈哈。

10:34 算法推荐管理规定意见稿

http://www.cac.gov.cn/2021-08/27/c_1631652502874117.htm

  • 用户自主权,举双手赞成

    第十五条 算法推荐服务提供者应当向用户提供不针对其个人特征的选项,或者向用户提供便捷的关闭算法推荐服务的选项。用户选择关闭算法推荐服务的,算法推荐服务提供者应当立即停止提供相关服务。
    
    算法推荐服务提供者应当向用户提供选择、修改或者删除用于算法推荐服务的用户标签的功能。
    
  • 要求存档六个月的日志及相关信息,应该是指给用户推荐了什么之类的记录?

    第二十三条 具有舆论属性或者社会动员能力的算法推荐服务提供者应当按照国家有关规定开展安全评估。
    
    算法推荐服务提供者应当完善算法推荐服务管理机制,对算法推荐服务日志等信息进行留存,留存期限不少于六个月,并在相关执法部门依法查询时予以提供。
    
  • 订单分配应保障劳动者权益,具体不知道该怎么执行

    第十七条 算法推荐服务提供者向劳动者提供工作调度服务的,应当建立完善平台订单分配、报酬构成及支付、工作时间、奖惩等相关算法,履行劳动者权益保障义务。
    
  • 禁止大数据杀熟

    第十八条 算法推荐服务提供者向消费者销售商品或者提供服务的,应当保护消费者合法权益,不得根据消费者的偏好、交易习惯等特征,利用算法在交易价格等交易条件上实行不合理的差别待遇等违法行为。
    
  • 互联网信息服务算法备案系统

    第二十条 具有舆论属性或者社会动员能力的算法推荐服务提供者应当在提供服务之日起十个工作日内通过互联网信息服务算法备案系统填报服务提供者的名称、服务形式、应用领域、算法类型、算法自评估报告、拟公示内容等信息,履行备案手续。
    

2021-09-01 周三

07:58 噩梦

梦到现实中可能发生的糟糕事情,没有比这更恐怖的了。

2021-09-02 周四

08:00 秋意

晚上不盖东西睡觉已经会觉得冷了。

21:56 新高

经过了一个多月的低谷期,我的投资的累计收益终于回来并且新高了,不容易啊。

20210902_keysersoze.png

注:上图显示的是收益金额不是收益率,由于我不想把我真的赚了多少钱展示出来,就把所有数据除以最大值相当于做了一个隐私保护,所以图上的 1.0 并不是说我有了 100% 的收益。

2021-09-05 周日

21:04 三十周岁

(生日在工作日,本来 ZZ 提议周末去吃湘彬萱和她做的蛋糕,但我忽然很想吃珍滋味,就提议去珍滋味请大家吃顿饭,最后变成了在 ZZ 家六人聚餐,ZZ 安排外卖了珍滋味的火锅、采买了各类海鲜肉类,我就只用安心出钱了)

昨晚三点多才睡着,醒来仍然头疼,吃了点东西玩了会后洗了个热水澡精神才好起来。天气阴凉,出门后还能看到夜里下雨的痕迹,等我坐地铁到 ZZ 家的时候 K 已经到了,随后 XKL 也到了,LY 惯例晚到。珍滋味的外卖到的稍迟了一些,先吃了 ZZ 做的提拉米苏 —— 我对甜点没什么研究和鉴赏能力,嘴里只能蹦出来甜和好吃两个词,不过其他人评价都很高啦,说不比外面店里卖的还好,因为用料很好嘛。等珍滋味的瑶柱粥锅底和佛跳墙锅底到了后就正式开吃了,两份锅底都分别用六个较大的外卖用塑料碗装着,大概两碗能盛满一锅的样子,所以锅底其实并不贵。瑶柱粥煮开后滚烫,夹肉的时候溅到两滴到手背和手指上疼得我直咧嘴。吃饭过程就没啥好说的啦,吃了小象拔蚌、基围虾、雪花牛肉、吊龙、牛五花、肥牛、五花趾、海螺、赤贝、鲍鱼、扇贝、松茸、竹荪、猴头菇、炸响铃,都很好吃啦,粥里下牛肉特别有潮汕牛肉粥的感觉,快收尾的时候还敲了几个鸡蛋到粥里。吃完肉后大家还一起分吃了 ZZ 做的水果芝士蛋糕,然后在逗渔总、闲聊中度过了一个下午。

20210905_food.jpg

水果芝士蛋糕 20210905_cake.jpg

寿星吃面图和饭后葛优瘫 20210905_person.jpg

闲言碎语(2021/07/01-2021/08/01)

2021年8月1日 08:00

2021-07-01 周四

08:04 触发

我自己的一个小项目,今天有个人给我提 PR 修了一个低级错误,虽然并不是多大的事情,但让很久都没去管这个项目的我又想把这个项目捡起来了,看今年能不能发个 1.0 吧。

10:43 无心工作

百年党庆,无心工作。

18:34 个人特点

我同时是 Emacs 用户、RSS 爱好者、NLP 从业人员、游戏爱好者,如果能结合我自身的这几个特点来做一些事情,想必会非常有意思。

2021-07-11 周日

18:31 优化

为 org-agenda 写的过滤函数特别慢,今天想着优化了一下重写了一版,结果更慢了,尴尬……

22:01 电话

和爸妈分别聊了电话,得知

  • 今年高考的那位表弟执意选计算机,和他爸妈希望他当医生、老师意愿强烈冲突,有一些矛盾,可能我得和姨妈打个电话,避免对方认为是我让他选的;等他入学了我还是帮一帮吧,我们其实都一样,如果不能证明自己比父母强,那么他们就会以爱的名义要求我们按照他们的意愿来生活
  • 今年毕业的一位表弟没有回家,好像在上海那边找了什么事情做
  • 在家做公务员的一位高中同学升职调走了 —— 我其实不太在意他是什么级别也不想通过他获得什么便利,我想这是我能和他保持还算友好关系的原因吧

2021-07-12 周一

23:44 蛍

我不认识他,也不知道该说些什么,只是觉得难过。想起我一个伯父。

uekawakuyuurei_1.png

23:48 数字墓地

有没有什么办法可以把一个过世的人的信息收集归档起来呢?我怕有一天,李文亮微博下一百万的评论会消失,像蛍这样的人的痛苦呼喊和其他人拼命但或许徒劳的安慰会消失。

失落的信息和知识都可以被重新发现,每一个鲜活的人却不能。

23:53 死亡

我们终将迎来死亡,如果都能以一种体面而舒适的形式就好了。

2021-07-13 周二

23:13 酒

劝阻了一位想买招商中证白酒的朋友,希望酒厂不要打我的脸。

2021-07-14 周三

23:02 再见

和当初公司的一位实习生一起吃了个饭,他现在在阿里,说觉得以前在我们那实习的时候氛围特别的好,氛围好也没用哇。

2021-07-18 周日

21:43 扫码登录

网易云音乐的网页版去掉了帐号密码登录,只允许通过手机 APP 扫码登录了。

2021-07-19 周一

00:03 发愁

突然觉得自己很不专业,突然觉得事情很匆忙。

08:01 吵

一大早上的,小区里有一个电摩的警报声一直在响,令人无法睡着。突然就产生了换房子的想法。

15:39 电话推销

接到中信银行的贷款之类的推销,这种电话接太多了,我开玩笑说了一句「不用了我很有钱」,说完自己都觉得好笑,又笑着说「不好意思开玩笑的不需要」,对面可能也是觉得是个能开玩笑的人,语气马上生动起来了,缠着让我加微信啥的又聊了几句。

2021-07-21 周三

20:46 犬吠

邻居又在楼道里溜那只极其胆小怕人的狗,不知道又嗅着了哪位陌生人的气息在那狂吠不止。

2021-07-22 周四

10:17 可爱

我可爱在哪里哇?我当然明白自己身上的优点和缺点,不过不太清楚触动别人的点是什么,或许只是「呆到深处自然萌」也说不定。

10:18 引路人

在集智的群里讨论救灾互助的手段,我反复在说信息的自由流通,提到 RSS,有朋友说我是他的 RSS 引路人,开心!

13:53 华宝证券

到网站上登录可以按月导出交易对账单: https://wsyyt.touker.com:7070

huabao_web_bill.png

2021-07-24 周六

17:08 疫苗接种

加上今天我妈给我的电话,已经是这两周第三次地方政府通过家里催促我打疫苗了,我妈说隔一两天就登记一次,挺烦人的。

23:09 重来

看了《重制人生》的前几集,有点感慨。

如果时光倒流的话,该犯的错大概率还是会犯,要经历的痛苦还是要经历,会有不同但也不会有太大的不同。

什么时候开始这种近似宿命论想法的呢?

2021-07-25 周日

20:31 杀死自己

今天在小说里看到一个有趣的说法:过去的和现在的自己差距如此之大,如果在某种神秘力量作用下,现在的自己直接占据了过去自己的身体(或者未来的自己占据了现在的身体),无异于直接杀死了过去的自己,把这个过程拉长并不改变这件事情的性质。觉得这个说法很有意思,但它其实只是特修斯之船的一个变形罢了。

2021-07-27 周二

23:04 离开

晚上吃烤串,K 说 YS 要离职回豆瓣了,已经开始休假。我觉得挺好的,这样他有更多的时间陪 ZZ 和渔总啦。

2021-08-01 周日

10:00 疲惫

昨天又去 LY 家蹭饭了,钵钵糍粑(啊并没有这道菜是我把糍粑扔钵钵鸡的汤汁里)意外的不错。

0731_food.jpg

见到了以前的一位实习生,他要从腾讯研究院跑路去留学了,硕博连读要 5 年。吃完饭他还有事就离开了,我们剩下四个人玩 PICO PARK 到晚上,特别欢乐,但是最后有一关容错率太低始终过不了。

pico_park.png

打完游戏去吃了1998小树林烧烤,没拍照,不过我还是比较喜欢,本来特别困了的,最后还是吃了不少。吃完回到家已经很晚了,睡了一觉起来还是觉得挺累的,这就是快乐的代价吧。

21:00 七月总结

月度总结就随便一点不画图了。

  • 休息时间 252 小时,包含了在公司午睡的时间和周末赖床的时间,总之 8.14 小时这个平均值是有水分啦……
  • 日常行为耗时 121 个小时,大头在通勤和吃饭上,并没有什么大变化
  • 休闲时间 112 小时……基本上就是下班回来后打开 B 站看《黑暗之魂》的视频,偶尔看点番;以及,开始玩《只狼》了,目前卡在火牛那里
  • 进行了 10 个小时的个人项目开发,主要是在优化我的 python-inoreader 项目,目前已经有比较完整的规划了,计划在国庆前发个 1.0 吧
  • 写作花了 11 个小时,其中 7 个小时是用在写半年总结上,还有 4 个小时花在一篇未完成的 org-mode 相关的文章上
  • 工作时间 167.5 个小时,7 月一共 22 个工作日,所以日均工作时间是 7.6 个小时,还行哦
  • 7 月份大项目推进很快,我这边事情也开始多起来了,工作量吧也不能说大,但事情多有时候就会觉得烦躁。
  • 投资方面这个月亏损严重,主要原因是没有仓位控制的意识,下跌的时候没有节奏地进行频繁加仓 —— 很多道理以前都知道,但还是要挨了打才会长记性

半年总结

2021年7月4日 08:00

2021_semi_annual_stats.png

从 2019 年开始,我对自己每天在各项事情上的时间消耗进行跟踪和记录,上图是根据这些记录画出来的 2021 年上半年时间开销统计图,181 天中未被记录的时间开销放到了「其他」部分,约为 2% 左右。从统计结果来看,我个人最大的时间开销分别为:

  • 休息:包含晚间的睡眠、午睡等,主要组成部分还是晚间的睡眠
  • 工作:包含了在公司工作以及在家处理工作的时间
  • 日常:包含做饭、吃饭、通勤、洗漱、打扫卫生、购物、非工作目的的交通、上厕所、购物等日常行为的用时
  • 休闲:逛社交网站、看视频、看小说、玩游戏所花费的时间

这四部分内容占据了我所有时间的 84%,剩下 16% 的时间里我虽然进行了学习、运动、(个人的)开发、写作、自我管理等有益于个人成长的事情,但花费的时间都远远小于 TOP4 的内容。我曾经对自己的时间开销作过分析,首先工作和休息的时间是固定开销无法削减;日常时间开销中的 TOP5 分别是吃饭(239 小时)、通勤(204 小时)、洗漱(102 小时)、做饭(69 小时)和上厕所(55 小时),除了通勤可以(以搬家到公司附近作为代价)削减外,其他的时间开销也很难削减;倒是第四大块内容即休闲时间,其实是可以削减的。学习、运动、开发、写作这些有益事情我做得却很少,我不认为这是自制力的问题,而是目前我在这几类事情上都还没有建立起正反馈机制,无法从中获得足够的满足感,再加上工作带来的精力耗尽,自然就往娱乐方面投入了更多的注意力 —— 相比之下,记账和记录生活这两个事情我却做得非常好,这是因为我能从这两个行为中获得满足感,而不是因为所谓的自制力。

基于上述分析,下半年我的行动方向就是削减休闲娱乐时间获得更多的个人成长。

抛开统计数据来说一下上半年的概况:

  • 工作上:

    一月换了工作,收入有较大提升,工作内容是分析金融领域的资讯并用于推荐,不过苦于人手不足大部分时间在做一些工程向、业务向的事情,这半年来在 NLP 方面可谓毫无建树,不过学到挺多金融知识,也还不错。

  • 生活上:

    生活上还是以宅为主,除了玩游戏和看番之外,因为公司在望京所以体验到了许多新的美食。

    游戏一直在玩《黑暗之魂3》,还差修女芙莉德、无名之王、奴隶骑士盖尔三个 BOSS 没有打过,剧情已经走完了。下半年应该会玩《只狼》和《血源诅咒》。

    2021_animes.jpg

    动画方面,看了上面这些:《ODD TAXI》的故事超级棒,甚至连角色的动物形象也是叙事的一部分,这点是没有想到的,我给第一名;《Vivy》也非常棒,我觉得并不逊色于《ODD TAXI》,但由于是两种不同的类型而目前我偏爱《ODD TAXI》一点,就给个第二好了;《致不灭的你》故事催泪感人,《影宅》设定有趣富有深意,《悠哉日常大王》童趣日常,共同排列第三好了;《里世界郊游》新怪谈的故事挺喜欢,《记录的地平线》故事发挥稳定但没什么惊喜,《转生成蜘蛛又怎样》看个乐呵不用费脑子,这三个排在第四档好了;剩下的《奇蛋物语》、《动物狂想曲》我还没看完,不做评论。

    为了达成分工合作,社会要求我们一定程度上了解和理解他人,但更进一步地深入理解却并非必需,大家的悲喜并不相通,故事在传达情感方面却没有这个限制,正因如此,游戏、动画、小说都是我重要的情感源泉,虽然那些故事是虚假的甚至荒诞的,但他们能表达和传递的情感都可以很真实,甚至能比现实中更浓烈,我爱它们。

  • 投资上:

    今年我大幅调高了个人风险偏好,将绝大部分钱都投入各种投资品种中了,在个人投资水平并无明显提高的情况下获得的收益已经接近 2020 年一整年的收益,对此我还是比较满意的。我给自己的投资定的第一个阶段目标是年收益能满足在北京的一年日常开支,以上半年的情况来看,今年是有可能完成这个目标的,当然我也知道这些收益并非源自我的个人能力,绝大部分都是市场本身带来的,我还有很多东西要学习。

    2021_semi_annual_invest.png

    学习投资之外,苦于没有一个工具来对不同平台上的多个投资账户进行分析,我自己写了 Keysersoze 这个工具,虽然目前还远远谈不上完善,但自用已然足够,希望下半年能完善这个项目让别人也能使用。

  • 写作上:

    两个多月里每周写一篇当周的想法和生活记录,这种写作产出的内容价值不高,但也有收获一些交流,所以我还会继续坚持下去,希望以后能提供更有价值的内容吧。

下半年的计划里,除了前面提到的投资目标、完善 Keysersoze 项目、写作,我还有一些其他的想法:

  • 在 NLP 方面,准备正式启动「棱镜计划(暂命名)」,事实上这个计划的最初想法是 2019 年年末产生的,它会是一个庞大的项目,今年肯定是没有办法完成的,但我希望下半年能切实地开始一些准备工作 —— 包括想一个更贴切、好听又有内涵的名字,毕竟现在这个名字和美国的网络监听计划同名了……
  • 希望能为 Emacs 社区和 RSS 社区做点事情 —— 以前我主要是通过写一些相关的文章来参与到社区讨论里,今后我希望能为相关的开源项目做一点事情,有可能的话自己写一些项目就更好了。

闲言碎语(2021/06/21-2021/06/27)

2021年6月27日 08:00

2021-06-21 周一

23:33 故地

下班后去圆明园附近进行饭罪活动,吃完后我走去圆明园地铁站,一路看着住过两年的地方附近熟悉的夜景,城市太大我又太宅,以至于我搬离北河沿小区后再也没回去过,其实我还是挺喜欢这一片的。

23:37 无知

看盗月社新的视频,发现我对南昌了解很少,不过我了解少的何止是南昌呢,我的家乡、我的亲朋我所知也不多 —— 除了人际、社会和周遭的环境,即使是赖以为生的专业知识我也仅仅是掌握到勉强够用的程度。

2021-06-24 周四

21:31 志愿

表弟勉强过了一本线,在看计算机专业;表妹差一点,决定复读。表弟既然对计算机感兴趣,我就和他多聊了一点。

2021-06-27 周日

09:21 分析

这两天把持仓分析和相关性分析加上了。

持仓分析是根据上季度披露的基金持仓数据来估算我的投资组合里都包含了什么股票、债券,效果如下:

keysersoze_holdings_analysis.png

相关性分析用来把组合里高相关性的基金、股票找出来,方便后续优化组合,效果如下:

keysersoze_corr.png

当然,这些功能所需的数据都是从公开渠道获取的,小问题还挺多,以及展示样式也还需要优化,但总之有个东西了,慢慢调整好了。

10:25 歧视

讨厌被叫中年人被叫叔叔阿姨,是不是一种年龄歧视?

17:33 蹭吃蹭喝

又是蹭吃蹭喝的一天,感谢渔总,感谢 ZZ!

20210627_foods.jpg

17:44 分数

今年江西理科一本线 518,我当年是 519,非常接近,然而我校计算机当年在江西的录取线是 570 多(具体记不清了),今年是 654,真是恐怖……

20:50 带宽

月底电信宽带到期,之前接到过几次电话来着,我想着手机上也能办,晚上一看,原来 200M 的套餐续费最低只能选 300M 的,得多花 400 块钱,问题是我也用不上那么高的带宽啊!

闲言碎语(2021/06/14-2021/06/20)

2021年6月20日 08:00

2021-06-14 周一

17:15 灰羽

心血来潮看了看《灰羽联盟》的评论,看到有人说故事可能借鉴了村上春树的《世界尽头与冷酷仙境》,于是找来开始看了,挺有意思的。说起来,他的书我都没看过。

haibane_renmei.jpg

18:27 新机

之前想换手机纠结了挺长时间不知道该选啥,最后决定买了个二手 iPhone SE2,毕竟我不想花太多时间在适应新系统上,也不想花太多钱在手机上。我这台 iPhone 6s 用了五年多快六年了,别人会觉得我节省其实我只是懒。手机刚刚送到了,成色很好。不得不说苹果在用户体验方面做得还是不错的,开机后自动检测到旧手机,直接就提示让我把手机拿到旁边同步数据,按照提示一步步来非常友好。

19:14 不胖秘诀

写上周总结的时候瞄了一眼,发现上厕所的时间接近两个小时,平均下来每天有 15 分钟以上在上厕所,我可能找到我能吃不(那么快)胖的秘诀了。

2021-06-15 周二

21:46 醉

回家路上,路旁一个男子趴在车上,嚎着歌,边上两个男子在旁边看护着,应该是喝醉了酒。有些人需要靠着酒醉或者别的什么东西才能说出所谓的真心话,其实人和人都有可能真切地传达自己的情感,只不过语言经常会把这一切都扭曲掉,或者说,不是语言而是所谓的语境、语用、上下文这堆东西会把语言的所指卷入一个无穷的嵌套里,所谓的酒后吐真言,无非是用来把自己身上的语境锁链破坏掉的一种手段而已,通过对自己进行降智来告诉别人「我所有的话都是本能没有别的任何用意所以请单纯地相信我语言的字面含义吧」,但一个不理智的人说出来的话里,真的有多少信息吗?语言是这个世界上最复杂的游戏之一了。

22:00 环

《Elden Ring》后面又爆出老贼的采访内容,信息量超大,恨不得一下快进到 2022 年呀!

23:09 工具

不断有新的笔记工具出来,但问题根本不在工具上,而在人本身的复杂性、语言的不精确性以及世界的多样性上。总之,我对新工具已经没什么热情。

2021-06-16 周三

10:14 RSS

RSS 已死、古典互联网已死的感慨不差我们这一两句,不用过于悲观。小宇宙、Spotify 要跑马圈地又怎么样,Google Reader 倒了 RSS 也照样好好的。

22:00 事件抽取

看了一天事件抽取方面的东西,大体上搞明白了,感觉可以和知识图谱构建共用一套框架。

22:00 野炊2!

《塞尔达传说·旷野之息》续作发布新预告片了,信息量很大,不过公主又被抓走了,还以为可以和塞尔达一起冒险来着,哼!

2021-06-17 周四

17:51 鸟

敞开心扉!你不是犯人,你是空中逐梦飞翔的鸟! —— 《世界尽头与冷酷仙境》

2021-06-18 周五

14:23 躺平

六月以来的这一波回调,让我的整体收益直接回到了一个月前的水平,也没有钱加仓了 —— 备用的流动资金是不会去动的,索性躺平了。

june_drawdown.png

22:00 异常

想起来一个奇怪的地方,端午节前我每天早上出门上班,小区门口都还有很多共享单车,端午过后同样的时间一辆都没有了,发生了什么呢?

2021-06-19 周六

12:50 新旧手机

旧手机换个电池然后卸载掉一堆国产应用(比如说微信)的话应该还是挺好用的,所以还是想利用起来,不过暂时没有比较明确的想法。

18:20 世界尽头

把《世界尽头与冷酷仙境》读完了,第一人称视角、双线叙事、满溢的表达欲望,是个复杂的故事,不好简单评价。看完后发现《灰羽联盟》确实有非常多受这本书影响的痕迹,但有很大不同的一点是,《灰羽》里只有“世界尽头”这一部分而缺失了“冷酷仙境”。

haibane_renmei2.png

19:49 涨价

生气,FeedPress 涨价到 60 欧一年,之前是 50 刀一年,这就太过份了吧!考虑换个别的 RSS 烧录平台了 —— 我其实都不在乎他那些分析功能,只是想要一个稳定的 RSS 地址而已,大不了像以前一样直接输出 Huginn 的地址好了。

2021-06-20 周日

08:53 喜加一

Steam 上黑魂2和黑魂3史低,果断入手,就等显卡降价了。

闲言碎语(2021/06/07-2021/06/13)

2021年6月13日 08:00

2021-06-07 周一

14:05 信用卡

信用卡 7/16 到期,之前我看到过直接寄送到指定地点的功能,想着去操作一下直接寄到公司来,结果发现找不到这个功能了,然后信用卡还变成了「未开卡」的状态,大惊,以为新卡已经寄出,犹豫了下联系人工客服询问,告知新卡还没寄出,只是提前更新一下卡的状态而已。松了口气但也有点疑惑,按照流程还有一周才会寄新卡,干嘛这么早就改卡的状态呢?

21:53 公积金

把住房公积金也纳入到我的资产统计里了,这样我的总资产数字又涨了一截!逐渐完善记账体系也是一个逐渐完善自我认知的过程,虽然这笔钱暂时不怎么用得到,但是感觉很好。

2021-06-08 周二

08:10 头疼

连续两天没睡好了,头疼欲裂。

08:32 京豆和京东E卡

把京豆和京东E卡也用 beancount 记起来了。京东E卡就是上周公司发的端午节福利啦;京豆是开通了京东 PLUS 会员后按 10 倍付款金额奖励的,100 个京豆可以在购物的时候抵扣 1 块钱,积少成多也挺不错的。

2021-06-08 * "记录京豆"
  Assets:积分卡券                                               4851.00 JDB
  Equity:Opening-Balances

2021-06-08 * "添加京东E卡"
  Assets:积分卡券                                                200.00 JDE
  Equity:Opening-Balances

2021-06-10 周四

16:12 神清气爽

经过两晚的恢复,今天的状态非常好。周一周二那两天吧,一来是没睡好,二来颈椎也很不舒服,睡好睡不好这个主要靠自己,颈椎的话公司准备换好点的椅子了,应该也会有改善。

08:38 如何选择

我个人的习惯,是先把可能(而不只是当下可以)的选项列举出来,然后用排除法去掉一些特别离谱或不喜欢的,最后根据不同的维度进行相对客观的比较选出少量的候选,可以的话全都要,不可以全要的话看心情吧。和机器学习的优化过程有共通之处,尽力找全局最优或次优,如果只凭感觉或有限的信息,是很容易落到局部最优里的。

2021-06-11 周五

09:21 老头环!

出新预告片和发售日期了!明年 1 月份发售,虽然不是原来想的今年发售,但也没差多久,开心!在此之前可以去看看凯尔特神话故事,嘿嘿。

elden_ring.jpg

09:47 盐!

《盐与避难所》也公布续作了 ——《盐与牺牲》,今天是什么好日子啊,就缺《丝之歌》了!

salt_and_sacrifice.jpg

12:15 西瓜

上周日买了个西瓜,吃了一半,剩下一半拿保鲜膜盖起来放冰箱了,之后一直没空吃,还以为坏了结果没有,开心。

2021-06-12 周六

10:47 黑魂

把守墓大狼打过了,然后去试了试修女小姐姐,好歹是打到第三阶段了……

friede.jpg

17:37 发呆

靠在沙发上两个小时啥也没干,戴上耳机听着老歌,随意遐想,看着自己的念头从这边涌到那边,然后逐渐平静下来。

2021-06-13 周日

15:29 新玩具

昨天买的新玩具到啦,海信 Touch 墨水屏阅读器,128G 存储、1440x720 276ppi 的分辨率,1599 的价格相比手机来说可能不太划算,但和 Kindle 比起来就太棒了:Android 11,能自由安装微信读书等 APP;5.8 寸的屏幕,单手操作自如;128G 的存储空间可以放下更多的书,之前我的 Kindle 才塞了几部高清动漫进去空间就用完了。

简单开箱

hisense_touch_1.jpg

hisense_touch_2.jpg

hisense_touch_3.jpg

hisense_touch_4.jpg

19:04 跨模态搜索

看到一个网站,能通过台词来搜索电影甚至给出对应的电影截图,感觉这种跨模态的搜索还有进一步的想象空间,比如说视频的搜索,如果能根据视频里人说的话、展示的文字来搜索,应该会很有意思 —— 技术上来说是可行的,搜了下大厂也有在做,但都是作为内容平台的一个普通工具,我说的有意思是指在产品形态上应该会有一些有意思的点,比如说自动生成鬼畜视频哈哈哈哈。

关闭部分公众号RSS的全文输出

2021年6月8日 08:00

出于服务器压力、对微信反爬的担忧等多方面考虑,我决定关闭一些我制作的微信公众号 RSS 的全文输出,选择的标准如下:

  1. Inoreader 上订阅数少于 3 的,考虑关闭全文输出;
  2. 每日发表文章在 3 篇以上的,考虑关闭全文输出;
  3. 作者(或写作团队)有在其他平台发布内容且提供 RSS 输出的,考虑关闭全文输出;
  4. 内容很简短的,考虑关闭全文输出;
  5. 我个人对内容已经不感兴趣的,考虑关闭全文输出。

按照上述标准,筛讯出来了 8 个公众号 RSS,在昨晚做调整把它们的全文输出关闭了,这 8 个公众号及关闭全文理由如下:

  • AINLP: 更新频率过高
  • 艾格吃饱了: 美食类公众号,我个人不是很有兴趣,订阅数 4 人也比较少
  • 碗丸食事:同上
  • 也谈钱:作为投资理财类公众号,内容比较水(个人观点),我不太想看了,且订阅数少于 3
  • 微软研究院AI头条:其内容同样被发布在 MSRA 的官方博客上,并提供全文RSS输出,所以这个公众号 RSS 其实就没必要了
  • 观人看世界:虽然订阅者有 17 人,但是其主要内容是引用其他文章并发表简短评论
  • 民俗学论坛:订阅数少于 3 人
  • 华山感染:订阅数 4 人,且内容我不是很有兴趣

如果对上述公众号有全文需求,有如下解决办法:

  1. 使用 Inoreader,打开文章后,按下 w 键来载入全文,经测试对微信公众号的抓取效果还不错
  2. 我提供的 RSS 输出的都是文章原始链接,所以可以使用我提供的 RSS 抓取全文

闲言碎语(2021/05/31-2021/06/06)

2021年6月6日 08:00

2021-05-31 周一

11:54 端午福利

今年这回发的是京东卡,比以前发粽子好啦。还没想好用来买什么。

23:33 伞

痛失 kumamon 图案的折叠伞,想必会有需要它的人成为它的新主人的。这伞还是去招行驻场开发时买的,就让这回忆和它一起随风而去吧!

kumamon.jpg

2021-06-01 周二

20:07 无意义

三家供应商,其实算是内定了一家,那我这几周在做的各种指标评测有啥意义呀?领导回曰「没有意义」。嗯,我倒没觉得这几周白干了,毕竟公司付我钱了,只是想起了以前做乙方的日子,各种中不了标,除了自己产品的问题,可能也遇到过类似的状况吧?

2021-06-02 周三

10:43 心态

在金融相关的行业能看到各种(看似能)暴富的机会,要保持平稳的心态真的挺难的。不过我还好,大部分资金还是在场外的指数基金里,场内的部分,是用来释放交易欲望和磨练投资操作的,在有足够的自信前,我应该不会再追加投入。

2021-06-03 周四

15:16 专利

听说我有两个专利通过了耶!老实说专利感觉没啥用,又不是多先进的东西,而且相比申请专利,我更想把它们给开源了给有需要的人。

17:39 少不是目标

工作头两年会觉得代码写少一点很酷,现在不刻意追求这个了,整体结构上的合理性、模块化以及代码的可读性,远远比绞尽脑汁在某个局部把代码从 20 行优化到 10 行有价值。同样的,变量名写个完整的单词也不长,但非要按不知道什么样的标准拿其中两三个字母出来拼凑成一个缩写的行为,我也是很讨厌的。

22:00 定投

让我弟每月定投 100 块钱到中证 500 里,前天触发的,今天在我的投资管理小工具里给他建了个账户来进行追踪。

keysersoze_new_account.png

2021-06-04 周五

08:10 念头

脑子里翻滚着好多又好乱的念头,好累,需要花点时间做一场大扫除。

2021-06-05 周六

22:35 忙碌

哎呀呀,本来还想放空一下的,又忙碌了一天,真是闲不住。嗯,主要是把 keysersoze 的代码整理了下提交到了 github 上,顺便把几年前买的阿里云 VPS 废物利用了下。

2021-06-06 周日

12:40 记账

两周没记账了,花了一上午才梳理完,看到一丝不差的数字以及总资产的增长曲线,还是很满足的。记账和打扫收纳很像,它不能帮助我产生更多的钱,就像打扫收纳不能让房子从 50 平变成 100 平,但都能获得更整洁的环境,感受到自己对生活的掌控感。

从某个神秘的角度来说,记账和追求永生都是一样的呀,相比起永生来说,记账简单多啦。

20210606_balance_sheet.png

14:41 做饭

久违的做了个饭,荷兰豆焯水简单炒炒调味也很好吃。

15:47 道路

推上看到一位应该刚进大学不久的人在抱怨学校生活,说了很多事情,但不免天真和简单,就像曾经的自己。本想说点什么,但想想还是作罢了,认识自我和世界并建立起在两者之间适当的相处模式,是每个人必经的过程。少数幸运或卓越的人能在很早的时候就发现自己的价值所在,但大部分人普通人可能得花一辈子去追逐答案,我不过也是这样一个普通人而已,没啥可讲的。

16:06 道理和故事

道理可能简单,没有相关经历却很难切身体会,但故事人人能懂。想给别人讲道理时,想想自己有没有相关的故事可讲,如果没有,可能自己懂得这个道理也只是一个幻觉。

是不是这个道理呢?

闲言碎语(2021/05/24-2021/05/30)

2021年5月30日 08:00

2021-05-24 周一

08:24 失眠

三点多还没睡着,今天精神又好不了了……

14:00 表演

前公司同事聚餐,和 WR 聊新番,我说《ODD TAXI》挺好看来着,WR 觉得这个动画的表演没太多看点,简单来说就是人物的肢体语言都很朴实,镜头也特别简单,基本上只靠对话在推动剧情 —— 这个确实,不过我确实更喜欢故事性比较强的动画(其实游戏和电影也是),反倒因为故事内容过于单薄而并不喜欢一些很知名、表演很好的动画。

23:08 自由

某同事说,要是能回到上学那会就好了,想干嘛干嘛。先不说上课和作业的问题,说这种话的人有想过自己有空闲了后要做什么事情么?完全没想过就只想吃喝玩乐那自然没问题,如果有想做的事情却完全没有实际去做过,那么即使有空闲了大概率也不会去做的。当然了,同事只是觉得现在事情繁杂感叹一下而已,不用太认真,就自己这么想一想,毕竟这种想法我也有过。

2021-05-25 周二

16:46 偏差

公司要采购一些数据,出了一份评估列表,最近两周我都在根据评估列表上的需求做数据分析和统计,今天和采购相关的人讲评估结果,聊完发现有偏差,还得和供应商继续沟通然后我们再做处理。这里面的问题很多啦,比如供应商给了采购负责人他们能提供什么数据,但这个信息并没有给到我,这就导致我给的一部分分析结果其实是没有意义的;再比如说评估需求是我们这边的人写的而采购负责人并没有在开始就提出来哪些不合理,然后我开始接手后也觉得有些地方不清楚但也犯了懒就觉得给我这样我就这样做好了。这种涉及很多方的事情,可能先进行大量沟通把问题说得透透的再来执行会好很多吧,我是不太擅长应付这种场景,跟不熟的人对接往往我会在那为了写个说明文档纠结个半天的。

21:40 圆月

今晚月亮好圆哦,而且没什么云看得很清楚。

22:00 今日交易

  • 证券ETF上涨,网格触发卖出
  • 红利ETF上涨,网格触发卖出
  • 传媒ETF上涨,网格触发卖出
  • 银行ETF上涨,网格触发卖出
  • 卖出若干招行,买入若干格力电器

今天市场大涨,我场内和场外收益都很不错,总资产又新高啦,开心!

2021-05-26 周三

21:53 月食

走到地铁口的时候,很多人对着对面拍照,仔细一看,月亮缺了一块,低低地挂在对面小区楼顶上,显得特别的近。我还是第一次看见月食耶,有趣,可惜没看到全过程。

22:04 今日交易

  • 清仓军工ETF
  • 通信ETF上涨,网格触发卖出
  • 恒生ETF上涨,网格触发卖出

22:53 微博API

发现微博有 API 可以获得指定微博的所有评论,不过访问稍微多一点就会要认证,不知道该咋解决。

2021-05-27 周四

10:09 不理解

不理解是怎么来的呢?我能想到这几点:信息的不对称、信任的缺失,还有语言模式的问题。

2021-05-28 周五

00:07 早睡个屁啊

写自己的小项目,看了两集《Legal High》,又折腾到零点了。

11:07 过程和结果

想做的事情别人已经做了,而且做得比自己想得还完善,那还要不要做呢?我还是想做的,因为觉得有意思,至于结果,本来也没想获得什么利益,又有什么关系呢?

14:00 惯性

「我们已经不是在XX」了,确实啊,不过,是不是每个人都能意识到这个事实,并且把自己的行事方式调整过来呢?承认变化,可能并不是那么容易的事情,人的惯性是很强的。

18:00 留言

博客新文章下有一条新留言「来个一个推特的联系方式」,我觉得吧,如果有问题想要交流,直接在留言区交流就好了。

2021-05-29 周六

10:58 演戏

在戏里演戏,太有意思了。

20:00 真实和虚假

对我来说,虚构世界里并不比物理世界虚假。

2021-05-30 周日

17:33 催婚

我爸又拿几个人试探了下:一个在北京做编辑的,一个在老家当法官的。还问了下去年加的那位小学同学。唔,没太多想法。

17:35 疫苗

我爸和我妈都打完第一针疫苗了,近期我也在考虑这个事情。

22:00 进化

这两天又把自己的投资管理小工具打磨了下,现在很像那么一回事了呢!

  • 除了总资产走势和累计收益率走势外,还可以查看累计收益(金额)走势和日收益历史了

    keysersoze_day_return.png

  • 累计收益率走势图里加上了最大回撤的显示

    keysersoze_max_drawdown.png

  • 持仓列表的展示方式表格改成了卡片,还加上了品种的十日走势图

    keysersoze_holdings.png

  • 交易记录列表的展示方式也从表格改成了卡片

    keysersoze_deals.png

可视化方面我觉得已经挺舒服的了,下一步准备加上一些有用的分析功能,比如持仓的相关性分析之类的。

(已修复)对微信公众号 RSS 停止更新的说明

2021年5月23日 08:00

年初换了新工作后,变得更忙了一点,看 RSS 的习惯也逐渐荒废了,今天上去看看,才发现我之前制作的微信公众号 RSS (见文章1文章2)全部停止更新了,故障发生的时间点大概是三到四周前,赶紧去看下到底是怎么回事。

由于 RSS 最终是由 FeedPress 输出的,所以我先登录到 FeedPress 上看了下,确认 FeedPress 自己没有出问题,只是输出的数据都是三到四周前的了;接着去看 Huginn,也是一样,工作正常,但接收到的原始数据就缺失了,而这些原始数据是我在一台国外 VPS 上的定时任务产出的,通过 Telegram API 获取 EFB 同步过来的微信公众号文章消息(详见我的另一篇文章),看了下日志,确认是这个定时任务出问题了。

手工执行了下定时任务,得到了这么一条错误输出:

The authorization key (session file) was used under two different IP addresses simultaneously, and can no longer be used. Use the same session exclusively, or use different sessions'

出错的代码见 Github

至此明白原因了:大概三周前,我在本地使用相同的 token 跑了自己写的脚本,用来导出聊天记录,当时我本地也报了这个错,但没太在意,就是从那个时候起 VPS 上的 token 就失效了,然后加上没有做什么监控也没有用户反馈,导致到今天我才发现这个问题。

明确问题后,按照如下过程修复问题:

  1. 删除 VPS 上的 token,并重新认证
  2. 获取 2021.04.30 后的微信公众号更新消息
  3. 对每个有输出 RSS 的公众号,取最近 10 篇文章发送到 Huginn,生成相应的 RSS

这样处理后问题暂时就解决了,FeedPress 应该会在之后陆续更新相应的 RSS 源了。

在此对订阅这些 RSS 源的朋友表示歉意!

闲言碎语(2021/05/17-2021/05/23)

2021年5月23日 08:00

2021-05-18 周二

23:00 面试

依然是简历看着不错最后不通过,不过今天这个还挺积极的,最后请教我该如何提升啥的,不会是拿我们公司来练习面试了吧?

23:04 新高

总资产连续三天新高,开心开心。

23:09 今日交易

  • 军工ETF上涨,条件单触发卖出若干
  • 恒生ETF上涨,小网格触发卖出若干

2021-05-19 周三

08:50 图谱

早上起来把某个聊天群里的消息做了下分析,画了个花里胡哨但没啥用的图。

chatgraph.png

17:04 gakki

听说 gakki 结婚了,对我当然是没什么影响,很多人说伤心其实也是一种玩梗的心态,或许真的有倾注感情的人,但因为自己喜欢就不允许别人做一个正常人,这是不好的。

逃避可耻但是可以结婚。

gakki.jpg

18:00 愧疚

供应商给的数据有一些小问题,我提出来要求修正,感觉会打乱对面的技术同学的工作节奏,虽然说是他的工作失误,但如果我能及早发现及早告知就好了。当过乙方,我知道对面现在是怎么一个焦头烂额的状态,愧疚。

20:00 大屠杀

币圈今日大跌,最近一两周因为下跌加了不少仓,仓位从过去的 50% 不到飙升到 90%,所以这波下跌我吃得很结实,直接把最近两三个月的收益都跌没了,没有纪律还是很容易出问题的,尤其是在币圈这种波动巨大的场子里。

2021-05-20 周四

10:19 死工资

上班路上听到一个路人在打电话,说「我最近也在想,就我这点死工资……」,好像是对面有什么商机,然后这位路人在考虑要不要参与啥的。一方面担心这个人会不会陷入到什么骗局里,一方面也觉得死工资这个贬义的称呼没有必要。问题其实不在死工资上,而是工资少吧。我就觉得死工资很好,为我提供了源源不断的现金流,至于其他的什么个人成长、零食、住房补助之类的福利,都是次要的,我就这么俗。

19:00 面试

看简历准备面试思路一小时,远程面试一小时,我生命中宝贵的两个小时又被浪费了。话说,我现在看简历挺有感觉的了,最近若干次面试前的感觉都被验证了,比如说这次面试前我就写了这么一个简历总结:看起来擅长的东西和我是很接近的,但我的感觉是一些细节描述的不清楚,简历中有「推动xxx」的字样,有点担心在相关的项目中是不是更偏重于规划和管理而非技术执行。

21:01 剑风

震惊地得知《剑风传奇》作者三浦健太郎在月初去世了,我以为我可以到四十岁继续骂着三浦老贼追着一年更新几次的漫画的,真是太遗憾了。

guts.jpg

23:00 今日交易

  • 买入太阳纸业若干

2021-05-22 周六

12:30 视频

一个朋友找我帮忙下载学习强国上的视频,说找了不少工具都没法下载下来,我浏览器打开开发者工具一看这不视频链接直接就给了嘛……被喊一声大佬受之有愧,真的不是什么有门槛的东西。

zhihu_video_questions.png

23:00 且慢

写了个能把且慢上各项资产的日收益数据导出的脚本,主要是用来导出货币三佳的数据,其他的能用净值算出来倒不用这个。货币三佳选择三个比较好的货币基金来保证收益,产品是好的,但是这三个基金会变来变去,如果是去解析交易记录跟踪份额和净值就太麻烦了,我就只当现金,把收益当再投资来记就好了。

qieman_profits.png

其实货币三佳的收益很一般,但是为了管理方便,在想出更好的办法前还是保持现状好了。

23:00 手机

想换手机,网上看了看,又选择困难了,算了,再说。

闲言碎语(2021/05/10-2021/05/16)

2021年5月17日 08:00

2021-05-10 周一

08:59 甲方

做甲方也不好做啊,感觉没有做甲方的命。

21:43 今日交易

  • 卖出平安证券上的所有华宝油气,正式清仓,从 2019.09 开始到现在历时 612 天,算交易日的话是 402 天,累计收益约 2600,最开始那段时间的下跌的时候在平安证券上自己手工做网格,跌傻了,后来用华宝智投自动做,才逐渐恢复正收益

    trading_162411.png

  • 证券ETF下跌触发网格买入若干
  • 卖出有色ETF若干
  • 中概互联下跌,条件单触发买入若干
  • SHIB 暴涨 90%,把昨天买的 SHIB 出了一部分,赚了 150$ 的样子

23:33 面试

今天又面试了一个小伙,简历写一年工作经验但代码能力完全不像,反倒像刚毕业的,还是那种平时大作业和班里同学抄来抄去那种。我有点怀疑最近两份简历是不是被专门的人包装过,因为最近两份简历有非常雷同的一个项目经历,并且同样在被问及这个项目中非深度学习的部分时啥也回答不了的状况。培训班也好、见深度学习火(实际上现在已经没几年前火了)转行也好,咱至少把基础功打好吧,简历上写了熟悉 Python 那 Python 的基础语法至少过一遍吧,写熟悉数据库那最最简单的查询语句要能写出来吧,写熟悉 Linux 不能除了 ls/cd 啥都不知道吧,就只会 BERT 一把梭,太难了。

另外,讲个笑话,BERT 可以用来做天气预测。

2021-05-11 周二

10:24 说话

在朋友的公众号文章里留言,她说「你初中时可是很爱说话的,后来越来越少」,为什么呢,大概是因为一系列事件导致的性格变化吧。

23:07 合并

清仓华宝油气后,把平安证券上的钱全部挪到华宝证券里了,然后在自己写的应用里把两个账户的统计合并到了一起。合并后可以看到,今年以来我在证券账户里的投入翻了三倍多,但在投入增加的同时还是获得了 6% 的收益,还是不错的。

exchange_sofar.png

2021-05-13 周四

22:12 创造力

被琐事缠身还能发挥创造力吗?我不知道,但我过往比较自得的一些产出,都是在比较悠闲的时候做出来的。

2021-05-14 周五

22:27 海鲜

下了班和 LY、K 去吃了海鲜锅,我果然对海鲜还是欣赏不来,或许也和食材新鲜程度有关吧,不是太懂。

seafood.jpg

23:00 不灭

看了最新一集《致不灭的你》,有点冲击,怕不是每一两集就要死一两个人……有点想去看漫画,不过漫画看完后往往就会对动画失去兴趣,所以不妨先看完动画再看漫画把。

to_you_eternity.png

23:00 今日交易

  • 证券ETF上涨超过 7%,卖出若干
  • BTC 下跌,以 49000$ 的价格买入若干
  • DOGE 上涨,以 0.52$ 的价格卖出若干

2021-05-15 周六

20:00 ODD TAXI

新番里的《ODD TAXI》非常不错呀,看海报的话很容易被这个画风骗过去,实际故事非常出色,人物也挺饱满的,一口气看了好几集。

odd_taxi.png

20:00 影宅

另一部新番《影宅》也很不错,设定有意思,若隐若现地暗示着什么,等之后谜题逐渐揭晓应该会很有意思。另外,音乐也很好听。

shadow_house.jpg

22:00 今日交易

  • SHIB 下跌,以 0.000019$ 的价格买入若干
  • BTC 下跌,以 48974.05$ 的价格买入若干
  • ATOM 上涨,以 27.5$ 的价格卖出若干

2021-05-16 周日

22:00 今日交易

  • BTC 下跌,以 48000$ 的价格买入若干

22:00 Vivy

又一部好看的动画《Vivy》,讲得是一个使命被设定为「用歌声让人类获得幸福」的机器人阻止人类和 AI 发生战争的故事,算是比较典型的科幻设定。差不多每集相当于一个小单元,女主机器人在(从未来返回的)搭档的提示下去阻止或推动一些事件的发生,但往往这个事件和搭档给出来的资料有一些不一致的地方,这样的故事结构还是挺吸引人的。

vivy.jpg

闲言碎语(2021/05/03-2021/05/09)

2021年5月9日 08:00

2021-05-03 周一

22:56 电影

下午看了《指环王》前两部,发现故事细节我基本记不清了,就像看了两部新电影一样,挺好的。美中不足的是看第二场的时候,左边两个人一直在聊天,我说了两次才消停,气死。

2021-05-04 周二

12:16 HT

整理火币上的交易记录的时候,发现火币自己发的 HT 币从 2 月初到现在涨了三倍多,当时买 HT 是为了购买用来抵扣手续费的点卡的,还挺好玩的。

17:51 安心

一个月前,清理关注的公众号的时候,发现一个公众号自 2020 年 8 月后再没有更新过,记得在知乎上看到作者过去受到过巨大的伤害,去看知乎个人页上面也贴着一张不太正式的寻人启事,当时就担心这个人会不会出什么事,犹豫再三在公众号上留了个言。这个留言一直没有得到回复,但今天发现这个人的 B 站帐号(和公众号、知乎用户名相同)有给一个 2021-04-10 发布的视频点投过币,所以人应该是没事,松了一口气。

19:40 加密货币

花了半天时间,把火币上今年以来的交易记录导出来,做完整理和校验后转成 beancount 格式纳入到我的资产记录里了。不知道是不是因为浮点数精度问题,有些币种按照交易记录算出来的资产额和火币上给出来的会差一点点,懒得去管了直接抹掉这个差额。

huobi_beancount.png

21:30 黑暗之魂

黑魂3的故事相比黑魂1更加令人绝望,虽然整个系列的故事背景都比较灰暗,但在1代起码所有人都还有希望,“神明”和他们的追随者基本也还存在,但到3代就是整个世界都衰败了,1代的神明被囚禁、被吃掉、被诅咒还有疯掉的,传火派和灭火派激烈对抗,篡火派在旁边潜伏着,随时准备着把初火的掌控权从没落的神族拿到自己手里 —— 都在进行权力斗争,世界到底怎样才能变得更好其实没多少人关心,虽然关心也没用,毕竟初火的熄灭已经是注定的命运,黑暗终将降临。

我选择灭火,是真的看不下去整个世界苟延残喘的样子,并且相信这可以加快下一个纪元的到来;但也有人选择传火,是因为这样能让主角认识的朋友还能过完相对正常的一生 —— 但按照游戏设定,主角作为一个对初火有病态般渴望的人,最终必然是会选择篡火或者夺火的。没有谁是正确的或者错误的,至少我们都按照自己的心情做出了选择。

2021-05-05 周三

23:25 假期结束

感觉这五天时间完全不够啊,前后又要调休,实际只放一天假,真没意思。

2021-05-06 周四

18:00 表弟

5.1 那位给我打电话姨妈的儿子,加了我微信,闲聊了两句,语言还算活泼,有空再慢慢聊吧。

22:00 今日交易

  • 中概互联下跌触发买入
  • 招行涨到 54 以上,卖出若干
  • 恒生ETF网格触发买入
  • 把平安证券上的华宝油气卖了一半,由于手续费太高,准备在将来的一段时间内把在平安证券上的东西卖完然后注销平安证券的账户

2021-05-07 周五

15:12 面试

远程面试一个 97 年的小伙,聊项目虽然没亮点,但也没觉得有多不好,但在我共享屏幕把根据他简历准备的一些基础问题展示给他后,不一会他就表示放弃,还一个劲说对不起。都是非常基础的一些问题,答不出来那也没办法。

简历上糊个深度学习项目是不是有点太容易了,估计准备了一些深度学习模型方面的知识以应对面试,但却没想到我不问算法只问计算机和编程基础?

20:00 今日交易

  • 证券ETF下跌,条件单触发买入
  • 通信ETF下跌,条件单触发买入

23:30 吃肉

下班后去附近吃烤肉吃了两小时,肥肠干净没有异味,一共点了四份,金砖五花肉一大块在那烤看着就很满足了,牛五花的油脂特别的香。

2021-05-08 周六

08:34 币都疯了

ATOM 窜到了 30$,网格彻底没法做了;DOGE 突破 0.7$,和五一前比直接翻了一番。7x24 的赌场真可怕。

23:30 又吃肉

今天吃的是壹圣元,鲜鸭血不行,鳝片超大块吃起来很满足,泥鳅这次没什么精神没在锅里蹦达有点失望,其他的肉维持了水准。吃得很饱,毫无睡意。

2021-05-09 周日

12:28 资产走势

上午给自己写的投资管理小工具加上了资产走势图,看起来更直观了。

assets_history.png

闲言碎语(2021/04/26 - 2021/05/02)

2021年5月2日 08:00

2021-04-26 周一

20:34 疲惫

团建完只觉得疲惫,所谓的开会就是听老板讲了两个多小时不知道重点在哪里的话,没睡好没吃好没玩好。

下班路上和 C 讨论,再次感叹教授这样的人的珍稀程度。

22:00 今日交易

  • 卖出若干1000ETF
  • 卖出若干宁波银行

2021-04-27 周二

21:15 时间

想做点有意思的事情,但是没有大块的时间可用,所以大概率五一我又要闭门不出了。

21:19 重复工作

每次新业务都要把分词、实体识别、文本这些东西重新做一遍,好无聊啊。人工标数据很麻烦,但也是因为初始效果不好导致的。

如果各个行业各个公司能把自己的标注数据、模型都共享出来就好了,能减少多少重复工作啊。

我不是想偷懒,就觉得无聊,想启动我拖延一年多的棱镜计划(暂命名)。

2021-04-28 周三

21:14 尴尬

今天面试了一个人,全是偏学术而且偏图像的经历,开场特尴尬不知道聊啥,后来聊了聊 NLP,出了道编程题让他做,然后我出去等了,期间和同事稍微讨论了下这个面试,感觉有可能有被他听到(会议室几乎不隔音),感觉好尴尬。

21:20 计划

下班路上和 XY 聊了聊我的棱镜计划(暂命名)的一些想法,嗯,感觉我想得还不清楚,可以再捋一捋。

21:59 今日交易

  • 顶格申购了正川发债
  • 卖出若干生物医药ETF,也没什么计划,就是感觉涨够了想卖一卖
  • 卖出若干宁波银行,一个是涨得不错了卖一点,另外就是腾点钱出来好捡捡垃圾

22:57 投资建议

怂恿一个朋友买了中证500ETF联接基金(定投)和兴全低波动小确幸。

如果能帮助别人赚到钱,那会很开心的。

23:36 新浪博客

朋友说她的新浪博客登录不了了,我去瞅了下,无意中看到了自己新浪博客上 10 年前的留言,那是些美好的记忆,虽然再也回不去了。

新浪博客上的文章我都备份到本地后删掉了,除了这个留言也没别的痕迹了。

sina-blog-comment-1.png

sina-blog-comment-2.png

sina-blog-comment-3.png

sina-blog-comment-4.png

2021-04-29 周四

13:41 泸州幺妹

多多要离职了,大家一起去泸州幺妹吃了个饭,大家都吃了个撑,也就人均 60,实惠又好吃。

22:00 今日交易

  • 卖出温氏转债
  • 卖出若干招行
  • 长赢150份发车,跟车买入一份中证传媒、一份全指信息技术,卖出一份德国DAX30
  • 长赢S发车,跟车买入一份中证传媒,卖出 0.5 份 德国DAX30

2021-04-30 周五

22:00 表弟

表弟今年高考,今天加了我微信准备聊一聊 —— 去年本来准备过年回去了解一下情况的,但因为疫情没回家,能网上聊一聊还挺好的吧。

23:05 今日交易

  • 中证军工ETF的网格触发买入
  • 买入若干信息技术ETF
  • 买入若干建设银行
  • 卖出若干生物医药ETF

2021-05-01 周六

16:30 和表弟聊天

说现在高考外语可以不学英语,他选择了学日语,学得比英语好。令我吃惊的是谈到专业的时候他说也对计算机有点兴趣,不知道有没有受到我这个大表哥的间接影响。

这个表弟和我还是比较亲近的,能聊不少天,还有一个大四的表弟虽然也学计算机但从来没有主动找我说过话。

21:30 另外的表弟

另外一个姨妈给我打电话,她儿子也是我表弟刚工作,她好像不太满意儿子的工作,就想让我帮忙给点意见之类的。

我能给啥意见啊,他又不学计算机,计算机之外的其他行业我两眼一抹黑。姨妈又问让学计算机怎么样,我是觉得还是要看个人兴趣。

最后就还是和表弟加微信再聊呗。

2021-05-02 周日

18:10 小饭桌

LY 的小饭桌今天又开张了,今天的参与者除了我还有 XKL、XX 和多多。小饭桌每次都很丰盛。

0502-food.jpg

18:10 清风

在 LY 家吃完饭一起去了附近的公园,找了个地方铺了地垫、挂好吊床,几个人悠闲地在小树林里看书闲聊,非常惬意。

0502-park.png

躺在吊床上吹着风半睡半醒的时候,小学时在外婆家吊楼地板上铺席子午睡的记忆浮现了出来,感觉挺接近的。

18:10 指环王

预订了明天两部《指环王》的 IMAX2D,在大屏幕上重温一下经典。

闲言碎语(2021/04/19 - 2021/04/24)

2021年4月24日 08:00

2021-04-19 周一

08:18 ATOM

一觉醒来,ATOM(一种加密货币) 的网格已经赚了 80$ 了,大佬们吃肉我们喝喝汤也不错。

08:32 zlibrary 的捐赠策略

收到邮件,说捐赠带来的权限提升改成了持续 31 天,原来好良心的,捐赠任意金额后就能一直使用所有功能,看来是有支出压力了。既然这样,我考虑每月给捐点呗。

11:15 恒生ETF大小两个网格都触发卖出

卖完成本变成了负数,持仓还剩下 100 份,赶紧再跌下去让我买回来吧。

14:04 清仓5GETF

今天看 5GETF 浮盈了,就全部卖出了。5GETF 现在应该还是很低估的,长期拿着应该不会亏,但我已经持有和 5GETF 相关性很高的通信ETF了,两个留一个就好了,而且也可以腾点资金出来用来买别的更好的品种。

2021-04-20 周二

10:53 ATOM网格浮亏

价格跌到网格下限之下了,导致所有网格利润都被吃掉了。

所以,应该如何判断网格策略的设置需要做调整呢?我想到的是设置止盈线,结合价格走势,或许还可以结合短时风险评估来做判断。

21:00 哭泣的人

出地铁站的时候,看到一个姑娘坐上电动车时摔了,并且在哭(感觉不光是因为摔了还有别的原因),犹豫要不要上前询问一下,但最终还是走开了。

2021-04-21 周三

22:06 情绪

吃完饭 K 打了个车回家,我和 LY 蹭一段,我打算到望京西站就下车换地铁站,因为之后就是熟悉、确定的路线,而且因为吃得撑想要站一站走一走,然后 LY 觉得我可以一直蹭到底甚至让司机送到奥林匹克公园站,这样我会方便很多。到望京西站 LY 下车时我依然表示我也要下车,LY 对我说「你不用下车啊」,当时有点恼火,回了句「我要下你管我呢」。

这当然是个小插曲,并不影响我下车后和 LY 一起聊天走到地铁站,但我这种情绪反应和语言表现是很有问题的,这些年来这种表现反反复复出现过很多次了,其实不带情绪解释一下就好了。反省过好多次,但好像也没什么改善。

2021-04-22 周四

08:54 阴天

天阴沉沉的,下着小雨,不喜欢这样的天气。

2021-04-23 周五

14:39 茂盛

路旁树木的叶子开始茂盛起来了,隐约有点夏天的感觉呢。

21:33 降噪耳机

回到小区,进电梯按完楼层按钮后一转身发现身后有一个人,把我吓了一跳。戴着降噪耳机完全没注意到身后还有一个人,这效果也太好了吧。

2021-04-24 周六

16:42 绝望

和我妈电话,她跟我说到的情况,令人绝望。

我曾经预感到我将会迎接越来越大的风浪,这预感每一年都在变成现实,暂时我还能承受下去。

17:19 锚

想念二十多年前的夏天,白色的房子,静谧的傍晚。

可以用来锚定自己的东西有点少,得去多创造一点。

闲言碎语(2021/04/12 - 2021/04/18)

2021年4月18日 08:00

年纪大了,微博、twitter、朋友圈这些都不玩了,但还是想有个地方能公开记录一些简单的想法和琐事。本来的想法是像搭这个博客一样,整个静态站点,像微博一样自己能写东西,还能输出 RSS,想法挺好的,但一直拖延着,索性以博客文章的形式发出来吧,大概会每周一次。

我之前写博客喜欢在自己把事情、想法都进行了系统性梳理后再写长文,这个每周一篇的闲言碎语,就会比较碎片化、个人化了。

每一条内容的时间只是我写下这个内容时的时间,都是在描述事件发生的时间之后,滞后多久也并不一定。

2021-04-12 周一

13:44 午饭燃面

和 LY、XKL 骑车去悠乐汇,我们的目标望花路燃面在二楼,之前 LY 和 K 去吃过说很好吃。我和 XKL 各要了一份 17 的大份荤燃面, LY 点了一份小份的什么面,另外点了一份蒜泥白肉、一份口水鸡和一份豆花,都挺不错。燃面第一筷子入口辣到我了,用了不少猪油,香。我没吃过多少次燃面,不太清楚什么是正宗不正宗的,但是口感和味道都不错,据说是北京做燃面最好的几家店之一了。

回程时风大了一点,路边的花落了不少,感觉要下雨。

13:52 恒生ETF网格启动

上周新建了恒生ETF的网格交易,格子比较小,打算跑一跑看能不能吃点小波动的肉,原来的网格当然也还在跑着。新建的网格在一点整的时候触发了第一次买入,看看之后表现如何吧。

要严肃一点的话,应该从关注的品种里找一个近期比如说一个月内整体涨跌幅不大,然后日收益率波动率稍微大一点的品种来去做网格,目前只是凭感觉选了恒生,之后可以考虑做个工具来自动筛选适合做网格交易的品种吧。

14:09 仓位

今天开盘后招商银行和宁波银行都下跌了一点,想加仓,但证券账户里的钱已经没多少了,于是又从银行卡转了点到里面。我目前并没有什么仓位控制的概念,春节前上涨行情中因为没太多可买的,焦虑更多的是适合买入的机会不知道什么时候才会出现,春节后这几个月的下跌给了买入机会,但我并没有什么计划性,大部分时候就看心情觉得够低了就买一点点,结果买到证券账户内没什么现金了,这样还是不太好的。

21:04 理中客

在鱼塘讨论新闻源可信度,我表现出了理中客的嘴脸,就是认为媒体都有立场所以看新闻不用那么认真这种态度,XKL 有点生气,蹭她车下班的时候聊了聊,她觉得理中客这种貌似理性客观的态度是抹杀了那些那些在为美好事物奋斗的人(如有操守的记者等)的价值,所以生气,她这么说的话我是能理解的。

转念一想,我觉得美好的人事物,如果被别人否定,我好像也不会在意,可能我在心里并没有真的多么认可这些美好吧。现在就觉得一切都没什么意义,有点虚无主义的倾向。

21:13 理解

人和人互相理解是非常困难的,所以在合适的时候,我也应该努力去理解别人,同时也尽量让自己容易被理解。

21:14 风

出了地铁往回走,风超大,有一点点冷,但是很舒服,开心。

2021-04-13 周二

10:06 李文亮

走去地铁站的路上在想他的事情,去年疫情对我心理和情绪的影响仍然在持续。

10:22 贪婪

昨天招行和波行下跌,我设置了一个接近当时最低价的加仓条件单,最后当然是没有成交,今天又涨回来了,就会有一点点懊悔。我这样的行为和情绪,还是买卖没有一个成型的原则,感觉有一天会吃亏的。

13:39 又建了一个中证500的网格

看了下 2020 年 7 月后,中证500ETF一直在 6.7-7.4 之间震荡,感觉合适做网格

21:00 意识网

这两天有人讨论 Neuralink 猴子用意念玩电子乒乓球游戏的新闻,自媒体重复着以意驭物、记忆存储、记忆移植这些词,我倒是希望这个可以成为意识网的基础。直接获取别人的感受,《奇诺之旅》认为这只会带来痛苦,那是因为奇诺只在一个地方停留三天吧,我相信人和人更多、跟深地互相理解后,是可以带来翻天覆地的变化的。

可惜,我们没有意识网,有生之年可能也不会有什么希望,目前只有语言可用。

2021-04-14 周三

08:33 睡眠

六点多就醒了,七点多爬了起来,虽然还想睡。

08:42 离线

收到短信,购买的《离线》杂志回归后的第一本新刊到了快递柜,开心。不过暂时不想去取,下班回来再取,然后好好读一读吧。

08:52 再想 NeuralLink

我并不是觉得记忆存储、记忆移植、意识上传这些东西不好,但如果新技术不能修补人和人之间的裂缝,那么大概率会成为新的剥削工具。

10:36 招行触发买入

触发价 49.8,成交价 49.79,成交后看到价格一路下到 49.71,不过接着又回来了。前两天刚转入的钱又快没了,感觉被掏空。

15:54 写博客的乐趣

一篇多年前写的文章,因为搜索引擎的存在,还能帮到新的朋友,真开心呀。

23:29 千秋梦

K 发了一个视频,一个通过歌曲《千秋梦》讲中古汉语的,没多大兴趣,不过是龚琳娜老师的歌,就去听了听,嗓音一如既往的好听!发现龚琳娜老师还有 B 站帐号了,火速关注,顺便又听了一些歌,开心。

23:32 读《离线·共生》

看了 50 页,大部分内容都是知道的东西,不过还是很满足,除了阅读本身,还有那种四年后和朋友重逢的感觉。美好的东西还能继续,这就让人很开心。

如果好奇心日报以及其他一些东西也能回来就好了。

23:53 少一点概念,少一点抽象

交流前还要先对一遍术语表就让人很头疼,要是都是个人化而非标准化的术语表,那就更是鸡同鸭讲。

潜水某微信群有感。

2021-04-15 周四

11:00 银行股继续下跌

招行跌到 49 以下,波行和建行微跌(不到 1%),关注的银行也都是绿的,看看今天能不能触发银行ETF网格的买入。

18:06 MagicMirror

推上看到的,一个开源的智能镜子方案,看起看很酷!

magicmirror.png

项目主页: https://magicmirror.builders/

2021-04-16 周五

14:00 三刷

这周吃了三次枣庄人家了,风味茄子的口感真的超赞。

14:16 赞美的语言

产生了赞美的心情,却羞于用语言表达出来。

21:07 语言

是不是因为在用语言表达自己的感受这件事情上很糟糕,才会渴望意识网这种东西呢?大部分互相理解上的困境,也许通过合理运用语言就足够了。

我的 2020

2021年1月3日 08:00

2020 年终于过去了,回想起这一年真是五味杂陈,心情复杂,这年终总结一时不知道从哪里说起。相对来说,我在这一年里受社会形势的冲击很少很少,没有因为疫情被隔离,也没有因为疫情和贸易战导致收入减少或丢失工作,之所以能这样,都是因为我所处的行业和我所在的公司,与我的个人能力并没有太大关系,真的是幸运啊。除了感到幸运,这些巨大的社会事件也让我深深地感受到个体的渺小,我一度因为疫情的资讯情绪低落难以入睡,也曾想象自己所处的行业、公司一夜之间倒闭后的惨淡情形,思考自己能为他人做些什么最后只能捐钱安心了事,而对困境加身时该如何自救却全然没有头绪,只能承认自己的无能、渺小。

感叹完了,当然还是需要梳理一下 2020 年的一些收获、得失,为新的一年做点准备。2020 年我主要在知识管理、投资上主动做了一些尝试,但效果都一般;技术上则几乎没什么进展,绝大部分工作都在做业务、工程,业余时间虽然也看了不少论文,但学术研究能落地的毕竟有限;此外,由于这一年在家工作居多,休闲娱乐方面的时间大幅上升,虽说大部分都是打发时间,但也有几本书和一些影视作品可以聊一聊。

接下来就分开讲一讲吧。

投资

投资是我 2020 年相对来说做得还好的一个事情,主要的成果是完善了投资体系并开始尝试一些新的投资方式。

我现在的投资方式,严格来说,还远远算不上“形成体系”,所谓“完善”是相比 2019 年而言。2019 年的时候我主要是在跟投别人的投资组合,收益虽然很不错,但有两个问题:

  1. 我在跟投的投资组合上投入的钱不多,这导致我的储蓄大部分都在货币基金里
  2. 我跟投的是长赢指数投资计划-150份长赢指数投资计划-S定投,前者已经将投入上限规划好了不需要额外增加投入,后者的买入频率很低,这导致我每月的工资结余没有规划,最终绝大部分也买了货币基金

这两个问题就导致我的总体收益并不高,2019 年没有做很详细的记录,但估算我投资的总体年收益在 5% 到 6% 左右。另外就是大笔闲钱放在货币基金里带来了比较大的负担,一看到这笔钱就会想要不要买点啥,但因为没有规划最后又不了了之。高峰时期,我的总资产里货币基金所占的比例高达 40%,但以我这几年的生活状态是不需要这么多流动资金的,放在货币基金里真的是浪费。

这个问题我一直都明白,但一直没有去行动,上半年的话主要是疫情导致情绪混乱而没有心情想这个事情,最终拖到 10 月份才开始重新规划资产配置,然后又磨蹭到 11 月才做完调整,留下一小笔流动资金继续放在货币基金里,剩下的钱买入债基组合,同时增加了更多的定投计划。把流动资金和投资资金彻底分开后,以后统计收益、分析资产成分等事情都更方便了 —— 原来一大笔钱放在货币基金里,虽说收益率低吧,但投入大导致收益金额也不少,如果不纳入统计也不好,纳入统计的话,它的投入一直在不规律地变化,投入金额还不是一个整数,看着特别烦。

这样做完调整后,我已有的钱都清楚整齐了,然后我每月收入,扣完房租以及日常支出预算(2020 年每月 2500),剩下的一部分以定投的形式进入投资组合,再剩余一部分则作为流动资金依然放到货币基金里,也不用管它的收益了,爱多少多少。

新的投资方式方面,都是 10 月份以后才开始尝试的,主要有这些:

  • 可转债打新,从 11 月 20 才开始的,到现在中签 3 支,大概能赚 300 多点,为了提高收益,还是要多开几个账户才行
  • 购买主动基金,目前买了谢治宇、张坤等明星基金经理的一些基金,了解的应该知道是哪几支,不了解的话本文不构成推荐意见,后果自负
  • 了解下港美股投资,但我没有境外银行卡,出入金太麻烦,放弃了,就通过购买 ETF 少量持有了一些港美股的资产,2021 年打算加大这方面的投入
  • 自动网格交易 —— 2019 年就有自己做网格,但代价是要比较频繁地盯着价格,对生活工作都有不小影响,2020 年用上了华宝智投,它支持设定网格规则后自动执行,非常省心,目前正在逐步把其他地方的网格挪到华宝智投上

到 2020/12/31 为止,我的投资组合表现如下图所示:

2020_investment.png

图1  2020投资收益

这一年里绝大部分的收益都是长赢指数投资计划和U定投贡献的;基金网格因为重仓了华宝油气导致整体收益很低,但其他成分的收益还是不错的;债基组合和主动基金都刚买不久,算出来的年化收益率有点虚高,但看起来势头不错,希望他们能保持下去。

2021 年的话,我打算继续调整资产配置,追求更高的整体收益,目标 10% —— 截止 2020/12/31,增加的收益除以总投入得到的结果是 9.66%(实际的年收益率计算应该没这么简单),这样一看 10% 的目标似乎很容易,但其实不然,一来 2020/12/31 这天市场表现比较好,往前一周的 2020/12/25 那天我的年收益才 8.71%,所以这个 9.66% 看看就好;二来在 2021 年我还会有稳定的工作收入,如果新增加的投入没有接近收益的话,估计很难达到 10% 的年收益了。

知识管理

我一直都想摸索出一套符合自己使用习惯(基于 org-mode)和理念(形成知识图谱)的知识管理体系出来,但一直没有什么很合适的工具,2019 年的时候,自己结合 org-mode 和 Neo4j 做了一个简单原型,但使用起来不是太方便,很多地方感觉也没有想清楚,还是没有用起来。比较惊喜的是 org-roam 的出现,让我学习到了不少新东西,也成为了我这一年主要的笔记管理方式,为此我还专门写了一篇文章

org-roam 里我觉得对我来说很有启发的点有这么一些

  • 基于 org-capture 来实现快速新建笔记的功能
  • 使用 vis-network 来进行知识网络的绘制,其效果比 Neo4j 自带的好,而且更容易自己定制
  • 使用 helm 来快速查询笔记、选中笔记中的词语或短语直接将其转变为链接等友好的交互设计

不过对我来说它和我理想中的工具还是有一定的差距,在我看来有以下一些缺点

  • 数据存储使用 sqlite3,想要进行更复杂的图操作会非常麻烦
  • 可视化知识网络的 web server 在 Emacs 中运行,当笔记数量较多时严重影响 Emacs 的性能
  • web server 只能做知识网络的可视化,欠缺更灵活的查询和分析功能
  • 它本身并没有设计成为适合表示知识图谱的结构,如果自己想要在笔记中维护一些 schema 信息将会很麻烦

然后,我理想中的知识管理系统大概是这个样子的

  • 使用 Emacs org-mode 编辑和维护,并存储结构化数据至 Neo4j 或其他图数据库,支持 web 端的显示、查询和分析
  • 每则笔记内容分成 meta 信息、普通笔记、任务、卡片四块:
    • meta 信息包括创建时的上下文以及预先定义的 schema 所要求的一些数据字段;
    • 普通笔记用来记录这则笔记的详细内容,不做内容和格式的限制;
    • 考虑加一个任务部分,主要是有些时候在学习、了解某些知识点的时候需要查阅资料进行理解,如果将这些行为在另外的 org 文件里作为任务管理的话会造成一定的数据和操作冗余;
    • 卡片部分是想用来撰写或自动生成一些细粒度的知识卡片,然后可以通过 AnkiConnect 将这些知识卡片同步到 Anki 中用于加强记忆和理解。

上面两点是比较核心的想法,此外还有一些零碎的想法就不展开了。我准备在 2021 年把我设想的这个系统开发出来,涉及前后端和 Emacs 的插件开发,对我来说也是一个挑战呢。

当然,除了工具本身,对信息源的控制也非常重要,我在 2020 年就深受其扰,因为想要关注学术研究动态,通过 RSS 订阅了 Arxiv 的论文更新,导致每天得花上近一个小时去清理更新的论文,但其实大部分论文都是我并不感兴趣或者灌水的论文,投入产出比非常糟糕,后来我干脆启用了白名单过滤,总算减轻了一些压力。再就是作为一个重度 RSS 用户,看到感兴趣的东西下意识地会去寻找 RSS 源,于是订阅也在慢慢增加,这种习性一时半会难以扭转,只能定期做一下清理了。

技术

上半年一直在做抽取类的业务项目,顺手写了个文本抽取的框架,但混杂了很多业务逻辑在里面,然后这种项目做完后一般都不会有人主动去推优化的事情,因此我也没有去做进一步的抽象和设计;下半年的话一大半精力放在项目重构上,重构做得很好,但已经是一个边缘项目了,所以算是我的自娱自乐吧;除此之外还还工作中做了点 metric learning、机器阅读理解和文本复述的工作,不过因为一些原因落地的都不多;最后工作之外的业余时间我也没有学什么新东西,倒是花了大力气去梳理我在 NLP 领域积累的笔记,只是对已有的东西做了更好的理解,并没有什么新的收获。总而言之 2020 年我在技术方面没什么建树。

NLP 方面我这两年其实都没有太多新的认识,不过 2020 年的时候,有想到几个将 NLP 和我熟悉的一些其他东西比如 Emacs、RSS、知识管理做结合的点子,不过目前也仅仅是点子而已,也许以后会拎出来折腾一番。预计在 2021 年,我在 NLP 方面的工作并不会有太多很新鲜的东西,老实说问答和文本抽取做得已经有点腻了,自己会重点关注 few-shot learning、文本复述、text-based games 这些方向。

阅读

2020_books.jpg

图2  2020阅读记录

2020 年主要看了上面这些书,大部分都是上半年在家的时候看的,其中《血疫》《我们为什么会生病》《病毒星球》这三本都是受疫情刺激而看的相关书籍,《血疫》描绘了埃博拉病毒爆发时的惨烈场景,《病毒星球》展示了我们对病毒是有多么的不了解,《我们为什么会生病》介绍了演化医学并揭露了我们对疾病的一些错误认识。对于以前不怎么关心相关知识的我来说,这几本书还是让我得到了一些新知的。

《呼吸》《城市与群星》《月海沉船》《变化的位面》《圣天秤星》这五本书是科幻小说,其中《呼吸》是特德·姜的短篇小说集,我很喜欢里面的同名短篇;《城市与群星》和《月海沉船》是克拉克的长篇,前者有点像一个科幻版的《美丽新世界》,后者则是一出月球上的沉船救援行动故事,无论是细节的描写还是故事的讲述都非常棒,我真是太喜欢克拉克了;《圣天秤星》嘛,本质就是一个刑侦+野外探险故事,感觉一般;《变化的位面》是厄休拉·勒古恩的作品,虽然所有故事都在同一个前提(可以穿越到其他世界的机场)来展开的,但本质上就是作者放飞想象力在写各种稀奇古怪的异世界,同时把作者对社会、人性的一些看法夹杂在故事里,不乏一些辛辣的讽刺,在读这本书的时候会有一种错乱感,毕竟语言的运用也非常放飞自我,和《地海传奇》里那种文风差异太大了。

《坏小孩》《隐秘的角落》的原著,电视剧我没看,但去跟风读了原著小说。我对推理小说不是很懂,故事反正是不错啦。

剩下的书种类各异,就不再一一写感想了,只聊一下《世界观》《欲望的美学》这两本我最喜欢的书。

《世界观》这本书是讲科学哲学和科学史的,把一个个观点体系类比成拼图来分析科学史上的世界观体系及其转变,这些分析让我认识到,这个世界上,并没有一种绝对普适的世界观可以让所有人都接受,甚至「世界观」可以推广到更广泛层面的观念体系上 —— 看起来这似乎是一个很简单的道理,但是随随便便就能找出一些反例出来,比如说受过高等教育的人对信奉中医的人不屑一顾难以理解、无神论者觉得神压根就不存在怎么会有人信仰神明呢 —— 这两种看法在我周围的人身上都不少见,甚至有人说如果面试发现应聘者信中医的话立即拒绝掉。看完《世界观》这本书就会明白,这种以为自己正确而别人错误的想法下面潜藏着多少思维和认知的陷阱,而且这些自大和盲目又是怎样在历史中一次次上演,包括现在 —— 举个例子,重力其实是不存在的,广义相对论已经给出了新的解释,我们所谓的重力,其实是质量使空间发生弯曲后造成的效应,但是重力这个概念在现代教育的推动下已经变成了一个基本常识,我想数百年后的人们看待我们认为重力存在的感受,大概和我们现在看待亚里士多德认为存在以太时候的感受差不多。这本书虽然是讲科学史的,但直到第三部分才开始讲相对论和量子力学,前两部分也就讲到牛顿世界观,阅读门槛并不高,强烈推荐。

《欲望的美学》是 2020 年台湾出版的一本书,副标题是“心灵世界的陷阱与门径”,对于这两年来在艰难地进行自我认知和重建的我来说非常有帮助,书的第二部分通过分析历史上许多著名的哲学家、思想家的一些思想如柏拉图的洞穴之喻、康德的纯粹理性、中国的宋明理学,来揭露人类在探索心灵世界时所遭遇的种种陷阱,和《世界观》这本书正好可以相互印证,非常有意思。在此之前,我还读了项飙的《把自己作为方法》,老实说非常不满意,读完以后就觉得项彪说得很有智慧但在各种问题上都含糊不清并没有给读者任何确切的答案,在读过《欲望的美学》以后才大致理解了项飙想表达的是什么,本质上来说项飙的思想和《欲望的美学》这本书中表达的思想是很接近的,但《欲望的美学》胜在结构清晰层层递进,正如作者所说像是一本“心灵使用手册”,而《把自己作为方法》这本书受限于谈话的体裁,受提问者本身认知的限制,想到哪说到哪,比较随意,自然就会带来理解上的障碍了,当然如果读者本身已经有在思考项飙想传达的那些观点,读项飙的这本书的时候感觉可能会更好一些吧。

《欲望的美学》这本书目前在京东上有卖,感兴趣的可以自己去了解一下。

动画

2020_animes.jpg

图3  2020动漫记录

2020 年看过的评价尚可的动画如上图所示。

《阿薇尔的虚构世界》《阿基拉》《横滨购物纪行》都是比较老的动画了,都是偏科幻体裁的。《阿薇尔》是蒸汽朋克类的故事,科幻要素丰富,画风和宫崎骏很接近,又充满了对科技发展的反思,各方面都是喜欢的元素,所以感觉非常棒;《阿基拉》是一部影响了后来很多作品的动画,被视为日本动画的一个里程碑,我很早就知道但一直没看,趁疫情在家给补完了,对它的评价和分析在网上都可以找到,我这里就不做什么评价了;《横滨购物纪行》是很小众的动画,其原作漫画获得 2007 年日本科幻奖项“星云奖”,我是在 2019 年通过《电脑线圈》找到这部动画的,故事的背景是全球海平面上升人类近乎绝迹的末世,虽然是末世,但故事整体却非常平静、乐观,我还蛮喜欢的。

《日常》《来自深渊》《剑风传奇》和《犬夜叉》也都是老动画了,不过没有前面三个老,《日常》自不必说,轻松搞笑充满各种脑洞的日常番(指没有明确主线,以单元剧形式描绘日常生活的动画),大家都喜欢;《犬夜叉》是因为 B 站上线了就去重温了一遍,没太多好说的;《来自深渊》Q 萌的画风和残酷的剧情形成了强烈的对比,看完致郁那种,不过我挺喜欢的,大概因为这种故事里的人物会显得特别有张力和真实吧,当然得是合理、自洽地虐,为虐而虐的就算了;《剑风传奇》的话,主要是漫画没有更新来重温下动画吧。

剩余的就是 2020 年新出的动画了,这一年因为京都动画纵火案和疫情,导致很多动画延期或取消,所以总体质量比往年稍差一些,但是也不乏高品质的动画。

最喜欢的就是汤浅政明的《别对映像研出手》了,我的豆瓣友邻也纷纷给出满分评价,它讲的是三个高中生共同制作动画的故事,故事中表达出来的对动画制作的热爱和执着大概也是汤浅政明这些从业人员自己的心态,至于传达出来的对于生活的热爱和细致观察、天马行空的想象力则是非动画行业的人也能产生共鸣的,总之,这是一部能让人想起自己的理想、产生热情拥抱生活冲动的作品。《映像研》之后我把《异度侵入》排在第二位,这两部动画在我看来是在一个梯队上,但相比之下我还是更偏爱《映像研》。

剩下的动画里:

  • 《动物新世代》的结尾觉得有点强行大团圆,主角的形象很有《小魔女学园》中亚可的既视感,就是那种活力爆棚我想做的事情一定做得成的形象,虽说也不讨厌,但稍微有点没新意;
  • 《没落要塞》的是一个科幻世界观设定的故事,设定上我觉得挺有意思的,但并没有过多展开,然后女主角的性格和《动物新世代》又雷同,有点审美疲劳;
  • 《思维覆写》的故事是以若干能操纵修改他人记忆的角色为中心展开的争斗,故事讲的真的是太好了,但格局稍微小一点,里面“中国人”的塑料普通话也有点违和;
  • 《神之塔》是韩国漫画改编,据说已有故事已经非常长了,这次动画化的内容其实只是一个开篇,人物还在成长、塑造阶段,期待后续吧;
  • 《小书痴第二季》的世界观和故事我还是满意的,但是制作组有点贫穷,画面制作没有跟上去;
  • 《放学后海堤日记》《阿松》都是日常番,通过前者能学习到钓鱼知识但我并不会去钓鱼呢,后者更无厘头更搞笑一点,看起来都很轻松愉快;
  • 《咒术回战》是很典型的正反派争斗的热血故事,制作精良,如果人物能再真实丰满一点不要正邪二元分立的话我会给高一点的评价。

其他

因为疫情,2020 年我在老家待到五月份,过完五一后才返京,而之后公司一直采用两天现场办公+三天远程办公的形式,于是这一年省下了大量原来用于通勤的时间,可以自己做饭,也有了更多的独处时间,在这一点上非常感谢我的公司。由于自己做饭控制了油盐碳水,我的体重从 65kg 一路下降到了 60kg,还挺开心的,不过除了体重降低并没有其他方面的改善,因为大部分时间都宅着并没有去运动,这也是理所当然的。此外自己做饭还帮我很好地控制住了每月支出,除房租外,我努力把每月支出控制在了 2500 左右,如果不是远程工作,每天上班吃饭可能就得花掉六七十,一个月下来光餐饮支出就能超出 2000,周末偶尔聚个餐再加上水电交通日常用品,2500 基本是不够的。

2020 年我一如既往的宅,但这一年在同事们爱上了爬山,我也跟着爬了五回 —— 他们总共组织了十一次爬山,还有两位国庆去走了长线。五次爬山分别是五月份的灵山、六月份的京西古道、八月份的百花山、九月份的马栏林场和十月份再次登顶灵山。爬山的过程还是很愉快的,特别是爬野山,比走景区路线有意思太多了。当然了,不怎么运动的我体力是弱项,往往上山前半段走得飞起,后半段就没力气迈不动腿了,虽然慢慢变强了一点点,但总体上还是很弱。

灵山天梯.png

图4  灵山天梯

灵山山顶风光.jpg

图5  灵山山顶风光

灵山狐狸.jpeg

图6  灵山上完全不怕人的狐狸

迷雾森林.jpg

图7  百花山之迷雾森林

指点江山.jpg

图8  百花山之指点江山

百花山顶.jpeg

图9  百花山山顶草甸

期待 2021 年的爬山活动!

本周每日写作尝试总结

2020年11月22日 08:00

尝试了在本周(周一到周五)每天写一个当日摘要发到博客上,想确认一下我每天可以输出什么东西,也看一下自己是否能坚持这种写作方式,下面这五篇文章就是成果:

  1. ZMonster's 每日摘要 2020-11-16 · ZMonster's Blog
  2. ZMonster's 每日摘要 2020-11-17 · ZMonster's Blog
  3. ZMonster's 每日摘要 2020-11-18 · ZMonster's Blog
  4. 2020-11-19: Neo4j 多 DB, 笔记的目的 · ZMonster's Blog
  5. 2020-11-20: 柏拉图立体 · ZMonster's Blog

经过一周的尝试后,我有了以下体会或收获

  • 对于仍然在全职工作的我,每天写作这件事情很有挑战性、很困难,即使我每天只是贴了一点干巴巴的笔记上去 —— 这当然有多方面的原因了,一方面在写作上我缺乏练习,另外一方面我的知识和见解也不足以支撑起我每天的表达,比较关键的一点是,我可能有点缺乏表达欲
  • 在 11 月 19 日这篇博客里,我有提到这个短期写作计划的主要目的之一是想尝试用输出的行为去倒逼我完善自己的知识管理系统,经过一周的尝试后我也确认了这个方式对改进我的系统帮助并不大,因为我的知识管理系统的主要问题并不在这里。我反思了下,我主要的问题是(通过 RSS)接收碎片信息过多 —— 虽然目前我的 RSS 里数量最大的部分是 Arxiv 上的论文更新,但是大多数论文我都是扫一眼就略过,真正会去看的并不多,这种情况下它们和碎片化信息的价值无异
  • 通过这几天的写作,我意识到我目前的 org-roam 笔记结构中的一些问题,比如说对写作不友好、链接过于集中在某些点上之类的

我决定停止这个写作计划,改为每周写一篇,这样可以做到言之有物,同时对我来说负担也不算重。

2020-11-20: 柏拉图立体

2020年11月20日 08:00

尝试在本周每天写一个当日摘要发到博客上,对于当日摘要要写些什么东西,暂定会有「笔记」和「时间」两块,不过我的想法随时可能会变,也许会在之后的几天产生新的想法,这一周时间一来是想确认一下我每天可以输出什么东西,然后也看一下自己是否能坚持这种写作方式吧。

想法

最近都在重构一个项目的代码,遇到一个问题是,梳理完旧代码中的各种问题后,想出来一个更理想的设计,但在真正开始实现后,就会发现这个理想的设计的假设是有问题的,一般都是某个具体的问题没有考虑到。不过我并不想讨论编码问题,我只是觉得,这种问题还蛮常见的,就是自己设计一个理想的流程、工具、系统,但总是会遇到各种问题,而我们都会在这个「提出理想设计->发现问题->修正设计」的过程中积累一些经验,这些经验(遇到的问题、修正的思路)是不是有共性的呢?

我觉得这是一个有意思的问题。

笔记

20201120-daily-notes.png

  • 著作: 蒂迈欧篇

    柏拉图晚年的一部对话录作品,以苏格拉底等哲学家对话的形式试图去阐明宇宙万物的真理。

  • 术语: 柏拉图立体

    几何学中凸正多面体的另一个称呼,由于柏拉图对它们的记录而得名。凸正多面体有且仅有五个,分别是正四面体、立方体、正八面体、正十二面体和正二十面体,柏拉图在对话录《蒂迈欧篇》中将这五个正多面体和宇宙的构成联系起来,认为四大基础元素中火是正四面体、土是正六面体、气是正八面体、水是正二十面体,而正十二面体代表宇宙。

    platonic-solid.jpg

    看到一篇科研文章1说到这么一个事情,从几何学上,可以证明把一个三维物体反复二分割后得到的碎片,最后它的形状会趋近于六面体,并且该理论通过地质学研究得到了一定程度的验证,这和柏拉图认为土元素是正六面体的说法似乎就对上了。

    当然,大可不必因此就觉得柏拉图代表了真理。我比较感兴趣的是上面这个研究的几何部分,似乎可以用程序来去模拟,找了下就发现了 sympy 这个 Python 库可以用来做相关的几何计算。

  • 软件: sympy

    一个 Python 的符号计算库,能用来进行包括微积分、代数、几何、方程求解等多种数学问题的计算。

时间

20201120-time-usage.png

2020-11-19: Neo4j 多 DB, 笔记的目的

2020年11月19日 08:00

尝试在本周每天写一个当日摘要发到博客上,对于当日摘要要写些什么东西,暂定会有「笔记」和「时间」两块,不过我的想法随时可能会变,也许会在之后的几天产生新的想法,这一周时间一来是想确认一下我每天可以输出什么东西,然后也看一下自己是否能坚持这种写作方式吧。

今天加了一节「想法」,改了一下标题。

想法

写当日摘要第四天了,老实说感觉挺困难的,因为不是每天都能学到新的有价值的知识分享出来,像今天展示的笔记内容,其实没有一条是今天才创建的,十点多的时候我就在发愁今天写些什么好,想不出来,干脆去整理自己的笔记(因为存在不少笔记的层次混乱或者没有填充具体内容),在这个过程中想到今天得知的 Neo4j 新版变化,然后才整理了点东西出来。

我开始这个博客写作的短期计划,其主要目的之一是想尝试用输出的行为去倒逼我完善自己的知识管理系统,因为我发现我最近一段时间陷入了一个机械式学习东西然后往笔记库里填充材料的不良状态,除了看到笔记越来越多,得到的其他反馈很少。那么这个写作的计划有没有效果呢?确实是有的,现在我每天在扫资料的时候,开始会自发地询问自己看的这些东西是否有分享出去的价值、是否能和我已有的知识关联起来。

不过一天一篇还是感到有点累了,好在今天是周四了,周五熬过去,周末会有更多的时间用来做学习和写作的事情。

笔记

20201119-daily-notes.png

  • 术语: 知识图谱

    知识图谱并没有一个非常清晰的定义,大致可以用来泛指以图的结构存储实体以及实体之间关系,并以此来表示人类知识的一种知识库。其实通过结构化数据来表示并应用人类的知识,这是人工智能这一领域最早的时候符号主义学派的主张,自动定理证明、棋类 AI、游戏 AI 都和这个思想有关,这一条路子的发展在上世纪 80 年代左右的时候随着专家系统的商用达到顶峰。

    我做过一段时间知识图谱相关的工作,这一方向的主要问题是,构建图谱很困难 —— 先不说应用阶段是否有足够强的推理算法能力,因为如果真的要在知识图谱上建立起丰富强大的应用,一个大前提是要有一个足够大的、细节足够完善的知识图谱,而构建知识图谱是一个系统性工程,不是一个不可分割的原子问题,这就决定了没有一个单独的工具或流程能解决它,但目前也并没有成套的、成熟的、易用的解决方案。

  • 术语: 图数据库

    所谓图数据库是一种 NoSQL 数据库,相比于 MySQL 之类的关系数据库(RDBMS),能更灵活地表示数据,这种灵活性体现在多方面:

    1. 像所有 NoSQL 数据库一样可以灵活地设计、扩展 schema
    2. 更适合表示实体之间的关系,特别是当实体之间存在大量的、复杂的关系的时候

    图数据库强调实体和关系两个基本概念,虽然说在关系数据库中也可以表示实体和关系,但如果关系的种类繁多且实体之间通过关系构成复杂的结构的时候,用图数据库可能会更合适一些。此外,图数据库会对一些常见的图操作进行支持,典型的比如查询最短路径,如果用关系数据库来做就会比较麻烦。

    知识图谱数据是比较典型的图数据。

  • 软件: Neo4j

    Neo4j 是一个流行的、Java 编写的图数据库,有比较易用的查询语句和可视化操作界面,我在之前写过一篇相关的介绍文章

    今年二月份 Neo4j 发布了 4.0 版,终于支持多 database 了 —— 多 database 这种在关系数据库中非常基本的功能,Neo4j 4.0 之前的版本是不支持的,这就导致当我们想要隔离数据时不得不部署多个 Neo4j 应用,管理起来非常的麻烦。

    为什么二月份发布的我现在才知道呢?因为很久没碰知识图谱这块了,但这周我司升级狂魔 SA 在迁移集群资源的时候发现了这个更新并告知了我。有多 database 功能的话,我构想中的基于 org-mode 和 Neo4j 的知识管理工具就更有吸引力了啊(然而挖坑半年了)。

  • 插件: ob-cypher

    主页: https://github.com/zweifisch/ob-cypher

    一个让 Emacs 用户能在 org-mode 的代码块中写 Neo4j 的查询语言并执行得到结果的插件,如下图所示:

    ob-cypher.png

时间

20201119-time-usage.png

ZMonster's 每日摘要 2020-11-18

2020年11月18日 08:00

目录

尝试在本周每天写一个当日摘要发到博客上,对于当日摘要要写些什么东西,暂定会有「笔记」和「时间」两块,不过我的想法随时可能会变,也许会在之后的几天产生新的想法,这一周时间一来是想确认一下我每天可以输出什么东西,然后也看一下自己是否能坚持这种写作方式吧。

笔记

20201118-daily-notes.png

  • 机构: 北京智源人工智能研究院

    主页: https://www.baai.ac.cn/

    北京智源人工智能研究院(简称“智源研究院”)是落实“北京智源行动计划”的重要举措,是在科技部和北京市委市政府的指导和支持下,由北京市科委和海淀区政府于2018年11月推动成立的新型研发机构。其组织架构中的成员有旷视、百度、小米、美团各大公司的高管以及清华、北大、中科院、加州大学、普林斯顿大学、康奈尔大学的学术研究者。

    看起来是一个重量级的机构,可以保持关注。

  • 术语: 清源CPM

    cpm.png

    主页: https://cpm.baai.ac.cn/

    智源研究院清华大学合作开展的大规模预训练模型开源计划,首期开源内容包括预训练中文语言模型和预训练知识表示模型,可广泛应用于中文自然语言理解、生成任务以及知识计算应用,所有模型免费向学术界和产业界开放下载,供研究使用。

  • 术语: CPM-LM

    清源CPM 推出的 26 亿参数的中文预训练语言模型,是至 2020 年 10 月为止最大的中文预训练语言模型,从宣传上来看是在对标 GPT3 模型。

    cpm-lm-example1.png

    cpm-lm-example2.png

    详细介绍见智源研究院公众号的文章1

  • 术语: 预训练语言模型

    PLMfamily.jpg

    指用大量文本数据训练出来的语言模型,这种模型可以在之后后用做各种各样的 NLP 任务的基础设施,在这些 NLP 任务上只需要对模型结构做微小修改(通常是输出层)然后用该特定任务的数据进行少量继续训练(这种训练一般称之为「微调」)后,通常就能取得不错的效果。

  • 术语: ERNIE

    baidu-ernie.png

    百度提出的使用知识图谱进行增强的预训练语言模型。

  • 平台: EasyDL

    主页: https://ai.baidu.com/easydl/

    百度的 AI 平台,和以前那些提供 API 的功能不一样,这个平台可以让用户管理数据、选择和训练模型、发布模型,有点对标 AWS SageMaker 的感觉。EasyDL 对非 AI 技术人员很友好。

    提供三种不同的模式:

    1. 经典版2:用户不需要关心(也不让关心)模型细节,只需要上传数据后触发训练然后发布模型即可使用,非技术人员也可以轻松上手

      经典版可处理的任务类型有:

      • 图像领域:图像分类、物体检测、图像分割
      • 文本领域:文本分类(单标签)、文本分类(多标签)、情感倾向分析、文本相似度
      • 视频分类
      • 声音分类

      我比较关心文本方面的,以文本分类(单标签)为例,其操作如下:

      • 创建模型

        easydl-1.png

      • 训练模型

        easydl-2.png

        easydl-3.png

        easydl-4.png

        easydl-5.png

        「训练中」后面那个感叹号点击后有一个选项,开启后可以在训练完成后发送短信通知用户。我用 cnsenticorp 情感分类数据 8249 条数据进行训练,用时 8 分钟,应该是用到了预训练模型(很有可能就是 ERNIE),不然不可能这么慢的。

      整体的使用体验还挺好的。

    2. 专业版3:面向算法工程师的服务,让用户可以自主选择使用什么深度学习模型(CNN/BiLSTM 等)并进行参数调整

      专业版可处理的任务类型有

      • 图像领域:图像分类、物体检测
      • 文本领域:文本分类(单标签)、文本分类(多标签)、短文本匹配、序列标注

      以短文本匹配为例来看下其使用过程:

      • 创建项目

        easydl-6.png

      • 创建任务

        easydl-7.png

        这里会确定模型结构,同时直接生成代码,需要的话用户可以编辑这个代码。如果使用预训练语言模型 ERNIE 的话,选择网络那里就只有 FC(全连接层) 可选了。如果想对模型做一些自定义修改的话,可以且仅可以在这个时候编辑模型的代码。

      • 训练

        运行环境居然有 GPU V100,16G 显存,56G12U,可以啊……

        easydl-8.png

        完成前面的设置后即可提交训练任务。我选择了 LCQMC 数据集用于训练,共 23w 条数据,耗时 55 分钟,也还行哦。

        可以看到训练过程产生的日志

        easydl-9.png

        训练完成后可以看到模型评估报告

        easydl-10.png

    3. 零售版4:面向零售行业,提供商品检测、货架拼接及其他零售方面的 AI 能力

时间

  • 虽然昨晚也睡了近 8 个小时,但睡得晚起得晚,中间还醒过几次,导致今天精神不太好。

20201118-time-usage.png

ZMonster's 每日摘要 2020-11-17

2020年11月17日 08:00

目录

尝试在本周每天写一个当日摘要发到博客上,对于当日摘要要写些什么东西,暂定会有「笔记」和「时间」两块,不过我的想法随时可能会变,也许会在之后的几天产生新的想法,这一周时间一来是想确认一下我每天可以输出什么东西,然后也看一下自己是否能坚持这种写作方式吧。

笔记

20201117-daily-notes.png

  • 术语: transclusion

    transclusion-multiple-foo.png

    一种通过模板语法的形式将其他文档的内容嵌入到当前文档,并支持将嵌入文档的内容和当前文档的内容共同渲染展示甚至一起编辑的功能,从维基百科1来看该功能是 MediaWiki 提出的,该功能在知名的个人 wiki 软件 TiddlyWiki 中也有实现。

  • 软件: MediaWiki

    MediaWiki 是一款开源的百科引擎软件,维基百科和很多百科类网站都是使用它来构建的。

  • 软件: TiddlyWiki

    一个个人 wiki 工具,它自称是一个用于捕获、组织、分享复杂信息的非线性笔记本,可以通过《使用 TiddlyWiki 打造轻便个人 Wiki 知识库》这篇文章了解其特点。

  • 插件: org-transclusion

    在 Emacs org-mode 中实现 transclusion 的一款插件,要求 Emacs>=27.1 & org-mode>=9.4,目前该项目的开发还处于非常早期的状态。对 transclusion 概念不是很熟悉的读者可以通过这个 Youtube 视频获取更直观的认识。

时间

20201117-time-usage.png

  • 昨晚睡得很不错
  • 今天去公司上班,但因为要通勤的缘故,相比昨天 remote 工作时间反倒减少了
  • 中午吃了涮羊肉,感觉还能再来几盘
  • 同事带的风吹饼吃得停不下来
  • 公司 2080Ti+3090 的 GPU 机器过于耗电,导致了一次断电事故

ZMonster's 每日摘要 2020-11-16

2020年11月16日 08:00

目录

尝试在本周每天写一个当日摘要发到博客上,对于当日摘要要写些什么东西,暂定会有「笔记」和「时间」两块,不过我的想法随时可能会变,也许会在之后的几天产生新的想法,这一周时间一来是想确认一下我每天可以输出什么东西,然后也看一下自己是否能坚持这种写作方式吧。

笔记

20201116-daily-notes.png

  • 术语: 内部协方差漂移(Internel Covariate Shift, ICS)

    指深度学习模型在训练过程中,每次迭代后因为参数被改变导致模型输出的分布发生变化,导致训练过程为了适应不同的模型输出分布而训练收敛慢的问题。

  • 术语: Batch Normalization

    指在训练时用一个 batch 的数据计算均值和方差后将每层输出归一化到标准正态分布,以解决内部协方差漂移使得模型训练慢的问题,实际做法中为了保持每一层的表达能力,在归一化到正态分布后又会通过两个可学习参数(均值和方差)再变换到一个非标准正态分布上,所以实际上内部协方差还是会在漂移,后来也有研究说这种做法会使得 loss 曲面更加平滑所以有效。

  • 术语: Layer Normalization

    指在训练时用一个样本在每层的输出单独计算均值和方差后将这个输出归一化到标准正态分布上的做法,据说也能缓解内部协方差漂移问题从而提高模型的收敛速度。Transformer 系的模型都使用了 Layer Normalization,下图中的 Norm 指的就是 Layer Normalization。

    transformer.png

  • 术语: Pre Layer Normalization

    在 Transformer 之类的模型中,输入经过 self-attention 后,经过一次线性变换后才做 Layer Normalization 处理,所谓 Pre Layer Normalization 是指在类似的情况下,在线性变换之前就进行 Layer Normalization 处理。提出这一方法的论文1声称它这一简单的修改就能对模型有提高。

  • 观点: Pre Layer Normalization 在 Transformer 模型上比 Layer Normalization 好

    论据:

    • 从理论上分析,先做线性变换再做 Layer Normalization 的问题在于,线性变换的参数矩阵如果是从一个各向同性的高斯分布中采样初始化的话,即使输出不是零均值的,经过变换后也会变成零均值,这会使得 Layer Normalization 的效果变差
    • 在 LM1B 数据集上,改成使用 Pre Layer Normalization 的模型比原版模型的 PPL 低 0.3(38.341->38.002)

    个人看法

    • 实际上到目前为止 Batch Normalization/Layer Normalization 起作用的原因也并没有研究很清楚,所谓 ICS 问题,在 Batch Normalization 上其实不太站得住脚,倒是让 loss 曲面更加平滑这个理由更好理解一些,所以在 Transformer 中线性变换在前而 Layer Normalization 在后会导致效果变差,可能并不是一个很强的理论分析
    • 再说效果,PPL 只降了区区 0.3 不到 1%,这也不是一个很强的改进,再说了 LM1B 是个啥数据集?不在 GLUE 上跑一跑难以服众啊

时间

20201116-time-usage.png

脚注:

1

Is Batch Norm unique? An empirical investigation and prescription to emulatethe best properties of common normalizers without batch dependence

用 Huginn 为高频 RSS 生成每日摘要并输出新的 RSS

2020年10月24日 08:00

我订阅了一些资讯类的 RSS,但是这种 RSS 通常更新频率都较高,好一点的一天十来篇文章,烦一点几十篇都有可能。但其实我订阅这些 RSS,只是想要对相关的领域(如时事、游戏)保持一定的关注度,更希望是定期(比如每天)整体扫一眼看有没有关心的内容,而不是在一天的各个时间段内连续不断地收到更新。

基于这个想法,我就想找个现成的工具为这种 RSS 生成一个每日摘要,但是只能找到从 RSS 生成摘要邮件的一些方法,而在邮件里看资讯并不是我习惯的方式,还是得自己动手啊。

思考尝试了下,这个问题有两种解决方法:

  1. 先用 Email Digest 工具(如IFTTT)将 RSS 转成摘要邮件,然后再将邮件转成 RSS,最后这一步可以用 Zapier
  2. 直接上 Huginn,RSS Agent + Digest Agent + Delay Agent + Data Output Agent 一套搞定

第一个方案我虽然也能操作,但是始终要在邮箱里过一遍,我还是嫌麻烦,而 Huginn 是我非常熟悉的工具,于是就选用了第二套方案。花了几天验证效果,调整之后的 Scenario 如下图所示:

digest-scenario.png

机核的 RSS 为例,从上到下五个 Agent 分别是:

  • RSS Agent:负责监听原始的 RSS 源,接收更新生成 event

    {
      "expected_update_period_in_days": "5",
      "clean": "false",
      "url": "https://www.gcores.com/rss"
    }
    
  • Digest Aget:负责聚合一定时间 RSS Agent 输出的 event 产生一个新的 event,我设定为每天凌晨 1 点执行,这样能把前一天所有的 RSS 更新聚合起来

    {
      "message": "<ul>{% for event in events %}<li><a href={{ event.url }}>{{ event.title }}<\/a><\/li>{% endfor %}<\/ul>",
      "expected_receive_period_in_days": "2",
      "retained_events": "0"
    }
    
  • Event Formatting Agent:给 Digest Agent 产生的 event 添加一个标题

    {
      "instructions": {
        "content": "{{ message }}",
        "title": "机核{% assign current_date = 'now' | date: '%s' | minus: 86400 %} {{current_date | date: \"%Y-%m-%d\" }} 摘要"
      },
      "matchers": [
    
      ],
      "mode": "clean"
    }
    
  • Delay Agent:延迟一定时间后将 Event Formatting Agent 的输出再传递给最后的 Data Output Agent,我设置为延迟到早上六点

    {
      "expected_receive_period_in_days": "3",
      "max_events": "100",
      "keep": "newest",
      "max_emitted_events": "1"
    }
    
  • Data Output Agent:将最终结果输出为新的 RSS

    {
      "secrets": [
        "gcore-daily"
      ],
      "expected_receive_period_in_days": 2,
      "template": {
        "title": "机核-每日摘要",
        "description": "机核每日消息汇总",
        "item": {
          "title": "{{ title }}",
          "description": "{{ content }}",
          "link": "https://www.gcores.com/"
        }
      },
      "ns_media": "true"
    }
    

这样我每天早上六点就能收到一个前一天的汇总列表了,效果如下:

digest-rss-item.png

除了机核,当然还会有其他资讯类 RSS 想做这个转换,每次都在 Huginn 上手动创建一个个 agent 也不是个事,就顺手在我的个人脚本仓库里加了一个脚本,还是以机核为例,只要执行下面的命令就会生成一个 Huginn 的 Scenario 文件,然后到 Huginn 上直接导入就好了:

zs-rss gen-daily-scenario --feed-url "https://www.gcores.com/rss" -n 机核 -o gcore.json

目前制作了两个这样的每日摘要 RSS,分别是:

输出的 RSS 里只有原始 RSS 里文章的标题和链接,文章内容被我丢掉了,之后考虑改一下把内容加上(如果有的话)。

把自我作为方法

2020年10月11日 08:00

自我是什么

在面对生活中各种压力的时候,总能发现一些自己身上以前没有注意过的问题,早些年我会将其中一些问题归咎于外界,认为这是世界的问题、他人的问题,带着愤怒消极或者激烈地去对抗,有时候会有点效果,但更多的时候不过都是徒劳。世界不以个体为中心而存在,这个道理或许很早的时候我们就能脱口而出,但希望世界是以我为中心这种隐秘的想法,或多或少、某时某刻,还是会涌现出来,为了对抗我们自身存在的虚无感。

遭遇挫折后,首先,我们会发现我们掌控不了全世界,接着就会发现自己周围的小世界也是如此,疾病随后当头一棒逼迫我们认识到即使是掌握自己的身体也需要持久的付出,而心灵 —— 连笛卡尔也只能说我思故我在,普通人心灵的浅薄和无知,老实说并没有什么价值。这种对自我存在价值的全面否定 —— 或者说找不到自我存在价值的感受,我想所有人都会有。哲学家、心理学家、科学家在这方面都有很多讨论,但最终怎么在废墟上把自己捡起来,还是得靠自己的学习、思考和实践。很惭愧,我在哲学、心理学、科学的相关方面都涉猎不深,不过是断断续续有一些自己的想法,并且觉得这些想法开始慢慢形成一个不那么脆弱的整体,勉强可以应付一些小问题,所以尝试把这些想法写下来。

既然自我价值的崩塌是由外而内的,那么重建的时候不妨逆着这个过程,由内而外来进行,先把自我观念建立起来,简单来说,就是好好认识自己,诚实地面对自己的想法,在此基础上,或许能更为有效地与周围的世界共存,不说改变世界,起码过得开心自在,岂不美哉。不过所谓内、外可能并没有那么界限分明,我所谓的由外而内、由内而外不过是由于我这个人类个体浅薄的认知和语言的无力而产生的不精确表述,不必纠结。

在我的想法里,自我观念会是支撑世界观、人生观、价值观的基石,因为:

  • 我们所认识到的世界不过是真实世界通过我们的感受器官形成的一个投影 —— 按照柏拉图的说法,我们只是在山洞里看到了外部世界的影子;从生物学上来说,人类能感受到的可见光只有波长 300 多纳米到 800 纳米这么小小一段,可感知的声音也只是频率为 20 赫兹到 20000 赫兹的一段;从物理学上来说,时空是十一维的,我们不过能感知其中的四个维度 —— 三维空间和时间,想象一下四维空间就能让普通人的大脑宕机,这方面我们不比蚂蚁有太多优势,数学家除外……这些与生俱来的限制,决定了我们对世界的认识必然是极其不完善的,我们没有办法去克服这些限制,但认识到这些限制,或许能帮助我们纠正一些对世界的错误看法
  • 所谓人生观,不过是如何看待自己的过去、如何看待自己的未来,而人类记忆的不靠谱和自我欺骗的习性都会扭曲我们对过去的印象,至于对未来的看法,无外乎基于过去的经验对未来进行预测或规划,首先受到过去经验的限制,接着又受到此刻自我观念的影响,所以更清楚地认识自己,能帮我们更理智地看待过去并应对未来的各种不确定性
  • 世界观和人生观方面,我们或许还可以努力客观一点,但价值观则完全是主观的,因此也和自我观念的关系更加紧密;此外,自我观念是时刻在变化的,价值观又可以反过来模拟「理想自我」来帮助自我改进

事实上自我观念、世界观、人生观、价值观四者之间的关系很复杂,并不能简单区分开来,但整体上来说,不妨抛开哲学、心理学的话语,将「自我」作为一件认识、影响世界和实现自我价值的工具,熟悉和改进这个工具则是我的目的。至于为什么要抛开哲学、心理学的话语,自己新发明一套话语,主要是哲学、心理学方面的话语我不懂(吃了没有文化的亏),另外就是觉得哲学、心理学的话语都不够精确。

我将「自我」简化为这样一个系统:

\[y_{t} = f(x_{t}|C, b, h)\]

其中 \(x_{t}\) 表示当前发生的事件,\(C\) 表示当前的环境,\(b\) 表示当前我的身体状况,\(h = x_{0},x_{1},...,x_{t-1}\) 表示我的过往经历(由过去发生在我身上的事件序列构成),\(y_{t}\) 表示这个系统输出的一个事件。也就是说,它接受一个事件输入,根据当前的环境、身体状况和过往经历作出反馈,仅此而已。

这样定义之后,我就可以摆脱「我思故我在第一个我和第二个我是不是同一个对象」这种无比纠结的语言游戏了,也不用在什么本我、自我、超我、经验自我、纯粹自我、现实自我、理想自我、客我、主我……的话语里打转了。我所定义的这个系统局部来看是处理好一个个事件,也就是做好当下的一件件小事情,全局来看则是将来也能处理好一件件事情,这样就足够了。

在这个基础上,我也可以尝试去回答一些以前难以回答的问题了,比如说:

  • 如何认识自我?

    简单,给定各种场景,询问自己在该场景下会如何反应,仅此而已,不需要去刻画一个整体的「人格」,也可以摆脱「我爱读书」、「我很内向」这种标签化的自我评价。

  • 如何改善自我?

    简单,改进这个系统,让它能在未来,对相同的事件作出更好的响应,仅此而已,不用考虑什么幸福、自由之类的虚无缥缈的概念 —— 我并不是说幸福、自由没有意义,只不过幸福、自由是一个难以界定的准则,直接以此作为评价标准是否具有指导意义很不好说。

  • 如何确定自我价值?

    简单……好吧,仍然不简单。必须要承认,确定自己的价值,仍然是一个很困难的事情,毕竟人类的存在毫无意义。但基于我这个定义,仍然可以尝试来回答这个问题:好比软件的价值是其所具备的功能,我所描述的这个系统也是一样,它的功能就是输入一个事件然后输出一个事件,那么这些功能得到使用就是其价值所在,换言之活着就是自我的价值所在(看来我是一个存在主义者);或者换另外一种说法,还是类比软件,软件的价值是对用户有用,我所描述的这个系统,大一点不妨将整个世界当作用户,小一点不妨将和我有互动的人、物当作用户,响应这些用户的操作就是这个系统的价值所在,只不过这样的话,对无神论者来说要满足很多很多用户的需求,宗教信徒的话只要让神这个 VIP 用户满意就好了。

    虽然这样得到的回答仍然不令人信服,但至少我有了一个相对自洽的结果,对此我很满意了。

初步解决了「自我」的建模后,仍然存在一些根本性的问题需要回答,比如说:

  • 改善自我方法中提到了「更好的响应」,所谓的「更好」是什么?
  • 在上一个问题未得到满意回答的情况下,是否有办法制定「更好的自我」的评判标准?

第一个问题,所谓的「更好的响应」,是无法由这个系统本身来给出精确答案的,让系统评价自己的行为是否合理,这将会陷入一个死循环。但正如我作出这个定义是为了避免去追寻一个单一、绝对的自我评价一样,一个单一、绝对的判断这个系统行为是好是坏的准则,在我这里也不需要存在,它可以是一个动态的、随时变化的标准,简单来说就是好坏是需要根据当时所处的环境 \(C\)、身体状况 \(b\) 以及个人经历 \(h\) 来共同做决定,比如说

  • 早上睡醒了,根据过去的经验,在床上刷手机会推迟起床时间影响当日计划导致晚上因为碌碌无为而心生懊悔,那么马上起床就是一个更好的行为
  • 出门去上班,此时天气晴朗微风缕缕气温舒适,那么骑车 10 公里去上班,比坐地铁去上班,就是一个更好的行为
  • ……

虽然上述例子已经把评价的前提条件缩减到了「当前场景」,但其实还是隐含了一些价值判断在这里,比如说「完成今天计划做的事情」比「没有完成今天计划做的事情」更好,这就涉及到价值观的问题,仍然非常模糊,但这样已经足够了,一件件小的事情的价值是相对容易判断、记录、分析的。

如果非要用一个终极的准则比如说幸福感去指导,会怎么样呢?早起会让我更幸福吗,不见得,躺被窝里多幸福啊;骑车 10 公里去上班会让我更幸福吗,那也未必,顶多就是心情舒畅一些。再说了,幸福到底又是什么呢?

对这种标准的另外一个担心是,只看眼前,会不会因为过于短视而损害未来的长期收益?当然会有这个问题,但我的理念是,未来是不可预测的,长期收益也很难判断,至少我判断不了,与其在每一件事情上去衡量未来能有多少回报,不如做好当下的事情;此外,可以将长期收益逐级分解,比如说把一年目标分解成很多小目标,这些小目标就有可能作为指导当前行为的一个准则,也不失为解决短视的一个办法,但这需要很强大的问题拆分、执行能力,不知道有多少人能做到呢?

熟悉人工智能的人,大概能看出来我对「自我」的建模,其实借鉴了强化学习的模型,在强化学习里也是一样,评估长期价值很困难。最近有看到一篇新的论文很有意思,是在 NLP 任务中应用强化学习,然后用情感分析的结果作为每一个决策的收益,这个也可以借鉴到这这篇文章讨论的问题上来,比如说,如果一个事件响应完后能让「我」感到开心、平静这种正面的情绪,那么它就是一个好的响应。甚至可以用金钱收益(或损失)作为标准,这里就不再展开了。

第二个问题,我认为是有的,那就是去考察这个系统内在的一些特性,比如说

  • 连续性: 在短时间内,相似的场景下,对相同的事件,系统能作出相近的响应 —— 不要做一个反复无常的人
  • 可解释性: 对每个事件的反应,可以用语言给出合理的解释 —— 不要做一个不可理喻的人
  • 适应性: 当场景、身体状况、事件发生变化时,能给出不同的响应,但这种变化的程度不应大到破坏「连续性」 —— 不要做一个顽固死板的人
  • 逻辑上的一致性: 从可解释性那里提炼的逻辑,应该在适用的场景下也发挥作用,如果在某个适用该逻辑的场景下实际的响应逻辑与前面的逻辑矛盾,那么视为破坏了逻辑一致性 —— 不要做一个自相矛盾的人

对「自我」这个系统的内在评判指标,暂时就想到这么多了。

把自我作为方法

前面我聊了很多我建模「自我」的想法,但说到底,这些都不过是思维游戏,或者说语言游戏,如果它不能真地在我的个人生活中发挥作用,那么无论我将其设计得多么精确、细致,也不过是空中楼阁、纸上谈兵。工具当然要拿来使用,模型当然要拿来进行预测。项飚在《把自己作为方法》一书中表达的一个核心观点,就是「亲身实践」对认识世界和社会是很重要的,看起来像是废话,但我们可以反思一下,我们很多行为的动机和依据,可能都不是来自于自己的经验,而是「别人都是这么做的」、「我听说……所以……」,很多人口口声声独立思考、自由人格,实则不堪一击。

我将「自我」建模,就是要将其作为一件工具,这件工具的目的是自我完善、探索世界甚至影响世界 —— 不要误解,影响我周围一平方米也算是影响,我并没有什么很大的野心。为此,我首先要做的是熟悉这件工具,与自己磨合,并逐步改善。

作为一个程序员,在我的学习、工作经验中得到的一个经验就是,如果你想了解一个工具,那么就去使用它,然后进行观察,在足够的观察之后对其进行分析,在此基础上才能进行改进。为什么自我改进、自我提高这么难,因为很多人对「自我」这个系统并没有有意识地、主动地去使用和观察,在这种情况下何谈改进呢?因此,为熟悉「自我」这件工具,我正在做以下几件事情

  • 记录我的情绪反应,包括当时的场景、事件,并尝试解释我的情绪反应是如何发生的
  • 记录我每天的时间使用,包括我的工作、学习、日常生活和娱乐,目前我已经坚持了一个多月,平均能将自己每天 90% 以上的时间使用情况记录下来
  • 记录我每天的支出状况,已经坚持进行四年有余
  • 记录我读过的书、看过的影视剧、玩过的游戏

通过这些记录,我比过去更加清楚了我自己的一些特性,在详细的记录下,自我欺骗无所遁形,比如说我的情绪控制能力其实很差,过去我会给自己各种借口,比如说工作压力很大啊、我是对的别人是错的怎么对方那么顽固啊,但记录下来当时的细节后,哪怕我当时做记录的时候还有情绪,但是对环境、人物的描写仍然会尽量客观,那么之后在冷静下来后再去回顾,就会发现我自己的顽固和进攻性是那么的明显。

做记录还有一个好处,就是觉察自己的每一个行为 —— 比如说,在我开始做时间使用记录前,我决定去玩游戏可能是一个潜意识的行为,可能是因为厌倦了工作想要松口气或者心情不好想要发泄一下,但这些都是下意识完成的,但相比玩游戏,可能去休息 10 分钟会更好一些;当我开始做记录后,我需要在开始玩游戏时进行计时,这里就让我获得了一个停顿的时间点,让我能意识到我要主动地去玩游戏了,多这么一个停顿,就能觉察到自己所要做的事情,就给了我判断这个行为合理性的时间,这对我来说是非常有用的。觉察到自己的行为,除了帮助我更好地决策外,还让我对「现在」的感受更加的强烈和清晰了,让我感受到了更多的乐趣。

目前为止,我做的主要事情,还是在记录上,在分析、改进方面做得并不多,接下来的计划有这么一些

  • 做更多方面的记录,比如食物的摄入、周围环境的变化、我对不同事件(除情绪外)的反应……
  • 定期(按周或按月)回顾这些记录,分析问题、总结经验

除了记录、分析外,需要注意的是,这个工具是会受到环境、身体状况影响的,所以学习相关的知识也是很有必要的(看,学习的意义也有了!),不过学习是一件终身的事情,不太好做很具体的规划,目前也还是以记录为主,我目前是这样记录阅读过程的:

  • 用 Zotero 从豆瓣或者 Amazon 抓取一本书的元信息(作者、出版社、ISBN 等)
  • 找到这本书的电子版,作为附件添加到 Zotero 条目中
  • Zotero 使用 Better BibTex 导出成一个文献数据库文件,其中包含了这个书籍
  • 在 Emacs 中使用 helm-bibtex 查找到这本书,使用 org-roam 创建阅读记录,使用 org-noter 进行阅读,记录标注、笔记
  • 使用 org-roam 将阅读过程中学习到的概念、方法分离出去形成单独的笔记,并链接回来

关于笔记、知识管理,准备之后单独写一篇文章来阐述细节,这里就不展开了。

漫谈

前面也提到,我为「自我」所做的定义,其实很大程度借鉴了人工智能里的强化学习模型,就是击败围棋世界冠军的 AlphaGo 所使用的核心算法。人工智能领域的很多算法、模型,都比心理学、哲学更具有参考价值,首先里面的模型其实本来就是从认知科学里分化出来的,然后为了应对实际问题而进行了简化、量化,消解掉「心智」这个神秘的成分,留下冷酷的数学公式,这就使得这些模型天然地就和人类的认知过程贴近,同时又精确而可执行,是任何人都能借鉴的。

除了强化学习,我再列举一些人工智能、机器学习里的概念

  • 误差反向传播: 对比实际结果和预期结果之间的差异,从后往前反推差异的来源并进行调整 —— 类比到人类身上的话,就是设定预期,然后复盘分析问题,很直白的道理
  • 过拟合: 当一个模型只在很少的数据上进行学习时,往往会过度适应这些数据,导致无法应对正确新的数据 —— 类比到人身上的话,见识太少,面对新问题时就很容易手足无措
  • 对比学习; 通过大量相同事物的有差异的表现来认识这个事物
  • 知识蒸馏: 用一个很大但效果很好的模型去教一个小模型,让这个小模型也能表现很好 —— 类比到人身上的话,向优秀的人学习是提高自己的一个很好的方法
  • 对抗训练: 模型 A 的目标是生成数据,尽量让模型 B 分辨不出来这是生成的,而模型 B 则要努力辨别出来,最终会让模型 A 生成的结果特别逼真 —— 类比到人身上的话,有一个水平相当的对手竞争,会让人得到快速的提高
  • ……

这些类比还能写很多,一时半会是写不完的。总之,哪怕不是人工智能行业的从业人员,泛泛地了解一点相关的理论知识也对自我认识和改善会有帮助的。

使用 org-roam 构建自己的知识网络

2020年6月27日 08:00

前言

最近 Roam Research 一类的以网状结构来关联笔记、并以 backlink 的形式来展现笔记上下文的工具非常热门。所谓网状结构,是认为知识和知识是互相关联的,并通过这种互联形成复杂的网络,就像我们的大脑一样;所谓 backlink,是指对单条笔记,展示出链接到这条笔记的其他笔记,这样有助于更好地理解这条笔记的意义。本质上,网状结构和 backlink 其实是一回事,说的都是知识之间的互相链接,不过网状结构着眼于整体结构,而 backlink 则呈现局部形态。

Roam Research 这类工具中的理念,叫做卡片盒笔记法,本文无意对这一想法做过多介绍,如果想进一步了解,可以参考下列文章:

在 Emacs 中,很早就有一个工具 org-brain,想要以 org-mode 为基础让人能建立自己的知识网络,本质上思想是类似的,但交互并不算特别友好,所以我以前稍微用了下就没有继续下去了,而最近出现的 Org-roam 则对标 Roam Research,实现了非常友好的交互,并提供了 org-roam-server 这样非常棒的知识网络可视化界面,经过短时间的使用后,我推荐所有使用 org-mode 来记录自己笔记的人都用一下 org-roam,理由如下:

  1. org-mode 本来就提供了极其强大的链接能力,可以链接到文件、headline 甚至文件的随便一行,也支持了对大量不同类型的外部链接,而 org-roam 为这种能力提供了友好而高效的交互操作
  2. org-roam 复用了 org-capture 的强大功能,使得我们可以自定义各种笔记模板来更好地表示、呈现知识
  3. org-roam-server 提供的笔记网络可视化界面和 org-roam 深度集成,点击界面上的笔记节点就能在 Emacs 中打开对应的笔记

环境说明

  • 操作系统: Ubuntu 16.04
  • Emacs 版本: GNU Emacs 26.1
  • org-mode 版本: 9.3.7
  • org-roam 版本: 开发版 20200615
  • org-roam-server 版本: 开发版 20200621
  • 浏览器: Firefox/Chrome
  • GIF 录制工具: byzanz-record

安装及初步配置

直接从 MELPA 安装即可

(package-install 'org-roam)
(package-install 'org-roam-server)

安装完成后,首先需要设置 org-roam-directory 指向一个目录,用来存放使用 org-roam 创建的笔记。我把笔记都放在 Dropbox 里,所以设置如下

(setq org-roam-directory "~/Dropbox/org/roam")

然后让 org-roam 在 Emacs 启动后就启用

(add-hook 'after-init-hook 'org-roam-mode)

然后设置并启动 org-roam-server 来监听笔记的变化并进行可视化(见org-roam-server 安装说明

(setq org-roam-server-host "127.0.0.1"
      org-roam-server-port 9090
      org-roam-server-export-inline-images t
      org-roam-server-authenticate nil
      org-roam-server-network-label-truncate t
      org-roam-server-network-label-truncate-length 60
      org-roam-server-network-label-wrap-length 20)
(org-roam-server-mode)

上面的配置生效后,会在本地启动一个网页服务,访问 http://127.0.0.1:9090 ,会看到下面这样的界面:

org-roam-server-web.png

由于刚开始并没有创建笔记,上面只会显示一片空白。

然后启用 org-roam-protocol,用来在笔记可视化网页上和 org-roam-server 通信

(require 'org-roam-protocol)

这个 org-roam-protocol 是使用 org-protocol 实现的,依赖操作系统的相关功能,相关设置参考文档

完成上述设置后,就可以开始体验 org-roam 了,执行 M-x org-roam-find-file 创建一条新的笔记,然后刷新笔记可视化页面,就能看到页面上多了一个新的节点了,如下图所示:

org-roam-new.gif

org-roam 的基本使用

首先来看下 org-roam 的基本功能

函数 功能 备注
org-roam-find-file 打开或新建笔记  
org-roam-capture 新建笔记  
org-roam-insert 插入一个指向其他笔记的链接,如果不存在会新建一个笔记  
org-roam-insert-immediate 类似 org-roam-insert,但新建笔记后不打开这个笔记 需要 org-roam 1.2.1
org-roam 显示 backlink  

核心的功能就这么多,没有太多概念、操作要学习,这也是我推荐大家使用它的原因。其基本工作流也很简单,下面是一个示例:

  1. 打开已有笔记,或新建笔记
    • 使用 org-roam-find-file 来新建一个笔记

      org-roam-new.gif

    • 或者,使用 org-roam-find-file 来打开已有的笔记

      org-roam-open-note.gif

    • 或者,在笔记可视化网页上浏览,点击想要查看或编辑的笔记节点,在 Emacs 中打开这个笔记

      org-roam-open-note-2.gif

  2. 选中笔记内容中的某些关键词,使用 org-roam-insert-immediate,创建新的笔记并链接过去,并继续编辑当前的笔记

    org-roam-insert-immediate.gif

    从图上右侧可以看到产生了一个名为 org-mode 的新节点,并和 Emacs 这个节点关联起来了。

  3. 或者,选中笔记内容中的关键词,使用 org-roam-insert,创建新的笔记并链接过去,同时打开新的笔记进行编辑

    org-roam-insert-new.gif

    从图上右侧可以看到产生了一个名为 calc 的新节点,并和 Emacs 这个节点关联起来了。

  4. 或者,用 org-roam-insert-immediate/org-roam-insert 插入一个指向已有笔记的链接

    org-roam-link-to.gif

    上图和步骤 3 一样执行的是 org-roam-insert,但从图上右侧可以看到,只是已有的两个节点之间产生了一条关联,并没有新的节点产生。

  5. 使用 org-roam 展示笔记的 backlinks

    org-roam-show-backlinks.gif

  6. 用 org-roam-capture 在已有笔记中新增内容

    org-roam-append.gif

    org-roam-capture 也可以用于新建笔记,实际上 org-roam-find-file 的逻辑就是:先检查笔记文件是否存在,如果存在就打开,否则就调用 org-roam-capture 来新建笔记。但 org-roam-capture 除了用于新建笔记文件,还可以快捷地在已有笔记中新增内容,且新增内容时可以利用模板来提高效率,比用 org-roam-find-file 打开笔记文件再手工新增会更高效一些。

  7. 重复上述过程

掌握上述工作流后,剩下的事情就是把自己所学到的东西用 org-roam 来进行记录、整理了。

org-roam 进阶

定制笔记模板

org-roam-find-file/org-roam-capture 新建笔记的时候,会要求我们输入笔记标题,假如我们输入的笔记标题是 "org-roam",那么会在新建这个笔记后发现这个笔记只在第一行把我们输入的标题写上去了,别的什么都没有:

#+title: org-roam

在实际使用中,我们可能会有不同的笔记需求,比如说:当我为一个专业术语记录笔记时,我想写下这个术语所属的领域以及它的含义;当我记录一个观点时,我会想写上这个观点是谁提出来的、论据是什么、我自己是支持还是反对;当我读一篇深度学习的论文时,我要记录这篇论文的相关工作、要解决的问题、使用了什么方法、进行了怎么样的实验……

这些需求用 org-roam 是能够满足的,因为 org-roam 通过 org-roam-capture-templates 这个变量提供了定制笔记模板的能力。

具体来说,默认的笔记模板是这样的

'(("d" "default" plain (function org-roam-capture--get-point)
   "%?"
   :file-name "%<%Y%m%d%H%M%S>-${slug}"
   :head "#+title: ${title}\n"
   :unnarrowed t))

每个模板都由 8 个部分组成,我这里以上面的默认模板来进行说明

模板组成 对应默认模板中的内容 描述
key "d" 用来选择模板的快捷键
description "default" 展示用的模板描述
type plain 新增内容的类型
target (function org-roam-capture–get-point) 新增内容的位置, 不可更改
template "%?" 新增内容的模板
file-name :file-name "%<%Y%m%d%H%M%S>-${slug}" 新增笔记文件的文件名模板
head :head "#+title: ${title}\n" 新增笔记的初始化内容,仅新建时生效
properties :unnarrowed t 新增笔记的其他属性

下面对这 8 个模板元素分别说明一下

  • 用来选择模板的 key:

    对应默认模板里的 "d",一个字符的情况下用来直接选择模板,两个字符的情况下用第一个字符表示模板分组、第二个字符用来选择这个分组下的实际模板。

    下面的配置设置了四个模板,其中第二个 ("g" "group") 用来指明一个模板分组,后面的 "ga" 和 "gb" 是这个模板下的子模板。

    (setq org-roam-capture-templates
          '(
            ("d" "default" plain (function org-roam-capture--get-point)
             "%?"
             :file-name "%<%Y%m%d%H%M%S>-${slug}"
             :head "#+title: ${title}\n#+roam_alias:\n\n")
            ("g" "group")
            ("ga" "Group A" plain (function org-roam-capture--get-point)
             "%?"
             :file-name "%<%Y%m%d%H%M%S>-${slug}"
             :head "#+title: ${title}\n#+roam_alias:\n\n")
            ("gb" "Group B" plain (function org-roam-capture--get-point)
             "%?"
             :file-name "%<%Y%m%d%H%M%S>-${slug}"
             :head "#+title: ${title}\n#+roam_alias:\n\n")))
    

    上面的模板生效后,首先在执行 org-roam-find-file 新建笔记时,就会看到一个选择界面,如下图所示:

    org-roam-capture-select-template.gif

    如果输入 d 会直接打开新建笔记的编辑窗口,如果输入 g 会展开分组模板要求我们再输入一次来选择具体的模板,如下图所示:

    org-roam-capture-select-group-template.gif

  • 用来描述模板的 description:这个元素就是起到单纯的描述作用,没有功能上的意义
  • 用来说明新增内容类型的 type:本来有 plain/entry/item/checkitem/table-line 五种取值,但在 org-roam 中作用都是一样的,建议一律使用 plain
  • 用来说明新增内容位置的 target:这一项在 org-roam 中不可更改
  • 设置新增内容模板的 template:

    这个元素是整个模板中的核心,其中的内容可以分为两类:

    • 普通的文本,将会原样出现在新增内容中
    • 以 % 开头的特殊标记,如默认模板中的 "%?",将会在最后根据类型自动扩展成不同的内容

    对于第一类内容没啥可说的,唯一值得一提的是,如果需要模板是多行的文本,需要在模板中用 "\n" 来指明要换行,如模板内容 "第一行\n第二行" 最后就会在笔记中显示为:

    第一行
    第二行
    

    这里说一下以 % 开头的特殊标记,由于这块内容很多,这里只列举一些常用的供读者参考:

    标记 描述
    %<…> 自定义格式的时间戳,如: %<%Y-%m-%d>,会得到 <2018-03-04 日>
    %t 当前日期,展开后的格式固定为 <2018-03-04 日> 这样
    %T 当前日期和时间,展开后的格式固定为 <2018-03-04 日 19:26> 这样
    %u 当前日期,展开后的格式固定为 [2018-03-04 日] 这样
    %U 当前日期和时间,展开后的格式固定为 [2018-03-04 日 19:26] 这样
    %prompt 用 prompt 作为提示要求我们输入并填充在这个模板元素所在的位置
    %? 其他所有特殊标记填充完毕后,光标将停留在这个元素的位置等待我们编辑
  • 指定新增笔记文件名的 file-name:

    org-roam 中新建笔记一般都是以一个新文件的形式来创建的,支持用 file-name 来设置这个新文件的文件名,默认模板中的这块设置为 "%<%Y%m%d%H%M%S>-${slug}",分为两部分

    • %<%Y%m%d%H%M%S> :参考上一节 template 部分的特殊标记
    • ${slug} :将笔记标题文字做处理后得到的文本,这些处理包括大写字母转小写、去除一些特殊字符等

    这块建议使用默认模板就好。

  • 设置新增笔记初始内容的 head:

    这个设置用来在新建笔记文件时设置初始内容,只会执行一次,也就是说之后如果使用 org-roam-capture 新增内容到已有笔记中时,这个设置的内容是不会再写入到文件中的。

    默认模板这块的内容是 "#+title: ${title}\n",只设置了笔记文件的标题,建议改为如下内容:

    :head "#+title: ${title}\n#+roam_alias: \n#+roam_tags: \n"
    

    这样设置后新建笔记文件的初始内容将会是:

    #+title: 示例标题
    #+roam_alias:
    #+roam_tags:
    

    "#+roam_alias" 可以给这条笔记设置别名,这样在其他笔记中引用的时候会更方便;"#+roam_tags" 可以用来为这条笔记添加标签,使得在用 org-roam-find-file 查找已有笔记时能根据 tag 来进行过滤。

  • 设置新增内容其他属性的 properties:

    这些属性用于对新建笔记内容的行为做一些额外的控制,列举几个常用的:

    • :unnarrowed t: org-roam 推荐的设置,表示现实整个笔记文件,如果不加这个设置,用 org-roam-capture 增加内容到已有笔记文件中时,仅会显示当前我们输入的内容,而不会显示这个笔记文件中已有的内容
    • :empty-lines 1: 在新增的笔记内容前后加一个空行,使用 org-roam-capture 增加内容到已有笔记文件中时比较有用

org-roam 的笔记模板是利用 org-capture 实现的,上述模板元素中 file-name 和 head 是 org-roam 在 org-capture 模板的基础上增加的新元素;其他六个部分,target 在 org-roam 中不可更改,key、description、type 和 template 的更详细说明可以参考我之前写的一篇介绍 org-capture 文章中的相关内容,capture 模板的五个部分

为了能更直观地理解模板的工作机制,这里给几个模板的示例:

  • 用于记录专业术语的模板

    (add-to-list 'org-roam-capture-templates
                 '("t" "Term" plain (function org-roam-capture--get-point)
                   "- 领域: %^{术语所属领域}\n- 释义:"
                   :file-name "%<%Y%m%d%H%M%S>-${slug}"
                   :head "#+title: ${title}\n#+roam_alias:\n#+roam_tags: \n\n"
                   :unnarrowed t
                   ))
    

    将上面的配置拷贝到你的 Emacs 配置中,并置于所有 org-roam 相关配置的后面,就可以在你的 org-roam 中使用这个模板,后面的示例模板也是一样,但要注意不同模板的 key 不要有冲突。

    org-roam-new-term.gif

  • 用于记录论文笔记的模板

    (add-to-list 'org-roam-capture-templates
                 '("p" "Paper Note" plain (function org-roam-capture--get-point)
                   "* 相关工作\n\n%?\n* 观点\n\n* 模型和方法\n\n* 实验\n\n* 结论\n"
                   :file-name "%<%Y%m%d%H%M%S>-${slug}"
                   :head "#+title: ${title}\n#+roam_alias:\n#+roam_tags: \n\n"
                   :unnarrowed t
                   ))
    

    org-roam-new-paper-note.gif

另外,org-roam-insert-immediate 不使用 org-roam-capture-templates,而是使用一个专门的 org-roam-capture-immediate-template 来设置新建内容的模板,且只能有一个模板,所以设置这个模板的配置是这样的(以默认配置为例)

(setq org-roam-capture-immediate-template
      '("d" "default" plain (function org-roam-capture--get-point)
        "%?"
        :file-name "%<%Y%m%d%H%M%S>-${slug}"
        :head "#+title: ${title}\n"
        :unnarrowed t))

实现网页内容摘录

这部分内容需要 org-protocol,后续内容是在 org-protocol 已经设置好的基础上展开的,如果 org-protocol 设置存在问题,请查阅文档,或这评论区留言来讨论。

利用 org-protocol 这样的外部程序和 Emacs 进行通信的机制,我们可以使用 javascript 来抓取网页上的信息发送到 Emacs 中,而 org-roam 也支持了这种机制。在 org-roam 中可以通过 org-roam-capture-ref-templates 来设置网页捕获相关的模板,默认的设置是这样的:

(setq org-roam-capture-ref-templates
      '(("r" "ref" plain (function org-roam-capture--get-point)
         ""
         :file-name "${slug}"
         :head "#+title: ${title}\n#+roam_key: ${ref}\n"
         :unnarrowed t)))

可以看到,模板本身和前面的笔记模板是一样的,没有什么特别。但我们可以创建一个小书签,来利用这个模板,抓取网页标题和链接然后新建一个笔记到 org-roam 中,如下图所示:

org-roam-store-link.gif

上图中小书签的内容来自 org-roam 的文档,具体内容为:

javascript:location.href = 'org-protocol://roam-ref?template=r&ref=' + encodeURIComponent(location.href) + '&title=' + encodeURIComponent(document.title)

添加的方法是在浏览器中新增一个书签,书签的名字随意(上图中我设置为了“网页抓取”),书签的 URL 填上上面的 javascript 代码。下图是 Firefox 中创建这样的小书签的示意图:

create-bookmarklet-in-firefox.gif

这个小书签的内容分成几部分:

  • 第一部分说明小书签要访问的地址,这个就是 org-roam-protocol 的通信地址

    javascript:location.href='org-protocol://roam-ref'
    
  • 第二部分指定要使用的笔记模板,从 org-roam-capture-ref-templates 中匹配

    '?template=r'
    
  • 第三部分获取一些网页的信息,并设置到变量中,供模板填充使用

    '&ref=' + encodeURIComponent(location.href) + '&title=' + encodeURIComponent(document.title)
    

    前面的默认模板中,head 部分内容为

    :head "#+title: ${title}\n#+roam_key: ${ref}\n"
    

    需要填充 "title" 和 "ref" 两个变量,小书签中第三部分内容就是获取了当前网页的链接赋值给 "ref" 变量,并获取网页标题文本赋值给 "title" 变量了,这样这个模板就能自动填充好了。

不过这个模板和小书签过于简单,只能记录网页链接,我设计了一个模板和对应的小书签,可以做到进行网页标注、摘录,效果见下图:

org-roam-annotate-web.gif

要达到上图的效果,首先,在 org-roam-capture-ref-templates 中新增一个模板

(add-to-list 'org-roam-capture-ref-templates
             '("a" "Annotation" plain (function org-roam-capture--get-point)
               "%U ${body}\n"
               :file-name "${slug}"
               :head "#+title: ${title}\n#+roam_key: ${ref}\n#+roam_alias:\n"
               :immediate-finish t
               :unnarrowed t))

然后新建一个小书签,内容为

javascript:location.href = 'org-protocol://roam-ref?template=a&ref=' + encodeURIComponent(location.href) + '&title='+encodeURIComponent(document.title) + '&body='+encodeURIComponent(function(){var html = "";var sel = window.getSelection();if (sel.rangeCount) {var container = document.createElement("div");for (var i = 0, len = sel.rangeCount; i < len; ++i) {container.appendChild(sel.getRangeAt(i).cloneContents());}html = container.innerHTML;}var dataDom = document.createElement('div');dataDom.innerHTML = html;['p', 'h1', 'h2', 'h3', 'h4'].forEach(function(tag, idx){dataDom.querySelectorAll(tag).forEach(function(item, index) {var content = item.innerHTML.trim();if (content.length > 0) {item.innerHTML = content + '&#13;&#10;';}});});return dataDom.innerText.trim();}())

我所使用过的微信公众号文章转 RSS 的方法

2020年4月17日 08:00

背景

作为一个 RSS 重度用户,在微广场关闭前,我一直使用微广场来把自己想看的微信公众号、知乎专栏转成 RSS。在微广场关闭后,不仅是我,很多人也意识到了,类似的公开服务最终都可能走向同样的结局。在那之后,我就开始探索自己的方案,尝试不依赖第三方服务来把微信公众号转成 RSS。比较幸运的是,当时我已经接触到了 Huginn使用它来获取一些学术论文资讯,而我加入的一个 Huginn 交流群里,早就有人在做相关的尝试了。基于交流群中朋友分享的方案,我也开始使用 Huginn 来为公众号输出 RSS,在这三年时间里,我前后使用过的方案有下面这些:

  1. 基于搜狗微信搜索的方案
  2. 基于即刻/快知和 RSSHub 的方案
  3. 基于 EFB 和 Telegram 的方案

我将在后文中按顺序讲一下这三种方案。

当然,除了这三种方案外,也有一些门槛相对低的方案,不想折腾的人可能会更感兴趣一些,所以我会在文末也介绍一下这些方法。

Huginn 简介

由于几个方案主要都是使用 Huginn 来实现的,这里简单介绍一下 Huginn。

huginn.png

Huginn 是一个 Ruby 编写的自动化工具,在理念上类似 IFTTT 和天国的 Yahoo! Pipes,即进行事件的监听然后根据预先设定的规则自动化地进行后续操作。

Huginn 的 wiki 上列举了一些典型的使用场景,如:

  1. Never Forget Your Umbrella Again: 下雨提醒
  2. Adding RSS Feeds to Any Site: 为任意网站生成 RSS 输出
  3. Follow stock prices: 监听股票价格

在 Huginn 中,主要有 event 和 agent 两个概念,agent 类似 IFTTT 里的 channel,event 则是 agent 的输出。在 Huginn 中可以将一个 agent 的输出作为另外一个 agent 的输入,由此产生复杂的自动化操作。

huginn_diagram.png

上图来自 Huginn:烧录RSS的神器 一文,该文对 Huginn 的使用做了很详细的介绍。

和 IFTTT 不同的是,Huginn 需要自己部署,很多细节需要自己定制,因此在使用门槛上会高很多。想尝试一下的话,可以参考文档一键免费部署 Huginn 到 PaaS 平台 Heroku 上,轻度使用的话足够了。

本文的目的并不是专门介绍 Huginn,如果读者想了解更多关于 Huginn 的内容,可以自行搜索、阅读文档和相关资料。

基于搜狗微信搜索的方案

搜狗微信搜索是一个开放的网站,同时又能获取指定公众号的最新文章,所以就成了早期的公众号文章转 RSS 的重要工具。

在搜狗微信搜索中,如果知道一个微信公众号的 ID,可以直接拼接出该公众号在搜狗微信搜索上的页面,以“经济学人”的公众号举例,其公众号 ID 是 TheEconomistGroup ,那么其对应的搜狗微信搜索页面就是 https://weixin.sogou.com/weixin?query=TheEconomistGroup ,访问这个页面,可以看到如下内容:

sogou_wexin_search.png

从最下方的“最近文章”处就可以解析出该公众号最新文章的标题和链接,因此基于搜狗微信搜索的方案,通过下面几个步骤来得到公众号的 RSS 输出:

  1. 解析公众号的搜狗微信搜索页面,获取最新文章的标题和链接
  2. 访问文章的链接,解析出文章的全文
  3. 输入包含文章标题、文章链接、文章内容的 RSS

看起来是比较简单的,然而这个方案在实际中却遇到了一个比较大的问题,那就是搜狗的反爬机制。大概是出于保护内容、流量的缘故,搜狗微信搜索虽然能够比较方便地访问到微信公众号文章,但我们通过它获得的文章链接,都不是文章的原始链接:2017 年的时候这个链接是一个有时效性的临时链接,这个链接在不到一天的时间内就会失效而不可访问,所以早期的方案都会在链接没有失效的时候先解析出全文,这样在 RSS 阅读器里直接就能阅读;而现在,这个链接是一个重定向链接,在重定向后才会得到一个临时的文章链接。

我个人没有办法解决链接重定向的问题,所以认为这个方案现在已经无效了,如果有读者认为该问题可以被解决,那么可以参考 Huginn教程:微信公众号 转换成 RSS 一文去尝试一下。

(已失效)基于即刻/快知和 RSSHub 的方案

即刻上线机器人功能后,用户就可以自定义微信公众号机器人来抓取任意公众号并生成一个主题了,而即刻的每个主题,又是存在公开 WEB 链接的,所以就可以用 Huginn 解析一个即刻的公众号主题页面,来获取一个公众号的文章列表更新了。而后来 RSSHub 又能直接输出一个即刻主题的 RSS,所以这个流程又能进一步简化一下,省略掉解析即刻主题页面这一步,直接从 RSSHub 输出的 RSS 中,进一步解析出公众号文章的原始链接并抓取全文了。

在即刻服务可用的时候,我就彻底抛弃了基于搜狗微信搜索的方案,因为基于即刻和 RSSHub 的方案一来可以获取文章的原始链接,二来在整体流程上比前者都更简单。

可惜的是,即刻也因为某些暂不了解的原因而停止服务了,不过依赖第三方服务服务会有这个下场我也是有心理准备的,后来就有了第三个方案。

在说第三个方案之前得说一下,现在有一个非常类似即刻的 APP 叫做快知,可以充当这个方案里即刻的替代品,唯一的差别是没有办法获得文章的原始链接,所以在 Huginn 中就少了一个文章直链获取的 Agent,见下图对比:

huginn_scenarios_comparison.png

目前基于快知的这个方案是可用的,如果想使用这个方案,可以有两种办法:

  1. 安装我的个人项目 zs 然后执行下面的命令来生成一个 Huginn Scenario 文件,然后在 Huginn 中导入

    zs-rss gen-scenario -t kz -n 晚点LatePost -i postlate --kz-topic-id k69QJvO82RKoA -o postlate.json
    

    命令参数解释如下:

    • "-t kz" 表示生成一个基于快知的 Huginn Scenario
    • "-n 晚点LatePost" 将这个 Scenario 命名为“晚点LatePost”,同时也设置了最后输出的 RSS 的标题
    • "-i postlate" 指定这个公众号的微信 ID,会用在最终的 RSS URL 中,比如这里指定的是 postlate,假设 Huginn 服务的域名是 https://myhuginn.com 那么最终的 RSS URL 可能是 https://myhuginn.com/users/1/web_requests/395/wx-postlate.xml —— 这里如果不在乎可维护性的话,不用公众号的微信 ID 也可以,给一个自己喜欢的英文 id 即可
    • "–kz-topic-id k69QJvO82RKoA" 这里指定“晚点LatePost”这个公众号在快知中的主题 ID,可以通过快知搜索到公众号后,分享链接到浏览器获得,比如“晚点LatePost”在快知中对应的主题链接是 https://kz.sync163.com/web/topic/k69QJvO82RKoA ,那么主题 ID 就是尾部那串符号 "k69QJvO82RKoA"
    • "-o postlate.json" 指定输出文件名,随意
  2. 直接下载我准备好的 Huginn Scenario 文件 kz_scenario_template.json ,在 Huginn 中导入后再修改其中的设置

基于快知的方案目前可用,但我并不看好这个方案,按照经验,这种做信息抓取的服务,总是存在风险的。

基于 EFB 和 Telegram 的方案

由于我一直使用 Linux 系统,工作中需要使用微信进行沟通的时候非常不方便,后来了解到 EFB 后就用它来把收到的微信消息转发到 Telegram,而 Telegram 是有 Linux 客户端的,这样我就能在 Linux 系统上查看微信消息了。

EFB 本质上是在服务器上登录网页微信,然后监听网页微信上的消息来做转发,只要不去搞什么自动回复机器人之类的,那么在使用 EFB 的过程中,所有通过 EFB 收到的消息就是微信上好友、关注的公众号主动发过来的消息,通过 EFB 发送的消息也都是以个人身份发出去的,总之是一个正常用户的正常行为,并不会有封号之类的风险。我使用 EFB 已经有两年了,除了偶尔需要重新登录一下,并没有遇到什么大问题。

使用 EFB 后可以在 Telegram 上收到我关注的公众号的更新消息,而 Telegram 的 API 丰富易用,完全可以写点代码把 Telegram 上收到的公众号文章更新收集起来再转成 RSS。于是在即刻停止服务后,我就开始摸索出了一套新的方案,大致流程是这样的:

  1. 写一个脚本,利用 Telegram 的 API,定时获取更新的公众号文章,并保存下来
  2. 在 Huginn 上新建一个 WebhookAgent,它会提供一个对应的 webhooks
  3. 将保存下来的公众号文章发往到 WebhookAgent,然后通过后续的其他 Agent 进行全文解析和 RSS 输出

一个完整的 Scenario 是下面这个样子的:

huginn_efb_scenario.png

这个方案的好处有:

  1. 不依赖除 Telegram 外的任何第三方服务,因此也不用担心反爬、服务停止等各种问题
  2. 能获得极低延时的 RSS 更新

不过相对的,这个方案的门槛也是最高的:

  1. 需要自己部署 EFB 和 Huginn,其中 Huginn 虽然可以免费部署,但 EFB 却不行,所以至少需要一个 VPS
  2. 需要熟悉 Telegram 的 API 并编写代码 —— 不过这块我已经做了,其他人有兴趣可以直接使用我的代码
  3. 需要一个能登录网页微信的个人微信号,而 2017 年 9 月份之后注册的微信号已经无法登录网页微信

这个方案的总体过程是这样的:

  1. 部署 EFB,可参考小众软件的文章:EFB 简明安装教程:用 Telegram 收发微信
  2. 在 Telegram 上新建一个频道,比如“微信.公众号”
  3. 与 EFB 机器人对话,将需要输出 RSS 的公众号链接到刚才建立的频道,如 "/link 晚点LatePost",这样该公众号的消息就会被 EFB 发送到这个频道了

    telegram_linked_channel.png

  4. 创建一个包含 WebhookAgent 的 Huginn Scenario,同样有两种方法

    • 第一种方法是安装我的项目 zs,然后执行下面的命令来生成 Scenario 文件

      zs-rss gen-scenario -n 晚点LatePost -i postlate -o postlate.json
      

      参数含义同之前生成基于快知的 Scenario 时一样

    • 也可以直接下载我项目中提供的 Huginn Scenario 文件 efb_scenario_template.json 然后在 Huginn 中导入

    创建好 Huginn Scenario 后,点击进入“微信公众号 Webhooks”这个 Agent,获取 webhooks 链接,比如:https://myhuginn.com/users/1/web_requests/318/4SXo3X2T2X7HCDjv

  5. 在 VPS 或某台能 24 小时开机的机器上上新建定时任务
    • 首先安装前面反复提到的我的个人项目 zs
    • 然后创建本地数据库,用来存储收集到的微信公众号文章更新

      zs-rss create-db
      

      这条命令会在 $HOME/.zs/data 目录下新建一个数据文件 rss.db

    • 在 $HOME/.zs/config 目录下新建配置文件 rss.json,写入刚才获得的 webhooks 链接,如

      {
          "huginn_webhooks": {
              "default": "https://myhuginn.com/users/1/web_requests/372/d742b76e",
              "晚点LatePost": "https://myhuginn.com/users/1/web_requests/372/4SXo3X2T2X7HCDjv"
          }
      }
      

      这个配置的意思是,公众号“晚点LatePost”的更新发送到一个 webhooks,其他的发送到另外的 webhooks。如果每个公众号都要输出一个 RSS,那么就需要为每个公众号都设置一个 webhooks 链接;如果有多个公众号想要合并输出一个 RSS,那么可以不设置 webhooks 而使用 default 对应的 webhooks。

    • 使用 crontab 新增两条定时任务

      7,17,27,37,47,57 * * * * zs-rss fetch-wx-articles -n 微信.公众号 >> log.txt
      */10 * * * * zs-rss send-wx-articles >> log.txt
      

      具体的时间频率可以自己调整。

      其中第一条定时任务需要 Telegram 的授权认证,具体来说是需要在 $HOME/.zs/config/telegram.json 中有如下内容

      {
          "api_id": "561071",
          "api_hash": "22691769c5decd501fd49d96ecff58e3",
          "session": "AUTHENTICATION SESSION CODE"
      }
      

      其中 "api_id" 和 "api_hash" 可以在 https://my.telegram.org/ 上获取,而 session 的值会在第一次运行时生成并自动写入到上面的配置文件里。

目前 zs 这个项目仅考虑了我自己的需求,所以设计未必很合理,如果有开发能力的话可以参考我的代码自行更改、增加功能。

其他参考方案

如果不想折腾,也有一定的经济来源,可以考虑一些付费服务,比如:

  1. 今天看啥:以前可以免费订阅若干个公众号的,现在必须付费了,价格见今天看啥-RSS订阅方法
  2. WeRss:免费试用三天,试用期间可订阅 8 个公众号,具体价格见WeRss 付费价格

另外,Kindle4rss 上面有不少热门的微信公众号全文 RSS,我大致数了下差不多有 100 个,如下图所示:

kindle4rss_feeds.png

Kindle4rss 的免费用户可以有 12 个订阅,付费价格也不贵,一年 36 人民币就可以订阅 300 个源,比前面两个便宜多了,而且这个服务存在很多年了,可以说一直很稳定,非常推荐。

此外 RSSHub 也有微信公众号支持,见文档。在 RSSHub 里支持了六种方案,分别是:

  1. 基于 WeMP 的方案
  2. 基于传送门的方案
  3. 基于北美生活引擎 CareerEngine 的方案
  4. 基于微信公众号数据分析平台二十次幂的方案
  5. 基于知识管理工具优读的方案
  6. 基于 EFB 和 Telegram 的方案

RSSHub 的前五个方案都是基于一些第三方信息抓取服务的,相对来说非常易用,但仍然存在第三方服务被关停的风险,最后那个方案和我的第三个方案类似,只不过把 Huginn 替换成了 RSSHub。

后记

也许有人会说微信就是个大毒瘤我就不看微信公众号,但确实有一些很好的作者只在微信公众号上写作,还有很多博客时代的知名写作者也迁移到了微信公众号上,原来的博客不再更新甚至不可访问了,加上微信生态的封闭,所以一个微信公众号文章的抓取和开放访问需求始终是存在的,也因此各种第三方抓取服务层出不穷。厌恶微信到完全不想碰微信产品包括微信公众号的人自然是有的,但既然大众的需求存在,我想也没什么好争辩的,每个人都有自己选择的自由。虽说如此,在看到一个好的公众号的时候,也不妨先去了解一下作者是否有同步更新的独立博客或知乎专栏,如果有的话还是更推荐去订阅其博客或知乎专栏等更开放的文章来源。

我制作的微信公众号RSS(2)

2020年4月11日 08:00

上一篇文章一样,都是全文 RSS,罗列如下:

公众号名称 RSS 链接 简介
晚点LatePost https://feedpress.me/wx-postlate 《财经》杂志与小晚团队推出的商业新闻媒体
深响 https://feedpress.me/wx-deep-echo  
看理想 https://feedpress.me/wx-ikanlixiang  
华山感染 https://feedpress.me/wx-hsinfect 张文宏所在的华山医院感染科的公众号
民俗学论坛 https://feedpress.me/wx-folklore-forum 中国民俗学会的公益学术公众号
LCA https://feedpress.me/wx-lca “热爱生活 喜欢文化 关注艺术”
余晟以为 https://feedpress.me/wx-yurii-says  
老顾谈几何 https://feedpress.me/wx-conformalgeometry  
观人看世界 https://feedpress.me/wx-iwatch1024 “传递认知,让你看到我眼中的世界”
笔下求生 https://feedpress.me/wx-tmt-invest “互联网分析师,CFA,在某创业公司扫地”
筹码 https://feedpress.me/wx-chouma2016 “认知是唯一的筹码”
艾格吃饱了 https://feedpress.me/wx-aigechibaole 美食类公众号
碗丸食事 https://feedpress.me/wx-foodfile-111010 美食类公众号

其中 LCA 是网友在上一篇博客的评论中推荐的。

注:若公众号作者看到本文认为我制作的 RSS 侵犯了版权还请告知我,我会关闭全文输出。

一次与公务员朋友的疫情讨论

2020年4月4日 08:00

H 和我是高一同学,人长得帅气、高大,我总觉得和周润发有几分神似。高一时的事情我已经不太有印象了,但高考后复读的那年,我和他在一个复读班里,他做班长,尽心尽力,也帮助过不少人,这些我仍然记得很清楚 —— 总体来说,他是一个很友善、乐于帮助他人的人。去年冬天他去北京出差,我请他吃了顿壹圣元,就着火锅,我给他讲互联网和 Aaron Swartz,他给我讲地方政府的经济困境。毕业多年,我们联系并不太多,但是如果碰在一起,仍是能自在地聊天。

他前些年考了公务员,就在家里工作,疫情发生后,也投身到地方疫情防控的一线工作中,比我这种只会窝在家里对着四面墙壁发脾气的人是做了更多实事的。三月初的时候,他联系我,说之后会调动到我在的乡镇的政府部门工作,周二的时候似乎是安排妥当了,就跟我打了招呼,从乡政府开车十几分钟到了我家。由于今年情况特殊,我们并没有聊工作家庭这些平常会聊的事情,只聊了下疫情。

方才也说了,H 是一个友善的人,所以我并不担心我们观点不一样而争吵,但他毕竟所处的环境不一样,思考模式肯定是会与我不一样的,这点我是有心理准备的,同时我也好奇在 H 是怎么看待这次灾难的。

开始的时候,H 表达了这么一些看法:

  1. 这次疫情是美国政府为了搞乱中国政府而制造的人祸,但因为大意而把自己也拖下水了
  2. 这次疫情对中国来说是一个好机会,可以趁此机会获得更好的世界地位
  3. 中国政府防控有力,在一个月内控制了国内疫情,是值得肯定的
  4. 美国如果能狠下心来强力防控的话,疫情今年内结束是有可能的

H 认同中国政府的工作这点是很好理解的,他作为一个亲身参与到其中的人,对于其中细节的认识是远多于我的;此外湖北省外的其他省份的政府,不管是出于政绩也好还是出于安全也好,也确实做了迅速的反应,积极进行防控,这点从各个省份的确诊人数的变化是能看出来的,我认为是值得肯定的。

至于病毒美国制造说,从外交部的几次发言以及中国政府对网络上的相关言论不加管控来看,中国政府是在有意地散播这种观点的,身为体制内的 H,受到这种信息的冲刷肯定远多于我。当然,我并不认同这一说法,但是有意思的是,我们双方在这个本应严肃的话题上,都拿不出什么有力的论据。

H 提出了这些论据:第一,中国外交部赵立坚都暗示病毒来自美国,肯定是中央政府授意的,而中央政府一定是有足够证据才会这样做;第二,据某篇论文说,美国存在中国没有的一些病毒株,且比在我国发现的更原始。我呢,我其实也给不出什么很明确的反对证据,我认为病毒不一定源自美国的理由有这些:第一、我不相信中国外交部特别是赵立坚说的话;第二、我认为流行病学调查不是短期就能得出结论的,且我知道学术论文往往表述都比较谨慎,不会轻易给出特别明确的结论;第三、我在网上看过较多批驳美国源头说、病毒人造说的言论,并且相信。

我和 H 在这个观点上,都是有一些先行的立场,然后看了一些网络上的言论并选择性地采信了,所以在这个事情上,我们有什么区别呢?我们所自以为的思考,又有多少是周围的环境强加给我们的呢?我想了一下,这段时间里,我们朋友、同事讨论这些事情时,其实有不少时候都是抛出一个报道、截图,然后共同批评、驳斥,鲜少去反思我们所看到的东西,也没几个人去深入了解相关的研究 —— 我们这些人的价值观是近似的,我们所讨论的观点大都是我们认可的观点,我们所相信的也大都是我们选择去相信的,所以我是不是也是在一个信息茧房里呢?

之前有一次和朋友聊中医话题 —— 总体来说我们都对中医持负面态度 —— 稍微聊深一点,就有一个朋友觉得我在钻牛角尖而不想聊下去了,深入的讨论交流在价值观近似的人之间也很难进行下去,就别提观点冲突的人群之间了。万幸我跟 H 一来没有做很深入的讨论,二来我们都不是那种认为对方观点与自己不同就无话可说的人。

在病毒来源这个事情上,我跟 H 勉强达成了几点共识:第一、从外交部观点推测病毒源自美国,是一种主观推测,不管你多相信中国政府,它也只是推测,而不是事实;第二、应当以科学研究为准,不能扭曲学术研究的结论,把相关性当作因果性,在学术界明确病毒来源并达成共识之前,我们都不应该对病毒来源这个问题轻易下结论。

最后,作为一个政府工作人员,H 比较乐于相信一些宏大叙事和事件后面的积极意义,我觉得这无可厚非,但我不喜欢这种东西。我试图脱离他的语境,跟他讨论一线医护人员待遇、李文亮的调查结果、湖北人遭受歧视、失业影响等更小的问题。一方面,我希望他不要总将目光投向高处,而忽视了一个个真实存在的个体之间的差异和不同的境遇;另一方面,我想我也应当向他学习,多一些更宏观的视角和积极的心态,而不是一味沉溺在情绪里。

我不是一个善于待客的人,H 在我家小坐了一会就忙着回去工作了。我觉得这次讨论,虽然没有让我获得更多、更深入的关于疫情的信息,但让我看到了自己思考模式和信息获取上的问题,还算是比较有收获吧,如果他也能从我这获得一些有价值的东西那就更好了。

基于 Github Pages 的博客快速启用 HTTPS

2020年3月27日 08:00

我这个个人博客一直是基于 Github Pages 的,前几年启用了自定义域名,但一直都是 HTTP 访问的,鉴于越来越恶劣的网络环境,启用 HTTPS 这个事情就变得越来越重要了。不过我个人对于 WEB 方面的知识其实很薄弱,再加上懒,就一直没有去做这个事情,今天做完工作,想着处理一下这个问题,没想到简单一搜索就找到了简单的解决方案。

我用 "github pages https" 搜索了一下,结果发现 Github Pages 服务已经有这个支持了。

search-github-pages-https.png

根据 Github 2018 年的这篇博客,只需要在对应项目的设置里勾选 "Enable HTTPS" 即可,如下图所示:

github-pages-https.png

这样设置后,我的博客也可以通过 HTTPS 来访问啦!如果有朋友的博客、网站也是基于 Github Pages 的,那么也推荐用这个方法来启用 HTTPS 哦。

我制作的微信公众号RSS

2020年3月10日 08:00

都是全文 RSS,罗列如下:

公众号名称 RSS 链接 备注
AINLP https://feedpress.me/wx-ainlp  
CatCoder https://feedpress.me/wx-catcoder 微广场作者的个人公众号
LateNews by 小晚 https://feedpress.me/wx-latenews  
L先生说 https://feedpress.me/wx-lxiaoshengmiao  
ResysChina https://feedpress.me/wx-resyschina  
中文信息学报 https://feedpress.me/wx-jcip1986  
六神磊磊读金庸 https://feedpress.me/wx-dujinyong  
利维坦 https://feedpress.me/wx-liweitan  
哈工大SCIR https://feedpress.me/wx-hit-scir  
孟岩 https://feedpress.me/wx-dreamytalks  
安静的书桌 https://feedpress.me/wx-quiet-desk  
MorningRocks https://feedpress.me/wx-morningrocks  
微软研究院AI头条 https://feedpress.me/wx-msrasia  
程序媛的日常 https://feedpress.me/wx-girlswhocode  
视觉求索 https://feedpress.me/wx-thevisionseeker  
语音杂谈 https://feedpress.me/wx-yyzt  
长赢指数投资 https://feedpress.me/wx-chinaetfs  
单读 https://feedpress.me/wx-dandureading  
大象公会 https://feedpress.me/wx-idxgh2013  
呦呦鹿鸣 https://feedpress.me/wx-youyouluming  

读者如果有好的公众号可以推荐给我,我感兴趣的话会去为其制作 RSS。

数字独立与自由

2020年3月7日 08:00

1996年 的时候,电子前线基金会的创始人之一 John Perry Barlow 发表了《数字空间独立宣言》,在宣言中,他说道:

Governments of the Industrial World, you weary giants of flesh and steel, I come from Cyberspace, the new home of Mind. On behalf of the future, I ask you of the past to leave us alone. You are not welcome among us. You have no sovereignty where we gather。

在这份宣言中,John Perry Barlow 宣称数字空间是无国界的,是一个不在所有政府的疆界内的自由的空间,是一个人人可以表达自己的信仰和思想的新世界,并拒绝政府以解决现实世界问题的借口来侵犯这个空间。当时持这种态度的人不少,比如说 RSS 发明人、Markdown 作者之一的 Aaron Swartz,但最终,他死于捍卫网络世界的自由,年仅 26 岁。

从 Aaron Swartz 自杀到现在,我们已经可以清楚的知道,数字空间并非天堂,并没有真正地把人类的心灵都联结起来。跨国界的自由,似乎也没有真正地实现过,各个国家都建起了自己的围墙,并以反恐等各种名义监视着每个数字空间里的个体;与此同时,数字空间里的住民们,也在这种畸形的环境里,展现出了很多丑陋的姿态。

深受以 Aaron Swartz 为代表的早期互联网先驱们影响的我们,该如何面对这样一个世界呢?作为一个技术人员,最好的形式,当然是利用自己的技术能力来让这个世界变得更好一些,然而这个世界的复杂性有很大一部分其实来自于人性的复杂,在行动的同时,我觉得,我们也应当进行更多的思考和讨论,本文就是我对相关问题的一些思考的记录。

大体上,我仍然是抱持数字空间独立与自由理念的,但在当下的大环境里,谈论整个数字空间与现实世界的独立,似乎已经不太现实,我更想谈的是,个人在数字空间中,如何保持独立与自由。如果每个个体,都能对自身在数字空间中的权利与义务有足够的认识,我相信即使技术没有得到革新,这个世界也会有很大的改观。

要谈独立与自由,那就得了解我们在数字空间中有哪些权利和义务。由于各国相关法律也并不统一,数字空间内所有人也并没有形成共识,下面的一些内容,仅仅是参考了一些相关资料后我个人的想法。

首先,最基本的权利是互联网接入权。

2009 年 10 月 14,芬兰交通运输部宣布互联网访问权成为法定权利,从 2010 年 7 月开始,芬兰公民将具有至少 1Mb/s 带宽的互联网访问权利,并计划在 2015 年将这个带宽下限提高到 100Mb/s,这应该是第一个将互联网接入权写入到法律里的案例。在这之后,很多国家以成文或不成文的形式承认这个权利。从现实世界来看,我们想要接入到互联网,似乎是轻而易举的事情,但是这个权利仍然面临如下威胁:贫穷导致一部分人无法支付宽带接入的费用、被滥用的监管导致每个人都可能被断网。一方面我们应当努力去促成合理、完善的法律法规的产生,另外一方面我们也应该在法律法规完善并且能被正确实施前,掌握足够的方法和工具来保障我们的权利。

另外,并不是我们能上网,我们的互联网接入权就得到保障了。重点是,我们应当可以接入世界互联网,而不是一个受限的局域网络;再更进一步,接入网络后,我们还应当具有自由地访问网络上的内容的权利。而这些延伸的权利,同样也受到很多威胁,当我们访问的文章、网站显示 404、被屏蔽或者被删除时,我们应该要意识到,在那一瞬间,我们的权利被剥夺了。

其次,我们应当享有在数字空间上的言论自由权利,可以在网络上自由地表达自己的观点,也可以自由地创作文字、音频、视频等各种形式的作品。这份权利受到的威胁,我想有不少人是深有体会的,比如说发表的言论其他人完全看不到、创作的文章不被允许发表或被删除,有些人甚至因为在网络上的言论而导致现实中的人身权利受到威胁。一个理想的数字空间,当然是大家都可以堂堂正正地发表自己的言论,互相碰撞并求同存异,但在现今这样恶劣的环境里,我们也得考虑通过一些方法和工具来保障我们的言论自由权利,比如说脱离对垄断平台(如微信)的依赖,或在恶劣环境中隐匿自己。

第三,我们应当享有在数字空间中的隐私和安全权,个人身份信息、个人数据都应该被保护,在未经个人允许的前提下,任何其他个人、机构,都不应该访问、利用我们的个人身份信息和个人数据。对于这点,很高兴看到欧盟《一般数据保护条例》(General Data Protection Regulation, GDPR)的通过和施行,但如果我们并非身处 GDPR 作用范围内的地区,那么,在现实世界的法律法规并未完善时,我们还是得时刻谨记自己享有这份权利的正当性,并在这些权利受到侵犯时奋力抗争。

在个体角度上,我并不信任大部分的法律、平台,或者说,出于对上述数字权利的敏感,必须得带着怀疑的目光看待所有可能侵犯这些权利的个体或者机构。为了保障我们的数字权利,我认为我们必须提高自身的认知、实践总结相关的方法并利用好一切能利用的工具。

所谓的提高认知,包括但不限于:

  1. 理解互联网的构成和原理
  2. 了解现实世界与数字权利相关的法律法规
  3. 认识数字空间中信息传播规律、群体构成等社会规律
  4. 建立数字空间中的权利意识和道德观念
  5. 掌握基本的信息搜集和甄别能力

保障我们数字权利的方法和工具,则可能有下面这些:

  1. 数据加密的方法和工具
  2. 突破互联网封锁的方法和工具
  3. 数据备份与恢复的方法和工具
  4. 信息搜集和筛选的方法和工具
  5. ……

我计划在未来的几年内,阅读更多相关的书籍和资料、积极思考,以此提高自己的认知水平,同时实践相关的方法和工具。除此以外,还准备为如 EFF、GNU 这样符合我个人价值观的机构或组织贡献一点个人的力量,比如更积极地参与开源项目、定期捐赠、传播相关理念等。也欢迎志同道合的朋友的交流和补充。

2019 年盘点

2020年1月4日 08:00

各方面的记录

2019 年计划时候,我的一个目标是在 2019 年能多记录一些东西,总体来说我还是做到了,这里简单罗列一下。

睡眠时间统计

数据来自 LifeCycle 的记录。由于我的作息大致上是以周为单位变化的,所以统计的是一周每一天的平均值和中位数,如下图所示。

2019_sleep.png

我的睡眠状况是比较糟糕的,这个统计基本反映了事实。九月和十月有一段时间我其实保持了比较好的睡眠时间,不过入冬后没能维持下去。而九月十月我是如何改善睡眠的呢?大概是做到了以下几点:

  • 给自己设闹钟,晚上到点就把手机放到桌子上,绝不带到床上,然后睡觉
  • 早上也定闹钟,但只是提醒自己到了起床的时间
  • 晚上定时煮粥啥的,早上到点后食物的味道会增强我起来的动力

当然,这些事情什么时候都能做,但那段时间执行得更好一些,原因可能在于那段时间北京天气比较舒服,精神状态也比较放松。

今年我有看到好几篇关于睡眠的论文,这些论文提到,睡眠过程中大脑会进行自我修复,所以说如果长期睡眠不好是会导致各种认知上的问题的。这些论文如下所示:

  • Coupled electrophysiological, hemodynamic, and cerebrospinal fluid oscillations in human sleep
  • Sleep increases chromosome dynamics to enable reduction of accumulating DNA damage in single neurons
  • Zebrafish as an emerging model for studying complex brain disorders
  • Characterization of Sleep in Zebrafish and Insomnia in Hypocretin Receptor Mutants

第一篇是 2019 年 11 月份发表的,相关的研究人员发现睡眠时血液会周期性的、大量地流出大脑,然后脑脊液会大量流入,而脑脊液会清除各种有害的代谢副产物,从而起到修复大脑的作用。

第一篇论文是在宏观层面观察到了睡眠时大脑的运行机制,后面几篇论文则是在微观层面发现了睡眠的一些好处:在对斑马鱼进行实验后,发现睡眠时神经元的 DNA 损伤在减少。

总之睡眠真的是蛮重要的,2020 年的话我希望能改善下自己的睡眠质量。

工作时间统计

数据同样来自 LifeCycle。需要说明的是,LifeCycle 在记录工作时间时,会把我午饭的时间排除在外,也就是说这个工作时间是我的实际工作时间,不是简单用下班时间减去上班时间得到的。

2019_work.png

总的来说,我的每日工作时间约为 9 小时,按照劳动法规定我应该有加班工资的,不过我们是弹性工作制,大家都懂的。

我上半年比较惨,一直在出差,驻场做一些没什么意义的事情;下半年相对好一些了,我的业务性工作也少了很多,主要在做一些基础工具和代码重构的事情,不过就那样吧。

日常开销统计

记账我用的 EasyCost,一个简单的记账 App,每笔都得自己记那种。虽然功能很简单,但我目前没复式记账这种复杂的需求,所以从 2016 年起一直都用的这个,2019 年的话结合且慢小账本进行预算控制,我觉得还 OK 吧。

按月统计了下餐饮、家居和交通三大块的支出,如下图所示:

2019_cost.png

上半年的数据有点奇怪,主要是因为在各种出差(前面有提到)。六月份搬家后不免要购置各种生活用具,导致这个月的数据比较高。在那之后也就是下半年,除了工作日的午餐外,我一直坚持自己做饭,所以每月的餐饮支出略有下降,不过降的幅度和我的直觉还是有点差异,大概是因为自己做饭省了钱后我会更放肆地去进行饭罪活动吧……

总之呢,每月的日常开销要 2000+,房租目前是 4900,每月能领 1500 的公积金,所以一个月的固定开销大概是 5000~6000 左右……

2019 年的时候,每月的日常开销我都是预算制的,也就是说每月发下工资后,给自己留一笔固定数额的钱,剩下的钱全都转走,放到货币基金和各种理财产品里。自我感觉这种形式还蛮好的,预算划出去后我就当这钱不是我的了,花完了的话就去喝西北风,没花完的话就当赚到累积到下一个月,这样换了一个角度去看这笔钱后感觉心态会好很多,支出也更容易控制。目前是每个月给自己 3000 预算,一般都用不完,2020 年的话会尝试降低这个预算,比如到 2500 这个样子。

家居那个分类主要是一些家居用品的购置,交通的话主要是每月的通勤支出,这些就没太多好说的。

除了日常开销外,我在进行预算制的支出还有游戏和网络服务,游戏给了自己一年 2000 的预算,网络服务是一年 3000,到年底的时候都差不多用完了。网络服务主要有这些:

  • RSS 阅读器 Inoreader
  • RSS 烧录工具 FeedPress
  • 两个 Vultr 的 VPS,分别部署了 HuginnEFB:前者用来给不输出 RSS 的网站制作 RSS,比如说微信公众号;后者用来将微信消息转发到 Telegram 上,这样我就能在电脑上回复微信消息了 —— 我是 Linux 用户,没有微信客户端呀。Vultr 有点贵,在想要不要换成别家的 VPS。
  • 若干其他 VPS
  • 某不能说的代理工具
  • 域名

目前这两个预算不变,进入 2020 后我已经重新分配了预算,可以开心地买买买啦,什么健身环啊、丝之歌啊,我全都要~

投资收益

投资的话以前也有做,不过 2019 年的话,了解了更多东西,自己也做了一些尝试。目前的收益状况如下图所示:

2019_investment.png

图上的“现金1”、“现金2”、“现金3” 其实就是流动资金,目前分别放到了三个地方(主要是货币基金),之所以这么做是因为货币基金每日提取额度只有一万,六月份换房子的时候就因为钱在一个货币基金里,提取额度不够交房租,最后跟同事借的钱……这之后就把钱分散到多个地方了。不过现在有些产品也在解决这个问题,做法就是在外部包装成一个产品,然后能让你配置多个不同的货币基金,比如腾讯理财通的“余额+”就是这样的。

网格投资是下半年才开始做的,目前还不会自己挑选投资目标,主要是根据 E 大的一些推荐买了一些,小额投入,尝试熟悉操作方法和相关知识。在掌握了更多知识后,计划 2020 年慎重地加大投入。

基金定投和长赢计划的话,是我的长期投资计划,是会长期持有的,所以就算现在有 10 几个百分点的浮盈我也很淡定,不会想着赚了就赶紧退出,这种心态的变化,是我跟投长赢计划后最有价值的收获 —— 涨跌都舒服,踏踏实实的。

另外,年初我自己鼓捣了个总额 1000 元的组合,瞎加了几个债券进去,没想到现在也有 10 几个百分点的收益,我在想 2020 年是不是也多买点债券呢?

一年下来,对指数基金投资更有信心了,相信 2020 年我会在投资方面有更多的进步。

书影音和游戏

主要是用豆瓣记录的,我挨个去把图片存下来,然后写了点代码把同一个分类下的图片拼接成一张大图,之后完善一下,期望是直接从自己的豆瓣主页上抓取。

2019 年读了 11 本书,主要是以小说特别是科幻小说为主,如下图所示:

2019_books.jpg

小说里面最喜欢《龙蛋》这一本,非常正能量的一本小说,故事的大致情节是:人类发现一颗中子星上的名为奇拉的生物后,这些生物逐步进化的故事,人类和奇拉互通有无,共建和谐宇宙!

然后《华氏451》《水刀子》也挺不错的。

电影和剧的话看了这些:

2019_movies.jpg

《千与千寻》和《天气之子》虽然也是动画,不过是电影院看的,所以归到电影分类里去了。动漫的话看了这些:

2019_animes.jpg

《Rick and Morty》是期待了一年的动画,看了两集,剩下的还攒着没看,所以暂时不做什么评论。

《电脑线圈》是比较老的一部动画了,不过意外的不错,里面涉及了 VR、AR、人工智能、意识等各种放现在也不过时的元素,而且程序员奶奶太厉害了,我也想老了还能写程序!

《约定的梦幻岛》故事讲得太好了,各种转折、反转,看的过程中惊喜不断,看完后我还去追了漫画。不过这种叙事非常巧妙的故事,重复观看的乐趣会衰减很多。相比之下《冰海战记》就是一部非常耐看的动画了,是 2019 年我最喜欢的日本动画。

《灵能百分百 第二季》《动物狂想曲》我也很喜欢,由于后者是校园故事,所以我会喜欢前者多一些。

剩下的《多罗罗》《石纪元》《小书痴的下克上》《刀剑神域 爱丽丝篇》都是优秀但在我看来存在一些问题的动画。

一年下来,能看的新番大概不到 10 部,不知道 2020 年会不会好点。

玩过和在玩的游戏一共有 9 部,如下所示:

2019_games.jpg

Hollow Knight 我吹过一次,就不再吹了,这回强势吹一波黑魂:地图设计实在是太厉害啦!第一次有这个感觉,是在不死镇打完第一个 BOSS 牛头恶魔后,找到回不死镇篝火点捷径的时候,之后在城外不死教区乘电梯回到传火祭祀场的时候也非常震惊。另外游戏剧情非常晦涩,所以也给人强烈地想要搞明白的冲动,我们戏称黑魂的叙事手法是“废墟考古式叙事”,玩家在游戏里就像考古学家一样,只能从游戏里各种物品、对话中得到各种故事的残片,需要自己进行大量的联想、推理才能得到相对完整的剧情,非常有意思。另外,玩多了后,发现 Hollow Knight 从黑魂里借鉴的元素太多了,难怪我都喜欢。

GRIS 和 ORI 在音乐和画面上都当得上艺术品的称呼,不过相对来说,GRIS 有它想要表达的核心思想,而 ORI 的剧情过于简单,我就把它当作一个画面好看音乐好听的平台跳跃游戏,2020 年的续作我就不打算玩了。

其他

这一年里,整个世界发生了很多变化,大部分都是不太好的;身边的小世界也发生了一些变化,难说好坏,毕竟具体到不同的人看法会不一样。总的来说,大部分人对将来社会的走向都持偏悲观的态度,我也不例外。但是除了表示悲观外,我其实还想知道:为什么会发生这些变化?这些变化是怎么发生的?我们能做什么?

对于这些问题,我没有答案,一方面我知道我并没有提出正确的问题,另一方面其实也没有太多可以讨论的人。

带着这些想不明白,八月底去青岛散了散心,倒是想明白了一些事情。一来确认自己是个不适合旅行的人,无论去哪估计只要有个安静人少的角落就行了,什么美食、景点一概没太多兴趣 —— 可能也是因为独自一人的缘故吧,但如果不是一个人的话就压根没法想事情了;再就是在散心的时候想明白了关于“人生意义”的一些小问题,对于将来自己要做什么事情更清楚了一点,对于什么事情是自己想要的或不想要的、在乎的或不在乎的也更明白了。

我很难描述我在青岛时“想明白”的具体是什么,但它给我带来的心态变化是很明显的。下半年我的精神状态一直很平稳,完全没有出现过往年时常会出现的空虚感、无意义感;对于工作、家庭上的一些问题,我的承受能力也更好了一些,因之而来的情绪变化也更加平缓了;此外,我开始强烈地意识到,哪怕是一个很强的团体,想要做成一些事情也是很难的,所以指望靠着其他人的帮助来达到自己的目标,不如多想想自己能做点什么。

从这个“想明白”出发,我找到了一些自己想做的事情,有些在计划表上,有些已经在进行(包括但不限于个人项目、写作),希望 2020 年会有一些成果出来。

外界不需要谁来拯救,能够拯救的唯有还肯接纳外界的自己。新的一年,我想更好地认识、接纳自己,践行自己的想法。

祝所有阅读此文的朋友 2020 年快乐。

更新了“关于”页面

2019年12月8日 08:00

更新了下关于页面,见 http://www.zmonster.me/about.html

关于页面改过多次,后来不知道写什么好就空着了。主要是,不太清楚该怎么介绍自己会比较合适,说太具体自己嫌烦,想抽象一点但发现给自己贴标签也挺困难的,而且觉得人都是复杂的,不太喜欢贴标签 —— 比如说,我虽然是十年的 Emacs 用户,但我也会用 Vim,非要写上的话 Vim 用户看到就跑了怎么办?

今天简单写了写,以后觉得对自己认识更清楚了,或许会改一改。

其实我尝试过写微信公众号,但是现在已经近于废弃了。我并不想说什么微信公众号封闭之类的话,只是单纯觉得这个博客才是我自己的小天地。在这里,我也不用操心谁关注了我谁没关注我,安静地写点到东西,偶尔就一些话题和网友聊一聊,感觉挺好。

博客从开始写到现在已经 6 年多了,有一些早年间认识的网友,我们通过博客还在网络上保持着联系,说不定加个微信聊两句就会变成联系人列表里再也不会戳开的图标了。所以说我还是喜欢博客、论坛这种靠内容激发出来的交流,IM 上的碎片化消息,让我比较有心理负担。

除了关于页面,其实我是有一系列改进博客体验的想法的,希望能让更多优秀的网友留下来,不过一直没空搞就是了,这就 12 月了,那就明年搞吧,哈哈哈!

论文笔记:Few-Shot Learning

2019年12月8日 08:00

相关概念

Few-Shot Learning 泛指从少量标注数据中学习的方法和场景,理想情况下,一个能进行 Few-Shot Learning 的模型,也能快速地应用到新领域上。Few-Shot Learning 是一种思想,并不指代某个具体的算法、模型,所以也并没有一个通用的、万能的模型,能仅仅使用少量的数据,就把一切的机器学习问题都解决掉,讨论 Few-Shot Learning 时,一般会聚焦到某些特定的问题上,比如 Few-Shot Learning 思想应用到分类问题上时就称之为 Few-Shot Classification。

本文涉及的论文都是 Few-Shot Classification 相关的。

在 Few-Shot Classification 里,问题是这样的:

  • 已有一定量的标注数据,数据中包含较多个类别,但每个类别的数据量不多,将这个数据称之为 train set
  • 用 train set 通过某种方法得到一个模型 \(M\)
  • 给定一个新的标注数据,有 \(N\) 个类,每类中有 \(k\) 个样本,称之为 support set,注意 support set 中的类别和 train set 的不存在交叉
  • 要求借助 support set 但不修改模型 M 的参数,使之在给定一个新的输入时,能将其识别为 \(N\) 个类中的一个

上述问题称之为 \(N\)-way \(k\)-shot 问题,\(k\) 一般较小(10 以下)。特别的,当 \(k=1\) 时称之为 one-shot,当 \(k=0\) 时称之为 zero-shot。

本文涉及的论文,都是在相同的框架下来解决这个问题的,具体来说,模型 \(M\) 具有两大块能力

  1. 将一个类别的数据表示为一个向量,作为这个类的 representation
  2. 将一个输入和一个类的 representation 进行比较,判断两者的匹配程度

第 1 点的存在,让其产生的模型能处理任意新的类别,不管你给什么样的 support set,它总能产生 \(N\) 个向量表示这 \(N\) 个类别。

最简单的办法是用 train set 进行表示学习,然后对给定的 suppor set,将每个类中的 k 个样本的向量加和或平均作为类的 representation,待预测的数据也编码成向量和,和这些类的 representation 计算相似或距离就好了。之后可以看到本文中涉及的论文,其方法就是这个简单想法的扩充和改进。

为了尽量和实际使用时接近,Few-Shot Classification 在训练时引入一个叫做 episode 的概念,每个 episode 包含从 train set 中采样出来的一部分数据,以及用这部分数据进行训练的过程:

  • 首先从 train set 的类别集合中随机选取 \(N\) 个类别
  • 然后,对每个类别,从 train set 中采样 \(k\) 个样本,这 \(N\times k\) 个样本,同样称之为 support set,用来模拟实际的 support set
  • 然后,对每个类别,在上一步采样后剩余的样本里,采样 \(N_{q}\) 个样本,这 \(N\times N_{q}\) 个样本,称之为 query set,用来模拟实际使用时的待预测输入
  • 模型 \(M\) 作用于 support set 上,得到 \(N\) 个向量 \(C = \{c_{1}, c_{2}, \ldots, c_{N}\}\)
  • 模型 \(M\) 作用于 query set 上,得到每个样本的向量,并和 \(c_{i}\) 计算距离,选择距离最小的作为预测类别
  • 根据 query set 上样本的预测结果与期望结果,得到损失,然后使用优化算法去调整模型 \(M\)

模型测试时通常会在一个和 train set 类别不交叉的标注数据集上进行,称这个数据集为 test set。测试过程同样以 episode 为基础,一般是采样若干个 episode 计算 query set 的预测精度,然后地多个 episode 的结果平均作为整体结果。

相关数据集

  • Omniglot: 一个手写字符数据集,包含 50 个类共 1623 个样本

    20191208_15571575791863screenshot.png

论文笔记

一、Matching Networks for One Shot Learning1

观点

  • 数据增强和正则化技术能减轻数据很少时的过拟合状况,但是不能算是解决了它。

    所谓的 Few-Shot Learning 也不能算解决啊,也没有什么方法敢说是解决了。

  • 非参数化模型(如最近邻方法)能直接用已有的数据作为先验来对新的数据进行分类,也能快速地适应新的类别,这种优点值得借鉴

    考虑分类这个问题,非参数化模型的问题在于它需要将先验数据存储下来进行比对,当数据量很大时会有效率问题;另外就是现有的一些非参数化模型,往往只能使用一些通用的度量方法,往往会有泛化性问题。Few-Shot Learning 某种程度上来说确实算是借鉴了非参数化模型的思路,用 support set 作为先验知识,然后在度量计算上使用可训练的模块。

  • 论文作者认为,如果实际处理的数据和训练用数据的分布相差较大时,论文中提出的模型可能会没啥效果

模型和方法

模型的大致结构是这样的

screenshot_20191103_22171572790637.png

Given Support Set \(S={(x_{i}, y_{i})}_{i=1}^{k}\) ——由于本文讨论的 One-shot 的,所以 support 里的每个样本的类别也就是 \(y_{i}\) 都是不同的。

任给 \(\hat{x}\),将它预测为 \(y_{i}\) 的概率为 \(P(y_{i}|\hat{x}, S)\),最大概率的预测也就是 \(\mathop{\arg\max}\limits_{y}P(y|\hat{x}, S)\)

Few-Shot Learning 要学习的,就是这个 \(P\)。

如果用一个向量来表示预测输出的话(这也是通常做法),用 one hot 向量来表示每一个 \(y_{i}\) 的话,那么模型就可以写成

\[\hat{y} = \sum_{i=1}^{k}a(\hat{x}, x_{i})y_{i}\qquad (x_{i}, y_{i})\in S\]

这里的 \(a\) 可以是一个 attention 模块,其定义为

\[a(\hat{x}, x_{i})=\frac{e^{c(f(\hat{x}), g(x_{i}))}}{\sum_{j=1}^{k}e^{c(f(\hat{x}), g(x_{j}))}}\]

其中 \(f\) 和 \(g\) 都是一个 embedding 函数,在不同的任务上会不一样,比如在 NLP 任务上可以是一个 word embedding,在图像任务上则可以是一个 CNN 网络层;匹配函数 \(c(\cdot, \cdot)\) 则用来计算两个 embedding 的相关度,它可以是任意一个距离度量方法或相似度量方法,也可以是一个神经网络模块。

在模型上,这篇论文针对 embedding 函数 \(f\) 和 \(g\) 做了一些特殊设计,文中称为 Full Context Embedding,简称 FCE ;而匹配函数 \(c(\cdot, \cdot)\) 则统一使用 cosine 方法。下面则来看看这个 Full Context Embedding。

所谓 Context 指的是 support set \(S\);进一步的,所谓的 Full Context Embedding,意思就是说,在计算 embedding 的时候,也要用到 \(S\),而不是简单将数据输入到一个网络模块中然后得到一个表示向量。这个思想和观点,怎么说呢,熟悉 attention 机制的话应该不会觉得陌生。

\(f\) 的 FCE 版本定义如下:

\[f(\hat{x}, S) = attLSTM(f^{\prime}(\hat{x}), g(S), K)\]

其中 \(f^{\prime}\) 是一个单纯的编码器,可以是任意一种合适的神经网络,其输入只有 \(\hat{x}\),\(g(S)\) 是所有 \(g(x_{i})\) 的简化表示。\(K\) 表示处理步骤数 —— 打个比方,就是hi把 \(f^{\prime}(\hat{x})\) 重复 \(K\) 次作为 LSTM 的 \(K\) 个 timestep 的输入,然后用这 \(K\) 个 timestep 的 hidden state 输出和 \(g(S)\) 做 attention 计算,具体来说,在第 \(k\) 步,有:

\[\hat{h_{k}},c_{k} = LSTM(f^{\prime}(\hat{x}), [h_{k-1},r_{k-1}], c_{k-1})\]

\[h_{k}=\hat{h_{k}} + f^{\prime}(\hat{x})\]

\[r_{k-1}=\sum_{i=1}^{|S|}a(h_{k-1}, g(x_{i}))g(x_{i})\]

\[a(h_{k-1}, g(x_{i}))=softmax(h_{k-1}^{T}g(x_{i}))\]

注意:上面的 \(k\) 与 support set 的大小 \(k\) 无关,仅仅是一个符号表示上的巧合。

除了对 \(f\) 做改进外,\(g\) 也有 FCE 版本,如下所示:

\[g(x_{i}) = \overrightarrow{h}_{i} + \overleftarrow{h}_{i} + g^{\prime}(x_{i})\]

\[\overrightarrow{h}_{i}, \overrightarrow{c}_{i} = LSTM(g^{\prime}(x_{i}), \overrightarrow{h}_{i-1}, \overrightarrow{c}_{i-1})\]

\[\overleftarrow{h}_{i}, \overleftarrow{c}_{i} = LSTM(g^{\prime}(x_{i}), \overleftarrow{h}_{i+1}, \overleftarrow{c}_{i+1})\]

其中 \(g^{\prime}\) 和 \(f^{\prime}\) 类似,剩下的部分也很好理解,相当于把 \(g^{\prime}(x_{0})\ldots g^{\prime}(x_{k})\) 当作序列作为一个 BiLSTM 的输入了。

实验和结论

使用了 Omniglot 数据集、ImageNet 数据集和 Penn Treebank 三个数据集分别去和其他模型做对比实验,大致上,在三个数据集上,都是先划分出类别完全不交叉的训练集和测试集然后进行训练和测试,对测试的细节没有说清楚,我所谓的没说清楚包括:

  • 前面的模型部分都是以 one-shot 为背景来说的,但实验中却有 5-shot 的情况,模型在 5-shot 的时候是怎样的?直接将每个类别的 embedding 平均?
  • Few-Shot Learning 的测试有别于普通的分类问题,一般来说是多次采样出 support set 和 query set 进行测试,然后对多次采样结果进行平均,那么,采样多少次,是否覆盖了测试集中的大部分数据?

在 Omniglot 数据集上实验时,对比用的模型有:

  • PIXELS: 直接用图像的像素值做匹配
  • BASELINE CLASSIFIER: 先在训练集上做训练,由于普通的分类器并不能识别测试集中未见过的类别,所以训练完后测试时,用模型最后一层的输出作为图像的特征向量,然后去进行匹配
  • MANN: 一篇关于 Meta Learning 论文中2提出的模型
  • CONVOLUTIONAL SIAMESE NET3
  • MATCHING NETS: 这篇论文提出的模型

下面是在 Omniglot 数据集上的对比结果:

screenshot_20191118_22501574088651.png

我的疑惑:

  • BASELINE CLASSIFIER 的第三个模式用的 Matching Fn 是 Softmax,是什么意思? softmax 函数不是用来做匹配或者相似计算的呀?
  • MATCHING NETS 的 embedding 部分具体用的什么结构,单层 CNN、多层 CNN 或者别的什么?

其中第三列的 Fine Tune 如果为 Y 则说明在测试时,会在测试集的 support set 上进行微调,然后再在 query set 上预测,下同。

由于 Omniglot 数据集比较简单,所以这里的 MATCHING NETS 没有使用 FCE。

在 ImageNet 上做实验时,由于 ImageNet 太大,不方便实验,所以实际上作者是从 ImageNet 中按每类 600 个样本采样了 100 个类得到了 60000 个样本组成的更小的数据集,作者称之为 miniImageNet。其中 80 个类用来做训练,剩下 20 个类用来做测试。

下面是在 miniImageNet 上的对比结果:

screenshot_20191118_22521574088764.png

我的疑惑:为什么这里就不用 MANN、CONVOLUTIONAL SIAMESE NETS 两个模型来对比了,是不是因为这两个模型的效果都比 MATCHING NETS 好,被打脸了?

除了在 miniImageNet 上做实验外,作者也尝试在完整的 ImageNet 上做实验,但做了一些特殊的设置

  • 从 ImageNet 中随机去除了 118 个类别作为训练集,称之为 randImageNet,测试时在那 118 个类别的数据上进行
  • 从 ImageNet 中去除狗狗相关的所有类别共 118 个,用剩下的作为训练集,称之为 dogsImageNet,测试时在 118 个狗狗类别的数据上进行

对比的模型除了 PIXELS,还有就是两个 INCEPTION 的分类器

  • INCEPTION CLASSIFIER: 在去除 118 个类别后的数据上训练,然后同 BASELINE CLASSIFIER 一样,在测试时只用模型来输出图像的向量表示,得到向量表示后用进行匹配
  • INCEPTION ORACLE: 用完整的 ImageNet 进行训练,测试时同 INCEPTION CLASSIFIER

同时,由于从头训练会比较慢,所以 MATCHING NETS 直接使用训练好的 INCEPTION CLASSIFIER 来计算图像特征,在这个基础上再去训练 MATCHING NETS。

结果如下:

screenshot_20191118_23541574092492.png

注:表中的 \(\neq L_{rand}\) 和 \(\neq L_{dogs}\)表示训练集,,\(L_{rand}\) 和 \(L_{dogs}\) 表示测试集。

从表中可以看到,MATCHING NETS 在 dogsImageNet 上表现是比较差的,对此作者的猜测是训练时只去除狗狗的类别,对数据的分布产生了较大的影响,导致训练集上的数据分布和测试集上的数据分布差异很大,从而表现出了较差的结果。说白了,领域迁移性、泛化性差呗……

最后用 Penn Treebank 做了一个类似语言模型的实验,大致是这样的:

  • 给定 support set,里面有 5 条句子,每个句子都被挖了一个空,并给出了这个空要填的词是什么
  • 给定一个被挖掉一个词的句子,预测要填入的词,是 support set 中哪个被挖掉的词

如下所示:

screenshot_20191119_00051574093146.png

有点像语言模型,有点像阅读理解,但又都不是,奇奇怪怪的。

和 LSTM 语言模型做的对比:LSTM 语言模型能达到 72.8% 的效果;本文的模型,在 one-shot 的时候效果为 32.4%,2-shot 的时候是 36.1%,3-shot 的时候是 38.2% —— 就比瞎猜的 20% 高一点点吧。

个人总结

首先,放到现在来讲,这个 MATCHING NETS 的结构在 NLP 领域是很常见的,就是一个常规的匹配模型啊,不同之处在于使用 FCE 的时候它会用整个 support set 去做 attention,但这也不是什么新奇的东西。当然了这篇论文是 2016 年的,那个时候无论是 attention 机制还是我刚说的匹配模型,可能都还是挺创新的吧。

然后就是论文在一些细节上有没说清楚的,这个我已经在前面用「我的疑惑」这样的字眼指出来了。

二、Prototypical Networks for Few-shot Learning4

观点

  • 虽然 Few-Shot Classification 很难,但人类具备很强的这种能力,甚至在某类事物只见过一次后就能在之后非常精准地辨认出来

    嗯,这些论文都这么说,但也没见有引用哪怕一篇认知科学方面的论文来说明这点 —— 成年人见过一次新事物就能辨认出来我是相信的,但婴儿从出生后到什么阶段才具备这种能力呢?我对这个还是比较好奇的,总之对这种抛出一个看起来大家都认可的但没什么实质内容的观点感到无趣。

  • Vinyals 提出的 Matching Networks 可以视作一个在 embedding 空间中的加权最近邻分类器

    FCE 是 Matching Networks 里比较精髓的一个设计,虽然整个模型要说是一个最近邻分类器也没错,但这么说未免有点简化过头,而且当前这篇论文提出的 Prototypical Networks,也可以说是一个 embedding 空间中的加权最近邻分类器啊……没有营养的话……

  • Vinyals 提出的 episode 的概念让训练结果更加可信、更具有泛化性了

    我不知道在 Vinyals 提出基于 episode 的训练流程之前,做 Few-Shot Learning 的学者们都是怎么做的,但毫无疑问的是这套流程已经在之后成为了一个标准了。

  • 在 Prototypical Networks 中,距离度量的选择至关重要,实验表明选择欧式距离比余弦距离好得多

    后面有一堆推论,不是很能看懂。

模型和方法

一个假设: 每个类别都能在空间中对应到一个类比为「原型」的点上,该类的其他数据的表示以这个点为中心分布。

20191125_22491574693389screenshot.png

如上图所示,模型要做的事情是将 support set 中的数据映射到一个 embedding 空间中,然后对同类数据的 embedding 平均作为原型的 embedding;同时在预测的时候将输入数据也映射到这个 embedding 空间中,计算与各个原型的距离后,选择距离最小的类别作为预测结果。也就是说,这个模型的重点是:

  • 训练一个 encoder
  • 选择合适的距离度量方法

下面是模型的具体说明。

给定一个 support set \(S=\{(x_{1}, y_{1}),\ldots, (x_{N},y_{N})\}\),其中 \(x_{i} \in \mathbb{R}^{D}\) 是一个 \(D\) 维的向量,而 \(y_{i}\in \{1,\ldots, K\}\) 表示 \(K\) 个类别中的一个。特别的,\(S\) 中所有第 \(k\) 类组成的子集记为 \(S_{k}\)。

然后,第 \(k\) 个类别的原型,就可以表示为:

\[c_{k} = \frac{1}{|S_{k}|}\sum\limits_{(x_{i},y_{i})\in S_{k}}f_{\phi}(x_{i})\]

其中 \(f_{\phi}: \mathbb{R}^{D}\rightarrow \mathbb{R}^{M}\) 就是要学习的 encoder。

预测的时候,选定一个距离函数 \(d: \mathbb{R}^{M}\times\mathbb{R}^{M} \rightarrow \left[0,+\infty\right)\),然后对给定的输入 \(x\) 按如下方式进行预测:

\[p_{\phi}(y=k|x)= \frac{exp(-d(f_{\phi}(x), c_{k}))}{\sum_{k^{\prime}}(-d(f_{\phi}(x), c_{k^{\prime}}))}\]

对应的,训练的目标就是最小化损失函数 \(J(\phi) = -\mathop{log}p_{\phi}(y=k|x)\)。

下面是训练过程的伪代码描述:

20191126_21421574775748screenshot.png

实验和结论

注:这篇论文还做了一部分 Zero-Shot Learning 的实验,这个和 Few-Shot Learning 的细节还是有一点区别,我这篇文章专注于 Few-Shot Learning,所以 zero-shot learning 部分的内容暂且忽略,有需要的可以自行阅读论文。

在 Omniglot、miniImageNet 两个数据集上进行了 Few-Shot Learning 的实验。

在 Omniglot 数据集上做实验时,对模型做如下设置:

  1. 使用一个四层的 CNN 作为 \(f_{\phi}\)
  2. 每层由 64 个 3x3 的卷积核组成,并使用 Batch Normalization,激活函数用 ReLU,最后来一个 2x2 的 max-pooling
  3. 使用 Adam 优化算法
  4. 学习率初始化为 \(10^{-3}\),每 2000 个 episodes 裁剪一半

实验结果如下表所示:

20191126_22041574777087screenshot.png

这个实验没有和其他模型做对比 —— 可能是各个算法在这个数据集上的效果都能比较轻松地达到 90% 多吧。

作者从这个实验得到这么两个结论:

  1. 训练和测试时 support set 中的类别数比测试时的多,会带来更好的效果,这个从上面的表可以很清晰的看出来:1-shot 时 60-way 的效果好于 20-way 好于 5-way,5-shot 时也是同样的。
  2. 训练和测试时每类中样本的数量一样会比较好 —— 这我倒看不出来,不知道是不是我理解的不对,原文是这样的

    We found that it is advantageous to match the training-shot with the test-shot.

在 miniImageNet 数据集上做实验时,因为要和 Matching Networks 做对比,所以采用了同样的数据集划分方式。模型设置仍然和前一个实验一样使用四层 CNN。

实验结果如下:

20191126_22171574777857screenshot.png

作者从实验中得到的结论是:

  • 使用欧式距离比余弦距离要好,这个结论不光是对 Prototypical Networks 而言,甚至在 Matching Networks 上也成立;
  • 进行 N-way k-shot 训练时,N 的值越大,总体上看来模型效果会越好,可能是因为训练时类别数多会导致模型能更好地学习到不同类别之间的细微差别,从而提高了泛化性

个人总结

作者在论文中不厌其烦地说自己的方法是 simple method,我觉得确实如此,其模型结构一目了然,非常直白。作者在文中有说到这么一句话:

We hypothesize this is because all of the required non-linearity can be learned within the embedding function.

也就是说,为什么简单的方法也会有效,很大的成分还是因为模型学到了一个很好的表示。那么,如果将 encoder 的部分替换成一个表示学习能力更强的模块,是不是就能做到更好的效果?我觉得是的。

三、Learning to Compare: Relation Network for Few-Shot Learning5

观点

  • 之前的一些工作将工作重心放在学习一个好的数据表示上,然后使用一个已有的度量方法来进行分类,如果距离度量(或相似度量)也通过训练得到,可以得到泛化性更好的模型
  • 一些 meta learning 的方法在用于 Few-Shot Learning 问题时,都需要在新的数据上进行 fine tuning 才能得到较好效果,而本文提出的模型可以无需更新模型参数就能用于新类别数据的预测

    这个观点可以分几方面来看

    1. 我这个模型哪怕不 fine tuning 也能表现不错
    2. 提到的那几个 meta learning 的模型不 fine tuning 效果就不行

    对于第一点,看后面的实验结论即可,不过第二点在这篇论文中就看不到什么有力的证据了。

  • 与本文工作最接近的是 Prototypical Networks 和 siamese networks

    其实我觉得和 Matching Networks 也很像啦。

模型和方法

Relation Network(RN) 模型由 embedding 模块 \(f_{\varphi}\) 和 relation 模块 \(g_{\phi}\) 组成,如下图所示:

20191127_22311574865070screenshot_20191101_11331572579231.png

对比一下这张图和 Matching Networks 的结构图,实在是太像了。

具体来说

  • embedding 模块使用 4 层 CNN,每层 64 个 3x3 的卷积核,使用 Batch Normalization,激活函数用 ReLU —— 基本上和 Matching Networks、Prototypical Networks 是一样的,不过稍有区别,就是这里的 embedding 模块的后两层 CNN 没有加 max pooling,这个是为了适配 relation 模块做的修改
  • relation 模块用来将 support sample 和 query sample 的 feature map 融合起来并计算匹配分数

    因此首先有一个拼接函数 \(C(\cdot, \cdot)\),这个函数将两个 feature map 在 depth 通道上拼接起来。

    拼接起来后再通过一个 \(g(\phi)\) 进行融合并计算分数。

    对一个 N-way 1-shot 的任务,在给一个 query sample \(x_{j}\) 的时候,会得到 N 个分数:

    \[r_{i,j} = g_{\phi}(C(f_{\varphi}(x_{i}), f_{\varphi}(x_{j})))\qquad i=1,2,\ldots, N\]

    如果是 N-way K-shot 任务且 \(k > 1\),则把 support set 中同一类的 embedding 简单加起来再输入到 relation 模块中 —— 这里我有一个疑问,同一类的多个样本的 embedding 加起来后不平均一下吗?

    此处的 \(g_{\phi}\) 是两层 CNN 再加上两层全连接层。

模型的总体结构如下图所示:

20191127_22521574866354screenshot.png

训练时使用 MSE 作为 loss。

完了,模型上可以说也是比较简单直白的。

实验和结论

和前两篇论文一样在 Omniglot 和 miniImageNet 两个数据集上做的实验。

统一的训练设置

  • 使用 Adam 优化算法
  • 初始学习率置为 0.001
  • 每 10 万个 episodes 后对学习率进行裁剪

在 Omniglot 数据集上实验时,有如下额外设置:

  • 训练时的数据采样设置
    • 5-way 1-shot 时,query set 中每个类别有 19 张图片
    • 5-way 5-shot 时,query set 中每个类别有 15 张图片
    • 20-way 1-shot 时,query set 中每个类别有 10 张图片
    • 20-way 5-shot 时,query set 中每个类别有 5 张图片
  • 测试时,从 test set 中采样了 1000 个 episodes,将这 1000 个 episodes 的测试结果平均作为整体测试结果

实验结果如下:

20191128_22571574953069screenshot.png

在 miniImageNet 数据集上实验时,有如下额外设置:

  • 训练时的数据采样设置
    • 5-way 1-shot 时,query set 中每个类别有 15 张图片
    • 5-way 5-shot 时,query set 中每个类别有 10 张图片
  • 测试时,对 600 个 episodes 的结果平均作为整体结果

实验结果如下:

20191128_23021574953320screenshot.png

此外还有 zero shot learning 的实验,略。

个人总结

从结果上来看,Relation Networks 的效果并没有显著好于 Matching Networks 和 Prototypical Networks,在 miniImageNet 上的 5-way 5-shot 设置时甚至还比 Prototypical Networks 差一些。不过我觉得这个思路是没有问题的。

以及,可以看到各个方法都能轻松在 Omniglot 数据集上达到 99% 的准确率,所以这个数据集其实已经没有什么参考意义了……倒是各个模型在 miniImageNet 上都还有很大的提升空间。

四、Induction Networks for Few-Shot Text Classification6

观点

  • 之前的一些工作在计算一个类别的 representation 的时候,只是简单的将这个类的 support sample 的 representation 加起来或平均一下,这样做可能会丢失一些重要的信息

    不对哦,Matching Networks 那里用的 FCE 已经是很接近 attention 的思路了,可不是将 support sample 的 representation 简单加起来或平均的。

  • 一个 Few-Shot Classification 的任务,如果有足够多的类别供训练,那么过拟合的风险会比较低。

    文中说这个的时候举了一个例子:假设一个数据集中有 159 个训练用类别,按 5-way 的设置来训练的话,也就是从 159 个类里选 5 个类用于训练,这样可以有 794747031 种不同的子任务。

    我在想啊,这个想法能不能借鉴到普通的匹配任务上。在如检索式问答一类的匹配任务上,通常是做二分类,也就是选两个样本,判断他们是否属于同一类。而当 \(N > 3\) 且 \(k>2\) 的时候,组合数 \(C^k_{N}\) 总是比 \(C^{2}_{N}\) 大,所以如果借鉴 Few-Shot Learning 里的 episode 的思想的话,就能得到更多的训练机会;其次,原来在匹配的时候,对于负样本之间(可能属于不同的问题簇)是不做区分的,区分一下的话感觉也会有好处。

模型和方法

20191207_16081575706118screenshot.png

上图是一个 3-way 2-shot 的模型结构示例图。

模型由 Encoder 模块、Induction 模块和 Relation 模块组成。 Encoder 模块是双向 LSTM,加上 self attention —— 给定输入文本 \(x=\left(w_{1}, w_{2}, \ldots, w_{T}\right)\),最终得到一个向量 \[e = \sum_{t=1}^{T}a_{t}\cdot h_{t}\]

其中 \(h_{t}\) 是一个 biLSTM 的 hidden state:

\[\overrightarrow{h_{t}} = \overrightarrow{LSTM}(w_{t}, h_{t-1})\]

\[\overleftarrow{h_{t}} = \overleftarrow{LSTM}(w_{t}, h_{t+1})\]

\[h_{t} = concatenate(\overrightarrow{h_{t}}, \overleftarrow{h_{t}})\]

而 \(a_{t}\) 是 attention 权重,所有 \(a_{t}\) 组成的向量 \(a\) 是这样计算出来的:

\[a = softmax(W_{a2} tanh(W_{a1} H))\]

其中 \(W_{a1} \in R^{d_{a}\times 2u}\),\(W_{a2}\in R^{d_{a}}\),\(H = \left[h_{1} \ldots h_{T}\right]\) 表示由所有 \(h_{t}\) 组成的矩阵, \(2u\) 是 \(h_{t}\) 的向量长度,\(d_{a}\) 是超参数。

Induction 模块要做的是将 \(C\times K\) 个 support sample 的向量表示 \(e\) 转换成 \(C\) 个类的向量表示 —— 之前的一些工作在这里大都是通过将一个类中的 support sample 的向量表示平均或相加得到。这篇论文创新的地方是,在 Induction 模块这里,用了胶囊网络里的动态路由机制7

胶囊网络我不是太懂,暂时也没太多兴趣和精力去看 Hinton 那篇论文。论文中列出了 Induction 模块的计算过程,还是比较清楚的,如下图所示:

20191208_16101575792611screenshot.png

可以理解为在计算类的向量表示的时候,对这个类中的 support sample 做了加权,然后不同的 support sample 的权重是靠这个动态路由机制来计算的,其中所需的一些参数(如 \(W_{s}\) 和 \(b_{s}\))也是在训练中得到的。

最后的 Relation 模块,用来比对 query sample 也就是待预测的样本和每个类的向量表示 \(c_{i}\),以判断这个 query sample 应该被分到哪个类上。

Relation 模块首先用一个张量网络8来建模两个向量(也就是 query sample 的向量和类向量 \(c_{i}\))的关系,然后再经过一个全连接层输出匹配分数:

\[v(c_{i}, e^{q}) = f(c_{i}^{T}M^{\left[1:h\right]}e^{q})\]

\[r_{iq} = sigmoid(W_{r}v(c_{i}, e^{q}) + b_{r})\]

训练时的目标函数则为:

\[L(S, Q) = \sum\limits_{i=1}^{C}\sum\limits_{q=1}^{n}(r_{iq} - \mathbf{1}(y_{q} == i))^{2}\]

训练时候使用的优化算法是 Adagrad9

实验和结论

在两个数据集上做了实验,一个是 Amazon Review Sentiment Classification 数据集(简称为 ARSC 数据集),另外一个是阿里巴巴整理出来的 Open Domain Intent Classification for Dialog System 数据集(简称为 ODIC 数据集)。

两个实验都和这些模型进行对比:Matching Networks, Prototypical Networks, Relation Networks, Graph Network10, SNAIL11, ROUBUSTTC-FSL12

ARSC 数据集是在不同的电商产品类型上的评论数据,每个数据会被标记为正向、负向和不确定三个类别,总共有 23 个产品类型,所以加起来有 69 个类别。在该数据上实验时,设置如下:

  • 将四个产品的 12 个类别作为 test set,剩下产品的 57 个类别作为 train set
  • 使用 300 维的 Glove embedding
  • LSTM 的 hidden state size 设置为 128
  • 计算 self attention 时的超参 \(d_{a}\) 设置为 64
  • 训练时 support set 为 2-way 5-shot

实验结果如下:

20191207_20121575720750screenshot.png

ODIC 数据集有 216 个类别,将其中 159 个类别共 195775 个样本作为 train set,剩下 57 个类别共 2279 个样本作为 test set。在该数据集上实验时,设置如下:

  • 使用 300 维的中文 word embedding
  • LSTM 的 hidden state size 和 self attention 的超参同 ARSC 数据集
  • 分别进行了 5-way 5-shot、5-way 10-shot、10-way 5-shot 和 10-way 10-shot 四种实验
  • 测试时,在 test set 上采样 600 个 episode 进行测试,然后将平均测试结果作为整体结果

在 ODIC 数据集上的实验结果如下所示:

20191207_20121575720767screenshot.png

除了两个对比实验外,论文还做了一些模型的分析。

第一个分析是尝试将 Induction Networks 中的部分模块替换为更简单的模块,看模型的最终效果变化,来衡量这个模块是否有效。结果如下:

20191207_20131575720787screenshot.png

上图中前三行是 Induction Networks 自己的结果,后面三行就是替换其中部分模块或的结果,其中:

  • 第四行表示将 Relation 模块替换为简单的余弦相似方法
  • 第五行表示将 Induction 模块替换为将类中向量加和作为 \(c_{i}\) 的方法
  • 最后一行表示用 Attention 替代 Induction 模块 —— 文中说是 self attention,应该是指在计算一个类内 support sample 的权重时,对该类内所有的 support sample 进行 attention 计算来得到权重

可以看到,直接使用 Attention 替代 Induction 模块,比 Induction 模块不做迭代要好一点点,但三步迭代后本文模型还是会好一些。

然后还有一些可视化的分析,都是在论证 Induction Networks 比其他模型要好,这里不一一列举了。

个人总结

论文声称的「其他方法都是将 support sample 的向量表示简单加起来或平均作为」其实并不是事实,早在 2016 年的时候提出 Matching Networks 的论文中所使用的 Full Context Embedding 即 FCE 的目标和这篇论文里的 Induction 模块就是一样的,无非是方法不同而已。

借鉴胶囊网络思路的那个 Induction 模块还有点意思。

ODIC 数据集好像没有开放出来,遗憾。

五、A Closer Look at Few-Shot Classification13

这篇 ICLR 2019 的论文旨在分析现有 Few-Shot Classification 方法的一些问题。

观点

  • Few-Shot Classification 虽然在最近取得了不少进展,但是各个方法由于复杂的模型设计和不同的实现细节,导致他们很难互相比较
  • Few-Shot Classification 所使用的 baseline 方法效果被显著地低估了
  • 目前的 Few-Shot Classification 模型评估,都是从同样的数据集中采样出来进行的,导致模型的领域迁移性较低

    我理解之所以这么说,是因为 Few-Shot Learning 本来宣称的一个目标就是能应付新的类别的数据,而如果这个所谓的「新的类别」必须是在同一个领域内的,那么其实会很受限制。

    这点在 Matching Networks 那篇论文里也有提到。

模型和方法

这篇论文的目标不是提出一个新的模型来在 Few-Shot Classification 问题上得到更好的效果,所以在模型部分,只是对一个简单的 baseline 模型做了增强得到了 baseline++ 模型,用来在之后和其他模型对比,以论证前面的「baseline 方法效果被显著低估了」这个观点。

20191208_10521575773575screenshot.png

如上图所示,baseline 模型由一个 feature extractor 或者说 encoder 和一个分类模块组成,作者之所以认为之前的一些工作里 baseline 模型效果被低估了,是因为之前的一些论文,在分类模块这里,用的是一个固定的、简单的距离函数(如余弦距离、欧式距离)。

作者在这里做了一些改动,包括:

  • baseline 模型的分类模块使用一个全连接层,训练好后,用于新类别数据时,会用新的 support set 进行 fine tuning (之前一些工作里是不做的)
  • baseline++ 在 baseline 模型的基础上对全连接层的计算做了简单的调整

    记权重矩阵 \(\mathbf{W} = [\mathbf{w}_{1}, \mathbf{w}_{2}, \ldots, \mathbf{w}_{c}]\),最终分类模块会输出一个长度为 \(c\) 的向量,表示预测为各个类别的概率,记为 \(\mathbf{s} = [s_{1}, s_{2}, \ldots, s_{c}]\)。

    在 baseline 模型里,是这么计算的:\(\mathbf{s} = softmax(\mathbf{W}^{T}f_{\theta}(x))\)

    而在 baseline 模型里,则是这么计算的:\(s_{i} = \frac{f_{\theta}(x)^{T}\mathbf{w}_{i}}{\parallel f_{\theta}(x) \parallel\parallel \mathbf{w}_{i} \parallel}\)

当然,作者在这里强调,这个 baseline++ 模型并不是他们的贡献,而是来自于 2018 年的一篇论文14

实验和结论

作者对比了 Matching Networks、Prototypical Networks、Relation Networks 和一个 meta learning 的方法 MAML 模型。

20191208_11271575775643screenshot.png

这个对比图还是挺直观的,对于理解不同模型之间的差异挺有帮助。

图上的 meta-training 和 meta-testing,其实就是指训练和测试。但是 Few-Shot Classification 的训练用数据和实际使用是要预测的类别原则上是不一样的,学到的更多的是区分不同类别的能力而不是区分某个指定类别的能力,这个还有监督分类问题是不太一样的,这里加个 meta 是和普通的分类模型的训练、测试进行区分,不用太在意。b

进行了三个实验:

  • 使用 miniImageNet 数据集进行一般的图像分类实验
  • 使用 CUB-200-2011 数据集(后面简称为 CUB 数据集)进行细粒度图像分类实验
  • 使用 miniImageNet 数据集训练,在 CUB 数据集上进行验证和测试,对比各个模型的跨领域适应能力

实验的通用设置为:

  • 分别进行 5-way 1-shot 和 5-way 5-shot 实验,训练时的 query set 每类 16 个样本
  • 对 1-shot 实验,训练 60000 个 episodes;对 5-shot 实验,训练 40000 个 episodes
  • 测试时,将 600 个 episodes 上测平均测试结果作为整体结果
  • baseline 模型和 baseline++ 模型训练时设置 batch size 为 16,训练 400 个 epochs
  • baseline 模型和 baseline++ 模型,在测试时,让 encoder 参数不变,用 support set 对分类模块进行 fine tuning,batch size 设置为 4 训练 100 次
  • 所有模型训练时都使用 Adam 优化算法,初始学习率设置为 0.001

由于作者自己重新实现了用于对比的几个模型,所以先和原作者论文中报告的结果做了对比,如下图所示:

20191208_11571575777421screenshot.png

可以看到作者实现的这几个模型和它们的原实现的效果相比,大部分是稍差一些,也有些表现更好的。

在 CUB 和 miniImageNet 两个数据集上做实验时,使用四层 CNN 作为 encoder,实验结果如下图所示:

20191208_11591575777591screenshot.png

可以看到,baseline++ 模型和这些 SOTA 的模型相比并没有很大的差距,甚至在一些情况下比 SOTA 的模型还要好。baseline++ 模型仅仅是对 baseline 模型的一个简单修改,所以作者说 baseline 模型的能力被显著地低估了。

接着,作者认为 encoder 是很重要的,所以尝试了将 encoder 替换成更深的网络,结果显示使用更深的网络后大部分模型的效果都有了显著的提升。

20191208_12071575778024screenshot.png

第三个实验 —— 也就是那个先在 miniImageNet 数据集上训练然后在 CUB 数据集上测试的实验,统一使用更深的 ResNet-18 作为 encoder,结果显示 baseline 模型的效果最好。

20191208_14091575785358screenshot.png

由于 CUB 数据集都是鸟类的数据,所以训练与测试时的领域差异性很小;miniImageNet 数据集包含了很多不同类别的物体,所以领域差异性比 CUB 要大;当训练使用 miniImageNet 数据集而测试时使用 CUB 时,领域差异性是最大的。同样使用 ResNet-18 作为 encoder 时,三个不同的 5-way 5-shot 实验对比结果也说明了目前这些 Few-Shot Classification 模型在领域迁移上的问题,如下图所示:

20191208_14131575785609screenshot.png

而之所以 baseline 模型会最好,有可能是因为 baseline 模型进行了 fine tuning,所以作者再进一步实验,在其他模型上也加上 fine tuning 操作,结果显示领域差异性大时,fine tuning 是很有必要的,如下图所示:

20191208_14151575785741screenshot.png

总结

Few-Shot Learning 这个概念最早是李飞飞提出来的15,不过早先的一些工作方法都比较复杂,除了上述我看的一些论文外,还有一些从 meta learning 的方向来做的。目前看来,Few-Shot Learning 特别是 Few-Shot Classification 的方法,主要都是在 2016 年 Matching Networks 提出的框架下使用越来越复杂的模型,比如还有一篇我没有通读的微软的论文16的做法就是使用复杂的 attention 模型,我相信 ELMo、BERT 等更强大的预训练模型也会逐步用到这个领域里。

回到这几篇论文,可以看到 Few-Shot Learning 应用到分类问题上时,能取得一定的成果,但也还是有一些问题或者限制的

  1. train set 中需要有足够多的类别,虽然每类的数据可以不多 —— 一定要认识清楚这点,不要以为 Few-Shot Learning 就真的只需要很少很少的数据就够了
  2. 领域迁移能力不够好 —— 当然这是目前几乎所有模型的问题,但 Few-Shot Learning 本来就想要去解决新类别的学习问题,希望未来能看到在这方面更多的一些讨论吧

我个人对于数据稀缺时该如何训练模型这个话题是很感兴趣的,除了 Few-Shot Learning,目前了解到的一些方法还有:数据增强、远程监督、多任务学习。从这几篇论文来看,表示学习也是很重要的一环,一个表示能力很强的预训练模型,也会很有帮助。

脚注:

1

Vinyals, Oriol, Charles Blundell, Timothy Lillicrap, Koray Kavukcuoglu, and Daan Wierstra. “Matching Networks for One Shot Learning.” ArXiv:1606.04080 {Cs, Stat}, June 13, 2016. http://arxiv.org/abs/1606.04080.

2

Santoro, Adam, et al. "Meta-learning with memory-augmented neural networks." International conference on machine learning. 2016.

3

Koch, Gregory, Richard Zemel, and Ruslan Salakhutdinov. "Siamese Neural Networks for One-Shot Image Recognition." In ICML Deep Learning Workshop, Vol. 2, 2015.

4

Snell, Jake, Kevin Swersky, and Richard S. Zemel. "Prototypical Networks for Few-shot Learning." ArXiv:1703.05175 {Cs, Stat}, March 15, 2017. http://arxiv.org/abs/1703.05175.

5

Sung, Flood, Yongxin Yang, Li Zhang, Tao Xiang, Philip H. S. Torr, and Timothy M. Hospedales. "Learning to Compare: Relation Network for Few-Shot Learning." ArXiv:1711.06025 {Cs}, March 27, 2018. http://arxiv.org/abs/1711.06025.

6

Geng, Ruiying, Binhua Li, Yongbin Li, Xiaodan Zhu, Ping Jian, and Jian Sun. "Induction Networks for Few-Shot Text Classification." ArXiv:1902.10482 {Cs}, September 29, 2019. http://arxiv.org/abs/1902.10482.

7

Sabour, Sara, Nicholas Frosst, and Geoffrey E. Hinton. "Dynamic Routing between Capsules." In Advances in Neural Information Processing Systems, 3856–3866, 2017.

8

Socher, Richard, Danqi Chen, Christopher D. Manning, and Andrew Ng. "Reasoning with Neural Tensor Networks for Knowledge Base Completion." In Advances in Neural Information Processing Systems, 926–934, 2013.

9

Duchi, John, Elad Hazan, and Yoram Singer. "Adaptive Subgradient Methods for Online Learning and Stochastic Optimization." Journal of Machine Learning Research 12, no. Jul (2011): 2121–2159.

10

Garcia, Victor, and Joan Bruna. "Few-Shot Learning with Graph Neural Networks." ArXiv:1711.04043 {Cs, Stat}, February 20, 2018. http://arxiv.org/abs/1711.04043.

11

Mishra, Nikhil, Mostafa Rohaninejad, Xi Chen, and Pieter Abbeel. "A Simple Neural Attentive Meta-Learner." ArXiv Preprint ArXiv:1707.03141, 2017.

12

Yu, Mo, Xiaoxiao Guo, Jinfeng Yi, Shiyu Chang, Saloni Potdar, Yu Cheng, Gerald Tesauro, Haoyu Wang, and Bowen Zhou. "Diverse Few-Shot Text Classification with Multiple Metrics." ArXiv:1805.07513 {Cs}, May 19, 2018. http://arxiv.org/abs/1805.07513.

13

Chen, Wei-Yu, Yen-Cheng Liu, Zsolt Kira, Yu-Chiang Frank Wang, and Jia-Bin Huang. "A Closer Look at Few-Shot Classification." ArXiv:1904.04232 {Cs}, April 8, 2019. http://arxiv.org/abs/1904.04232.

14

Gidaris, Spyros, and Nikos Komodakis. "Dynamic Few-Shot Visual Learning without Forgetting." In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition, 4367–4375, 2018.

15

Fei-Fei, Li, Rob Fergus, and Peitro Perona. "One-Shot Learning of Object Categories," 2006.

16

Sun, Shengli, Qingfeng Sun, Kevin Zhou, and Tengchao Lv. “Hierarchical Attention Prototypical Networks for Few-Shot Text Classification.” In Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing and the 9th International Joint Conference on Natural Language Processing (EMNLP-IJCNLP), 476–485, 2019.

NLP哪里跑: 文本分类工具一览

2019年10月20日 08:00

本文是《NLP 哪里跑》系列的第四篇文章,系列文章如下:

  1. NLP哪里跑: 开篇及一些碎碎念 · ZMonster's Blog
  2. NLP哪里跑: 什么是自然语言处理 · ZMonster's Blog
  3. NLP哪里跑: Unicode相关的一些小知识和工具 · ZMonster's Blog
  4. NLP哪里跑: 文本分类工具一览 · ZMonster's Blog

关于文本分类

所谓的文本分类,其实就是机器学习中分类问题在 NLP 领域中的应用,它的理论相对简单、工具成熟、上手简单,大家基本上是把它当作一个「理论上已经解决」的问题来看待,但其实在实际场景中处理文本分类任务时还是会遇到不少问题的,加上文本分类又是 NLP 领域中最常见的任务之一,我想把自己在这方面的一些经验和学习成果慢慢地整理出来。

2017 年的时候,为了提高做文本分类任务的效率,将 sklearn 中的文本分类功能做了一些封装,后来断断续续地优化,产出了一个我自己用起来很顺手的文本分类工具。在我开始写《NLP 哪里跑》这个系列的博客后,我计划是把自己在 NLP 方面的经验进行系统地梳理,第一块就是文本分类 —— 这一块当然有很多很多想讲的东西和想做的事情,一篇文章是写不完的,所以最初的想法是先看一下工具方面的情况。我对我在公司维护的文本分类工具还是挺满意的,但也会想自己会不会对一些先进的工具认识不够,所以就去了解了很多其他的同类工具,本篇文章就是对这些工具的一个简单的罗列,不涉及分类模型的理论,也不涉及某个分类模型的具体实现的优劣评价,仅仅是一个非常工具向、实用向的整理记录。

我在挑选文本分类工具时是有一些标准的,不是非常严格,但大概能分成以下几点:

  • 工程化程度良好的,能提供易用的编程接口或命令行接口
  • 以 Python 生态内的工具为主 —— 很多其他语言实现的同类工具,限于精力就没有了解了

后面的内容会分成两块:第一部分讲我的踩坑经历,主要是一些本来以为会好用的工具结果发现不符合我标准的情况;第二部分是我实验之后确认可用的工具和它们的使用方法。

踩坑列表

本节中列举的工具,建议读者不要浪费时间在上面。

腾讯的 NeuralClassifier

注:下文仅代表个人观点和感受,不服不管。

三个月前腾讯开源的,见:【开源公告】NeuralNLP-NeuralClassifier - 深度学习文本分类工具 - 云+社区 - 腾讯云

项目地址:https://github.com/Tencent/NeuralNLP-NeuralClassifier

在我的列表里最坑的一个:

  • 作为一个 Python 项目,没有发布到 pypi 就算了,连 setup.py 也没有,无法安装,项目组织也没眼看,只能像个原始人一样拷贝代码到自己的目录跑跑脚本,很难想象是一个大厂的项目
  • 训练需要使用一个格式复杂的 json 格式的配置文件,然后这个配置的文档太过简略,不少细节藏在项目提供的脚本里……

最终我魔改了一通后跑起来了,但是已经恶心到我了,弃。

无人维护的 keras-text

项目地址:https://github.com/raghakot/keras-text

这个项目我觉得蛮可惜的,从文档和实际使用来看,作者在代码结构和使用流程上是做了一些用心的设计的,但是有些关键模块没有完成,在很多细节上存在令人难以忍受的小问题。

项目已经两年没有更新了,可以参考它的代码,但不建议作为一个正经的工具在实际任务中使用。

差强人意的 text-classification-keras

项目地址:https://github.com/jfilter/text-classification-keras

该项目是对 keras-text 项目的改进,总体上来说是一个可用的项目,但存在以下问题:

  • 使用文档不齐全
  • 代码上仍然有一些致命伤,比如
    • 将文本转成特征向量后,先存了一个文件,然后从文件中加载后再喂给模型……意义不明……
    • 每次调用的时候都要把 spaCy 的模型重新加载一遍,慢得要死

作者应该是从 keras-text 项目 fork 过来然后改成能用的状态的,也挺不容易的,但不管怎么说在我这里是一个不合格的工具。

可用的文本分类工具及其使用方法

使用 NLTK 进行文本分类

安装: pip install nltk

文档: https://www.nltk.org/api/nltk.classify.html

NLTK 提供了大量的文本处理方法,同时提供了通用的分类器接口,组合起来就能进行文本分类了。

以其中的朴素贝叶斯 NaiveBayesClassifier 为例,可以这样来进行文本分类

  • 实现一个方法,将文本转成 dict 形式的特征

    以英文为例,可以直接用 NLTK 中的分词方法,需要的话还可以加上 stemming 或者 lemmatization

    from nltk.corpus import stopwords
    from nltk.tokenize import word_tokenize
    
    
    def extract_feature(text):
        feature = {}
        for word in word_tokenize(text):
            if word not in stopwords:
                feature[word] = 1
    
        return feature
    

    中文的话可以换成 jieba 或者其他中文分词工具。

  • 训练

    from nltk.classify import NaiveBayesClassifier
    
    
    train_texts = [
        # ...
    ]
    train_labels = [
        # ...
    ]
    
    train_features = [extract_feature(text) for text in train_texts]
    train_samples = list(zip(train_features, train_labels))
    classifier = NaiveBayesClassifier.train(train_samples)
    
  • 评估

    from nltk.classify import accuracy
    
    test_texts = [
        # ...
    ]
    test_labels = [
        # ...
    ]
    
    test_features = [extract_feature(text) for text in test_texts]
    test_samples = list(zip(test_features, test_labels))
    acc = accuracy(classifier, test_samples)
    
  • 预测

    用 classify 方法直接预测最可能的类别

    text = "blablabla"              # 待预测的文本
    feature = extract_feature(text)
    pred_label = classifier.classify(feature)
    

    用 prob_classify 方法获得所有可能类别的预测分数

    text = "blablabla"              # 待预测的文本
    feature = extract_feature(text)
    prob = classifier.prob_classify(feature)
    
  • 模型保存和读取

    可以直接用 pickle 保存、读取训练好的模型

    保存:

    import pickle
    
    with open('model.pkl', 'wb') as f:
        pickle.dump(classifier, f)
    

    读取:

    import pickle
    
    classifier = None
    with open('model.pkl', 'rb') as f:
        classifier = pickle.load(f)
    

NLTK 中还有其他分类器,使用方法和 NaiveBayesClassifier 大同小异。

使用 TextBlob 进行文本分类

注意:TextBlob 仅支持英文

安装: pip install textblob

文档: https://textblob.readthedocs.io/en/dev/classifiers.html

TextBlob 是一个基于 NLTK 的文本处理工具,其中的文本分类功能也是建立在 NLTK 中分类器的基础上的。

  • 训练

    from textblob.classifiers import NaiveBayesClassifier
    
    train_texts = [
        # ...
    ]
    train_labels = [
        # ...
    ]
    train_samples = list(zip(train_texts, train_labels))
    classifier = NaiveBayesClassifier(train_samples)
    
  • 评估

    test_texts = [
        # ...
    ]
    test_labels = [
        # ...
    ]
    test_samples = list(zip(test_texts, test_labels))
    acc = classifier.accuracy(test_samples)
    
  • 预测

    只有一个 classify 接口预测得到最有可能的类别

    label = classifier.classify("this is a sentence to be classified")
    
  • 模型保存和读取

    同 NLTK。

相比 NLTK 中原来的文本分类,TextBlob 的封装隐藏了一些细节,简化了接口,用起来还是挺方便的。不好的一点是,TextBlob 里强制依赖了 NLTK 里的 word_tokenize,虽然说 word_tokenize 可以通过 language 参数设置语言,但在 TextBlob 里没有提供传递这个参数的机会,这就导致 TextBlob 只能对英文进行分类。

使用 TextGrocery 进行文本分类

注意:TextGrocery 仅支持 Python2

安装: pip install tgrocery

文档: https://github.com/2shou/TextGrocery/blob/master/README_CN.md

  • 初始化

    需要给分类器指定一个名字

    from tgrocery import Grocery
    
    classifier = Grocery('test')
    

    默认使用 jieba 作为分词器,但也支持在初始化分类器的时候通过 custom_tokenize 参数来自定义分词器

    classifier = Grocery('test', custom_tokenize=list)
    

    要求 custom_tokenize 的参数值是一个 python 的函数。

  • 训练

    支持传入 python 数据进行训练:

    train_src = [
        ('education', '名师指导托福语法技巧:名词的复数形式'),
        ('education', '中国高考成绩海外认可 是“狼来了”吗?'),
        ('sports', '图文:法网孟菲尔斯苦战进16强 孟菲尔斯怒吼'),
        ('sports', '四川丹棱举行全国长距登山挑战赛 近万人参与')
    ]
    classifier.train(train_src)
    

    也支持从文件中读取训练数据然后训练,要求文件中一行是一个数据,且行中有一个分隔符把文本和文本的类别标签分隔开,如用竖线分隔:

    education|名师指导托福语法技巧:名词的复数形式
    education|中国高考成绩海外认可 是“狼来了”吗?
    sports|图文:法网孟菲尔斯苦战进16强 孟菲尔斯怒吼
    sports|四川丹棱举行全国长距登山挑战赛 近万人参与
    

    假设上面的内容存储在 train.txt 中,则将 train.txt 作为 train 的参数,同时要用 delimiter 参数指明分隔符

    classifier.train('train.txt', delimiter='|')
    
  • 评估

    test_src = [
        ('education', '福建春季公务员考试报名18日截止 2月6日考试'),
        ('sports', '意甲首轮补赛交战记录:米兰客场8战不败国米10年连胜'),
    ]
    report = classifier.test(test_src)
    report.show_result()
    

    上述代码会输出如下内容:

                   accuracy       recall
    education      50.00%         100.00%
    sports         0.00%          0.00%
    

    也可以从文件中读取数据进行评估,文件的要求同训练

    report = classifier.test('test.txt', delimiter='|')
    report.show_result()
    
  • 预测

    使用 predict 接口来进行预测

    preds = classifier.predict('意甲首轮补赛交战记录:米兰客场8战不败国米10年连胜')
    print preds.dec_values         # => {'education': 0.00604235155848336, 'sports': -0.006042351558483356}
    print preds.predicted_y        # => education
    print str(preds)               # => education
    
  • 模型保存和读取

    用 save 方法来保存模型

    classifier.save()
    

    保存模型时会用分类器初始化时给的名字来创建一个目录,比如最开始给的名字是 test,所保存的模型会在 test 目录下,如下所示:

    test
    ├── converter
    │   ├── class_map.config.pickle
    │   ├── feat_gen.config.pickle
    │   └── text_prep.config.pickle
    ├── id
    └── learner
        ├── idf.pickle
        ├── liblinear_model
        └── options.pickle
    

    用相同的名字创建一个分类器,然后执行 load 方法来读取模型

    classifier = Grocery('test', custom_tokenize=list)
    classifier.load()
    

    这里需要注意的是,保存模型的时候,自定义的分词器是没有被保存下来的,所以在读取的时候,还需要重新设置一下分词器。

TextGrocery 是一个基于 liblinear 的小巧的文本分类实现,可惜作者已经放弃维护了,目前只能在 Python2 环境里面使用。

使用 sklearn 进行文本分类

安装: pip install scikit-learn

文档: https://scikit-learn.org/stable/tutorial/text_analytics/working_with_text_data.html#training-a-classifier

sklearn 中实现了很多的分类器,并且提供了统一的接口,我个人是比较喜欢的。

  • 训练

    首先创建一个 vectorizer 用来将文本编码成向量,最常用的可能是 TfidfVectorizer 了

    from sklearn.feature_extraction.text import TfidfVectorizer
    
    vectorizer = TfidfVectorizer()
    

    默认会按空格来分词,如果需要自定义分词器,可以通过 tokenizer 参数来传入一个函数,比如

    import jieba
    
    def jieba_tokenize(text):
        return jieba.lcut(text)
    
    vectorizer = TfidfVectorizer(tokenizer=jieba_tokenize)
    

    注意:由于 jieba 中加了线程锁,将 jieba.lcut 直接传入,会导致模型无法保存

    这个 vectorizer 是需要训练的

    texts = [
        '名师指导托福语法技巧',
        '中国高考成绩海外认可',
        '法网孟菲尔斯苦战进16强',
        '四川丹棱举行登山挑战赛',
    ]
    vectorizer.fit(texts)
    

    一旦训练后,对任意一个文本,会产生一个固定长度的向量,比如:

    print(vectorizer.transform(['名师指导托福语法技巧']).toarray())
    

    上面的代码会输出

    [[0.        0.        0.        0.        0.4472136 0.        0.
      0.        0.        0.4472136 0.4472136 0.4472136 0.        0.
      0.        0.        0.        0.        0.        0.4472136 0.
      0.       ]]
    

    向量化还有其他方法,如下:

    from sklearn.feature_extraction.text import (
        TfidfVectorizer,
        CountVectorizer,
        HashingVectorizer,
    )
    from sklearn.feature_extraction import DictVectorizer
    

    创建一个分类器,比如 SVM

    from sklearn.svm import LinearSVC
    
    classifier = LinearSVC()
    

    如果想使用 GBDT 分类器的话,可以执行 pip install xgboost 安装 XGBoost 这个包,它提供了符合 sklearn 规范的接口,可以直接使用并像 sklearn 的分类器一样用在后面的训练、预测过程中:

    from xgboost import XGBClassifier
    
    classifier = XGBClassifier()
    

    首先用 vectorizer 将训练数据中的文本转成矩阵,然后喂给分类器进行训练

    train_texts = [
        # blablabla
    ]
    train_labels = [
        # blablabla
    ]
    train_feats = vectorizer.transform(train_texts)
    classifier.fit(train_feats, train_labels)
    
  • 评估

    用分类器的 score 方法可以计算测试集的 accuracy

    test_texts = [
        # ...
    ]
    test_labels = [
        # ...
    ]
    test_feats = vectorizer.transform(test_texts)
    acc = classifier.score(test_feats, test_labels)
    

    这个方法其实是调用了 accuracy_score 这个函数,所以也可以自己来计算

    from sklearn.metrics import accuracy_score
    
    pred_labels = classifier.predict(test_feats)
    acc = accuracy_score(test_labels, pred_labels)
    

    还可以用 classification_report 这个函数来得到更详细的评估报告

    from sklearn.metrics import classification_report
    
    pred_labels = classifier.predict(test_feats)
    print(classification_report(test_labels, pred_labels))
    

    输出结果是下面这个样子的:

                  precision    recall  f1-score   support
    
         class 0       0.50      1.00      0.67         1
         class 1       0.00      0.00      0.00         1
         class 2       1.00      0.67      0.80         3
    
        accuracy                           0.60         5
       macro avg       0.50      0.56      0.49         5
    weighted avg       0.70      0.60      0.61         5
    

    有时候我们还需要输出分类的混淆矩阵,虽然 sklearn 提供了 sklearn.metrics.confusion_matrix 这个方法来计算混淆矩阵,但它的输出不够直观,我个人比较喜欢用 pandas.crosstab

    import pandas
    
    pred_labels = classifier.predict(test_feats)
    cnf_matrix = pandas.crosstab(
        pandas.Series(test_labels), pandas.Series(pred_labels),
        rownames=['targets'], colnames=['preds']
    )
    

    输入结果是下面这个样子:

    preds     negative  positive
    
    targets
    negative       590       126
    positive       383       901
    
  • 预测

    用 predict 方法来预测最可能的类别,或用 predict_proba 方法来获得所预测类别的分数

    texts = ['text1', 'text2', 'text3']
    feats = vectorizer.transform(texts)
    labels = classifier.predict(feats) # labels: ['label1', 'label2', 'label3']
    # or
    prob_list = classifier.predict_proba(feats)
    # prob_list:
    # [
    #     {'label1': 0.1, 'label2': 0.3, 'label3': 0.6},
    #     {'label1': 0.1, 'label2': 0.3, 'label3': 0.6},
    #     {'label1': 0.1, 'label2': 0.3, 'label3': 0.6},
    # ]
    

    注意 sklearn 中的 predict/predict_proba 都被设计为批量预测,没有单个数据预测的接口。

  • 模型保存和读取

    保存模型用 pickle 或者 joblib 都可以,注意要把 vectorizer 和 classifier 一起保存。

    import pickle
    from sklearn.externals import joblib
    
    with open('model.pkl', 'wb') as f:
        data = [vectorizer, classifier]
        pickle.dump(data, f)
    
    # or
    data = [vectorizer, classifier]
    joblib.dump(data, 'model.pkl')
    

    如果使用 pickle.dump 保存的模型,则用 pickle.load 来读取;如果是用 joblib.dump 保存的则用 joblib.load 读取

    vectorizer, classifier = None, None
    with open('model.pkl', 'rb') as f:
        vectorizer, classifier = pickle.load(f)
    
    # or
    vectorizer, classifier = joblib.load('model.pkl')
    

除了上面这样先创建 vectorizer 再创建 classifier 的方法,sklearn 还提供了 Pipeline 这个类来简化这个过程,非常推荐使用。

创建 vectorizer 和 classifier 后,用 Pipeline 把它们组合起来:

from sklearn.pipeline import Pipeline

vectorizer = TfidfVectorizer()
classifier = LinearSVC()
pipeline = Pipeline([('vec', vectorizer), ('model', classifier)])

然后可以直接将文本喂给 pipeline,不用自己再去调用 vectorizer.fit 和 vectorizer.transform 来将文本编码成向量了!

train_texts = [
    # blablabla
]
train_labels = [
    # blablabla
]
pipeline.fit(train_texts, train_labels)

评估、预测和非 pipeline 方式的差不多,都是可以省略掉将文本转成向量的这个步骤;模型保存时只需要将 pipeline 保存成文件即可。

使用 FastText 进行文本分类

安装: pip install fasttext

文档: https://fasttext.cc/docs/en/python-module.html#text-classification-model

  • 数据格式

    fasttext 的训练和评估都只能从文件中读取数据,而不能直接传入 Python 的值,而且对文件的格式是有要求的

    1. 文件中一行一个样本
    2. 每行用制表符分隔,第一列是标签,第二列是文本
    3. 第一列的标签要有 __label__ 前缀
    4. 第二列的文本必须是用空格分隔的词序列,对中文来说,意味着需要先分好词

    文件内容示例如下:

    __label__education	名师 指导 托福 语法 技巧 : 名词 的 复数 形式
    __label__education	中国 高考 成绩 海外 认可 是 “ 狼 来了 ” 吗 ?
    __label__sports	图文 : 法网 孟菲尔斯 苦战 进 16强 孟菲尔斯 怒吼
    __label__sports	四川 丹棱 举行 全国 长距 登山 挑战赛 近 万人 参与
    
  • 训练

    假设训练数据按照前面的要求写在了 train_data.txt 里,则用下面的代码来训练:

    import fasttext
    
    model = fasttext.train_supervised('train_data.txt')
    
  • 评估

    假设测试数据在 test_data.txt 中,使用 test 方法来评估模型效果,它会返回数据集中的样本数量,以及 precesion 和 recall 值:

    num, precesion, recall = model.test('test_data.txt')
    

    也可以用 test_label 方法获得每个类别的 precesion、recall 和 f1 值:

    print(model.test_label('test_data.txt'))
    

    输出结果是下面这个样子的:

    {
        '__label__education': {
            'precision': 0.8830022075055187,
            'recall': 0.8784773060029283,
            'f1score': 0.8807339449541285
        },
        '__label__sports': {
            'precision': 0.883881230116649,
            'recall': 0.853121801432958,
            'f1score': 0.8682291666666667
        }
    }
    
  • 预测

    用 predict 接口来对单条文本进行预测,同样要求文本是用空格分隔的、分好词的

    top3_labels, top3_scores = model.predict('土豆网 拟 明年 登陆 纳市 募资 1.5 亿美元', k=3)
    
  • 模型保存和读取

    保存

    model.save_model('model.bin')
    

    读取

    import fasttext
    model = fasttext.load_model('model.bin')
    

使用 Kashgari 进行文本分类

安装: pip install kashgari-tf tensorflow==1.14.0

文档: https://kashgari.bmio.net/

Kashgari 是一个基于神经网络模型的 NLP 工具,内部实现大多数常用的神经网络模型,也支持了最新的 BERT,使用体验挺不错的。

进行常规的文本分类

  • 训练

    Kashgari 要求输入的文本是分好词的,把分词的事情留给用户自己处理。不过分好词就能直接输入到模型中了,不需要像 sklearn 一样通过 vectorizer 转成向量:

    from kashgari.tasks.classification.models import CNN_Model
    
    train_x = [['Hello', 'world'], ['Hello', 'Kashgari']]
    train_y = ['a', 'b']
    
    model = CNN_Model()
    model.fit(train_x, train_y)
    

    训练时还可以设置校验集

    val_x = [['Hello', 'world'], ['Hello', 'Kashgari']]
    val_y = ['a', 'b']
    
    model.fit(val_x, val_y)
    
  • 评估

    test_x = [['Hello', 'world'], ['Hello', 'Kashgari']]
    test_y = ['a', 'b']
    model.evaluate(test_x, test_y)
    

    会打印测试结果到标准输出,其内容是下面这个格式的:

                  precision    recall  f1-score   support
    
          sports     1.0000    1.0000    1.0000      1000
       education     1.0000    0.9980    0.9990      1000
      technology     0.9930    1.0000    0.9965      1000
    
        accuracy                         0.9985     10000
       macro avg     0.9985    0.9985    0.9985     10000
    weighted avg     0.9985    0.9985    0.9985     10000
    
  • 预测

    使用 predict 方法来预测最可能的类别

    tokens = ['姚明', ':', '对', '奥尼尔', '不得', '不服']
    model.predict([tokens])          # => ['sports']
    

    或者用 predict_top_k_class 来获取 topk 的预测结果及分数

    print(model.predict_top_k_class([tokens], top_k=3))
    

    结果

    [
        {
            'label': 'sports',
            'confidence': 0.50483656,
            'candidates': [
                {'label': 'education', 'confidence': 0.057417843},
                {'label': 'technology', 'confidence': 0.048766118},
            ]
        }
    ]
    
  • 模型保存和读取
    • 保存

      使用 save 方法将模型保存到 test 目录中,目录不存在会创建

      model.save('test')
      

      目录中会有一个描述模型结构的 model_info.json 和记录模型参数的 model_weights.h5

      test
      ├── model_info.json
      └── model_weights.h5
      
      0 directories, 2 files
      
    • 读取

      使用 kashgari.utils.d_model 来读取模型

      from kashgari.utils import load_model
      
      model = load_model('test')
      

基于 BERT 进行文本分类

先下载 BERT 模型。

中文的话可以用 Google 开放的: https://storage.googleapis.com/bert_models/2018_11_03/chinese_L-12_H-768_A-12.zip

中文模型下载后解压得到 chinese_L-12_H-768_A-12 这个目录

然后创建基于 BERT 的分类器

from kashgari.embeddings import BERTEmbedding
from kashgari.tasks.classification.models import CNN_Model

embedding = BERTEmbedding('chinese_L-12_H-768_A-12/', task=kashgari.CLASSIFICATION)
model = CNN_Model(embedding)

之后的训练、评估、预测,都和非 BERT 的模型一样。

默认情况下 BERTEmbedding 被设置为不可训练,如果需要对 BERT 进行 finetuning 的话,那么按如下设置:

embedding = BERTEmbedding('chinese_L-12_H-768_A-12/', task=kashgari.CLASSIFICATION, trainable=True)

使用 AllenNLP 进行文本分类

安装: pip install allennlp

文档: https://allennlp.org/tutorials

进行常规的文本分类

AllenNLP 完全通过配置文件来对数据处理、模型结果和训练过程进行设置,最简单的情况下可以一行代码不写就把一个文本分类模型训练出来。下面是一个配置文件示例:

{
    "dataset_reader": {
        "type": "text_classification_json",
        "tokenizer": {
            "type": "word",
            "word_splitter": {
                "type": "jieba",
            }
        }
    },
    "train_data_path": "allen.data.train",
    "test_data_path": "allen.data.test",
    "evaluate_on_test": true,
    "model": {
        "type": "basic_classifier",
        "text_field_embedder": {
            "tokens": {
                "type": "embedding",
                "embedding_dim": 100,
                "trainable": true
            }
        },
        "seq2vec_encoder": {
            "type": "cnn",
            "embedding_dim": 100,
            "num_filters": 1,
            "ngram_filter_sizes": [2, 3, 4]
        }
    },
    "iterator": {
        "type": "bucket",
        "sorting_keys": [["tokens", "num_tokens"]],
        "batch_size": 64
    },
    "trainer": {
        "num_epochs": 40,
        "patience": 3,
        "cuda_device": -1,
        "grad_clipping": 5.0,
        "validation_metric": "+accuracy",
        "optimizer": {
            "type": "adam"
        }
    }
}

配置文件中的内容可以分成

  • 数据部分: 包括 dataset_reader/train_data_path/test_data_path 这几个 key 及其 value
  • 模型部分: 就是 model 这个 key 的内容
  • 训练部分: 包括 evaluate_on_test/iterator/trainer 这几个 key 及其 value

由于本文不是专门介绍 AllenNLP 的文章,所以只对这些配置做简要说明,详细内容可查看文档。

  • 数据部分

    train_data_path 和 test_data_path 比较好理解,它们指定了训练数据和测试数据的文件路径;而 data_reader 则限定了数据文件的格式。

    data_reader 中的配置,会被用来构建一个 DatasetReader 的子类的对象,用来读取数据并转换成一个个 Instance 对象。

    • 内置的可用来读取分类数据的 DataReader 是 TextClassificationJsonReader ,所以配置中有

      "type": "text_classification_json"
      

      这个 type 的值是 TextClassificationJsonReader 这个类实现的时候注册上的,去看代码会看到有这样的片段

      @DatasetReader.register("text_classification_json")
      class TextClassificationJsonReader(DatasetReader):
      

      这个 TextClassificationJsonReader 要求的数据文件是一行一个 json 数据,如下:

      {"label": "education", "text": "名师指导托福语法技巧:名词的复数形式"}
      {"label": "education", "text": "中国高考成绩海外认可是“狼来了”吗?"}
      {"label": "sports, "text": "图文:法网孟菲尔斯苦战进16强孟菲尔斯怒吼"}
      {"label": "sports, "text": "四川丹棱举行全国长距登山挑战赛近万人参与"}
      
    • DataReader 通过配置中 tokenizer 部分会创建一个分词器,用来将文本转换为词序列

      "tokenizer": {
          "type": "word",
          "word_splitter": {
              "type": "jieba",
          }
      }
      

      type 的值设置为 word,这没什么好说的。

      tokenizer 中的 word_splitter 指定的才是真正的分词器(比较绕)。

      如果是英文的数据,那么 word_splitter 的配置可以不写,默认就是支持英文分词的。

      但如果是用于中文处理的话,有一个 SpacyWordSplitter 可以用于中文分类,但是现有的中文 spaCy 模型仅支持 spaCy 2.0.x,和 AllenNLP 中 spaCy 要求的版本不兼容,这个是比较坑的。

      好在 AllenNLP 提供了加载自定义模块的方法,按照如下方法来处理这个问题

      mkdir allen_ext/
      touch allen_ext/__init__.py
      touch allen_ext/word_splitter.py
      

      然后在 allen_ext/word_splitter.py 中写入如下内容

      from typing import List
      
      import jieba
      from overrides import overrides
      from allennlp.data.tokenizers.token import Token
      from allennlp.data.tokenizers.word_splitter import WordSplitter
      
      
      @WordSplitter.register('jieba')
      class JiebaWordSplitter(WordSplitter):
      
          def __init__(self):
              pass
      
          @overrides
          def split_words(self, sentence: str) -> List[Token]:
              offset = 0
              tokens = []
              for word in jieba.lcut(sentence):
                  word = word.strip()
                  if not word:
                      continue
      
                  start = sentence.find(word, offset)
                  tokens.append(Token(word, start))
      
                  offset = start + len(word)
      
              return tokens
      

      使用 WordSplitter.register('jieba') 后就可以在配置中 word_splitter 部分写上 "type": "jieba" 来启用。

      在 allen_ext/__init__.py 中写入如下内容

      from .word_splitter import JiebaWordSplitter
      
      __all__ = ['JiebaWordSplitter']
      

      自定义了 JiebaWordSplitter 后在训练的时候还要加载 allen_ext 这个目录才能生效,这个之后再说。

  • 模型部分

    因为是做文本分类,所以 type 设置为 basic_classifier

    这个分类器需要 text_field_embedder 和 seq2vec_encoder 两个参数:

    • text_field_embedder 用来定义 word embedding,这个配置应该还好理解

      "text_field_embedder": {
          "tokens": {
              "type": "embedding",
              "embedding_dim": 100,
              "trainable": true
          }
      }
      
    • seq2vec_encoder 则用来产生句子的编码向量用于分类,这里选择了 CNN

      "seq2vec_encoder": {
          "type": "cnn",
          "embedding_dim": 100,
          "num_filters": 1,
          "ngram_filter_sizes": [2, 3, 4]
      }
      
  • 训练部分:略

配置文件写好后,假设配置文件为 config.json,直接执行下面的命令来训练即可

allennlp train config.json -s model_save_dir --include-package allen_ext

选项 --include-package allen_ext 用来来加载自定义的模块。

最终会在 save_dir 目录下产生一个 model.tar.gz 文件,就是模型参数,然后目录下还会产生 tensorboard 能读取的 log,这个挺方便的。

评估的话,用 evaluate 命令

allennlp evaluate model_save_dir/model.tar.gz test.jsonl --include-package allen_ext

比较麻烦的是,预测需要一个 Predictor,而 AllenNLP 中内置的 TextClassifierPredictor 要求的输入是 {"sentence": "xxx"} ,这个和 TextClassificationJsonReader 的要求不一样……

如果是在代码里进行预测,那么是没有问题的,可以这样

from allen_ext import *         # noqa
from allennlp.models.archival import load_archive
from allennlp.predictors.predictor import Predictor

archive = load_archive('model_save_dir/model.tar.gz')
predictor = Predictor.from_archive(archive)

inputs = {"sentence": "名师指导托福语法技巧:名词的复数形式"}
result = predictor.predict_json(inputs)

得到的 result 是这样的结构

{
    'label': 'education',
    'logits': [
        15.88630199432373,
        0.7209644317626953,
        7.292031764984131,
        5.195938587188721,
        5.073373317718506,
        -35.6490478515625,
        -7.7982988357543945,
        -35.44648742675781,
        -18.14293098449707,
        -14.513381004333496
    ],
    'probs': [
        0.999771773815155,
        2.592259420453047e-07,
        0.0001851213601185009,
        2.2758060367777944e-05,
        2.013285666180309e-05,
        4.153195524896307e-23,
        5.1737975015342386e-11,
        5.085729773519049e-23,
        1.6641527142180782e-15,
        6.273159211056881e-14
    ],
}

这个输出结构完全是由 TextClassifierPredictor 决定的。

如果要自定义 Predictor,可以参考文档

基于 BERT 进行文本分类

AllenNLP 是基于 pytorch 实现的,所以 Google 提供的 BERT 模型在它这里没法用,需要下载它自己提供的模型,以中文模型为例:

mkdir chinese_bert_torch && cd chinese_bert_torch
wget https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese-pytorch_model.bin -O pytorch_model.bin
wget https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese-config.json -O config.json
wget https://s3.amazonaws.com/models.huggingface.co/bert/bert-base-chinese-vocab.txt -O vocab.txt

然后 config.json 中 data_reader 部分这样写

{
    "dataset_reader": {
        "type": "text_classification_json",
        "tokenizer": {
            "type": "word",
            "word_splitter": {
                "type": "bert-basic",
            }
        },
        "token_indexers": {
            "bert": {
                "type": "bert-pretrained",
                "pretrained_model": "./chinese_bert_torch/vocab.txt"
            }
        }
    }
}

model 部分这么写

{
    "model": {
        "type": "bert_for_classification",
        "bert_model": "./chinese_bert_torch",
        "trainable": false
    }
}

这里 trainable 设置成 false 的话 BERT 就只是充当一个 encoder,不参与训练;如果要进行 finetuning 的话将其改为 true。

完整的配置是这个样子的

{
    "dataset_reader": {
        "type": "text_classification_json",
        "tokenizer": {
            "type": "word",
            "word_splitter": {
                "type": "bert-basic",
            }
        },
        "token_indexers": {
            "bert": {
                "type": "bert-pretrained",
                "pretrained_model": "./chinese_bert_torch/vocab.txt"
            }
        }
    },
    "train_data_path": "allen.data.train",
    "test_data_path": "allen.data.test",
    "evaluate_on_test": true,
    "model": {
        "type": "bert_for_classification",
        "bert_model": "./chinese_bert_torch",
        "trainable": false
    },
    "iterator": {
        "type": "bucket",
        "sorting_keys": [["tokens", "num_tokens"]],
        "batch_size": 64
    },
    "trainer": {
        "num_epochs": 5,
        "patience": 3,
        "cuda_device": -1,
        "grad_clipping": 5.0,
        "validation_metric": "+accuracy",
        "optimizer": {
            "type": "adam"
        }
    }
}

训练、评估、预测等操作同未使用 BERT 的时候一样。

论文笔记:Reading Wikipedia to Answer Open-Domain Questions

2019年8月7日 08:00

drqa.png

这篇论文实现了一个基于维基百科的开放域问答系统,其实可以归类到机器阅读理解里面。相应的,这篇论文有配套的开源代码(见 facebookresearch/DrQA),这个系统的工程部分还是蛮实在的。

论文地址:https://arxiv.org/abs/1704.00051

作者

一作是毕业于 Stanford 的陈丹琦,目前在 Facebook 人工智能研究院(Facebook AI Research, FAIR) 。陈丹琦在机器阅读理解方面做了不少工作,博士论文《Neural Reading Comprehension and Beyond》我计划之后也读一下。

其他参与者是 FAIR 的几个人:Adam Fisch, Jason Weston, Antoine Bordes。

相关工作

  • Ryu et al. 2014: They combine article content with multiple other answe matching modules based on different types of semi-structured knowledge such as infoboxes, arfticle structures, category structure and definitions
  • Ahn et al. 2004: combine Wikipedia as a text resource with other resources, in this case with information retrieval over other documents
  • Sun et al. 2015, QuASE: full pipeline QA approche
  • Microsoft's AskMSR, 2002
  • IBM's DeepQA, 2010: DeepQA is a very sophisticated system that relies on both unstructured information including text documents as well as structured data such as KBs, databases and ontologies to generate candidate answers or vote over evidence.
  • YodaQA, 2015

术语

  • Open-Domain Question Answering

    Open-domain QA was originally defined as finding answers in collections of unstructured documents, following the setting of the annual TREC competitions

  • Machine Reading at Scale, MRS

    In order to answer any question, one must first retrieve the few relevant articles among more than 5 million items, and then scan them carefully to identify the answer. We term this setting, machine reading at scale.

  • MurMurHash 算法

    我去了解了下,MurMurHash 算法是一种被广泛应用的速度快且碰撞少的哈希算法,sklearn 里的 HashingVectorizer、Redis 中都用到了这个算法。

观点

  • Freebase/DBPedia 等知识库对计算机友好,但是过于稀疏,直接用于 open-domain question answering 并不好

    标了一个来源: Miller et al., 2016

  • 如 IBM DeepQA(Ferrucci et al., 2010) 的大规模 QA 系统,依赖多个不同的数据源之间的信息冗余来提高回答效果,比如说 Wikipedia、知识库、词典、大量的新闻文章、书籍……

数据集

论文中提到的数据集

论文中使用的数据集

  • 2016 年 12 月 21 日导出的英文维基百科数据,包含 5075182 篇文章
  • SQuAD 数据集,这个是机器阅读理解领域最有名的数据集了
  • CuratedTREC 数据集,包含 2180 个问题
  • WebQuestions 数据集
  • WikiMovies 数据集,包含了 96000 对电影领域的问答数据

SQuAD 数据集是标准的阅读理解数据集,每一个样本都包含一个 (Q, A) 对,以及关联的维基百科文章中的段落,但 CuratedTREC、WebQuestions 和 WikiMovies 三个数据集只有单纯的 (Q, A) 对,所以论文中需要为后面这三个数据集补充上关联的文章段落数据才能用于训练。

具体来说,是通过远程监督的手段来为 CuratedTREC/WebQuestions/WikiMovies 三个数据集补充相关的文章段落数据的,过程如下:

  • 从数据集中选择一个 (Q, A) 对
  • 用问题文本也就是 Q 作为 query,检索 5 篇维基百科的文章
  • 将这 5 篇百科文章切分成段落,并按下面的步骤去除无关的内容
    • 若段落中不能找到 A,则去除
    • 去除字数在 25 以下或 1000 以上的段落
    • 如果 Q 中有命名实体(如人名、地名等),而某个段落中没有这个命名实体,则去除这个段落
  • 如果没有剩余可用段落,那么丢弃这个样本,如果有则进行排序:取段落中匹配到 A 的位置,以其为中心取一个 20 个词的窗口,计算其与 Q 的重叠程度(我理解是 LCS 之类的),选择最匹配的 5 个段落

模型和方法

模型分为两大块,首先有一个 Document Retriever,对给定的问题 question,从所有维基百科文章中检索;检索到文章后切分成段落,然后用一个称之为 Document Reader 的模块在段落中预测答案位置并给出分数。后者其实就是标准的阅读理解模型了,完全可以替换成其他的机器阅读理解模型。

总体上来说这个模型不复杂,或者说非常的简单直观……所以我就不在这里写那些模型的计算式了,需要的话可以自行查阅论文。

Document Retriever 部分,我去阅读了对应开源项目的代码,有两种方法

  • TfidfDocRanker: 将 question 用 TFIDF+BOW 编码成向量,然后和文档的向量表示做內积进行排序

    看代码的话是直接和所有文档的向量组成的矩阵相乘的,维基百科的所有文章数量是很多的,我只能说像我这种穷惯了的人没法想象吧……

  • ElasticDocRanker: 这个直接就是把所有维基百科的文章存到 ES 上,然后用 ES 的 search 接口进行检索,用检索结果里 _score 来排序

可以看到,所谓的 DocumentRetriever 的部分真的是超简单的……

DocumentReader 是一个标准的机器阅读理解模型了,输入一个段落和一个 Q,从段落中预测一个 start 位置和一个 end 位置,取两个位置中间的文本作为答案输出。其中 start 和 end 的计算都是一样的,用 question 的向量表示和段落的向量表示做 attention 得到每个位置的 score。对任意一个 (start, end) 区间,取两者 score 的乘积作为这个区间的 score 并用之来排序,要求区间长度不超过 15。

由于使用的数据有一部分是靠远程监督产生出来的,必然会存在噪音,所以这篇论文在训练阶段做了一些特殊处理,具体来说做了两个尝试:

  • 用 SQuAD 数据先训练出一个模型,然后在远程监督的数据上进行 finetuning
  • 或者,SQuAD 数据和远程监督数据一起作为训练数据,进行多任务学习,也就是分开训练,但是共享部分参数

实验和结论

  • 单独在 SQuAD 上做训练,本文的 Document Reader 模型,在 dev 集上效果要好于 R-net 等一干模型,在 test 集上基本与 R-net 持平而好于剩下的模型,如下表所示:

    drqa_eval.png

    注: 这个表的结果是指不做文档检索、已经给定文章段落时预测答案的模型效果,而下文那个表则是包含文档检索、答案预测的整体效果。

  • 加远程监督数据,无论是 finetunning 还是多任务学习,效果都有显著提升,如下表所示

    drqa_eval2.png

    虽然说在 WikiMovies 数据集上有多达 12 个百分点的提升,不过 top1 的精度最好也就 30% 多……

个人总结

  • 这是一篇工程向的论文,在模型结构上并没有什么亮点
  • 远程监督制造大量带噪声的数据,然后以多任务学习或者 finetuning 的形式参与模型训练,这个其实是蛮有用的一个经验,我个人也在多个领域看到不少这种做法,对于深度学习这种对数据太过贪婪的方法,这也是被逼出来的方法吧
  • DocumentRetriever 部分做得太简单了,我认为这是最终系统整体效果才 30% 多的一个较大原因,顺便吐槽一下,加个 ES 检索就说自己是「Machine Reading at Scale」了……

搬家记

2019年6月9日 08:00

经过一周的奔走,看了十来个房子,最终我在 2019 年儿童节这天选定了一个并迅速和房东签好了合同,然后马不停蹄地打包东西,在次日搬入了新居,正式回归了独居生活。从 2015 年 3 月初起,我已经和朋友一起合租了 4.25 年,可以说室友是除家人外共同居住时间最长的人了。6 月 3 日,搬家后第一个工作日,在下班回去的公交车上,有一瞬间感觉特别的孤单,但其实,我刚来北京的时候就是独自居住的。

刚来北京的时候,住在昌平沙河,一个城中村的区域,700 一个月的大开间,每月去二楼给房东交钱,房东收完钱后手写一张收据,这收据我现在还留着一张,不过也就一张了。同学 ZS 和我住在同一栋楼,数次 12 点才回来叫我下去帮他开门,有一次干脆连房门钥匙都忘了就在我那对付了一晚上。城中村里混乱而充满着活力,生活该有的东西一应俱全,包括但不限于:各种卖食物的小摊(记得西瓜很便宜)、大的小的服装店、杂货店百货店、各种风格的小饭馆、5 元一位的小三轮、象棋残局骗子、街边吆喝的房地产中介、深夜街头的吵架、挨家挨户寻找离家出走猫咪的姑娘……那个时候还没学会做饭,除了几个同学也不认识什么人,我又不是那种混得了酒吧趴踢的人,生活了无情趣,下班回到窝里,能做的事情只有打炉石、啃至今没啃完的机器学习英文原版书,没钱但是简单而无畏地快乐着。

15 年初,跳槽工资 double 后,老板找我谈,先给了一胡萝卜,说给我开不了那么多工资能不能涨点工资然后给点期权,我哪懂什么期权就拒绝了;末了给了我一大棒,说我负责的工作还有很多问题希望我有点责任心云云。我很愧疚,然后回头就去办了离职手续。此时恰好另外一位同学 L 跳槽到华为,一合计就决定一起(包括同学 ZS)找一个房子合租。运气很好的是,L 在华为内部论坛看到一位前华为员工发帖,说自己房子想出租找一个华为员工,迅速约好去看,看完后我们在去地铁的路上,听到一个擦肩而过双肩包青年在给中介打电话,要去我们刚看过的小区,如临大敌,马上联系中介交了 1000 订金把事情定了下来。然后就是第一次搬家,印象很深刻,因为我和 ZS 一起叫了一个金杯车拉东西过去,然后我拎了几个装了我和 ZS 一些零碎东西的包坐地铁过去,结果 ZS 往包里塞了菜刀没有告诉我,在地铁被安检发现并没收,还让我签字留念了。

第二个住处是在一个正经的小区内,往西走两百米是一个购物中心,往东走一百米是一个果蔬店,南边紧挨着北京体育大学,可以说是很舒服的。我在这里学会了做饭(厨艺一般),把至今没有啃完的英文原版书又多啃了三五章,后来还买了一自行车骑车上班 —— 刚开始在北京没有自行车道的大马路上穿行的时候我慌得不行。周末无聊就骑车在上地一片转,工作日这里大堆公司都灯火通明,但到周末就静悄悄的,能见到一群老大爷下象棋 —— 就是那种两个正主磨磨叽叽边上大爷摇着扇子说“您看他这車一过来你就……”的状况,偶尔也有一些路人,但大部分时候就是我在路口旋转跳跃都不会有人管的样子。由于客厅够大,这次租的房子,成了北京这波大学同学常用的聚会地点,多的时候来过四五个同学加上我们住那的就七八个人,我和 L 就各做几个菜来招待大伙,通常是 L 的更受好评一些哈哈。

好景不长,两年后,我被告知,原来我们住的房子是廉租房,其实是租给我们的“房东”的。按照规定,我们的“房东”在租用廉租房若干年后是能够直接购买的,但在这之前不能将这房子拿来盈利(比如说转租),所以租给我们其实是不合规矩的,只不过“房东”和原来居委会关系好所以居委会睁一只眼闭一只没管,但两年后也就是 2017 年居委会管事的换人了,这事就藏不住了。总之这是一个双输的事情,“房东”丢失了未来可能的买房机会,我们则失去了便宜的住房。

于是第二次去看房子,还决定和 L 合租,而 ZS 因为工作原因已经提前搬出去,到了望京附近。L 忙前忙后约好中介,我们一晚上看了中科院家属楼、海淀路某小区一间房和圆明园地铁站附近一小区:中科院家属楼看起来像是那种有人住了十来年刚搬出去的样子,破旧异常;海淀路小区在四环边上,正对着北大南门,地理位置可以说非常好了,然而天花板残破脱落,我们看的时候簌簌地往下掉粉,屋里一大哥成堆印着 NVIDIA LOGO 的袋子让我一度以为这大哥是 NVIDIA 员工,回头才明白可能是电脑城卖显卡的;圆明园地铁站附近那个小区虽然是个老房子,但是装修贼好(相对前两个而言),我和 L 刚进门,相视无言,心里想得大概都一样,“就是它了!”。然而之后中介告诉我们我们之前已经有人看过了准备签,无奈只好又去清河(小米总部所在地)附近看,结果正在看一个装修正点南北通透的房子时,中介又打电话过来,说圆明园那小区,租客太小器惹毛了房东于是谈崩了。于是我们最终住进了圆明园地铁站附近那个小区。这次搬家呢,在出发地从三楼把东西搬下去,然后在目的地把东西搬到六楼,都没有电梯,委实是辛苦了搬家师傅。

在这地方一住又是两年。从五环外搬到五环内,离市中心更近了,但生活并没有变方便太多,某些方面甚至更加不方便了。由于是在景区(圆明园)附近,周围没有近距离的商超,最近的菜市场也要坐两站公交,小区背后的博雅酒店半夜三四点大卡车卸货还是什么经常吵得我睡不好。我放弃了啃英文原版书,开始买书如山倒干脆不看书,也放弃了在厨房中探索我对豆腐的爱,转而给 L 洗碗刷锅来免费蹭饭,并这样持续到今年的儿童节。这两年零三个月发生了很多事情:我第一次出差并在之后变本加厉地出差,买了 Nintendo Switch 并浪费了我人生五六百个小时在上面,由于工作开始跟投长赢计划并开始接触投资知识……

合租的好处是不那么孤单,但 L 有女朋友(后来领证了)没什么孤单可言,孤单其实都是我的。感到孤单也没什么可耻的,我一向如此 —— 当然,也没什么可自豪的。哪怕是合租的四年里,那种陡然间觉得一切毫无意义的时刻也不在少数,这吊诡的世道在人心里造成的空洞,钻进人群中时只是被掩盖起来,是无法彻底填满的,擘起火把向前走是唯一的路,我在最绝望的时候已然明白这个道理。我需要面对自己,而不是靠人群、加班来假装自己一切都好。

又一次冗长的出差回来,我以超高的效率开始看房,在一周的时间内,生生让一个知春路片区的链家中介陪我走了好几个北京城北部区域,最后敲定五个候选,最优的两个一个被租走一个只租给女生,于是定了综合评分第三的这个,稍微偏远一些,但好在面积大价钱相对更便宜。五月三十一日敲定后,和中介约好六月一日签合同,隔天早上起来我就开始打包东西,六七箱书、两箱衣服、电器杂物若干,如此而已。

packages.jpg

搬后第一周的工作日里,箱子堆在屋子里没精力去整理。趁着这次端午加周末的三天时间,彻底收拾好了所有东西,重新调整屋内家居布局,做好各个角落的清扫,然后购置好各种厨房用具,到附近的菜市场疯狂扫购(了几个西红柿和鸡蛋)填满了冰箱,喝上第一碗前一晚预约煮好的小米粥时,觉得自己真是个家政小能手!前一个租客如果上门拜访,一定会把肠子都悔青了然后决定不再出国留学的,嗯。话说,你永远不知道清理上一任租客的遗留物品时会遇到什么惊喜……

附豪宅照片。

看到这光可鉴人的地板了吗?原来可脏了……

room1.jpg

两张沙发原来拼在一起把图中空着的区域占满了

room2.jpg

冰箱原来在外面,我给搬了进来。图中的桌子拿来办公娱乐用,堆着的书都是计算机、数学类书籍,和科幻小说

room3.jpg

休息区和阅读区,配备大功率照明用具 —— 真的,这盏台灯是这屋子里晚上亮度最高的光源……

room4.jpg

工作、娱乐区近照(晚上拍的有点黄)

desk1.jpg

阅读区近照

desk2.jpg

也许以后养个猫什么的,嗯哼~

图数据库 Neo4j 的部署、数据导入和简单使用

2019年4月30日 08:00

本文介绍了 Neo4j Server 的不同部署方式,并以豆瓣电影图谱数据为例说明了不同的数据导入方式,并简单介绍了 Cypher 查询语言的使用。

Neo4j 简介

neo4j.jpg

Neo4j 是一个流行的、Java 编写的图数据库 —— 所谓图数据库是一种 NoSQL 数据库,相比于 MySQL 之类的关系数据库(RDBMS),能更灵活地表示数据,这种灵活性体现在多方面:

  1. 像所有 NoSQL 数据库一样可以灵活地设计、扩展 schema
  2. 更适合表示实体之间的关系,特别是当实体之间存在大量的、复杂的关系的时候

图数据库强调实体和关系两个基本概念,虽然说在关系数据库中也可以表示实体和关系,但如果关系的种类繁多且实体之间通过关系构成复杂的结构的时候,用图数据库可能会更合适一些。此外,图数据库会对一些常见的图操作进行支持,典型的比如查询最短路径,如果用关系数据库来做就很比较麻烦。

目前的图数据库有很多种,根据一些排行数据,Neo4j 应该是其中最流行、使用最多的了。

Neo4j 由一个商业公司在开发、维护,并提供 GPLv3 协议的开源社区版本,当然相比他们商业授权的闭源版本,开源版本缺少一些特性,但基本功能都是完整的。

Neo4j 的部署

最简单的办法是从 Neo4j 的下载中心下载 Neo4j Server,解压后运行即可。可以看到下载页有三个不同的版本

neo4j_downloads.png

  • Enterprise Server: 企业版,需要付费获得授权,提供高可用、热备份等特性
  • Community Server: 社区开源版,只能单点运行,在性能上较企业版可能差一些
  • Neo4j Desktop: 顾名思义,是一个桌面版的客户端,可以用其连接 Neo4j Server 进行操作、管理,不过其中也内置了一个本地的 Neo4j Server,用户可以直接通过 Neo4j Desktop 来创建数据库并启动

对于仅仅想了解一下 Neo4j 的人来说,不妨下载 Neo4j Desktop 体验一下,本文则仅讨论 Neo4j Community Server。

目前 Neo4j Server 的版本是 3.5.x,虽然更旧的版本也能用,但建议使用 3.5.0 之后的版本,因为更早的版本是不支持全文索引的。

以 Linux 为例,假如下载的是最新的 3.5.5 版本,那么解压运行即可

我的做法是解压放到 /opt 目录下,并把对应的目录加到环境变量 PATH 里

tar xzvf neo4j-community-3.5.5-unix.tar.gz
mv neo4j-community-3.5.5 /opt/neo4j/
export PATH=$PATH:/opt/neo4j/bin

这样之后就能使用 neo4j start 来启动服务了。

另外一种办法是通过 docker 来启动服务,这个就更简单了,直接利用官方提供的镜像即可。

docker pull neo4j:3.5.5
mkdir $HOME/neo4j/data -p
docker run -p 7474:7474 -p 7687:7687 -v $HOME/neo4j/data/:/data neo4j

这之后就可以通过 http://localhost:7474/browser/ 这个地址访问 Neo4j Server 的 WebUI,可以在上面查询、修改数据。

然后有一些 Server 设置,可以根据自己的情况适当地进行修改,完整的配置见文档,这里罗列一些个人认为重要的

  • 认证方式设置

    默认情况下启动的 neo4j,会要求在访问时通过用户名密码进行认证,初始的用户名密码为 neo4j/neo4j ,但是会在第一次认证之后要求更换密码,有点不太方便。

    一个办法是彻底关闭用户名密码认证,如果是非 docker 模式部署的,直接改 /opt/neo4j/conf/neo4j.conf 这个文件,加上这行配置

    dbms.security.auth_enabled=false
    

    如果是 docker 模式部署的,则在启动容器时,设置环境变量 NEO4J_AUTH 为 none

    docker run -p 7474:7474 \
           -p 7687:7687 \
           -v $HOME/neo4j/data/:/data \
           -e NEO4J_AUTH=none \
           neo4j
    

    另外一个办法是主动设置好密码,如果是非 docker 模式部署,需要在初次启动通过 neo4j-admin 这个命令来设置

    neo4j-admin set-initial-password neo4j_password
    

    如果是 docker 模式部署,则在启动容器时通过环境变量 NEO4J_AUTH 来设置

    docker run -p 7474:7474 \
           -p 7687:7687 \
           -v $HOME/neo4j/data/:/data \
           -e NEO4J_AUTH=neo4j/neo4j_password \
           neo4j
    
  • 内存设置

    这块有三项设置,分别是

    • dbms.memory.heap.initial_size
    • dbms.memory.heap.max_size
    • dbms.memory.pagecache.size

    前两者决定了查询语言运行时候可用的内存,第三个则用于缓存数据和索引以提高查询效率。

    非 docker 模式部署的,可以直接在 /opt/neo4j/conf/neo4j.conf 里修改,比如说这样

    dbms.memory.heap.initial_size=1G
    dbms.memory.heap.max_size=2G
    dbms.memory.pagecache.size=4G
    

    docker 模式部署则还是在启动容器时通过环境变量来设置,如下所示

    docker run -p 7474:7474 \
           -p 7687:7687 \
           -v $HOME/neo4j/data/:/data \
           -e NEO4j_dbms_memory_heap_initial__size=1G \
           -e NEO4j_dbms_memory_heap_max__size=2G \
           -e NEO4j_dbms_memory_pagecache_size=4G \
           neo4j
    
  • 其他
    • dbms.security.allow_csv_import_from_file_urls

      设置为 true,这样在执行 LOAD CSV 语句时,可以使用远程而非本地的 csv 文件。

      docker 的话这样:

      docker run -d -p 7474:7474 \
             -p 7687:7687 \
             -e NEO4J_dbms_security_allow__csv__import__from__file__urls=true \
             -v /home/emonster/data/neo4j/:/data \
             neo4j
      

      这个之后会具体再聊一下。

    • dbms.connectors.default_listen_address

      这个不设置的话,部署起来的 server 就只能监听本地的请求,如果是在生产中用 Neo4j Server 的话,要设置成

      dbms.connectors.default_listen_address=0.0.0.0
      

      docker 的话默认已经设置好了,不用自己再单独设置。

所有的配置项及其值可以用如下查询语言查询

call dbms.listConfig()

如果要查询单独某项的值,比如 "dbms.connectors.default_listen_address",则这样

call dbms.listConfig("dbms.connectors.default_listen_address")

数据加载

为方便说明,我准备了一份豆瓣电影的图谱数据(说是图谱其实结构很简单)放在 Github 上,可以先将其 clone 到本地

git clone https://github.com/Linusp/kg-example

在这个项目下的 movie 目录里有按照 Neo4j 支持的格式整理好的实体、关系数据

(shell) $ cd kg-example
(shell) $ tree movie
movie
├── actor.csv
├── composer.csv
├── Country.csv
├── director.csv
├── district.csv
├── Movie.csv
└── Person.csv

0 directories, 7 files

上述数据包含三类实体数据:

实体类型 数据文件 数量 说明
Movie Movie.csv 4587 电影实体
Person Person.csv 22937 人员实体
Country Country.csv 84 国家实体

此外还包含四类关系数据

关系类型 主语实体类型 宾语实体类型 数据文件 数量 说明
actor Movie Person actor.csv 35257 电影的主演
composer Movie Person composer.csv 8345 电影的编剧
director Movie Person director.csv 5015 电影的导演
district Movie Country district.csv 6227 电影的制片国家/地区

下图是这份数据加载到 Neo4j 后的部分可视化示例

movie_graph.png

使用 neo4j-import 用 csv 数据创建实体和关系

使用 neo4j-import 命令行工具导入 csv 数据是几种数据加载方式中最快的一种,但它不能导入数据到已有的数据库中,每次执行都是产生一个全新的数据库,因此必须在一条命令里将数据库中要包含的数据全部都制定好。

可以用下面的命令来导入豆瓣电影图谱数据

neo4j-import --into graph.db --id-type string \
             --nodes:Person movie/Person.csv    \
             --nodes:Movie movie/Movie.csv \
             --nodes:Country movie/Country.csv \
             --relationships:actor movie/actor.csv \
             --relationships:composer movie/composer.csv \
             --relationships:director movie/director.csv \
             --relationships:district movie/district.csv

上述命令会在当前目录下生成一个 graph.db 目录,就是最终产生的一个全新的数据库。要启用这个数据库,必须将其放置到 Neo4j Server 的 data 目录下:

  • 如果当前 Neo4j Server 正在运行,需要先停掉它

    neo4j stop
    
  • 删除或备份原有的数据库

    mv /opt/neo4j/data/databases/graph.db /opt/neo4j/data/databases/graph.db.bak
    
  • 将产生的 graph.db 放置到 server 的 data 目录下

    cp graph.db /opt/neo4j/data/databases/ -r
    
  • 重新启动 Neo4j Server

    neo4j start
    

实体和关系一共 8 万多条,在我的个人电脑上一共花费 3s 多

IMPORT DONE in 3s 692ms.
Imported:
  27608 nodes
  54844 relationships
  91628 properties
Peak memory usage: 524.24 MB

如果是以 docker 的方式来使用 Neo4j,则稍有不同,需要在执行的时候将 movie 目录和输出结果所在的目录都挂载到容器里。假设说我们希望最终输出结果到 $HOME/neo4j/data 目录下,那么,先创建这个目录

mkdir $HOME/neo4j/data/databases -p

然后执行

docker run -v $PWD/movie:/movie:ro -v $HOME/neo4j/data:/data/ \
       neo4j neo4j-import --into /data/databases/graph.db --id-type string \
             --nodes:Person /movie/Person.csv    \
             --nodes:Movie /movie/Movie.csv \
             --nodes:Country /movie/Country.csv \
             --relationships:actor /movie/actor.csv \
             --relationships:composer /movie/composer.csv \
             --relationships:director /movie/director.csv \
             --relationships:district /movie/district.csv

然后再用 docker 启动 Neo4j Server,并让其使用刚刚产生的数据库

docker run -p 7474:7474 \
       -p 7687:7687 \
       -v $HOME/neo4j/data/:/data \
       -e NEO4J_AUTH=neo4j/neo4j_password \
       neo4j

使用 LOAD CSV 加载 csv 数据

LOAD CSV 语句同样可以加载 csv 数据,不过和 neo4j-import 不一样,本质上它只是负责从 csv 文件中读取数据,如果要将读取到的数据写入到数据库中,还必须通过 CREATE 语句。也正因如此,用 LOAD CSV 语句来加载数据,不需要将 Neo4j Server 停掉。

LOAD CSV 语句将豆瓣电影图谱加载到数据库中的做法是下面这样的

  • 从 Movie.csv 中加载电影数据并创建 Movie 实体

    USING PERIODIC COMMIT 1000
    LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/Movie.csv' as line
    CREATE (:Movie {
           id:line["id:ID"],
           title:line["title"],
           url:line["url"],
           cover:line["cover"],
           rate:line["rate"],
           category:split(line["category:String[]"], ";"),
           language:split(line["language:String[]"], ";"),
           showtime:line["showtime"],
           length:line["length"],
           othername:split(line["othername:String[]"], ";")
           })
    

    其中 "using periodic commit 1000" 表示每读取 1000 行数据就写入一次。

  • 从 Person.csv 中加载人员数据并创建 Person 实体

    USING PERIODIC COMMIT 1000
    LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/Person.csv' as line
    CREATE (:Person {id:line["id:ID"], name:line["name"]})
    
  • 从 Country.csv 中加载国家数据并创建 Country 实体

    USING PERIODIC COMMIT 1000
    LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/Country.csv' as line
    CREATE (:Country {id:line["id:ID"], name:line["name"]})
    
  • 创建关系

    每个关系的 csv 文件都是如下格式(以 actor.csv 为例)

    ":START_ID",":END_ID"
    "5ec851a8b7b7bbf0c9f42bbee021be00","3a20ded16ebce312f56a562e1bef7f05"
    "5ec851a8b7b7bbf0c9f42bbee021be00","8101549e05e6c1afbea62890117c01c6"
    "5ec851a8b7b7bbf0c9f42bbee021be00","111a3c7f6b769688da55828f36bbd604"
    "5ec851a8b7b7bbf0c9f42bbee021be00","5cc5d969f42ce5d8e3937e37d77b89b5"
    "5ec851a8b7b7bbf0c9f42bbee021be00","a5e6012efc56f0ca07184b9b88eb2373"
    "5ec851a8b7b7bbf0c9f42bbee021be00","435c8172c14c24d6cd123c529a0c2a76"
    "5ec851a8b7b7bbf0c9f42bbee021be00","5dfb355a385bcfe9b6056b8d322bfecb"
    "5ec851a8b7b7bbf0c9f42bbee021be00","5076a2f7479462dcc4637b6fe3226095"
    "5ec851a8b7b7bbf0c9f42bbee021be00","c7103a9ad17cf56fd572657238e49fff"
    

在创建关系的时候实际上是根据两个 id 查询到对应的实体,然后再为其建立关系。虽然我在准备这份数据时,已经保证了每个实体的 id 都是全局唯一的,但在没有创建索引的情况下,用这个 id 来查询实体会以遍历的形式进行,效率很差,所以在创建关系前,先创建一下索引。

为 Movie 实体的 id 属性创建索引

CREATE INDEX ON :Movie(id)

为 Person 实体的 id 属性创建索引

CREATE INDEX ON :Person(id)

为 Country 实体的 id 属性创建索引

CREATE INDEX ON :Country(id)

然后继续用 LOAD CSV 来创建关系

  • 从 actor.csv 中加载数据并创建 actor 关系

    USING PERIODIC COMMIT 1000
    LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/actor.csv' as line
    MATCH (a:Movie {id:line[":START_ID"]})
    MATCH (b:Person {id:line[":END_ID"]})
    CREATE (a)-[:actor]->(b)
    
  • 从 composer.csv 中加载数据创建 composer 关系

    USING PERIODIC COMMIT 1000
    LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/composer.csv' as line
    MATCH (a:Movie {id:line[":START_ID"]})
    MATCH (b:Person {id:line[":END_ID"]})
    CREATE (a)-[:composer]->(b)
    
  • 从 director.csv 中加载数据创建 director 关系

    USING PERIODIC COMMIT 1000
    LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/director.csv' as line
    MATCH (a:Movie {id:line[":START_ID"]})
    MATCH (b:Person {id:line[":END_ID"]})
    CREATE (a)-[:director]->(b)
    
  • 从 district.csv 中加载数据并创建 district 关系

    USING PERIODIC COMMIT 1000
    LOAD CSV with headers from 'https://raw.githubusercontent.com/Linusp/kg-example/master/movie/district.csv' as line
    MATCH (a:Movie {id:line[":START_ID"]})
    MATCH (b:Country {id:line[":END_ID"]})
    CREATE (a)-[:district]->(b)
    

使用 Cypher 语句创建数据

严格来说,上面的 LOAD CSV 的方式,也是在用 Cypher 语句,不过说到底它还是要依赖一个外部的 CSV 文件,自由度没那么高。而 Neo4j Server 本身还提供 RESTful API,利用这个 API 就可以进行编程来完成更复杂的需求。

以创建实体为例来说明一下 Neo4j Server 的 RESTful API。假设说我们要创建三个 Person 实体,简单起见,我们假设每个 Person 实体需要有 id, name, age 三个属性,比如

[
    {
        "id": "person1",
        "name": "张志昂",
        "age": 23
    },
    {
        "id": "person2",
        "name": "刘文刀",
        "age": 18
    },
    {
        "id": "person3",
        "name": "孙子小",
        "age": 22
    }
]

通过 RESTful API,可以一次性创建这三个 Person 实体

POST http://neo4j:neo4j_password@localhost:7474/db/data/cypher
Content-Type: application/json
{
    "query": "UNWIND {values} as data CREATE (:Person {id: data.id, name: data.name, age: data.age})",
    "params": {
        "values": [
            {"id": "person1", "name": "张志昂", "age": 23},
            {"id": "person2", "name": "刘文刀", "age": 18},
            {"id": "person3", "name": "孙子小", "age": 22}
        ]
    }
}

这种通过带参数的 query 进行批量写入的方式,和 MySQL 等数据库的接口很相似,不过在 Cypher 中可以通过 UNWIND 语句做一些复杂的事情。详见文档

用 Python 来做的话大概是这个样子

import requests

url = "http://neo4j:neo4j_password@localhost:7474/db/data/cypher"
payload = {
    "query": (
        "UNWIND {values} as data "
        "CREATE (:Person {id: data.id, name: data.name, age: data.age})"
    ),
    "params": {
        "values": [
            {"id": "person1", "name": "张志昂", "age": 23},
            {"id": "person2", "name": "刘文刀", "age": 18},
            {"id": "person3", "name": "孙子小", "age": 22}
        ]
    }
}
requests.post(url, json=payload)

或者也可以使用 Neo4j 官方的 Python 客户端

import neo4j

client = neo4j.GraphDatabase.driver(
    'bolt://localhost:7687', auth=('neo4j', 'neo4j_password')
)
with client.session() as session:
    query = (
        "UNWIND {values} as data "
        "create (:Person {id: data.id, name: data.name, age: data.age})"
    )
    values = [
        {"id": "person1", "name": "张志昂", "age": 23},
        {"id": "person2", "name": "刘文刀", "age": 18},
        {"id": "person3", "name": "孙子小", "age": 22}
    ]
    session.run(query, {'values': values})

Cypher 查询语言

此处仅记录我个人认为常用或重要的部分,完整内容请参考官方文档

在 Cypher 中,用小括号来表示一个实体,用中括号来表示关系,这个是 Cypher 语言中最基础的表示了。

实体的各种表示方式如下:

  • 表示一个 Person 类型的实体,并记其名字为 a

    (a:Person)
    
  • 表示一个 id 值为 "person1" 的实体,并记其名字为 a

    (a {id:"person1"})
    
  • 表示任意一个实体,并记其名字为 a ,之后可以通过 WHERE 语句来对其进行约束

    (a)
    
  • 表示一个任意的匿名实体

    ()
    

关系的各种表示方式如下

  • 表示一个 actor 类型的实体,并记其名字为 r

    [r:actor]
    
  • 表示任意一个实体,并记其名字为 r

    [r]
    
  • 表示一个任意的匿名实体

    []
    

在上面的基础之上,即可方便地表示图数据中的一条实际的边,比如说

  • 表示命名为 m 的 Movie 类型实体到命名为 p 的 Person 类型实体、匿名的边

    (m:Movie)-[]->(p:Person)
    

    这里的 "->" 表示关系的方向是从 mp

  • 同上,但要求关系类型为 actor

    (m:Movie)-[:actor]->(p:Person)
    
  • 同上,并记关系的名字为 r

    (m:Movie)-[r:actor]->(p:Person)
    
  • 更复杂的表示:Person p 是 Movie m1 的主演,同时也是 Movie m2 的导演

    (m1:Movie)-[r1:actor]->(p:Person)<-[r2:director]-(m2:Movie)
    

掌握上述表示方法后,就可以用其来进行数据的创建、查询、修改和删除操作也就是俗称的 CRUD 了。

  • 查询实体

    MATCH (p:Person {name:"黄渤"}) RETURN p
    

    或者

    MATCH (p:Person) WHERE p.name="黄渤" RETURN p
    

    结果如下图所示

    neo4j_match_1.png

    当然也可以不带筛选条件

    MATCH (p:Person) RETURN p LIMIT 10
    

    neo4j_match_2.png

    (没错,我非常心机地把结果排成了整齐的两排哈哈)

  • 创建实体

    语法类似这样

    create (:Person {id:"ac1d6226", name:"王大锤"})
    
  • 修改实体

    MATCH (p:Person) WHERE p.id="ac1d6226" SET p.name="黄大锤"
    
  • 删除实体

    MATCH (p:Person) WHERE p.id="ac1d6226" DELETE p
    

    注意,删除实体时,如果这个实体还有和其他实体有关联关系,那么会无法删除,需要先将其关联关系解除才可以。

  • 查询关系

    查询 actor 类型的关系,不对起点、终点做任何约束

    MATCH (m)-[r:actor]->(p) RETURN * LIMIT 10
    

    结果如下图所示:

    neo4j_match_3.png

    查询 actor 类型的关系,对起点(或终点)做约束,比如说,查询主演是黄渤的所有电影

    MATCH (m:Movie)-[r:actor]->(p:Person) WHERE p.name="黄渤" RETURN *
    

    结果如下图所示:

    neo4j_match_4.png

  • 创建关系

    语法如下,要求涉及到的两个实体 ab 是已经存在的。

    MATCH (a:Person {id:"person_id_a"}), MATCH (b:Person {id:"person_id_b"})
    CREATE (a)-[:KNOWS]->(b)
    

    之前导入的豆瓣电影图谱其实缺少人和人之间的关系,比如说宁浩和黄渤彼此都认识,可以加上这个关系

    MATCH (a:Person), (b:Person) WHERE a.name="黄渤" and b.name="宁浩"
    CREATE (a)<-[:knows]->(b), (b)-[:knows]->(a)
    
  • 删除关系

    先用 MATCH 语句进行查询,并为其中的关系命名,然后在 DELETE 语句中用这个关系的名字即可。

    MATCH (a:Person)-[r:knows]-(b:Person) WHERE a.name="黄渤" and b.name="宁浩"
    DELETE r
    
  • 查询两个节点之间的最短路径

    查询黄渤和汤姆·克鲁斯之间的最短路径

    MATCH (a:Person), (b:Person), p=shortestpath((a)-[:actor*]-(b))
    WHERE a.name="黄渤" and b.name="汤姆·克鲁斯"
    RETURN p
    

    结果如下图所示:

    neo4j_match_5.png

CRUD 之外,索引的创建也是很重要的,如果没有创建索引或者索引设计有问题,那么可能会导致查询效率特别差。我最早开始用 Neo4j 的时候,在批量导入数据时没有建索引,导致不到五十万的数据量(包括实体和关系)的导入需要近一个小时,而在正确设置了索引之后,十几秒就完成了。对于比较慢的查询,可以用 PROFILE 语句来检查性能瓶颈。

以本文用来做示例的豆瓣电影图谱来说,如果没有给 Person.name 建立索引,那么下面这个查询语句就会很慢

MATCH (p:Person) where p.name="黄渤" RETURN p

PROFILE 语句做一下分析,只需要再原来的 query 前加上 PROFILE 这个关键词即可。

PROFILE MATCH (p:Person) where p.name="黄渤" RETURN p

分析结果如下图所示:

neo4j_profile_1.png

从上图来看,这个查询语句的逻辑是遍历了一下所有 Person 实体,挨个比较哪个实体的 name 是「黄渤」,这无疑是极其低效的。而在创建了索引后,PROFILE 的结果是下图这个样子:

neo4j_profile_2.png

关于索引可以展开更多内容,准备另外写一篇,这里只是强调一下 PROFILE 语句的作用。

2018 年总结与 2019 年展望

2019年1月1日 08:00

有三年没有写年末总结了,2018 年觉得似乎可以写一下。

2018 年总结

先定个性,2018 年是我工作以来「感觉」最累的一年。

工作

我有写日记和记工作日志的习惯,日记里多写一些心理、情绪感受,工作日志则主要记录工作事项的前因后果和过程。所以呢,日志基本能反映我这一年的心理状况,而工作日志则能反映我这一年来的工作状况。我过了一遍自己的工作日志,以下面的原则对一年的工作内容进行了一个统计:

  1. 若工作内容是某个算法功能的实现、优化,则统计为一项算法工作,并给一个类别
  2. 若工作内容是将已有的某项算法功能应用到某个业务上,则记为业务工作,不做细分
  3. 每项类别当一天中仅记一次
  4. 去掉最终统计次数为 1 类别

得到的统计结果是这样的

2018_job.png

可以看到三大块分别是:信息抽取、业务和知识图谱。业务部分占了 1/4 ,不过没什么好说的。知识图谱的部分分为图谱的构建和图谱的应用两块,前者其实就是从信息抽取中产生的数据进一步处理得到的,而信息抽取最初也是为了给知识图谱提供数据的,不过到后来慢慢延伸到了其他方面了。知识图谱早期的验证、实践的技术工作基本都是我做的,一开始的时候对于图谱应该是什么结构、该怎么用都是不清楚的,和产品的同事辛苦地讨论、修改,一步步才形成雏形。与此同时,要从大量的未结构化数据里抽取出结构化数据再建立成知识图谱,这个过程的初期则更加令人痛苦;而在这个过程建立起来后,为了修复各种各样的适配问题以及支持新增的抽取需求,仍然有大量的精力投入到这里面去。

知识图谱呢,至少推理问答的框架建立起来后,就是一些功能的增改的问题,倒还好。信息抽取和图谱构建这两个事情是我主要的压力来源,做过的人大概会明白的,这里就不想多说什么技术细节了。

另外一方面,上半年的时候,大量的工作内容都是围绕着 POC(Proof Of Concept) 做的,也就是根据「潜在客户」的需求做一个原型,给对方演示,以求打动对方得到签单的机会。本来呢,应该是做一个原型的,但为了给客户足够的吸引力,基本上我们就是当作正式项目在做,该有的功能都要有 —— 但是呢,POC 要么就是没有了后续消息,或者有消息了,还要进行招投标,最后才能签单,于是在比较长的一段时间内,你看不到工作的任何直接成果,到后来突然说要正式签单,我都已经麻木了,内心毫无波澜。进一步的,我们在做的东西,到底有多少社会价值这个问题也很难回答,to B 的业务是这样,双方先花个半年一年进行 POC、招标、二次招标,签订合同后开始接入,之后又要过挺长一段时间你才能知道效果怎么样,而像算法功能呢就更不好评估了 —— 大部分的算法效果其实都只能在一个标注数据集上做评估,而系统上线后的新的数据到底怎么样你其实是不知道的,只能去定期采样然后交给人工来评估,总之就是挺难受的。就算是真的效果好了,企业真的就需要一个对话机器人吗?要我说这并不是必要的,还是锦上添花的东西。从目前业界的特定领域对话机器人的普遍效果来看,我觉得都还不够,或许应该先将 NLP 技术用到更基础、更普遍的一些问题上去。

最后就是小公司的通病了,人力不足,杂事繁多。

从我个人的角度上来说,我很担心自己的一点是,我几乎没法从工作中获得正面的情绪反馈,那种碰到新技术的激动感、写完代码赞美自己「哇我写得真好」的骄傲感之类的,可以说几乎没有了。我不太喜欢这样的变化,如果说工作对我来说只剩下折磨,不如向窃格瓦拉看齐好了。

dagong.gif

生活

年初买了 Nintendo Switch,然后陆续玩了几款游戏,分别有:

  1. 塞尔达:荒野之息(救完公主索然无味)
  2. 空洞骑士(三周目!)
  3. 盐与避难所
  4. 光之子
  5. 星之卡比
  6. Gris

当然 Steam 上也买了一些游戏,但受限于电脑性能,多是一些体量较小的,玩的比较多的有

  1. FEZ(补票)
  2. 洞窟物语(补票)
  3. Reigns
  4. Pixel Dungeon:这个一直在刷,没有打穿

明年可能会考虑入 PS(想玩《赛博朋克2077》!),不过据说 2020 年会发 PS5,有点纠结,再说吧。

我玩游戏其实不太多,加上之前没有买过主机也没有一个较高配置的 PC,所以基本就是 Steam 玩玩较小的游戏和手机上玩一些,早几年的话一直在玩炉石,2018 年买了 NS 后才开始萌生玩一些稍大型游戏的想法,因为游戏实在是很好玩呀。个人会比较喜欢有在讲故事的游戏,竞技、对抗类的就不太感冒了,FPS 头晕,弹幕类游戏苦手(东方简直是恶梦),太肝的游戏玩不动,大概是这样吧。

游戏之外,影视作品(包括电影、动漫、电视剧等)看了一些,但并不多

  • 看过的电影里,国产的《爆裂无声》、《无名之辈》和《无双》都很不错,倒是国外的感觉都不咋样,《神奇动物2》本来挺期待的,也就那样吧
  • 动漫的话,有印象的也不多:一月的《刻刻》设定有意思,不过后期反派突然漂白有点意犹未尽;四月的《Lost Song》的叙述性诡计非常赞;沙雕的《千绪的通学路》和(并不沙雕的)《赌博默示录》外传《中间管理录利根川》都很轻松愉快;年末的《刀剑神域 Alicization》的故事是我认为刀剑里叙事最好的一部分了,女性角色偏少,设定、剧情都更为合理,而且是《加速世界》中核心设定的来源;《恶魔人》和《石头门0》都还没看就不发表看法,其他的就没什么感觉了,然后又把一些老番重看了一遍 ,有《XXXHOLIC》、《希德尼亚的骑士》、《灵能百分百》(要出第二季了!)等
  • 似乎没看什么剧,西部世界?

书就没看太多了,而且没怎么记录,回顾起来是个问题。话说我对有花大力气记自己的工作内容,但生活内容似乎没怎么记录,过个几年要想了解之前自己是怎么过日子怎么想的,可能会有点困难,从新年开始调整一下吧 —— 我可能很难做到很会生活,但好好记录自己的生活,还是不难做到的,下班后少管点工作的事情就是了。

现在住的地方周围没有方便的菜市场,厨房也很逼仄,所以2018 年完全没什么做饭的欲望,周末和工作日主要都是靠外卖和速食来解决的,靠着和同事互相交流经验倒是也收藏了好几家店铺,换着吃……好像有点惨,不过在公司里有一个吃委会,隔三岔五会出去大吃一顿,所以也还好。我其实还蛮喜欢吃的,不过我比较被动,就是那种如果有人号召我会积极参与但你要我主动去觅食有点困难那种……话说公司里的同事在吃这个事情上都有一些奇怪的设定,有喜欢吃各种土豆的、不吃葱蒜和猪肉的、不吃芒果的(我 OTZ)、不吃榴莲和香菜的(这个比较常见)……

再就是开始尝试理财吧,现在把钱分散到了货币基金、大平台的网贷以及指数基金上,主要的钱现在都在长赢计划里面,赌赌国运吧。过程中规中矩,倒没指望能赚大钱,就是开始了解一点金融的概念。比较意外的是,年初的时候,2013 年获赠的 Ripple 大涨就提现了一部分,白赚了一万软妹币,但是 7 月份的时候在一个网贷里雷掉一万,两相抵消,就当小小地感受了一下生活的刺激吧。

翻自己的日记都是在抱怨,这样回头一梳理事情还是蛮多的,可惜在日记里都没记。

可能没什么用的 2019 年计划

工作方面可能不会有什么计划,主要还是跟着公司业务走,我能做的打算是,尽量减少在工作时间之外处理公司事务的时间支出。再像2018 年这样,我担心会触及自己的心理承受能力上限,到时候损失最大的是我,我想我没有必要真的给公司卖命对吧。倒也不是说就和公司彻底划清界限,就是尝试提高自己的效率吧,在工作时间处理完重要的事情,一些小的、琐碎的、不那么重要的事情,没有必要那么及时响应,对个人对公司都没什么好处。

以及,我也不再奢望能在工作中学习、成长了,这方面还是得靠自己,为了达到这个目的,也需要更多的业余时间。这方面的具体内容需要动态调整,不可能现在就列出细则,不过大方向肯定是自然语言处理相关的,其他领域的可能也会涉及,随缘。

春节后可能就不再和合租了四年的同学一起合租了,希望能租一个一居室能自己安安静静地看书打游戏,不过北京的一居室房租真的好贵……纠结……

嗯……好像上述内容都不构成「计划」,更像是一种想法。按照经验的话,做太详细的计划往往会无法执行,而且在年初设想的过于具体的计划可能并不合理。我决定用 2018 年学到的一个工具 OKR(Objectives and Key Results)来写一下 2019 年的计划,这里只列出 Objectives,Key Results 的话则视情况而定再行制定,所以以下是我 2019 年的 Objectives:

  • 完善自己的 NLP 工具箱,对于大部分 NLP 问题,可以快速寻找到工具来解决
  • 实践并完善自己的学习、知识体系,并用之掌握两到三个领域的基础知识

    这方面 2018 年有做一些,目前把 org-mode 的笔记体系和 anki 结合起来了,但利用得不够充分。

  • 使用日记及其他工具来更充分地记录自己的生活

    2018 年有用 Life Cycle 来自动跟踪粗粒度的事件,还蛮不错的,2019 年会继续用下去。然后就是前面提到的游戏、阅读、观影这些东西希望也能好好记录下。

  • 读更多的小说,看更多的动漫,玩更多的游戏,收获更多的快乐

新的一年,重新出发~

论文笔记: ReDecode Framework for Iterative Improvement in Paraphrase Generation

2018年11月24日 08:00

作者

  • Milan Aggarwal
  • Nupur Kumari
  • Ayush Bansal
  • Balaji Krishnamurthyk

观点与事实

观点

  • 当前的一些序列生成方法都是一锤子买卖,在生成的时候改正错误的能力太差,生成之后也不能去改进生成质量
  • 人类对话中用复杂的复述来表达相同意图的行为很普遍,但对机器来说要辨别或生成复述句是很困难的
  • VAE 学习的是一个概率分布,所以用于生成式的任务很合适

相关工作

  • 1983. Paraphrasing questions using givenand new information
  • 2003. Learning to paraphrase: anunsupervised approach using multiple-sequence alignment
  • 2004. Monolingualmachine translation for paraphrase generation. Associationfor Computational Linguistics.
  • 2004. Synonymous para-phrasing using wordnet and internet
  • 2016. Neural paraphrase genera-tion with stacked residual lstm networks
  • 2017. Learn-ing paraphrastic sentence embeddings from back-translatedbitext
  • 2017. Joint copying andrestricted generation for paraphrase
  • 2017. Learning to paraphrase for question answering
  • 2017. Adeep generative framework for paraphrase generation
  • 2018. Adversarial example generation with syn-tactically controlled paraphrase networks

数据集

论文里用到了两个数据集。

第一个是 Quora 的相似问题数据集,这个没啥好说的。

第二个是微软的 MSCOCO 数据集,这个其实是一个 Image Caption 的数据集,但是因为这个数据集里每张图片都标注了五个标题,可以认为这个五个标题互为复述句。

模型/方法/结论

模型结构如下图所示

redecode.png

模型又三部分组成,分别是

  • Sampling Encoder

    这个 Encoder 用来根据输入句子 \(x_{o}\) 产生一个概率分布,然后从 x 中进行采样得到一个 latent vector \(z\)

    在这篇论文里,Sampling Encoder 由一个单层的 LSTM 和两个全连接层构成。

    首先输入句子 \(x_{o}\) 经过 LSTM 后得到整个句子的向量表示 \(v_{o}\),然后分别输入一个全连接层,得到期望概率分布的均值 \(\mu\) 和方差 \(\sum\),用这个均值和方差就得到了一个正态分布 \(N(\mu, \sum)\),从这个概率分布中采样得到最终需要的 \(z\)。

    为了让最终模型的输出能更有多样性,在训练的时候,作者对 \(\mu\) 和 \(\sum\) 施加高斯噪声扰动来得到不同的 \(z\)。

  • Sentence Encoder

    这个 Encoder 用来对得到输入句子 x 的语义表示,用来作为 decoder 的语义输入。

    这个 Encoder 就比较简单了,一个两层的 LSTM,输出 hidden state 序列 \(H={h_{1}^{o}, h_{2}^{o}...h_{n}^{o}}\),并在 Decoder 中对其进行 attention 计算。

  • Sequencd Decoders

    这个是最终用来生成的复述句的,也是整篇论文中的要点。

    和常规的一个 decoder 做法不一样的是,作者认为生成的部分,应该用多个 decoder 来迭代地生成结果。具体来说,这篇论文的做法是这样的

    • 第一个 decoder 以 Sentence Encoder 输出的 hidden state 向量 \(H\) 和 Sampling Encoder 输出的 latent vector \(z\) 作为输入

      \(p_{1}=Dec_{\phi_{1}}(z, Attn(H))\)

    • 随后的 decoder 则以前一个 decoder 的输出和 Sampling Encoder 输出的 \(z\) 作为输入

      \(p_{i}=Dec_{\phi_{i}}(z, Attn({s_{Dec_{\phi_{i-1}}}}))\)

下图是该模型在两个数据集上的效果。 redecode_eval.png

以及一些输出示例

redecode_examples.png

总结

总的来说,模型并不算很复杂,也很简单易懂。

VAE 的部分我其实并不太熟悉,需要去补课。至于 Decoder 的部分,这种迭代生成的路子,其实早就见过了,比如说 HMN 模型,以及一些在 attention 机制上的改进工作之类的。

论文笔记:Word Embedding based Edit Distance

2018年11月17日 08:00

作者

Yilin Niu, Chao Qiao, Hang Li, and Minlie Huang

看这篇论文才知道李航去今日头条 AI 实验室了……

观点

  • 近年来深度学习被广泛应用到文本相似计算中,但这些方法一般都需要在标注数据上做有监督训练,而标注数据的收集、构建的代码是很高的
  • 在特定条件下,WED 可以退化为编辑距离或者 jaccard 系数的变种

    One can easily verify that WED degenerates to ED and a variant of Jaccard under certain condition.
    

    不过论文并没有细说这些方法之间是怎么转换的。

数据集

  • Quora: The dataset released from Quora contains 400k question pairs in community questionanswering labeled as matched or not-matched, 40w 样本
  • MSRP: The dataset for paraphrase detection released from Microsoft Research, 5.8k 句子对
  • CPC: The dataset referred to as Crowdsourced Paraphrase Collection (CPC), 2.6k 对复述句子

模型/方法/结论

记 \(S_{A}=(w_{A}^{1}, w_{A}^{2}, ..., w_{A}^{l_{A}})\) 和 \(S_{B}=(w_{B}^{1}, w_{B}^{2}, ..., w_{B}^{l_{B}})\) 为两个句子,其中 \(w_{A}^{i}\) 表示句子 \(S_{A}\) 中第 \(i\) 个词,\(w_{B}^{j}\) 表示句子 \(S_{B}\) 中第 \(j\) 个词。

在计算编辑距离时,以 \(S_{A}\) 为基准句子,计算 \(S_{B}\) 相对 \(S_{A}\) 的编辑距离,即认为 \(S_{B}\) 是 \(S_{A}\) 经过删除、插入、替换后得到的新的句子。

然后定义 \(c_{i,j}\) 为 \((w_{A}^{1}, w_{A}^{2}, ..., w_{A}^{i})\) 经过编辑变换成 \((w_{B}^{1}, w_{B}^{2}, ..., w_{B}^{j})\) 所需的最小操作代价。

那么就有

\[\begin{eqnarray}c_{i,j} = \begin{cases} c_{i,j-1}+i(w_{B}^{j}) \\ c_{i-1,j}+d(w_{A}^{i})\\ c_{i-1,j-1}+s(w_{A}^{i}, w_{B}^{j}) \end{cases}\end{eqnarray}\]

其中 \(i(w_{B}^{j})\) 表示插入 \(w_{B}^{j}\) 的代价;\(d(w_{A}^{i})\) 表示删除 \(w_{A}^{i}\) 的代码。这两者在传统的编辑距离里往往值是 1,在生物信息学的序列对比上,删除的代码可能会再适当地高一点。

\(s(w_{A}^{i}, w_{B}^{j})\) 则表示将 \(w_{A}^{i}\) 替换为 \(w_{B}^{j}\) 的代价,当两者想等时代码就是 0 ,当两者不相等时这个代价就要大于 0。替换操作在不同的地方使用的代价会不一样,有些地方当作和插入、删除一样,即 1,本文的方法则是将替换视作先删除后插入两次操作,所以代价是 2。

所谓的结合 embedding,就是对这三个代价函数做了修改,使之利用词的 embedding 信息。

对插入操作,定义代价为:

\[i(w_{B}^{j})=1 - \lambda \cdot max_{w_{A}^{k}\in S_{A}, w_{A}^{k} \neq w_{A}^{i}} (sim(w_{A}^{k}, w_{B}^{j}) + \mu)\]

对删除操作,定义代价为:

\[d(w_{A}^{i})=1 - \lambda \cdot max_{w_{B}^{k}\in S_{B}, w_{B}^{k} \neq w_{B}^{j}} (sim(w_{A}^{i}, w_{B}^{k}) + \mu)\]

对替换操作,定义代价为:

\[s(w_{A}^{i}, w_{B}^{j})=2 - 2 sim(w_{A}^{i}, w_{B}^{j})\]

第三个替换操作的定义很好理解,但是前面两个感觉有点疑惑呀……

能想到的解释是这样的

  • 当插入新的词时,如果词和句子 A 中的词有相似的,那么对语义的影响就不是那么大
  • 当删除句子 A 中的词时,如果句子 B 中还有词和这个删除的词相似,那么对语义的影响也不会那么大

但是 LCS 的好处在于每个词是一一对应的,而这里在计算插入和删除的时候并没有保证有序的一一对应关系呀。

和其他方法的对比

  • ED: 经典编辑距离
  • TF-IDF: 基于 tfidf 的 cosine 相似
  • Embedding: 基于 embedding 的 cosine 相似
  • Jaccard: jaccard 系数
  • Autoencoder: 这个应该是指语言模型一类的,但是为什么引用的是 Hinton 2006 年的论文……
  • BOW+MLP: 有监督方法,给定两个句子,先将句子中的 embedding 加和,然后拼接起来输入到 MLP

    分两种情况:一层 MLP 和三层 MLP

  • LSTM+MLP: 用 LSTM 编码句子,然后拼接起来输入到 MLP

    分两种情况:一层 MLP 和三层 MLP

对比结果如下图所示:

wed_results.png

结论:

  • 本文的方法要优于这几种无监督方法:编辑距离、基于TFIDF的 cosine、基于 word embedding 的 cosine 方法、jaccard 系数
  • 对比两个有监督方法(BOW/LSTM+3层MLP),发现大概在 30k 这个数据量以上,有监督的方法才会显著好于 WED 方法
  • 在 Quora 数据集上做了一些采样来分析错误,发现超过 50% 的错误在于没有区分开关键词和非关键词,所以如果有机制能给关键词更高的权重,那么 WED 应该还会有更高的效果 —— 比如说用 TF/IDF 来给每个词加权?

    嗯,没有给一些例子呀……

相关工作

  • Learning semantic representations using convolutional neural networks for web search, 2014
  • Convolutional neural network architectures for matching natural language sentences, 2014
  • Text matching as image recognition, 2016
  • A deep architecture for semantic matching with multiple positional sentence representations, 2016
  • A decomposable attention model for natural language inference, 2016
  • 编辑距离:Navarro 2001, Jurafsky and Martin 2009
  • jaccard 系数:Tan et al 2005
  • 基于 word embedding 的 cosine 相似:Mitchell and Lapata 2008, Milajevs et al 2014
  • 基于 TFIDF 的 cosine 相似:Buckley, 1988

概念和术语

  • WED: Word Embedding based Edit Distance 缩写(不应该是 WEED 吗)

论文笔记:Visualizing and understanding recurrent networks

2018年11月10日 08:00

作者

  • Andrej Karpathy
  • Justin Johnson
  • Li Fei-Fei

观点

  • LSTM 在实践中表现出了非常好的结果,但我们对其性能的来源和限制理解地都还很不够
  • 过去的一些分析都是靠最终测试集上的全局困惑度来评价 LSTM 的效果,并没有在「真实数据」上进行分析,也不够直观

数据集

  • 托尔斯泰的《战争与和平》文本,共 3,258,246 字
  • Linux 内核代码,共 6,206,996 字

模型/实验/结论

模型:

  • RNN,分别尝试层数为 1 层、2 层和 3 层,隐层大小分别尝试 64, 128, 256 和 512,共 12 个模型
  • LSTM,同 RNN
  • GRU,同 RNN

实验和结论

  • 用上述模型在两个数据集上训练语言模型,最后在测试集上计算交叉熵误差,对比三类共 36 个模型之间的结果
  • 对 LSTM/GRU 的 gate 输出分布做可视化分析。如下图所示,图中的小圆圈代表一个神经元,横轴表示该神经元 gate 值超过 0.9 的比例,纵轴是 gate 值小于 0.1 的比例

    lstm_gru_saturation.png

    其中

    • forget gate 值超过 0.9 的比例很大的神经元,说明它能一直记住比较早之前的信息
    • input gate 值超过 0.9 的比例很大的神经元,说明它对当前输入比较敏感
    • output gate 的值超过 0.9 的比例很大的神经元,没什么意义,单纯的控制大小

    对 LSTM 而言

    • 第一层只有很少一部分 gate 值超过 0.9 或小于 0.1,其比例比较密集地分布在 0 附近,说明大部分都是在 0.1 到 0.9 之间
    • 有一些神经元 forget gate 值超过 0.9 的比例超级大,也就是说它一直都超过 0.9,一直记着很早以前的东西
    • 有一些神经元 forget gate 的值小于 0.1 的比例很大,但没有一直都小于 0.1 的,比例最大在 0.8 左右
    • input gate 倾向于超过 0.9(相对比例大的神经元更多), output gate 的值分布比较均匀

    对 GRU 来说

    • 第一层的 update gate 普遍比较大而 reset gate 普遍比较小,注意本文中的 update gate 相当于 LSTM 中的 input gate
    • 高层不像第一层那么极端,但总体形式差不多,就是 update gate 大而 reset gate 小这样
  • 分析了 LSTM 在《战争与和平》文本上的错误类型

    • ngram 错误,1-9 阶 ngram 模型能预测正确但 LSTM 预测失败的部分
    • 罕见词错误: 既由词频不大于 5 的词导致的错误。这部分错误通过扩充数据集和 pretraining 是可以得到缓解的
    • 词建模错误: 在遇到空格、换行、引号等词分隔符后,预测错了下一个词的第一个字符,和前面个动态长期记忆错误不一样的是,这个相当于在前面的词的基础上要选择一个词,而前面那个相当于是已经知道是什么词了,但是要补全它,这两者的信息量是完全不一样的
    • 标点符号预测错
    • 最后剩余的错误称为 boost error,

结论

  • 多个隐藏层模型比单个隐藏层模型的效果要好
  • LSTM 和 GRU 之间难分伯仲,但都显著好于 RNN
  • LSTM 表现出了对长程结构的记忆能力,如在处理被引号括起来的长文本时,对开头和结尾的引号有特殊的响应
  • 在多层的 LSTM/GRU 中,高层的神经元都开始分化,会有一部分倾向于接收新信息,有一部分则倾向于记住旧的信息
  • GRU 的第一层几乎不怎么使用旧的信息,即使到高层后也更倾向于使用当前输入
  • LSTM 建模长程依赖的能力大大超过 ngram 模型,一个 11MB 的 LSTM 模型效果能略微超过一个 3GB 的 20-gram 模型
  • 对本身明显包含结构的文本(如内核代码)进行建模,当序列长度在 10 以下时,LSTM 和 20-gram 模型的差异不大,但随着序列变长,两者之间的差距逐渐变大,在 Linux 内核代码上,LSTM 能记忆的最大约 70 左右的距离
  • LSTM 在迭代训练的过程中,首先建模了短程依赖,然后在此基础上逐渐地学习到对长程依赖的建模能力,这也是 seq2seq 论文中提到的逆序源语言序列有效的原因,因为这让模型先开始建模短程依赖再去建模长程依赖
  • LSTM 并不能完全利用好最近的一些输入,LSTM 的错误中,有 42% 是 0-9 阶的 ngram 模型能够正确预测的
  • 相信像 Memory Networks 那样,如果能直接对序列中最近的历史进行 attention 操作,那么能够提高 RNNLM 的效果
  • 增大数据集、无监督预训练能提高 LSTM LM 对罕见词的效果
  • 增大模型大小,显著减小了 ngram 错误,但对其他类型的错误却没有明显的改善,这说明光是增大模型大小是不够的,可能需要设计更好、更新的结构

论文笔记:Visualization Analysis for Recurrent Networks

2018年11月10日 08:00

简介

这篇论文是清华大学语音和语言技术中心(Center for Speech and Language Technologies, CSLT)2016 年的一篇论文,不过奇怪的是在 Google Scholar 上找了一圈,给出的引用链接竟然是 CSLT 的 wiki 链接,难道是没有正式发表的一篇论文吗?

这篇论文主要从从近年来 LSTM 在 ASR 中的优异表现出发,用一些可视化方法,希望能深入理解这种有效性是如何产生的,非常有意思。这方面相关的工作有

  1. 2013 年的《Training and analysing deep recurrent neural networks》
  2. 2014 年的《Empirical evaluation of gated recurrent neural networks on sequence modeling》
  3. 2015 年的《LSTM: A search space odyssey》
  4. 2015 年 Andrej Karpathy 的《Visualizing and understanding recurrent networks》
  5. 2015 年的《Visualizing and understanding neural models in nlp》
  6. 2016 年的《Representation of linguistic form and function in recurrent neural networks》
  7. 2016 年的《Simplifying long short-term memory acoustic models for fast training and decoding》

后续这些论文的笔记我也会补充上来。

实验和结论

首先论文用 LSTM 和 GRU 分别在华尔街日报(Wall Street Journal, WSJ)数据集上进行对比,结果显示在 ASR 任务上,GRU 的效果要比 LSTM 稍好,至于为什么会更好,则在后面的实验中进行了可能的探讨。

Activation patterns

接着,在已经训练好的 ASR 模型上,输入 500 条音频数据,然后在网络的各层中随机挑选 50 个神经元,观察这些神经元的 cell value 的分布情况。

在基于 LSTM 的模型上,1-4 层的 cell value 分布情况如下图所示:

lstm_activation_distribution.png

可以看到,在 LSTM 上,cell value 基本上分布在 [-10, 10] 这个区间,且在 0 附近比较密集,而这种密集程度在更高的层级上慢慢降低。

而 GRU 的情况则很是不同,cell value 分布在 [-1,1] 这个区间中,然后在边界(即-1 和 1处)上比较密集,并且随层级变高而更加集中。如下图所示:

gru_activation_distribution.png

通过下面的图则能更直接地看到,LSTM 中的大部分 cell value 都在 (-2, 2) 区间上,而 GRU 中大部分 cell value 都在 [-1, -0.7] 和 [0.7, 1] 中。

activation_percentage.png

当然 GRU 的 cell value 分布在 [-1, 1] 这个区间是可以直接通过 GRU 的计算式推导出来的,只要 cell value 的初始值是 [-1, 1] 内的值,后面 cell value 的新值都会严格地被限制在 [-1, 1] 这个区间。

论文对 LSTM 和 GRU在这点上的差异,作出如下结论:

  1. LSTM relies on greatpositive or negative cell values of some units to represent information.
  2. GRU relies on the contrast among cell values of different units to encode information.
  3. This difference in activationpatterns suggests that information in GRU is more distributed than in LSTM.
  4. this may lead to a more compact model with a better parameter sharing(in GRU)

Neuronal Responsibility

第二个可视化实验稍微复杂一些。首先要为一个神经元定义一个叫做 responsibility 的东西,这个 responsibility 指一个神经元对一个音素的 cell value 是 irregular 的概率,而所谓 irregular ,在 LSTM 上是至 cell value 超出 (-10, 10) 这个区间,在 GRU 上则指 cell value 包含在区间 (-0.5, 0.5) 之中。刚才的实验也说明了,神经元的 cell value,大部分都是 regular(和刚才的 irregular 对应)的,那么如果某个神经元对某个音素经常产生 irregular 的响应,这就说明这个神经元对这个音素很敏感,也就是说,它能识别出这个音素来。这个实验就是以此为出发点来进行的。

在有上述定义后,统计 LSTM 各层对各个音素的 responsibility 超过 80% 的神经元数量,如下图所示:

lstm_responsibility.png

统计 GRU 各层对各个音素的 responsibility 超过 50% 的 unit 数量,如下图所示。

gru_responsibility.png

首先,两者都有一个共性,那就是同一层上,对某个音素的 responsibility 超过 80% 的神经元数量基本一样;区别是,在 LSTM 上,随着层数变高,对音素的 responsibility 超过 80% 的神经元总体呈现变多的趋势,而且会对某些个特别的音素的特别多;而 GRU 上层数变高,responsibility 超过 80% 的神经元在变少,也有对某些音素 responsibility 超过 80% 的神经元,但相对少一些。

LSTM 在高层倾向于识别更多的音素,而 GRU 则相反倾向于只对少数音素作出响应。这种差异作何解释有待进一步讨论。

就这个实验的出发点来说,在我看起来,应该是可以用统计物理学上的能量函数来作解释。

Temporal Trace

第三个可视化实验也挺有意思,它将基于 LSTM 或 GRU 的 ASR 模型识别一条音频数据时的 cell vector 的连续变化情况记录下来,并用 t-SNE 降维后做可视化。结果如下图所示:

temporal_trace.png

可以看到 LSTM 再次和 GRU 产生了区别。LSTM 的每层的轨迹大体上都是连续平滑的,论文里的解释是说 LSTM 能记得更多的东西,因此当前的输出会很大地受到过去记忆的影响,被平均后就变得平滑;而与此对比的 GRU,则倾向于给予新的数据更大的权重,换言之遗忘地会更快,因此在前几层的时候,轨迹变化地会更剧烈一些。

另外一个不同是,GRU 的轨迹在高层后慢慢地也变得连续了,这就说明 GRU 的底层是倾向于记忆新的输入,但在高层则倾向于把旧的记忆也融合进来。这就很有意思了。

Memory robustness

这个实验是为了比较 LSTM 和 GRU 的抗噪能力,方法也很简单,就是在输入数据的前部插入一些噪声数据,然后和正常的数据对比,观察网络中神经元的 cell value 的差异,结果如下图所示:

noise_influence.png

可以看到 LSTM 中噪声的影响会长时间地持续下去,而在 GRU 中这种噪声的影响会比较快地被消除掉。也就是说 GRU 更鲁棒,从另外一方面来说,也可以认为 GRU 能记忆的东西更少,倾向于记忆新的输入,因而遗忘地更快,于是噪声数据产生的干扰就在后面被抹除了。

Application to structure design

在论文的最后,作者根据前几个实验得到的结论,尝试对 LSTM 作出一些调整。第一个调整是把 cell state 的更新操作放到计算输出之后,像 GRU 一样,并称这个特性为「lazy cell update」。结果显示这样修改后,LSTM 在 WSJ 数据集上的效果稍有提升并和 GRU 接近了。

lstm_gru_improve.png

有意思的一个现象是在这样修改后,LSTM 高层的 temporal trace 变得更碎更不平滑了,接近 GRU 的情况。这就说明前面的一些差异,很可能是和 LSTM 与 GRU 更新 cell state 的顺序差异造成的,从计算式上来看似乎也说得通。

此外作者还尝试在 LSTM/GRU 上加上 residual learning,效果也稍有提升。但是这个我持保留态度。

此外比较遗憾的一点是,作者虽然尝试修改 LSTM 让 cell state 的更新延后,却没有尝试对 GRU 作出相反的修改也就是让 GRU 的 cell state 的更新提前。我的想法是这样修改后再去做对比,如果得到的结论和修改 LSTM 的结论一致,就更能说明问题了。

论文笔记:Sequence to Sequence Learning with Neural Networks

2018年11月10日 08:00

三位来自 Google 的作者在这篇论文中提出了一种以两个 RNN 组合方式构成的网络结构,用来处理英语到法语的翻译问题,并且认为对于传统的深度神经网络(Deep Neural Network, DNN)不能处理的输入和输出都是变长序列的问题,这种模型都能很好地进行处理。

自 2006 年 Hinton 掀起深度学习这股浪潮,联结主义强势回归到现在已经有 10 年了,这 10 年里,深度学习已经在图像和语音两大领域取得了卓越的成就。然而大部分的深度神经网络仍然需要输入和输出的大小固定不变,并将一层一层不同类型的网络层组合在一起,通过巨量的数据、更有效的优化方法和训练技巧来达到目的,而这里的 "输入和输出的大小固定不变" 是一个很大的限制,比如说我们的语言、文字,天然就是变长的序列,这些网络结构要处理的话就必须要在核心模型之外在加上一些额外的处理。举个例子,图像识别中的图像一般也不是固定大小的,输入前一般都需要进行预处理将图像规整到同一尺寸。

面对这一问题,一个很自然的方法就是使用循环神经网络(Recurrent Neural Network, RNN),这是一种在空间结构上非常简单的模型,在一些不太复杂的问题上,甚至只需要三层结构就足够了,如下图所示(配图来自 Elman 的 《Find Structure in Time》)

simple_rnn.png

相比传统的前馈神经网络(Feedforward Neural Network, FNN),RNN 的独特之处在于其隐藏层的一个环状结构,这个结构相当于能够缓存当前的输入,并用之参与下一次的计算,这样就隐式地将 时间信息 包含到模型中去了,在输入变长序列时,可以序列中的最小单元逐个输入。RNN 虽然在空间结构上可以很简单,但在进行训练时通常需要在 时间维度 上展开(unroll),所以可以认为它是一个在时间维度上的 DNN,于是 DNN 训练中会出现的 gradient vanish (梯度消失)也会出现,直观上可以将其理解为 "记忆的衰退",换句话说, RNN 只能 "记住" 短期的信息。1997 年 "长短期记忆单元(Long Short-Term Memory, LSTM)" 被提出来解决这个问题,而本文提出的模型就是利用了 LSTM 的优点。

虽说 RNN 能用于处理变长序列,但具体要怎么做呢?最早提出 RNN 之一的 Elman 做过的一些实验是这样的: 将序列中的元素逐个输入网络,并预测下一时刻的输入,比如一条句子逐个字符输入网络,并在输入第 n 个字符时预测第 n+1 个字符。这种方式虽然能处理变长的输入序列,但得到的输出序列却是和输入序列长度一样的,限制仍然还在。

一种办法是在输出时增加一个 "空白" 的输出候选,然后在每次输出时取每一种可能输出结果的概率,得到一张路径网络后用类似 beam search 的方法来组装起真正的输出,由于 "空白" 输出的存在,最后得到的非空白输出序列的长度就变成可变的了。语音识别和一些 OCR、手写识别是这么做的,效果也还不错。

而作者提出的方法是将两个 RNN 组合起来,以更加灵活地处理变长输入序列和变长输出序列。其模型结构如下(注意,这是一个已经 unroll 的网络结构)

seq2seq.png

模型的左侧(到输入为 <EOS> 为止)是一个 RNN 在输入序列 "ABC<EOS>" 上的展开,右侧是在输出序列 "wxyz<EOS>" 上的展开,其中 <EOS> 是一个表示序列结束的特殊符号。功能上,第一个 RNN 用来将输入序列映射成一个固定长度的向量,这个 "固定长度的向量" 即是 RNN 中间隐藏层所缓存的对整个输入序列的 "记忆",我们可以说它表示了输入序列的语义;然后用第二个 RNN ,来从这个向量中得到期望的输出序列。

除了这个特殊的模型结构之外,再就是用 LSTM 来保留 一定程度 的长期记忆信息,并且作者表示复杂的网络结构(更多的参数)具有更强的表达能力,因此每个 RNN 用的都是 4 层的 LSTM, 参数两多达 380M, 也就是 38 亿 —— Google 的朋友们你们真是站着说话不腰疼啊,38 亿参数的模型,一般人哪来这么多数据喂饱这个大胃王,哪来那么强劲的机器来训练……

再就是,作者说将输入序列倒序后,效果得到了显著地改善,BLEU 从使用该方法之前的 25.9 上升到 30.6,然而自己也对其原因表示不太清楚,只作出了一些猜想(也就是说,并无明确的理论依据)。

原文中,作者对这个技巧的解释如下

While we do not have a complete explanation to this phenomenon, we believe that it is caused by the introduction of many short term dependencies to the dataset

以及

By reversing the words in the source sentence, the average distance between corresponding words in the source and target language is unchanged. However, the first few words in the source language are now very close to the first few words in the target language, so the problem's minimal time lag is greatly reduced.

模型训练时,以最大化条件概率为目标,也就是说,其目标函数为

\[\frac{1}{|S|}\sum \log P(T|S)\]

而在模型训练好后,用于实际的预测时,则也采用了简单的 beam search 方法,即在模型参数确定的情况下,对输入序列 \(S\) ,按下面的式子求解输出序列

\[\hat{T} = \arg\max_{T}P(T|S)\]

下表是与其他模型在机器翻上的的效果对比,其中上面两行是其他模型的效果,下面六行是作者模型在不同参数设置时的结果。

seq2seq_model_comparision.png

此外作者还尝试将自己的模型与传统的 SMT 系统进行结合,效果显著, BLEU 最好的达到了 37,超过 baseline system 4 个点。

seq2seq_model_comparision2.png

sequence to sequence 模型被提出后,由于其灵活性,受到了广泛的关注,我个人是很喜欢这个模型中的想法的。然而现在流行的几个开源库对 sequence to sequence 模型的支持仍然不太理想,它们都要求在模型定义时就将输入序列的最大长度和输出序列的最大长度确定,对于长度不足的,则要用特殊符号进行填充,并在模型内部或外部做一些特殊处理。比如用 Python 的深度学习框架 Keras 来实现一个弱化版的 sequence to sequence 模型,可以这样:

# coding: utf-8
"""Sequence to Sequence with Keras 1.0"""

from keras.models import Sequential
from keras.layers.core import Dense, RepeatVector
from keras.layers.recurrent import LSTM
from keras.layers.wrappers import TimeDistributed

def build_model(input_size, max_output_seq_len, hidden_size):
    """建立一个 sequence to sequence 模型"""
    model = Sequential()
    model.add(LSTM(input_dim=input_size, output_dim=hidden_size, return_sequences=False))
    model.add(Dense(hidden_size, activation="relu"))
    # 下面这里将输入序列的向量表示复制 max_output_seq_len 份作为第二个 LSTM 的输入序列
    model.add(RepeatVector(max_output_seq_len))
    model.add(LSTM(hidden_size, return_sequences=True))
    model.add(TimeDistributed(Dense(output_dim=input_size, activation="linear")))
    model.compile(loss="mse", optimizer='adam')

    return model

上面这段代码,从模型定义上,只对输出序列做了最大限制,但训练数据集中 不允许出现不同长度的输入序列 ,实际上不同长度的目标输出序列也不被允许。希望这种情况在将来能够有所改善,当然啦,不行的话可以自己用 Theano 写嘛~

论文笔记:Learning Phrase Representation using RNN Encode-Decoder for Statistical Machine Translation

2018年11月10日 08:00

作者在这篇论文中提出了一种新的模型,并用来进行机器翻译和比较不同语言的短语/词组(phrase)之间的语义近似程度。这个模型由两个 RNN 组成,其中一个(Encoder)用来将输入的序列表示成一个固定长度的向量,另一个(Decoder)则使用这个向量重建出目标序列 —— 在机器翻译任务里,可以认为 Encoder 产生的向量是输入文本的 语义表达 ,而 Decoder 则根据这个语义表达产生目标语言的文本。

模型结构如下图所示

encoder_decoder.png

这个结构其实和 "Sequence to Sequence Learning with Neural Networks" (后面简称 "Sequence to Sequence")里提出的结构是非常相似的,见下图:

seq2seq.png

"Sequence to Sequence" 这篇论文的作者自己也表示 "Our approach is very similar to Cho et al" 。

两种结构的不同之处在于,Encoder 输出的向量,在后者这里只用来作 Decoder 的第一个 time step 的输入,而在 Cho 的这篇论文里,它是要在 Decoder 的每一个 time step 中都参与计算的。即在 "Sequence to Sequence" 中,Decoder 中第 t 个 time step 时计算输出的式子为

\[P(y_{t})=f(h_{t}, y_{t-1})\]

而在 Cho 的论文中,则是

\[P(y_{t})=f(h_{t}, y_{t-1}, c)\]

其中 \(h_{t}\) 表示第 t 个 time step 时 Decoder 的内部状态(RNN 单元的 "memory"),\(c\) 表示 Encoder 输出的向量。

再有一点不同是,Cho 在这篇论文中提出了 GRU 这种 LSTM 的简化版本单元结构,使用的 RNN 也是以 GRU 而非 LSTM 组成的。对 LSTM、GRU 的细节这里就不深入了,反正 Cho 自己还有一篇论文就是比较 LSTM 和 GRU 的各自优缺点的,到时候也读一读记录一下笔记什么的。

Cho 的 Encoder-Decoder 模型中,Encoder 的行为比较简单,重点在 Decoder 上。

Decoder 中 t 时刻的内部状态的 \(h_{t}\) 为:

\[h_{t}=g(h_{t-1}, y_{t-1}, c)\]

该时刻的输出概率则为:

\[P(y_{t}|y_{t-1}, y_{t-2},...,y_{1}, c)=f(h_{t}, y_{t-1}, c)\]

模型训练时则去最大化给定输入序列 x 时输出序列为 y 的条件概率:

\[\arg\max_{\theta}\frac{1}{N}\sum_{n=1}^{N}logP_{\theta}(Y_{n}|X_{n})\]

这个模型用来做机器翻译是很自然的,除了机器翻译外,作者还用这个模型来得到英语 phrase 和法语 phrase 之间的语义近似值(或说相似性),方法是用大量的英语和法语的 phrase pair 进行训练,每个 pair 在训练时最后都会有概率输出,就用这个概率值作为这个 pair 对的 score ,并在整个训练过程中更新,最后得到一张记录很多 phare pair 近似值的表,这个表可以在其他的统计机器翻译(Statistical Machine Translation, SMT)系统中使用 —— 比如要将某个英语 phrase 翻译成法语 phrase 时,可以从表中找到与该英语 phrase 最接近的 K 个法语 phrase 作为候选。

在机器翻译上,作者用 Moses (一个 SMT 系统) 建立了一个 phrase based 的翻译模型作为 baseline system ,然后对比了以下四个模型的 BLEU 值

  1. Baseline configuration
  2. Baseline + RNN
  3. Baseline + CSLM + RNN
  4. Baseline + CSLM + RNN + Word penalty

所谓 BLEU 呢,是 BiLingual Evaluation Understudy 的缩写,是业界用来评估一个机器翻译系统水平的基准,如果遇到做机器翻译的上去问问 “你们的系统 BLEU 是多少” ,绝对会被认为你是专业人士,所以不妨记一下这个词 :) 。不过实际上 BLEU 和人工评价相差甚远,而且对于一些小的错误非常敏感,Yvette Graham 等人在 NAACL 2016 上专门有一篇长论文(Achieving Accurate conclusions in Evaluation of Automatic Machine Translation Metrics)讨论这个事情,这篇论文受到了很大的重视。

回归正题,四种不同的模型的 BLEU 值如下表所示:

bleu_of_models.png

其实也不见得比传统方法要好多少。当然据我所知, Encoder-Decoder 这个模型还是挺受重视的,如果我没记错的话,上次在 QCon 上看到阿里的跨境电商翻译系统应该是用上了 Encoder-Decoder 结构(不想误导,他们还有专业团队进行人工翻译)。

phrase pair 打分的结果如下:

pair_score.png

其中第一栏是输入的英语 phrase ,第二栏是用传统的模型得到的最近似的三个法语 phrase,第三栏是用 Encoder-Decoder 模型得到的最近似的三个 phrase。

就我对这篇论文的理解来说,应该是这样的。结尾再对计算两个序列之间的相似性这件事情说说自己的想法,在这件事情上,作者只是在训练过程中得到这样一个额外的、静态的结果,最后得到的那张表,其中包含的 phrase 是确定的、有限的,姑且认为其中的 phrase 都是粒度比 sentence 更细的合法单元,但实际上可以把 Decoder 的输出看成一个搜索空间,在给定 任意 输入序列的时候,用输出序列在这个空间里确定搜索路径,这条路径上每一条边都是有置信度(概率)的,这样就可以直接得到一个条件概率来表征连个序列之间的相似程度,好处是可以处理任意的 sequence pair,这对实际问题应该会有帮助。当然这个想法可行性有待验证就是了。

论文笔记:Memory Networks

2018年11月10日 08:00

这篇论文是第一篇提出 Memory Network 的论文,和神经元图灵机(Neural Turing Machine)的论文在同一年。不过说实话,我觉得这篇论文的质量很一般,是比不上 NTM 那篇论文的,只不过其中提出来的一些观点和思想在后面逐渐被补充和完善,才有了现在 Memory Network 的名声。

论文的出发点有这么几个:

  1. 大部分 ML 方法都缺乏建立长期记忆(long term memory)的能力以及基于长期记忆的推理和处理 —— RNN 虽然能一定程度上做到,但由于 gradient vanish 的问题,实际上长期记忆在 RNN 中会衰退为短期记忆,LSTM 的提出对此问题有所改善,但效果有限
  2. 作者主要是想进行基于知识库(Knowledge Base,或称知识图谱)的问答,但希望知识库可以作为模型的一部分并在此基础上进行 QA
  3. NTM 的结构很好,但是它能使用的 memory 还是太小了

首先明确一下这里的「Memory」也就是「记忆」的概念。在不同的领域,比如脑科学、认知科学、物理学,对记忆的定义和理解都不一样,我对这些学科不太懂,不知道该对应到其中的哪部分上去。在机器学习/深度学习里,所谓的记忆,只是指「处理过的数据中会对目前的处理过程造成影响的信息」,也就是说,是和时序存在关联的。图像识别里用大量的图片来训练模型,最后能识别出新的图像的类别,这算不算「记忆」呢?按照我们的观点,其实也算,但这种我们更多认为是从大量同类图片中提取出了「模式」,本文讨论的「记忆」并不是这样的,除了有时序关系,还要有一定的因果关系,比如说我前 1 分钟给我看一张搞笑图片,现在给我一张根据这个搞笑图片 PS 过的川普和希拉里的图片,我会 get 到笑点然后拍桌狂笑。

总结一下的话是两点:

  1. 时序性: 信息的输入是时序的,有先有后
  2. 因果性: 先输入的信息对后输入的信息会产生影响,如果将两者调换,那么结果会不一样

看到时序性,很自然会想到 RNN,而它也满足第二点,只不过在它里面,「记忆」是隐式存在的,也就是它里面的 hidden state。但众所周知,RNN 因为 gradient vanish 的问题没法对长期记忆进行建模,LSTM 的提出倒是神来之笔,大大提高了 vanilla RNN 这方面的能力,后来 residual network 里的 skip connection 和 LSTM 里的 constant error flow 是很相似的。

对概念的讨论就这样,先来看看论文的模型,大致如下:

memnn_model.png

整个模型分成五个模块,分别是:

  1. 输入模块 I: 用来对输入进行特征提取
  2. 泛化模块 G: 用来将输入根据一定的机制写入到 memory 中去
  3. 记忆模块 memory: 用来存储信息,供后面的处理使用
  4. 响应模块 R: 对特定的问题,用来从 memory 中提取信息并进行推理,得到输出信息
  5. 输出模块 O: 对模块 R 的输出进行处理,表达成自然语言

如果读过神经元图灵机那篇论文,应该能一眼看出来模块 G 和模块 R 其实就是 NTM 里面的 write heads 和 read heads,在读写的时候计算 memory 里所有成分和当前数据的相似度,其实就是类似现在的 attention 机制了。然而有一个问题是,如果 memory 超级大,比如像作者说的一样,要将整个 freebase 作为 memory,别说计算 softmax 了,就是遍历 memory 都会是难以承受的,这问题该怎么解决呢?

Consequently, for efficiency at scale, G(and O) need not operate on all memories: they can operate on only a retrieved subset of candidates(only operating on memories that are on the right topic). We explore a simple variant of this in our experiments.

如上,作者的办法是,建立 word 到 memory 的哈希,然后根据 question 来去检索出关联的 memory,只对这部分 memory 进行推理 —— 这其实就是一个倒排索引了……

在这个模型中,模块 G 和模块 R 是核心所在,真的如作者所说要存储大规模的 memory 并在其基础上进行推理时,模块 G 和模块 R 的具体设计是很重要的,然后作者在后面描述基于 freebase 的 QA 任务时,模块 G 只是填充「下一个空白的 memory cell」,模块 R 则是「根据倒排索引去检索相关的 memory cell」,完全就是一套检索系统,整个训练过程也完全不是端到端的,臭不要脸 :)。直到 15 年 Sukhbaatar 提出了端到端的模型后, Memory Network 才算是真正意义上的完成了,我是这样认为的。

总之我个人对这篇论文评价不高,虽然它提出的 Memory Network 这个模型的思想是不错的,某种意义上来说,它其实是一个框架而不是一个具体的模型,因为其中的 I/G/R/O 四个模块都是可以用不同 NN 来实现,扩展性很好,这也是后来持续地有 Memory Network 相关的工作的原因, NIPS 2015 还专门有一个 RAM workshop —— 所谓「RAM」是 "RNN-Attention-Memory" 的缩写。

以上。

论文笔记:LSTM: A Search Space Odyssey

2018年11月10日 08:00

作者

  • Klaus Greff
  • Rupesh Kumar Srivastava
  • Jan Koutnik
  • Bas R. Steunebrink
  • Jurgen Schmidhuber

观点

  • LSTM 结构的核心思想是其能维护历史状态的记忆单元,以及能调节信息出入的非线性门控单元(gating unit)
  • 自 LSTM 提出后,陆续有多种对 LSTM 结构的改进工作,并广泛应用到了许多规模、性质迥异的问题上,但却缺乏对 LSTM 及其变体中各个计算部件的系统性分析

模型/实验/结论

模型,LSTM 即八种待对比的 LSTM 变体

  • V: vanilla LSTM,即经典的 LSTM 模型
  • NIG: 在 LSTM 基础上去除 input gate 得到的结构
  • NFG: 在 LSTM 基础上去除 forget gate 得到的结构
  • NOG: 在 LSTM 基础上去除 output gate 得到的结构
  • NIAF: 在 LSTM 基础上去除 input activation function 得到的结构
  • NOAF: 在 LSTM 基础上去除 output activation function 得到的结构
  • NP: 在 LSTM 基础上去除 peephole 得到的结构
  • CIFG: 既 GRU
  • FGR: 在 LSTM 基础上让门控单元互相之间都有连接(full gate recurrence)

实验

  • 在 TIMIT 数据集和 IAM 在线手写数据库上使用双向 LSTM,在 JSB Chorales 数据集上使用 LSTM
  • 在 TIMIT 数据集和 JSB Chorales 上使用交叉熵作为损失函数,在 TIMIT 数据集上则使用 CTC
  • 对总共 27 个模型各进行 200 次对数尺度上的超参搜索,然后进行训练,共进行 5400 次实验
  • 每个数据集上的每个变体的 200 次实验中,最好的 20 个实验结果被拿来和 vanilla LSTM 模型的结果对比

结论

  • 在三个数据集上,移除 forget gate 或 output activation function 都严重损害了模型性能,forget gate 对 LSTM 来说至关重要
  • 对连续实数数据上的监督学习问题,input gate、output gate 和 input activation function 的存在非常重要
  • GRU 和 移除 peephole 的变体相比 vanilla LSTM 没有显著的性能差异,但它们都在一定程度上简化了 LSTM 结构
  • full gate recurrence 结构没有改善 LSTM 的性能,相反还在 JSB Chorales 数据集上让结果变差了不少,加上它让 LSTM 更复杂了,不建议使用
  • 动量项对性能和训练速度都没有提高作用
  • 梯度裁剪会损害整体性能
  • 在使用 SGD 进行训练时,动量项对训练没什么显著好处;但在用 BSGD 进行训练时可能会起到一定的作用
  • 学习率和网络大小是 LSTM 中非常重要的两个超参

论文笔记:Hierarchical Memory Networks for Answer Selection on Unknown Words

2018年11月10日 08:00

这篇论文是中科院自动化研究所(CASIA)在 9 月份发表的一篇论文,收录于 COLING2016。该论文基于 Memmory Network 做了一些改进,使得模型在特定的 QA 任务中能更好地从 memory 中选择答案,并且能一定程度上处理低频词甚至未登录词(unknown words)。论文的数据集以及模型实现已经在 Github 上开源,如果对论文细节没有太多兴趣,可以直接去 项目地址 了解项目详情。

论文使用的数据集如下表所示

hmn_datasets.png

数据集包含机票预订和酒店预订两个领域,而且包含中文的数据集哦,这点很赞,虽然数据量看起来并不是很多。截取中文的机票预订数据中的片段如下:

1 下午 好 , 我 是 机票 预订 服务 代理 , 需要 什么 服务 ?
2 我要 预订 一张 机票 。
3 请问 先生 您 从 哪里 起飞 ?
4 由 BGI 起飞 的 飞机 。
5 去 到 哪里 ?
6 到 印第安纳 去 。
7 电话 号 ?
8 13228762221 , 这 是 我 的 号码
9 时间 是 ?
10 2015年09月26日22点 之前 。
11 先生 , 乘客 的 身份证 是 ?
12 我 的 身份证号 是 110100195352319154 。
13 麻烦 说 下 您 的 名字 ? 谢谢 先生 。
14 好 的 , 名字 是 袁磊 。
15 先生 , 我们 已经 成功 为 您 预订 。
16 这么 快 , 非常 感谢您
17 订票 人 的 姓名 叫 什么 ? 袁磊 16
18 出发 城市 是 哪里 ?       BGI 16
19 到达 城市 是 哪里 ?       印第安纳 16
20 出发 时间 是 什么 时候 ?  2015年09月26日22点 16
21 证件号码 是 多少 ?        110100195352319154 16
22 联系电话 是 多少 ?        13228762221 16

数据集的情况似乎有点像多轮对话,但并不完全是,如上 22 轮对话,其中前 16 轮是客服和客户之间的对话,这段内容被作为 history 输入到模型中储存为 memory,而 17-22 则是问题和对应的答案,每个问题的答案都是一个单独的词,是从 memory 中挑选出来的。所以从数据集上来看,本文的方法适用于一些像机票预订、酒店预订这种流程比较明确的业务。

像这种数据集,如果要我做我会怎么做呢?粗暴点的思路是将 history 和 question 各自 encode,然后将两者一起用于计算来去预测输出,事实上之前不少 QA 方面的工作都是这种思路。论文开头就批评这种做法

the memory of these methods, such as Long Short-Term Memory(LSTM) (Hochreiter and Schmidhuber, 1997) and Gated Recurrent Unit (GRU) (Cho et al., 2014) compressing all the external sentences into a fixed-length vector, is typically too small to accurately rememberfacts from the past, and may lose important details for response generation

这个也是当前在 sentence representation 讨论得比较多的话题吧,将句子直接 encode 成一个固定长度的向量,是会丢失一些细节的。不过我觉得还是看应用场景,如果是那种一问一答且目的性不是非常强的 QA 场景,encoder-decoder 的框架问题不大,语义漂移(如万能回复)的问题可以在前期进行意图识别、情感分析来得到额外的特征输入到模型里。但像本文的这种数据集就不是简单的一问一答,而是先有了一部分历史信息,然后给定问题从历史信息里寻找答案,有点类似英语的阅读理解题目 —— 因此 PaperWeekly 在 教机器学习阅读 中介绍了《End-to-End Memory Networks》这一篇作为本篇论文基础的论文(好绕口呀)。

对于类似本文的 QA 任务,早先的一些相关工作也是可以用上的,比如 Sukhbaatar 提出的端到端的 Memory Networks,文中记为「MemNN」。但是 MemNN 的问题在于它单纯在句子级别进行 "推理",具体一点是只使用了 sentence level 的 attention 机制,见之前写的笔记: End-to-End Memory Networks。如果能进一步地在词级别进行分析,结果应该会更好一点。事实上也有人这么做了,2015 年俞扬等人的《Empirical study on deep learningmodels for question answering》论文中就提出了一种 "Search-Response" 结构的模型,先使用 MemNN 从 history 中挑选出相关的句子(supporting sentences),这一步称为 "Search";然后用 NTM 或者 NMT(Neural Machine Translation) 来从这些 supporting sentences 中生成答案,这一步称为 "Response"。在 "Search-Response" 结构中,Search 和 Response 两个步骤是分别独立训练的,也就是说这其实是一个 pipeline 的方法。

所以本文的基本思想是: 结合 MemNN 和 "Search-Response" 的思想,得到一个端到端的系统。本文的模型结构如下图所示:

hmn.png

图中左侧是模型的整体结构,右边的两个小图是左图中两个模块的细节图示。整个模型大体上可以划分为四个部分,分别是:

  1. sentence level memory and reasoning

    这部分将 history 信息转换成内部的 memory,并在 此基础上计算结果,同 MemNN,上图中右下块是这部分的图示,要注意的是这里为了简化画成了类似 RNN 的结构图,但并不是说 \(X\) 中的句子依次输入这个模型,然后更新自连接的 \(u_{r}{(S)}\),这里的自连接只是表示多层结构(文中使用的层数为 3),而这种多层结构和 RNN 的结构有共同之处。

    在这里,history 也就是 \(X\) 中的每个句子 \(x_{i}\) 和 question 也就是 \(q\) 都要表示成一个向量,使用的是《End-to-End Memory Networks》中的 position encoding 方法,即将句子中每个词的 embedding 加权求和,而这个权值和词在句子中的次序有关。

    总之这部分跟《End-to-End Memory Networks》中的内容基本一样,不赘述,详见 MemNN 的笔记

  2. k-max pooling

    这部分连接 1 和 3,利用 1 输出的 internel state 和 question 一起挑选出 history 中和 question 最相关的 k 个句子。

    所谓的 internel state,就是图中的 \(\alpha^{(S)}\)

    \[\alpha^{(S)}=softmax(M^{T}u_{1}^{(S)})\]

    上式中 \(M\) 为 sentence level memory,\(u_{1}\) 为问题 \(q\) 的 representation。

  3. word level memory and attention: 使用 2 得到的 k 个句子,进行 word level 的 attention 来得到结果

    这部分使用 BiGRU 来得到 attention

    \[M^{(W)} = \{m_{t}\}_{t=(1,2,...)}\]

    \[m_{t}=\overrightarrow{h_{t}}+\overleftarrow{h_{t}}\]

    \[\alpha^{(W)}=softmax(v^{T}\tanh(Wu_{R}^{(S)}+U\hat{m}_{t}))\]

    这里的 \(alpha^{(W)}\) 就是 word level 得到的结果,但是这个结果是在 k 个句子中的词上的概率分布,没法直接用于第 4 步的计算,因此要做一次转换,将 \(\alpha^{(W)}\) 扩充为长度为 \(V\) 的向量,\(V\) 是词典的大小,方法是用 0 填充。

  4. output: 综合 1 和 3 的输出来计算最后的结果

    第一步得到的输出记为 \(p^{(S)}\),第三步得到的输出记为 \(p^{(W)}\),将这两者直接相加作为最后的输出

    \[p = p^{(S)}+p^{(W)}\]

如上所述,因为在 sentence level memory network 的基础上加上了 word level memory,作者将这个模型称为「Hierarchical Memmory Networks」,这是一个能够进行端到端训练、能在更细粒度的语言成分上进行"推理"的 QA 模型。

下图是 HMN 和其他模型在给定的数据集上的对比结果

hmn_compare_with_others.png

以及 HMN 模型使用不同的 word level encoder 时的效果对比

hmn_results.png

下面的图显示了模型回答一个问题时的过程

  1. 表格第一列是 history,这些内容将会被 encode 后储存到 sentence level memory 中
  2. 需要回答的问题是: When does the client depart,正确答案是 history 中第 14 条中的日期
  3. 表格第二列是 sentense level reasoning 的过程,其中的值是每一个 reasoning 层的 internel state 也就是 \(u_{r}^{(S)}\),可以看到进行三次 reasoning 后正确的 supporting sentence 也就是第 14 条的权重已经非常的高了
  4. 第三列是 k-max pooling,这里的 k 取值为 4,得到的就是 8、10、13、14 这四个候选的句子
  5. 第四列是 word level 部分,使用 attention 来得到每个候选句子中的词作为答案的概率,这里概率最大的就是第 14 条句子中的日期

hmn_example.png

以上就是本篇论文的主要内容,至于开头作者提到的未登录词问题的处理,作者就是指着上面的图说:你看, 10/13/2018 这样的词也被找出来了!总之,作者没有提出一种对未登录词的解决办法,我猜他的意思是,在构建 vocabulary 的时候,不要根据词频去除低词频的词,而是照单全收喂给模型,而这个模型是能够将这种词频非常低的词正确地找出来作为答案的。

我觉得这就有点标题党了哈……

论文笔记:Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling

2018年11月10日 08:00

作者

  • Junyoung Chung
  • Caglar Gulcehre
  • KyungHyun Cho
  • Yoshua Bengio

观点

  • RNN 在很多机器学习任务尤其是变长输入输出的任务上效果拔群
  • 经典 RNN 有两个主要的问题: 梯度消失, 长期记忆急速衰减。
  • 解决 RNN 难以训练的尝试有两种: 一种是设计更好的学习方法(Bengio 2013),另外一种是设计更复杂的激活函数
  • LSTM 不会每次都重写 memory,而是可以通过 input/forget gate 在需要的时候尽量地保留原来的 memory
  • LSTM/GRU 中额外增加的 cell state,让它们能记住较早之前的某些特定输入,同时让误差反向传播时不会衰减地太快

数据集

模型/实验/结论

实验: 在上述几个数据集上,分别使用经典 RNN、LSTM、GRU 进行训练,并记录 NLL 的变化情况。

结论: LSTM/GRU 在收敛速度和最后的结果上,都要比经典 RNN 要好,但 LSTM 和 GRU 在不同的数据集和任务上虽然互有优劣但差异不大,具体使用 LSTM 还是 GRU 还要视情况而定。

概念和术语

  • polyphonic music:

    (来自维基百科)

    复音音乐/复调音乐/和弦,一种“多声部音乐”。作品中含有两条以上(含)独立旋律,通过技术性处理,和谐地结合在一起,这样的音乐就叫做复音音乐。

    复音音乐第一个“音”字表示旋律,中国音乐界习惯将“复音音乐”称为“复调音乐”,主要是着眼于曲调一词,但“复调音乐”容易与二十世纪的“复调性音乐”一词混淆。

总结

实验很粗暴,结论很简单。

论文笔记:An Ensemble of Retrieval- and Generation-Based Dialog Systems

2018年11月10日 08:00

这篇论文在指出基于检索方法(retrieval-based)和基于生成方法(generation-based)的两种对话系统的问题后,提出了一种结合检索方法和生成方法的对话系统,并在 700w 的中文对话数据(来自微博、百度知道、贴吧等)上获得了比两种方法都更好的结果。

检索方法在一些垂直领域的对话系统中比较常见,这是因为特定领域的对话系统中用户的目的性比较明确,多为查询信息或者提供信息(以完成某种「任务」),通过检索和人工规则一般就能解决问题。而开放域的闲聊类场景下,如果有足够的数据,用生成方法会是一种比较好的选择。作者关注的开放域中的对话系统,这种场景下也可以应用一些检索方法和人工规则,但检索方法只能根据浅层语义来发现关联的回复,而尝试用人工规则覆盖开放域下的种种不同情况将会导致规则数量爆炸增长;生成方法固然可以生成知识库中没有的回答,但很容易抛出类似「呵呵」、「是吗」这种万能回复,这种语义漂移现象已经被很多将生成方法应用于对话系统的人发现并进行研究了。

文本的思路是将检索方法和生成方法结合起来,主要思想有两点:

  1. 使用生成方法时,使用检索到的回复作为额外的信息
  2. 对检索方法和生成方法给出的结果,在最后进行重新排序

ensemble-dialog-system.png

具体一点的话,过程是这样的:

  1. 对给定的 query \(q\),首先从知识库中进行检索,得到候选的回复 \(r^{\ast}\),这个 \(r^{\ast}\) 会有很多个
  2. 将每个候选回复 \(r^{\ast}\) 和 \(q\) 一起输入到生成模型也就是图中的「multiseq2seq generator」中,后文又称之为「biseq2seq」
  3. \(q\) 和 \(r^{\ast}\) 都是一个句子,它们在 biseq2seq 中经过 encoder 被各自编码成一个向量
  4. 将 \(q\) 和 \(r^{\ast}\) 的向量 拼接 起来,decoder 以其作为输入,生成一个回复 \(r^{​+}\)
  5. 将所有检索出的回复 \(r^{\ast}\) 和生成的回复 \(r^{​+}\) 混合起来,进行重排序,返回最优的一个

检索阶段没有太多好说的,就是搜索系统用的那一套东西,先根据 query 内容找到知识库中可能有关联的 "问-答" 对 \((q^{\ast}, r^{\ast})\),然后进行排序,选出 1000 条最好的结果。需要注意的是,在排序时,\(q\) 和 \(q^{\ast}\) 的关联性、\(q\) 和 \(r^{\ast}\) 的关联性会一起被考虑。

生成阶段,其所谓的「biseq2seq」的结构如下图所示

biseq2seq.png

这个模型里有两个 encoder 和一个 decoder,一个 encoder 用来对 \(q\) 进行编码,另一个 encoder 用来对 \(r^{\ast}\) 进行编码。两个 encoder 输出会拼接成一个向量输入到 decoder 中。不过论文没有就 biseq2seq 的具体数学表示和训练做详细说明,从引用文献里看,biseq2seq 的想法是来自 Barret Zoph 和 Kevin Knight 的一篇论文《Multi-source neural translation》,是 2016 年发表的一篇论文,回头再去了解一下。

下表是论文中方法和其他一些方法的实验对比结果。

ensemble-dialog-system-results.png

论文中还展示了 biseq2seq 和 seq2seq 在生成回复时的结果对比,从下表来看,biseq2sq 生成的结果确实更有意义一些。

biseq2seq_vs_seq2seq.png

论文笔记:End-to-End Memory Networks

2018年11月10日 08:00

这篇论文是在看 Hierarchical Memory Networks for Answer Selection on Unknown Words 时追过去的 —— 这篇论文里有些东西讲的不明不白,还好它里面说明一些思想来自哪里。

论文标题为 "End-to-End Memory Networks" ,很显然这篇论文是基于 Memory Networks 来做了一些工作,按照惯例,论文在开头说了一些原始的 Memory Networks 的缺点:

  1. Memory Networks 难以进行 BP 训练
  2. Memory Networks 需要更多的 supervision(这个没太懂,回头把 Memory Networks 再看一下)

模型的基本结构如下图所示。

memn2n_single_layer.png

模型的输入包含两部分,一个是图左端那个「Sentences」,即由多个句子组成的内容,这些句子会被输入到网络中并储存为「memory」,可以认为是在检索/问答时的历史信息或者说上下文;另一部分是 query 或说 question,就是图中下端那个 q,也是一个句子。模型最后的输出也是一个句子。

模型的输出,按照我对这张图的理解,应该是一个经过 softmax 的 d 维向量(d 是 vocabulary 的大小),但是这样的话就只能输出一个词了?从后面的实验部分来看确实是这样的,总感觉这样的话并不是很适用于常规的 QA 任务了。

输入的 Sentences,首先通过 embedding 矩阵 A 得到 input memory。其实就是将每一个句子 encode 成一个向量,也就是图上的 \(m_{i}\),这些 \(m_{i}\) 组合起来就得到 input memory,也就是涂上标为 Input 的那块矩形 M。问题 q 同样通过 embedding 矩阵 B 来 encode 成一个向量 u,然后用 u 和 M 內积得到一个权值向量:

\[p=softmax(u^{T}M)\]

接着,输入的 Sentences 通过另外一个 embedding 矩阵 C 得到另外一个 memory 表示,也就是图上标为 Output 的那块矩形 C,然后用上一步得到的权值向量来对 C 进行加权求和:

\[o=\sum_{i}p_{i}c_{i}\]

对于这部分,我的理解是,u 和 M 的內积相当于一个 attention,得到 attention 后,就用来在用于输出的 memory 也就是 C 中进行检索得到输出结果。不过还是有些地方不太明白,比如说反映在网络结构上,u 和 M 的內积是怎么体现的?输入连接两个 embedding 矩阵又是怎么体现的?

在得到输出向量 o 后,按理来说直接 softmax 就可以得到结果吧,但是这里却和 question 的向量 u 做了一个加法才进行 softmax,不是很懂,也许和 ResNet 的那个 residual connection 是同样的思想?但是 2015 年 3 月份(这篇论文的发表时间)的时候 ResNet 还没有正式发表吧。

以上就是单层时的情况,多层的时候就用前一层的(未经过 softmax 的)输出作为本层的输入向量 u,即

\[u^{k+1} = o^{k}+u^{k}\]

一图胜千言:

memn2n_multi_layer.png

在多层结构的时候,作者又对 "weight typing" 做了不同的尝试 —— 不知道「weight typing」该怎么翻译,且看下文吧。

可以看到,在上面的多层网络结构中,每一层都有一个 A 和 C,对 A 和 C 的处理,有两种方式:

  1. Adjacent: 将前一层的输出 embedding 矩阵 C 作为本层的输入 embedding 矩阵 A: \(A^{k+1}=C^{k}\)
  2. Layer-wise: 像 RNN 一样,所有输入 embedding 矩阵 A 共享参数: \(A^{1}=A^{2}=...=A^{K}\),输出矩阵 C 同理

在对句子的 encode 上,也有两种不同的处理方式:

  1. BoW: 简单的将句子中每个词的 embedding 累加起来: \(m_{i}=\sum_{j}Ax_{ij}\),u 和 \(c_{i}\) 的计算同理
  2. position encoding: \(m_{i}=\sum_{j}l_{j}\cdot Ax_{ij}\),这里的 \(l_{j}\) 就是所谓的反映了词序的 position encoding,它和后面的值进行 element-wise 的乘法而不是內积,而这个 position encoding 的计算方法是: \(l_{kj}=(1-j/J)-(k/d)(1-2j/J)\),其中 j 是词在句子中的位置,J 是句子的长度,k 是这个词的 vocabulary id,d 是 vocabulary 的大小。

此外就是 Sentences 中的句子之间也是有次序的,为了反映这个次序,又在计算 \(m_{i}\) 和 \(c_{i}\) 时加了一个额外的叫做 temporal encoding 的东西,分别记为 \(T_{A}(i)\) 和 \(T_{C}(i)\):

\[m_{i}=\sum_{j}Axij + T_{A}(i)\]

但是并没有说 \(T_{A}(i)\) 和 \(T_{C}(i)\) 是如何表现的,只是带了一句: Both \(T_{A}\) and \(T_{C}\) are learnd during training。另外,在模型中,句子的次序对真实的次序而言是倒序的。

作者提出这个模型除了用在 QA 上外,还在语言模型上做了实验。在做语言模型时,是没有 question/query 的,所以 u 被设置为一个常量的向量(值都为 0.1);然后每个 \(m_{i}\) 和 \(c_{i}\) 都是对应一个词而不是一个句子了,因为 LM 是 word-level 的问题嘛。

最后总结一下,本文的主要贡献是增强了原始的 Memory Networks,让其称为一个端到端的模型,能更方便地使用 BP 方法进行训练。

就酱!

论文笔记:Decoupled Neural Interfaces using Synthetic Gradients

2018年11月10日 08:00

这篇论文是 DeepMind 在 2016 年 8 月 18 日发布的,最初是在智能单元上看到这个消息的,觉得挺有趣的,就去读了一下。

这篇论文干了什么呢?它在现有的神经网络模型基础上,提出了一种称为 Decoupled Neural Interfaces(后面缩写为 DNI) 的网络层之间的交互方式,用来加速神经网络的训练速度。

作者在开篇指出,在神经网络模型训练中存在几个问题

  • Forward Locking: 前一层没有用输入完成计算,后一层无法进行根据输入进行计算
  • Update Locking: 如果一个网络层依赖的层没有完成前馈计算,该层无法进行更新
  • Backward Locking: 如果一个网络层依赖的层没有完成反向传播,该层无法进行更新

刚看到这说不定会想你这不是废话吗,前一层没计算好后一层怎么计算怎么更新?哎,作者表示我就是不信这个邪我就是要上天和太阳肩并肩啊!为什么呢,因为在一些情况下,神经网络的这种计算模式会带来一些问题,这些情况包括:

  • 由多个异步模块组成的复杂的系统(这个不懂……)
  • 分布式模型,模型的一部分共享给多个下游客户端使用,最慢的客户端将会成为模型更新速度的短板

所以呢,作者就想办法把这种训练时层与层互相依赖的限制干掉咯。

第一个想法,是干掉 Update Locking 和 Backward Locking,让网络的每一层在完成前馈计算后能马上计算出残差,然后立即进行该层的参数更新,而不用等待后一层的前馈计算和误差反向传播。这个是怎么做到的呢?办法就是, 增加一个额外的模型 ,去根据当前网络层的输出去预测其残差,并用预测到的残差去计算梯度,这个实际梯度的估计值,被称为「合成梯度(synthetic gradients)」。

先来看看传统的误差反向传播(Backpropagation, BP)算法,我们假设一个有 N 层的神经网络,其第 i 层的激活值即输出为 \(h_{i}\) ,那么对第 i 层,其参数更新式子为:

\[\begin{array}{rcl} \theta_{i} &\leftarrow& \theta_{i} - \alpha\delta_{i}\frac{\partial h_{i}}{\partial \theta_{i}} \\ &\leftarrow& \theta_{i} - \alpha\delta_{N}\frac{\partial h_{i}}{\partial \theta_{i}}\cdot \prod_{j=0}^{N-i-1}\frac{\partial h_{N-j}}{\partial h_{N-j-1}}\end{array}\]

可以看到,需要依赖后面所有层的激活值的计算。作者的思路就是只利用当前层的激活值 \(h_{i}\) 去得到当前层的残差 \(\delta_{i}\) 的估计值 \(\hat{\delta_{i}}\):

\[\begin{array}{rcl} \delta_{i} &\sim& \hat{\delta_{i}} \\ &=& M_{i+1}(h_{i})\end{array}\]

但是可想而知,在初始的时候,这样一个「额外的模型 M」所给出的残差估计值肯定是与实际的残差有很大的偏差的,那么怎么办呢?办法就是用每一层的实际的残差去训练这个额外的模型 M —— 不过这样不就将 BP 的过程转移到另外一个模型里去了么,这不是耍流氓嘛!不过作者在这里又使用了一个技巧,将 BP 过程彻底干掉了。

我们都知道,所谓误差反向传播,是先计算出最后一层的残差,然后用最后一层的残差去计算倒数第二层的残差,依次类推,故称「误差反向传播」。如果在训练模型 M 时依然遵照这个流程,毫无疑问 Update Locking 和 Backward Locking 依然存在,所以作者在计算每一层的“实际残差”时,用的是后一层的“合成残差”,而合成残差的计算是可以立即计算的。这里实际上又做了一次近似,也就是:

\[\delta_{i} = \hat{\delta}_{i+1}\frac{\partial h_{i+1}}{\partial h_{i}}\]

这样用这个近似的 \(\delta_{i}\) ,去评估模型 M 给出的残差估计值 \(\hat{\delta_{i}}\) ,并用两者之间的误差去更新模型 M。由于最后一层是能得到真正的残差的,所以最后一层的模型 M 能较快得到训练,随着最后一层的模型 M 被训练的越来越好,它所估计出的 \(\hat{\delta_{i}}\) 也能越接近真实的 \(\delta_{i}\) 。如下图所示:

dni_update.png

再进一步地,作者表示,利用 DNI 的思想,去预测每一层的输入也是可以的,这样就把 Forward Locking 也去掉了。基本思想和合成梯度是一样的,不同之处在于预测每一层的输入时只用到第一层也就是输入层的输入:

dni_unlock_all.png

这样 Forward Locking、Update Locking 和 Backward Locking 都被去掉了,通过适当的设计,整个训练可以被很好地并行化、异步化了。

DNI 的思想除了用在前馈神经网络上,也可以用于循环神经网络(Recurrent Neural Network, RNN)的训练上面,因为 RNN 在时间维度上展开后,其实就相当于是一个前馈神经网络了。而且由于应用 DNI 的模型,最多只有两层的网络层依赖,那么在用于 RNN 训练时,可以不用将 RNN 完全展开,而是可以以两个 time step 为最小单元进行展开,即一次只展开两个 time step,这样在存储上的消耗也可以被降低。

rnn_dni.png

来看看作者的牛皮

Although we have explicitly described the application of DNI for communication between layers in feedforward networks, and between recurrent cores in recurrent networks, thereis nothing to restrict the use of DNI for arbitrary network graphs. The same procedure can be applied toany network or collection of networks, any number of times.

以上就是 DNI 的理论部分,实际上还有很多东西没有讲清楚,比如:

  • 那个额外的模型 M 的具体细节?
  • 增加了额外的模型 M ,相当于增加了参数数量,是不是更容易过拟合了?

作者首先在 MNIST 和 CIFAR-10 两个数据集上测试了 DNI 方法和 BP 方法之间的训练效果(只去除 Update Locking 和 Backward Locking)

dni_every_layer.png

需要说明的是,这里用的模型(FCN 和 CNN)层数都不多,实验分别使用了 3-6 的层数,见上图中右侧部分,从该图来看,使用 DNI 的训练方法在训练速度和训练效果上并没有什么优势。这个实验只是表明,使用 DNI ,模型 能够被训练

第二个实验是这样的:对一个四层的前馈网络,以随机的顺序来更新每一层,并且每一层在被选中都是有概率的。在这样的情况下,模型依然是可以被训练的。

dni_sparse_update.png

不过明显能看出来,概率值越大,收敛是越快的,最后在迭代次数达到 50w 次时,不同的概率都达到了接近的精度(2% 的误差),不过一个四层的网络,真的不是过拟合了么……

第三个实验在第二个实验的基础上,加上了 synthetic inputs ,也就是把 Forward Locking 去掉了,从结果上来看,和第二个实验差不多的样子。

dni_sparse_update_without_locking.png

最后在 RNN 上进行了三个实验,分别是:

  • Copy: 读入 N 个字符,然后将这 N 个字符原样输出,有点类似 char-level language model 和 autoencoder。
  • Repeat Copy: 读入 N 个字符,以及一个表示重复次数的数字 R,然后重复输出 R 次这 N 个字符构成的序列。
  • char-level language modeling: (持续地)读取一个字符,并预测下一个字符。

结果如下图所示:

dni_on_rnn.png

上图中 Copy 和 Repeat Copy 两栏中的值表示建模的最大序列长度,越大越好;Penn Treebank 一栏表示语言模型的困惑度(Perplexity,这里用 bits per word 进行度量),越小越好。从实验结果上来看,在 Copy 和 Repeat Copy 任务上,使用 DNI 的模型能建模更长的序列。

论文笔记:Ask Me Anyting: Dynamic Memory Networks for NLP

2018年11月10日 08:00

这篇论文的作者来自一家 AI 公司 MetaMind,因此虽然用了「Memory Network」这个概念,但在思路上和 FAIR 提出 Memory Network 的几个人都很不一样。按我的观点,这篇论文里的模型与其说是 Memory Network,不如说是 encoder-decoder + attention。

这篇论文同样是用来处理 QA 任务的,用的是 FAIR 公开的 bAbI 数据集。这个数据集所描述的场景,其实和 reading comprehension 更接近一点,先会有一段话进行描述,然后给出若干个问题,要求根据前面的描述来寻找答案,下面是一个例子:

I: Jane went to the hallway.
I: Mary walked to the bathroom.
I: Sandra went to the garden.
I: Daniel went back to the garden.
I: Sandra took the milk there.
Q: Where is the milk?
A: garden
I: It started boring, but then it got interesting.
Q: What’s the sentiment?
A: positive
Q: POS tags?
A: PRP VBD JJ , CC RB PRP VBD JJ .

其中以 "I" 开头的句子就是描述性句子,被称为上下文(Context);"Q" 开头的句子则是问题,"A" 开头的句子是回答。在这个场景下,context 有一定的长度,要比 RNN 能处理的信息长度更大,但又是有限的。

与 14 年那提出的 Memory Network 模型和 Neural Turing Maching 模型不同的是,DMN 里没有使用外部的存储空间,而是直接用一个 RNN 的 encoder 对 context 进行编码,将这个过程中输出的 hidden state 作为 memory —— 所以我会认为这个模型更接近 encoder-decoder + attention 而不是 Memory Network 了。

dmn_overview.png

如上图所示,整个模型分为四个模块,和 Memory Network 里的 I/G/R/O 四个模块一一对应,但刚才也说了,其实这个模型和 Memory Network 并不是很像,所以作者为了对应上 Memory Network 的四个模块而作出类比,搞得有点不太好理解。图中的 Episodic Memory 模块并不对应 Memory Network 里的 memory 存储区域,非要说的话,应该是对应 Memory Network 里的 Response 模块,用来做结果的推理也就是 attention。

接下来我还是按我的理解来梳理这篇论文吧。

模型的 Input Module,其实就是一个 encoder,用来将 context 表示成 hidden state 形式的 "memory"。与 Memory Network 不同的是,各个 context 并不是分开处理的,而是串联成一个长的 sequence,然后喂给 RNN,然后只取每个 context 结束时输出的 hidden state。当然也有一些极端情况,比如说只有一个 context 的句子,这种情况下就直接输出每个 time step 的 hidden state 作为 memory。如下图所示:

dmn_input_module.png

然后同样用一个 RNN encoder 对 quesiton 进行处理,不过这时只取这个 encoder 最后一个 time step 的 hidden state,作为这个 question 的表示。

Episodic Memory Module 其实就是一个 attention 模块,用 question 在 Input Module 产生的 memory 上进行 attention。不过与普通的 attention 不同的是,这里的 attention 不是单纯地用 question 和 memory 进行內积算 softmax,而是再用一个 RNN encoder,将 Input Module 产生的 memory 依次输入到这个 encoder 中,并且每次都将 Question Module 的输出作为额外的信息参与计算,然后取这个 encoder 的最后一个 time step 的 hidden state 作为最后 Answer Module 里的 decoder 的 context vector。除了这点不同外,再就是这个过程可能会进行多遍,用每次计算出来的 context vector 输入到一个 decoder 里,用产生的 hidden state 再参与到 attention 中,这个迭代次数是认为设定的,在论文里作者就迭代了三次。如下图所示:

dmn_memory_module.png

最后,将 Episodic Memory Module 的输出作为 context vector 输入到 Answer Module 的 decoder 里去,来生成最终的答案,这块比较简单,如下图所示。

dmn_answer_module.png

比较有意思的是它里面那个 Episodic Memory Module 多遍迭代的结果,作者可视化后发现,第一遍的时候可能找到的是字面上相关的 context 句子,然后后面在迭代会慢慢定位到真正语义相关的 context 句子上,相当于是在做了推理。

dmn_focus.png

此外,作者还把模型用在了情感分析上,对句子中词的 attention 结果,在迭代过程中的变化也展现出了类似的现象。

dmn_focus2.png

以上。

空洞骑士

2018年10月27日 08:00

hollow_knight.jpg

手残在神居被打懵,随便写点东西……

《荒野之息》和《空洞骑士》是我入了 NS 后主要玩的两款游戏,刚才去瞄了眼,前者的游戏时间是 250 小时,后者是 75 小时。两款游戏都非常棒,不过给我的感觉是很不一样的。《荒野之息》的游戏引导(包括 NPC 指引、环境暗示)等做得非常好,所以玩起来非常舒服,哪怕是在守护者环绕的平原之塔,也可以避免战斗巧妙地利用障碍物阻挡怪物的视线一路爬上去,整个游戏除了必要的一些战斗外,玩家完全可以当一个和平主义者,爬爬山、做做菜、找找呀哈哈,真的可以当作一个放松心情的方式。《空洞骑士》就不一样了,成天要担心离长椅(存档点)太远死了要跑好长一段路去拣尸,地图上布满各种陷阱和麻烦的小怪,BOSS 战斗节奏和小怪相差极大难以适应,存档点居然还是要花钱买的……

在某些方面,我还是要偏爱《空洞骑士》一些。

开放世界的游戏,往往不会强迫你去完成具体的任务,并且会给玩家在主线剧情外充分多的可玩内容和形式,早几年很喜欢这种类型的游戏,如《Minecraft》、《上古卷轴》之类的。这一类的游戏如果愿意投入足够多的时间在里面,是能够发掘出很多乐趣的,然而工作几年后,我已经没有这么多精力去做这种探索了,与其花大量的时间在《荒野之息》里像做化学实验一样制造不一样的菜谱,我还不如去下厨房多学几个快手菜、家常菜呢。所以对我来说,游戏中故事对我的吸引力渐渐地成为了主要因素。而《荒野之息》的故事实在是有点单薄:远古魔王苏醒,王国发掘出古代遗迹制造四神兽和守护者作为战力,但最终被魔王控制,最后一刻公主觉醒力量封印了魔王,同时把为了保护自己而伤重濒死的护卫(就是主角)送到复苏神庙治疗,一百年后,主角醒来,然后去重新控制四神兽、解救公主。说故事梗概可以扩充更多的细节来进一步丰富,这在《荒野之息》里是通过主角失忆并在特定地点唤醒记忆的形式来得到体现,此外还有一些关键 NPC 的对话,除此以外绝大部分 NPC 的对话、任务都和主线剧情毫无关系。我玩过的游戏不多,开放世界的游戏更少,所以我觉得,这类游戏本来也不是为了讲故事的,它们希望的是玩家能通过游戏来讲自己的故事。

相比之下,《空洞骑士》整个游戏,都在不遗余力地讲着一个故事,所有的 NPC 讲话都在补全着故事背景,甚至玩过一遍游戏后会发现某些 NPC 的讲话里其实暗示了故事中非常核心的部分,同时在游戏进度引导上也是不断地进行着设置悬念、揭晓答案然后设置更多悬念的过程,而且这个游戏讲故事的方式远远不止通过主线剧情、核心 NPC,场景音乐、背景装饰甚至怪物的内心话语,都在为这个过程服务。更要命的是,游戏里没有《荒野之息》里英拉、吟游诗人、回忆这些元素,来直接告诉你一些完整的信息,设定上任何一个角色对完整图景的了解、掌握都很少,更有很多文字是通过诗歌、历史记录、实验记录之类的方式留下来的,这就产生了很大的留白,给玩家带来了巨大的想象空间,甚至在不少地方玩家对故事的理解产生巨大的分歧并彼此争论。这是我喜欢的讲故事的方式,也是我喜欢的故事。

正因为《空洞骑士》这种故事性,让我不能不联想起多年前玩的另外一款独立游戏来。什么游戏呢?那就是《洞窟物语》(Cave Story),是日本一个独立开发者制作的经典独立游戏,甚至在游戏类型上和《空洞骑士》也接近,也是类银河战士恶魔城的游戏。这游戏是 2004 年发布的,年代有点久远,所以画面没有办法和《空洞骑士》相比(像素风),细节的丰富程度也要逊色不少,但音乐也很不错,作为一款十几年前的游戏,可以说是非常不错了。难度上比《空洞骑士》要低一些,目前在 steam 平台和 NS 上都有售。

在游戏难度上,《空洞骑士》要比《荒野之息》难不少,但随着游戏时间的增加,基本上都能适应过来。而且我认为在这种故事性很强的游戏里,适当的难度是很必要的,如果故事内容太容易得到,那么那种「经过自己的努力挖掘出真相」的感受就会弱很多,毕竟游戏主要还是以视觉互动为主,不可能真的像小说一样用各种写作手法来提升悬念和制造带入感。

最后附上两款游戏的 steam 链接 :)

近况

2018年10月20日 08:00

近一年来心理、精神状态总体上来说都不佳,一方面是工作上琐事繁多,另外一方面家庭方面的压力也开始上来了。

工作上,主要是一直做的都是算法落地的事情,这个过程和学术研究不一样,远不是简单重复调调参就行,会有大量的脏活、定制,繁杂到怀疑人生。这种工作状况也导致我基本上没有精力去在 NLP 本身上做太多沉淀,逐渐有些危机感和厌烦。上半年出了好多次差,在高铁上写代码、在出租车上写代码,也算是难忘的经历吧,但是真的很累。要命的是,我这种忙碌是否真的有价值(无论是对公司还是对自己),我也是持怀疑态度的。渐渐地对工作这件事情本身开始感到厌烦,因为我知道商业公司基本上都是这样的,但我又不是那种有勇气去走自由职业道路的人。

家庭上,父母年纪大了,虽然不想说,但是真的是开始老态显露,开始担心他们的身体。我并不想回家去生活,但没有能力把他们接到身边来,也没有办法做到让他们在家里过得足够好 —— 这件事情一想起就难受,只能尽量不去想这个问题。弟弟还有几年也即将进入社会,也担心他将来的工作、生活。

我的情绪控制能力不太好,压力一大的时候,性格里糟糕的一面就开始暴露出来,并且在长期高压的状况下,这些负面的音素已经开始逐渐地往上浮现,哪怕是在相对轻松的状况下,一些不好的行为也开始更多的出现了。还好公司里有一些同事始终非常耐心、理智,给了我一个锚点,让我能及时通过对比发现自己的问题,另外也感谢他们的包容吧。

这是休完一个较长假期后的一些想法,虽然不是年末,但最近也在做一些新的计划。

回来的第一周,调整了自己的作息,提早了一个小时起床,一方面是让早上有更多的时间去吃早饭、思考或者看点东西,另外一方面也是找点新鲜感吧。

另外 《NLP 哪里跑》这个系列,原来是准备尽量系统化地去谈自己的理解,结果就是一篇文章牵扯内容太多,整理资料、梳理思路的周期有点太长了,那篇《自然语言处理在文学分析中的应用》的草稿已经在我本地躺了两个月了,还处在整理资料的阶段 —— 所以我打算换一个思路,一些小块的内容可以考虑单独发出来,然后有时间再汇总成长文吧。

其他各种事情就不说了,近期我就想多写点东西,这件事情能让我心情舒缓不少。

NLP哪里跑: Unicode相关的一些小知识和工具

2018年10月20日 08:00

本文是《NLP 哪里跑》系列的第三篇文章,系列文章如下:

  1. NLP哪里跑: 开篇及一些碎碎念 · ZMonster's Blog
  2. NLP哪里跑: 什么是自然语言处理 · ZMonster's Blog
  3. NLP哪里跑: Unicode相关的一些小知识和工具 · ZMonster's Blog
  4. NLP哪里跑: 文本分类工具一览 · ZMonster's Blog

一、Unicode 简介

我们都知道,所有的数据,在计算机上都是以数字(严格来说是二进制)的形式存在的,文字也是如此,只不过咱们的编辑器、浏览器对这些数字做了特殊处理,将其对应的形状展示出来了而已。在这个基础上,不同的操作系统、平台、应用为了能够正常地交流,就必须约定一个统一的「计算机中的数字」到「实际文字」的对应关系(即编码标准),比方说数字 97 对应小写英文字母「a」、33528 对应「言」字之类的 —— 没错,所谓的编码标准,就相当于一个大的索引表,每个文字在这个索引表里都有一个对应的索引号(也就是我们刚才说到的数字)。

在计算机系统发展早期,其实是并没有这样一个统一的编码系统的,美国一开始就用了 0-127 的值来编码,包括了大小写字母、数字、标点符号以及一些特殊符号,这就是“美国信息交换标准代码(American Standard Code for Information Interchange, ASCII)”。显然 ASCII 是不适用于中文的,所以后来我国推出过 GB2312 标准,收录了 6763 个汉字,并在之后经过扩展有了 GBK 和 GB18030 多个编码标准;另外一方面,港澳台地区又独立发展出了繁体的 BIG5 编码……这些编码都是互相不兼容的,这就会导致使用编码 A 的网站,被使用编码 B 的计算机访问后显示为乱码的状况,而这里只提到了中英文的编码体系,实际上很多国家都有过自己的标准,而且很多是还在使用的。

基于这种状况,后来计算机领域产生了一个叫做 Unicode 的统一编码,又称「万国码」,收录了世界上各个国家大部分的文字,并且仍然在不断增修,今年六月份发布了第十一个正式版本。目前使用最广泛的是 Unicode 实现是 UTF-8 编码。

本文无意就编码标准和编码实现的关系、不同编码之间的同异这类问题做太多展开,只是说一下在做自然语言处理的工作时会涉及到的一些小知识以及相关的工具。

二、Unicode 在 NLP 中的应用

也许读者会疑惑,编码标准不就是字符和索引值的对应嘛,和 NLP 有什么关系呢?

是这样的,Unicode 这个标准,并不是单纯做好所有文字的索引,它还对文字分门别类做了很多的整理,比如说同一个语系的文字会放在索引表的邻近区域,而一个文字是否是数字或标点、数学符号这些信息也都在 Unicode 标准中有记录,并且所有这些数据都是公开的。如果能善加利用这些信息的话,能帮助到咱们在 NLP 工作中对文字进行处理的部分。

这里不准备对 Unicode 数据做系统、全面的说明,如有兴趣,可以前往 http://unicode.org/charts/ 查看完整的 Unicode 数据,这里就以几个例子来讲讲我个人的一些认识和经验吧。

根据 Unihan 数据来从文本中筛选中文字符

Unicode 中中文数据的部分被称为「Unihan 数据库」,在这个页面可以看到 Unihan 中数据的范围。根据 Unihan 数据,我们可以得知在 Unicode 编码里,中文的索引值的范围包括以下几部分:

  • U+3400 - U+4DB5: 「U+」表示这是 Unicode 编码,3400 是十六进制表示,换算成十进制是 13312,下同
  • U+4E00 - U+9FCC
  • U+F900 - U+FAD9
  • U+20000 - U+2A6D6
  • U+2A700 - U+2B734
  • U+2B740 - U+2B81D
  • U+2B820 - U+2CEA1
  • U+2F800 - U+2FA1D

根据这个我们能很容易地写出一个检查某个字符是不是中文字符的方法来,如下

def is_chinese_char(char):
    char_idx = ord(char)
    if 0x3400 <= char_idx <= 0x4DB5 or \
       0xF900 <= char_idx <= 0xFAD9 or \
       0x4E00 <= char_idx <= 0x9FCC or \
       0x20000 <= char_idx <= 0x2A6D6 or \
       0x2A700 <= char_idx <= 0x2B734 or \
       0x2B740 <= char_idx <= 0x2B81D or \
       0x2B820 <= char_idx <= 0x2CEA1 or \
       0x2F800 <= char_idx <= 0x2FA1D:
        return True

    return False

或者写个正则

import re

CHINESE_CHAR_PAT = re.compile(
    r'[\u3400-\u4DB5\u4E00-\u9FCC\uF900-\uFAD9'
    r'\u20000-\u2A6D6\u2A700-\u2B734\u2B740-\u2B81D'
    r'\u2B820-\u2CEA1\u2F800-\u2FA1D]'
)

def is_chinese_char(char):
    return bool(CHINESE_CHAR_PAT.match(char))

以上都是笨办法,因为实际上我们并不需要去记中文的编码范围,而且由于 Unicode 是在扩展的,如果将来扩充了,那么扩充进来的新的字可能就没有办法用上面的方法检查了。前面提到,Unicode 标准做了多方面的整理,而这些整理结果都作为属性附加到每个 Unicode 字符上了。

首先,每个 Unicode 字符都会被赋予一个名字,下面是一部分对照表

Unicode Name
a LATIN SMALL LETTER A
9 DIGIT NINE
CJK UNIFIED IDEOGRAPH-6211
α GREEK SMALL LETTER ALPHA

对于中文,我们只要取其 name,然后判断是否包含 CJK 这个关键词就行了。要怎么获取 Unicode 字符的 name 呢?用 Python 标准库里的 unicodedata 模块即可

import unicodedata

def is_chinese(char):
    return unicodedata.name(char).startswith('CJK')

而除了 name,Unicode 字符还有 block 和 script 两个属性:block 其实就是我们前面提到的连续编码区域,不过会有一个名字;script 是指每个文字的书写体系,可能会包含多个 block,详情见 文档

前面提到的汉字的编码区域和 block 名称的对应关系如下表所示

Block Name Block Range
CJK Unified Ideographs Extension A U+3400 - U+4DB5
CJK Unified Ideographs U+4E00 - U+9FCC
CJK Compatibility Ideographs U+F900 - U+FAD9
CJK Unified Ideographs Extension B U+20000 - U+2A6D6
CJK Unified Ideographs Extension C U+2A700 - U+2B734
CJK Unified Ideographs Extension D U+2B740 - U+2B81D
CJK Unified Ideographs Extension E U+2B820 - U+2CEA1
CJK Compatibility Ideographs Supplement U+2F800 - U+2FA1D

汉字对应的 script 名字是 Han,直接包括了上述所有 block。使用 Python 的 regex 这个工具,可以直接在正则表达式中使用 block 和 script。仍以汉字判断为例,可以这么写

import regex

def is_chinese_char(char):
    return bool(regex.match(r'\p{script=han}', char))

可惜在标准库 unicodedata 中并没有访问 Unicode 字符的 block、script 等属性的方法。

对于其他语言的文字,将上述方法中的参数(编码区域、script 等)稍作修改也是可行的,不再赘述。

用 category 属性判断标点、数字、货币单位等

Unicode 数据中,每个 Unicode 字符还有一个叫做 category 的属性,这个属性和字从属的语言无关。category 一共有 Letter、Mark、Number、Punctuation、Symbol、Seperator、Other 七大类,然后每个大类下还有一些小类,总体上是一个二级分类结构。因此在 Unicode 中有两个字母来组合表示一个 Unicode 字符的类型信息,我们可以用 unicodedata.category 来得到这个信息

import unicodedata

for char in '1天。':
    print(char, unicodedata.category(char))

结果为

1 Nd
天 Lo
。 Po

类型码和分类信息的对照表如下

类型码 类型信息
Lu Letter, uppercase
Ll Letter, lowercase
Lt Letter, titlecase
Lm Letter, modifier
Lo Letter, other
Mn Mark, nonspacing
Mc Mark, spacing combining
Me Mark, enclosing
Nd Number, decimal digit
Nl Number, letter
No Number, other
Pc Punctuation, connector
Pd Punctuation, dash
Ps Punctuation, open
Pe Punctuation, close
Pi Punctuation, initial quote (may behave like Ps or Pe depending on usage)
Pf Punctuation, final quote (may behave like Ps or Pe depending on usage)
Po Punctuation, other
Sm Symbol, math
Sc Symbol, currency
Sk Symbol, modifier
So Symbol, other
Zs Separator, space
Zl Separator, line
Zp Separator, paragraph
Cc Other, control
Cf Other, format
Cs Other, surrogate
Co Other, private use
Cn Other, not assigned (including noncharacters)

如上,标点符号的类型码都是 P 开头的,根据这个就能把标点筛出来了

import unicodedata

def is_punctuation_char(char):
    return unicodedata.category(char).startswith('P')

类似的,货币单位符号的类型码为 Sc,可以直接判断

import unicodedata

def is_currency_char(char):
    return unicodedata.category(char) == 'Sc'

类型码 N 开头的是数字字符,除了我们常见的十个阿拉伯数字外,像罗马数字、带圆圈的数字序号等都被涵盖在内。

此外类型信息也可以在 regex 这个工具里使用,例如

  • 找到文本中所有数字

    regex.findall(r'\p{N}', '第⑩项')
    

    结果

    ['⑩']
    
    
  • 找到各种括号表示开始的那一个

    regex.findall(r'\p{Ps}', '「Unicode」(又名万国码)见《标准》')
    

    结果

    ['「', '(', '《']
    
    

    或找到表示结束那一个

    regex.findall(r'\p{Pe}', '「Unicode」(又名万国码)见《标准》')
    

    结果

    ['」', ')', '》']
    
    

根据这张类型表,我们也可以写出一个用于文本预处理的简单清洗函数来,用来把一些奇奇怪怪的字符都从文本里去掉

import regex

def clean_text(text):
    # 去除明确无意义的字符
    # 1. Zl: Separator, line
    # 2. Zp: Separator, paragraph
    # 3. Cc, Cf, Cs, Co, Cn
    text = regex.sub(r'[\p{Zl}\p{Zp}\p{C}]', '', text)

    # 将空白符归一化
    text = regex.sub(r'\p{Zs}', ' ', text)
    return text

我们还可以根据数字类型 Unicode 字符的 name 来将其归一化到阿拉伯数字上,先看看数字类型的 Unicode 字符的 name 吧

import unicodedata

chars = ['⑩', '1', 'Ⅲ', '〹', '⒗']
for char in chars:
    print('{}: {}'.format(char, unicodedata.name(char)))

结果

⑩: CIRCLED NUMBER TEN
1: DIGIT ONE
Ⅲ: ROMAN NUMERAL THREE
〹: HANGZHOU NUMERAL TWENTY
⒗: NUMBER SIXTEEN FULL STOP

可以看到,在 NUMBER/DIGIT/NUMERAL 后面的那个单词,就是对应数值的英文单词,只要把这个英文单词提取出来就得到了一个统一的表示,然后再将其转换成阿拉伯数字即可。

NLP哪里跑: 什么是自然语言处理

2018年6月25日 08:00

本文是《NLP 哪里跑》系列的第二篇文章,系列文章如下:

  1. NLP哪里跑: 开篇及一些碎碎念 · ZMonster's Blog
  2. NLP哪里跑: 什么是自然语言处理 · ZMonster's Blog
  3. NLP哪里跑: Unicode相关的一些小知识和工具 · ZMonster's Blog
  4. NLP哪里跑: 文本分类工具一览 · ZMonster's Blog

什么是自然语言处理

  • 教授:“你笑什么?”
  • Z:“老师,学自然语言处理是我从小的梦想,能在这里我很开心!”
  • 教授:“用不着开心,给自然语言处理下个定义。”
  • Z:“能帮助我们理解自然语言的技术就是自然语言处理。”
  • 教授:“能说详细点吗?”
  • Z:“自然语言处理能帮我们理解人类的语言。你写了一篇文章,编辑器告诉你有些字写错了,这是自然语言处理;来了一封邮件,分个类告诉你这是垃圾邮件,这是自然语言处理;在输入框里输入‘香农’两个字,搜索引擎把香农的百科、学术成果展示在你面前,这是自然语言处理;你发了一条微博,说‘我想连任你的男朋友’,被微博自动删掉了,这是自然语言处理;你写论文从英文文献里抄了一段,用谷歌翻译转成中文放到自己论文里,这是自然语言处理;你对着 iPhone 说「hey siri 定 7 点钟的闹钟」,7 点钟 siri 提醒你该起床了,这是自然语言处理……”
  • 教授(生气):“净说废话!定义是什么!?”
  • Z:“我刚说了呀,老师。”
  • 教授:“考试你也这样?自然语言处理是,连任你的男朋友?白痴!换个人回答!”
  • M:“老师,自然语言处理可以定义为研究在人与人交际中以及在人与计算机交际中的语言问题的一门学科。自然语言处理要研制表示语言能力和语言应用的模型,建立计算框架来实现这样的语言模型,提出相应的方法来不断完善这样的语言模型,根据这样的语言模型设计各种实用系统,并探讨这些实用系统的评测技术。”
  • 教授:“太棒了!好极了!”

以上是一则瞎编的小品,套用了《三傻》中 Rancho 被教授问及“什么是机械装置”的场景。电影里通过这个场景里教授和“优等生”的死板与 Rancho 的灵活风趣之间的对比,来抨击僵硬死板的教育体制。不过电影总归是电影,它批评的问题我们要承认是客观存在的,但 Rancho 的行为是作为与一个极端对立的另一个极端来呈现并强化冲突和矛盾的,在我们认识事物、学习知识时,机械式地死板记忆固然不可取,毫无章法纯凭个人感性认识也不是什么值得鼓励的行为。

那么什么是自然语言处理呢?

自然语言处理,即 Natural Language Processing,简称 NLP ,是一门旨在利用计算机技术来理解并运用自然语言的学科。在不同的场景下,有时候也称之为计算语言学(Computational Linguistics, CL)或者自然语言理解(Natural Language Understanding, NLU)。

要理解 NLP 这个领域,只要紧紧抓住这几个要点就好

  1. NLP 主要通过计算机技术来进行

    在底层理论层面,NLP 会涉及数学、语言学、认知科学等多个学科,但在最后一般是通过计算机技术来承载这些理论知识并发挥效果的。

    看起来好像是废话,但仍然要在此进行强调,计算机和人脑不同,它有其优点也有其缺点,而 NLP 技术也会受到现在计算机技术优缺点的影响。因此不要用我们人脑处理自然语言的过程和效果来要求 NLP 技术 —— 对我们中的绝大部分人来说,使用语言是很自然、很简单的,但不要因此就觉得用计算机来处理也会很简单。

  2. NLP 要理解和运用的,是自然语言

    所谓「自然语言」是指在我们的世界中自然地演变出来的语言,比如说英语、汉语、法语……之所以称之为「自然语言」,是为了和程序设计语言(如 C 语言、Java 语言、Python 语言等)等人造的语言进行区分。

    程序设计语言是有非常明确的、固定的语法的,用程序设计语言写出来的每一个句子,都会有唯一确定的含义,因此计算机只需要按照语法规则对其进行解析并执行就好了。

    自然语言则不同,它有相对稳定的语法规则,但这些语法规则都会存在例外而且一直在演变,加上其他一些特性,经常会出现歧义。处理歧义是 NLP 中非常核心的一部分内容。

  3. NLP 试图理解自然语言,但何谓「理解」其实并没有一个确定的标准

    理想意义上的「理解」自然语言,是要求 NLP 可以像人脑一样理解自然语言,然而现在脑科学研究上对于我们在使用语言时大脑是如何运作的,并没有一个系统的、全面的认识。因此这也就不能称之为一个标准,实际上在现有的技术框架下,用计算机做到完全理解自然语言,是不可能的。

    退而求其次的,我们一般认为只要在特定的场景中,机器能对我们用自然语言表达的要求进行正确的响应,就是理解了自然语言。

    注意这里有几个前提

    • 「在特定的场景中」:一般我们认为,在限制了场景后,人们的目的以及语言的表达也会受到限制,因此能把语言表达的多样性降低,这样理解才具备可能性
    • 「进行了正确的响应」:我们认为机器的行为符合预期就是理解了,并不关心这中间的过程是否和人脑的运作机制是否一致、是否真正意义上的理解了语言的内涵

    当然,这只是现在实际的 NLP 系统所遵循的标准,事实上还是有人从语言学、脑科学等不同角度尝试确定「理解」的过程和标准,让我们保持关注、期待未来吧。

  4. NLP 在理解自然语言之后还有加以运用,因此凡是有用计算机来处理、分析自然语言的应用,我们都可以说它是一个 NLP 过程 —— 当然有可能不止是 NLP。

接下来我将根据这几点来讨论一下

  1. NLP 有哪些困难和限制
  2. NLP 有哪些主要的应用
  3. NLP 有哪些主要的技术

自然语言处理的困难和限制

我们每天都在使用自然语言,并不会对「理解自然语言」这件事情的困难之处有太多认识,但实际上,自然语言自身的一些特性,给计算机理解自然语言这件事情带来了非常多的困难。

第一个特性是自然语言中普遍地存在歧义现象,这种歧义体现在多个层面:

  1. 在语言的最小单元也就是词语的级别,存在多义词、同义词等现象

    比较经典的是 20 世纪 60 年代一位早期的机器翻译研究者给出的经典例子

    The box is in the pen.
    

    我们虽然可能不太清楚 pen 的另外一个含义是「围栏」,但肯定能意识到这句话中 pen 的含义不是「笔」。这个 badcase 从提出到现在已经有 50 多年了,但我们可以看到,仍然没有翻译系统能够在不加特殊处理的情况下解决这个问题(换言之这个 badcase 是可以通过特殊而丑陋的手段来解决的)。

    Google 翻译 google-translate.png

    百度翻译 baidu-translate.png

    搜狗翻译 sogou-translate.png

    与之相对的,中文里有一个例子是「意思」这个词的多义性

       他说:“她这个人真有意思(funny)。”她说:“他这个人怪有意思(funny)的。”于是人们认为他们有了意思(wish),并让他向她意思意思(express)。他火了:“我根本没有那个意思(thought)!”她也生气了:“你们这么说是什么意思(intention)?”事后有人说:“真有意思(funny)。”也有人说:“真没意思(nosense)”。(原文见《生活报》1994.11.13 第六版)
    

    当然,上述两个例子都是经过人为设计的精巧例子,大部分人们日常活动中使用的语言,并不会这么复杂。

    多义词之外,同义词也是非常普遍的语言现象,如:词语的缩写(如「人影办」之于「人工影响天气办公室」)、专有名词的别名(如「感冒」之于「上呼吸道感染」)、网络用语(如「蓝瘦」之于「难受」)、方言(如「包谷」之于「玉米」)、口语和书面语(如「脑袋」之于「头部」)……在 NLP 应用中,对同义词的辨别和处理工作也是非常多的,如:搜索引擎会通过查询改写(Query Rewrite)来将用户的搜索改写为同样含义但表述更为精准的语句;知识图谱里的实体链接技术本质上就是将实体名称(如人名、地名)的不同形式的表达和标准的表达对应起来;智能对话里可以通过同义词来更好地理解用户的问题……

    而在多义词、同义词理解上,目前 NLP 领域并没有一劳永逸的解决办法,有靠人工构建的小规模高质量知识库如 WordNet、HowNet、同义词词林,也有靠机器学习方法从大量语料中学习到词语的向量表示来隐式地反映词义 —— 量大但并不足够精确。前者往往质量很好但只能覆盖人们实际语言中很小的一部分,后者依赖于语料的数量、质量和领域,对于语料中较少出现的一些词往往就会得到莫名其妙的结果。

  2. 在句子乃至更高层的语言单元如段落、篇章里,又存在结构性歧义的现象

    以句子为例,它是由词组成的,词和词之间有些是有关联的、有些是没有关联的,因此整体会形成一个结构,也就是我们在学习语言时了解到的「语法」。

    理想的情况下,任意给定一个句子,如果它的语法结构是唯一、确定的,那么我们只需要把它这个语法结构计算出来,然后再把前面提到的同义词、多义词的问题也解决了,那么这个句子的含义就能确定了。但实际情况是,自然语言的语法规则并不是一个非常严格的规则,在句子比较复杂的时候,往往会造成句子可以有多种不同但都合理的解释。

    举一些例子

    • “喜欢乡下的孩子” 可以有下面两种解释
      • [喜欢/乡下]的/孩子: “喜欢乡下的”作为定语修饰“孩子”
      • 喜欢[乡下/的/孩子]: “乡下的孩子”作为“喜欢”的宾语
    • “他背着总经理和副总经理偷偷地把钱存进了银行” 可以有下面 2 种解释
      • 他[背着/总经理/和/副总经理]偷偷地/把/钱/存进/了/银行: “他”独自存钱
      • 他[背着/总经理]和/副总经理/偷偷地/把/钱/存进/了/银行: “他”和“副总经理”一起存钱
    • “放弃美丽的女人让人心碎” 可以有下面 2 种解释
      • [放弃/美丽/的]女人/让/人/心碎: “女人”让人心碎
      • [放弃/美丽/的/女人]让/人/心碎: “放弃”让人心碎

    再看英文的一个经典例句: "Put the block in the box on the table"。它可以有两种解释:

    • Put the block [in the box on the table]: "on the table" 修饰 "box"
    • Put [the block in the box] on the table: "on the table" 限定 "block"

    如果再加上一个介词短语 "in the kitchen",那么它可以有 5 种不同的解释;再增加一个的话,将会得到 14 种不同的解释。就这个介词短语导致歧义的情况来说,可能的歧义结构数量基本上是随着介词短语数量的增加而呈指数级上升。「指数级上升」是什么概念?那意味着想要靠枚举所有的可能性来解决歧义是非常低效甚至不可能的。

上述两种情况互相组合,又会造成更多、更复杂的歧义情况。

正如前文所说,如何处理歧义并排除歧义,是 NLP 中非常主要的一部分内容,而且是非常困难的一部分,这是因为造成语言歧义的原因多种多样。

造成词语歧义的原因,前面已经说过了一部分,也就是缩写、别名、网络用语、方言、口头语和书面语这几个;除此以外,比喻、拟人、谐音等手法也会使已有的词产生新的含义或和原来不相关的词产生同义;还有省略、指代等依赖上下文的情况也容易造成歧义……总之造成词语歧义的情况是非常多的。

在词义消歧这个问题上,除了造成歧义的情况多种多样外,还有一个困难是消歧经常依赖于文本之外的、海量的「常识」,比如说前面那个机器翻译的例子「The box is in the pen」,我们为什么能判断「pen」的含义不是「笔」,是因为我们具备这些知识

  1. 盒子是有体积的
  2. 笔不是一个容器
  3. 相对盒子,笔一般都更小

并由此推理出:盒子在笔中是不合理的。但是抱歉,这种我们认为是常识的知识,计算机并不具备,它是无法作出这种推理的。我们说的机器学习、深度学习,本质上还是依赖于数据,也就是说,要见过相同的或者相近的数据,它才能理解,对于知识本身则基本没有学习到。

在 NLP 发展历程中,也有人耗费巨大精力和金钱来将我们说的知识和规则整理成计算机可操作的数据,如 CycDBpediaFreebase,并演变成我们现在所说的知识图谱(其实以前就叫「知识库(Knowledge Base)」的,「Knowledge Graph」最早是 Google 的一个知识库的名字),但在 NLP 任务比如说词义消歧中,如何使用这些知识,仍然是一个很大的问题。

至于结构歧义或者说语法歧义,其原因也可能有几方面

  1. 当其中的词语有歧义时,这个词的不同词义可能具有不同的语法功能,而造成最终的语法结构不同,如前文的例句「放弃美丽的女人让人心碎」的两个不同解释中,「美丽」一词分别作为名词和形容词,而造成了不同的语法解析结果
  2. 自然语言语法本身并不是确定性的,即存在一些情况,哪怕其中每个词的词性、词义都是确定无疑的,可以得到的合理的语法解析结果也会有多个,前面那个「他背着总经理和副总经理偷偷地把钱存进了银行」的例句和那个英文例句就是这种情况

再加上语言一直在演变,新的语法规则不断出现,比如原来认为是错误的语法因为被普遍使用而被接受为新的语法规则,比如原有的语法规则被简化称为更简单的语法规则。这就导致很多语法规则都会有例外,而很多例外逐渐又成为了新的语法规则,就造成了「所有规则都有例外,所有例外都是规则」的现象。这就导致如果我们如果想靠语法规则来完成任务的话,最终就会陷入不停补充新的语法规则(或说例外)的境地里。

前面说了歧义这个自然语言的特性,而自然语言还有一个很重要的特性,就是前面提到的语言的动态演变。除了它给带来的歧义现象,更重要的是,由于语言的动态演变,新的知识、语言现象一直在出现,如何捕获、学习到这些新的知识和语言规则,对 NLP 来说,同样是非常大的一个挑战,因为如果不能学习到新的语言知识,那么建立在旧数据上的 NLP 模型,往往连基本的分析都做不到,更别谈去解决歧义并理解了。

此外,对中文、日文等一些语言来说,还有特殊的一点,就是在文字中,词和词不是天然就分隔开的。出于效果和效率等多方面的考虑,现有的 NLP 方法基本都建立在词的基础上,所以对中文这些语言来说,还需要对文字进行额外的、确定词和词之间边界的处理,也就是我们常说的「分词(Word Segmentaion)」。而分词这个过程本身,又存在不确定性,即同一个句子,可能的分词结果不一定是唯一的。

先看看这些例子

  1. “梁启超生前住在这里” 可能有两种分词结果
    • 梁启超/生前/住/在/这里
    • 梁启/超生/前/住/在/这里
  2. “武汉市长江大桥” 可能有两种分词结果
    • 武汉/市长/江大桥
    • 武汉市/长江/大桥
  3. “阿拉斯加遭强暴风雪袭击致多人死亡” 可能有两种分词结果
    • 阿拉斯加/遭/强/暴风雪/袭击/致/多人/死亡
    • 阿拉斯加/遭/强暴/风雪/袭击/致/多人/死亡
  4. “已取得文凭的和尚未取得文凭的干部” 可能有两种分词结果
    • 已/取得/文凭/的/和/尚未/取得/文凭/的/干部
    • 已/取得/文凭/的/和尚/未/取得/文凭/的/干部
  5. “今后三年中将翻两番” 可能有两种分词结果
    • 今后/三年/中将/翻/两番
    • 今后/三年/中/将/翻/两番

可以看到,仅仅是分错一两个词,整个句子的含义就完全不一样了。中文分词的困难在于,如果要正确地进行分词,就要对句子的语义有正确的理解;另一方面,要正确理解句子的语义,又需要正确的分词结果。于是就出现了一个先有鸡还是现有蛋的问题。

虽然有上述诸多困难,但 NLP 领域也形成了相应的应对方法,不过也只能说是「应对」而非「解决」,因为这些方法的目的都是追求解决上述问题在实际应用中常见的一部分,使对应的 NLP 系统能在受限场景中正确处理绝大部分比如 80% 或者 90% 的自然语言;而剩下未能处理的部分,则可以通过系统、产品上的一些设计,根据用户行为把它们找出来,加以研究并更新技术,如此逐渐迭代,来让 NLP 系统逐渐达到令人满意的效果。

自然语言处理的主要应用和关键技术

我们比较熟知的一些比较成体系的 NLP 应用,有

  • 机器翻译: 将一种自然语言的文字转换成另外一种自然语言的文字
  • 信息检索: 从海量的文档里检索出我们需要的信息,搜索引擎如 Google/Bing/Baidu 就是非常典型的信息检索系统
  • 文本摘要: 从较长的文档或文章中生成篇幅更小但内容完整的摘要
  • 智能对话: 通过对话的形式直接自动回答用户的问题或者执行特定的行为
  • 垃圾邮件过滤: 将垃圾邮件筛选出来并进行标记
  • 與情分析: 检测民众对公共事件的意见、态度、情感等倾向

当然实际上 NLP 的应用是非常多的,限于篇幅这里只讨论一下上述比较主要的应用。

上面六个 NLP 应用,其实可以划分成两类,一类是需要理解文本的完整语义并作出响应,如机器翻译、文本摘要和智能对话;另一类只需要理解文本中特定的信息就可以,对于文本的完整语义并不太关心,比如信息检索、垃圾邮件过滤和與情分析。当然我这样划分并不太严格,比如说搜索引擎里也会有人用自然语言去描述自己的问题并期望得到正确的结果,而垃圾邮件过滤和與情分析有时候也需要在理解完整语义的基础上再去提炼「特定信息」,但大部分情况下我认为这样划分是没有问题的。

在第一类即机器翻译、文本摘要和智能对话中,前两者都有明确的目标和评估标准,比如机器翻译有 BLEU 指标,文本摘要有 ROUGE 指标,而智能对话至今仍没有一个通用的评估标准,要么是借用一下机器翻译的 BLEU,要么是在特定的对话形式和对话任务里制定特殊的标准 —— 比如任务型对话里的槽填充率、检索型问答里的命中率和召回率。一个真正可用的智能对话系统,必然是一个融合系统,既有任务型问答也有检索型问答,说不定还会有开放域闲聊,那么在这样一个融合的系统里,各扫自家门前雪肯定是不行的,所以就现在来说,智能对话会比机器翻译和文本摘要要难一点 —— 倒不是说技术上难,而是难在确定目标和评估标准上。

在第二类即信息检索、垃圾邮件过滤、與情分析这三者中,與情分析除了要分析出情感、态度倾向,还要确定这种倾向针对的是什么对象,所以相比信息检索和垃圾邮件过滤,又会更难一点。

总的来说,在大部分情况下,对于 NLP 的应用,可以按照下面的原则来判断其难易程度

  • 需要理解完整语义的任务,要比只需理解部分信息的任务更难
  • 有确定目标和评估标准的任务,要比没有的更容易
  • 要做复杂分析的任务,要比只做简单分析的任务更难

在技术方面,各个应用会涉及的更底层的关键 NLP 技术如下:

  • 早期基于规则的翻译系统: 句法分析
  • 统计机器翻译: 语言模型,隐马尔可夫模型(HMM,早期被广泛用于序列标注)
  • 神经网络机器翻译: seq2seq,注意力模型
  • 信息检索: 大量的文本预处理,实体抽取,主题模型,文本相似度量,知识图谱
  • 检索型的问答系统: 同信息检索,但还需要进行指代消解、省略消解
  • 任务型的问答系统: 意图识别,实体抽取,指代消解,省略消解,对话状态管理,自然语言生成
  • 垃圾邮件过滤: 文本分类
  • 與情分析: 观点词抽取,情感分析,关系抽取,意见挖掘
  • 文本摘要: (这个不太清楚,略过)

上面的东西就是罗列一下以便形成整体印象,实际上像机器翻译和文本摘要我都没有做过,所以里面的关键技术我说不了太多,而信息检索、智能对话方面我相对有经验一些因此就写得比较多。

虽然各个应用的目标都不一样,但最后我们会发现它们用到的很多方法都是共通或者干脆是一样的。

最基础的,各个应用在开始前都需要对文本做预处理,不过预处理是一个很宽泛的概念,并没有一个或一套特定的方法,就我个人经验来说,因为处理的都是汉语,所以基本上都会做简繁统一转换,会把全角字符转成半角字符,会做标点的归一化处理(我才不会告诉你希腊字母里的问号在外形上和英文的分号一个样子呢),会去除一些无效的字符(比如说不可见的零宽度的空白符);英文里则会做词干提取、词形还原。很多讲 NLP 的文章说到预处理都会说停用词(stopwords)去除 —— 所谓停用词是指在特定领域里使用频率很高且往往不表达语义的词,如「了」、「的」,但实际上不是所有的应用上都需要去停用词、都可以去停用词的,比如说在作者身份鉴别这个应用上,是重度依赖虚词、介词等所谓功能词的,而在 NLP 里,虚词、介词这种词一般都会被视作停用词。

预处理的目的是让文本变得更「干净」,少一些「脏东西」,同时尽量更加地规范,所以有时候也会把预处理形象地称为「清洗」。好的预处理能减少干扰信息,并且保留重要信息,同时一些规范化处理(如繁简统一转换、标点归一化处理、英语的词干提取和词形还原)还能够减少后续步骤需要处理的信息量。

在做完预处理后,一般就是变着花样从文本里提取信息出来,以机器学习的角度来讲就是特征提取。根据不同的应用,这一步可能会非常简单,也可能会非常复杂。

最初始的一个处理,就是从文本里,把词都提取出来,这一步对英文等语言来说很自然,按空格和标点分一下就好了,而对中文来说则需要专门做中文分词,这个是前面提到过的。从计算语言学的角度来讲,分词是「词法分析」的一部分,所谓「词法分析」除了确定词和词之间的边界,还需要确定词性(一个词是动词、名词还是别的什么)、词义(「苹果」是一种水果还是一种电子产品),通俗一点来说,就是要解决: 哪些是词,有哪些词,各个词都是什么意思。

分词的方法有多种,简单的如准备好一个超大的词表,然后在这个词表里找可能拼成当前这句话的词,然后在多种可能的组合里确定一个最有可能的,看着很粗暴但其实也还可以。不过现在主流上都是将分词当作序列标注问题来处理。所谓序列标注是说,给一个序列,模型能给这个序列上每一个元素都标注成为一个特殊的记号,对分词而言,要标注的记号可以有三种: 是不是词的第一个字,是不是词的最后一个字,是不是词中间的字。

序列标注问题并不是 NLP 中特有的,但有不少 NLP 任务或技术都和序列标注有关,刚才说到的分词和词性标注(确定词性),基本上都是当作序列标注问题来处理。除此以外,实体抽取或者更复杂的信息抽取,也被视为序列标注问题。所以如果是作为一个 NLP 从业人员,序列标注问题的相关方法,是必须要掌握的,建议 HMM、CRF、RNN 的原理摸熟吃透,掌握一两个好用的序列标注工具,囤点序列标注问题的数据集打磨一下自己的感觉。

虽然说主流的方法都是将分词当作序列标注问题来做,但在实践中,囤点词典还是有益无害的,比如说做做新词挖掘、同义词挖掘、领域实体词的积累等等。至于怎么做这些挖掘、积累,各个应用有各个应用的做法,比如说搜索引擎靠 query 点击日志能把同义词或近义词找出来,原理就是类似两个人用了不同的词去搜索,但最后都点击了同一个链接这种思路;没有搜索引擎这样比较方便的用户反馈的,可以通过一些无监督或半监督的方法来做,会更难一些但总之是有方法的。

做完词法分析后,一般会有一些不同的后续处理

  1. 区分哪些词是重要的,哪些词是不重要的,可能涉及到关键词提取、实体抽取、停用词去除等
  2. 分析哪些词之间是有关联的,哪些词之间是没有关联的,可能涉及到短语抽取、关系抽取、句法分析等
  3. 基于词对整个文本进行分类得到一个高度抽象的结果,可能涉及情感分析、意图识别、主题分类等

实体抽取前面说了,可以当作序列标注问题来做。停用词去除没什么好说的,一般就是在通用的一个停用词表的基础上,统计一下数据中高频的词加进去而已。关键词提取我觉得是很重要的一件事情,不过大部分情况下用 TFIDF 就能提取出不错的关键词来了,在此基础上辅以之前词法分析的一些结果如词性基本上就能把关键词捋得差不多,基本上名词、动词、形容词、实体词拿出来做关键词就差不多了。

这里说到了 TFIDF,这个也是 NLP 里非常经典非常重要的一个技术,思路是很简单的,就是说对一个词,用两个数值来反映其在某篇文档或者某个句子里的重要程度,第一个是所谓词频(Term Frequency, TF),即这个词在当前文档/句子中出现的次数;第二个是所谓逆文档频率(Inverse Document Frequency, IDF),指出现过这个词的文档/句子的数量的倒数。通俗来说,就是,一个词在其他文档/句子里越少出现(IDF 值比较大),在当前文档/句子中越多出现,那么它在当前文档/句子中就越重要。很朴素的思想,实际上这个思路最终是可以用信息熵来解释的,总之很实用。

短语抽取、关系抽取和句法分析都是为了得到文本中的层次结构以更好地理解语义。其中短语抽取较简单,因为只需要看前后两个词是否有关系就好了,而关系抽取和句法分析都需要分析不相邻的词之间是否存在关联。这块不太熟,只是用过相关工具。不过我的观点是,句法分析是理解语义非常核心非常重要的一件事情,一个好的句法分析系统或工具,能够大大提高整个系统的效果。

然后是情感分析、意图识别、主题分类这些技术,其实大部分情况下都是文本分类在不同应用场景里的别名而已。文本分类是一个相对简单但应用非常广泛的技术,也是一块值得花精力去好好掌握的内容,逻辑回归、支持向量机、GBDT,然后是深度学习常见的分类模型结构,把这些都好好掌握一下,并且在实际数据中熟练掌握好预处理、特征提取、特征选择的流程和技巧,受用无穷。至于说深度学习不用特征的都是傻蛋,我分类的时候用深度学习一样能把特征加上去,就是比你不加好,爱调不调。

上述技术,基本上就能把任务相关的「理解」做得差不多了。在理解之后,一般还会有涉及应用的更具体的响应处理,如机器翻译、文本摘要、智能对话中会有自然语言生成,信息检索及检索型问答系统会有文本相似度量。

其中自然语言生成(Natural Language Generation, NLG)至今仍然是一个很困难的问题,在技术上,有基于统计语言模型的生成方法,也有基于神经网络语言模型的生成方法,近几年的变分自编码器(Variational Auto-Encoder, VAE)和基于强化学习的生成方法使用得也越来越多了。但这里说的 NLG 多是在特定任务中,基于前面已经有的分析结果来生成较短的文本,至于生成小说什么的,看看都没有媒体和公司去吹就知道是多么难的一件事情了。

这里提到了语言模型,也是一块可以好好掌握一下的内容。

然后就是文本相似度量,也是非常重要的一项 NLP 技能。某种意义上来说,它和 NLP 的终极目标是等价的。如果任意给定两个句子,一个 NLP 系统能够准确地判断这两个句子是相似还是不相似,那么它就已经是一个完全理解自然语言的系统了。以机器翻译为例,其 BLEU 指标,就是判断翻译出来的句子,和给定的标准翻译是否相似,但实际上其计算方法很简单只是计算了表面相似,所以 BLEU 指标也并不是非常准确,在实际应用中还是要靠人来评估实际效果,但是这么多年了学术界和业界也并没有找到比 BLEU 更好多少的指标,所以将就用着,倒是也有 AMBER 和 METEOR 这种不光看表面相似的评估标准,但是计算太复杂了,所以并没有成为公认的标准。假如有一天有更好的评估标准能和人为评估效果更加接近,那才是机器翻译的大突破。

文本相似的困难对很多非技术人员或者非 NLP 领域人员来说也经常难以理解,“这两个句子就是一个意思为什么就不理解呢”这样的话也是经常能听到的。理解一下,它真的很难。

简单的、传统的相似度量有 LCS 相似度、Jaccard 系数、余弦相似度等,都是久经考验非常实用的方法,应对一些简单的任务措措有余,当然这些方法基本上都是看表面相似,所以泛化性会差一些,需要不断补充特征才能提高效果。如果有足够数据的话,一些基于深度学习的匹配模型会有非常不错的效果,这个不错不光是指其在固定的测试集上的效果,在集外数据的泛化性上都会很不错,当然前提是要有米。

文本相似度量的应用也是很多的,在机器翻译和文本摘要里可以用来评估系统效果,在信息检索和智能对话里可以用来匹配正确的文档或问题,甚至有些时候一些分类任务也可以用文本相似度量来解决(有 A 类数据 1000 条 B 类数据 1000 条,判断下和 A 类数据更像还是和 B 类数据更像)。

另外一方面,除了给定两个句子判断其相似程度外,相似句子的挖掘和生成,也是很有意思的一件事情。生成相似问题这件事情,专业的说法叫做「复述(Paraphrase)」,它和相似度量是互相促进的,相似度量做的好,那么就可以挖掘出更多的相似句子来做训练复述生成模型;复述生成做的话,反过来又可以给相似度量模型提供训练数据。

对于深度学习在 NLP 中的应用,比较核心的技术有

  1. word embedding,也就是我们俗称的词向量,通过大量数据无监督(不需要标注)训练,可以用一个向量来表示一个词的词义,理想情况下如词性、词义、情感、主题等各种信息都可以在这个向量中得到表达 —— 当然这只是一个感性认识,目前对于这个向量实际的含义和解释,我们并不太关心,好用就对了。此外 word embedding 因为能比较好地反映词义,还可以用来做关键词、同义词挖掘,在各项深度学习相关的 NLP 任务中,基本也是标配了
  2. 序列标注方面,BiLSTM+CRF 已经是很主流的做法
  3. seq2seq 和 attention,这两个技术都是在神经网络机器翻译的研究中被提出来的,然后也被广泛用到如智能问答、文本摘要等重要 NLP 应用上。这两者一般是一起出现的,现在已经演变成了一种计算框架,具体的方法和细节则已经有了很多的变化
  4. 自然语言生成方面前面提到过 VAE 和强化学习,近一年内生成对抗网络(Generative Adversarial Nets, GAN)也开始有在这方面的应用

深度学习相关的技术一般都会需要比较多的数据,但我们会发现在实际的任务中,可能并没有那么多数据来让我们上深度学习,或者数据太多但却包含了太多噪音数据。土豪的办法是用金钱来为所欲为顺便创造点特殊岗位造福社会,对这件事情我是举双脚支持的。比较经济的做法是先使用前面提到的传统的、经典的方法,同时在产品、系统的上设计好良好的反馈渠道,在冷启动后不断进行迭代优化,同时有意识地从日志、用户反馈中积累数据,到达一定的量后开始上深度学习来解决传统模型泛化性不够好的问题。

NLP哪里跑: 开篇及一些碎碎念

2018年3月26日 08:00

本文是《NLP 哪里跑》系列的第一篇文章,系列文章如下:

  1. NLP哪里跑: 开篇及一些碎碎念 · ZMonster's Blog
  2. NLP哪里跑: 什么是自然语言处理 · ZMonster's Blog
  3. NLP哪里跑: Unicode相关的一些小知识和工具 · ZMonster's Blog
  4. NLP哪里跑: 文本分类工具一览 · ZMonster's Blog

从事算法工作已经有四年多了,回过头来看过去几年自己做的事情,说实话,谈不上优秀和努力,中规中矩平凡普通,仅此而已。去 Crow 家玩,给他在中科院读研即将毕业的妻子梳理 NLP 的脉络,看到的仿佛是刚工作时候的自己 —— 什么概念都有一点,但又仿佛什么概念都没有,不知道该往哪个方向走,但焦虑和欲望又逼迫你不得不往前走。后来懂得越来越多,做得事情也越来越多,心思却越来越浮泛,在重复繁琐机械的工作里寻得一股虚伪的充实感,只有在偶尔写日记的时候、拖着疲惫的身体行走在路上的时候,自省的想法才会一闪而过。

工作呢,说实话有一个靠谱的团队已经是谢天谢地了,倒也没有办法说我一定要做我感兴趣的事情,毕竟团队需要的是分工协作而不是自大狂的我行我素。话是这么说有时候还是会觉得好无趣啊,想做点有趣点的事情呢。

跑题了,总之,我的一些思考、见闻,我想把他们都写出来。正好最近对 NLP 也就是自然语言处理开始有了一些大致上比较体系的想法了,开始在整理,所以决定开一个系列,专门讲讲 NLP 的东西 —— NLP 是什么呀,有什么应用啦什么的,有什么好玩的东西啦……系列名「NLP 哪里跑」是顺手瞎起的哈哈,"Why so serious",对不?

正经地讲,可能(但不保证)会有以下内容

  1. 个人对 NLP 的整体性思考,会涉及 NLP 的一些基础概念、技术
  2. 常见 NLP 的一些应用场景和解决思路
  3. 好用的轮子介绍,或者顺手造点轮子
  4. 前沿方向的介绍和踩坑
  5. NLP 与其他领域结合的好玩的例子 —— 比如去年在公司年会里分享的《NLP 在文学分析及创作中的应用》

以上。

强大的 Org mode(4): 使用 capture 功能快速记录

2018年2月28日 08:00

本文是《强大的 Org mode》系列的第四篇文章,系列文章如下:

  1. 强大的 Org mode(1): 简单介绍与基本使用 · ZMonster's Blog
  2. 强大的 Org mode(2): 任务管理 · ZMonster's Blog
  3. 强大的 Org mode(3): 表格的基本操作及公式、绘图 · ZMonster's Blog
  4. 强大的 Org mode(4): 使用 capture 功能快速记录 · ZMonster's Blog

简介

Capture 是 Org mode 中非常重要的一个功能,使用它可以让我们快速地新建内容到特定的 Org mode 文件中去,具体一点,可以有下面这些场景

  • 新建一条笔记到 inbox.org 中,将剪贴板中的内容自动插入,并且附上当时的时间

    org-capture-note.gif

  • 新增一条日志,按照「年-月-日」的层级结构插入到 journal.org 中,如下图所示

    org-capture-journal.gif

  • 以表格的形式,新增一条消费支出记录到用于存放备忘信息的 memo.org 中

    org-capture-2.gif

  • 新增一条任务到 task.org 中,并且开始计时

    org-capture-task.gif

  • 新增一个代码片段到 snippet.org 中

    org-capture-snip.gif

上述看似很不一样的操作,只需要在配置里设置不同的 capture 模板即可,模板里支持的元素很多,甚至能在模板里写 elisp 代码来做到已有模板元素不能做到的事情。在写好模板并加载后,我们只需要调用 org-capture 这个函数,就能在弹出的临时 buffer 里选择对应的模板来记录不同的内容,而不用耗费精力去记忆应该打开哪个文件。

org-capture-buffer.png

此外,使用 capture 后将会打开一个临时的 buffer,在我们编辑好内容后轻按 C-c C-c,它就会消失无踪,因此对我们原先在做的事情的打断非常轻微。

总结一下就是:

  1. capture 可以预先设置记录内容的模板和存储入口
  2. capture 提供统一的输入入口
  3. capture 用完即走,不干扰当前工作流

如果你是一个 Org mode 用户,应该会用 Org mode 来做笔记记录、日志记录、任务管理这些事情,而这些事情,用 capture 来作为输入是非常自然而方便的,我也在这里建议,在 Org mode 环境里时,应当使用 capture 来作为主要的输入方式。

最小配置

capture 功能包含在 org 包里,所以只要安装了 org,那么直接就是能使用 capture 功能的。不过不做配置的话,那么

  • 没有快捷键可以触发功能
  • 默认只有一个用于创建任务的 Task 模板可选,并且存储在变量 org-default-notes-file 指定的文件里

默认的模板是在 org-capture-select-template 中定义的,其逻辑是,当执行 org-capture 命令的时候,如果检查到没有配置任何模板,就会使用一个默认的模板来保证不会出错,这个默认的模板如下所示

'("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n  %u\n  %a")

但是呢,如果我们连 org-default-notes-file 都没有设置,它会默认存储到 ~/.notes 中去,然后会由于这个文件不是 Org 文件而报错……

所以,假如我们想真正地使用起 org-capture 来,最小的配置工作,应该包含下述事情

  • 为 org-capture 命令设置一个快捷键
  • 设置 org-default-notes-file 变量的值为一个 Org 文件,比如说 ~/org/inbox.org

按照这个要求,可以得到最小的 org-capture 的配置如下

(global-set-key (kbd "C-c c") 'org-capture)
(setq org-default-notes-file "~/org/inbox.org")

这样,我们就能新建任务到 ~/org/inbox.org 这个文件中了,见下图示例

org-capture-minimum.gif

capture 模板的五个部分

上一节讲到,默认的 capture 模板是下面这个样子的

'("t" "Task" entry (file+headline "" "Tasks") "* TODO %?\n  %u\n  %a")

后面我们要自己添加新的模板,也是这个格式。这个模板包含五个部分,分别是

模板组成 对应默认模板中的内容 描述
key "t" 用来选择模板的字符
description "Task" 展示用的模板描述
type entry 新增内容的类型
target (file+headline "" "Tasks") 新增内容的存储位置
template "* TODO %?\n %u\n %a" 新增内容的模板

下面针对这五部分进行详细说明。

用于快速选择模板的 key

对应前面默认模板里的 "t",这个 key 可以是一个或两个字符,用来在执行 org-capture 的时候选择模板 —— 两个字符的情况是用来给模板分组的,第一个字符表示分组名,第二个字符用来选择这个分组下的实际模板。在我们有很多模板的时候,分组是非常有用的,一来可以让执行 org-capture 时显示的可选项更少,而来可以用来组织相近性质的模板以便管理。模板分组稍后一点会做详细说明,此处就先不展开了。

另外,经验证,这里的 key 是支持中文的 XD

描述模板的 description

对应前面默认模板里的 "Task",这个就是用来对模板进行描述的,方便我们正确地选择模板。

key 和 description 这两部分会在执行 org-capture 进入模板选择 buffer 后,会显示的内容,其他模板内容在模板选择 buffer 中都是不显示的,如下所示:

org-capture-buffer.png

这两部分,务必要注意

  • 不同模板的 key 不能是一样的
  • description 应当尽量清晰以减轻自己的记忆负担

设置新增内容类型的 type

对应前面默认模板里的 "entry",这个用来设置新增内容的类型,可选的类型如下表所示

type description
entry 带有 headline 的一个 Org mode 节点
item 一个列表项
checkitem 一个 checkbox 列表项
table-line 一个表格行
plain 普通文本

根据不同的 type,org-capture 会尝试将新增内容添加到文件中不同类型的数据的后面,比如

  • 如果 type 是 item/checkitem,那么会找到目标位置后最近的一个列表,并将新增列表项添加到这个列表的后面
  • 如果 type 是 table-line,那么会找到目标位置后最近的一个表格,并将新增行添加到表格的后面

因此对于不同的 type,要求后面的内容模板是按照一定的格式来编写的,下面是不同的 type 对应内容模板的简单示例

  • type 为 entry 时,内容模板示例

    "* headline"
    

    也就是说,template 的形式上必须是一个 headline

  • type 为 item 时

    如果内容模板为空,会插入一个普通列表项,并且等待输入;如果有需要自定义的内容,那么才需要去写内容模板。

    而此时的内容模板不需要在形式上是一个列表项,也就是说

    "- item"
    

    "item"
    

    的效果是一样的,都会在 target 对应的位置里插入下面这样一个列表项

    - item
    
  • type 为 checkitem 时

    与 type 为 item 时行为大部分一样,仅有一点区别,就是在内容模板为空的时候,它会插入一个 checkbox 列表项。

    也就是说,如果内容模板不为空,那么它其实是不保证插入的是 checkbox 列表项的,需要我们自己来保证。

    相应的内容模板应该是类似下面的格式

    "[ ] item"
    
  • type 为 table-line,内容模板示例

    "| colum 1 | colum 2 | colum3 |"
    

    就是说,内容模板必须是一个表格的行

设置新增内容写入位置的 target

对应前面默认模板里的 "(file+headline "" "Tasks")",target 用来指定

  • 新增内容要写入到哪个文件
  • 新增内容要写入到文件的什么地方

如前面的默认模板所示,target 部分用一个 list 来表示,其中第一个元素用来表示 target 的类型,可用的类型如下表所示

type description example
file 文件 (file "path/to/file")
id 特定 ID 的某个 headline (id "id of existing Org entry")
file+headline 文件的某个唯一的 headline (file+headline "path/to/file" "node headline")
file+olp 文件中的 headline 路径 (file+olp "path/to/file" "Level 1 heading" "Level 2" …)
file+regexp 文件中被正则匹配的 headline (file+regexp "path/to/file" "regexp to find location")
file+datetree 文件中当日所在的 datetree (file+datetree "path/to/file")
file+datetree+prompt 文件中的 datetree,弹出日期选择 (file+datetree+prompt "path/to/file")
file+weektree 文件中当日所在的 weektree (file+weektree "path/to/file")
file+weektree+prompt 文件中的 weektree,弹出日期选择 (file+weektree+prompt "path/to/file")
file+function 文件中被函数匹配的位置 (file+function "path/to/file" function-finding-location)
clock 当前正在计时中的任务所在的位置 (clock)
function 自定义函数匹配的位置 (function function-finding-location)

(翻译有点生硬,如有疑惑,请执行 「M-x describe-variable」并输入「org-capture-templates」查看对应的文档)

其中 file+headline 是比较常用的,用来记录笔记、创建任务一般用这个就好了。不过这个要求 headline 在文件中是唯一的,如果不是唯一的话,最好使用 file+olp,指定对应 headline 在文件中的完整路径。

而 file+datetree、file+weektree 这两种用来创建日志是非常合适的,记录的内容能按年-月-日的层级结构组织好,方便回顾和管理。

如果有自己的特殊需求,那么 file+function、function 这两个也提供了极大的自由扩展的空间。

需要注意的是,上述与文件相关的 target 类型,如果指定了文件名,那么将会优先使用这个文件名而不是变量 org-default-notes-file 指定的文件 —— 反之,如果文件部分留空,那么就会默认使用 org-default-notes-file 指定的文件了。

设置新增内容模板的 template

对应前面默认模板里的 "* TODO %?\n %u\n %a",这部分的内容是实际上新增内容的模板,通过设置它,我们可以在新增内容时

  • 自动插入时间、链接、剪贴板内容、文件内容
  • 交互式地要求输入特定内容,如 tag、headline 属性或其他自定义的字段
  • 自动插入外部应用传入的特定信息,如浏览器上当前网页的链接、选中的文本等

这部分的配置,其中的内容可以分为两类

  • 普通的文本,将会原样出现在新增内容中,如默认模板里的 "* TODO"、"\n"、" "
  • 以 % 开头的特殊标记,如 "%?" 和 "%a",将会在最后根据类型自动扩展成不同的内容

    这些特殊标记包括这些

    • 时间、日期相关

      标记 描述
      %<…> 自定义格式的 timestamp,如: %<%Y-%m-%d>,会得到 <2018-03-04 日>
      %t 当前仅包含日期的 timestamp,如: <2018-03-04 日>
      %T 当前包含日期和时间的 timestamp,如: <2018-03-04 日 19:26>
      %u 当前包含日期的未激活的 timestamp,如: [2018-03-04 日]
      %U 当前包含日期和时间的未激活的 timestamp,如: [2018-03-04 日 19:26]
      %^t 类似 %t,但是弹出日历让用户选择日期
      %^T 类似 %T,但是弹出日历让用户选择日期和时间
      %^u 类似 %u,但是弹出日历让用户选择日期
      %^U 类似 %U,但是弹出日历让用户选择日期和时间

      注: 激活(active)和未激活(inactive)的 timestamp 的区别在于,后者不会出现在 agenda 中 —— 所以如果是新建一个 headline 到 org-agenda-files 中并且不希望它出现在 agenda 列表中时,应当使用未激活的 timestamp。

    • 剪贴板相关

      标记 描述
      %c 当前 kill ring 中的第一条内容
      %x 当前系统剪贴板中的内容
      %^C 交互式地选择 kill ring 或剪贴板中的内容
      %^L 类似 %^C,但是将选中的内容作为链接插入
    • 标签相关

      标记 描述
      %^g 交互式地输入标签,并用 target 所在文件中的标签进行补全
      %^G 类似 %^g,但用所有 org-agenda-files 涉及文件中的标签进行补全
    • 文件相关

      标记 描述
      %[file] 插入文件 file 中的内容
      %f 执行 org-capture 时当前 buffer 对应的文件名
      %F 类似 %f,但输入该文件的绝对路径
    • 任务相关

      标记 描述
      %k 当前在计时的任务的标题
      %K 当前在计时的任务的链接
    • 外部链接的信息

      这里的链接不仅仅指如 http://www.google.com 这样的网页链接,还包括文件、邮箱、新闻组、IRC 会话等,详情见 Org mode 手册的 External links 一节。

      当然在 capture 里我们用不到所有类型的外部链接,从文档docstring 来看,在 capture 里能用的外部链接只有下面几种

      link type description
      bbdb BBDB 联系人数据库记录链接
      irc IRC 会话链接
      vm View Mail 邮件阅读器中的消息、目录链接
      wl Wunder Lust 邮件/新闻阅读器中的消息、目录链接
      mh MH-E 邮件用户代理中的消息、目录链接
      mew MEW 邮件阅读器中的消息链接
      rmail Emacs 的默认邮件阅读器 Rmail 中的消息链接
      gnus GNUS 邮件/新闻阅读器中的群组、消息等资源链接
      eww/w3/w3m 在eww/w3/w3m 中存储的网页链接
      calendar 日历链接
      org-protocol 遵循 org-protocol 协议的外部应用链接

      注: 文档的内容来自 org-mode 仓库 中的 doc/org.texi,从 commit 历史来看,可能是过时的;但奇怪的是 org-protocol 明明是支持的,docstring 里却完全没有提及……

      这些外部链接,大部分都会在 Emacs 中通过 org-store-link-pros 记录起来,其中会包含这些链接的各个属性,而在 capture 的模板里面,就支持以 %:keyword 的形式来访问这些属性,比如 vm/wl/mh/mew/rmail/gnus 消息中的发件人名称、发件人地址之类的。因为邮件阅读器这块我个人不怎么用,需要详细了解的请查阅文档,而 calendar 完全可以用前面的「时间、日期相关」中的 %t、%T 等标记来替代,因此这里只详细说一下 eww 和 org-protocol。

      eww 可用的特殊标记有如下三个

      标记 描述
      %:type 固定值,eww
      %:link 页面的链接
      %:description 页面的标题,如无则为页面的链接

      org-protocol 可用的特殊标记有如下六个

      标记 描述
      %:type 链接的类型,如 http/https/ftp 等
      %:link 链接地址,在 org-protocol 里的 url 字段
      %:description 链接的标题,在 org-protocol 里的 title 字段
      %:annotation 靠 url 和 title 完成的 org 格式的链接
      %:initial 链接上选中的文本,在 org-protocol 里的 body 字段
      %:query org-protocol 上除掉开头和子协议部分的剩下部分

      此外,在内容模板中还支持自定义函数来插入内容,以 %(sexp) 的形式,比如说我们可以自己写一个 get-current-time 函数来插入当前的时间,那么内容模板可以是这个样子的

      "%(get-current-time)"
      

      而在内容模板中使用自定义函数时,可以将上面 eww 和 org-protocol 的这些特殊标记作为函数的参数,比如一个场景是,用 org-protocol 捕获的网页 title 中包含中括号,会导致下面这样的内容模板出错

      "[[%:link][%:description]]"
      

      这个时候可以定一个一个函数来将 %:description 中的中括号替换成下划线

      (defun replace-bracket-in-title (title)
        ;; blablabla
        )
      

      那么上面那个内容模板可以改成这样

      "[[%:link][%(replace-bracket-in-title \"%:description\")]]"
      
    • 其他

      还有一些特殊标记,不太好归类,就在这里罗列一下。

      "%i" 可以插入一段初始化内容,通常是 org-store-link-plist 中 "initial" 属性的值;如果没有的话,会使用当前 buffer 中被选中的内容;都没有的话就什么也不插入。

      "%^{prop}p" 会提示输入内容,这将会在新增内容中插入一个 property 到 target 中,并且这个 property 的名字是 prop,值则是我们输入的文本。

      "%^{prompt}" 则会用 prompt 作为提示符要求我们输入,并且用我们输入的文本替换模板中相应的内容。比如说 "%{姓名}" 会用 "姓名" 作为提示符要求输入。当有多个标记时,可以用 "%\N" 来插入第 N 个提示输入标记产生的内容,举个例子,下面的内容模板

      "- name: %^{姓名}\n- age: %^{年龄}\n\n%\\1的年龄是%\\2"
      

      (注: 此处的反斜线「\」需要转义,否则「\1」会被视作值为 1 的 ASCII 码特殊字符,感谢 Emacs China 网友 slack-py 指出该问题)

      会要求我们输入姓名和年龄,假如我们输入姓名是 "张三",年龄是 "25",那么最后得到的内容是

      - name: 张三
      - age: 25
      
      张三的年龄是25
      

      "%?" 是一个更特殊的标记,它不会产生任何内容,当所有其他的特殊标记都展开完毕或者输入完毕后,光标将会停留在这个标记所在的位置。

capture 模板示例

所有的 capture 模板都应当以 list 的形式记录在变量 org-capture-templates 中,下面的示例可能会存在模板的 key 冲突的情况,请根据自己的情况来选用或参考示例。

在开始之前,我们先将 org-capture-templates 设置为空

(setq org-capture-templates nil)

用 org-capture 来做任务管理

GTD 一般会有一整套系统的设计,这里只讲一下最一般的新建任务的做法,下面是一个新建书籍阅读任务的示例

(add-to-list 'org-capture-templates
             '("r" "Book Reading Task" entry
               (file+olp "~/Dropbox/org/task.org" "Reading" "Book")
               "* TODO %^{书名}\n%u\n%a\n" :clock-in t :clock-resume t))

上面的两个属性 ":clock-in" 和 ":clock-resume" 在之前没有讲过,是用来对新建内容的行为做一些设置的,不影响内容本身。可用的这些属性一共有 14 个,这里及后面只对涉及到的做说明,其他的请查阅文档

":clock-in" 设置为 t 的时候,会在新建内容时开始计时,这在 GTD 这种场景下是挺有用的。但有可能我们在新建内容时,本来就有一个任务在计时,这种情况下原来的计时会中断掉,这个时候将 ":clock-resume" 设置为 t,可以在新任务完成后,自动恢复原来任务的计时状态。

有些时候我们会对我们需要做的任务做分类,比如上面有一个阅读任务,可能还有工作任务、写作任务,这个时候我们可以利用前面说到的模板分组来更好地进行管理。

在做模板分组前,我们的 org-capture 的任务模板可能是这样的

(add-to-list 'org-capture-templates
             '("r" "Book Reading Task" entry
               (file+olp "~/Dropbox/org/task.org" "Reading" "Book")
               "* TODO %^{书名}\n%u\n%a\n" :clock-in t :clock-resume t))
(add-to-list 'org-capture-templates
             '("w" "Work Task" entry
               (file+headline "~/Dropbox/org/task.org" "Work")
               "* TODO %^{任务名}\n%u\n%a\n" :clock-in t :clock-resume t))

在这个基础上,假设我们要添加一个模板,用来记录从网页上收集的资源、文章的时候,遵循使用描述中关键词的首字母作为选择键的原则,我会希望新建这样一个 capture 模板

(add-to-list 'org-capture-templates
             '("w" "Web Collections" entry
               (file+headline "~/Dropbox/org/inbox.org" "Web")
               "* %U %:annotation\n\n%:initial\n\n%?"))

但这个时候的模板选择键 "w" 和之前任务里的 "Work Task" 就冲突了,为了解决冲突,我只好在其中一个使用小写的 "w" 字母而在另外一个中使用大写的字母 "W"。当我们的模板数量更多时,这种 capture 模板选择键冲突的情况可能会更多。

虽然并不是非常大的问题,但使用模板分组,能尽量地减少这种情况,让我们的模板更加清爽一些

上述情况,我们可以将任务相关的 capture 模板分到一组里,如下所示:

(add-to-list 'org-capture-templates '("t" "Tasks"))
(add-to-list 'org-capture-templates
             '("tr" "Book Reading Task" entry
               (file+olp "~/Dropbox/org/task.org" "Reading" "Book")
               "* TODO %^{书名}\n%u\n%a\n" :clock-in t :clock-resume t))
(add-to-list 'org-capture-templates
             '("tw" "Work Task" entry
               (file+headline "~/Dropbox/org/task.org" "Work")
               "* TODO %^{任务名}\n%u\n%a\n" :clock-in t :clock-resume t))

和前面未分组的模板,有两个不同

  • 多了一个只有 key 和 description 而没有 entry/target/template 的 capture 模板,也就是

    (add-to-list 'org-capture-templates '("t" "Tasks"))
    

    这个模板至关重要,它设定了一组模板的名称和选择键前缀。如果缺失了这个模板,那么后面两个模板是不会起作用的。

  • 原来的两个任务模板,其选择键多了一个前缀 "t"

一图胜千言,在建立分组前,执行 M-x org-capture 时,弹出的模板选择 buffer 是这个样子的

org-capture-buffer-no-group.png

建立分组后,在模板选择 buffer 里看到的是这个样子

org-capture-buffer-group-1.png

这里只会显示模板的 group 的 key 和 description,等我们按下 t 后才会出来组内所有模板的列表

org-capture-buffer-group-2.png

用 org-capture 来记录日志

这个之前也提到过,就是用来做日志记录、日记写作一类的事情,新增的内容和过去的内容都按时间顺序排列,方便我们进行回顾。

做日志记录时,比较推荐用 file+datetree 或者 file+weektree 这两个 target type,当然也不是绝对的,比如说下面这个 capture 模板也是满足基本要求的

(add-to-list 'org-capture-templates
             '("j" "Journal" entry (file "~/Dropbox/org/journal.org")
               "* %U - %^{heading}\n  %?"))

上述模板在每次执行后,在 journal.org 的尾部插入下面这样的内容

* [2018-03-24 六 21:42] - 某件事情的记录

  具体的记录 blablabla

就是如果想要快速地找到某一天或者某一个月的记录,会稍微费力一点,使用 file+datetree 的话,新增的记录会按照「年-月-日」的层次组织起来;而使用 file+weektree 的话,新增的记录会按「年-周-日」的层次组织,下图是两者的对比

org-capture-datetree-and-weektree.png

我个人目前是使用 file+datetree 的。

用 org-capture 收集灵感、记录笔记

这类模板也比较简单,基本上用 file+headline 的 target,然后视情况而定预先设置 tag 什么的。

我个人有一个 capture 模板,用来快速记录未归类的东西,然后会在后面使用 refile 来将这些东西迁移到任务或者笔记中

(add-to-list 'org-capture-templates
             '("i" "Inbox" entry (file "~/Dropbox/org/inbox.org")
               "* %U - %^{heading} %^g\n %?\n"))

笔记则用另外一个 capture 模板

(add-to-list 'org-capture-templates
             '("n" "Notes" entry (file "~/Dropbox/org/notes/inbox.org")
               "* %^{heading} %t %^g\n  %?\n"))

可以看到两个模板其实差不多,无非就是写入的文件不一样。实际上的不同之处在于,在我的笔记本 ~/Dropbox/org/notes/inbox.org 中,我设置了一些文件级别的 tag,如下所示

#+TITLE: 笔记本
#+STARTUP: hideall
#+TAGS: [coding: shell python]
#+TAGS: [shell: grep tail sed ssh]
#+TAGS: [python: ipython pandas numpy]

这样在特殊标记 %^g 展开的时候,就可以用上面设置的 tag 进行补全。

用 org-capture 记录账单

在 Org mode 中利用表格来记录账单是非常合适的一个方式,记录好后利用表格公式(见我的上一篇文章)可以很方便地进行计算、绘图什么的。

下面是我用来记录账单的 capture 模板,利用自定义的函数,来将同一个月的支出记录在同一张表里

(add-to-list 'org-capture-templates
             '("b" "Billing" plain
               (file+function "~/Dropbox/org/billing.org" find-month-tree)
               " | %U | %^{类别} | %^{描述} | %^{金额} |" :kill-buffer t))))

其中的函数 find-month-tree,用来做类似 file+datetree 的事情,不过层级结构只到月为止,其实现如下

(defun get-year-and-month ()
  (list (format-time-string "%Y年") (format-time-string "%m月")))


(defun find-month-tree ()
  (let* ((path (get-year-and-month))
         (level 1)
         end)
    (unless (derived-mode-p 'org-mode)
      (error "Target buffer \"%s\" should be in Org mode" (current-buffer)))
    (goto-char (point-min))             ;移动到 buffer 的开始位置
    ;; 先定位表示年份的 headline,再定位表示月份的 headline
    (dolist (heading path)
      (let ((re (format org-complex-heading-regexp-format
                        (regexp-quote heading)))
            (cnt 0))
        (if (re-search-forward re end t)
            (goto-char (point-at-bol))  ;如果找到了 headline 就移动到对应的位置
          (progn                        ;否则就新建一个 headline
            (or (bolp) (insert "\n"))
            (if (/= (point) (point-min)) (org-end-of-subtree t t))
            (insert (make-string level ?*) " " heading "\n"))))
      (setq level (1+ level))
      (setq end (save-excursion (org-end-of-subtree t t))))
    (org-end-of-subtree)))

效果如下图所示

org-capture-billing.gif

用 org-capture 来记录联系人信息

联系人会有一些常见的属性比如姓名、手机号、邮箱、住址之类的,简单起见可以用表格来做,比如

(add-to-list 'org-capture-templates
             '("c" "Contacts" table-line (file "~/Dropbox/org/contacts.org")
               "| %U | %^{姓名} | %^{手机号}| %^{邮箱} |"))

不过在一些场景下用表格来做可能会不太方便,比如说我们想对联系人进行一些细致的描述之类的,这种情况下一个表格行太长就不太方便了。

因此因外一个方案是,将每个联系人的信息记录为一个 headline 中,联系人的具体属性作为 headline 的 property 进行记录,如果要进行什么描述说明的话就作为 headline 下属的内容就好,如下所示:

(add-to-list 'org-capture-templates
             '("c" "Contacts" entry (file "~/Dropbox/org/contacts.org")
               "* %^{姓名} %^{手机号}p %^{邮箱}p %^{住址}p\n\n  %?" :empty-lines 1))

用 org-capture 来管理密码

和记录联系人类似,如果只是单纯地记录密码,那么可以直接用表格。不过作为一个密码管理方案,我们可能要考虑以下事情

  • org 文件本质上是文本文件,怎么保证密码的安全性?
  • 当我需要新建密码时,是否能在 org-capture 中直接来生成密码?

以上两点都是可以解决的。安全方面,Org mode 支持对文件、headline、headline 中正文等不同层级的加密,详情见 Encrypting org Files.,这里只讲最简单的文件级加密。

首先我们在 Emacs 中先新建好一个后缀为 cpt 的 org 文件,比如 passwords.org.cpt —— 在一个正常的 org 文件后再附加上 cpt 这个后缀,就会被当作一个加密文件,在创建这个文件的时候会要求我们输入加密用的密码,我们只需要把这个主密码记住就好了。

然后我们要写一个函数来要求输入密码,当输入密码为空时,我们就自动生成一个密码 —— 简单起见这里限定生成的密码长度是 16 位,只用字母和数字组成,如下所示。

(defun random-alphanum ()
  (let* ((charset "abcdefghijklmnopqrstuvwxyz0123456789")
         (x (random 36)))
    (char-to-string (elt charset x))))

(defun create-password ()
  (let ((value ""))
    (dotimes (number 16 value)
      (setq value (concat value (random-alphanum))))))


(defun get-or-create-password ()
  (setq password (read-string "Password: "))
  (if (string= password "")
      (create-password)
    password))

然后新建一个模板如下就可以了。

(add-to-list 'org-capture-templates
             '("p" "Passwords" entry (file "~/Dropbox/org/passwords.org.cpt")
               "* %U - %^{title} %^G\n\n  - 用户名: %^{用户名}\n  - 密码: %(get-or-create-password)"
               :empty-lines 1 :kill-buffer t))

用 org-capture 来新建博客文章

我是直接使用 Org mode 的原生支持的 project 来写博客的,写好后将整个 project 导出成 html,放置到配置好的 jekyll 目录下。Org mode 的 project 要求设置一个目录,这个目录下的 org 文件都会被当作 project 中的文章,比如说我的博客对应的 project 设置是这样的

(setq org-publish-project-alist
      '(("blog-org"
         :base-directory "~/Dropbox/org/blog/"
         :base-extension "org"
         :publishing-directory "~/Projects/github-pages/"
         :recursive t
         :htmlized-source t
         :section-numbers nil
         :publishing-function org-html-publish-to-html
         :headline-levels 4
         :html-extension "html"
         :body-only t     ; Only export section between <body> </body>
         :table-of-contents nil
         )
        ("blog-static"
         :base-directory "~/Dropbox/org/blog/"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|php"
         :publishing-directory "~/Projects/github-pages/"
         :recursive t
         :publishing-function org-publish-attachment
         )
        ("blog" :components ("blog-org" "blog-static"))))

那么我每次新写文章的时候,就需要用 C-x C-f(find-file) 去在这个目录下新建一个文件,这个过程,是可以用 org-capture 来优化的。

为了方便示例我们把问题简化一下,我需要的是执行 org-capture 后,自动在一个固定的目录下,产生一个命名类似 2018-03-25.org 的文件,并在文件中写入一些固定的内容。

用 org-capture 我们可以这么做

(add-to-list 'org-capture-templates
             `("b" "Blog" plain (file ,(concat "~/Dropbox/org/blog/"
                                               (format-time-string "%Y-%m-%d.org")))
               ,(concat "#+startup: showall\n"
                        "#+options: toc:nil\n"
                        "#+begin_export html\n"
                        "---\n"
                        "layout     : post\n"
                        "title      : %^{标题}\n"
                        "categories : %^{类别}\n"
                        "tags       : %^{标签}\n"
                        "---\n"
                        "#+end_export\n"
                        "#+TOC: headlines 2\n")))

注意,这里和前面的模板有一些不同

  • `("b" "Blog" 这里开头的符号不是单引号
  • target 和 template 两部分中有一个 concat 函数的调用,在其前面有一个逗号

这里涉及到 emacs-lisp 的一些语法细节,想要详细了解的可以查看相关文档: Backquote - GNU Emacs Lisp Reference Manual

用 org-capture 来做网页内容收集

结合 org-protocol,我们可以在外部程序中发送数据到 Emacs 中并触发 org-capture,是非常方便的一个功能。

由于 org-protocol 本身还有很多细节,展开来讲的话内容会很多,这里就只重点讲一下和 org-capture 相关的部分。

首先我们要知道 org-protocol 其实是定义了一个类似通信协议一样的东西,因此我们需要启动 Emacs server 来让外部程序可以访问,在配置文件中加入下面这行配置即可

(server-start)

要启用 org-protocol 的话,还需要在 Emacs 之外做一些设置,本文不准备在这里做过多说明,详情可以参考 org-capture-extension 这个项目,里面对 Linux/OSX/Windows 三个操作系统上的设置都做了详细说明。

在 Emacs 中我们需要加载一下 org-protocol

(require 'org-protocol)

当用 org-protocol 触发 org-capture 时,它会设置 org-store-link-plist 这个变量,根据外部传入的数据设置其中的一些属性。从 org-protocol-do-capture 这个函数的源代码中,我们可以发现这么一段

(org-store-link-props :type type
                      :link url
                      :description title
                      :annotation orglink
                      :initial region
                      :query parts)

也就是说,在 org-store-link-plist 中的属性有六个,分别如下

属性 描述
type 链接的类型,如 http/https/ftp 等,是靠正则 (string-match "^\\([a-z]+\\):" url) 解析出来的
link 链接地址,在 org-protocol 里的 url 字段
description 链接的标题,在 org-protocol 里的 title 字段
annotation 靠 link 和 description 完成的 org 格式的链接
initial 链接上选中的文本,在 org-protocol 里的 body 字段
query org-protocol 上除掉开头和子协议部分的剩下部分

这和我们前面「capture 模板的五个部分」中提到的 org-protocol 在内容模板中可用的六个特殊标记,是一一对应的。

利用这六个属性及对应的六个特殊标记,我们就可以方便地做网页内容的收集了。

我们先为 org-protocol 相关的 capture 模板设立一个分组

(add-to-list 'org-capture-templates '("p" "Protocol"))

最简单的情况是用 org-capture 来做网页书签管理,相应的 capture 模板会比较简单,只需要记录下网页的标题和链接即可,如下所示:

(add-to-list 'org-capture-templates
             '("pb" "Protocol Bookmarks" entry
               (file+headline "~/Dropbox/org/web.org" "Bookmarks")
               "* %U - %:annotation" :immediate-finish t :kill-buffer t))

再进一步的,我们可以选中网页上的内容,通过 org-protocol 和 org-capture 快速记录到笔记中

(add-to-list 'org-capture-templates
             '("pn" "Protocol Bookmarks" entry
               (file+headline "~/Dropbox/org/web.org" "Notes")
               "* %U - %:annotation %^g\n\n  %?" :empty-lines 1 :kill-buffer t))

当然,上面的 capture 模板会有一个问题,假如说一个网页上,有多处我觉得有价值的内容,我都选中了然后通过 org-protocol 调用了 org-capture,那么实际上是会产生多条记录的。这种情况如果能将同一个网页的内容都按顺序放置到同一个 headline 里面,显然是更加合理的。对上面的 capture 模板稍作调整,得到的下面的模板就能满足这个需求:

(add-to-list 'org-capture-templates
             '("pa" "Protocol Annotation" plain
               (file+function "~/Dropbox/org/web.org" org-capture-template-goto-link)
               "  %U - %?\n\n  %:initial" :empty-lines 1))

这里用了 file+function,函数 org-capture-template-goto-link 的定义参考了 reddit 上的这篇帖子

(defun org-capture-template-goto-link ()
  (org-capture-put :target (list 'file+headline
                                 (nth 1 (org-capture-get :target))
                                 (org-capture-get :annotation)))
  (org-capture-put-target-region-and-position)
  (widen)
  (let ((hd (nth 2 (org-capture-get :target))))
    (goto-char (point-min))
    (if (re-search-forward
         (format org-complex-heading-regexp-format (regexp-quote hd)) nil t)
        (org-end-of-subtree)
      (goto-char (point-max))
      (or (bolp) (insert "\n"))
      (insert "* " hd "\n"))))

此外,结合 abo-abo 的 orca 工具,我们还可以针对不同的网站域名,来自动地将网页收集内容进行归类,可以应用的场景有

  • 在 arxiv、google scholar 上用 org-protocol 触发 org-capture 时,自动新增内容到记录待读论文列表的 papers.org 中
  • 在淘宝、京东、亚马逊网站上用 org-protocol 触发 org-capture 时,自动新增内容到记录心愿单列表的 wishlist.org 中

而结合 org-protocol-capture-html 这个工具,我们可以在用 org-protocol 触发 org-capture 时,将网页内容全文转换成 org 文件存储到特定目录中,打造一个类似稍后阅读的工具。

用 org-capture 来新建 Anki 卡片

利用 anki-editor,我们可以在 org 文件中创建卡片并同步到 Anki 软件中。这里就以 anki-editor 中的卡片结构,来展示如何用 org-capture 创建 Anki 卡片。

最简单的例子,是新建单词卡,用来辅助记忆我们学习到的一些新的单词。那么对应的 capture 模板是这个样子的:

(add-to-list 'org-capture-templates
             `("v" "Vocabulary" entry
               (file+headline "~/Dropbox/org/anki.org" "Vocabulary")
               ,(concat "* %^{heading} :note:\n"
                        "%(generate-anki-note-body)\n")))

其中的 generate-anki-note-body 函数如下

(defun generate-anki-note-body ()
  (interactive)
  (message "Fetching note types...")
  (let ((note-types (sort (anki-editor-note-types) #'string-lessp))
        (decks (sort (anki-editor-deck-names) #'string-lessp))
        deck note-type fields)
    (setq deck (completing-read "Choose a deck: " decks))
    (setq note-type (completing-read "Choose a note type: " note-types))
    (message "Fetching note fields...")
    (setq fields (anki-editor--anki-connect-invoke-result "modelFieldNames" `((modelName . ,note-type))))
    (concat "  :PROPERTIES:\n"
            "  :ANKI_DECK: " deck "\n"
            "  :ANKI_NOTE_TYPE: " note-type "\n"
            "  :END:\n\n"
            (mapconcat (lambda (str) (concat "** " str))
                       fields
                       "\n\n"))))

这个函数的定义是抄了 anki-editor(version:20180729) 中的代码,做了一些修改得到的。

PyTorch 初体验

2018年1月7日 08:00

这两周简单看了下 pytorch,虽然说还没有非常系统的、全方面的认识,但姑且总结一下好了。

基础模块构成

最核心的模型组件都在 torch.nn 这个模块里,这个模块里包含了

  • 不同类型的网络结构,如:Embedding, LSTM, Conv1d, MaxPool1d, Linear
  • 不同类型的激活函数,如:RELU, SELU, Sigmoid, Tanh
  • 不同类型的目标函数,如:CrossEntropyLoss, MSELoss, HingeEmbeddingLoss

不一一列举,总之,如果是想构建起一个网络,不考虑训练的话,那么只用 torch.nn 这个模块里的东西就足够了。

比较重要的是 torch.nn.Module 这个类,上述的网络结构、激活函数、目标函数都继承自这个类,如果是想自定义模型、激活函数、目标函数的话,继承这个类就好。所以这个类的行为和内在机制有必要好好了解一下。

其次就是 torch.autograd 这个模块,其中的 Variable 是 torch 里的输入、输出数据的标准类型,也就是说,我们定义好一个模型后,如果想输入东西,就得把数据都转成 Variable 类型的值。

torch.optim 里则定义了常用的一些优化方法,不多,罗列如下

  • Adadelta
  • Adagrad
  • Adam
  • SparseAdam
  • Adamax
  • ASGD
  • SGD
  • Rprop
  • RMSprop
  • Optimizer
  • LBFGS

差不多就是这个样子。

其他

暂时了解还不多,就不长篇大论了,这里随便写写。

在模型层面,pytorch 使用起来确实舒服很多。主要的点有这些

  • 不需要管 session、graph 这些东西,定义好的网络结构,直接就能接受输入并得到输出
  • 模块都继承自 torch.nn.Module 这个类,而这个类被设计成了 picklable 的,我们直接用 pickle.dump 和 pickle.load 就能对模型进行保存和加载,相比之下,tensorflow 默认将模型拆成若干个文件然后通过 saver 的方式来保存和加载一直让我非常抗拒 —— 倒不是说我认为模型存成多个文件就不好,但至少提供让我不存成多个文件的选项吧?在 tensorflow 里想要自己去把模型结构和模型参数拿出来按自己的想法存储这件事情,我是一直都没有成功过……
  • torch.nn.Module 类有一个 bool 类型的 training 成员,如果将其设置成 False,那么 Dropout、BatchNorm 之类的层就会失效,这个虽然是个很小的点但也是非常让人舒服的一点,tensorflow 里为了解决这个问题,通常就得自己来设置选项来保证在训练和预测的时候产生两张不同的图,贼恶心

    详情见 这个帖子这个帖子

当然,也有一些坑,或者说我觉得不太满意的地方吧

  • 目前 pypi 上的版本落后于官网版本,按照官网上的安装方法要下几百兆的东西……吐血……
  • 没有对整个项目结构和机制的总体介绍,当然,毕竟版本还在 0.3.0……
  • 模型内部的数据类型好像有不一致的地方,我在默认行为下,遇到过什么 DoubleTensor 的错误,可能和这个 issue 有关
  • 需要手动选择是否使用 CUDA,但是我明明看到 torch.cuda 下有个 is_available 的方法
  • RNN 的输入和输出,默认第二个维度是 batch,如果想让第一个维度表示 batch,就得手动把 batch_first 设置成 True,挺奇怪的

Emacs 的 Python3 开发环境配置

2017年9月16日 08:00

基础配置

基础配置就没啥好说的了,就是缩进宽度啦,各种 minor-mode 的添加啦之类的,直接上配置

(defun my-python-mode-config ()
  (setq python-indent-offset 4
        python-indent 4
        indent-tabs-mode nil
        default-tab-width 4

        ;; 设置 run-python 的参数
        python-shell-interpreter "ipython"
        python-shell-interpreter-args "-i"
        python-shell-prompt-regexp "In \\[[0-9]+\\]: "
        python-shell-prompt-output-regexp "Out\\[[0-9]+\\]: "
        python-shell-completion-setup-code "from IPython.core.completerlib import module_completion"
        python-shell-completion-module-string-code "';'.join(module_completion('''%s'''))\n"
        python-shell-completion-string-code "';'.join(get_ipython().Completer.all_completions('''%s'''))\n")

  (add-to-list 'auto-mode-alist '("\\.py\\'" . python-mode))
  (hs-minor-mode t)                     ;开启 hs-minor-mode 以支持代码折叠
  (auto-fill-mode 0)                    ;关闭 auto-fill-mode,拒绝自动折行
  (whitespace-mode t)                   ;开启 whitespace-mode 对制表符和行为空格高亮
  (hl-line-mode t)                      ;开启 hl-line-mode 对当前行进行高亮
  (pretty-symbols-mode t)               ;开启 pretty-symbols-mode 将 lambda 显示成希腊字符 λ
  (set (make-local-variable 'electric-indent-mode) nil)) ;关闭自动缩进

(add-hook 'python-mode-hook 'my-python-mode-config)

另外如果是 Emacs 25.1 的话,有一个已知 bug,会导致执行 run-python 的时候,python shell 里显示一堆乱码,下面的方法能够解决

(setenv "IPY_TEST_SIMPLE_PROMPT" "1")

Emacs + Python3 的问题

从去年开始,因为工作的原因,日常的开发环境从 Python2 切换成了 Python3,一开始还是有一点不太习惯的,其中 Python 本身的语法差异倒真没有带来太多的不适应,一开始的抗拒主要还是因为不少 Python 的库在对 Python3 的支持上多少有点问题。

当然,因为公司是用 Python3 的,碰到上述问题的时候就会去找替代方案了,加上主流的一些库也有了对 Python3 的支持,所以现在已经习惯了用 Python3,而且本身是从事 NLP 相关的工作,读写文本的时候不用每次都 encode/decode,还是挺舒服的。

Python2 还是 Python3 这个就不想讨论了,网上相关的讨论也不少了。我这边的问题主要是,切换成 Python3 后,原来 Python 的配置多少都有点问题,比如语法检查、自动补全等默认都是用系统的 Python 环境的,要处理 Python3 的代码就需要额外做点事情,我这个人实在是懒于是去掉了语法检查、自动补全这些功能,将就着用着最基础的一些功能,倒也不是不能过日子。

有时候也有考虑重新配置一下 Python 环境,但是一看到 elpy 啊 projectile 这些稍微复杂点的 package 就犯懒,倒是这阵子用一些更小的 package 一点一点地加新功能,貌似倒是已经解决了自己的需求了,加上有些时间没写东西了,想着写点东西,顺便分享下踩到的坑吧。

company + jedi-core 的 Python3 配置

首先是自动补全了,一开始是用 auto-complete 的,不过被 auto-complete 坑过太多次了, company 算是后起之秀,配置起来也挺方便的。

把 company 装上

(when (not (require 'company nil :noerror))
  (message "install company now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'company))

然后在启动 Emacs 的时候开启全局的 company-mode

(add-hook 'after-init-hook 'global-company-mode)

company-mode 默认已经配置好了多个语言的 backends,基本上是开箱即用的,查看变量 company-backends 可以看它当前使用的 backends,默认是

(company-bbdb
 company-nxml
 company-css
 company-eclim
 company-semantic
 company-clang
 company-xcode
 company-cmake
 company-capf
 company-files
 (company-dabbrev-code company-gtags company-etags company-keywords)
 company-oddmuse company-dabbrev)

这些应付一些简单的场景足够用了。

然后是安装 company-jedi

(when (not (require 'company-jedi nil :noerror))
  (message "install company-jedi now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'company-jedi))

company-jedi 是 company 的一个 backend,使用 jedi 这个 Python 的自动补全和静态分析工具。需要注意的是,使用 package-install 安装 company-jedi 就好了,它会安装 jedi-core 这个 package,里面有对 jedi 的封装。说这个是因为用户如果没有看 company-jedi 的说明,有可能会去安装 jedi 这个 Emacs package,但实际上这个 package 是一个 auto-complete 的后端,完全不用。

到目前为止的操作都是通用的,和 Python2/Python3 都没有关系,但要知道,jedi 的工作原理是根据一个 Python 环境里的标准库及安装的非标准库来进行补全的,也就是说,它需要依赖一个外部的 Python 环境,如果去看 emacs-jedi 的文档,会看到要求用户执行 jedi:install-server 来建立一个 Python 环境,而这个命令实际上会在 ~/.emacs.d/.python-environments 这个目录下建立一个 virtualenv 环境,默认用的是 Python2.7。

所以如果想为 Python3 配置 jedi,请注意 不要使用 jedi:install-server 这种方式

既然知道了 emacs-jedi 的工作原理,那就好办了,那我就自己在 ~/.emacs.d/.python-environments 这个目录下建立一个 Python3 的 virtualenv 环境呗。

首先建立 ~/.emacs.d/.python-environments/ 这个目录

mkdir -p ~/.emacs.d/.python-environments/

然后在其中创建 virtualenv 环境,下面的示例中为这个 virtualenv 环境命名为 jedi,取别的名字都可以的

cd ~/.emacs.d/.python-environments/
virtualenv -p /usr/bin/python3  --prompt="<venv:jedi>" jedi

然后在这个 virtualenv 环境中安装需要的 Python 依赖,依赖分两部分,一部分是 jedi 相关的几个 Python 包,是自动补全必须的,这些东西都在 jedi-core 这个 Emacs package 里的 setup.py 里写好了,其内容如下

setup(
    name='jediepcserver',
    version='0.2.7',
    py_modules=['jediepcserver'],
    install_requires=[
        "jedi>=0.8.1",
        "epc>=0.0.4",
        "argparse",
    ],
    entry_points={
        'console_scripts': ['jediepcserver = jediepcserver:main'],
    },
    **args
)

可以看到,依赖的是 jedi 和 epc 两个 Python 包,我们可以手动安装它们

~/.emacs.d/.python-environments/jedi/bin/pip install jedi>=0.8.1 epc>=0.0.4 argparse

也可以直接使用这个 setup.py 来安装

~/.emacs.d/.python-environments/jedi/bin/pip install --upgrade ~/.emacs.d/elpa/jedi-core-20170319.2107/

其次是需要用于补全的 Python 的非标准库,比如说我经常用 sklearn、tensorflow 之类的工具,我想在写相关的代码的时候能补全,那么要在我们刚才建立好的 virtualenv 环境里安装好这些 Python 包。

~/.emacs.d/.python-environments/jedi/bin/pip install tensorflow==1.3.0 scipy==0.19.1 numy==1.13.1 scikit-learn==0.19.0

至此外部的设置都已经好了,然后就是要在 Emacs 里设置来使用我们刚才建立好的这个 virtualenv 环境

(setq jedi:environment-root "jedi")
(setq jedi:server-command (jedi:-env-server-command))

然后设置当打开 Python 代码文件的时候,启动 jedi

(defun config/enable-jedi ()
  (add-to-list 'company-backends 'company-jedi))
(add-hook 'python-mode-hook 'jedi:setup)
(add-hook 'python-mode-hook 'config/enable-jedi)

还有一些补全的细节可以设置,如

  • 输入句点符号 "." 的时候自动弹出补全列表,这个主要是方便用来选择 Python package 的子模块或者方法

    (setq jedi:complete-on-dot t)
    
  • 补全时能识别简写,这个是说如果我写了 "import tensorflow as tf" ,那么我再输入 "tf." 的时候能自动补全

    (setq jedi:use-shortcuts t)
    
  • 设置补全时需要的最小字数(默认就是 3)

    (setq compandy-minimum-prefix-length 3)
    
  • 设置弹出的补全列表的外观

    让补全列表里的各项左右对齐

    (setq company-tooltip-align-annotations t)
    

    如果开启这个,那么补全列表会是下面这个样子

    company-aligned-tooltip.png

    默认是这个样子

    company-default-tooltip.png

    补全列表里的项按照使用的频次排序,这样经常使用到的会放在前面,减少按键次数

    (setq company-transformers '(company-sort-by-occurrence))
    

    在弹出的补全列表里移动时可以前后循环,默认如果移动到了最后一个是没有办法再往下移动的

    (setq company-selection-wrap-around t)
    
  • 对默认快捷键做一些修改

    默认使用 M-n 和 M-p 来在补全列表里移动,改成 C-n 和 C-p

    (define-key company-active-map (kbd "M-n") nil)
    (define-key company-active-map (kbd "M-p") nil)
    (define-key company-active-map (kbd "C-n") 'company-select-next)
    (define-key company-active-map (kbd "C-p") 'company-select-previous)
    

    设置让 TAB 也具备相同的功能

    (define-key company-active-map (kbd "TAB") 'company-complete-common-or-cycle)
    (define-key company-active-map (kbd "<tab>") 'company-complete-common-or-cycle)
    (define-key company-active-map (kbd "S-TAB") 'company-select-previous)
    (define-key company-active-map (kbd "<backtab>") 'company-select-previous)
    

结合 virtualenv 来使用 flycheck

首先我们要安装 flycheck 这个实时语法检查工具

(when (not (require 'flycheck nil :noerror))
  (message "install flycheck now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'flycheck))

在 python-mode 里启用也很简单

(defun config/enable-flycheck ()
  (flycheck-mode t))
(add-hook 'python-mode-hook 'config/enable-flycheck)

flycheck 使用 pylint 来对代码进行语法和代码规范的检查,实际上会使用 executable-find 这个方法来确定使用的 pylint

(defcustom flycheck-executable-find #'executable-find
  "Function to search for executables.

The value of this option is a function which is given the name or
path of an executable and shall return the full path to the
executable, or nil if the executable does not exit.

The default is the standard `executable-find' function which
searches `exec-path'.  You can customize this option to search
for checkers in other environments such as bundle or NixOS
sandboxes."
  :group 'flycheck
  :type '(choice (const :tag "Search executables in `exec-path'" executable-find)
                 (function :tag "Search executables with a custom function"))
  :package-version '(flycheck . "0.25")
  :risky t)

而 executable-find 的工作原理是从 exec-path 这个变量里包含的的路径下寻找对应的可执行程序

(defun executable-find (command)
  "Search for COMMAND in `exec-path' and return the absolute file name.
Return nil if COMMAND is not found anywhere in `exec-path'."
  ;; Use 1 rather than file-executable-p to better match the behavior of
  ;; call-process.
  (locate-file command exec-path exec-suffixes 1))

如果只是为了支持 Python3,那么我们可以自己建立一个 Python3 的 virtualenv,然后将其路径加到 exec-path 的最前面

(push "<YOUR PYTHON3 VENV>/bin/" exec-path)

当然记得在里面安装 pylint,不然还是会用系统环境也就是 Python2 环境里的 pylint。

这种方法可以 work,但是会有不方便的地方,比如说我有时候也有可能会写 Python2 代码,遇到 Python3 已经不兼容的语法,上述方法会导致 flycheck 认为是语法错误。另外一个就是,比较良好的开发习惯,是用 virtualenv 隔离开每个项目的依赖,不同项目的同一个依赖可能会版本不一样,这样的话 flycheck 如果只使用静态的环境就没有办法应付。

当然,上一节的自动补全用的是一个统一的 virtualenv 环境,也会有类似的问题,不过要改起来会麻烦一些,所以先略过。

flycheck 的这个问题倒是好解决,既然我每个项目都会有一个独立的 virtualenv,那么能不能做到我打开对应项目的代码的时候就使用对应的 virtualenv 环境呢,比如说将对应的路径添加到 exec-path 这个列表的前面?

答案是可以的,方法是使用 auto-virtualenvwrapper,这个 package 可以根据当前的文件寻找当前目录或者上级目录中的 virtualenv 环境,然后启用。

(when (not (require 'auto-virtualenvwrapper nil :noerror))
  (message "install auto-virtualenvwrapper now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'auto-virtualenvwrapper))

然后设置一下在 python-mode 里启用它

(add-hook 'python-mode-hook #'auto-virtualenvwrapper-activate)

可以做到切换 buffer 的时候自动切换对应的 virtualenv 环境

(add-hook 'window-configuration-change-hook #'auto-virtualenvwrapper-activate)

然后我们要保证 flycheck 会在这个 virtualenv 环境里去寻找 pylint,也就是说,我们要临时修改一下 exec-path 的值

(declare-function python-shell-calculate-exec-path "python")

(defun flycheck-virtualenv-executable-find (executable)
  "Find an EXECUTABLE in the current virtualenv if any."
  (if (bound-and-true-p python-shell-virtualenv-root)
      (let ((exec-path (python-shell-calculate-exec-path)))
        (executable-find executable))
    (executable-find executable)))

(defun flycheck-virtualenv-setup ()
  "Setup Flycheck for the current virtualenv."
  (setq-local flycheck-executable-find #'flycheck-virtualenv-executable-find))

注:上述代码来自lunaryorn 的配置

PEP8 的支持

上面配置好的 flycheck 所做的语法检查和静态分析,对于不符合 PEP8 规范的语句已经会做一些提示了,不过说实话,一些东西我们可能并不想在上面化太多精力,运算符前后一个空格啦、函数之间空两行啦、类内方法之间空一行啦之类的,其实可以靠 py-autopep8 来格式化代码自动完成。

安装相应的 Emacs package

(when (not (require 'py-autopep8 nil :noerror))
  (message "install autopep8 now...")
  (setq url-http-attempt-keepalives nil)
  (package-refresh-contents)
  (package-install 'py-autopep8))

当然它其实是使用的 Python 的 autopep8 这个外部工具,所以也需要安装它

pip install autopep8

然后在 python-mode 里启用就好了,下面的配置会让 Emacs 每次在保存 Python 文件的时候自动调用 autopep8 进行格式化

(add-hook 'python-mode-hook 'py-autopep8-enable-on-save)

当然我们也可以额外设置一些参数,比如默认的一个标准是每行最大字符数为 80,如果超过了,格式化的时候会将该行折行。下面的配置可以设置为 100

(setq py-autopep8-options '("--max-line-length=100"))

深度学习在自然语言处理中的应用: 集智俱乐部活动笔记

2016年12月14日 08:00

简介

本文为对 2016 年 12 月 10 日集智俱乐部公开活动「深度学习与自然语言处理」进行的总结。

活动主讲人是自然语言处理领域的知名学者吕正东,他先后在 MSRA 和华为诺亚方舟实验室担任过研究员,在顶级会议、期刊上发表过 40 余篇论文,并长期担任一些会议、期刊的审稿人。微博上的「鲁东东胖」就是他啦,最近他创办了一家人工智能公司,正在进行招聘。

深度学习在自然语言处理中的应用

自然语言处理(Natural Language Processing, NLP)是人工智能中的一个重要分支,从人工智能这个领域刚发展起来的时候就一直是一个重要的研究方向,到现在也发展出了很多的细分领域和非常多的方法,大致上来说,我们可以认为是在 2013 年的时候,伴随着 word2vec 这个 word embedding 的开源工具的出现,正式地将深度学习引进了 NLP 的研究中 —— 当然要认真考据的话,word embedding 的思想早在上世纪 80 年代的时候就已经初露端倪1,用神经网络来建模语言的工作也可以追溯到现为 IDL 科学家的徐伟在 2001 年的工作2以及 2003 年 Bengio 在 JMLR 上发表的著名论文《A Neural Probabilistic Language Model》,大家去了解 word2vec/word embedding 的时候经常会见到的一张经典的模型图(见图1)就来自 Bengio 的这篇论文。

nplm_structure.png

图1  Bengio 的 NPLM

从不那么严格的大众的角度来看,可以说 word2vec 的出现是深度学习进入 NLP 领域的一个标志性事件,其中的 word embedding 思想带来了更好的文本的表示方法,也催生了各种在更高层级做 embedding 的工作,比如 sentence embedding,比如知识图谱中实体和关系的 embedding(TransE/TransR等);而 sequence to sequence 思想的提出以及 attention 机制的出现,则在机器翻译(Machine Translation, MT)领域产生了重要的作用。当然除此以外,CNN 和 GAN 乃至强化学习等最开始并不是用于 NLP 的方法,也被引入到 NLP 领域当中,并且取得了不错的结果。

总得来说,深度学习给 NLP 研究带来的新方法可以列举如下:

  • 以 embedding 为代表的表示学习
  • sequence to sequence 模型/encoder-decoder 框架
  • 注意力机制(Attention Mechanism)
  • 记忆网络(Memory Network)

在 embedding 思想出现前,在各种 NLP 处理中都是用一个离散的值来表示每个词的,这种表示方法称为 one-hot 表示,是一种难以扩展、难以表达语义的方法。embedding 将一个词表示为多维空间中的一个连续实数向量,能在一定程度上反映语义乃至上下位关系。word embedding 的价值不在于它能得出的「男人 - 女人 = 国王 - 王后」这种类比关系,而在于它作为 NLP 中基本处理单元的一个基础表示,可以用来构建更高层级文本单元(句子、段落、篇章)的表示(见图2),用来支撑更为复杂的上层操作如文本分类、机器翻译。

sentence_embedding_similarity.png

图2  句子的 embedding 表示

这种思想还能拓展到其他领域如知识图谱(见图3)、复杂系统中。

transr.png

图3  知识图谱中的 TransR 模型

当然 word embedding 称不上完美,它还做不到完美地表达语义,但不用对这些问题太过刻薄纠结。我非常同意吕正东老师以及很多 NLP 学者的观点: word embedding 的效果在语言学层面还有尚待改进的地步,在留意这些问题的同时,还应该积极地去使用它去推动更复杂的 NLP 的研究和应用。

sequence to sequence 或说 encoder-decoder 框架最初是在机器翻译中提出来的,以机器翻译为例,其基本思想是:

  1. 将输入的句子经过一个 RNN 生成一个能反映句子语义的向量,类似计算 sentence embedding 的过程,
  2. 用另一个 RNN ,从输入句子的向量表达中解码出另一个句子

第一个过程称之为 encoding,第二个过程称之为 decoding,整个模型非常的简单优雅,不需要词对齐,不需要额外的语言模型,输入英语,直接得到法语,这样便成了一个端到端(end-to-end)的模型,如图4所示。

seq2seq.png

图4  sequence to sequence 框架

之所以称其为「框架」而不是模型,在于它提出了一种通用的、可扩展的模型设计方式,其中的 encoder 和 decoder 都可以换成其他类型的神经网络结构,从而被广泛应用到不同的领域中,比如语音识别、OCR、自动驾驶、机器翻译、对话、image caption……而在 encoder-decoder 的基础上,为了改进机器翻译系统,attention 机制又被提了出来。

attention 是一种在根据模型内部状态动态聚焦于输入或输出不同部分的一种机制,比如说做机器翻译时在不同时刻关注源语言文本中不同的词,如图5所示。

attention_alignment.png

图5  神经机器翻译中的 attention 矩阵

在 image caption 任务中,要为一张图片生成描述性文字,在这里,attention 会根据当前要生成的词去聚焦于图片的不同区域,见图6

attention_in_image_caption.png

图6  Image Caption 中的 attention

memory network 则是一种附带额外存储结构的神经网络模型,通过这个额外的存储结构来记忆更丰富的信息,使得模型具有更强的表达能力和学习能力。在这之中,对这个存储结构中信息的访问,使用的就是 attention 机制;相应的,在机器翻译和 image caption 这些任务中使用 encoder-decoder 模型时,也可以把 encoder 产生的输入的表达当作是 memory,attention 机制是在这个 memory 上挑选合适的内容。总之,在相关的场景中,我们可以认为 memory 是一个可以容纳有用信息的数据结构,而 attention 则是操作这个数据结构的一种方式,这也是这次活动中最主要讨论的内容。

从应用上来说,深度学习给以下 NLP 任务带来了比较大的推动作用:

  • 机器翻译
  • 对话系统(Dialogue/Chatbot/Question Answering)
  • 自动推理

当然,这其中最受关注的就是机器翻译和对话系统了,毕竟语言的最终目的是用于进行交流,机器翻译能够让原来语言不同的两个人能够互相交流,而对话系统则能让人和机器以人类熟悉的方式进行交流,备受关注都是理所当然的。机器翻译在 NLP 领域中一直都是比较重要的一个方向,从应用意义上来说是一件能极大提高人类交流效率的事情,从学术意义上来说是一个数据相对充足且定义相对良好的研究问题,在神经机器翻译(Neural Machine Translation, NMT)突破前,就已经有基于统计学方法的统计机器翻译(Statistical Machine Translation, SMT)并且得到广泛应用。随着 encoder-decoder 框架和 attention 机制的提出,NMT 已经大幅超越了传统 SMT,并且在翻译问题上对 attention 机制的改进仍然在继续。

谈到 NMT,当然离不开 Google 的翻译系统,图7就是 Google 神经机器翻译系统(Google Neural Machine Translation, GNMT) 的模型结构,看起来有点复杂,其实就是 encoder-decoder 结构:左侧框图是 encoder 部分,右侧框图是 decoder 部分,它们之间靠 attention 机制联系起来。

gnmt.png

图7  GNMT 模型结构

Google 在 NMT 方面的翻译效果,一方面固然来源于新的方法,另一方面也在于 Google 庞大的数据积累和强大的工程能力。数据是深度学习中很重要的成分,我在 Quora 上看到有人用「without data, it's nothing」来评论 NLP 的 Python 工具包 NLTK,在深度学习这块,我觉得也可以这样讲 —— 当然这么讲是强调数据的重要性,并不是说深度学习就不重要了,「巧妇难为无米之炊 」,要做好一桌可口的饭菜,原材料、烹饪工具和厨艺是缺一不可的。

在对话方面,我们现在已经可以很简单地使用基于 encoder-decoder 的模型来得到一个用于对话的生成模型,在处理单轮对话和某些特定领域的多轮对话上取得不错的结果。最简单的办法,是将 encoder-decoder 框架用到对话领域,仿照机器翻译的方法,将对话看作是一个翻译问题,根据用户的话语来生成对应的回应。区别于传统的基于检索和基于规则、模板的方法,生成模型可以完全是数据驱动的, 在有足够丰富数据的情况下 ,可以几乎不需要人工介入学习过程内部。图8是一个例子。

nrm.png

图8  Neural Responding Machine

但我们能看出来,对话和翻译本质上是两个不同的事情,翻译是在两个不同语言但具有相同语义的文本之间做转换,对话则不是这样的,它是一个持续的过程,我们会希望它能记忆上下文、能识别实体、能辨别细微的感情等等等等。再加上在对话这个领域,数据的积累还不够,健全的评价标准还有待建立和完善。按照吕老师的看法,对话是比翻译复杂一到两个量级的任务,在真正得到突破前,可能还需要一定时间去提高我们对对话这个任务的认识。基本上来说,这两年在对话任务上所做的工作,也经常使用 encoder-decoder 框架,attention 机制基本也是标配,在多轮对话需要处理上下文时则还会使用如 memory network 之类带额外存储结构的模型来记忆上下文。除此以外,学术界还尝试了很多其他方法来用在对话任务上,比如整合知识图谱来提供精确的回答,比如应用强化学习来让优化对话结果3(见图9)。

dialogue_with_rl.png

图9  强化学习在对话中的应用

在另外一个应用「自动推理」上,按我的看法,可以分为两部分。我们谈推理的时候,可以是在谈形式逻辑的推理,也可以是自然语言层面的事实、因果、关系的推理,形式逻辑的推理可以用来做定理证明、类比归纳等各种事情,也是人工智能中的一个重要研究领域,这一块的定义是比较明确的;而自然语言推理则很难给出一个公认的定义,对此吕老师的观点是,如果我们在处理一个 NLP 任务时,需要对输入进行多遍处理,那么这个 NLP 任务就可以被认为是一个自然语言推理的任务。比如说 Facebook AI Research(FAIR) 用 memory network 在 bAbI 数据集(见图10)做的问答4,就可以认为是一个推理过程。

fair_babi.png

图10  FAIR 的 bAbI 数据集

bAbI 数据集是 FAIR 设计的用来考察对话系统个方面能力的一个数据集,包含了十几种从易到难的推理任务,此后有不少做对话或者推理的工作都通过这个数据集来评判模型的表达能力,比如 15 年 8 月香港中文大学 Baolin Peng 的 Neural Reasoner5,图11是 Neural Reasoner 的模型结构,很显然也可以看作是一个 encoder-decoder 的模型。

neural_reasoner.png

图11  Neural Reasoner

不管是形式逻辑的推理,还是自然语言推理,它都是在一定的知识基础上去做推理的,特别是自然语言推理,对于何谓知识何谓推理,都得是在一个具体的场景下才能界定清楚。据吕老师讲,他创立的「深度好奇」就是尝试将自然语言推理的技术应用到法律领域上,比如去做法律条文的理解和推断,想一想还是蛮酷的。

可微数据结构(Differentiable Data-structure)

吕老师提出一个新的概念「可微数据结构」,其实并不是一个新的东西,而是对已有的各种模型中具有相同性质的模块或成分的一个统称。前面我们提到了 attention 和 memory network 两个重要的技术,讲到在 NMT 中 attention 其实就是在 encoder 输出的 hidden state 序列上根据当前要计算的输出选取有用的信息,这里这个 hidden state 序列在性质上和 memory network 或者神经元图灵机(Neural Turing Machine, NTM)6增加的那个额外的存储结构其实是一样的,它们都保留了历史输入的信息并在输出时被反复读取,并且都是以一种模糊、可微的操作来进行处理。回过头来看 LSTM 的 cell state 也是这样的,forget gate 和 input gate 对它进行读写,output gate 则根据 cell state 得到输出。NMT 中的 hidden state 序列、memory network 和 NTM 中的额外存储结构,这就是吕老师称之为可微数据结构的事物 —— 当然,我个人觉得 LSTM 中的 cell state 也算。

LSTM 中的 cell state 的操作相对还是比较简单,但毫无疑问它是现在这些 memory 一类思想的起源 —— 当然更早还可以追溯到 Hopfield Network,这里就不展开了 —— 我们不妨先从 LSTM 来讨论一下可微数据结构。首先的一点也是最关键的一点,作为一个「数据结构」,它提供了更强的信息存储能力,然后极大地增强了神经网络模型的表达能力和学习能力。LSTM 为什么能大幅地缓解 RNN 的梯度下降问题,提高长程依赖能力?从优化角度来说,是因为其中类似 residual network 中 skip connection 的 constant error flow,使得梯度不再是指数级衰减的,但只有 constant error flow 也不行呀,一味地记住过去的信息不加区分显然也不是合理的,而 forget gate 和 input gate 就起到了对过去信息选择性增强和丢弃的作用。这是我对可微数据结构的认识,它不仅能存储可用的信息,还能在此基础上用来做对信息的筛选,我们说得很多的 attention 机制,其实就是起这个作用的。

当然,既然说 可微 ,那么对这个数据结构的操作,还必须是连续可微的才行。这个限制是为了保证这个数据结构和神经网络模型整合在一起时,不会破坏模型整体可微的的特性,我们都知道,现在大部分的神经网络模型,基本上都是用误差反向传播算法来进行训练的。

对于可微数据结构,其实我更习惯叫它 memory,虽然是个容易和认知科学、心理学、物理学等各个领域中类似概念混淆的词。一图胜千言,神经元图灵机的结构示意图可以当作是对可微数据结构的一个普适的描述,如图12所示。

ntm.png

图12  神经元图灵机

我们可以按照其持续作用时间,将可微数据结构或说 memory 粗略地划分成短期记忆(Short Term Memory, STM)、中间记忆(Intermediate Memory)和长期记忆(Long Term Memory, LTM),我们可以在感性上把它们和心理学上的瞬时记忆、工作记忆和长期记忆类比,当然其中的对应是否合理就不是本篇要讨论的内容了。encoder-decoder 模型中 encoder 产生的 hidden state 序列,是我们所说的短期记忆;而在问答中,模型会在 memory 中存储前几轮对话的信息,这个信息我们可以叫做中间记忆;而在一些基于知识图谱的推理问答任务中,还需要从知识图谱中进行知识的查找和推导,这个知识图谱,我们也可以将其视为 memory,这种独立于具体输入的信息,则叫做长期记忆。如图13所示。

memory_types.png

图13  可微数据结构: 按持续时间分类

当然,也可以从结构上来对可微数据结构来分类,比如有像 NTM 中 memory 一样固定大小的 memory,也有像 Neural Stack Machine 这样大小可扩展的 memory7(见图14),再复杂一点的还有像 Deep Memory 这样的层次化的 memory8(见图15)。使用什么样结构的 memory ,当然也要视具体任务而定。

neural_stack_machine.png

图14  Neural Stack Machine

deep_memory.png

图15  Deep Memory

借鉴计算机概念,我们可以说读写操作的基础是寻址操作,对可微数据结构的寻址是模糊的,我们称之为 soft addressing;而计算机寻址最后得到的是一个精确的地址值,我们称之为 hard addressing。所谓 soft addressing,得到的不是一个精确的地址,而是得到在所有地址上的权重,其中权重高的地址中的数据就是对当前计算更有价值的信息。我们前面说得很多的 attention,本质上就是一种寻址操作,对应的也有所谓 soft attention 和 hard attention。

我们可以用 Python 代码来进一步解释一下 soft/hard addressing 的区别。假设我们有一个列表

import random

x = [0, 1, 2, 3, 4, 5, 6]

以读取操作为例,所谓 hard addressing,是直接按下标来取一个元素

hard_address = random.randint(0, len(x))
hard_value = x[hard_address]

"Hard addressing: {}".format(hard_value)
Hard addressing: 6

所谓 soft addressing,是先计算出一个权重向量,再按这个权重向量去计算

from operator import mul

def gen_weights(length):
    weights = [random.random() for _ in range(length)]
    weights_sum = sum(weights)
    return [w / weights_sum for w in weights]

soft_addresses = gen_weights(len(x))
soft_value = sum(map(mul, soft_addresses, x))

"Soft addressing: {}".format(soft_value)
Soft addressing: 2.49246559272

当然,上面的寻址都是随机产生的,实际我们在做 attention 的时候是,一般来说,寻址可以分为按内容寻址和按地址寻址,在大部分场景中,我们使用按内容寻址的方式来对可微数据结构进行读写。需要注意的是,在做 soft addressing/attention 的时候,模型要学习的是「计算权重的函数」即上面代码中的 gen_weights 方法,而不是 soft_addresses 所代表的「每个时刻实际的权重」,以 NMT 为例,decoder 在每个 time step 时计算 attention 时用 decoder 的 hidden state 和 encoder 的 hidden state 做內积然后经过一个非线性变换得到 attention 权重,这里的这个非线性变换的参数就是学习到的,见图16。在可微性得到保证的前提下,学习到的寻址方法应当是能增大更有用信息的权重、降低无关信息权重的。

attention_as_addressing.png

图16  Attention 与 Addressing

和 word embedding 的意义近似,可微数据结构和寻址方法的存在,可以作为更高层级操作的基础构成,想想图灵机吧,在纸带和读写头的基础上所产生出来的复杂计算,和这个是可以类比的。

最开始在 NMT 中提出来的 attention 方法,只是对 encoder 的 hidden state 序列进行读操作,今年的 NMT 工作在为了解决 under-translation 和 over-translation 的问题,也开始提出了一些更复杂的 attention 方法,比如将 encoder 的 hidden state 存起来,然后每次不仅去读它们,还会去修改它们9,这对 NMT 的结果有显著提高。图17是一个应用了改进的 attention 方法的模型。

interactive_attention.png

图17  Interactive Attention

我们前面说可微数据结构对提高神经网络模型的表达能力和学习能力有很大的增强作用,但须知有一利必有一弊,在具体的场景下,该选择什么粒度的 memory 表示、该如何对其进行读写操作、该如何用这些基本操作来表达更复杂操作,都是需要去思考的;再比如 attention 计算的复杂度和 memory 的大小是正相关的,那么具体场景下,肯定是要在表达能力和训练效率之间折衷;另外可微性的要求也是对模型设计和训练的一个挑战,因此也有用 hard attention 加强化学习来设计模型的。

语言学习的范式

这里说的「语言学习」,当然是指机器的「语言学习」。首先来看看我们人类的学习过程:我们会有老师或父母教我们字词和语法,这帮助我们学会最基本的表达和理解,这可以算作是监督学习;在有一定的词汇量并掌握基本语法后,我们可以通过大量阅读来更好地理解语言的使用,这是无监督学习;同时我们的学习过程是通过不停地试错来进行的,环境会给予我们各种反馈,比如说你说了一句在用词和语法方面都很糟糕的奇怪的语句,别人会表达出不理解或诧异,因此也可以将我们的学习过程视作强化学习;此外我们可以在掌握常识后基于常识来去对我们未接触过的语言现象或者知识进行快速的掌握,这是基于解释的学习,和最近的 zero-shot 学习类似?

对于机器的「语言学习」范式,吕老师从不同的方面对其进行了划分:

  1. 学习过程是 end-to-end 还是 step-by-step 的

    所谓端到端,是指整个学习过程直接从最后的输出中得到反馈来进行调整,而 step-by-step 则要对显式地对学习过程中的中间步骤进行调整。以机器翻译为例,神经机器翻译是端到端的,在 encoder-decoder 框架下,输入一个语言,直接去学习另外一个语言的输出,中间过程我们不用做人工干涉;而在统计机器翻译里,我们要得到两个语言之间的语言单元(词或短语)的翻译统计,要统计两个语言各自的词语搭配频率(语言模型),最后还要学习对齐过程,这些都是分开进行的,只不过最后组合在一起而已。

  2. 学习目标是连续可微的还是离散的
  3. 学习过程中接受的 supervision,是来自人类的标注,还是来自环境的反馈
  4. 是监督学习还是强化学习

2、3、4 讲的都是更高层级的对比,我们主要关心的还是 end-to-end 和 step-by-step 的对比。对于大部分问题,如果允许的话,我们都会希望整个学习过程是端到端的,喂进去一头猪,出来一堆火腿肠,这样多好!

NLP 的学习任务,我们基本上可以将它们划分三个层级,即语法(syntax)、语义(semantics)和语用(pragmatics),如图18所示。

nlp_phrases.png

图18  NLP 层级

语用对应着真实的、具体的任务场景,从中我们可以得到反馈信息,这反馈信息向下层传递,调整整个学习过程(见图19),根据这个我们可以为很多 NLP 任务设计端到端的模型,比如基于 encoder-decoder 的 NMT 和单轮对话,都算是比较成功的案例,以及在复杂情况下的多轮对话,也有通过设计更复杂的模型来做到了端到端的学习,这个前文已经提过了。

nlp_phrases2.png

图19  NLP 层级(2)

但我们前面也说过了,端到端的学习过程当然是很好的,但它也有一些问题,它对模型设计的要求会比较高,如果模型设计地不好,导致信息在模型中间传递时出现问题,那么这个模型很可能就不能有效地学习。吕老师提到一个例子,给定一个俄罗斯方块的图像,最后要学习一个模型,预测其中俄罗斯方块类型的数量是偶数还是奇数,设计了一个端到端的模型如图20所示。

failed_end2end_task.png

图20  俄罗斯方块类型奇偶预测

看起来是很简单的问题,但是结果非常差,是一个失败的例子。对于这种端到端失败的情况,可能的原因有两个,一个是路径很长(比如说模型很深),另外一个是信息在某一层的时候传递很困难。

再就是,从我们自身的角度出发,人类的学习过程并不是端到端的,我们一味地在机器的语言学习过程中追求端到端地方式,未必是正确的。

针对这些问题,吕老师提出,所谓语言学习的新范式,应该是

  • 监督学习与强化学习的结合

    如果我们能放下对「端到端」的执念,应用强化学习的思想来让机器进行语言学习,我们前文提到的模型设计的困难会减轻很多。

  • 更具有可解释性的学习
  • 前文中我们提到的基于解释的学习
  • 无监督学习和其他方法的结合

脚注:

1

Elman, Jeffrey L. "Finding structure in time." Cognitive science 14.2 (1990): 179-211.

2

Xu, Wei, and Alexander I. Rudnicky. "Can artificial neural networks learn language models?." (2000).

3

Li, Jiwei, et al. "Deep Reinforcement Learning for Dialogue Generation." arXiv preprint arXiv:1606.01541 (2016).

4

Weston, Jason, et al. "Towards ai-complete question answering: A set of prerequisite toy tasks." arXiv preprint arXiv:1502.05698 (2015).

5

Peng, Baolin, et al. "Towards neural network-based reasoning." arXiv preprint arXiv:1508.05508 (2015).

6

Graves, Alex, Greg Wayne, and Ivo Danihelka. "Neural turing machines." arXiv preprint arXiv:1410.5401 (2014).

7

Grefenstette, Edward, et al. "Learning to transduce with unbounded memory." Advances in Neural Information Processing Systems. 2015.

8

Meng, Fandong, et al. "A Deep Memory-based Architecture for Sequence-to-Sequence Learning." arXiv preprint arXiv:1506.06442 (2015).

9

Meng, Fandong, et al. "Interactive Attention for Neural Machine Translation." arXiv preprint arXiv:1610.05011 (2016).

个人知识管理系统(二): 信息的取舍与收集

2016年10月22日 08:00

接上一篇 《个人知识管理系统(一): 概述》 ,本篇主要谈一下我是如何对信息进行取舍和收集的。

信息的取舍

前文已经提过,信息的取舍是建立整个系统的第一步。而信息取舍也是分阶段的: 最初的阶段,剔除对个人来说无用的信息;第二个阶段,区分有用信息的重要程度,只保留重要的信息。

第一个阶段是对信息的初步过滤,这个阶段可以发生在很多地方,不一定说是要整合在 PKM 之中。比如说屏蔽实际上不熟悉的微信好友的朋友圈,这是在系统之外;比如说在 RSS 阅读器中设置过滤规则将一些不感兴趣的信息自动标记为已读,这是在系统内。这个步骤是比较容易做到的。

第二个阶段是按照信息的重要程度来进行取舍,相比第一个阶段要难得多,很多信息看起来都是有用的而让我们难以舍弃。但事实上低优先级的信息可能会在接收到的信息中占据很大的比利,如果不进行取舍,那么将大量时间花费在低质量信息的处理上,那只会事倍功半。

需要说一下的是,系统外的信息取舍同样重要,因为如果被系统外的信息耗费了太多精力,在系统中的注意力就会减少,刷微博、刷知乎就是很典型的事例。

对于信息的重要程度的区分,我按采铜的「收益值-半衰期」四象限法来进行区分。以下是采铜在知乎问题「你有什么相见恨晚的知识推荐给年轻人?」的回答中对这个概念的描述:

当我们评价一个事情值不值得去做、应该花多少精力去做的时候,应该抛弃单一的视角,而是分从两个不同的维度来看,一是该事件将给我带来的收益大小(认知、情感、物质、身体方面的收益皆可计入),即「收益值」;二是该收益随时间衰减的速度,我称为「收益半衰期」,半衰期长的事件,对我们的影响会持续地较久较长。

以半衰期长为优先条件,不同的信息按照重要程度衰减的顺序,可以这样划分:

  1. 高收益值长半衰期的信息

    如:

    • 深度学习和自然语言处理方面的论文
    • 像「阡陌的自留地」、「战隼的学习探索」这样的方法论博客
    • 深度学习和自然语言处理方面的深度文章
  2. 低收益值长半衰期的信息

    如:

    • 神话学、哲学、历史等方面的专著
    • 数学、计算机科学等学科的系统性知识
  3. 高收益短半衰期的信息

    如:

    • 段子、笑话之类的「趣味性」信息
    • 休闲小说
  4. 低收益短半衰期的信息

    如:

    • 微信、微博及其他社区或社交平台上的信息
    • 社会/行业的热点资讯

长半衰期的信息,基本上都需要深度的阅读,因此需要养成在固定的时间段内进行处理的习惯,这样从心理上和环境上都能保持比较稳定的状态,有利于快速进入阅读状态;短半衰期的信息,多半具有时效性,且数量庞大,因此需要进行筛选以减少数量,同时利用各种碎片化时间快速处理。

结合前面的「两个阶段」,我的总体的取舍方法是这样的:

  1. 减少社交平台上的信息量: 微信上屏蔽「不熟悉好友」的朋友圈,微博和知乎上限制关注数量
  2. 减少资讯类信息的数量,只对每日的热点做大概了解,比如使用「即刻」而不是订阅一堆资讯类信息源
  3. 降低信息冗余,删除不同渠道的相同信息源(比如同一个人的知乎专栏和微信公众号),删除只做信息转发的信息源
  4. 按照自己的兴趣和规划,对信息源进行分类,删除无法分类的信息源
  5. 维护一个关键词列表,在信息汇总的地方进行二次过滤

此外,尽量避免使用「稍后阅读」类工具,也不要在浏览器书签或者笔记工具中上收藏一大堆文章 —— 对此有些人可能会有异议,但就我而言,信息的收集只是为后续的知识提炼准备材料,我的目的 不是建立一个可供快速搜索的庞大资料库 。所有收集到的信息,都应该在一定时间内被消化然后清除掉,如果有哪些信息一直滞留在收集阶段,那么这样的信息对我来说是无用甚至有害的。

信息的收集

RSS 和「即刻」

我主要使用 RSS 阅读器来收集、汇总不同的信息源。

首先介绍一下 RSS,它是「简单信息聚合(Really Simple Syndication)」的简称,是一种标准化的信息格式,允许人们将不同的信息来源以统一的格式进行聚合和处理。RSS 最初用来整合不同新闻站点的内容,后来在博客上被广泛使用。需要明确的是,RSS 只是一种「信息标准」,包括以特定结构和格式来存储信息,以及对信息的更新,而 RSS 阅读器则是支持这种信息标准的一个整合工具,用来接收不同信息源的 RSS 输出,然后在统一的界面中进行呈现。

RSS 阅读器的好处是保证阅读环境和交互的统一,以及基于统一信息标准上的丰富的扩展操作。某种程度上来说,像 Pocket 等稍后阅读工具或者 Evernote/为知笔记 等笔记手机工具,也能做到「阅读环境和交互的统一」,但是这些工具只提供环境,不提供「信息的自动收集」这个功能。RSS 阅读器会定时去检查信息源的 RSS 输出,发现更新后就呈现出来 —— 当然也可以用 IFTTT 一类的工具来监听信息源然后自动化地添加内容到 Pocket/Evernote 里,但这样稍显麻烦一些。

在 RSS 阅读器中,我可以对信息源进行分类整理,RSS 阅读器帮我进行定时的更新同步,我则定期打开 RSS 阅读器,选择某个类别一口气读完而不用在乎它是从哪个来源产生的。这种阅读体验很难在别的地方获得 —— 有点类似微博的 feed 流,但是社交平台上的内容质量相对还是太低了。

inoreader.png

然而使用 RSS 阅读器会碰到一个问题,那就是一个信息源是否提供 RSS 格式的输出是由信息源自己控制的,它如果不愿意提供 RSS 输出,那么似乎就没有办法获得其中的信息了。当然实际上是有解决办法的,稍后再谈。

除了 RSS 阅读器,我还使用手机 App 「即刻」来接收资讯类信息。前文也说了,资讯类信息往往具有短时效性,且数量庞大,如果用 RSS 阅读器接收,很容易造成信息过载,对我来说,我只要知道每一天发生了哪些大事件即可,即刻上的「今天微博都在热议什么」和「一觉醒来世界发生了什么」很好地满足了我这个需求。

RSS 阅读器我用 Inoreader,再加上即刻,这就是我使用且仅使用的两个在线的信息收集工具。书籍阅读我要么用 Kindle,要么直接买实体书,书籍阅读笔记的收集属于知识的提炼部分,因此在本文不进行讨论。

Feed43: 从静态网站生成 RSS 输出

前文也说了,RSS 只是一种信息标准,信息源如果有提供 RSS 输出,那当然是最好的,如果没有,那么只要用一些方法将内容从信息源中「抽取」出来,然后按照 RSS 的格式组织好,不就生成了一个 RSS 输出结果吗?没错,任何一个可以公开访问的信息源,理论上都能转换为 RSS 输出,基本步骤就是「内容抽取-格式转换」两个步骤,由于 RSS 的格式是标准的,格式转换这一步没有太多难度,因此将一个网站的内容转换成 RSS 输出,主要的难度在内容抽取上。

最简单的一种情况是静态网站,所谓静态网站,是指访问的网页内容就是一个对不同的人来说内容不变的网页文件,这种网站上的内容访问不受限制,比如说不需要登录啊之类的,因此可以很简单地完成内容抽取这一步。而 Feed43 就是这样一个用来从静态网站中生成 RSS 输出的工具。

Feed43 的使用需要一点 HTML/CSS 的知识,稍微有点门槛,如果没有这方面知识的需要先了解一下。接下来以简书用户「计算士」的简书首页为例演示一下如何整个过程。

进入 Feed43 的页面后,点击「Create Feed」进入操作页。首先填入要处理的网页地址,在这里填入要解析的网页: http://www.jianshu.com/users/2e2954a2be81/latest_articles ,然后点击「Reload」来载入网页内容,如下图所示:

feed43_step1.png

其实每一步 Feed43 都给出了简洁但友好的提示,如果懂 HTML/CSS ,按照提示一步一步进行就可以了。在载入网页内容后,Feed43 会要求填写提取规则,包含两部分

  • Global Search Pattern: 用来定位内容的总体区域所在
  • Item(repeatable) Search Pattern: 用来定位每一条内容的具体位置

提取规则会有一些简单的语法,可以通过点击界面上的小问号来查看了解。

提取规则填写好后,点击「Extract」可以看到提取内容,如果提取规则写得不对,那么提取结果会给出反馈,回去继续修改直到能正确提取即可。 feed43_step2.png

提取出内容后,再定义好 RSS 输出的内容,其实就是填写 title 是什么,url 是什么之类的。

feed43_step3.png

然后点击 preview 可以预览生成的 RSS 输出

feed43_step4.png

如果 preview 没有问题,Feed43 会生成可用的 RSS 链接,比如上述「计算士」的简书页面,生成的 RSS 链接为: http://feed43.com/5507461252042782.xml

微广场: 从微信公众号、知乎、简书、百度百家生成 RSS 输出

微信公众号是一个封闭的内容平台,对于想使用 RSS 阅读器来聚合信息的人来说可能是一个难以割舍的信息来源。而微广场对此提供服务,从微信公众号、知乎、简书等站点生成 RSS 输出,这样就可以把订阅的一堆微信公众号以及知乎上关注的很多专栏都整合进 RSS 阅读器里了。

简书在 Feed43 一节讲了,由于是静态网站,是可以用一些简单的工具来转换的,不过有现成的服务自然是乐得轻松了。微信公众号其实是一个很封闭的平台,不过好在后面搜狗提供了微信公众号的搜索,所以也有办法进行转换了。

iwgc.png

由于免费用户只能订阅 10 个站点,我购买了一年的会员,除了没有订阅数量上限外,会员还能提交微广场没有收录的站点。

微博看看: 从微博用户的 feed 流生成 RSS 输出

「微博看看」原名「微博档案」,是一个微博备份工具,同时提供 RSS 输出。我原先在微博上关注了不少做机器学习、深度学习方面的微博用户,但他们的 feed 流经常被我关注的其他微博用户的 feed 流打乱,有了微博看看后我就取关了这些微博用户,在 RSS 阅读器里看他们输出的内容了,目前只对「爱可可-爱生活」老师的 feed 流做了转换。

weibo_rss.png

Huginn: 从任意网站生成 RSS 输出

huginn.png

Huginn 是一个 Ruby 编写的自动化工具,在理念上类似 IFTTT 和天国的 Yahoo! Pipes,即进行事件的监听然后根据预先设定的规则自动化地进行后续操作。

Huginn 的 wiki 上列举了一些典型的使用场景,如:

  1. Never Forget Your Umbrella Again: 下雨提醒
  2. Adding RSS Feeds to Any Site: 为任意网站生成 RSS 输出
  3. Follow stock prices: 监听股票价格

在 Huginn 中,主要有 event 和 agent 两个概念,agent 类似 IFTTT 里的 channel,event 则是 agent 的输出。在 Huginn 中可以将一个 agent 的输出作为另外一个 agent 的输入,由此产生复杂的自动化操作。

huginn_diagram.png

上图来自 Huginn:烧录RSS的神器 一文,该文对 Huginn 的使用做了很详细的介绍。

和 IFTTT 不同的是,Huginn 需要自己部署,很多细节需要自己定制,因此在使用门槛上会高很多。

目前我用 Huginn 来将 Google Scholar 关键词搜索结果输出为 RSS,这样我在 RSS 阅读器里就能看到我关心的一些深度学习的主题的论文更新情况了。此外,前文提到的提供微信公众号「微广场」,因为内容生产方的一些防护策略,可能会不稳定,替代方案是用 Huginn 从搜狗的微信公众号搜索结果中生成 RSS 输出。

个人知识管理系统(一): 概述

2016年10月11日 08:00

浏览器上堆满了书签、收藏的网址,知乎和微博的 feed 流奔涌从无休止,摘抄本里这一页还是宋时买房的趣事、下一页就堆满了产品经理必知必会的几大模型。微信公众号里有好多有深度的文章,知乎里学到了好多知识,简书上以文会友真让人羡慕,豆瓣读书上有人在某本书下坚持不懈地记着笔记 —— 「P81 如果我们想增强自控力……」…… 我还能数出好多非常棒的阅读应用,「单读」、「Mono」、「豌豆荚·一览」、「豆瓣·一刻」、「好奇心日报」、「ONE」、「界面」……

我曾沉溺其中,你们呢?

浏览器上千来个书签,有多少是当时觉得很有价值而收藏下来,此后再也没有打开过的呢?手机上安装的一堆阅读应用,上次打开时是什么时候呢?看到公众号更新的小红点、feed 流里「有 xx 条新动态」,是不是忍不住去看看发生了什么?

在这个信息爆炸的年代,我们的注意力时时刻刻被卷入信息流中,有人乐在其中,有人干净利落地脱离。我呢,稍微有点贪心,我不想彻底地离开信息的洪流,但又想在其中保持足够的理智 —— 当然了,这是我希望的方式,但并非我追求的结果,关乎选择,没有对错。

我所追求的,是「知识」,而非信息。信息,只是一段接受到的有价值或无价值的数据;知识,是「我」对这个世界的认识,是外界信息在我身上的内化。信息引发我的思考、行动,知识决定我思考、行动的方式和方向。

首先,要对信息有所取舍,将那些无休止地耗费注意力、但对于个人心智成长毫无意义的内容从视野中彻底剔除。得到对自己有用的信息后,要从中提炼出知识碎屑,将这些零散的知识梳理、系统化,这些知识就成了自己的东西。

pkm_overview.png

为了这个目的,我一直尝试建立一套高效的流程,即标题所谓「个人知识管理(Personal Knowledge Management)」,当然,专业的知识管理定义和我的表述未必相符,知识管理和信息管理之间的区别,我也不是很明了,姑且这么用着。

我对这套 PKM 流程/系统有以下期待:

  1. 有足够的信息来源,但这些信息来源能集中汇总到一处
  2. 信息的筛选和过滤由我自己来进行,我不需要推荐,不需要「智能处理」
  3. 尽量少的干扰,尽量与社交行为隔绝
  4. 从信息中提炼的知识能集中汇总到一处
  5. 梳理、系统化的过程能使用我最舒服的编辑环境

对于第一点,其实信息来源一直是很多的,但是这些信息来源有很多种类型,关注的博客、知乎专栏、微信公众号、阅读 App 等,每个我都需要在不同的地方用不同的方式接收,在不同的环境之间切换将导致效率的极大损耗,这是我不能忍受的,我希望我只在一个环境中接收信息。

对于第二点,我不信赖大多数的推荐功能,加之我对业界的推荐算法有所了解,我知道这些推荐算法是无法满足我的需求的,至于「人工智能」,有多少「智能」就有多少人工,我觉得某些方面还是我自己聪明一点。从另外的角度来讲,自己控制自己的信息源也是很重要的,这能保证自己得到的不是别人刻意筛选过的结果,保证自己接收到的不是片面的信息。

对于第三点,我认为阅读是一件需要沉浸其中的事情,即使是「碎片化阅读」,也要保证当我开始阅读的时候不会被打断,而微信公众号、微博、知乎这种带有社交属性的平台,很难做到这一点。当我阅读时,我只想阅读时;当我打开微信时,我只关心社交和通信 —— 两者应当泾渭分明。

对于第四点,与汇总信息源的动机同理,如果提炼的知识分散在不同的地方,如便签、纸质笔记本、网络上的笔记应用,那么将影响后续梳理、系统化的效率。在这个问题上,已经有了一些比较成熟的方案,比如用 evernote/印象笔记/为知笔记来管理自己的笔记,但对我来说,我最终需要的输出自己生产的系统化的知识,而不是维护一个庞大的可供快速搜索的「知识库」 —— 从这点来说,为知笔记其实是一个不错的选择,但是它不满足我的第五点需求。

最后一点,我是一个 Emacs 用户,是 org-mode 的重度用户,我的写作行为 —— 博客、笔记、工作日志,都是在 org-mode 中进行的,甚至在浏览器上的很多需要输入的地方,我都通过 Chrome 插件调用 Emacs 来进行编辑。org-mode 天生的大纲式的组织方式让我能很好地梳理思路;内置的 babel 模块能让我不脱离编辑环境就能得到代码片段的结果 —— 本文所有附图就是在同一个文件内使用 babel 对编写的 graphviz dot 代码求值得到的输出;强大的导出功能让我能在 org-mode 里写的 LaTeX 数学表达式在导出成 HTML 的时候以漂亮的形式展现,比如「\(e^{i\pi}+1=0\)」;capture 功能能让我快速地新建笔记、日程、灵感……我是已经无法脱离 org-mode 的,markdown 简陋的功能在我看来就是一个玩具,但即使这样 markdown 也比网上各种编辑环境强太多了。前面也说过为知笔记其实是一个很好的知识管理工具,但是它仍然是基于 markdown 的,虽然有 org-mode 插件但只能在 Windows 下使用 —— 但即使在 Linux 下为知笔记能使用 org-mode 插件,想来也是阉割过的 org-mode,毕竟 org-mode 的语法是一个很庞大的体系,不是谁都有精力去完全支持它,而没有了 babel 和 LaTeX 支持的 org-mode,对我来说无异于废物。这是因为我自身经历而比较特殊的一点。

最后我将我的 PKM 系统设计为如下形式

pkm_design.png

基本的思路是:

  1. 将所有自己关心的信息源 RSS 化,对于完全没有办法 RSS 化的信息源,直接丢弃 —— 而将信息源 RSS 化的理由是,这是目前唯一一个允许我自己管理信息来源的方式,也是和后续许多其他工具进行整合的前提条件
  2. 将信息源的 RSS 收集到一个统一的地方,以便进行后续处理
  3. 汇总信息流后,使用一些工具来对其进行过滤,主要出于两个考虑
    • 即使是优质的信息源,也会有我不关心的内容
    • 不同的信息源之间会出现重复的内容
  4. 对于信息收集器里未被过滤的信息,我会定期阅读、清空,并且从中产生笔记、摘抄,而这些笔记也会被收集到一个统一的地方
  5. 笔记收集起来后,会按照领域、用途梳理好,有些会转换为任务,有些则被系统地整理成特定领域的知识体系

为了实现上述系统,有几个问题要去解决:

  1. 如何取舍信息源,毕竟信息源是如此的多
  2. 如何将信息源 RSS 化,像微信公众号这样的封闭信息是没有提供 RSS 输出的
  3. 如何对信息进行过滤
  4. 除开 RSS 化信息源外,我还有纸质书和 Kindle,从其中产生的笔记该如何和从 RSS 信息源中产生的笔记汇集到一处呢?
  5. 笔记又该如何管理和输出呢

(待续)

2016 年第三季度总结

2016年10月3日 08:00

生活方面

计划内容有:

  1. 开始晨跑
  2. 改造卧室

第一个计划,嗯,没有执行 —— 懒惰如我……事实上这和以前制定然后没有执行的计划具有同样的问题,那就是不具体。当然根本原因是,我在心理上,并没有在这方面进行改变的强烈欲望。第二个计划倒是完成了,拆掉并且扔掉了笨重的衣柜,把我的床变了一下形 —— 我的床其实是那种储物床,相当于由两个大储物箱拼接起来的,我把其中一个旋转了 90 度,这样就能把房间尽头暖气片附近那点空间利用上了,如下图所示。

my_bed.jpg

然后把大量暂时没有看的书、冬天的衣服全塞进了床底下,就把夏天的一些衣服挂在了外面。

第三季度的生活状态,稍微有点乱。六月份离开前公司后,花了挺多的时间面试想尝试一些新的机会,直到八月份才安稳下来,并且决定以后就在 NLP 这个方向上走下去 —— 其实我在这个方向上并没有什么积累,但是已经毕业三年了,我想我必须要专注在某个特定的方向上。此外就是集智俱乐部在这期间开始了「自然语言处理与深度学习读书会」,我自告奋勇上去主讲了三期,倒是感觉自己收获非常大。

目前已经工作快两个月了,同事老板都很 nice,我对目前的境况是感到比较满意的。

Q4 就是秋冬时期了,除了有雾霾和静电,是我比较喜欢的时期:有风,温度比较低。我也就不提晨跑什么的啦,内心没有在这方面改变的想法,制定了计划也是白搭。外出锻炼、健身对我来说还是比较大的改变,我想我可以先从做一些更小的改变开始,比如早睡早起,比如吃早饭。

所以 Q4 的计划是:

  1. 至少 60 天 11 点半前睡觉, 6 点半前起床
  2. 至少 60 天去吃早饭
  3. 至少 60 天不玩游戏
  4. 看 30 部电影

是很小的计划,所以我会坚持去做好。

专业能力方面

计划内容有:

  1. 在 panic-notebook 中增加 Theano 的笔记
  2. 使用 Keras 在 Kaggle 上完成一些至少十个任务
  3. 完成集智俱乐部 NLP 线上读书会(其中 RNN 的部分由我主讲)
  4. 掌握 Dialogue/QA 的技术,实现一个对话系统

第一项和第二项未完成,第三和第四项完成。Theano 笔记未完成的原因是因为我对 Theano 并没有真正很熟悉,所以要写的时候不知道要写什么,就搁置了;kaggle 任务未完成的原因,在于我的动手能力太弱,每次一动手写什么东西,我就想在一开始尽善尽美,考虑效率考虑扩展性考虑代码的美观 —— 这种习惯在某些场景下可能是好事,但在有些时候并不是,而我设定这个任务的目的,本来就是为了对抗自己这种习性,希望能在快速的练习里提高自己的解决问题的能力和效率。

第三项之所以完成,是因为这是一个受到别人监督的任务,尽管如此,在这期间我仍然一再拖延。第四项因为是和现在工作内容紧密相关的,不过完成度不算高。

Q4 的话,我会更加专注在 NLP 上,但是这块内容很多,我可能更多地会跟着工作内容走。除此以外,我还会去了解一下网络信息抽取和爬虫,以及 Docker。根据以上目标,暂时设定 Q4 的计划为:

  1. 熟练掌握句法分析: 能用之进行句法成分地提取、句型的检测和转换等任务
  2. 熟练掌握 Scrapy、PhantomJS 和 Selenium: 完成至少 5 个爬虫
  3. 熟练掌握使用 Docker 进行项目部署、测试的流程
  4. 阅读 40 篇以上的 paper(大概两天一篇),主题不限,做了笔记并发布到博客或公众号上的记为有效结果

除此以外,还买了几本 NLP 相关的书,也计划在 Q4 把它们读完。

个人修养方面

计划内容:

  1. 读完《中国神话传说》、《神话与诗》、《失落的天书:山海经与古代华夏世界观》、《中国小说史略》、《雪人》和《火星崛起》六本书
  2. 坚持每天日记
  3. 坚持每周输出论文阅读笔记或书籍阅读笔记
  4. 建立起自己的 PKM 系统

阅读方面,《中国小说史略》和《神话与诗》尚未读完,其他四本都完成了。其中刘宗迪的《失落的天书》给了我很多惊喜,特别是其中对「龙星纪时」的论述,因此 Q4 会追着读过的几本中国古代神话、山海经相关著作的参考文献深入下去,《中国小说史略》暂时搁置,Q4 也不打算去阅读。闻一多的《神话与诗》打算快速翻一遍,然后之前买的顾颉刚的《三皇考》也快速翻一遍,再挑一本讲稷下学宫的书和一本讲中国天文学史的书看一看。小说就比较随意了,但作为一个科幻爱好者,我决定还是多读一点科幻小说,保持自己在这方面的触感。

每天日记这个没有坚持,大部分时间是靠自己自觉,我想还是需要靠自动提醒的一些手段来帮助加固这个习惯。

至于写作,九月共计有三篇博客输出和一篇论文笔记输出,并不达标。我写博客或笔记的一个自我限制是,总想把事情表达地很全面细致,这就要求我为之付出很多考证和查漏补缺的功夫,后面可以考虑稍微牺牲一下完整性和系统性来加快输出速度。另外之前尝试在公众号中进行写作,但是公众号里的写作体验很糟糕,后面决定还是统一发布到自己的博客上,然后挑选一些对别人有意义的文章放到公众号上去,就当给博客引流了。

第四项是我为之付出了大量精力的一项任务,并在这几天完成了收尾工作。

我所设想中的 PKM 系统,总的来说包含两个部分:

  1. 信息收集、筛选部分
  2. 知识提取、归纳和输出部分

第一部分一开始想用 Huginn 来完成,但是它并没有自己想象中那么好用,而且和我在使用的其他工具整合度不高,所以后来放弃了以 Huginn 作为核心工具,转而购买了 Inoreader 专业版,用 Inoreader 来进行信息的汇总和过滤,然后通过 IFTTT 来进行二次过滤和中转。如下图所示。

information_collect.jpg

第二部分通过 Emacs 的 org-mode 来进行 —— 之前也是一直使用 org-mode 来进行知识的整理,但是尚未形成自洽的系统。系统的大致结构如下图所示。

knowledge_process.jpg

除此以外,发现自己房间空白的墙相当于一块大白板,我现在会把自己在家时的一些读书笔记、杂乱的一些想法写到便签上然后贴在墙上,感觉很不错。

stickynotes_on_wall.jpg

关于 PKM 的设计,后面我会单独写一篇文章来讲。

所以 Q4 的计划是:

  1. 至少写 70 篇日记
  2. 早起后手抄一篇古诗文,至少完成 70 次
  3. 阅读完这几本书籍:
    • 顾颉刚《三皇考》
    • 闻一多《神话与诗》
    • 《稷下学研究》(要去三联买耶……)
    • 冯时《中国天文考古学》
    • 迟卉《荆棘双翼》
    • 《上帝的图书馆》
    • 《6号泵》
    • 《献给阿尔吉侬的花束》
    • 《24个比利》

然后,不再对自己的输出设立目标,因为实际上我的输出效果和效率是受我的 PKM 系统影响的,所以让自己的 PKM 系统真正地运转起来后,自然就能达到自己的期望了!

博客启用新域名

2016年9月18日 08:00

中秋的时候,在 Namesilo 上买了个域名,然后解析到了这个博客上,以后也会逐渐地把网络上的 ID 换成对应的 ID。

然后,讲点过去的小故事。

Linux, Lisp: ID "Linusp" 的由来

基本上每个人初看到我这个 ID,都会问这个 ID 和 Linus Torvalds 是不是有什么关系,或者我是不是崇拜 Linus 之类的。实际上这个 ID 和 Linus Torvalds 没有什么关系,我也不崇拜这个人(事实上我不崇拜任何人)。

事情的最初,是在 2010 年 9 月份,我去参加了学院社团组织的「自由软件日」或者什么活动,时间久远,不太记得清了。活动大致就是宣传了一下开源精神啦之类的东西,理所当然地也介绍了 Linux,然后现场演示了一下 Ubuntu 系统的使用,用 Compiz 展示了一番桌面特效,什么窗口最小化动画啦、桌面立方体啦、火焰特效啦,当时作为一个刚接触编程才半年不到只会在 Windows 下用 Turbo C 傻傻地写 C 代码的吃瓜群众,一下就被震惊到了。临走的时候从组织者那里领了一张 Ubuntu 的光盘,现在想想,那个光盘好像是从 Ubuntu 官方申请到的呢,可惜后来被我弄丢了。

光盘拿回去后,折腾了一阵子装上了双系统,那个 Ubuntu 是 10.04,还不赖。装好以后呢,大部分的时候是在进行各种桌面美化、特效配置,并没有做太多深入了解。后来美化、特效什么地玩腻了,就渐渐地不怎么碰了。

然后学期结束的时候,去图书馆借了几本书,准备带回家过冬。现在是想不起来当时的心情了,借了本《程序设计语言原理》,本来以为是本讲怎么编程的书,结果是本讲语言本身原理的书,大半我都没看懂。但这本书让我了解到了 Lisp 语言。当时觉得 Lisp 和 Prolog 有意思极了,于是回学校后就把尘封的 Ubuntu 系统捡起来了,开始在上面练习 Lisp 的编程。然后又因为 Lisp 了解到 Emacs 慢慢成为了 Emacs 用户。想想还是蛮奇妙的。

我接触电脑是在初二的时候,接触网络则是在高一,但高中那些年主要都是在玩网游和聊 QQ,到大学的时候才知道还有贴吧啊、微博啊好多好多东西。也是在那个时候开始在网络上活跃起来,注册了贴吧、微博帐号,逛各种社区、论坛。最开始的时候在各个地方会用不同的 ID,后来想着要有一个独特的个人标识,就把自己喜欢的 Linux 和 Lisp 两个词混合起来,得到了 Linusp 这个 ID,如果硬要说 Linus 是 Linux 之父那我也没办法,但我一向是个吃鸡蛋不问母鸡的人,另外 Linus 有些行为我也是不喜欢的 —— 嗯,但我不反对。

异或门, 邮件人和「电子怪兽」

刚学编程的时候,最搞不明白的就是异或运算了 —— 当然异或运算本身是很简单的,但是经常会有一些用异或运算来组合起来的复杂运算,当时感觉就像黑魔法一样,即使是现在我还是没法很好地操控这种「黑魔法」。再后来学电路,了解到异或门也是复杂芯片中的不可或缺的部分,事实上在 CPU 的最基础的部件加法器中,异或门就是核心的逻辑门。再后来了解到人工智能(Artificial Intelligence, AI),发现异或门又在第一次 AI 之冬中发挥了很大的影响(当然实际上并不是的)。

异或门是不进位的加法器,是非线性的逻辑单元。

在此之前,还在高中的时候,在《科幻世界》上看过一个小短篇,讲计算机上的逻辑单元因为故障衍生出智能的故事。不知道什么时候起,模模糊糊地在心里把 AI 和异或门联系起来了 —— 这当然不是说我认为异或门会产生智能,只是一种从科幻爱好者的视角产生的「它们之间好像有点什么共性」这样的想法。

说到科幻,其实我在七八岁的时候就开始看科幻了。小时候是个好奇宝宝,什么都想问一问一探究竟,但是父母长时间不在家,加上很多问题就算父母在身边也并不一定能回答上来,而书本给了我很好地了解世界的途径。刚开始的时候,是什么都看,武侠小说、科幻小说、知音、读者、故事会、半月谈……捡到什么看什么,大部分都是我父母的书或者父母的朋友的书,偶尔赶集买两本书就高兴坏了。印象里最开始读的一本小说是讲农民起义的《红袄军》,第一本科幻小说我记得是《2001 太空历险记》,不过后面查了一下是克拉克的书,和我印象中的剧情好像不太一样。之后的四五年小学生涯,主要是在读各种武侠小说,初中的时候读到了王晋康的《水星播种》和《终极爆炸》,印象中还读过阿西莫夫的一个短篇,但好像是讲双重人格的。自那以后,武侠小说和科幻小说就成了我的主要阅读对象。高一和高三的时候,有持续地买《今古传奇》和《科幻世界》,毕业的时候这些杂志都能摞成几堆一米多高的书堆了。

到大学的时候,能看到的书更多了,也了解到了更多的科幻作家和作品。在网上读过弗诺·文奇的《循环》和《天渊》后,对他的作品印象很深刻,后来就去读了《真名实姓》,非常棒。在《真名实姓》的故事里,有一个具有自我意识的程序在 Cyber Space 里活动,就叫做「邮件人」。另外,《真名实姓》成书比《神经浪游者》更早,可以说是赛博朋克的最早作品(比《攻壳机动队》、《黑客帝国》要早啦)。弗诺·文奇后面写的《彩虹尽头》也很棒。

所有这些混杂在一起,产生了「电子怪兽」这样一个 ID。从一名科幻爱好者的角度来看,我认为所谓的「强人工智能」会是从巨量的冗余信息里涌现出来的模式,就像「邮件人」一样,它是赛博空间中的异类,电子元件中的怪兽;从一名算法工程师的角度来看,「强人工智能」是镜中花水中月,所谓「人工智能」,能处理现实中的大部分的非线性映射,就已经是极大的进步了,而异或门就是一个最简单的非线性问题。

所以呢,本来是想换成 emonster 这个 ID 的,但是 Github 上这个 ID 已经被使用了,相应的域名也被买走了。退而求其次,把 "e" 换成我的姓的首字母,就是这样啦!

Github Pages 升级 Jekyll 版本造成博客构建失败

2016年9月11日 08:00

8 月底的时候写了一篇在 Windows 下建立 Python 开发环境的文章(嗯,其实是应一些朋友的要求写的),然后照常 push ,结果收到了一封邮件,告诉我博客构建失败了。

page_build_failure.png

点击邮件给出的链接,里面都是一些常规的排错建议,并没有给我相应的错误信息,看完还是一头雾水。为了确定是不是最近一次 commit 提交的文章内容有什么问题,我把 repo 回滚到了出错前的一个 commit 上,然后 'push -f' ,一秒钟之后又收到了相同的邮件。

当时感觉有点懵,但是因为手头还有别的事情,于是就丢在那里不管了。直到昨天,我才给 Github 发了封邮件,说了一下我的情况。很快就收到了回复。

page_build_failure_2.png

原来是 Github Pages 将 Jekyll 升级到了 3.2.1 ,而在 Jekyll 3.2.1 中,配置文件 _config.yml 中的 "theme" 字段,现在被用作设置 "gem-based theme",不能用于用户的自定义使用了。

按照邮件的建议,我把 _config.yml 中原先的 theme 字段改写为 theme_settings ,并将相应的模板文件中的名称也改过来,重新 push 之后没有再报错。

但我翻了下 Jekyll 的发布历史,并没有找到对这件事情的详细说明……

Windows 上 Python 开发环境搭建

2016年9月10日 08:00

前言

本文会涉及:

  1. Windows 上 Python 开发环境的建立(基于 Cygwin)
  2. Python 开发的一些基础知识
  3. 深度学习框架 Theano 在 Windows 环境下的安装和使用

简单介绍一下 Cygwin: Cygwin 是一个旨在提供 Windows 上的类 Linux 环境的软件,包含了提供 POSIX API 的底层 library 以及大量建立在 POSIX API 上的 GNU 软件和其他开源软件的集合。通过 Cygwin ,可以在不脱离 Windows 环境的情况下获得与 Linux 上一致的开发体验,当然也包括 Python。

Cygwin 的安装

Cygwin 的官网 上下载对应的安装程序,32 位系统就下载 setup-x86.exe,64 位就下载 setup-x86_64.exe —— 现在应该都是 64 位系统了吧。

cygwin_download.png

安装过程很简单,大部分情况下点击下一步进行下去即可。

cygwin_installation_1.png cygwin_installation_2.png

这里可以按自己的需要设置一下 Cygwin 的安装目录,同时在安装完成后,Cygwin 会将这个目录当作根目录(Linux 上的 /)。

cygwin_installation_3.png

安装过程中会从网络上下载各种东西,这里可以设置用于存放这些东西的 临时目录 :

cygwin_installation_4.png

到下面这一步需要选择下载站点,大致上来说选国内的站点速度会快一些,教育网的建议选中科大的源(http://mirrors.ustc.cn/),如图;非教育网的话可以在下面那个「User URL」输入框中填入网易的源(http://mirrors.163.com/cygwin/),点击 Add 后网易的源就会出现在列表中,选中即可。

cygwin_installation_5.png

然后会进入一个更详细的安装页面,一些基础的、必须的软件包已经默认选中了,可以按自己的需要选择一些额外的软件包。

cygwin_installation_6.png

点下一步继续,会开始下载、安装选中的软件包,如无意外,就可以顺利安装上 Cygwin 了。

cygwin_installation_7.png cygwin_installation_8.png cygwin_installation_9.png

至此安装结束,会在桌面上生成 Cygwin 的快捷方式,双击即可打开,正常的话,应该会看到下面这样一个黑乎乎的窗口,其实就是个终端啦。

cygwin_face.png

Cygwin 的界面设置

刚安装好时的 Cygwin 默认外观并不是很好看(当然比 cmd 还是好看一点的……),我们可以通过调整设置来获得一个更让人舒适的外观。

首先是字体,在 Cygwin 的窗口区域右键,选择 Options 会弹出 Cygwin 的设置,如下所示进行字体设置:

cygwin_font_setting.png

这里我选择了 DejaVu Sans Mono 这一款字体,这是我常用的编程字体 —— 以我的标准而言,一款编程用的字体,最低要求是要能区分出一些形状相似的字符,如下图所示:

cygwin_face_2.png

注: 这个字体并不是 Windows 自带的,需要自己下载和安装。

然后,如下图所示,在 Looks 一栏可以设置窗口的透明度

cygwin_ui_setting.png

这是选择透明等级 Med (中等)后的效果

cygwin_med_transparency.png

可以看到右边还有一个 "Glass" 可以选,选择后的效果为

cygwin_glass.png

注意,这里我为了配合透明和 Glass 效果,把窗口的滚动栏去掉了,见设置页面的 Window 一栏。

最后一项,我们设置一下 Cygwin 里的终端提示符。终端提示符一般通过环境变量 "PS1" 来进行设置,由于这个是 Cygwin(Linux) 特有的环境变量,就不在 Windows 的系统设置里设置这个环境变量了。我们可以新建一个名为 ".bash_profile" 的文件,Cygwin(严格来说是 shell) 启动时会读取这个文件并进行初始化。由于 Windows 下不允许直接以 "." 符号起始的文件,这个文件只能在 Cygwin 中创建,用 vi 命令直接创建并编辑即可(注意是 vi 而不是 vim),写入内容

CC_RESET='[00m'

CC_BLACK='[0;30m'
CC_RED='[0;31m'
CC_GREEN='[0;32m'
CC_BROWN='[0;33m'
CC_BLUE='[0;34m'
CC_PURPLE='[0;35m'
CC_CYAN='[0;36m'

CC_LIGHT_GRAY='[0;37m'
CC_DARK_GRAY='[1;30m'
CC_LIGHT_RED='[1;31m'
CC_LIGHT_GREEN='[1;32m'
CC_YELLOW='[1;33m'
CC_LIGHT_BLUE='[1;34m'
CC_LIGHT_PURPLE='[1;35m'
CC_LIGHT_CYAN='[1;36m'
CC_WHITE='[1;37m'

force_color_prompt=yes
export PS1='\[\e${CC_WHITE}\][\[\e${CC_RESET}\]\
\[\e${CC_LIGHT_BLUE}\]\w\
\[\e${CC_RESET}\]\[\e${CC_WHITE}\]]\[\e${CC_RESET}\]\n\
\[\e${CC_LIGHT_GREEN}\]\u \[\e${CC_RESET}\]\
\[\e${CC_LIGHT_RED}\]\$\[\e${CC_RESET}\] '

效果如下:

cygwin_bash_prompt.png

Python 的安装和使用

安装 Python/Python3

默认情况下,Cygwin 中没有安装 Python ,不过它会读取 Windows 系统中的环境变量,如果自己有单独安装 Python,且已经配置好环境变量,在 Cygwin 中也可以运行 Python 。但建议还是安装 Cygwin 自身软件仓库中的 Python。

首先下载一个 Cygwin 的软件包管理工具: apt-cyg 。这是一个类似 Debian/Ubuntu 中的 apt-get 的工具,可以让我们在 Cygwin 中查找、安装、卸载软件包,非常的方便。依次在 Cygwin 中执行下面两条命令即可安装 apt-cyg:

lynx -source rawgit.com/transcode-open/apt-cyg/master/apt-cyg > apt-cyg
install apt-cyg /bin

由于 apt-cyg 使用 wget 来访问服务器上的数据,建议先安装一下 wget (不安装的话会用 lynx 替代),同时一些常用的工具也先安装一下

apt-cyg install wget vim gcc-core tmux sl

apt-cyg 的几个子命令:

  • install <package>: 安装软件包
  • remove <package>: 卸载软件包
  • listall <keyword>: 以指定的关键词搜索相关的软件包

apt-cyg 安装好后就可以用来安装 Python 的基本环境了:

apt-cyg install python python3

美中不足的是 Cygwin 中的 Python 3 版本为 3.4.3 ,这对需要使用 Python 3.5 的朋友来说可能是一个比较大的问题。

安装 pip

在安装好 Python 后,通过下面的命令安装 pip 和 pip3:

python -m ensurepip
python3 -m ensurepip

鉴于国内访问 pip 官方的源速度有点慢,可以考虑修改 pip 的配置,使用国内的源,比如:

首先在 Cygwin 的 HOME 目录中新建配置文件:

mkdir ~/.pip/ && touch ~/.pip/pip.conf

然后往里写入以下配置(以使用豆瓣源为例):

[global]
trusted-host=pypi.douban.com
index-url=http://pypi.douban.com/simple/

建议配置好后也更新一下 pip 本身

pip install pip --upgrade

然后就可以直接安装自己想要使用的各种软件包了:

pip install numpy pandas nltk jieba

使用 virtualenv

在实际进行 Python 开发的时候,通常会使用 virtualenv 来进行环境的隔离,什么意思呢?举个例子,我们有两个项目,分别称之为项目 A 和项目 B,它们使用的 Python 版本和 Python 包的版本都不同,如下

项目 A:

  • Python 版本: 2.7
  • Python 包依赖:
    • jieba 0.37
    • nltk 3.1

项目 B:

  • Python 版本: 3.4
  • Python 包依赖:
    • jieba 0.38
    • nltk 3.2

由于系统上的同一个 Python 包是没法安装两个不同版本的,如果使用系统中的 Python 包,是不能满足以上这种情况的。virtualenv 的作用就是为每个单独的项目建立独立的环境。要使用 virtualenv ,在 Cygwin 上只能通过 pip 来安装:

pip install virtualenv

对项目 A ,按如下步骤建立环境:

  • 首先用 virtualenv 为项目 A 建立一个环境:

    virtualenv venv_for_a -p /usr/bin/python2.7
    

    这一步会建立目录 venv_for_a ,并将 python2.7 和 pip 拷贝到其中,形成一个最小的 Python2 运行环境。

  • 用 source 然后激活这个环境:

    source venv_for_a/bin/activate
    
  • 然后安装依赖

    pip install jieba==0.37 nltk==3.1
    

对项目 B 同样如此:

  • 建立环境

    virtualenv venv_for_b -p /usr/bin/python3.4
    
  • 激活环境

    source venv_for_b/bin/activate
    
  • 安装依赖

    pip install jieba==0.38 nltk==3.2
    

Theano/TensorFlow 的安装和使用

本节将在之前搭建好的 Python 开发环境基础上,展示 Windows 下深度学习框架 Theano的安装和使用。

下面的内容都在以下环境下进行:

  1. Cygwin PC x86_64
  2. python2.7
  3. pip 8.1.2
  4. ipythohn 5.1.0
  5. Theano 0.8.2

注: 我也尝试了在 Windows 上安装 TensorFlow ,但是以失败告终,详见后叙。

Theano

theano 的安装比较简单,直接用 pip 进行安装即可:

pip install theano --upgrade

Theano 还依赖 Numpy、Scipy,然后在安装 Scipy 的时候报错,错误信息为:

no lapack/blas resources found

LAPACK/BLAS 是提供线性代数相关计算的 C 库,既然缺失的话,用 apt-cyg 搜索看看:

apt-cyg listall lapack blas

结果为

lapack
lapack-debuginfo
liblapack-devel
liblapack-doc
liblapack0

liblasem0.4-devel
liblasem0.4-doc
liblasem0.4_4
libopenblas
openblas

把 liblapack-devel, libopenblas 和 openblas 装上

apt-cyg install liblapack-devel libopenblas openblas

重试安装,仍然发生错误,相应的错误信息如下:

error: library dfftpack has Fortran sources but no Fortran compiler found

这是因为缺少 fortran 编译器导致的,安装上 gcc-fortran 即可,但仍然报错,说找不到 g++ ,那把 g++ 也装上

apt-cyg install gcc-fortran gcc-g++

安装之后,尝试在 Python 交互环境中执行 import theano 出错,出错信息为

File "theano/gof/cmodule.py", line 2014, in compile_str
  return dlimport(lib_filename)
File "theano/gof/cmodule.py", line 289, in dlimport
    rval = __import__(module_name, {}, {}, [module_name])
ImportError: No module named cutils_ext

在 HOME 中的 .theano 目录下找到了 cutils,我的系统上是: ~/.theano/compiledir_CYGWIN_NT-6.1-2.2.1-0.289-5-3-x86_64-64bit–2.7.10-64/cutils_ext/。按照 这里这里 给出的信息,这个错误的原因在于里面一个名为 cutils_ext.pyd 的文件,它是 Python 生成的编译后的模块,实际上是一个动态链接库(Dynamic Link Library, DLL),见 Python 官方文档 。由于它是一个 DLL,Cygwin 在加载它的时候会希望它的后缀是 dll ,所以出错了,一种暂时的办法是修改一下它的后缀名

cd ~/.theano/compiledir_CYGWIN_NT-6.1-2.2.1-0.289-5-3-x86_64-64bit--2.7.10-64/cutils_ext/
cp cutils_ext.pyd cutils_ext.dll

这样在 import 时是可以成功的,但在执行下面这段代码时依然会出错

import numpy
import theano
import theano.tensor as T
from theano import function


x = T.scalar('x')
y = T.scalar('y')
z = x + y

f = function([x, y], z)
print f(3, 4)

这是因为用 Theano 定义的符号计算过程,最后也要编译成实际的模块,和前面的 cutils_ext.pyd 的问题是一样的,编译生成的模块后缀为 pyd ,但 Cygwin 加载时希望后缀是 dll 。所以要彻底解决这个问题,需要修改 Theano 中生成编译后模块的代码,让其生成的模块后缀为 dll 而不是 pyd。相关的代码在 Theano 代码中的 cmodule.py 中,在我的机器上,其路径为 "/usr/lib/python2.7/site-packages/theano/gof/cmodule.py"。其中有一个名为 get_lib_extension 的函数,其内容为:

def get_lib_extension():
    """
    Return the platform-dependent extension for compiled modules.
    """
    if sys.platform in ['win32', 'cygwin']:
        return 'pyd'
    else:
        return 'so'

将它改为:

def get_lib_extension():
    """
    Return the platform-dependent extension for compiled modules.
    """
    if sys.platform == 'win32':
        return 'pyd'
    elif sys.platform == 'cygwin':
        return 'dll'
    else:
        return 'so'

修改后可以尝试把 ~/.theano 这个目录删除,然后重新在 Python 交互环境中执行 import theano ,成功后会重新生成 ~/.theano/compiledir_CYGWIN_NT-6.1-2.2.1-0.289-5-3-x86_64-64bit–2.7.10-64/cutils_ext/ 这个目录,不出意外的话,里面应该只有一个 cutils_ext.dll 而没有 cutils_ext.pyd 了。

至此便可以在 Cygwin 中正常使用 Theano 了,如下图所示。

cygwin_run_theano.png

TensorFlow

在 TensorFlow 的 Download and Setup 中,介绍了在 Linux 和 Mac OS 平台上安装 TensorFlow 的详细过程,但并没有提供 Windows 的相关内容。不过其中的 Docker installation 一节给出的解决方案是一个在 Windows 上运行 TensorFlow 的折衷办法,但是仅仅为了安装一个 37MB 的 Python 包,而要下载 180 多 MB 的 Docker Toolbox,不太能接受。

目前在 Windows 上运行 TensorFlow 的方案有以下两种:

  1. 在 Windows 上安装 Docker 或者虚拟机,然后在其中安装 TensorFlow,参考: TensorFlow在Windows上的安装
  2. 使用 Windows 10 上的 Bash On Windows 安装 TensorFlow,有 Windows 10 的朋友可以自己尝试下

(这两种方法我也懒得去试验了,有兴趣和条件(我还用着 Windows 7 呢)的朋友自己再去折腾吧~)

事实上 TensorFlow 目前并没有对 Windows 的原生支持,不过对 Windows 平台的支持已经提上议程,见 Roadmap

在 Github 上有一个 Issue,从 2015 年年底至今,一直在讨论 TensorFlow 对 Windows 的原生支持这件事情,希望在 Windows 平台上使用 TensorFlow 的朋友可以关注一下这个 Issue。

其他方案

使用 Anaconda 是另外一个在 Windows 下建立 Python 编程环境的好主意,Anaconda 是一个基于 Python 的「数据科学平台」,内置了许多用于数据科学、机器学习的 Python 包,并有自己的软件包管理工具,也是很推荐的。

Anaconda 在 Windows 上的使用稍微尝试了一下,但没有对其细节做太多了解,所以就不罗嗦了。有需要的话后面再为 Anaconda 单独写一篇吧。

2016 年第二季度总结

2016年8月1日 08:00

计划及总结

生活方面的计划

计划内容有:

  1. 每周两次走路上下班,目的是保持良好的身体和精神状态
  2. 每两周参加一次活动,目的是保持与人交流

第一个计划,因为在 4 月份购买了自行车,所以改成了骑车上下班,确实对身体状态有帮助,早上到公司的时候感觉精神很多。车是在某次步行下班的时候在路边随便进了家自行车店,花了 300 多块钱买的,刚开始的时候不适应拥挤的车流,一路胆战心惊。周末晚上没事的时候会在上地南路到上地七街这片区域转悠,这一带有很多公司,平时是人很多的,但到周末特别是晚上基本是空荡荡的,很安静,到七街附近的时候经常能见到一些老人在昏黄的路灯灯光中下象棋,偶尔能看到情侣安静地散步,间或有一些车辆和其他零星的行人,除此以外便只有风和树影。心情苦闷的时候也会在这一块夜骑,效果不错。不过自从入夏后,天气炎热,慢慢地不怎么骑车上班了。

每两周参加一次活动这个计划我觉得后面还是终止吧,参加过不少愚蠢的活动了,大致上人多的活动都很愚蠢。最初订立定期参加活动这个计划,是为了结识优秀的朋友,以及开阔视野,但现在逐渐明白,自己必须优秀才能被其他优秀的人认可,而开阔视野这一点并没有从活动中有多大收获,因为参加的都是自己领域相关的活动,内容大部分都是已知的 —— 倒是可以考虑去参加一些其他领域的活动。

写这篇总结的时候第三季度已然过去三分之一。重新调整一下计划吧。

第三季度天气都会比较热,所以走路上班就算了,稍微凉快的时候骑车上班应该问题不大,就不列入具体计划中了。现在倒是想去晨跑,所以把这个加入到计划中吧!至于参加活动的计划就算了,还是那句话,「大致上人多的活动都很愚蠢」。

所以 Q3 计划如下:

  1. 开始晨跑(目前无具体计划)
  2. 改造卧室!

专业能力方面的计划

计划内容有:

  1. 系统学习 Theano
  2. 掌握一些 DL 的框架的使用
  3. 系统掌握 RNN 的相关知识,包括其理论及历史发展概况,后面可能会专门写一个 RNN 的系列文章
  4. 完成线性代数的学习

第四条没有完成,其他三条进行地都还可以。第三季度的话,计划做这些事情:

  1. panic-notebook 中增加 Theano 的笔记
  2. 使用 Keras 在 Kaggle 上完成一些至少十个任务
  3. 完成集智俱乐部 NLP 线上读书会(其中 RNN 的部分由我主讲)
  4. 掌握 Dialogue/QA 的技术,实现一个对话系统

除此以外,现在决定在 NLP 方向深入探索,但是现在并没有比较清晰的路线,暂时不做具体计划。

个人修养方面的计划

计划内容有:

  1. 坚持每日阅读
  2. 坚持写日记

阅读方面的话,主要读了两本神话学的书以及几本小说,第三季度会继续神话学方面的阅读。暂定要阅读

第三季度暂定要阅读的书有:

  1. 袁珂《中国神话传说》
  2. 闻一多《神话与诗》
  3. 刘宗迪《失落的天书:山海经与古代华夏世界观》
  4. 鲁迅《中国小说史略》
  5. 《雪人》
  6. 《火星崛起》

所以第三季度的计划如下:

  1. 读完上述六本书籍
  2. 坚持每天日记
  3. 坚持每周输出论文阅读笔记或书籍阅读笔记
  4. 建立起自己的「个人知识管理(Personal Knowledge Management, PKM)」系统

计划之外

  1. 公司散伙了,所以换了份工作
  2. 体检检出颈椎骨质增生

(完)

自然语言处理与深度学习: 集智俱乐部活动笔记

2016年7月4日 08:00

简介

本文为对 2016 年 7 月 3 日集智俱乐部活动「自然语言处理与深度学习」进行的总结。

集智俱乐部是张江在 2008 年建立的一个从事学术研究、享受科学乐趣的开放团体,它倡导以平等开放的态度、科学实证的精神,进行跨学科的研究与交流,力图搭建一个中国的 ”没有围墙的研究所”。我于 2014 年结缘集智俱乐部,从中结识到不少优秀又有趣的人。

集智会不定期地以读书会、讲座等形式进行线下活动,并且这两年的活动主要都聚焦于互联网和人工智能前沿技术上,7 月 3 日这期活动就是如此。

活动主讲人是是香港理工大学的在读博士李嫣然,其主要研究方向为自然语言的语义表达和语言生成。同时她还是微信公众号「程序媛的日常」的维护者之一小 S,这个公众号提供了高质量的关于机器学习和自然语言处理的文章,从事这方面工作或研究的朋友不妨关注一下。另,后文为了方便,一律用「小 S」来指代「李嫣然」。

下面是两个公众号的二维码,有兴趣的可以关注一下:

集智俱乐部 程序媛的日常
qrcode_swarmagents.jpg qrcode_girlswhocode.jpg

自然语言处理的基本任务

自然语言是我们日常用于互相沟通的最常见方式,结合个人的观点以及小 S 讲述的内容,「用自然语言进行沟通」可以分解为以下几个步骤:

  1. 对于接收到的信息,判断是否是自然语言
  2. 在确定接收到自然语言数据后,对其进行理解
  3. 在理解接收的自然语言数据后,组织好要回复的内容并将其传达给另一方

slap.jpg

好好好,我说人话,别打脸!

场景一: 接收到非自然语言

—— 小明: 汪汪汪汪喵喵喵
—— 小红: (说人话,不然滚犊子)

场景二: 接收到自然语言,但理解不能

—— 小明: 苟利国家生死已,岂因祸福避趋之,小红你有没有一种钦定的感觉?
—— 小红: (这个人在说什么……)

场景三: 接收到自然语言,理解并回复

—— 小红: 小明,下午放学后你把教室的窗户玻璃擦一擦吧!
—— 小明: 我擦,我不擦!

小 S 将这个过程划分为 Perceive、Understand 和 Communicate 三部分,并为 NLP 主要的研究或应用方向和这三部分进行对应,如下图所示:

nlp_task.png

Perceive、Understand 和 Communicate 和我之前总结的那三个步骤一一对应,或者说,之前我所说的那三个步骤,是我对 Perceive、Understand 和 Communicate 的理解吧。

  • Language Modeling: 对语言的建模是对整个语言或者特定场景下的语言的特性的一个刻画,所得到的结果通常被称之为「语言模型(Language Model, LM)」
  • Representation: 所谓表示一般是对具体的文本的语义、语法上的刻画,比如一个具体的词、一个具体的句子等等
  • Machine Translation: 机器翻译,无须多说
  • Language Generation: 即根据语义、语法生成有符合自然语言语法并能表达正确语义的文本

语言建模用来刻画整个语言的特性,所得到的语言模型有很多的应用,其中之一就是用来计算某个「给定句子」在这个语言的出现的概率以判别这个句子是不是一个「合理」的句子。而具体的词和句子等元素的表示,一方面是能用来分析这些元素的语义,另一方面也还反映了其所属语言的一些特性,因而是与 Perceive 和 Understand 都相关。

Language Generation 则是在理解之后进行的结果的组织,组织成自然语言当然是要给人看的,这是它和 Communicate 之间的关系所在。Machine Translation 作为一个重要的 NLP 应用方向,需要确保源语言句子和目标语言句子具备同样的语义,从目的上来说本身就是为了提高人与人之间的沟通效率的,因此和 Understand、Communicate 的关系也是很明白的。

对语言进行建模的若干方法

语言模型简介

语言模型是对语言整体特性的一个刻画,理想情况下,我们当然希望这个模型能够完全刻画「语言整体」,但有几个问题:

  1. 从时间角度来看,语言整体一直是在变化的,我们无法建立一个能刻画过去、现在和未来所有时刻语言特性的模型,因此一般的做法是对某个时段的语言数据进行采样
  2. 从语言本身来看,在不同的场景、人群中,同一种语言,其特性也会稍有不同,我们也无法建立一个能刻画所有可能的子语言特性的模型,尽管有人为建立「通用语言模型」而努力,但这种通用性本身也是相对的,绝对通用的语言模型不存在。换言之,在进行语言数据的采样时,按自己的需要,可能还需要将采样范围限制在某些特定的场景或人群中

按上述两个条件得到的语言数据,被称之为「语料(Corpus)」,理想的情况下,我们希望:

  1. 语料中不同时间的语言数据的特性基本稳定
  2. 在限制在特定场景或人群中时,希望数据能尽量涵盖受限条件下语言的各种情况

第一点要求语料的质量,第二点要求语料的规模,但实际情况中这两者很难一起得到保证。

有了语料后,我们怎么去得到语言模型呢?换言之,所谓的「特性」该如何量化?

首先,我们要知道,这个语言模型都涵盖了哪些基本元素,所谓「基本元素」,以英文为例,一般是单词,当然实际上也可以是词组或者字符,但单词是我们认识语言(至少是英文)的比较基本的单元。

然后呢,对任意的这些基本元素组成的序列,一般来说,就是句子,这个模型能给出其置信度(Confidence),用来判断这个句子在这个语言下的合理性,通常表现为一个概率。在语音识别以及其他一些应用中,通常会在最后有若干个候选的句子结果,此时就要用置信度来从中选择最合理的结果。比如说下面两个句子,他们的发音基本上是一样的,但我们是可以知道第一个句子其实是更合理的。

  1. It's hard to recognize speech
  2. It's hard to wreck a nice beach

总结一下,从给定的语料中可以得到一个语言模型,语言模型包含两部分:

  1. 包含语料中出现的基本元素的一个有限集合: \(V = \{ e_{1},e_{2}, ..., e_{n} \}\)
  2. 一个概率分布,对给定的序列 \(W = w_{1}w_{2}...w_{m}\),可以得到概率 \(P(W)\),其中 \(w_{i} \in V\)

也许我们会想,是不是需要考虑语法结构?毕竟一个合理的句子,应该是大致符合自然语言的语法结构的,但首先我们的自然语言并没有一个精确的、无歧义的语法规则,其次语法规则本身已经是一个更高抽象级别的概念了 —— 简单的就是好的,我们更愿意去寻找一些简单的方法,N-gram 语言模型和神经网络语言模型(Neural Network Language Model, NNLM)就是这样。

N-gram 语言模型

基于马尔可夫假设,我们可以认为,在句子中每个「基本元素」出现的概率只和其前面的元素有关,这样,对给定句子 \(W=w_{1}w_{2}...w_{m}\),可以得到其概率为:

\[P(W) = P(w_{1})\cdot P(w_{2}|w_{1})\cdot P(w_{3}|w_{1}w_{2})... P(w_{m}|w_{1}w_{2}...w_{m-1})\]

但是这样的条件概率仍然难以计算,因此我们再进一步地假设: 每个「基本元素」只和其前面 \(N-1\) 个元素有关。在这样的假设即 N-1 阶马尔可夫假设下,前面的式子可以进行近似简化(以 \(N=3\) 为例):

\[\begin{eqnarray} P(W) &=& P(w_{1})\cdot P(w_{2}|w_{1})\cdot P(w_{3}|w_{1}w_{2})\cdot P(w_{4}|w_{1}w_{2}w_{3})... P(w_{m}|w_{1}w_{2}...w_{m-1}) \\ &\approx& P(w_{1})\cdot P(w_{2}|w_{1})\cdot P(w_{3}|w_{1}w_{2})\cdot P(w_{4}|w_{2}w_{3})... P(w_{m}|w_{m-2}w_{m-1})) \end{eqnarray}\]

这样我们就得到了一个 3 阶的「N-gram 语言模型」,又称「Trigram 语言模型」,相应的,1 阶 N-gram 语言模型被称为「Unigram 语言模型」,2 阶 N-gram 语言模型被称为「Bigram 语言模型」,4 阶语言模型则可以简称为「4-gram 语言模型」,依次类推。除了能得到我们期望的概率分布外,大于 1 阶的 N-gram 语言模型还隐式地通过上下文信息一定程度上反映了语言的语法结构。

这里的「gram」是一个抽象的概念,在不同的粒度下它可以对应不同的单元,比如说我们如果基于字符来建模,这个 gram 就是字符;如果基于词来建模,这个 gram 就是词。一般情况下我们使用词作为 gram ,后文一些示例或讨论,如无特殊说明,默认使用词作为 gram。

根据上面的式子,我们要做的,就是从语料中得到每个「N-gram」的概率,最简单的办法是用「极大似然估计(Maximum Likelihood Estimation, MLE)」—— 即直接统计每个 N-gram 的在语料中的频次然后除以频次之和作为概率值。

from __future__ import division
from collections import Counter

def build_ngram(corpus, gram_level):
    """
    :param corpus: list of sentence
    :param gram_level: as it says
    """
    freqs = Counter()
    for sentence in corpus:
        seq = sentence.strip().split()
        for index in xrange(len(seq) - gram_level + 1):
            gram = tuple(seq[index:index+gram_level])
            freqs[gram] += 1

    total_count = sum(freqs.itervalues())
    return {gram: freq / total_count for gram, freq in freqs.iteritems()}

但这样的简单的计算方式是有问题的,在实际应用中,往往因为语料的规模不够,导致一些低频的 N-gram 在语料中未出现,这样在计算句子概率的时候,一个概率为 0 的 N-gram 将导致整个句子的概率为 0,此即「零概率问题」。随着 N 的增大,这个问题会越发的明显。

零概率问题的解决办法是将语料中一些有的 N-gram 的频次「分」一点给这些未出现的 N-gram,这个叫 「折扣(discount)」;或者用这个未出现的 N-gram 的低阶形式的概率来估计,这个叫做「backoff」。这些方法统称为「平滑(smoothing)」方法。

基于神经网络的语言模型

Bengio 在 2003 年提出的神经概率语言模型(Neural Probabilistic Language Model, NPLM)是影响较大的基于神经网络的语言模型1。其模型思想其实和 N-gram 语言模型还是同出一源,即基于 N-1 阶马尔可夫假设,认为句子中某个词是由其前面的 N-1 个词决定的。模型使用的网络结构如下图所示。

nplm_structure.png

这个模型将 N-1 个词的表示拼接起来作为输入,然后去预测下一个词最有可能是哪个。输出层会是一个很大的向量,每一维的值就是每一个可能的词的条件概率,这样要计算一个句子的概率也就没问题了。

这样基于神经网络的语言模型有什么好处呢?

  1. N-gram 需要显式存储每个 N-gram 的内容和概率,于是模型体积会随着语料的扩充而膨胀,但 NPLM 没有这个问题
  2. N-gram 需要应用各种平滑方法来解决零概率问题,但 NPLM 不存在这个问题,即使是语料中没出现的 N-gram ,依然能给出非 0 的概率值
  3. 模型中会学习一个固定长度的向量来表示一个词,且向量中各维度的值都是连续实值而非离散的 0/1 ,此即「Word Embedding」的较早形式

Bengio 还在论文中提到可以用「循环神经网络(Recurrent Neural Network, RNN)」来减少模型参数数量和捕获更远距离的上下文信息(longer term context),这些观点无疑是非常具有前瞻性的。

当然,值得一提的是,现为「IDL 杰出科学家」的徐伟曾在 2000 年独立提出用神经网络构建 Bigram 语言模型的思路2

在这之后,Tomas Mikolov 于 2010 年提出基于 RNN 的语言模型(Recurrent Neural Network Language Model, RNNLM)3,并且顺着这个方向一直发论文发到博士毕业,Bengio 还是厉害啊 —— 话说 Google Scholar 上 Tomas Mikolov 的照片一头长发害近视眼的我以为他是个姑娘呢 :)

RNNLM 的根本思想和 NPLM 是共通的,不同的地方在于词是逐个输入到模型中的,然后隐藏层有一个具有「记忆功能」的单元将之前的词的信息都记录了下来,然后在输出层去预测下一个词。如果把其中的记忆单元拿掉就变成了一个学习 Bigram 语言模型的前馈神经网络了。

语言的表示方法

在 NLP 的应用场景里,无论我们是要做分类、聚类、摘要、机器翻译,首先我们要将要处理的具体文本转换成模型能够处理的数学形式,并且要求这种表示能够反映文本的独特信息。这就是所谓「Representation」,其实也就是特征提取了。

说 representation 是 NLP 的核心应该不为过吧,事实上在其他机器学习相关的领域,特征表达都是非常重要的,特征设计、特征筛选在以前甚至现在依然是非常重要的一个学术和应用问题。

词的表示: one-hot 与 embedding

单个字符因为不具有语义,所以一般不对其表示方法作过多探讨(暂不考虑汉语)。词作为我们认识语言的基本单元,表示它们的最简单方法,就是为词进行编号,比如说我有一个包含 1 万个词的词典,第一个词就记为「1」,第二个记为「2」,依次类推。

这样当然是可以的,在一些简单的任务,比如文本分类上,问题不大,比如说我们可以统计不同 id 的词在两个文本中出现的频次来判断两个文本的相似程度。但问题在于 id 是一个类别变量值,它的值是不能用于比较、不能用于计算的,假想一下我们要构造一个简单的 NNLM ,某次输入的 N-gram 中包含了 id 为 10000 的词,如果直接输入到模型中,这个巨大的值会导致网络产生严重的偏差。

为了解决这个问题,我们一般用所谓「one-hot representation」来表示一个词,具体做法是,每个词都表示为一个向量,这个向量只有一个维度的值为 1,其他维度的值全为 0,且不同词值为 1 的维度都不同。比如说我们要处理的问题中只包含 5 个词,将这 5 个词编号后,可以分别表示为:

第一个词: [1 0 0 0 0]
第二个词: [0 1 0 0 0]
第三个词: [0 0 1 0 0]
第四个词: [0 0 0 1 0]
第四个词: [0 0 0 0 1]

这就是 one-hot 的含义所在,每个词的向量表示中,只有一个位置是「激活的」。

one-hot 表示应用广泛,但它本身有几个问题:

  1. 数据稀疏,一个包含 10000 个词的问题,每个词都要表示为长度 10000 的向量,但只有一个位置是有值的,这将会造成很大的存储开销
  2. 缺乏语义信息,两个词之间无法进行相似性比较

2003 年 Bengio 提出 NPLM 的时候,在模型中去学习每个词的一个连续向量表示,并经过 Tomas Mikolov 等人的发展4,发展出「Word Embedding」这一表示方法。比如说前面那五个词的表示,用 word embedding 表示后,可能是这样子的:

第一个词: [0.2 0.3 0.5]
第二个词: [0.7 0.1 0.2]
第三个词: [0.1 0.2 0.3]
第四个词: [0.2 0.3 0.4]
第四个词: [0.3 0.4 0.6]

这样两个词之间就可以比较了——当然,这种表示下相似值高的两个词,并不一定是具有相同语义的,它只能反映两者经常在相近的上下文环境中出现。但无论如何,one-hot 表示的两大问题,word embedding 都给出了一个解决方案。

word embedding 反映的词与词之间的相似性,可能在语义层面并不能说是真正的相似(取决于如何定义「相似」了),但「两个 word embedding 接近的词,它们是相关的」这一点是毋庸置疑的,如下图所示。

word_embedding_similarity.png

除了能反映同一语言的词之间的关系之外,不同语言之间语义近似或相关的词的 word embedding 也将这种关系反映了出来。

word_embedding_multilingual.png

此外,word embedding 还能一定程度上反映语言的内在规律(linguistic regularity),比如说 'king - man + woman = queen' 之类的,当然这个特定在实际应用场景里是很少用到的,而且我没记错的话主题模型也可以做到类似的事情,所以也并不是什么新东西了。

以上都是从 word embedding 本身来看的一些特点,但根据后来的一些讨论,word embedding 所反映的「相似性」其实要在具体的应用场景下才有确切的意义,像上面提到的 multi-lingual 的相似甚至 multi-modal 的相似,以及 linguistic regularity ,在实际应用中使用得还是较少的。引用小 S 的话,word embedding 更大的意义在于「它已经成为了各种后续或者更高级别粒度的语言表达的基础,这是完全超出相似度的一件事,也是词向量带来的最大贡献」。

当然除了 one-hot 和 word embedding 外,也还有其他的一些特征表示方法,比如说词性、语法结构等信息,都可以附加到词的特征上去。

更深入的关于 word embedding 的讨论,推荐这篇文章: “后 Word Embedding” 的热点会在哪里?

句子及更高层级数据的表示: VSM 和 embedding

句子、段落和文章,我们都可以把它们视为是词的序列,因此在很多场景下可以用统一的方式来进行表示。当然,在涉及句法结构分析时,基本是以句子为单位的,这里暂时不考虑这种情况。作为词的序列,我们该如何去表示它们呢?理想情况下当然是希望词的顺序啊、语义啊、语法结构啊都能够表达出来,但如果要将这些都反映出来,所使用的特征会比较复杂,光是语义和语法结构就够我们喝一壶了。

在文本分类、文本相似度量等一些主要的应用场景中,我们使用向量空间模型(Vector Space Model, VSM)来表示一段文本。VSM 经常和词袋模型(Bag Of Words, BOW)联系在一起。而 BOW 假设词和词之间是互相独立的,认为词和词之间的位置信息是不重要的,可以算作是 VSM 的一个前提、假设。

BOW 的解释也很形象,它假想我们有一个袋子,每遇到一个词就将其丢进袋子中,直到处理完所有文本。在此基础上, VSM 要求用一个向量来表示各个文档,这个向量的长度要与词袋中不同的词的数量一致。比如说我们有下面两段文本:

(1) John likes to watch movies. Mary likes movies too.
(2) John also likes to watch football games.

那么我们得到的词袋中包含的词就是 (John, likes, to, watch, movies, also, football, games, Mary, too) 共 10 个词,这样两段文本可以分别表示为:

(1) [1, 2, 1, 1, 2, 0, 0, 0, 1, 1]
(2) [1, 1, 1, 1, 0, 1, 1, 1, 0, 0]

如上所示,不同位置的值为对应的词在文本中的权重,这里使用的是词频。这样两段文本之间的相似程度可以简单地通过算余弦距离来得到。当然在实际中,我们希望不同维度的值是能反映对应的词在文本中的「重要程度」的,而直接使用词频并不能达到这个目的,事实上有一些常用的词会在很多的文本里都具有较高的频次,但它们本身并不包含重要的信息。所以一般用 TF-IDF(词频-逆文档频率)来作为词的权重。

VSM 的一个问题是,其特征表示往往会很稀疏(想象一个包含了 100,000 个不同词汇的文本集合),解决这个问题的一个方法是将一些文档频率很高的词去除,因为这样的词不能为文本与文本之间的区分性作出贡献,这样的方法能有效地降低向量的维度并保留有效的信息。

VSM 的另外一个问题是缺乏语义信息。比如说下面两个句子,它们表达的意义是不一样的,但在 VSM 中,两者的表示会一模一样

(1) Mary loves John
(2) John loves Mary

这个问题的一个解决办法是使用 N-gram 而非 word 来作为基本单元,比如用 Bigram,上述两句话得到的词袋会是: (Mary loves, loves John, John loves, loves Mary),对应的,两个句子可以表示为:

(1) [1, 1, 0, 0]
(2) [0, 0, 1, 1]

另外一个办法就是,使用句子级别的 embedding 表示。常用的方法有 RNN 和 CNN 两种。

RNN 的话比较简单,将句子中的词逐个输入到模型中,结束时取隐藏层的输出即可。这是因为 RNN 隐藏层的输出是由之前的「记忆」和当前的输入计算得到的,可以认为是「整个句子的记忆」,也就是一个句子的特征表示了。如下图所示:

sentence_embedding_rnn.png

CNN 的话是将句子中的词的向量表示拼接成一个矩阵,然后在上面进行卷积,最后得到一个向量表示,如下图所示:

sentence_embedding_cnn.png

CNN 和 RNN 得到的 sentence embedding,同 word embedding 一样,可以进行相似性的对比,语义相近的句子,其向量表示在空间中也会比较接近,如下图所示:

sentence_embedding_similarity.png

所以,既然可以得到 sentence embedding,能不能得到 paragraph embedding 乃至 document embedding 呢?对于前者,Tomas Mikolov 在 2014 年提出了「Paragraph Vector」5,后者的话似乎还没见到过。在 Mikolov 的实验中,paragraph vector 在情感分析上和信息检索两个任务上都取得了比其他模型更好的结果。不过和 word embedding 一样,sentence embedding 和 paragraph embedding 的可解释性仍然存在问题。

机器翻译: 从 SMT 到 end-to-end

沟通是人类的本能,所以将一种语言翻译成另外一种语言的需求,早在 17 世纪就有人提出来了,但我相信在更早之前,在不同语言的人群交界的地方,就已经有人在尝试做这个事情了。不过直到 1903 年,「机器翻译」这个词才被正式提出来。

1953 年的时候,Weaver 等人发表了一篇名为《Translation》的文章6,并在其中提出两个基本的思想:

  1. 翻译类似于密码解读的过程
  2. 原文与译文表达的是相同的意思,翻译可以通过将源语言转换成一个「通用语言」,然后再转换为目标语言

这里提到的「通用语言」是一种虚构的语言,假定为全人类共同的。这个思想其实和后面要讲到的 Encoder-Decoder 的思想有一些共通之处呢!

从方法上来说,最早使用的是基于规则的方法,后面出现了基于实例的方法,然后在大概 1999 年,在 IBM 推出了 5 种「基于词的统计翻译模型」并取得非常好的成绩后,统计机器翻译(Statistical Machine Translation, SMT)成为了机器翻译的主流方法,直到现在依然如此,Google、Bing、百度等几家的在线翻译系统,都是基于统计方法的。

统计机器翻译(Statistical Machine Translation, SMT)

目前主流的机器翻译系统,其基本思想来源于「噪声信道模型」,即认为源语言的句子 \(f\) 是目标语言 \(e\) 经过含噪声的信道编码后得到的,翻译的过程就是要求解出 \(\hat{e}\)

\[\hat{e} = \mathop{arg\,max}_{e}p(e|f)\]

然后利用贝叶斯公式,转换为

\[\hat{e} = \mathop{arg\,max}_{e}p(f|e)p(e)\]

其中 \(p(f|e)\) 被称为「翻译模型」,\(p(e)\) 则是语言模型。后者可以通过收集目标语言的语料训练得到,但前者怎么办呢?一个简单的思路是,将源语言句子中的每个词都翻译成目标语言对应的词,然后通过重新排列这些词得到符合目标语言语法的句子 —— 后面这个过程称为「词对齐(word alignment)」。

word_alignment.png

所以翻译模型会由两部分组成,一部分是词到词的映射,另一部分是对结果进行对齐。整个系统要在所有可能的对齐结果中找到最好的那个。

phrase_based_smt.png

这就是传统的统计机器翻译模型的基本原理。

端到端(end-to-end)的新方法

2014 年的时候,Kyunghyun Cho7 和 Sutskever8 先后提出了一种 End-to-End 即所谓「端到端」的模型,在英语到法语的翻译实验上取得了很好的成绩,前者将其模型命名为 Encoder-Decoder 模型,后者则将其命名为 Sequence-to-Sequence 模型,不过两者其实在结构上基本是一样的。其核心思想都是:

  1. 将输入的句子经过一个 RNN 生成一个能反映句子语义的向量,类似计算 sentence embedding 的过程,
  2. 用另一个 RNN ,从输入句子的向量表达中解码出另一个句子

第一个过程称之为 encoding,第二个过程称之为 decoding,整个模型非常的简单优雅,不需要词对齐,不需要额外的语言模型,输入英语,直接得到法语,这就是所谓「端到端」。

seq2seq.png

Encoder-Decoder 模型虽然在结构上很简单,但它有很重要的两个特点:

  1. 大部分序列到序列的预测问题,都可以使用该模型,这个序列甚至没有必要是文本,语音、图像同样可以
  2. 先 encoding 再 decoding 的 encoder-decoder 的结构,可以推广到非 RNN 的模型上去

当然,虽然 encoder-decoder 的模型结构简单优美,在一些问题上也还不错,它仍然是有缺点的,回到机器翻译问题上来,将一个句子 encode 成一个向量,真的是合适的么?一方面这个过程相当于进行数据压缩,肯定是会有信息损耗的,而且这种损耗随着句子长度的提高会越来越大;另一方面,翻译的时候并不一定需要完全理解整个句子的含义后才能进行,比如可能在看完句子的前半部分以后就可以进行部分的翻译,每个翻译结果中的词的生成都使用整个句子的语义的话,无疑准确度也会受到限制。

在这之后,Dzmitry Bahdanau 在 encoder-decoder 模型的基础上增加 attention 机制来解决这个问题9,通过双向 RNN 来在 decoder 时得到不同时刻的上下文信息。

其中 decoder 在每一个 time step 用下面的式子计算条件概率以判断该输出什么词。

\[p(y_{i}|y_{1}, ..., y_{i-1}, x) = g(y_{i-1}, s_{i}, c_{i})\]

其中 \(s_{i}\) 是 decoder 的隐藏状态

\[s_{i}=f(s_{i-1}, y_{i-1}, c_{i})\]

而 \(c_{i}\) 则是根据从 encoder 中不同时刻的隐藏状态得到的上下文信息,用来决定输出时该聚焦于输入序列的哪些部分。

\[c_{i}=\sum_{j=1}^{T_{x}}\alpha_{ij}h_{j}\]

上式中的 \(\alpha_{ij}\) 构成的权值矩阵就是最核心的内容,将其可视化的话,能发现高亮的部分其实相当于是源语言句子和目标语言句子之间的对齐路径

attention_alignment.png

对照传统的统计机器翻译系统,在 encoder-decoder 模型中,模型本身相当于隐含了语言模型和词到词翻译的两个部分,但却缺乏词对齐部分,而 attention 机制的加入弥补了这个缺陷。

另外要说的是,attention 机制和 encoder-decoder 一样,更大意义上在于它提供了一种解决问题的思路,而不是说某个具体的模型结构,事实上 attention 机制的实现是多种多样的,不一定要使用 RNN。在应用上,attention 机制在图像、视频上也有很大的应用空间,比如在目标识别时用来聚焦于不同的图像区域。

attention_image.png

2016 年, Kyunghyun Cho 又提出结合了 encoder-decoder 和 attention 的多语言翻译模型10,能够接收多用语言的输入,并翻译成多种不同的语言。

language_transfer.png

自然语言生成(Natural Language Generation, NLG)

从模板方法到 encoder-decoder

说到自然语言生成,我的第一反应是大刘的《诗云》,以及他制作的一款名为「电子诗人」的自动作诗软件。来看一看用电子诗人生成的一首「现代诗」:

作品第75509号——

我面对着黑色的艺术家和荆棘丛生的波浪

我看到,剌眼的心灵在午睡,程序代码在猛击着操场

在这橄榄绿的操场中,没有货车,只有蝴蝶

我想吸毒,我想软弱地变黄

我面对着光灿灿的冬雪和双曲线形的霞光

我看到,青色的乳房在漂荡,肥皂在聆听着海象

在这弱小的春雨中,没有贝多芬,只有母亲

我想上升,我想呼吸着歌唱

我面对着宽大的小船和透明的微波束

我看到,枯死的渔船在叫,蒸馏水在铲起羊

在这多孔的青苔中,没有夏娃,只有老师

我想冬眠,我想可恶地发光

我面对着多血的史诗和悠远的大火

我看到,生机勃勃的战舰在沉默,透明裙在爱抚着操场

在这曲线形的奋斗者中,没有月光舞会,只有风沙 我想摆动,我想粗糙地惊慌

自然语言生成除了上述这种娱乐性质的作用外,目前在自动天气预报、机器翻译、Image Caption 和对话系统中都有应用。

说到自然语言生成,如果要我们自己实现的话,能很快想到的一个基础的办法,就是模板生成方法。在一些受限环境下,需要返回的文本形式可能比较简单,用模板方法还是可以取得能用的结果的。

nlg_with_template.png

不过模板方法的缺点也是显而易见的:

  1. 泛化性极低,换一个场景就要重新设计一个不同的模板,如果要进行通用领域的自然语言生成,模板数量将会多到爆炸
  2. 输出非常死板僵硬,如果是在对话这样的场景下,用户很快就会发现得到的文本是由机器生成的
  3. 并没有真正「理解文本」

怎么样才算是理想的「自然语言生成」呢?

Once aware of the communicative goals, machines can understand the meaning of the content (what is important, what is what), and automatically generate natural, meaningful and plausible language. —— By 小 S

几个重点是:

  1. 理解得到的输入(根据不同的场景,可能是文本会图像)
  2. 要生成自然的语言表达,就算是胡说八道,也要一本正经

对于一本正经地胡说八道这件事情,其实是可以用语言模型来,用 N-gram 模型的话,随机选一个词,然后找到关联的概率最高的几个 N-gram ,依次类推。另外一种办法是在已有特定语言的「基本句法规则」的情况下,先生成句法结构,然后往里面填充词语。当然,这两种方法都是很简单的,如果直接使用的话生成的文本其实本身没有什么意义,要放到具体的应用场景里的话,这样的方法还必须和其他一些方法结合才行。

而使用之前提到的 encoder-decoder 框架,在一些场景下,能够做到 end-to-end 的生成,比如 Image Caption。

image_caption_seq2seq.png

所以说 encoder-decoder 这个想法真的是非常好用的呀!

对话系统中的自然语言生成

对话系统中使用到的技术,大致上可以分成三部分:

  1. 基于知识库的检索方法
  2. 基于知识图谱的简单推理
  3. 基于生成模型的自然语言生成方法

第一部分是在知识库中直接找到「合适的」现成的文本返回给用户,和自然语言生成基本没什么关系;第二部分涉及到对用户意图的分析,可能会有一些基于模板、规则的生成过程;第三部分,据我所知,好几家做问答的公司,都在尝试用 encoder-decoder 的模型。

没做过问答,以上都是我瞎编的 :)

前两部分就不谈了,第三部分使用 encoder-decoder 模型,基本都是基于 RNN 来去实现。这种生成模型虽然能够「理解」用户的问题并且一本正经的胡说八道,但仍然存在一些需要解决的问题。

  1. 语义漂移(semantic drift),直观点来说就是答非所问
  2. 生成的文本在语法上有时并不符合自然语言特性(这真是在胡说八道了……)
  3. 训练语料中的高频句式会频繁出现,典型的如「万能回复」问题,其实也算是语义漂移的一个表现
  4. 生成的文本语言风格可能与实际场景不符合

本质上 encoder-decoder 也可以认为是一种检索办法,只不过相比基于知识库的检索方法,它是一种模糊的、更细粒度的检索,语义漂移的问题其实就是模型在其巨大的解空间中迷路了,要解决这个问题,一方面是提高方向感,一方面也可以将解空间进行限制。Google 的 Allo 就被设计成使用混合响应策略来解决这个问题。

所谓的「混合响应策略」,其实就是通过其他的方法来辅助咯,比如获取用户的个人信息作为辅助特征,比如通过知识图谱并将推理结果输入到模型中,再比如确定用户的地理位置和环境因素作为额外的特征。

allo_mixed_response_strategy.png

至于语言风格的问题,可以通过情感分析等手段来获取用户的情绪状态,然后输入到模型中以得到不同风格的响应。

此外,在对话系统中,一个很典型的问题是「多轮对话问题」—— 大部分对话系统都是「一问一答」的,很少考虑前几轮对话的内容,也就是我们常说的「上下文」。对于这个问题,小 S 的回答是,目前在一块的研究中提到的方法大概有这么几种:

  1. 将前几轮的历史对话信息作为额外的输入
  2. 设计多层次的对话系统
  3. 结合用户画像(user profile)来生成文本,取 top-5 的答案再进一步筛选

综上,encoder-decoder 模型虽然简单强大,但仍然有自身固有的问题,如果要设计一个良好的对话系统,以目前的情况,还是需要和传统方法进行结合。

后记

这应该是我参加集智的活动之中质量最高的一次了,75 页的 slides,覆盖 NLP 几大基本问题的内容和展开,加上小 S 本身在 NLP 这块的造诣,给我带来了很大的收获。又因为自己对 NLP 兴趣浓厚,参加完活动后就兴起了写一个完整笔记的念头,历时五天,查阅了很多的论文,把个中尚未明白的一些细节搞清楚,总算是完成了这篇 1 万多字的笔记。

希望以后能看到更多这样高质量的活动!

脚注:

1

Bengio, Yoshua, et al. "A neural probabilistic language model." journal of machine learning research 3.Feb (2003): 1137-1155.

2

Xu, Wei, and Alexander I. Rudnicky. "Can artificial neural networks learn language models?." (2000).

3

Mikolov, Tomas, et al. "Recurrent neural network based language model." Interspeech. Vol. 2. 2010.

4

Mikolov, Tomas, et al. "Distributed representations of words and phrases and their compositionality." Advances in neural information processing systems. 2013.

5

Le, Quoc V., and Tomas Mikolov. "Distributed Representations of Sentences and Documents." ICML. Vol. 14. 2014.

6

Weaver, Warren. "Translation." Machine translation of languages 14 (1955): 15-23.

7

Sutskever, Ilya, Oriol Vinyals, and Quoc V. Le. "Sequence to sequence learning with neural networks." Advances in neural information processing systems. 2014.

8

Cho, Kyunghyun, et al. "Learning phrase representations using RNN encoder-decoder for statistical machine translation." arXiv preprint arXiv:1406.1078 (2014).

9

Bahdanau, Dzmitry, Kyunghyun Cho, and Yoshua Bengio. "Neural machine translation by jointly learning to align and translate." arXiv preprint arXiv:1409.0473 (2014).

10

Firat, Orhan, Kyunghyun Cho, and Yoshua Bengio. "Multi-way, multilingual neural machine translation with a shared attention mechanism." arXiv preprint arXiv:1601.01073 (2016).

在 NLTK 中使用 Stanford NLP 工具包

2016年6月8日 08:00

注意:本文仅适用于 nltk<3.2.5 及 2016-10-31 之前的 Stanford 工具包,在 nltk 3.2.5 及之后的版本中,StanfordSegmenter 等接口相当于已经被废弃,按照官方建议,应当转为使用 nltk.parse.CoreNLPParser 这个接口,详情见 wiki,感谢网友 Vicky Ding 指出问题所在。

NLTK 与 Stanford NLP

NLTK 是一款著名的 Python 自然语言处理(Natural Language Processing, NLP)工具包,在其收集的大量公开数据集、模型上提供了全面、易用的接口,涵盖了分词、词性标注(Part-Of-Speech tag, POS-tag)、命名实体识别(Named Entity Recognition, NER)、句法分析(Syntactic Parse)等各项 NLP 领域的功能。

Stanford NLP 是由斯坦福大学的 NLP 小组开源的 Java 实现的 NLP 工具包,同样对 NLP 领域的各个问题提供了解决办法。

斯坦福大学的 NLP 小组是世界知名的研究小组,如果能将 NLTK 和 Stanford NLP 这两个工具包结合起来使用,那自然是极好的!在 2004 年 Steve Bird 在 NLTK 中加上了对 Stanford NLP 工具包的支持,通过调用外部的 jar 文件来使用 Stanford NLP 工具包的功能。

从 NLTK 的 commit 历史中可以找到相应的提交记录:

commit e1372fef56bfb88d02fdb6c0ea88474d5f414a38
Author: Steven Bird <stevenbird1@gmail.com>
Date:   Tue Aug 3 12:20:20 2004 +0000

    added Stanford

    svn/trunk@2088

现在的 NLTK 中,通过封装提供了 Stanford NLP 中的以下几个功能:

  1. 分词
  2. 词性标注
  3. 命名实体识别
  4. 句法分析,依存句法分析

安装和配置

NLTK 3.2 之后加入了用于中文分词的 StanfordSegmenter 这个类,作者是知名 NLP 博主 52nlp,见 相关文章。而 NLTK 3.1 及之前则只有以下几个类:

  1. 分词: StanfordTokenizer
  2. 词性标注: StanfordPOSTagger
  3. 命名实体识别: StanfordNERTagger
  4. 句法分析: StanfordParser
  5. 依存句法分析: StanfordDependencyParser, StanfordNeuralDependencyParser

方便起见,本文以 NLTK 3.2 这个版本来说明如何进行相关的安装和配置,3.1 及之前的版本基本相同。

注意事项

需要注意这么几点:

  1. Stanford NLP 工具包自 2014 年 10 月后(大概是 version 3.5.0)需要 Java 8 及之后的版本,如果出错请检查 Java 版本
  2. 下面的配置过程都以 Stanford NLP 3.6.0 为例,如果使用的是其他版本,请注意替换相应的文件名
  3. 下面的配置过程以 NLTK 3.2 为例,如果使用 NLTK 3.1,需要注意该旧版本中 StanfordSegmenter 未实现,其余大致相同
  4. 下面的配置过程是针对不同的接口分别讲述各自如何配置,根据来自 NLTK 的源代码,分别是

    • nltk/tokenize/stanford.py
    • nltk/tag/stanford.py
    • nltk/parse/stanford.py

    如果不想了解这些细节,可以参考 NLTK 官方 wiki 页面上的内容,但需要注意的是,StanfordSegmenter 和 StanfordNeuralDependencyParser 这两者的配置和其他的都不一样,而 wiki 页面上并未覆盖到这部分内容。

  5. 事实上,也可以完全不进行环境变量设置,但这就需要在每次调用的时候手动指定参数

StanfordSegmenter

  1. http://nlp.stanford.edu/software/segmenter.html 处下载 stanford-segmenter-2015-12-09.zip (version 3.6.0)
  2. 将 stanford-segmenter-2015-12-09.zip 解压, 并将解压目录中的 stanford-segmenter-3.6.0.jar 拷贝为 stanford-segmenter.jar
  3. stanford-segmenter.jarslf4j-api.jar 加入到 CLASSPATH 中去

    例:

    export STANFORD_SEGMENTER_PATH="$HOME/stanford/segmenter"
    export CLASSPATH="$CLASSPATH:$STANFORD_SEGMENTER_PATH/stanford-segmenter.jar:$STANFORD_SEGMENTER_PATH/slf4j-api.jar"
    

之所以要将 stanford-segmenter.jarslf4j-api.jar 加入到 CLASSPATH 中,是因为在 StanfordSegmenter 的实现中显式依赖了这两个文件,并且优先在 CLASSPATH 中寻找这两个文件。如果在 CLASSPATH 中找不到 stanford-segmenter.jar ,则会在环境变量 STANFORD_SEGMENTER 指定的路径中寻找;同样的,如果找不到 slf4j-api.jar ,则会在环境变量 SLF4J 指定的路径中寻找。其他几个类也有同样的依赖设置,为了统一管理,可以将所有依赖的 jar 文件都加入到 CLASSPATH 中去,当然分别为不同的 jar 文件设置不同的环境变量也是可以的。

除了设置环境变量,也可以通过函数参数来传入依赖的 jar 文件的准确路径,此时将会忽略环境变量设置。

StanfordTokenizer

  1. http://nlp.stanford.edu/software/tagger.html 中下载 stanford-postagger-full-2015-12-09.zip (version 3.6.0)
  2. stanford-postagger-full-2015-12-09.zip 解压
  3. 将解压目录中的 stanford-postagger.jar 加入到 CLASSPATH 中,或者设置到环境变量 STANFORD_POSTAGGER 中

    export STANFORD_POSTAGGER_PATH="$HOME/stanford/postagger"
    export CLASSPATH="$CLASSPATH:$STANFORD_POSTAGGER_PATH/stanford-postagger.jar"
    

    export STANFORD_POSTAGGER="$HOME/stanford/postagger/stanford-postagger.jar"
    

StanfordNERTagger 和 StanfordPOSTagger

在 NLTK 里,StanfordNERTagger 和 StanfordPOSTagger 都继承自 StanfordTagger ,在设置上有共同之处,因此放到一起来讲一下。

  1. http://nlp.stanford.edu/software/CRF-NER.html 处下载 stanford-ner-2015-12-09.zip (version 3.6.0)
  2. http://nlp.stanford.edu/software/tagger.html 中下载 stanford-postagger-full-2015-12-09.zip (version 3.6.0)
  3. stanford-ner-2015-12-09.zipstanford-postagger-full-2015-12-09.zip 都解压
  4. 将解压后目录中的 stanford-ner.jarstanford-postagger.jar 加入到 CLASSPATH 中去,和 StanfordTokenizer 不一样,这两个类都只从 CLASSPATH 中寻找对应的 jar 文件(所以为了统一我建议都添加到 CLASSPATH 中去)

    export STANFORD_NER_PATH="$HOME/stanford/ner"
    export STANFORD_POSTAGGER_PATH="$HOME/stanford/postagger"
    export CLASSPATH="$CLASSPATH:$STANFORD_NER_PATH/stanford-ner.jar:$STANFORD_POSTAGGER_PATH/stanford-postagger.jar"
    

    同时将 stanford-ner-2015-12-09.zip 解压后目录中的 classifiers 目录和 stanford-postagger-full-2015-12-09.zip 解压后目录中的 models 目录加入到环境变量 STANFORD_MODELS 中去

    export STANFORD_MODELS="$STANFORD_NER_PATH/classifiers:$STANFORD_POSTAGGER_PATH/models"
    

StanfordParser, StanfordDependencyParser

StanfordParser 和 StanfordDependencyParser 都继承自 GenericStanfordParser ,使用 stanford-parser.jar 来提供句法分析功能。

  1. http://nlp.stanford.edu/software/lex-parser.html 处下载 stanford-parser-full-2015-12-09.zip (version 3.6.0)
  2. 将下载的压缩包解压,并将其中的 stanford-parser.jarstanford-parser-3.6.0-models.jar (这个在不同版本中名称会不一样) 都加入到 CLASSPATH 中

    export STANFORD_PARSER_PATH="$HOME/stanford/parser"
    export CLASSPATH="$CLASSPATH:$STANFORD_PARSER_PATH/stanford-parser.jar:$STANFORD_PARSER_PATH/stanford-parser-3.6.0-models.jar"
    

    或者将 stanford-parser.jar 加入到环境变量 STANFORD_PARSER 中,将 stanford-parser-3.6.0-models.jar 加入到环境变量 STANFORD_MODELS 中

    export STANFORD_PARSER="$STANFORD_PARSER_PATH/stanford-parser.jar"
    export STANFORD_MODELS="$STANFORD_MODELS:$STANFORD_PARSER_PATH/stanford-parser-3.6.0.models.jar"
    

StanfordNeuralDependencyParser

StanfordNeuralDependencyParser 虽然也继承自 GenericStanfordParser,并且用来进行句法分析,但它使用的 Stanford CoreNLP 中的功能和模型,不依赖 Stanford Parser 这个(子)工具包。

  1. http://stanfordnlp.github.io/CoreNLP/ 处下载 stanford-corenlp-full-2015-12-09.zip
  2. 将下载的压缩包解压,并将其中的 stanford-corenlp-3.6.0.jarstanford-corenlp-3.6.0-models.jar 加入到 CLASSPATH 中去

    export STANFORD_CORENLP_PATH="$HOME/stanford-corenlp-full-2015-12-09"
    export CLASSPATH="$CLASSPATH:$STANFORD_CORENLP_PATH/stanford-corenlp-3.6.0.jar:$STANFORD_CORENLP_PATH/stanford-corenlp-3.6.0-models.jar"
    

    或者可以更简单地将解压目录设置为环境变量 STANFORD_CORENLP 的值

    export STANFORD_CORENLP=$STANFORD_CORENLP_PATH
    

基本使用

使用 StanfordSegmenter 和 StanfordTokenizer 进行分词

StanfordSegmenter 是 52nlp 实现的对 Stanford Segmenter 的封装,用来进行中文分词。

# coding: utf-8
from nltk.tokenize import StanfordSegmenter

segmenter = StanfordSegmenter(
    path_to_sihan_corpora_dict="/home/linusp/stanford/segmenter/data/",
    path_to_model="/home/linusp/stanford/segmenter/data/pku.gz",
    path_to_dict="/home/linusp/stanford/segmenter/data/dict-chris6.ser.gz"
)
res = segmenter.segment(u"北海已成为中国对外开放中升起的一颗明星")
print type(res)
print res.encode('utf-8')

StanfordSegmenter 的初始化参数说明:

  • path_to_jar: 用来定位 stanford-segmenter.jar ,在设置了 CLASSPATH 的情况下,该参数可留空

    注: 其他所有 Stanford NLP 接口都有 path_to_jar 这个参数,同样在设置了环境变量的情况下可以留空,后面不再另加说明。

  • path_to_slf4j: 用来定位 slf4j-api.jar ,在设置了 CLASSPATH 或者 SLF4J 这个环境变量的情况下,该参数可留空
  • path_to_sihan_corpora_dict: 设定为 stanford-segmenter-2015-12-09.zip 解压后目录中的 data 目录,话说这个参数名真是让人摸不着头脑
  • path_to_model: 用来指定用于中文分词的模型,在 stanford-segmenter-2015-12-09 的 data 目录下,有两个可用模型 pkg.gzctb.gz

需要注意的是,使用 StanfordSegmenter 进行中文分词后,其返回结果并不是 list ,而是一个字符串,各个汉语词汇在其中被空格分隔开。

StanfordTokenizer 可以用来进行英文的分词,使用起来比较简单

# coding: utf-8
from nltk.tokenize import StanfordTokenizer

tokenizer = StanfordTokenizer()
sent = "Good muffins cost $3.88\nin New York.  Please buy me\ntwo of them.\nThanks."
print tokenizer.tokenize(sent)

使用 StanfordNERTagger 进行命名实体识别

所谓命名实体识别,是用来识别并标注文本中的人名、地名、组织机构名等单元,这些单元既是 "命名实体"。

# coding: utf-8
from nltk.tag import StanfordNERTagger

eng_tagger = StanfordNERTagger('english.all.3class.distsim.crf.ser.gz')
print eng_tagger.tag('Rami Eid is studying at Stony Brook University in NY'.split())

StanfordNERTagger 在初始化时需要指定所使用的模型,在 stanford-ner-2015-12-09.zip 解压后的 classifiers 目录中,有几个可用的英语 NER 模型:

/home/linusp/stanford/ner/classifiers/
├── english.all.3class.distsim.crf.ser.gz
├── english.all.3class.distsim.prop
├── english.conll.4class.distsim.crf.ser.gz
├── english.conll.4class.distsim.prop
├── english.muc.7class.distsim.crf.ser.gz
├── english.muc.7class.distsim.prop
├── example.serialized.ncc.ncc.ser.gz
└── example.serialized.ncc.prop

如果需要进行中文的命名实体识别,则可以在 Stanford Named Entity Recognizer 页面的 Models 一节找到中文模型的下载链接,下载得到 stanford-chinese-corenlp-2015-12-08-models.jar ,解压后将 edu/stanford/nlp/models/ner/ 目录下的 chinese.misc.distsim.crf.ser.gz 和 chinese.misc.distsim.prop 复制到模型目录下(stanford-ner-2015-12-09/classifiers)即可。

# coding: utf-8
from nltk.tag import StanfordNERTagger

chi_tagger = StanfordNERTagger('chinese.misc.distsim.crf.ser.gz')
sent = u'北海 已 成为 中国 对外开放 中 升起 的 一 颗 明星'
for word, tag in  chi_tagger.tag(sent.split()):
    print word.encode('utf-8'), tag

使用 StanfordPOSTagger 进行词性标注

所谓词性标注,是根据句子中的上下文信息,给句中每个词确定一个最为合适的词性标记,比如动词、名词、人称代词等。

和 StanfordNERTagger 一样,StanfordPOSTagger 需要的输入也是一个已经分好词的句子,下面是一个英文的词性标注实例:

from nltk.tag import StanfordPOSTagger

eng_tagger = StanfordPOSTagger('english-bidirectional-distsim.tagger')
print eng_tagger.tag('What is the airspeed of an unladen swallow ?'.split())

如果之前配置时下载的是 stanford-postagger-full-xxxx-xx-xx.zip ,在解压后,其中的 models 目录是包含有两个中文模型的,分别是 chinese-distsim.taggerchinese-nodistsim.tagger ,可以直接使用。

# coding: utf-8
from nltk.tag import StanfordPOSTagger

chi_tagger = StanfordPOSTagger('chinese-distsim.tagger')
sent = u'北海 已 成为 中国 对外开放 中 升起 的 一 颗 明星'
for _, word_and_tag in  chi_tagger.tag(sent.split()):
    word, tag = word_and_tag.split('#')
    print word.encode('utf-8'), tag

这个中文的词性标注输出的结果有点奇怪……

使用 StanfordParser 进行句法分析

句法分析在分析单个词的词性的基础上,尝试分析词与词之间的关系,并用这种关系来表示句子的结构。实际上,句法结构可以分为两种,一种是短语结构,另一种是依存结构。前者按句子顺序来提取句法结构,后者则按词与词之间的句法关系来提取句子结构。这里说的句法分析得到的是短语结构。

from nltk.parse.stanford import StanfordParser

eng_parser = StanfordParser(model_path=u'edu/stanford/nlp/models/lexparser/englishPCFG.ser.gz')
print list(eng_parser.parse("the quick brown fox jumps over the lazy dog".split()))

得到的结果是一个 list, 其中的元素是 Tree 类型的,在上面这个例子中,这个 list 的长度是 1 ,调用 Tree 的 draw 方法可以将句法树绘制出来。

eng_parse_tree.png

要进行中文的句法分析,只要指定好中文的模型就好,可用的中文模型有两个,分别是 'edu/stanford/nlp/models/lexparser/chinesePCFG.ser.gz' 和 'edu/stanford/nlp/models/lexparser/chineseFactored.ser.gz',依然拿 "_北海 已 成为 中国 对外开放 中 升起 的 一 颗 明星_" 这句话作为例子,得到的句法树如下所示。

chi_parse_tree.png

使用 StanfordDependencyParser 进行依存句法分析

见上一节,依存句法分析得到的是句子的依存结构。

from nltk.parse.stanford import StanfordDependencyParser

eng_parser = StanfordDependencyParser(model_path=u'edu/stanford/nlp/models/lexparser/englishPCFG.ser.gz')
res = list(eng_parser.parse("the quick brown fox jumps over the lazy dog".split()))
for row in res[0].triples():
    print row

绘制出来的依存句法结构如下图所示。

dep_parse_tree.png

中文的依存句法分析同理,在初始化时使用中文模型即可,不再赘述。

StanfordNeuralDependencyParser 的使用与 StanfordDependencyParser 一样,但是在本人的机器上执行非常耗时,即使是对一些简单句子,所以这里就不略过不讲了。

❌
❌