名师讲堂|突破性创新与颠覆性创新:使用BERT和 SBERT 模型计算专利文本相似度(一)

在研究突破性创新与颠覆性创新问题方面,目前一种比较流行的方式是使用 BERT 或者 SBERT 模型根据专利摘要文本进行相似度,进而构造突破性创新与颠覆性创新指标。BERT 或者 SBERT 模型通常是使用 Python 实现的,不过在之前的课程中 Python 内容涉及很少,所以今天的课程主要还是使用 R 语言调用编写好的 Python 函数。这样既避免了配置 Python 环境的头疼问题,又发挥了熟悉 R 语言的优势。

BERT(Bidirectional Encoder Representations from Transformers)和 SBERT(Sentence-BERT)是自然语言处理(NLP)中基于 Transformer 架构的两种重要模型。BERT 是通用的预训练模型,适合需要上下文理解的词级或句对任务,但直接用于句子相似度时效率低。SBERT 则是 BERT 的扩展,专为高效句子嵌入设计,适合大规模语义匹配任务,牺牲部分灵活性换取计算效率。

针对 BERT 和 SBERT 又有很多种预训练模型。

在 BERT 模型上,我在附件中准备了 bert-base-chinese 和 chinese_wwm_ext_pytorch 两种。bert-base-chinese 是由谷歌官方发布的原生中文 BERT 模型,基于简体中文语料训练;chinese_wwm_ext_pytorch 是由哈工大(HIT)和科大讯飞联合发布的中文全词掩码(Whole Word Masking, WWM)扩展模型。在谷歌 bert-base-chinese 的基础上,增加了更多中文语料(如社交媒体、论坛等)。

在 SBERT 模型上,我在附件中准备了 distiluse-base-multilingual-cased-v2 和 paraphrase-multilingual-MiniLM-L12-v2。paraphrase-multilingual-MiniLM-L12-v2 是 轻量级多语言模型,支持中文,速度快;distiluse-base-multilingual-cased-v2 是多语言蒸馏模型,效果较好。

除此之外,这些模型也可以自己使用语料库进行微调,不过这个就更复杂点,今天我们先来看现成模型的应用。

在附件里的 「数字知识流动如何促进区域协调发展——兼论经济增长和平衡发展双重目标」文献中就提到了使用 chinese_wwm_ext_pytorch 模型:

不过这个文献是使用该模型进行专利分类的。

「企业数字化转型与颠覆性技术创新——来自专利网络与SBERT模型的微观证据_黄先海」文献是使用微调后的 SBERT 模型计算专利文本相似度的。计算每个专利和其他所有专利的相似度是个计算量超级大的事情,所以这篇文献里面仅仅计算专利和其引用专利的相似度,这样大大减小了计算量(不过计算量依然非常大)。

在这篇文献中,作者定义专利的创新度为其与所有引用专利相似度平均值的倒数(取对数):

也就是和引用专利的相似度越低,创新度越高。

然后定义突破性技术创新的专利为:同年同一行业企业的专利中,创新度位于前 20% 的专利。重要影响力技术创新的专利为:同年同一行业企业的专利中,3 年内被引次数排名前 20% 的专利。

同时满足上述两个条件的专利即位具有颠覆性创新的。

在处理前仅保留发明专利并且删除金融和保险类企业。

使用 reticulate 安装和运行 Python

Python 使用中最头疼的就是安装和环境配置了。不过 R 语言的 reticulate 包可以很好的解决这个问题。为了避免库版本的冲突问题,这里我们使用虚拟环境来运行 Python 代码。

加载所需 R 包:

library(tidyverse)
library(reticulate)

创建虚拟环境:

# 安装 python3.10
conda_create(envname = "bert2", python_version = "3.10")

激活环境:

use_condaenv("bert2")

安装必要的库:

requirements.txt 文件里面存放了可能需要的 Python 库名称和版本,在 reticulate 中可以用下面的方式安装:

# 1. 读取requirements.txt文件
requirements <- readLines("requirements.txt")

# 2. 清理空行和注释
requirements <- grep("^[^#]", requirements, value = TRUE) # 移除注释行
requirements <- trimws(requirements) # 去除两端空格
requirements <- requirements[requirements != ""] # 移除空行

requirements

# 3. 安装所有依赖
conda_install(packages = requirements, envname = "bert2",
python_version = "3.10", pip = T,
pip_options = "--index-url https://pypi.tuna.tsinghua.edu.cn/simple")

下面三个 py 脚本是我编写的分别使用上述四个模型计算文本相似度的 Python 函数:

  • calculate_similarity_chinese_wwm_ext_pytorch.py
  • calculate_similarity_bert_base_chinese.py
  • calculate_similarity_SBERT.py

在 R 语言中使用也非常简单:

使用前记得把 py 脚本里面的模型路径改成自己电脑上的。

source_python("calculate_similarity_bert_base_chinese.py")
source_python("calculate_similarity_chinese_wwm_ext_pytorch.py")
source_python("calculate_similarity_SBERT.py")

例如 calculate_similarity_bert_base_chinese.py 文件里面的代码是:

# 创建虚拟环境:conda create -n bertpy python=3.10
# 激活虚拟环境:conda activate bertpy
# 安装特定版本的 Python 库:pip install -r requirements.txt
# 执行代码:python3 main.py

import torch
from transformers import BertTokenizer, BertModel
from scipy.spatial.distance import cosine

# 1. 加载预训练的 BERT 模型和分词器
model_name = "bert-base-chinese"
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertModel.from_pretrained(model_name)

# 2. 文本预处理
def preprocess(text):
inputs = tokenizer(text, return_tensors='pt', padding=True, truncation=True, max_length=512)
return inputs

# 3. 获取文本的嵌入表示
def get_embedding(text):
inputs = preprocess(text)
with torch.no_grad():
outputs = model(**inputs)
# 使用 CLS token 的隐藏状态作为文本的嵌入表示
embedding = outputs.last_hidden_state[:, 0, :].squeeze()
return embedding

# 4. 计算相似度
def calculate_similarity_bert_base_chinese(text1, text2):
embedding1 = get_embedding(text1)
embedding2 = get_embedding(text2)
similarity = 1 - cosine(embedding1, embedding2)
return similarity

示例文本:

text1 <- "我喜欢在 Stata 中编程"
text2 <- "Stata 是我最喜欢的编程软件"
text3 <- "我喜欢 Stata"
text4 <- "我不喜欢 Stata"

然后就可以把 Python 函数像 R 语言函数一样使用了:

calculate_similarity_bert_base_chinese(text1, text2)
#> [1] 0.8820695
calculate_similarity_chinese_wwm_ext_pytorch(text1, text3)
#> [1] 0.8379631
calculate_similarity_p(text1, text2)
#> [1] 0.8498675
calculate_similarity_d(text1, text4)
#> [1] 0.6805944

在判断喜欢和不喜欢的相似度的时候几个模型的表现就有差异了:

calculate_similarity_bert_base_chinese(text3, text4)
#> [1] 0.9762425
calculate_similarity_chinese_wwm_ext_pytorch(text3, text4)
#> [1] 0.9762425
calculate_similarity_p(text3, text4)
#> [1] 0.4283658
calculate_similarity_d(text3, text4)
#> [1] 0.8413995

不过需要注意,这些模型并不是为了进行情感分析,这里是用来计算文本相似度的。text3 和 text4 虽然语义相反,但是文本相似度是很高的。

计算专利文本的相似度

附件中的 2001年上市公司专利引用信息.rds 存放了 2001 年上市公司专利和其引用专利的标题和摘要等信息(仅保留发明专利):

read_rds("2001年上市公司专利引用信息.rds") -> df

df

#> # A tibble: 1,422 × 9
#> 股票代码 股票名称 newipzlid 标题 摘要 newipzlid2 标题2 摘要2 年份
#> <chr> <chr> <dbl> <chr> <chr> <dbl> <chr> <chr> <dbl>
#> 1 600028 中国石化 2001001259 生产低烯烃汽油和多产柴油… 一种生产… 199980468 一种制取… 一种制取… 2001
#> 2 600019 宝钢股份 2001003922 辊面堆焊用硬面合金管丝…… 本发明涉… 199817092 连铸辊修… 本发明涉… 2001
#> 3 000537 广宇发展 2001004017 镁基储氢合金材料及制备方… 本发明属… 199251691 镁基储氢… 本发明属… 2001
#> 4 000537 广宇发展 2001004017 镁基储氢合金材料及制备方… 本发明属… 2000118896 碳纳米管… 本发明涉… 2001
#> 5 000537 广宇发展 2001004017 镁基储氢合金材料及制备方… 本发明属… 2000103920 碳纳米管… 本发明涉… 2001
#> 6 000537 广宇发展 2001004017 镁基储氢合金材料及制备方… 本发明属… 199202422 镁基储氢… 本发明属… 2001
#> 7 600089 特变电工 2001004068 环式三相变压器及其生产方… 一种环式… 199451721 环式三相… 一种环式… 2001
#> 8 600089 特变电工 2001004068 环式三相变压器及其生产方… 一种环式… 199844765 一种三相… 本实用新… 2001
#> 9 600716 凤凰股份 2001004964 光致变色镜片的制造方法…… 本发明提… 199739276 抗紫外线… 抗紫外线… 2001
#> 10 603051 鹿山新材 2001004971 涂覆钢管专用胶粘剂…… 本发明提… 198611343 一种热熔… 本发明提… 2001
#> # ℹ 1,412 more rows

变量名称不带 2 的表示原专利,带 2 的表示被引专利。这份数据是根据之前分享的这个数据处理得到的:

1985~2024 年全部专利引用与被引用详细信息:https://rstata.duanshu.com/#/brief/course/225ad0b59a9945e1831d2e8b96ca1001

计算两个专利文本相似度非常简单:

calculate_similarity_d(df$摘要[1], df$摘要2[1])
#> [1] 0.7340376

如果要计算的数量很少,可以直接用 purrr 包的向量化编程:

df %>%
slice(1:5) %>%
mutate(similarity = map2_dbl(摘要, 摘要2, calculate_similarity_d)) %>%
select(股票代码, contains("new"), contains("标题"), similarity)

#> # A tibble: 5 × 6
#> 股票代码 newipzlid newipzlid2 标题 标题2 similarity
#> <chr> <dbl> <dbl> <chr> <chr> <dbl>
#> 1 600028 2001001259 199980468 生产低烯烃汽油和多产柴油的催化转化方法…… 一种制取… 0.734
#> 2 600019 2001003922 199817092 辊面堆焊用硬面合金管丝 连铸辊修… 0.732
#> 3 000537 2001004017 199251691 镁基储氢合金材料及制备方法及其应用…… 镁基储氢… 0.455
#> 4 000537 2001004017 2000118896 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.417
#> 5 000537 2001004017 2000103920 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.408

不过当运算量很大的时候这么做就不合适了,因为代码一旦出错就前功尽弃了。这个时候应该拆分运行,然后及时保存已经成功运行的结果。

单线程处理:

lapply(1:5, function(x){
df %>%
slice(x) %>%
mutate(similarity = map2_dbl(摘要, 摘要2, calculate_similarity_d)) %>%
select(股票代码, contains("new"), contains("标题"), similarity) %>%
write_csv("res.csv", append = T, quote = "needed")
}) -> tempres

read_csv("res.csv", col_names = F) %>%
set_names("股票代码", "newipzlid", "newipzlid2", "标题", "标题1", "similarity")

#> # A tibble: 20 × 6
#> 股票代码 newipzlid newipzlid2 标题 标题1 similarity
#> <chr> <dbl> <dbl> <chr> <chr> <dbl>
#> 1 600028 2001001259 199980468 生产低烯烃汽油和多产柴油的催化转化方法…… 一种制取… 0.734
#> 2 600019 2001003922 199817092 辊面堆焊用硬面合金管丝 连铸辊修… 0.732
#> 3 000537 2001004017 199251691 镁基储氢合金材料及制备方法及其应用…… 镁基储氢… 0.455
#> 4 000537 2001004017 2000118896 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.417
#> 5 000537 2001004017 2000103920 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.408
#> 6 600028 2001001259 199980468 生产低烯烃汽油和多产柴油的催化转化方法…… 一种制取… 0.734
#> 7 600019 2001003922 199817092 辊面堆焊用硬面合金管丝 连铸辊修… 0.732
#> 8 000537 2001004017 199251691 镁基储氢合金材料及制备方法及其应用…… 镁基储氢… 0.455
#> 9 000537 2001004017 2000118896 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.417
#> 10 000537 2001004017 2000103920 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.408
#> 11 600028 2001001259 199980468 生产低烯烃汽油和多产柴油的催化转化方法…… 一种制取… 0.734
#> 12 600019 2001003922 199817092 辊面堆焊用硬面合金管丝 连铸辊修… 0.732
#> 13 000537 2001004017 199251691 镁基储氢合金材料及制备方法及其应用…… 镁基储氢… 0.455
#> 14 000537 2001004017 2000118896 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.417
#> 15 000537 2001004017 2000103920 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.408
#> 16 600028 2001001259 199980468 生产低烯烃汽油和多产柴油的催化转化方法…… 一种制取… 0.734
#> 17 600019 2001003922 199817092 辊面堆焊用硬面合金管丝 连铸辊修… 0.732
#> 18 000537 2001004017 199251691 镁基储氢合金材料及制备方法及其应用…… 镁基储氢… 0.455
#> 19 000537 2001004017 2000118896 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.417
#> 20 000537 2001004017 2000103920 镁基储氢合金材料及制备方法及其应用…… 碳纳米管… 0.408

使用多线程效率更高。这里多线程的方案是先把数据拆分成 n 份,然后每个线程处理一个部分:

dir.create("df_part")
n_parts <- 16

# 仅仅选择 160 个演示
df %>%
slice(1:160) %>%
select(股票代码, newipzlid, newipzlid2, contains("摘要")) %>%
mutate(part = rep(1:n_parts, each = ceiling(n() / n_parts), length.out = n())) %>%
group_split(part) %>%
walk2(., paste0("df_part/", 1:n_parts, ".rds"), ~ readr::write_rds(.x, .y))

创建 16 个进程:

library(parallel)
makeCluster(16) -> cl
clusterEvalQ(cl, ({
library(tidyverse)
library(reticulate)
use_condaenv("bert2")
source_python("calculate_similarity_SBERT.py")
})) -> tempres

这里的 16 是我的电脑上的物理核心数:

detectCores()
#> [1] 16

理论上可以设置超过物理核心数的进程数(如 32),但实际效果取决于任务类型和系统调度。例如数据很大的时候,每个进程处理的数据大小总和超过了内存就会影响多线程的运行速度。

然后就可以分别在 16 个 R 进程里面读取相应部分的数据然后计算再保存结果了:

dir.create("res")
parLapply(cl, fs::dir_ls("df_part"), function(x){
readr::read_rds(x) -> dftemp
for (i in 1:nrow(dftemp)) {
dftemp %>%
slice(i) %>%
mutate(similarity = map2_dbl(摘要, 摘要2, calculate_similarity_p)) %>%
select(-contains("摘要")) %>%
write_csv(paste0("res/", Sys.getpid(), ".csv"), append = T, quote = "needed")
}
}) -> tempres

Sys.getpid() 返回的是对应进程的编号。

合并所有的结果:

lapply(fs::dir_ls("res"), read_csv, col_names = F, col_types = "d") %>%
bind_rows() -> dfres

dfres %>%
set_names("股票代码", "newipzlid", "newipzlid2", "parts", "similarity") %>%
select(-parts)
#> # A tibble: 640 × 4
#> 股票代码 newipzlid newipzlid2 similarity
#> <dbl> <dbl> <dbl> <dbl>
#> 1 600028 2001001259 199980468 0.724
#> 2 600019 2001003922 199817092 0.773
#> 3 537 2001004017 199251691 0.563
#> 4 537 2001004017 2000118896 0.400
#> 5 537 2001004017 2000103920 0.439
#> 6 537 2001004017 199202422 0.560
#> 7 600089 2001004068 199451721 0.930
#> 8 600089 2001004068 199844765 0.828
#> 9 600716 2001004964 199739276 0.802
#> 10 603051 2001004971 198611343 0.722
#> # ℹ 630 more rows

这样我们就快速计算了 160 个专利对的文本相似度。不过计算全部(数百万对)的还是很困难,大家要是计算的时候需要使用更好的设备以及预留充足的时间。

下次课我们将继续讲解突破性技术创新、重要影响力技术创新以及颠覆性技术创新指标的测算。

点击这里跳转到 RStata 短书平台获取附件:名师讲堂|突破性创新与颠覆性创新:使用BERT和 SBERT 模型计算专利文本相似度(一)

评论